You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
PurePath.match() incorrectly intercepts and breaks absolute shebangs #348
Describe the bug
In scriptutils.py, _find_shebang_command uses pathlib.PurePath.match(), which performs right-aligned suffix matching. This inadvertently intercepts explicit absolute paths and feeds them into bare-name routing rules.
When installing certain packages via uv (specifically older packages using setup.py that drop .py scripts with absolute shebangs rather than .exe entry-point shims), or when a script utilizes its own local python.exe (such as in a venv), the absolute shebang is falsely intercepted. This prevents the path from reaching _find_on_path(). Instead:
Paths ending in python.exe trigger the is_default check, hijacking execution to the global environment.
Paths ending in python*.exe (e.g., python_test.exe) bypass the default check and fall through to the wildcard fallback, which slices the string ([6:-4]) and crashes while looking for a non-existent tag.
To Reproduce
Steps to reproduce the behavior:
Create or install a script utilizing an isolated local environment (e.g., an older setup.py package installed via uv that relies on a shebang, or a standard venv).
Set the shebang to the absolute path of that isolated executable (e.g., #!C:\path\to\env\Scripts\python.exe).
NOTE: This is uv's default behavior for older packages.
Execute the script via pymanager (py.exe example_script.py) or directly if py files are already associated with it.
Observe that the script ignores the absolute path shebang and falls back to the global default Python environment.
(Optional) Rename the executable to python_test.exe, update the shebang, and observe a crash during tag extraction.
Expected behavior _find_shebang_command should only match bare alias names. Explicit paths containing directory separators should safely raise LookupError so _parse_shebang can natively delegate them to _find_on_path().
Additional context
As requested by @zooba in #345 this issue tracks the routing behavior observed with absolute shebangs prior to merging the fix. This alias-matching logic also contributes to the behaviors reported in #91 and #307.
Thanks for writing this up, it makes a lot more sense now and covers a legacy case that I wasn't aware of.
We intentionally are treating shebangs as a compatibility for scripts written on POSIX machines, and deliberately not making py a generic executable launcher. So it should always prioritise properly installed and registered Python runtimes. Otherwise, py becomes a vector for malicious actors to launch arbitrary processes via a (hopefully) trusted executable, which we want to avoid.
So treating any path ending in python as generic is by design, and if you want a different design then you need a tool other than the Python install manager. You can find this stated in the previous issue you linked.
That said, when the name doesn't end in a known tag - such as _test - we shouldn't crash. Can you show the output of this crash? Failing to find a matching runtime isn't a crash, given the intended behaviour, but I think it's reasonable enough for this case to fall through to the "launch arbitrary executable" behaviour (which shows a warning and can/should be disabled).
@zooba, thanks for taking the time to review this and for providing the context on the design goals! I completely agree that py shouldn’t be a silent vector for malicious actors, and prioritizing properly installed and registered Python runtimes makes perfect sense.
To answer your question first, here is the exact crash output you requested. To demonstrate this, I am using an isolated environment created by uv, which drops its own unregistered executable at C:\Users\mcexit\AppData\Roaming\uv\tools\exampletool\Scripts\python.exe.
If I rename that specific isolated executable to python_test.exe and update the script's absolute shebang to match (#!C:\Users\mcexit\AppData\Roaming\uv\tools\exampletool\Scripts\python_test.exe), here is what happens:
> example.py --help[ERROR] No runtime installed that matches _test. Try running "py install _test".
Because the string-slicing fallback (sh_cmd.name[6:-4]) assumes it's parsing a bare tag, it intercepts this explicit absolute path and crashes while looking for _test, which prevents it from falling through to the "launch arbitrary executable" behavior you mentioned.
Regarding the routing logic, I want to clarify that this PR does not impact how pymanager handles registered global runtimes, nor does it aim to turn py into a generic executable launcher. It strictly addresses how pymanager handles explicit Windows absolute paths that point to local, unregistered virtual environments (like standard venvs or uv tools).
Right now, because PurePath.match() evaluates from the right side of the string, an absolute path like #!C:\Users\mcexit\AppData\Roaming\uv\tools\exampletool\Scripts\python.exe evaluates to True when matching against the alias "python.exe". pymanager then intercepts the explicit path, discards the directory routing, and hijacks execution to the global Python environment (causing ModuleNotFoundErrors for isolated scripts).
An interesting side effect of this alias-matching is that the current logic actually allows arbitrary executables to run safely through the fallback, while intercepting legitimate absolute paths to local Python environments. For example, if I rename that same python.exe executable to malware.exe and update the absolute shebang (#!C:\Users\mcexit\AppData\Roaming\uv\tools\exampletool\Scripts\malware.exe), it bypasses the PurePath.match() alias checks, correctly delegates to _find_on_path(), and executes with your intended warnings:
> example.py --help[WARNING] A shebang 'C:\Users\mcexit\AppData\Roaming\uv\tools\exampletool\Scripts\malware.exe' was found but does not match any supported template (e.g. '/usr/bin/python'), so it will be treated as an arbitrary command.[WARNING] To prevent execution of programs that are not Python runtimes, set 'shebang_can_run_anything' to 'false' in your configuration file.Usage: example.py [input file list] [options]...
So currently, an unrecognized executable correctly falls through to your intended security warning block (governed by shebang_can_run_anything), but a perfectly valid absolute path to a local, isolated python.exe gets hijacked or crashes.
This PR simply adds an is_name_only check so that alias extraction and wildcard string-slicing only apply to bare names like python3.14.exe or /usr/bin/env python. Explicit absolute paths containing directory separators will safely bypass the alias check, raise LookupError, and cleanly fall through to the native fallback logic (_parse_shebang), where they are properly subjected to your existing security checks before execution.
Finally, regarding using another tool: since the Python 3.14 docs officially state that the legacy Python Launcher (py.exe) is deprecated and superseded by Python Install Manager (pymanager), users won't have another supported tool to fall back on for running standard venv and uv scripts natively on Windows. Merging this ensures pymanager correctly supports absolute paths to local environments while fully preserving your POSIX alias resolution and security goals.
Let me know what you think! I'm happy to adjust the PR if you have a different approach in mind.
a perfectly valid absolute path to a local, isolated python.exe
This is the part that's outside of the design - shebangs are for selecting a registered interpreter, not an isolated one, no matter how valid the path is. The fallback to launch arbitrary paths is entirely to satisfy people transitioning (hence the warning and kill switch), and wasn't supported in the initial design at all.
Basically, we don't want to launch any path that wasn't previously provided by the machine owner/user. That does argue for "compare the full path against all registered runtimes and allow it if it matches one", which we don't currently do (and perhaps we should), but it doesn't argue for bypassing the registrations (and then we get the same argument about /usr/bin/python potentially being a valid path on Windows, so we "shouldn't" interpret it as a template instead, and so we currently draw the line at "templates first, optional fallback if the template fails").
So things I think we can do:
if the name matched python*.exe but the captured tag can't be found, fall back to treating it as a regular path (so fix the _test bug). Obviously if there is no tag, the default runtime will be chosen unconditionally, so this doesn't affect the "full path to python.exe" case
add a new config section as described in shebang backwards compatibility #307 that can map a shebang template (the argv[0]) to either a -V: option or a full executable path, where the latter is subject to shebang_can_run_anything and prints the same warning
treat all alias and executable paths as templates for properly installed runtimes (at highest priority, but then launches as if given the -V tag)
Once that's done, if you want a shebang to launch an arbitrary runtime, then you can put the path into your config against itself and allow arbitrary paths and it'll run. If you want to avoid the warning, follow PEP 514 to register it properly (or activate the venv, and then it's automatically detected as the default runtime - launching unactivated venvs isn't supported, though it usually works).
Activity
zooba commentedon May 25, 2026
Thanks for writing this up, it makes a lot more sense now and covers a legacy case that I wasn't aware of.
We intentionally are treating shebangs as a compatibility for scripts written on POSIX machines, and deliberately not making
pya generic executable launcher. So it should always prioritise properly installed and registered Python runtimes. Otherwise,pybecomes a vector for malicious actors to launch arbitrary processes via a (hopefully) trusted executable, which we want to avoid.So treating any path ending in
pythonas generic is by design, and if you want a different design then you need a tool other than the Python install manager. You can find this stated in the previous issue you linked.That said, when the name doesn't end in a known tag - such as
_test- we shouldn't crash. Can you show the output of this crash? Failing to find a matching runtime isn't a crash, given the intended behaviour, but I think it's reasonable enough for this case to fall through to the "launch arbitrary executable" behaviour (which shows a warning and can/should be disabled).mcexit commentedon May 26, 2026
@zooba, thanks for taking the time to review this and for providing the context on the design goals! I completely agree that
pyshouldn’t be a silent vector for malicious actors, and prioritizing properly installed and registered Python runtimes makes perfect sense.To answer your question first, here is the exact crash output you requested. To demonstrate this, I am using an isolated environment created by
uv, which drops its own unregistered executable atC:\Users\mcexit\AppData\Roaming\uv\tools\exampletool\Scripts\python.exe.If I rename that specific isolated executable to
python_test.exeand update the script's absolute shebang to match (#!C:\Users\mcexit\AppData\Roaming\uv\tools\exampletool\Scripts\python_test.exe), here is what happens:Because the string-slicing fallback (
sh_cmd.name[6:-4]) assumes it's parsing a bare tag, it intercepts this explicit absolute path and crashes while looking for_test, which prevents it from falling through to the "launch arbitrary executable" behavior you mentioned.Regarding the routing logic, I want to clarify that this PR does not impact how
pymanagerhandles registered global runtimes, nor does it aim to turnpyinto a generic executable launcher. It strictly addresses howpymanagerhandles explicit Windows absolute paths that point to local, unregistered virtual environments (like standardvenvs oruvtools).Right now, because
PurePath.match()evaluates from the right side of the string, an absolute path like#!C:\Users\mcexit\AppData\Roaming\uv\tools\exampletool\Scripts\python.exeevaluates toTruewhen matching against the alias"python.exe".pymanagerthen intercepts the explicit path, discards the directory routing, and hijacks execution to the global Python environment (causingModuleNotFoundErrors for isolated scripts).An interesting side effect of this alias-matching is that the current logic actually allows arbitrary executables to run safely through the fallback, while intercepting legitimate absolute paths to local Python environments. For example, if I rename that same
python.exeexecutable tomalware.exeand update the absolute shebang (#!C:\Users\mcexit\AppData\Roaming\uv\tools\exampletool\Scripts\malware.exe), it bypasses thePurePath.match()alias checks, correctly delegates to_find_on_path(), and executes with your intended warnings:So currently, an unrecognized executable correctly falls through to your intended security warning block (governed by
shebang_can_run_anything), but a perfectly valid absolute path to a local, isolatedpython.exegets hijacked or crashes.This PR simply adds an
is_name_onlycheck so that alias extraction and wildcard string-slicing only apply to bare names likepython3.14.exeor/usr/bin/env python. Explicit absolute paths containing directory separators will safely bypass the alias check, raiseLookupError, and cleanly fall through to the native fallback logic (_parse_shebang), where they are properly subjected to your existing security checks before execution.Finally, regarding using another tool: since the Python 3.14 docs officially state that the legacy Python Launcher (
py.exe) is deprecated and superseded by Python Install Manager (pymanager), users won't have another supported tool to fall back on for running standardvenvanduvscripts natively on Windows. Merging this ensurespymanagercorrectly supports absolute paths to local environments while fully preserving your POSIX alias resolution and security goals.Let me know what you think! I'm happy to adjust the PR if you have a different approach in mind.
zooba commentedon May 26, 2026
This is the part that's outside of the design - shebangs are for selecting a registered interpreter, not an isolated one, no matter how valid the path is. The fallback to launch arbitrary paths is entirely to satisfy people transitioning (hence the warning and kill switch), and wasn't supported in the initial design at all.
Basically, we don't want to launch any path that wasn't previously provided by the machine owner/user. That does argue for "compare the full path against all registered runtimes and allow it if it matches one", which we don't currently do (and perhaps we should), but it doesn't argue for bypassing the registrations (and then we get the same argument about
/usr/bin/pythonpotentially being a valid path on Windows, so we "shouldn't" interpret it as a template instead, and so we currently draw the line at "templates first, optional fallback if the template fails").So things I think we can do:
python*.exebut the captured tag can't be found, fall back to treating it as a regular path (so fix the_testbug). Obviously if there is no tag, the default runtime will be chosen unconditionally, so this doesn't affect the "full path topython.exe" case-V:option or a full executable path, where the latter is subject toshebang_can_run_anythingand prints the same warning-Vtag)Once that's done, if you want a shebang to launch an arbitrary runtime, then you can put the path into your config against itself and allow arbitrary paths and it'll run. If you want to avoid the warning, follow PEP 514 to register it properly (or activate the venv, and then it's automatically detected as the default runtime - launching unactivated venvs isn't supported, though it usually works).
Adds shebang_templates configuration for overriding shebangs. (python…