From 70507701b780edd9aa5a7d8b1dc047dd56356766 Mon Sep 17 00:00:00 2001 From: johnslavik Date: Sun, 7 Dec 2025 14:26:18 +0100 Subject: [PATCH 01/10] Pick target depending on preconditions --- Lib/pdb.py | 10 ++++++---- .../2025-12-07-02-36-24.gh-issue-142315.02o5E_.rst | 1 + 2 files changed, 7 insertions(+), 4 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2025-12-07-02-36-24.gh-issue-142315.02o5E_.rst diff --git a/Lib/pdb.py b/Lib/pdb.py index 1506e3d4709817..5729624ba139ed 100644 --- a/Lib/pdb.py +++ b/Lib/pdb.py @@ -183,15 +183,17 @@ class _ExecutableTarget: class _ScriptTarget(_ExecutableTarget): def __init__(self, target): - self._target = os.path.realpath(target) - - if not os.path.exists(self._target): + if not os.path.exists(target): print(f'Error: {target} does not exist') sys.exit(1) - if os.path.isdir(self._target): + if os.path.isdir(target): print(f'Error: {target} is a directory') sys.exit(1) + # Be careful with realpath to support pseudofilesystems (GH-142315). + realpath = os.path.realpath(target) + self._target = realpath if os.path.exists(realpath) else target + # If safe_path(-P) is not set, sys.path[0] is the directory # of pdb, and we should replace it with the directory of the script if not sys.flags.safe_path: diff --git a/Misc/NEWS.d/next/Library/2025-12-07-02-36-24.gh-issue-142315.02o5E_.rst b/Misc/NEWS.d/next/Library/2025-12-07-02-36-24.gh-issue-142315.02o5E_.rst new file mode 100644 index 00000000000000..b3941fdd5231b3 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-12-07-02-36-24.gh-issue-142315.02o5E_.rst @@ -0,0 +1 @@ +Pdb can run scripts from pseudofiles. Patch by Bartosz Sławecki. From 2d8c231d7f26f8a6d8ce589dfd5fcf9c770a404f Mon Sep 17 00:00:00 2001 From: johnslavik Date: Sun, 7 Dec 2025 14:32:44 +0100 Subject: [PATCH 02/10] Clarify the news fragment --- .../Library/2025-12-07-02-36-24.gh-issue-142315.02o5E_.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2025-12-07-02-36-24.gh-issue-142315.02o5E_.rst b/Misc/NEWS.d/next/Library/2025-12-07-02-36-24.gh-issue-142315.02o5E_.rst index b3941fdd5231b3..e9c5ba3c0639f2 100644 --- a/Misc/NEWS.d/next/Library/2025-12-07-02-36-24.gh-issue-142315.02o5E_.rst +++ b/Misc/NEWS.d/next/Library/2025-12-07-02-36-24.gh-issue-142315.02o5E_.rst @@ -1 +1,2 @@ -Pdb can run scripts from pseudofiles. Patch by Bartosz Sławecki. +Pdb can now run scripts from anonymous pipes used in process substitution. +Patch by Bartosz Sławecki. From 7196224d565b4ed2e1d3d0c87a24baeda0ccaa3d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 7 Dec 2025 12:57:12 -0500 Subject: [PATCH 03/10] Add test capturing missed expectation. --- Lib/test/test_pdb.py | 54 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/Lib/test/test_pdb.py b/Lib/test/test_pdb.py index c097808e7fdc7c..c87162de57f0d1 100644 --- a/Lib/test/test_pdb.py +++ b/Lib/test/test_pdb.py @@ -3561,6 +3561,24 @@ def _assert_find_function(self, file_content, func_name, expected): self.assertEqual( expected, pdb.find_function(func_name, os_helper.TESTFN)) + def _fd_dir_for_pipe_targets(self): + """Return a directory exposing live file descriptors, if any.""" + proc_fd = "/proc/self/fd" + if os.path.isdir(proc_fd) and os.path.exists(os.path.join(proc_fd, '0')): + return proc_fd + + dev_fd = "/dev/fd" + if os.path.isdir(dev_fd) and os.path.exists(os.path.join(dev_fd, '0')): + if sys.platform.startswith("freebsd"): + try: + if os.stat("/dev").st_dev == os.stat(dev_fd).st_dev: + return None + except FileNotFoundError: + return None + return dev_fd + + return None + def test_find_function_empty_file(self): self._assert_find_function(b'', 'foo', None) @@ -3633,6 +3651,42 @@ def test_spec(self): stdout, _ = self.run_pdb_script(script, commands) self.assertIn('None', stdout) + def test_script_target_anonymous_pipe(self): + fd_dir = self._fd_dir_for_pipe_targets() + if fd_dir is None: + self.skipTest('anonymous pipe targets require /proc/self/fd or /dev/fd') + + read_fd, write_fd = os.pipe() + + def safe_close(fd): + try: + os.close(fd) + except OSError: + pass + + self.addCleanup(safe_close, read_fd) + self.addCleanup(safe_close, write_fd) + + pipe_path = os.path.join(fd_dir, str(read_fd)) + if not os.path.exists(pipe_path): + self.skipTest('fd directory does not expose anonymous pipes') + + script_source = 'marker = "via_pipe"\n' + os.write(write_fd, script_source.encode('utf-8')) + os.close(write_fd) + + original_path0 = sys.path[0] + self.addCleanup(sys.path.__setitem__, 0, original_path0) + + target = pdb._ScriptTarget(pipe_path) + code_text = target.code + namespace = target.namespace + exec(code_text, namespace) + + self.assertEqual(namespace['marker'], 'via_pipe') + self.assertEqual(namespace['__file__'], target.filename) + self.assertIsNone(namespace['__spec__']) + def test_find_function_first_executable_line(self): code = textwrap.dedent("""\ def foo(): pass From 5b129045be3506a3b5df8d018a81dee42afe0f55 Mon Sep 17 00:00:00 2001 From: johnslavik Date: Sun, 7 Dec 2025 19:24:42 +0100 Subject: [PATCH 04/10] Add more idiomatic safe realpath helper --- Lib/pdb.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/Lib/pdb.py b/Lib/pdb.py index 5729624ba139ed..a2930b5a854236 100644 --- a/Lib/pdb.py +++ b/Lib/pdb.py @@ -190,11 +190,9 @@ def __init__(self, target): print(f'Error: {target} is a directory') sys.exit(1) - # Be careful with realpath to support pseudofilesystems (GH-142315). - realpath = os.path.realpath(target) - self._target = realpath if os.path.exists(realpath) else target + self._target = self._safe_realpath(target) - # If safe_path(-P) is not set, sys.path[0] is the directory + # If PYTHONSAFEPATH (-P) is not set, sys.path[0] is the directory # of pdb, and we should replace it with the directory of the script if not sys.flags.safe_path: sys.path[0] = os.path.dirname(self._target) @@ -202,6 +200,18 @@ def __init__(self, target): def __repr__(self): return self._target + @staticmethod + def _safe_realpath(path): + """ + Return the canonical path (realpath) if it is accessible from the userspace. + Otherwise (for example, if the path is a symlink to an anonymous pipe), + return the original path. + + See GH-142315. + """ + realpath = os.path.realpath(path) + return realpath if os.path.exists(realpath) else path + @property def filename(self): return self._target From 4ca2bcf843337c8acf62a2d09afa6c02262320c7 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 7 Dec 2025 15:24:25 -0500 Subject: [PATCH 05/10] Restore logic where existance and directoriness are checked on realpath. --- Lib/pdb.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Lib/pdb.py b/Lib/pdb.py index a2930b5a854236..33ac6a13640f1f 100644 --- a/Lib/pdb.py +++ b/Lib/pdb.py @@ -183,15 +183,15 @@ class _ExecutableTarget: class _ScriptTarget(_ExecutableTarget): def __init__(self, target): - if not os.path.exists(target): + self._target = self._safe_realpath(target) + + if not os.path.exists(self._target): print(f'Error: {target} does not exist') sys.exit(1) - if os.path.isdir(target): + if os.path.isdir(self._target): print(f'Error: {target} is a directory') sys.exit(1) - self._target = self._safe_realpath(target) - # If PYTHONSAFEPATH (-P) is not set, sys.path[0] is the directory # of pdb, and we should replace it with the directory of the script if not sys.flags.safe_path: From 3c79be645026884cefe63ab06eacf0f7aa484b26 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 7 Dec 2025 15:30:06 -0500 Subject: [PATCH 06/10] Link GH issue to test. --- Lib/test/test_pdb.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Lib/test/test_pdb.py b/Lib/test/test_pdb.py index c87162de57f0d1..f4c870036a7a20 100644 --- a/Lib/test/test_pdb.py +++ b/Lib/test/test_pdb.py @@ -3652,6 +3652,11 @@ def test_spec(self): self.assertIn('None', stdout) def test_script_target_anonymous_pipe(self): + """ + _ScriptTarget doesn't fail on an anonymous pipe. + + GH-142315 + """ fd_dir = self._fd_dir_for_pipe_targets() if fd_dir is None: self.skipTest('anonymous pipe targets require /proc/self/fd or /dev/fd') From d1d85b9c4cbf08888ded140f9a308ba266d5923f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 8 Dec 2025 02:33:33 -0500 Subject: [PATCH 07/10] Extract a function to check the target. Remove the _safe_realpath, now no longer needed. --- Lib/pdb.py | 32 +++++++++++++------------------- 1 file changed, 13 insertions(+), 19 deletions(-) diff --git a/Lib/pdb.py b/Lib/pdb.py index 33ac6a13640f1f..007541ed0a46b3 100644 --- a/Lib/pdb.py +++ b/Lib/pdb.py @@ -183,34 +183,28 @@ class _ExecutableTarget: class _ScriptTarget(_ExecutableTarget): def __init__(self, target): - self._target = self._safe_realpath(target) - - if not os.path.exists(self._target): - print(f'Error: {target} does not exist') - sys.exit(1) - if os.path.isdir(self._target): - print(f'Error: {target} is a directory') - sys.exit(1) + self._check(target) + self._target = os.path.realpath(target) # If PYTHONSAFEPATH (-P) is not set, sys.path[0] is the directory # of pdb, and we should replace it with the directory of the script if not sys.flags.safe_path: sys.path[0] = os.path.dirname(self._target) - def __repr__(self): - return self._target - @staticmethod - def _safe_realpath(path): + def _check(target): """ - Return the canonical path (realpath) if it is accessible from the userspace. - Otherwise (for example, if the path is a symlink to an anonymous pipe), - return the original path. - - See GH-142315. + Check that target is plausibly a script. """ - realpath = os.path.realpath(path) - return realpath if os.path.exists(realpath) else path + if not os.path.exists(target): + print(f'Error: {target} does not exist') + sys.exit(1) + if os.path.isdir(target): + print(f'Error: {target} is a directory') + sys.exit(1) + + def __repr__(self): + return self._target @property def filename(self): From 855aac3d289dd096142ed9bd23d00c22ce6e1859 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 8 Dec 2025 02:42:39 -0500 Subject: [PATCH 08/10] Extract method for replacing sys_path, and isolate realpath usage there. --- Lib/pdb.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/Lib/pdb.py b/Lib/pdb.py index 007541ed0a46b3..1c2b70f6bdf555 100644 --- a/Lib/pdb.py +++ b/Lib/pdb.py @@ -183,13 +183,9 @@ class _ExecutableTarget: class _ScriptTarget(_ExecutableTarget): def __init__(self, target): + self._target = target self._check(target) - self._target = os.path.realpath(target) - - # If PYTHONSAFEPATH (-P) is not set, sys.path[0] is the directory - # of pdb, and we should replace it with the directory of the script - if not sys.flags.safe_path: - sys.path[0] = os.path.dirname(self._target) + self._replace_sys_path(target) @staticmethod def _check(target): @@ -203,6 +199,13 @@ def _check(target): print(f'Error: {target} is a directory') sys.exit(1) + @staticmethod + def _replace_sys_path(target): + # If PYTHONSAFEPATH (-P) is not set, sys.path[0] is the directory + # of pdb, so replace it with the directory of the script + if not sys.flags.safe_path: + sys.path[0] = os.path.dirname(os.path.realpath(target)) + def __repr__(self): return self._target From 03eae7e0f269d6937946c6bc88f45e6b626e4bb9 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 8 Dec 2025 02:59:26 -0500 Subject: [PATCH 09/10] Revert "Extract method for replacing sys_path, and isolate realpath usage there." This reverts commit 855aac3d289dd096142ed9bd23d00c22ce6e1859. --- Lib/pdb.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/Lib/pdb.py b/Lib/pdb.py index 1c2b70f6bdf555..007541ed0a46b3 100644 --- a/Lib/pdb.py +++ b/Lib/pdb.py @@ -183,9 +183,13 @@ class _ExecutableTarget: class _ScriptTarget(_ExecutableTarget): def __init__(self, target): - self._target = target self._check(target) - self._replace_sys_path(target) + self._target = os.path.realpath(target) + + # If PYTHONSAFEPATH (-P) is not set, sys.path[0] is the directory + # of pdb, and we should replace it with the directory of the script + if not sys.flags.safe_path: + sys.path[0] = os.path.dirname(self._target) @staticmethod def _check(target): @@ -199,13 +203,6 @@ def _check(target): print(f'Error: {target} is a directory') sys.exit(1) - @staticmethod - def _replace_sys_path(target): - # If PYTHONSAFEPATH (-P) is not set, sys.path[0] is the directory - # of pdb, so replace it with the directory of the script - if not sys.flags.safe_path: - sys.path[0] = os.path.dirname(os.path.realpath(target)) - def __repr__(self): return self._target From 677959782509eaa4f251fb6f0c17bdce3d2ffea9 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 8 Dec 2025 08:35:30 -0500 Subject: [PATCH 10/10] Restore _safe_realpath. --- Lib/pdb.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/Lib/pdb.py b/Lib/pdb.py index 007541ed0a46b3..c1a5db080dc7ef 100644 --- a/Lib/pdb.py +++ b/Lib/pdb.py @@ -184,7 +184,7 @@ class _ExecutableTarget: class _ScriptTarget(_ExecutableTarget): def __init__(self, target): self._check(target) - self._target = os.path.realpath(target) + self._target = self._safe_realpath(target) # If PYTHONSAFEPATH (-P) is not set, sys.path[0] is the directory # of pdb, and we should replace it with the directory of the script @@ -203,6 +203,18 @@ def _check(target): print(f'Error: {target} is a directory') sys.exit(1) + @staticmethod + def _safe_realpath(path): + """ + Return the canonical path (realpath) if it is accessible from the userspace. + Otherwise (for example, if the path is a symlink to an anonymous pipe), + return the original path. + + See GH-142315. + """ + realpath = os.path.realpath(path) + return realpath if os.path.exists(realpath) else path + def __repr__(self): return self._target