https://github.com/python/cpython/commit/40edefa348a06b25b9a794b0670b403d6c935071
commit: 40edefa348a06b25b9a794b0670b403d6c935071
branch: main
author: pat <[email protected]>
committer: zooba <[email protected]>
date: 2026-09-23T16:31:46+01:00
summary:
gh-119646: Include subprocess path in OSError on Windows (GH-157713)
files:
A Misc/NEWS.d/next/Library/2026-09-17-17-30-00.gh-issue-119646.kP8nQm.rst
M Lib/subprocess.py
M Lib/test/test_subprocess.py
diff --git a/Lib/subprocess.py b/Lib/subprocess.py
index d38cc756ec479f..466f2c10a2c3d4 100644
--- a/Lib/subprocess.py
+++ b/Lib/subprocess.py
@@ -1629,21 +1629,31 @@ def _execute_child(self, args, executable, preexec_fn,
close_fds,
assert not pass_fds, "pass_fds not supported on Windows."
if isinstance(args, str):
- pass
+ # Filename is the program only. Later arguments can
+ # hold secrets. A leading quote ends at the next quote.
+ # Otherwise stop at the first space.
+ if args[:1] == '"':
+ end = args.find('"', 1)
+ orig_filename = args[1:end] if end != -1 else args
+ else:
+ orig_filename = args.split(' ', 1)[0]
elif isinstance(args, bytes):
if shell:
raise TypeError('bytes args is not allowed on Windows')
+ orig_filename = os.fsdecode(args)
args = list2cmdline([args])
elif isinstance(args, os.PathLike):
if shell:
raise TypeError('path-like args is not allowed when '
'shell is true')
+ orig_filename = os.fsdecode(args)
args = list2cmdline([args])
else:
+ args = list(args)
+ orig_filename = os.fsdecode(args[0]) if args else None
args = list2cmdline(args)
-
if executable is not None:
- executable = os.fsdecode(executable)
+ orig_filename = executable = os.fsdecode(executable)
# Process startup details
if startupinfo is None:
@@ -1725,6 +1735,19 @@ def _execute_child(self, args, executable, preexec_fn,
close_fds,
env,
cwd,
startupinfo)
+ except OSError as e:
+ # gh-119646: POSIX already puts the attempted path on
+ # OSError.filename. Windows CreateProcess did not, so
+ # failures (missing exe, WSL paths, invalid cwd) were
+ # reported without naming the command.
+ if e.filename is None:
+ # ERROR_DIRECTORY (267): CreateProcess rejected cwd.
+ if cwd is not None and e.winerror == 267:
+ name = cwd
+ else:
+ name = orig_filename
+ raise type(e)(e.errno, e.strerror, name, e.winerror) from
None
+ raise
finally:
# Child is launched. Close the parent's copy of those pipe
# handles that only the child should have open. You need
diff --git a/Lib/test/test_subprocess.py b/Lib/test/test_subprocess.py
index fc94b9a972828c..dedd3cc0e9e867 100644
--- a/Lib/test/test_subprocess.py
+++ b/Lib/test/test_subprocess.py
@@ -1797,13 +1797,28 @@ def test_failed_child_execute_fd_leak(self):
fds_after_exception = os.listdir(fd_directory)
self.assertEqual(fds_before_popen, fds_after_exception)
- @unittest.skipIf(mswindows, "behavior currently not supported on Windows")
def test_file_not_found_includes_filename(self):
+ missing = (r'C:\opt\nonexistent_binary' if mswindows
+ else '/opt/nonexistent_binary')
with self.assertRaises(FileNotFoundError) as c:
- subprocess.call(['/opt/nonexistent_binary', 'with', 'some',
'args'])
- self.assertEqual(c.exception.filename, '/opt/nonexistent_binary')
+ subprocess.call([missing, 'with', 'some', 'args'])
+ self.assertEqual(c.exception.filename, missing)
+
+ def test_args_filter_iterable(self):
+ # gh-119646: Windows used to index args[0] before list2cmdline.
+ # test_faulthandler.test_sys_xoptions passes a filter() object.
+ args = filter(None, (sys.executable, "-c", "import sys; sys.exit(17)"))
+ self.assertEqual(subprocess.call(args), 17)
+
+ def test_file_not_found_includes_filename_from_iterable(self):
+ missing = (r'C:\opt\nonexistent_binary' if mswindows
+ else '/opt/nonexistent_binary')
+ args = filter(None, (missing, "with", "some", "args"))
+ with self.assertRaises(FileNotFoundError) as c:
+ subprocess.call(args)
+ self.assertEqual(c.exception.filename, missing)
- @unittest.skipIf(mswindows, "behavior currently not supported on Windows")
+ @unittest.skipIf(mswindows, "Windows reports NotADirectoryError (WinError
267)")
def test_file_not_found_with_bad_cwd(self):
with self.assertRaises(FileNotFoundError) as c:
subprocess.Popen(['exit', '0'], cwd='/some/nonexistent/directory')
@@ -3718,6 +3733,34 @@ def test_vfork_used_when_expected(self):
@unittest.skipUnless(mswindows, "Windows specific tests")
class Win32ProcessTestCase(BaseTestCase):
+ def test_createprocess_bad_cwd_includes_filename(self):
+ # gh-119646: invalid cwd should appear on OSError.filename.
+ missing_cwd = r'C:\some\nonexistent\directory'
+ with self.assertRaises(OSError) as c:
+ subprocess.Popen([sys.executable, '-c', 'pass'], cwd=missing_cwd)
+ self.assertEqual(c.exception.filename, missing_cwd)
+ self.assertEqual(c.exception.winerror, 267)
+
+ def test_command_string_filename_omits_later_args(self):
+ # gh-119646: a command-line string must not put later arguments
+ # on OSError.filename. Those arguments can hold secrets.
+ missing = r'C:\opt\nonexistent_binary'
+ secret = 'NOT-A-REAL-SECRET'
+ quoted = r'C:\Program Files\nonexistent_binary'
+ cases = [
+ (missing, missing),
+ (f'{missing} --token {secret}', missing),
+ (f'"{missing}" --token {secret}', missing),
+ (f'"{quoted}" --token {secret}', quoted),
+ ]
+ for command, expected in cases:
+ with self.subTest(command=command):
+ with self.assertRaises(FileNotFoundError) as c:
+ subprocess.call(command)
+ self.assertEqual(c.exception.filename, expected)
+ self.assertNotIn(secret, c.exception.filename or '')
+ self.assertNotIn(secret, str(c.exception))
+
def test_startupinfo(self):
# startupinfo argument
# We uses hardcoded constants, because we do not want to
diff --git
a/Misc/NEWS.d/next/Library/2026-09-17-17-30-00.gh-issue-119646.kP8nQm.rst
b/Misc/NEWS.d/next/Library/2026-09-17-17-30-00.gh-issue-119646.kP8nQm.rst
new file mode 100644
index 00000000000000..e546511677a93e
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-09-17-17-30-00.gh-issue-119646.kP8nQm.rst
@@ -0,0 +1,2 @@
+On Windows, :exc:`OSError` from :mod:`subprocess` now includes the attempted
+executable or working directory in ``filename``.
_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]