Skip to content

CLI: Build file URLs with as_uri instead of by hand - #2013

Merged
ikelos merged 1 commit into
volatilityfoundation:developfrom
Hmkz0x00:fix/cli-file-url-construction
Aug 13, 2026
Merged

CLI: Build file URLs with as_uri instead of by hand#2013
ikelos merged 1 commit into
volatilityfoundation:developfrom
Hmkz0x00:fix/cli-file-url-construction

Conversation

@Hmkz0x00

@Hmkz0x00 Hmkz0x00 commented Aug 3, 2026

Copy link
Copy Markdown

Summary

populate_config builds the file URL for every URIRequirement by hand:

value = f"file://{request.pathname2url(os.path.abspath(value))}"

On Windows pathname2url already returns a leading ///, so the two slashes
in the f-string are added on top of three and the result is
file://///C:/.... That parses as a UNC path with an empty host, and
urlopen fails before the file is ever touched.

Every option built on URIRequirement gets the broken value stored against
it, not just the image: --single-location, --yara-file,
--yara-compiled-file, --strings-file, --isf and volshell's --script.
I have reproduced the resulting failure end to end on --single-location,
--yara-file and --script.

Reproducing

No memory image needed, any existing file will do:

$ vol.py --save-config config.json isfinfo.IsfInfo --isf ./some-file.json
$ python -c "import json; print(json.load(open('config.json'))['isf'])"
file://///E:/Codes/volatility3/some-file.json

With an image it surfaces as a misleading error, because the advice printed
underneath asks the user to check that the file exists and is readable when it
is already both:

$ vol.py --single-location memory.dmp windows.info
WARNING  volatility3.framework.plugins: Automagic exception occurred:
urllib.error.URLError: <urlopen error [WinError 161] The specified path is
invalid: '\\\\\\\\E:\Codes\volatility3\memory.dmp'>
Unable to validate the plugin requirements: [...]

-f on the same file works, because it takes a different route:
CommandLine.run sends it to URIRequirement.location_from_file, which joins
the scheme with urljoin and gets it right. So the two options that --help
describes as equivalent behave differently, and only the shorthand works.

The fix

value = pathlib.Path(os.path.abspath(value)).as_uri()

as_uri is the standard library's own answer to this, and it is the only one
of the three places in this codebase that build file URLs which was doing it
by hand. The other two are already correct:

  • URIRequirement.location_from_file uses urljoin("file:", ...)
  • volshell.generic.run_script uses "file:" + ... with a single colon

os.path.abspath is kept so that the path normalisation does not change.

Why as_uri and not just dropping the two slashes

Deleting the // would be a smaller diff, but it changes the output on Linux
from file:///home/user/x.dmp to file:/home/user/x.dmp. Both are openable,
but the second is a visible change on the platform CI actually runs, and it
ends up in saved configs.

as_uri is byte-identical to the current output on Linux for every Python
version the project supports, so on that platform this commit changes nothing
at all:

path current (Linux, <=3.13) as_uri
/home/user/mem.dmp file:///home/user/mem.dmp same
/home/user/my images/mem dump.raw file:///home/user/my%20images/mem%20dump.raw same
/home/josé/café.dmp file:///home/jos%C3%A9/caf%C3%A9.dmp same

On Windows it changes as intended, and it also fixes UNC paths, which are
common enough when images live on a share:

path current as_uri
C:\a\b.dmp file://///C:/a/b.dmp file:///C:/a/b.dmp
\\server\share\mem.dmp file:////server/share/mem.dmp file://server/share/mem.dmp

(the UNC row is 3.13 and 3.14; 3.12 puts six slashes there instead of four)

I ran those against real 3.12, 3.13 and 3.14 interpreters rather than
assuming, because pathname2url was rewritten in 3.14. For 3.8, the version
CI uses, I read _PosixFlavour.make_uri instead, which is
'file://' + quote_from_bytes(bytes(path)); the only thing that differs from
the current quote(pathname) is the quoting call, and those agree on every
path I tried, including spaces, accents and reserved characters.

A note on Python 3.14

That rewrite makes the current code wrong on Linux too. pathname2url is no
longer quote() on POSIX; it now returns a leading /// for absolute paths
the same way the Windows version always did, so the current code produces
file://///home/user/mem.dmp there as well. Leading slashes collapse on
POSIX, so it still opens, but the stored URL is no longer canonical. as_uri
gives the same answer on 3.14 as it does on 3.8.

Effect on the test suite

test_volatility.runvol_plugin passes the image path straight to
--single-location, so on Windows the whole image-backed suite currently
fails before any plugin runs. Reverting only this commit and rerunning the
same three tests:

before: 3 failed
after:  3 passed

The full suite against win-10_19041-2025_03.dmp and
win-xp-laptop-2005-06-25.img is green with the change, which is the first
time I have been able to run it here without hand editing the harness:

windows.py, vol.py           63 passed
windows.py, volshell.py       2 passed
test/test_cli.py              3 passed

