From c0cfad71387dc5bb9ba8c229f7d869746abd10e4 Mon Sep 17 00:00:00 2001 From: Pat Date: Thu, 17 Sep 2026 17:16:50 -0500 Subject: [PATCH 1/2] gh-119646: Include the attempted path on Windows subprocess OSError Co-authored-by: Cursor --- Lib/subprocess.py | 21 ++++++++++++++++--- Lib/test/test_subprocess.py | 17 +++++++++++---- ...-09-17-17-30-00.gh-issue-119646.kP8nQm.rst | 2 ++ 3 files changed, 33 insertions(+), 7 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-09-17-17-30-00.gh-issue-119646.kP8nQm.rst diff --git a/Lib/subprocess.py b/Lib/subprocess.py index d38cc756ec479f2..f07f2e0f82213da 100644 --- a/Lib/subprocess.py +++ b/Lib/subprocess.py @@ -1629,21 +1629,23 @@ 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 + orig_filename = args 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: + 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 +1727,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 fc94b9a972828c1..53df16292f5a98c 100644 --- a/Lib/test/test_subprocess.py +++ b/Lib/test/test_subprocess.py @@ -1797,13 +1797,14 @@ 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) - @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 +3719,14 @@ 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_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 000000000000000..e546511677a93e6 --- /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``. From 3ae4021450b0dcf40486c69466e1987cddb5f38c Mon Sep 17 00:00:00 2001 From: Pat Date: Thu, 17 Sep 2026 17:33:54 -0500 Subject: [PATCH 2/2] gh-119646: Materialize iterable args before reading the executable name Co-authored-by: Cursor --- Lib/subprocess.py | 1 + Lib/test/test_subprocess.py | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/Lib/subprocess.py b/Lib/subprocess.py index f07f2e0f82213da..dee87695182cd54 100644 --- a/Lib/subprocess.py +++ b/Lib/subprocess.py @@ -1642,6 +1642,7 @@ def _execute_child(self, args, executable, preexec_fn, close_fds, 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: diff --git a/Lib/test/test_subprocess.py b/Lib/test/test_subprocess.py index 53df16292f5a98c..2bbd8adede8f733 100644 --- a/Lib/test/test_subprocess.py +++ b/Lib/test/test_subprocess.py @@ -1804,6 +1804,20 @@ def test_file_not_found_includes_filename(self): 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, "Windows reports NotADirectoryError (WinError 267)") def test_file_not_found_with_bad_cwd(self): with self.assertRaises(FileNotFoundError) as c: