Skip to content

Commit 9b86022

Browse files
committed
Add tests and use io.open_code
1 parent 6f6b4cd commit 9b86022

File tree

4 files changed

+241
-25
lines changed

4 files changed

+241
-25
lines changed

Lib/test/test_sys.py

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,15 @@
1212
import sys
1313
import sysconfig
1414
import test.support
15+
from io import StringIO
16+
from unittest import mock
1517
from test import support
1618
from test.support import os_helper
1719
from test.support.script_helper import assert_python_ok, assert_python_failure
1820
from test.support import threading_helper
1921
from test.support import import_helper
2022
from test.support import force_not_colorized
23+
from test.support import SHORT_TIMEOUT
2124
try:
2225
from test.support import interpreters
2326
except ImportError:
@@ -1923,5 +1926,193 @@ def write(self, s):
19231926
self.assertEqual(out, b"")
19241927
self.assertEqual(err, b"")
19251928

1929+
1930+
def _supports_remote_attaching():
1931+
PROCESS_VM_READV_SUPPORTED = False
1932+
1933+
try:
1934+
from _testexternalinspection import PROCESS_VM_READV_SUPPORTED
1935+
except ImportError:
1936+
pass
1937+
1938+
return PROCESS_VM_READV_SUPPORTED
1939+
1940+
@unittest.skipIf(not sys.is_remote_debug_enabled(), "Remote debugging is not enabled")
1941+
@unittest.skipIf(sys.platform != "darwin" and sys.platform != "linux",
1942+
"Test only runs on Linux and MacOS")
1943+
@unittest.skipIf(sys.platform == "linux" and not _supports_remote_attaching(),
1944+
"Test only runs on Linux with process_vm_readv support")
1945+
class TestRemoteExec(unittest.TestCase):
1946+
def tearDown(self):
1947+
test.support.reap_children()
1948+
1949+
def _run_remote_exec_test(self, script_code, python_args=None, env=None, prologue=''):
1950+
# Create the script that will be remotely executed
1951+
script = os_helper.TESTFN + '_remote.py'
1952+
self.addCleanup(os_helper.unlink, script)
1953+
1954+
with open(script, 'w') as f:
1955+
f.write(script_code)
1956+
1957+
# Create and run the target process
1958+
target = os_helper.TESTFN + '_target.py'
1959+
self.addCleanup(os_helper.unlink, target)
1960+
1961+
with os_helper.temp_dir() as work_dir:
1962+
fifo = f"{work_dir}/the_fifo"
1963+
os.mkfifo(fifo)
1964+
self.addCleanup(os_helper.unlink, fifo)
1965+
1966+
with open(target, 'w') as f:
1967+
f.write(f'''
1968+
import sys
1969+
import time
1970+
1971+
with open("{fifo}", "w") as fifo:
1972+
fifo.write("ready")
1973+
1974+
{prologue}
1975+
1976+
print("Target process running...")
1977+
1978+
# Wait for remote script to be executed
1979+
# (the execution will happen as the following
1980+
# code is processed as soon as the read() call
1981+
# unblocks)
1982+
with open("{fifo}", "r") as fifo:
1983+
fifo.read()
1984+
1985+
# Write confirmation back
1986+
with open("{fifo}", "w") as fifo:
1987+
fifo.write("executed")
1988+
''')
1989+
1990+
# Start the target process and capture its output
1991+
cmd = [sys.executable]
1992+
if python_args:
1993+
cmd.extend(python_args)
1994+
cmd.append(target)
1995+
1996+
with subprocess.Popen(cmd,
1997+
stdout=subprocess.PIPE,
1998+
stderr=subprocess.PIPE,
1999+
env=env) as proc:
2000+
try:
2001+
# Wait for process to be ready
2002+
with open(fifo, "r") as fifo_file:
2003+
response = fifo_file.read()
2004+
self.assertEqual(response, "ready")
2005+
2006+
# Try remote exec on the target process
2007+
sys.remote_exec(proc.pid, script)
2008+
2009+
# Signal script to continue
2010+
with open(fifo, "w") as fifo_file:
2011+
fifo_file.write("continue")
2012+
2013+
# Wait for execution confirmation
2014+
with open(fifo, "r") as fifo_file:
2015+
response = fifo_file.read()
2016+
self.assertEqual(response, "executed")
2017+
2018+
# Return output for test verification
2019+
stdout, stderr = proc.communicate(timeout=1.0)
2020+
return proc.returncode, stdout, stderr
2021+
except PermissionError:
2022+
self.skipTest("Insufficient permissions to execute code in remote process")
2023+
finally:
2024+
proc.kill()
2025+
proc.terminate()
2026+
proc.wait(timeout=SHORT_TIMEOUT)
2027+
2028+
def test_remote_exec(self):
2029+
"""Test basic remote exec functionality"""
2030+
script = '''
2031+
print("Remote script executed successfully!")
2032+
'''
2033+
returncode, stdout, stderr = self._run_remote_exec_test(script)
2034+
self.assertEqual(returncode, 0)
2035+
self.assertIn(b"Remote script executed successfully!", stdout)
2036+
self.assertEqual(stderr, b"")
2037+
2038+
def test_remote_exec_with_self_process(self):
2039+
"""Test remote exec with the target process being the same as the test process"""
2040+
2041+
code = 'import sys;print("Remote script executed successfully!", file=sys.stderr)'
2042+
file = os_helper.TESTFN + '_remote.py'
2043+
with open(file, 'w') as f:
2044+
f.write(code)
2045+
self.addCleanup(os_helper.unlink, file)
2046+
with mock.patch('sys.stderr', new_callable=StringIO) as mock_stderr:
2047+
with mock.patch('sys.stdout', new_callable=StringIO) as mock_stdout:
2048+
sys.remote_exec(os.getpid(), file)
2049+
print("Done")
2050+
self.assertEqual(mock_stderr.getvalue(), "Remote script executed successfully!\n")
2051+
self.assertEqual(mock_stdout.getvalue(), "Done\n")
2052+
2053+
def test_remote_exec_raises_audit_event(self):
2054+
"""Test remote exec raises an audit event"""
2055+
prologue = '''\
2056+
import sys
2057+
def audit_hook(event, arg):
2058+
print(f"Audit event: {event}, arg: {arg}")
2059+
sys.addaudithook(audit_hook)
2060+
'''
2061+
script = '''
2062+
print("Remote script executed successfully!")
2063+
'''
2064+
returncode, stdout, stderr = self._run_remote_exec_test(script, prologue=prologue)
2065+
self.assertEqual(returncode, 0)
2066+
self.assertIn(b"Remote script executed successfully!", stdout)
2067+
self.assertIn(b"Audit event: remote_debugger_script, arg: ", stdout)
2068+
self.assertEqual(stderr, b"")
2069+
2070+
def test_remote_exec_with_exception(self):
2071+
"""Test remote exec with an exception raised in the target process
2072+
2073+
The exception should be raised in the main thread of the target process
2074+
but not crash the target process.
2075+
"""
2076+
script = '''
2077+
raise Exception("Remote script exception")
2078+
'''
2079+
returncode, stdout, stderr = self._run_remote_exec_test(script)
2080+
self.assertEqual(returncode, 0)
2081+
self.assertIn(b"Remote script exception", stderr)
2082+
self.assertEqual(stdout, b"Target process running...\n")
2083+
2084+
def test_remote_exec_disabled_by_env(self):
2085+
"""Test remote exec is disabled when PYTHON_DISABLE_REMOTE_DEBUG is set"""
2086+
env = os.environ.copy()
2087+
env['PYTHON_DISABLE_REMOTE_DEBUG'] = '1'
2088+
with self.assertRaisesRegex(RuntimeError, "Remote debugging is not enabled in the remote process"):
2089+
self._run_remote_exec_test("print('should not run')", env=env)
2090+
2091+
def test_remote_exec_disabled_by_xoption(self):
2092+
"""Test remote exec is disabled with -Xdisable-remote-debug"""
2093+
with self.assertRaisesRegex(RuntimeError, "Remote debugging is not enabled in the remote process"):
2094+
self._run_remote_exec_test("print('should not run')", python_args=['-Xdisable-remote-debug'])
2095+
2096+
def test_remote_exec_invalid_pid(self):
2097+
"""Test remote exec with invalid process ID"""
2098+
with self.assertRaises(OSError):
2099+
sys.remote_exec(999999, "print('should not run')")
2100+
2101+
def test_remote_exec_syntax_error(self):
2102+
"""Test remote exec with syntax error in script"""
2103+
script = '''
2104+
this is invalid python code
2105+
'''
2106+
returncode, stdout, stderr = self._run_remote_exec_test(script)
2107+
self.assertEqual(returncode, 0)
2108+
self.assertIn(b"SyntaxError", stderr)
2109+
self.assertEqual(stdout, b"Target process running...\n")
2110+
2111+
def test_remote_exec_invalid_script_path(self):
2112+
"""Test remote exec with invalid script path"""
2113+
with self.assertRaises(OSError):
2114+
sys.remote_exec(os.getpid(), "invalid_script_path")
2115+
2116+
19262117
if __name__ == "__main__":
19272118
unittest.main()

Python/ceval_gil.c

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1327,19 +1327,38 @@ _Py_HandlePending(PyThreadState *tstate)
13271327
tstate->remote_debugger_support.debugger_pending_call = 0;
13281328
const char *path = tstate->remote_debugger_support.debugger_script_path;
13291329
if (*path) {
1330-
if (0 != PySys_Audit("debugger_script", "%s", path)) {
1331-
PyErr_Clear();
1330+
if (0 != PySys_Audit("remote_debugger_script", "s", path)) {
1331+
PyErr_FormatUnraisable("Error when auditing remote debugger script %s", path);
13321332
} else {
1333-
FILE* f = fopen(path, "r");
1333+
// Open the debugger script with the open code hook. Unfortunately this forces us to handle
1334+
// the resulting Python object, which is a file object and therefore we need to call
1335+
// Python methods on it instead of the simpler C equivalents.
1336+
PyObject* fileobj = PyFile_OpenCode(path);
1337+
if (!fileobj) {
1338+
PyErr_FormatUnraisable("Error when opening debugger script %s", path);
1339+
return 0;
1340+
}
1341+
int fd = PyObject_AsFileDescriptor(fileobj);
1342+
if (fd == -1) {
1343+
PyErr_FormatUnraisable("Error when getting file descriptor for debugger script %s", path);
1344+
return 0;
1345+
}
1346+
FILE* f = fdopen(fd, "r");
13341347
if (!f) {
13351348
PyErr_SetFromErrno(PyExc_OSError);
13361349
} else {
13371350
PyRun_AnyFile(f, path);
1338-
fclose(f);
13391351
}
13401352
if (PyErr_Occurred()) {
13411353
PyErr_FormatUnraisable("Error executing debugger script %s", path);
13421354
}
1355+
PyObject* res = PyObject_CallMethod(fileobj, "close", "");
1356+
if (!res) {
1357+
PyErr_FormatUnraisable("Error when closing debugger script %s", path);
1358+
} else {
1359+
Py_DECREF(res);
1360+
}
1361+
Py_DECREF(fileobj);
13431362
}
13441363
}
13451364
}

Python/remote_debugging.c

Lines changed: 26 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -595,16 +595,27 @@ _PySysRemoteDebug_SendExec(int pid, int tid, const char *debugger_script_path)
595595
}
596596
eval_breaker |= _PY_EVAL_PLEASE_STOP_BIT;
597597

598-
bytes = write_memory(
599-
pid,
600-
address_of_thread + local_debug_offsets.debugger_support.eval_breaker,
601-
sizeof(uintptr_t),
602-
&eval_breaker);
603-
604-
if (bytes == -1) {
598+
// Ensure our path is not too long
599+
if (local_debug_offsets.debugger_support.debugger_script_path_size <= strlen(debugger_script_path)) {
600+
PyErr_SetString(PyExc_ValueError, "Debugger script path is too long");
605601
return -1;
606602
}
607603

604+
if (debugger_script_path != NULL) {
605+
uintptr_t debugger_script_path_addr = (
606+
address_of_thread +
607+
local_debug_offsets.debugger_support.remote_debugger_support +
608+
local_debug_offsets.debugger_support.debugger_script_path);
609+
bytes = write_memory(
610+
pid,
611+
debugger_script_path_addr,
612+
strlen(debugger_script_path) + 1,
613+
debugger_script_path);
614+
if (bytes == -1) {
615+
return -1;
616+
}
617+
}
618+
608619
int pending_call = 1;
609620
uintptr_t debugger_pending_call_addr = (
610621
address_of_thread +
@@ -620,19 +631,14 @@ _PySysRemoteDebug_SendExec(int pid, int tid, const char *debugger_script_path)
620631
return -1;
621632
}
622633

623-
if (debugger_script_path != NULL) {
624-
uintptr_t debugger_script_path_addr = (
625-
address_of_thread +
626-
local_debug_offsets.debugger_support.remote_debugger_support +
627-
local_debug_offsets.debugger_support.debugger_script_path);
628-
bytes = write_memory(
629-
pid,
630-
debugger_script_path_addr,
631-
strlen(debugger_script_path) + 1,
632-
debugger_script_path);
633-
if (bytes == -1) {
634-
return -1;
635-
}
634+
bytes = write_memory(
635+
pid,
636+
address_of_thread + local_debug_offsets.debugger_support.eval_breaker,
637+
sizeof(uintptr_t),
638+
&eval_breaker);
639+
640+
if (bytes == -1) {
641+
return -1;
636642
}
637643

638644
return 0;

Python/sysmodule.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2459,7 +2459,7 @@ it hasn't been overwritten.
24592459
24602460
Args:
24612461
pid (int): The process ID of the target Python process.
2462-
script (str|bytes|PathLike): The path to a file containing
2462+
script (str|bytes): The path to a file containing
24632463
the Python code to be executed.
24642464
[clinic start generated code]*/
24652465

0 commit comments

Comments
 (0)