Skip to content

gh-135543: emit sys.remote_exec audit event when sys.remote_exec has been called #135544

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 24 commits into from
Jun 19, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions Doc/library/sys.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 <auditing>` ``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 <auditing>`
``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
Expand Down
28 changes: 28 additions & 0 deletions Lib/test/audit-tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
22 changes: 22 additions & 0 deletions Lib/test/support/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -3069,6 +3070,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.
Expand Down
8 changes: 8 additions & 0 deletions Lib/test/test_audit.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
19 changes: 2 additions & 17 deletions Lib/test/test_sys.py
Original file line number Diff line number Diff line change
Expand Up @@ -1943,22 +1943,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):
Expand Down Expand Up @@ -2117,7 +2102,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):
Expand Down
Original file line number Diff line number Diff line change
@@ -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``.
2 changes: 1 addition & 1 deletion Python/ceval_gil.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
5 changes: 5 additions & 0 deletions Python/sysmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -2488,6 +2488,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;
Expand Down
Loading