I mentioned this in passing on #2012 as something I had worked around
locally with a hand written file:// URL. This is the cause.

To check the other options rather than assume they follow, I passed the image
as an already correct file:/// URL so that it could not be the thing
failing, and ran the rest against win-xp-laptop-2005-06-25.img. Both fail
on develop and work with this change:

$ vol.py --single-location file:///.../win-xp.img yarascan.YaraScan --yara-file rules.yar
urllib.error.URLError: <urlopen error [WinError 161] The specified path is
invalid: '\\\\\\\\C:\...\rules.yar'>

$ volshell.py --single-location file:///.../win-xp.img --script script.py
urllib.error.URLError: <urlopen error [WinError 161] The specified path is
invalid: '\\\\\\\\C:\...\script.py'>

--script is worth calling out because run_script builds URLs correctly
itself, but never gets the chance: the value already has a scheme by the time
it arrives, so it is passed straight through.

Tests

Adds test/test_cli.py covering the three branches of the code being
touched: a path is converted to a URL that urlopen can actually read back,
a value that already has a scheme is left alone, and a missing path still
raises FileNotFoundError.

The first of those fails on develop and passes with this change on Windows.
On Linux it passes either way for Python <=3.13, since there is nothing wrong
to catch there yet, but it will catch the 3.14 behaviour described above.

Worth flagging that .github/workflows/test.yaml invokes pytest against
windows.py and linux.py by path, so a new file under test/ is not
collected by CI as things stand. Happy to wire it in, or to move the tests
somewhere they will run, if you would prefer either.

ruff format, ruff check and test/volatility3_code_analysis.py are clean.

Possible follow-up

The deeper issue is that -f and --single-location build the same value by
two different code paths. populate_config could call
URIRequirement.location_from_file and have one implementation instead of
two. I have not done it here because that function raises ValueError where
the CLI raises FileNotFoundError, so it is a user-visible change to error
handling rather than a bug fix. Happy to do it separately if you want it.

populate_config prefixed "file://" onto the result of pathname2url, which
already returns a leading "///" on Windows. Three slashes plus two gives
file://///C:/..., an empty authority that urlopen reads as a UNC path, so
every URIRequirement failed there before the file was ever opened. That
covers --single-location, --yara-file, --yara-compiled-file, --strings-file,
--isf and volshell's --script.

pathlib's as_uri produces the same string as the current code on POSIX for
every supported Python version, and the correct one on Windows, including
for UNC paths. It also sidesteps the Python 3.14 rewrite of pathname2url,
which gives POSIX the same leading "///" that Windows always returned.

The two other places in the codebase that build file URLs,
URIRequirement.location_from_file and volshell's run_script, were already
correct; this was the only one assembling the scheme by hand.
@ikelos

ikelos commented Aug 13, 2026

Copy link
Copy Markdown
Member

Thanks very much for spotting and fixing this! I worry slightly that as_uri has apparently been deprecated for 3.14, and will be removed in 3.19. How does your solution respond to the deprecation?

@Hmkz0x00

Hmkz0x00 commented Aug 13, 2026

Copy link
Copy Markdown
Author

Thanks for taking a look!

Good thing to check, but I think the deprecation is on a different method. In 3.14 it is
pathlib.PurePath.as_uri() that is deprecated (removal scheduled for 3.19).
pathlib.Path.as_uri() is not — Path overrides it with a new implementation, and the
deprecation message on PurePath actually says "Use pathlib.Path.as_uri()".

The fix calls pathlib.Path(os.path.abspath(value)).as_uri(), so it lands on the Path
version. On 3.14.5:

>>> pathlib.PureWindowsPath("C:/temp/x.img").as_uri()
DeprecationWarning: pathlib.PurePath.as_uri() is deprecated and scheduled for removal in
Python 3.19. Use pathlib.Path.as_uri().
'file:///C:/temp/x.img'

>>> pathlib.Path("C:/temp/x.img").as_uri()      # no warning
'file:///C:/temp/x.img'

>>> pathlib.Path.as_uri is pathlib.PurePath.as_uri
False

Running the call under -W error::DeprecationWarning is clean on 3.12.0, 3.13.5 and 3.14.5.

Path.as_uri() on 3.14 is now just pathname2url(str(self), add_scheme=True), i.e. it sits
on top of the newly public API rather than being replaced by it, so it looks like the one
that is meant to survive. There are also three existing Path.as_uri() calls in the tree
already (isfinfo.py, framework/symbols/intermed.py, development/banner_server.py), so
this is not adding a new dependency on the API.

So I do not think anything needs changing here. If you would like a guard against a future
surprise, I am happy to add an assertion to the new tests in test_cli.py that the call
raises no DeprecationWarning, so CI would fail loudly rather than quietly if that ever
changes.

@ikelos

ikelos commented Aug 13, 2026

Copy link
Copy Markdown
Member

No thanks, that all looks good, thanks for double checking it! 5:)

@ikelos
ikelos merged commit d5c88da into volatilityfoundation:develop Aug 13, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants