From fb644146f1732d6bb76fe62ad7ae284495442a9b Mon Sep 17 00:00:00 2001 From: Russell Davis <551404+russelldavis@users.noreply.github.com> Date: Mon, 4 May 2020 22:50:25 -0700 Subject: [PATCH 1/9] bpo-40507: Add filename to the error raised by os.exec* --- Lib/os.py | 6 ++++++ Lib/test/test_os.py | 14 ++++++++++++-- Modules/posixmodule.c | 2 +- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/Lib/os.py b/Lib/os.py index b794159f86c33e..181412046f29c3 100644 --- a/Lib/os.py +++ b/Lib/os.py @@ -598,6 +598,7 @@ def _execvpe(file, args, env=None): return saved_exc = None path_list = get_exec_path(env) + orig_file = file if name != 'nt': file = fsencode(file) path_list = map(fsencode, path_list) @@ -613,6 +614,11 @@ def _execvpe(file, args, env=None): saved_exc = e if saved_exc is not None: raise saved_exc + if isinstance(last_exc, FileNotFoundError): + # At this point, last_exc.filename will contain the full path of + # whatever directory happened to be last in path_list. Set it to the + # filename that was passed in, which is what the caller will expect. + last_exc.filename = orig_file raise last_exc diff --git a/Lib/test/test_os.py b/Lib/test/test_os.py index 73dc064d5ff752..acabbce27e4127 100644 --- a/Lib/test/test_os.py +++ b/Lib/test/test_os.py @@ -1816,9 +1816,19 @@ def mock_execve(name, *args): class ExecTests(unittest.TestCase): @unittest.skipIf(USING_LINUXTHREADS, "avoid triggering a linuxthreads bug: see issue #4970") + def test_execv_with_bad_program(self): + bad_filename = 'nosuchapp' + with self.assertRaises(OSError) as ctx: + os.execv(bad_filename, [bad_filename]) + self.assertEqual(ctx.exception.filename, bad_filename) + self.assertIn(bad_filename, str(ctx.exception)) + def test_execvpe_with_bad_program(self): - self.assertRaises(OSError, os.execvpe, 'no such app-', - ['no such app-'], None) + bad_filename = 'nosuchapp' + with self.assertRaises(OSError) as ctx: + os.execvpe(bad_filename, [bad_filename], None) + self.assertEqual(ctx.exception.filename, bad_filename) + self.assertIn(bad_filename, str(ctx.exception)) def test_execv_with_bad_arglist(self): self.assertRaises(ValueError, os.execv, 'notepad', ()) diff --git a/Modules/posixmodule.c b/Modules/posixmodule.c index 2157cbbe5d9b58..12aca2a63eb999 100644 --- a/Modules/posixmodule.c +++ b/Modules/posixmodule.c @@ -5321,7 +5321,7 @@ os_execv_impl(PyObject *module, path_t *path, PyObject *argv) /* If we get here it's definitely an error */ free_string_array(argvlist, argc); - return posix_error(); + return path_error(path); } From fd4b22e97bc952c791974445b8e531622227a628 Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Tue, 5 May 2020 06:05:25 +0000 Subject: [PATCH 2/9] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20blu?= =?UTF-8?q?rb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../NEWS.d/next/Library/2020-05-05-06-05-24.bpo-40507.ggjoGl.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2020-05-05-06-05-24.bpo-40507.ggjoGl.rst diff --git a/Misc/NEWS.d/next/Library/2020-05-05-06-05-24.bpo-40507.ggjoGl.rst b/Misc/NEWS.d/next/Library/2020-05-05-06-05-24.bpo-40507.ggjoGl.rst new file mode 100644 index 00000000000000..66015cf717b443 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2020-05-05-06-05-24.bpo-40507.ggjoGl.rst @@ -0,0 +1 @@ +The os.exec* family of functions now includes the filename when raising a FileNotFound error. \ No newline at end of file From 3424864845e97cfeba6d8d43ce81e3ee0095c3b7 Mon Sep 17 00:00:00 2001 From: Russell Davis <551404+russelldavis@users.noreply.github.com> Date: Fri, 5 Jun 2020 23:33:33 -0700 Subject: [PATCH 3/9] Test all all os.execv* functions; add decorators --- Lib/test/test_os.py | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/Lib/test/test_os.py b/Lib/test/test_os.py index acabbce27e4127..359468bde89204 100644 --- a/Lib/test/test_os.py +++ b/Lib/test/test_os.py @@ -1814,21 +1814,28 @@ def mock_execve(name, *args): @unittest.skipUnless(hasattr(os, 'execv'), "need os.execv()") class ExecTests(unittest.TestCase): - @unittest.skipIf(USING_LINUXTHREADS, - "avoid triggering a linuxthreads bug: see issue #4970") - def test_execv_with_bad_program(self): + def _test_bad_program(self, do_exec): bad_filename = 'nosuchapp' with self.assertRaises(OSError) as ctx: - os.execv(bad_filename, [bad_filename]) + do_exec(bad_filename) self.assertEqual(ctx.exception.filename, bad_filename) self.assertIn(bad_filename, str(ctx.exception)) + @unittest.skipIf(USING_LINUXTHREADS, "linuxthreads bug: see issue #4970") + def test_execv_with_bad_program(self): + self._test_bad_program(lambda name: os.execv(name, [name])) + + @unittest.skipIf(USING_LINUXTHREADS, "linuxthreads bug: see issue #4970") + def test_execvp_with_bad_program(self): + self._test_bad_program(lambda name: os.execvp(name, [name])) + + @unittest.skipIf(USING_LINUXTHREADS, "linuxthreads bug: see issue #4970") + def test_execve_with_bad_program(self): + self._test_bad_program(lambda name: os.execve(name, [name], {})) + + @unittest.skipIf(USING_LINUXTHREADS, "linuxthreads bug: see issue #4970") def test_execvpe_with_bad_program(self): - bad_filename = 'nosuchapp' - with self.assertRaises(OSError) as ctx: - os.execvpe(bad_filename, [bad_filename], None) - self.assertEqual(ctx.exception.filename, bad_filename) - self.assertIn(bad_filename, str(ctx.exception)) + self._test_bad_program(lambda name: os.execvpe(name, [name], {})) def test_execv_with_bad_arglist(self): self.assertRaises(ValueError, os.execv, 'notepad', ()) From 3b1cd2b04c9734d6d2c4bf0272c6630c38723219 Mon Sep 17 00:00:00 2001 From: Russell Davis <551404+russelldavis@users.noreply.github.com> Date: Fri, 18 Feb 2022 11:15:51 -0800 Subject: [PATCH 4/9] Reword changelog --- .../next/Library/2020-05-05-06-05-24.bpo-40507.ggjoGl.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2020-05-05-06-05-24.bpo-40507.ggjoGl.rst b/Misc/NEWS.d/next/Library/2020-05-05-06-05-24.bpo-40507.ggjoGl.rst index 66015cf717b443..239a8f1c8e4dcd 100644 --- a/Misc/NEWS.d/next/Library/2020-05-05-06-05-24.bpo-40507.ggjoGl.rst +++ b/Misc/NEWS.d/next/Library/2020-05-05-06-05-24.bpo-40507.ggjoGl.rst @@ -1 +1 @@ -The os.exec* family of functions now includes the filename when raising a FileNotFound error. \ No newline at end of file +The os.exec* family of functions now includes the filename as an attribute on the FileNotFound exception object. From acc9520cfd10b9ade3818eb19f818c214a7650ca Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Tue, 11 Aug 2026 10:19:37 +0300 Subject: [PATCH 5/9] Apply suggestion from @serhiy-storchaka --- Lib/os.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Lib/os.py b/Lib/os.py index 791f461dcc054d..0748e0eb7fd137 100644 --- a/Lib/os.py +++ b/Lib/os.py @@ -599,6 +599,7 @@ def _execvpe(file, args, env=None): argrest = (args,) env = environ + file = os.fspath(file) if path.dirname(file): exec_func(file, *argrest) return From 8fbe45b9d307114659c96c94056b90249c5e438a Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Tue, 11 Aug 2026 10:35:34 +0300 Subject: [PATCH 6/9] Test bytes and path-like program names --- Lib/test/test_os/test_os.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/Lib/test/test_os/test_os.py b/Lib/test/test_os/test_os.py index e8706c3a7ccd6a..4daead87078256 100644 --- a/Lib/test/test_os/test_os.py +++ b/Lib/test/test_os/test_os.py @@ -2638,27 +2638,30 @@ def mock_execve(name, *args): "need os.execv()") class ExecTests(unittest.TestCase): def _test_bad_program(self, do_exec): - bad_filename = 'nosuchapp' - with self.assertRaises(OSError) as ctx: - do_exec(bad_filename) - self.assertEqual(ctx.exception.filename, bad_filename) - self.assertIn(bad_filename, str(ctx.exception)) + for bad_filename in ('nosuchapp', b'nosuchapp', + FakePath('nosuchapp'), FakePath(b'nosuchapp')): + with self.subTest(bad_filename): + with self.assertRaises(OSError) as ctx: + do_exec(bad_filename) + self.assertEqual(ctx.exception.filename, + os.fspath(bad_filename)) + self.assertIn('nosuchapp', str(ctx.exception)) @unittest.skipIf(USING_LINUXTHREADS, "linuxthreads bug: see issue #4970") def test_execv_with_bad_program(self): - self._test_bad_program(lambda name: os.execv(name, [name])) + self._test_bad_program(lambda name: os.execv(name, ['nosuchapp'])) @unittest.skipIf(USING_LINUXTHREADS, "linuxthreads bug: see issue #4970") def test_execvp_with_bad_program(self): - self._test_bad_program(lambda name: os.execvp(name, [name])) + self._test_bad_program(lambda name: os.execvp(name, ['nosuchapp'])) @unittest.skipIf(USING_LINUXTHREADS, "linuxthreads bug: see issue #4970") def test_execve_with_bad_program(self): - self._test_bad_program(lambda name: os.execve(name, [name], {})) + self._test_bad_program(lambda name: os.execve(name, ['nosuchapp'], {})) @unittest.skipIf(USING_LINUXTHREADS, "linuxthreads bug: see issue #4970") def test_execvpe_with_bad_program(self): - self._test_bad_program(lambda name: os.execvpe(name, [name], {})) + self._test_bad_program(lambda name: os.execvpe(name, ['nosuchapp'], {})) def test_execv_with_bad_arglist(self): self.assertRaises(ValueError, os.execv, 'notepad', ()) From 1b37f52e16b9dec9cb8046e6ad2b06b973183c80 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Tue, 11 Aug 2026 10:46:21 +0300 Subject: [PATCH 7/9] Set the filename attribute of NotADirectoryError too --- Lib/os.py | 10 +++++----- Lib/test/test_os/test_os.py | 15 +++++++++++++-- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/Lib/os.py b/Lib/os.py index 41134cf3bf469b..87547e369db817 100644 --- a/Lib/os.py +++ b/Lib/os.py @@ -665,11 +665,11 @@ def _execvpe(file, args, env=None): saved_exc = e if saved_exc is not None: raise saved_exc - if isinstance(last_exc, FileNotFoundError): - # At this point, last_exc.filename will contain the full path of - # whatever directory happened to be last in path_list. Set it to the - # filename that was passed in, which is what the caller will expect. - last_exc.filename = orig_file + # At this point, last_exc.filename contains the full path of whatever + # directory happened to be last in path_list. Set it to the filename that + # was passed in, which is what the caller will expect. This is what + # subprocess does too (see err_filename in Popen._execute_child()). + last_exc.filename = orig_file raise last_exc diff --git a/Lib/test/test_os/test_os.py b/Lib/test/test_os/test_os.py index 4daead87078256..f0259f1b86a5b0 100644 --- a/Lib/test/test_os/test_os.py +++ b/Lib/test/test_os/test_os.py @@ -2637,11 +2637,11 @@ def mock_execve(name, *args): @unittest.skipUnless(hasattr(os, 'execv'), "need os.execv()") class ExecTests(unittest.TestCase): - def _test_bad_program(self, do_exec): + def _test_bad_program(self, do_exec, exc_type=OSError): for bad_filename in ('nosuchapp', b'nosuchapp', FakePath('nosuchapp'), FakePath(b'nosuchapp')): with self.subTest(bad_filename): - with self.assertRaises(OSError) as ctx: + with self.assertRaises(exc_type) as ctx: do_exec(bad_filename) self.assertEqual(ctx.exception.filename, os.fspath(bad_filename)) @@ -2663,6 +2663,17 @@ def test_execve_with_bad_program(self): def test_execvpe_with_bad_program(self): self._test_bad_program(lambda name: os.execvpe(name, ['nosuchapp'], {})) + @unittest.skipUnless(os.name == 'posix', 'POSIX specific test') + @unittest.skipIf(USING_LINUXTHREADS, "linuxthreads bug: see issue #4970") + def test_execvp_with_bad_path_entry(self): + # A regular file in PATH makes the exec fail with ENOTDIR. + create_file(os_helper.TESTFN) + self.addCleanup(os_helper.unlink, os_helper.TESTFN) + with os_helper.EnvironmentVarGuard() as env: + env['PATH'] = os.path.abspath(os_helper.TESTFN) + self._test_bad_program(lambda name: os.execvp(name, ['nosuchapp']), + NotADirectoryError) + def test_execv_with_bad_arglist(self): self.assertRaises(ValueError, os.execv, 'notepad', ()) self.assertRaises(ValueError, os.execv, 'notepad', []) From c8113c5916f65e7035fa910e9718578c889b967f Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Tue, 11 Aug 2026 10:46:57 +0300 Subject: [PATCH 8/9] Update the NEWS entry --- .../next/Library/2020-05-05-06-05-24.gh-issue-84687.ggjoGl.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2020-05-05-06-05-24.gh-issue-84687.ggjoGl.rst b/Misc/NEWS.d/next/Library/2020-05-05-06-05-24.gh-issue-84687.ggjoGl.rst index 1c09f5d5deee7e..b6dd108136f6dc 100644 --- a/Misc/NEWS.d/next/Library/2020-05-05-06-05-24.gh-issue-84687.ggjoGl.rst +++ b/Misc/NEWS.d/next/Library/2020-05-05-06-05-24.gh-issue-84687.ggjoGl.rst @@ -1,2 +1,3 @@ The :func:`os.exec\* ` functions now set the -:attr:`~OSError.filename` attribute of the raised :exc:`FileNotFoundError`. +:attr:`~OSError.filename` attribute of the raised :exc:`FileNotFoundError` +or :exc:`NotADirectoryError` to the program name passed by the caller. From c4750d78569474bb8feda371588bf0a670e77613 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Tue, 11 Aug 2026 12:01:00 +0300 Subject: [PATCH 9/9] Skip tests not supported on some platforms --- Lib/test/test_os/test_os.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_os/test_os.py b/Lib/test/test_os/test_os.py index f0259f1b86a5b0..4c1ab96065587e 100644 --- a/Lib/test/test_os/test_os.py +++ b/Lib/test/test_os/test_os.py @@ -2636,10 +2636,17 @@ def mock_execve(name, *args): @unittest.skipUnless(hasattr(os, 'execv'), "need os.execv()") +@unittest.skipIf(support.is_emscripten, + "Emscripten always fails with ENOEXEC") +@unittest.skipIf(support.is_android, + "PATH contains an inaccessible directory on Android") class ExecTests(unittest.TestCase): def _test_bad_program(self, do_exec, exc_type=OSError): - for bad_filename in ('nosuchapp', b'nosuchapp', - FakePath('nosuchapp'), FakePath(b'nosuchapp')): + bad_filenames = ['nosuchapp', FakePath('nosuchapp')] + if os.name != 'nt': + # Bytes program names are not supported on Windows. + bad_filenames += [b'nosuchapp', FakePath(b'nosuchapp')] + for bad_filename in bad_filenames: with self.subTest(bad_filename): with self.assertRaises(exc_type) as ctx: do_exec(bad_filename)