Skip to content

Commit 1942f4d

Browse files
johnslavikjaraco
authored andcommitted
pythongh-142315: Don't pass the "real path" of Pdb script target to system functions (pythonGH-142371)
* Pick target depending on preconditions * Clarify the news fragment * Add test capturing missed expectation. * Add more idiomatic safe realpath helper * Restore logic where existance and directoriness are checked on realpath. * Link GH issue to test. * Extract a function to check the target. Remove the _safe_realpath, now no longer needed. * Extract method for replacing sys_path, and isolate realpath usage there. * Revert "Extract method for replacing sys_path, and isolate realpath usage there." This reverts commit 855aac3. * Restore _safe_realpath. --------- (cherry picked from commit d716e3b) Co-authored-by: Bartosz Sławecki <[email protected]> Co-authored-by: Jason R. Coombs <[email protected]>
1 parent d14697d commit 1942f4d

File tree

3 files changed

+86
-7
lines changed

3 files changed

+86
-7
lines changed

Lib/pdb.py

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -183,19 +183,37 @@ class _ExecutableTarget:
183183

184184
class _ScriptTarget(_ExecutableTarget):
185185
def __init__(self, target):
186-
self._target = os.path.realpath(target)
186+
self._check(target)
187+
self._target = self._safe_realpath(target)
188+
189+
# If PYTHONSAFEPATH (-P) is not set, sys.path[0] is the directory
190+
# of pdb, and we should replace it with the directory of the script
191+
if not sys.flags.safe_path:
192+
sys.path[0] = os.path.dirname(self._target)
187193

188-
if not os.path.exists(self._target):
194+
@staticmethod
195+
def _check(target):
196+
"""
197+
Check that target is plausibly a script.
198+
"""
199+
if not os.path.exists(target):
189200
print(f'Error: {target} does not exist')
190201
sys.exit(1)
191-
if os.path.isdir(self._target):
202+
if os.path.isdir(target):
192203
print(f'Error: {target} is a directory')
193204
sys.exit(1)
194205

195-
# If safe_path(-P) is not set, sys.path[0] is the directory
196-
# of pdb, and we should replace it with the directory of the script
197-
if not sys.flags.safe_path:
198-
sys.path[0] = os.path.dirname(self._target)
206+
@staticmethod
207+
def _safe_realpath(path):
208+
"""
209+
Return the canonical path (realpath) if it is accessible from the userspace.
210+
Otherwise (for example, if the path is a symlink to an anonymous pipe),
211+
return the original path.
212+
213+
See GH-142315.
214+
"""
215+
realpath = os.path.realpath(path)
216+
return realpath if os.path.exists(realpath) else path
199217

200218
def __repr__(self):
201219
return self._target

Lib/test/test_pdb.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3561,6 +3561,24 @@ def _assert_find_function(self, file_content, func_name, expected):
35613561
self.assertEqual(
35623562
expected, pdb.find_function(func_name, os_helper.TESTFN))
35633563

3564+
def _fd_dir_for_pipe_targets(self):
3565+
"""Return a directory exposing live file descriptors, if any."""
3566+
proc_fd = "/proc/self/fd"
3567+
if os.path.isdir(proc_fd) and os.path.exists(os.path.join(proc_fd, '0')):
3568+
return proc_fd
3569+
3570+
dev_fd = "/dev/fd"
3571+
if os.path.isdir(dev_fd) and os.path.exists(os.path.join(dev_fd, '0')):
3572+
if sys.platform.startswith("freebsd"):
3573+
try:
3574+
if os.stat("/dev").st_dev == os.stat(dev_fd).st_dev:
3575+
return None
3576+
except FileNotFoundError:
3577+
return None
3578+
return dev_fd
3579+
3580+
return None
3581+
35643582
def test_find_function_empty_file(self):
35653583
self._assert_find_function(b'', 'foo', None)
35663584

@@ -3619,6 +3637,47 @@ def test_spec(self):
36193637
stdout, _ = self.run_pdb_script(script, commands)
36203638
self.assertIn('None', stdout)
36213639

3640+
def test_script_target_anonymous_pipe(self):
3641+
"""
3642+
_ScriptTarget doesn't fail on an anonymous pipe.
3643+
3644+
GH-142315
3645+
"""
3646+
fd_dir = self._fd_dir_for_pipe_targets()
3647+
if fd_dir is None:
3648+
self.skipTest('anonymous pipe targets require /proc/self/fd or /dev/fd')
3649+
3650+
read_fd, write_fd = os.pipe()
3651+
3652+
def safe_close(fd):
3653+
try:
3654+
os.close(fd)
3655+
except OSError:
3656+
pass
3657+
3658+
self.addCleanup(safe_close, read_fd)
3659+
self.addCleanup(safe_close, write_fd)
3660+
3661+
pipe_path = os.path.join(fd_dir, str(read_fd))
3662+
if not os.path.exists(pipe_path):
3663+
self.skipTest('fd directory does not expose anonymous pipes')
3664+
3665+
script_source = 'marker = "via_pipe"\n'
3666+
os.write(write_fd, script_source.encode('utf-8'))
3667+
os.close(write_fd)
3668+
3669+
original_path0 = sys.path[0]
3670+
self.addCleanup(sys.path.__setitem__, 0, original_path0)
3671+
3672+
target = pdb._ScriptTarget(pipe_path)
3673+
code_text = target.code
3674+
namespace = target.namespace
3675+
exec(code_text, namespace)
3676+
3677+
self.assertEqual(namespace['marker'], 'via_pipe')
3678+
self.assertEqual(namespace['__file__'], target.filename)
3679+
self.assertIsNone(namespace['__spec__'])
3680+
36223681
def test_find_function_first_executable_line(self):
36233682
code = textwrap.dedent("""\
36243683
def foo(): pass
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Pdb can now run scripts from anonymous pipes used in process substitution.
2+
Patch by Bartosz Sławecki.

0 commit comments

Comments
 (0)