diff --git a/Doc/library/sys.rst b/Doc/library/sys.rst index 71f9999464ab52..f8f727f4a23410 100644 --- a/Doc/library/sys.rst +++ b/Doc/library/sys.rst @@ -1933,12 +1933,21 @@ always available. Unless explicitly noted otherwise, all variables are read-only interpreter is pre-release (alpha, beta, or release candidate) then the local and remote interpreters must be the same exact version. - .. audit-event:: remote_debugger_script script_path + .. audit-event:: sys.remote_exec pid script_path + + When the code is executed in the remote process, an + :ref:`auditing event ` ``sys.remote_exec`` is raised with + the *pid* and the path to the script file. + This event is raised in the process that called :func:`sys.remote_exec`. + + .. audit-event:: cpython.remote_debugger_script script_path When the script is executed in the remote process, an :ref:`auditing event ` - ``sys.remote_debugger_script`` is raised + ``cpython.remote_debugger_script`` is raised with the path in the remote process. + This event is raised in the remote process, not the one + that called :func:`sys.remote_exec`. .. availability:: Unix, Windows. .. versionadded:: 3.14 diff --git a/Lib/test/audit-tests.py b/Lib/test/audit-tests.py index 08b638e4b8d524..6884ac0dbe6ff0 100644 --- a/Lib/test/audit-tests.py +++ b/Lib/test/audit-tests.py @@ -643,6 +643,34 @@ def test_assert_unicode(): else: raise RuntimeError("Expected sys.audit(9) to fail.") +def test_sys_remote_exec(): + import tempfile + + pid = os.getpid() + event_pid = -1 + event_script_path = "" + remote_event_script_path = "" + def hook(event, args): + if event not in ["sys.remote_exec", "cpython.remote_debugger_script"]: + return + print(event, args) + match event: + case "sys.remote_exec": + nonlocal event_pid, event_script_path + event_pid = args[0] + event_script_path = args[1] + case "cpython.remote_debugger_script": + nonlocal remote_event_script_path + remote_event_script_path = args[0] + + sys.addaudithook(hook) + with tempfile.NamedTemporaryFile(mode='w+', delete=True) as tmp_file: + tmp_file.write("a = 1+1\n") + tmp_file.flush() + sys.remote_exec(pid, tmp_file.name) + assertEqual(event_pid, pid) + assertEqual(event_script_path, tmp_file.name) + assertEqual(remote_event_script_path, tmp_file.name) if __name__ == "__main__": from test.support import suppress_msvcrt_asserts diff --git a/Lib/test/support/__init__.py b/Lib/test/support/__init__.py index 7ac95f078095cd..8afc9c28182ba5 100644 --- a/Lib/test/support/__init__.py +++ b/Lib/test/support/__init__.py @@ -46,6 +46,7 @@ # sys "MS_WINDOWS", "is_jython", "is_android", "is_emscripten", "is_wasi", "is_apple_mobile", "check_impl_detail", "unix_shell", "setswitchinterval", + "support_remote_exec_only", # os "get_pagesize", # network @@ -3070,6 +3071,27 @@ def is_libssl_fips_mode(): return False # more of a maybe, unless we add this to the _ssl module. return get_fips_mode() != 0 +def _supports_remote_attaching(): + PROCESS_VM_READV_SUPPORTED = False + + try: + from _remote_debugging import PROCESS_VM_READV_SUPPORTED + except ImportError: + pass + + return PROCESS_VM_READV_SUPPORTED + +def _support_remote_exec_only_impl(): + if not sys.is_remote_debug_enabled(): + return unittest.skip("Remote debugging is not enabled") + if sys.platform not in ("darwin", "linux", "win32"): + return unittest.skip("Test only runs on Linux, Windows and macOS") + if sys.platform == "linux" and not _supports_remote_attaching(): + return unittest.skip("Test only runs on Linux with process_vm_readv support") + return _id + +def support_remote_exec_only(test): + return _support_remote_exec_only_impl()(test) class EqualToForwardRef: """Helper to ease use of annotationlib.ForwardRef in tests. diff --git a/Lib/test/test_audit.py b/Lib/test/test_audit.py index 5f9eb381f605d9..077765fcda210a 100644 --- a/Lib/test/test_audit.py +++ b/Lib/test/test_audit.py @@ -322,6 +322,14 @@ def test_assert_unicode(self): if returncode: self.fail(stderr) + @support.support_remote_exec_only + @support.cpython_only + def test_sys_remote_exec(self): + returncode, events, stderr = self.run_python("test_sys_remote_exec") + self.assertTrue(any(["sys.remote_exec" in event for event in events])) + self.assertTrue(any(["cpython.remote_debugger_script" in event for event in events])) + if returncode: + self.fail(stderr) if __name__ == "__main__": unittest.main() diff --git a/Lib/test/test_sys.py b/Lib/test/test_sys.py index 94004b4d45cb78..577a075a1aaa1b 100644 --- a/Lib/test/test_sys.py +++ b/Lib/test/test_sys.py @@ -1942,22 +1942,7 @@ def write(self, s): self.assertEqual(out, b"") self.assertEqual(err, b"") - -def _supports_remote_attaching(): - PROCESS_VM_READV_SUPPORTED = False - - try: - from _remote_debugging import PROCESS_VM_READV_SUPPORTED - except ImportError: - pass - - return PROCESS_VM_READV_SUPPORTED - -@unittest.skipIf(not sys.is_remote_debug_enabled(), "Remote debugging is not enabled") -@unittest.skipIf(sys.platform != "darwin" and sys.platform != "linux" and sys.platform != "win32", - "Test only runs on Linux, Windows and MacOS") -@unittest.skipIf(sys.platform == "linux" and not _supports_remote_attaching(), - "Test only runs on Linux with process_vm_readv support") +@test.support.support_remote_exec_only @test.support.cpython_only class TestRemoteExec(unittest.TestCase): def tearDown(self): @@ -2116,7 +2101,7 @@ def audit_hook(event, arg): returncode, stdout, stderr = self._run_remote_exec_test(script, prologue=prologue) self.assertEqual(returncode, 0) self.assertIn(b"Remote script executed successfully!", stdout) - self.assertIn(b"Audit event: remote_debugger_script, arg: ", stdout) + self.assertIn(b"Audit event: cpython.remote_debugger_script, arg: ", stdout) self.assertEqual(stderr, b"") def test_remote_exec_with_exception(self): diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-06-16-02-31-42.gh-issue-135543.6b0HOF.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-06-16-02-31-42.gh-issue-135543.6b0HOF.rst new file mode 100644 index 00000000000000..6efe2a47bac5d4 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-06-16-02-31-42.gh-issue-135543.6b0HOF.rst @@ -0,0 +1,2 @@ +Emit ``sys.remote_exec`` audit event when :func:`sys.remote_exec` is called +and migrate ``remote_debugger_script`` to ``cpython.remote_debugger_script``. diff --git a/Python/ceval_gil.c b/Python/ceval_gil.c index 6d2383ac7c1c65..57d8f68b000b60 100644 --- a/Python/ceval_gil.c +++ b/Python/ceval_gil.c @@ -1220,7 +1220,7 @@ static inline int run_remote_debugger_source(PyObject *source) // that would be an easy target for a ROP gadget. static inline void run_remote_debugger_script(PyObject *path) { - if (0 != PySys_Audit("remote_debugger_script", "O", path)) { + if (0 != PySys_Audit("cpython.remote_debugger_script", "O", path)) { PyErr_FormatUnraisable( "Audit hook failed for remote debugger script %U", path); return; diff --git a/Python/sysmodule.c b/Python/sysmodule.c index 8692caa238a64d..7c6fd1d0e2400e 100644 --- a/Python/sysmodule.c +++ b/Python/sysmodule.c @@ -2487,6 +2487,11 @@ sys_remote_exec_impl(PyObject *module, int pid, PyObject *script) if (PyUnicode_FSConverter(script, &path) == 0) { return NULL; } + + if (PySys_Audit("sys.remote_exec", "iO", pid, script) < 0) { + return NULL; + } + debugger_script_path = PyBytes_AS_STRING(path); #ifdef MS_WINDOWS PyObject *unicode_path;