From 3e6245da1f7a93c6efc0db4e0195daedee806940 Mon Sep 17 00:00:00 2001 From: ShaharNaveh <50263213+ShaharNaveh@users.noreply.github.com> Date: Sat, 23 May 2026 15:48:57 +0300 Subject: [PATCH 01/16] Update tests --- Lib/test/audit-tests.py | 681 ++++++++++++++++++++++++++++++++++++++++ Lib/test/test_audit.py | 19 +- 2 files changed, 699 insertions(+), 1 deletion(-) create mode 100644 Lib/test/audit-tests.py diff --git a/Lib/test/audit-tests.py b/Lib/test/audit-tests.py new file mode 100644 index 00000000000..6884ac0dbe6 --- /dev/null +++ b/Lib/test/audit-tests.py @@ -0,0 +1,681 @@ +"""This script contains the actual auditing tests. + +It should not be imported directly, but should be run by the test_audit +module with arguments identifying each test. + +""" + +import contextlib +import os +import sys + + +class TestHook: + """Used in standard hook tests to collect any logged events. + + Should be used in a with block to ensure that it has no impact + after the test completes. + """ + + def __init__(self, raise_on_events=None, exc_type=RuntimeError): + self.raise_on_events = raise_on_events or () + self.exc_type = exc_type + self.seen = [] + self.closed = False + + def __enter__(self, *a): + sys.addaudithook(self) + return self + + def __exit__(self, *a): + self.close() + + def close(self): + self.closed = True + + @property + def seen_events(self): + return [i[0] for i in self.seen] + + def __call__(self, event, args): + if self.closed: + return + self.seen.append((event, args)) + if event in self.raise_on_events: + raise self.exc_type("saw event " + event) + + +# Simple helpers, since we are not in unittest here +def assertEqual(x, y): + if x != y: + raise AssertionError(f"{x!r} should equal {y!r}") + + +def assertIn(el, series): + if el not in series: + raise AssertionError(f"{el!r} should be in {series!r}") + + +def assertNotIn(el, series): + if el in series: + raise AssertionError(f"{el!r} should not be in {series!r}") + + +def assertSequenceEqual(x, y): + if len(x) != len(y): + raise AssertionError(f"{x!r} should equal {y!r}") + if any(ix != iy for ix, iy in zip(x, y)): + raise AssertionError(f"{x!r} should equal {y!r}") + + +@contextlib.contextmanager +def assertRaises(ex_type): + try: + yield + assert False, f"expected {ex_type}" + except BaseException as ex: + if isinstance(ex, AssertionError): + raise + assert type(ex) is ex_type, f"{ex} should be {ex_type}" + + +def test_basic(): + with TestHook() as hook: + sys.audit("test_event", 1, 2, 3) + assertEqual(hook.seen[0][0], "test_event") + assertEqual(hook.seen[0][1], (1, 2, 3)) + + +def test_block_add_hook(): + # Raising an exception should prevent a new hook from being added, + # but will not propagate out. + with TestHook(raise_on_events="sys.addaudithook") as hook1: + with TestHook() as hook2: + sys.audit("test_event") + assertIn("test_event", hook1.seen_events) + assertNotIn("test_event", hook2.seen_events) + + +def test_block_add_hook_baseexception(): + # Raising BaseException will propagate out when adding a hook + with assertRaises(BaseException): + with TestHook( + raise_on_events="sys.addaudithook", exc_type=BaseException + ) as hook1: + # Adding this next hook should raise BaseException + with TestHook() as hook2: + pass + + +def test_marshal(): + import marshal + o = ("a", "b", "c", 1, 2, 3) + payload = marshal.dumps(o) + + with TestHook() as hook: + assertEqual(o, marshal.loads(marshal.dumps(o))) + + try: + with open("test-marshal.bin", "wb") as f: + marshal.dump(o, f) + with open("test-marshal.bin", "rb") as f: + assertEqual(o, marshal.load(f)) + finally: + os.unlink("test-marshal.bin") + + actual = [(a[0], a[1]) for e, a in hook.seen if e == "marshal.dumps"] + assertSequenceEqual(actual, [(o, marshal.version)] * 2) + + actual = [a[0] for e, a in hook.seen if e == "marshal.loads"] + assertSequenceEqual(actual, [payload]) + + actual = [e for e, a in hook.seen if e == "marshal.load"] + assertSequenceEqual(actual, ["marshal.load"]) + + +def test_pickle(): + import pickle + + class PicklePrint: + def __reduce_ex__(self, p): + return str, ("Pwned!",) + + payload_1 = pickle.dumps(PicklePrint()) + payload_2 = pickle.dumps(("a", "b", "c", 1, 2, 3)) + + # Before we add the hook, ensure our malicious pickle loads + assertEqual("Pwned!", pickle.loads(payload_1)) + + with TestHook(raise_on_events="pickle.find_class") as hook: + with assertRaises(RuntimeError): + # With the hook enabled, loading globals is not allowed + pickle.loads(payload_1) + # pickles with no globals are okay + pickle.loads(payload_2) + + +def test_monkeypatch(): + class A: + pass + + class B: + pass + + class C(A): + pass + + a = A() + + with TestHook() as hook: + # Catch name changes + C.__name__ = "X" + # Catch type changes + C.__bases__ = (B,) + # Ensure bypassing __setattr__ is still caught + type.__dict__["__bases__"].__set__(C, (B,)) + # Catch attribute replacement + C.__init__ = B.__init__ + # Catch attribute addition + C.new_attr = 123 + # Catch class changes + a.__class__ = B + + actual = [(a[0], a[1]) for e, a in hook.seen if e == "object.__setattr__"] + assertSequenceEqual( + [(C, "__name__"), (C, "__bases__"), (C, "__bases__"), (a, "__class__")], actual + ) + + +def test_open(testfn): + # SSLContext.load_dh_params uses Py_fopen() rather than normal open() + try: + import ssl + + load_dh_params = ssl.create_default_context().load_dh_params + except ImportError: + load_dh_params = None + + try: + import readline + except ImportError: + readline = None + + def rl(name): + if readline: + return getattr(readline, name, None) + else: + return None + + # Try a range of "open" functions. + # All of them should fail + with TestHook(raise_on_events={"open"}) as hook: + for fn, *args in [ + (open, testfn, "r"), + (open, sys.executable, "rb"), + (open, 3, "wb"), + (open, testfn, "w", -1, None, None, None, False, lambda *a: 1), + (load_dh_params, testfn), + (rl("read_history_file"), testfn), + (rl("read_history_file"), None), + (rl("write_history_file"), testfn), + (rl("write_history_file"), None), + (rl("append_history_file"), 0, testfn), + (rl("append_history_file"), 0, None), + (rl("read_init_file"), testfn), + (rl("read_init_file"), None), + ]: + if not fn: + continue + with assertRaises(RuntimeError): + try: + fn(*args) + except NotImplementedError: + if fn == load_dh_params: + # Not callable in some builds + load_dh_params = None + raise RuntimeError + else: + raise + + actual_mode = [(a[0], a[1]) for e, a in hook.seen if e == "open" and a[1]] + actual_flag = [(a[0], a[2]) for e, a in hook.seen if e == "open" and not a[1]] + assertSequenceEqual( + [ + i + for i in [ + (testfn, "r"), + (sys.executable, "r"), + (3, "w"), + (testfn, "w"), + (testfn, "rb") if load_dh_params else None, + (testfn, "r") if readline else None, + ("~/.history", "r") if readline else None, + (testfn, "w") if readline else None, + ("~/.history", "w") if readline else None, + (testfn, "a") if rl("append_history_file") else None, + ("~/.history", "a") if rl("append_history_file") else None, + (testfn, "r") if readline else None, + ("", "r") if readline else None, + ] + if i is not None + ], + actual_mode, + ) + assertSequenceEqual([], actual_flag) + + +def test_cantrace(): + traced = [] + + def trace(frame, event, *args): + if frame.f_code == TestHook.__call__.__code__: + traced.append(event) + + old = sys.settrace(trace) + try: + with TestHook() as hook: + # No traced call + eval("1") + + # No traced call + hook.__cantrace__ = False + eval("2") + + # One traced call + hook.__cantrace__ = True + eval("3") + + # Two traced calls (writing to private member, eval) + hook.__cantrace__ = 1 + eval("4") + + # One traced call (writing to private member) + hook.__cantrace__ = 0 + finally: + sys.settrace(old) + + assertSequenceEqual(["call"] * 4, traced) + + +def test_mmap(): + import mmap + + with TestHook() as hook: + mmap.mmap(-1, 8) + assertEqual(hook.seen[0][1][:2], (-1, 8)) + + +def test_ctypes_call_function(): + import ctypes + import _ctypes + + with TestHook() as hook: + _ctypes.call_function(ctypes._memmove_addr, (0, 0, 0)) + assert ("ctypes.call_function", (ctypes._memmove_addr, (0, 0, 0))) in hook.seen, f"{ctypes._memmove_addr=} {hook.seen=}" + + ctypes.CFUNCTYPE(ctypes.c_voidp)(ctypes._memset_addr)(1, 0, 0) + assert ("ctypes.call_function", (ctypes._memset_addr, (1, 0, 0))) in hook.seen, f"{ctypes._memset_addr=} {hook.seen=}" + + with TestHook() as hook: + ctypes.cast(ctypes.c_voidp(0), ctypes.POINTER(ctypes.c_char)) + assert "ctypes.call_function" in hook.seen_events + + with TestHook() as hook: + ctypes.string_at(id("ctypes.string_at") + 40) + assert "ctypes.call_function" in hook.seen_events + assert "ctypes.string_at" in hook.seen_events + + +def test_posixsubprocess(): + import multiprocessing.util + + exe = b"xxx" + args = [b"yyy", b"zzz"] + with TestHook() as hook: + multiprocessing.util.spawnv_passfds(exe, args, ()) + assert ("_posixsubprocess.fork_exec", ([exe], args, None)) in hook.seen + + +def test_excepthook(): + def excepthook(exc_type, exc_value, exc_tb): + if exc_type is not RuntimeError: + sys.__excepthook__(exc_type, exc_value, exc_tb) + + def hook(event, args): + if event == "sys.excepthook": + if not isinstance(args[2], args[1]): + raise TypeError(f"Expected isinstance({args[2]!r}, " f"{args[1]!r})") + if args[0] != excepthook: + raise ValueError(f"Expected {args[0]} == {excepthook}") + print(event, repr(args[2])) + + sys.addaudithook(hook) + sys.excepthook = excepthook + raise RuntimeError("fatal-error") + + +def test_unraisablehook(): + from _testcapi import err_formatunraisable + + def unraisablehook(hookargs): + pass + + def hook(event, args): + if event == "sys.unraisablehook": + if args[0] != unraisablehook: + raise ValueError(f"Expected {args[0]} == {unraisablehook}") + print(event, repr(args[1].exc_value), args[1].err_msg) + + sys.addaudithook(hook) + sys.unraisablehook = unraisablehook + err_formatunraisable(RuntimeError("nonfatal-error"), + "Exception ignored for audit hook test") + + +def test_winreg(): + from winreg import OpenKey, EnumKey, CloseKey, HKEY_LOCAL_MACHINE + + def hook(event, args): + if not event.startswith("winreg."): + return + print(event, *args) + + sys.addaudithook(hook) + + k = OpenKey(HKEY_LOCAL_MACHINE, "Software") + EnumKey(k, 0) + try: + EnumKey(k, 10000) + except OSError: + pass + else: + raise RuntimeError("Expected EnumKey(HKLM, 10000) to fail") + + kv = k.Detach() + CloseKey(kv) + + +def test_socket(): + import socket + + def hook(event, args): + if event.startswith("socket."): + print(event, *args) + + sys.addaudithook(hook) + + socket.gethostname() + + # Don't care if this fails, we just want the audit message + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + try: + # Don't care if this fails, we just want the audit message + sock.bind(('127.0.0.1', 8080)) + except Exception: + pass + finally: + sock.close() + + +def test_gc(): + import gc + + def hook(event, args): + if event.startswith("gc."): + print(event, *args) + + sys.addaudithook(hook) + + gc.get_objects(generation=1) + + x = object() + y = [x] + + gc.get_referrers(x) + gc.get_referents(y) + + +def test_http_client(): + import http.client + + def hook(event, args): + if event.startswith("http.client."): + print(event, *args[1:]) + + sys.addaudithook(hook) + + conn = http.client.HTTPConnection('www.python.org') + try: + conn.request('GET', '/') + except OSError: + print('http.client.send', '[cannot send]') + finally: + conn.close() + + +def test_sqlite3(): + import sqlite3 + + def hook(event, *args): + if event.startswith("sqlite3."): + print(event, *args) + + sys.addaudithook(hook) + cx1 = sqlite3.connect(":memory:") + cx2 = sqlite3.Connection(":memory:") + + # Configured without --enable-loadable-sqlite-extensions + try: + if hasattr(sqlite3.Connection, "enable_load_extension"): + cx1.enable_load_extension(False) + try: + cx1.load_extension("test") + except sqlite3.OperationalError: + pass + else: + raise RuntimeError("Expected sqlite3.load_extension to fail") + finally: + cx1.close() + cx2.close() + +def test_sys_getframe(): + import sys + + def hook(event, args): + if event.startswith("sys."): + print(event, args[0].f_code.co_name) + + sys.addaudithook(hook) + sys._getframe() + + +def test_sys_getframemodulename(): + import sys + + def hook(event, args): + if event.startswith("sys."): + print(event, *args) + + sys.addaudithook(hook) + sys._getframemodulename() + + +def test_threading(): + import _thread + + def hook(event, args): + if event.startswith(("_thread.", "cpython.PyThreadState", "test.")): + print(event, args) + + sys.addaudithook(hook) + + lock = _thread.allocate_lock() + lock.acquire() + + class test_func: + def __repr__(self): return "" + def __call__(self): + sys.audit("test.test_func") + lock.release() + + i = _thread.start_new_thread(test_func(), ()) + lock.acquire() + + handle = _thread.start_joinable_thread(test_func()) + handle.join() + + +def test_threading_abort(): + # Ensures that aborting PyThreadState_New raises the correct exception + import _thread + + class ThreadNewAbortError(Exception): + pass + + def hook(event, args): + if event == "cpython.PyThreadState_New": + raise ThreadNewAbortError() + + sys.addaudithook(hook) + + try: + _thread.start_new_thread(lambda: None, ()) + except ThreadNewAbortError: + # Other exceptions are raised and the test will fail + pass + + +def test_wmi_exec_query(): + import _wmi + + def hook(event, args): + if event.startswith("_wmi."): + print(event, args[0]) + + sys.addaudithook(hook) + try: + _wmi.exec_query("SELECT * FROM Win32_OperatingSystem") + except WindowsError as e: + # gh-112278: WMI may be slow response when first called, but we still + # get the audit event, so just ignore the timeout + if e.winerror != 258: + raise + +def test_syslog(): + import syslog + + def hook(event, args): + if event.startswith("syslog."): + print(event, *args) + + sys.addaudithook(hook) + syslog.openlog('python') + syslog.syslog('test') + syslog.setlogmask(syslog.LOG_DEBUG) + syslog.closelog() + # implicit open + syslog.syslog('test2') + # open with default ident + syslog.openlog(logoption=syslog.LOG_NDELAY, facility=syslog.LOG_LOCAL0) + sys.argv = None + syslog.openlog() + syslog.closelog() + + +def test_not_in_gc(): + import gc + + hook = lambda *a: None + sys.addaudithook(hook) + + for o in gc.get_objects(): + if isinstance(o, list): + assert hook not in o + + +def test_time(mode): + import time + + def hook(event, args): + if event.startswith("time."): + if mode == 'print': + print(event, *args) + elif mode == 'fail': + raise AssertionError('hook failed') + sys.addaudithook(hook) + + time.sleep(0) + time.sleep(0.0625) # 1/16, a small exact float + try: + time.sleep(-1) + except ValueError: + pass + +def test_sys_monitoring_register_callback(): + import sys + + def hook(event, args): + if event.startswith("sys.monitoring"): + print(event, args) + + sys.addaudithook(hook) + sys.monitoring.register_callback(1, 1, None) + + +def test_winapi_createnamedpipe(pipe_name): + import _winapi + + def hook(event, args): + if event == "_winapi.CreateNamedPipe": + print(event, args) + + sys.addaudithook(hook) + _winapi.CreateNamedPipe(pipe_name, _winapi.PIPE_ACCESS_DUPLEX, 8, 2, 0, 0, 0, 0) + + +def test_assert_unicode(): + import sys + sys.addaudithook(lambda *args: None) + try: + sys.audit(9) + except TypeError: + pass + 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 + + suppress_msvcrt_asserts() + + test = sys.argv[1] + globals()[test](*sys.argv[2:]) diff --git a/Lib/test/test_audit.py b/Lib/test/test_audit.py index ddd9f951143..077765fcda2 100644 --- a/Lib/test/test_audit.py +++ b/Lib/test/test_audit.py @@ -23,6 +23,7 @@ def run_test_in_subprocess(self, *args): with subprocess.Popen( [sys.executable, "-X utf8", AUDIT_TESTS_PY, *args], encoding="utf-8", + errors="backslashreplace", stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) as p: @@ -79,6 +80,14 @@ def test_cantrace(self): def test_mmap(self): self.do_test("test_mmap") + def test_ctypes_call_function(self): + import_helper.import_module("ctypes") + self.do_test("test_ctypes_call_function") + + def test_posixsubprocess(self): + import_helper.import_module("_posixsubprocess") + self.do_test("test_posixsubprocess") + def test_excepthook(self): returncode, events, stderr = self.run_python("test_excepthook") if not returncode: @@ -125,7 +134,7 @@ def test_socket(self): self.assertEqual(events[0][0], "socket.gethostname") self.assertEqual(events[1][0], "socket.__new__") self.assertEqual(events[2][0], "socket.bind") - self.assertTrue(events[2][2].endswith("('127.0.0.1', 8080)")) + self.assertEndsWith(events[2][2], "('127.0.0.1', 8080)") def test_gc(self): returncode, events, stderr = self.run_python("test_gc") @@ -313,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() From cb9eac242f66bfe1480de7c9f9c4f544c104484d Mon Sep 17 00:00:00 2001 From: ShaharNaveh <50263213+ShaharNaveh@users.noreply.github.com> Date: Sat, 23 May 2026 15:49:14 +0300 Subject: [PATCH 02/16] Add basic audit support --- crates/vm/src/stdlib/sys.rs | 60 +++++++++++++++++++++++++++++++++---- crates/vm/src/vm/mod.rs | 2 ++ crates/vm/src/vm/thread.rs | 1 + 3 files changed, 58 insertions(+), 5 deletions(-) diff --git a/crates/vm/src/stdlib/sys.rs b/crates/vm/src/stdlib/sys.rs index 44e692f1783..fe64cfbdffb 100644 --- a/crates/vm/src/stdlib/sys.rs +++ b/crates/vm/src/stdlib/sys.rs @@ -754,11 +754,6 @@ pub mod sys { Ok(()) } - #[pyfunction] - fn audit(_args: FuncArgs) { - // TODO: sys.audit implementation - } - #[pyfunction] const fn _is_gil_enabled() -> bool { false // RustPython has no GIL (like free-threaded Python) @@ -1720,6 +1715,61 @@ pub mod sys { #[pyclass(with(PyStructSequence))] impl PyUnraisableHookArgs {} + + pub(crate) fn run_audit_hooks( + event: PyStrRef, + args: &PyObjectRef, + vm: &VirtualMachine, + ) -> PyResult<()> { + let hooks = vm.audit_hooks.borrow().clone(); + + if hooks.is_empty() { + return Ok(()); + } + + for hook in hooks { + hook.call((event.clone(), args.clone()), vm)?; + } + + Ok(()) + } + + #[pyfunction] + fn audit(event: PyStrRef, args: PosArgs, vm: &VirtualMachine) -> PyResult<()> { + if vm.audit_hooks.borrow().is_empty() { + return Ok(()); + } + + let args_tup = vm.ctx.new_tuple(args.into_vec()).into(); + run_audit_hooks(event, &args_tup, vm) + } + + #[pyfunction] + fn addaudithook(hook: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> { + let hooks = vm.audit_hooks.borrow().clone(); + + if hooks.is_empty() { + vm.audit_hooks.borrow_mut().push(hook); + return Ok(()); + } + + let args: PyObjectRef = vm.ctx.new_tuple(vec![]).into(); + let event: PyObjectRef = vm.ctx.new_str("sys.addaudithook").into(); + + for existing_hook in hooks { + let Err(exc) = existing_hook.call((event.clone(), args.clone()), vm) else { + continue; + }; + if exc.class().fast_issubclass(vm.ctx.exceptions.runtime_error) { + return Ok(()); + } else { + return Err(exc); + } + } + + vm.audit_hooks.borrow_mut().push(hook); + Ok(()) + } } pub(crate) fn init_module(vm: &VirtualMachine, module: &Py, builtins: &Py) { diff --git a/crates/vm/src/vm/mod.rs b/crates/vm/src/vm/mod.rs index 85416a09129..a99012cd14b 100644 --- a/crates/vm/src/vm/mod.rs +++ b/crates/vm/src/vm/mod.rs @@ -100,6 +100,7 @@ pub struct VirtualMachine { /// Current running asyncio task for this thread pub asyncio_running_task: RefCell>, pub(crate) callable_cache: CallableCache, + pub(crate) audit_hooks: RefCell>, } /// Non-owning frame pointer for the frames stack. @@ -750,6 +751,7 @@ impl VirtualMachine { asyncio_running_loop: RefCell::new(None), asyncio_running_task: RefCell::new(None), callable_cache: CallableCache::default(), + audit_hooks: RefCell::new(vec![]), }; if vm.state.hash_secret.hash_str("") diff --git a/crates/vm/src/vm/thread.rs b/crates/vm/src/vm/thread.rs index a81396bff34..f89cf818d2c 100644 --- a/crates/vm/src/vm/thread.rs +++ b/crates/vm/src/vm/thread.rs @@ -730,6 +730,7 @@ impl VirtualMachine { asyncio_running_loop: RefCell::new(None), asyncio_running_task: RefCell::new(None), callable_cache: self.callable_cache.clone(), + audit_hooks: RefCell::new(vec![]), }; ThreadedVirtualMachine { vm } } From 0a14b4efb879f934e3f898216b42adaa08e55ec1 Mon Sep 17 00:00:00 2001 From: ShaharNaveh <50263213+ShaharNaveh@users.noreply.github.com> Date: Sat, 23 May 2026 16:27:07 +0300 Subject: [PATCH 03/16] Add audit for `time.sleep` --- crates/vm/src/stdlib/time.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/vm/src/stdlib/time.rs b/crates/vm/src/stdlib/time.rs index 93d04341d26..a56dffe08c7 100644 --- a/crates/vm/src/stdlib/time.rs +++ b/crates/vm/src/stdlib/time.rs @@ -74,6 +74,10 @@ mod decl { #[pyfunction] fn sleep(seconds: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> { + if let Ok(audit) = vm.sys_module.get_attr("audit", vm) { + audit.call((vm.ctx.new_str("time.sleep"), seconds.clone()), vm)?; + } + let seconds_type_name = seconds.class().name().to_owned(); let dur = seconds.try_into_value::(vm).map_err(|e| { if e.class().is(vm.ctx.exceptions.value_error) From c3bf39e72e2b4ef80c634a3a959b727177e9a158 Mon Sep 17 00:00:00 2001 From: ShaharNaveh <50263213+ShaharNaveh@users.noreply.github.com> Date: Sat, 23 May 2026 16:58:11 +0300 Subject: [PATCH 04/16] Add some for `socket` --- crates/stdlib/src/socket.rs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/crates/stdlib/src/socket.rs b/crates/stdlib/src/socket.rs index 6aff5d452c4..29e43118655 100644 --- a/crates/stdlib/src/socket.rs +++ b/crates/stdlib/src/socket.rs @@ -1426,6 +1426,13 @@ mod _socket { let mut socket_kind = args.r#type.unwrap_or(-1); let mut proto = args.proto.unwrap_or(-1); + if let Ok(audit) = vm.sys_module.get_attr("audit", vm) { + audit.call( + (vm.ctx.new_str("socket.__new__"), family, socket_kind, proto), + vm, + )?; + } + let fileno = args.fileno; let sock; @@ -1555,6 +1562,18 @@ mod _socket { #[pymethod] fn bind(&self, address: PyObjectRef, vm: &VirtualMachine) -> Result<(), IoOrPyException> { let sock_addr = self.extract_address(address, "bind", vm)?; + + if let Some(addr) = sock_addr.as_socket() { + if let Ok(audit) = vm.sys_module.get_attr("audit", vm) { + let (ip, port) = match addr { + SocketAddr::V4(addr) => (addr.ip().to_string(), addr.port()), + SocketAddr::V6(addr) => (addr.ip().to_string(), addr.port()), + }; + + audit.call((vm.ctx.new_str("socket.bind"), (ip, port)), vm)?; + } + } + Ok(self.sock()?.bind(&sock_addr)?) } @@ -2300,6 +2319,10 @@ mod _socket { #[pyfunction] fn gethostname(vm: &VirtualMachine) -> PyResult { + if let Ok(audit) = vm.sys_module.get_attr("audit", vm) { + audit.call((vm.ctx.new_str("socket.gethostname"),), vm)?; + } + gethostname::gethostname() .into_string() .map(|hostname| vm.ctx.new_str(hostname)) From d7bd5b6ad95df7f5dbe8bbd90929ce7c0c7ee28b Mon Sep 17 00:00:00 2001 From: ShaharNaveh <50263213+ShaharNaveh@users.noreply.github.com> Date: Sat, 23 May 2026 17:44:51 +0300 Subject: [PATCH 05/16] Some syslog --- crates/stdlib/src/syslog.rs | 39 ++++++++++++++++++++++++++++++++----- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/crates/stdlib/src/syslog.rs b/crates/stdlib/src/syslog.rs index fab82f14f56..fcdfdea752b 100644 --- a/crates/stdlib/src/syslog.rs +++ b/crates/stdlib/src/syslog.rs @@ -53,12 +53,29 @@ mod syslog { fn openlog(args: OpenLogArgs, vm: &VirtualMachine) -> PyResult<()> { let logoption = args.logoption.unwrap_or(0); let facility = args.facility.unwrap_or(LOG_USER); - let ident = match args.ident.flatten() { + let ident = match args.ident.clone().flatten() { Some(args) => Some(args.to_cstring(vm)?), None => get_argv(vm).map(|argv| argv.to_cstring(vm)).transpose()?, } .map(|ident| ident.into_boxed_c_str()); + if let Ok(audit) = vm.sys_module.get_attr("audit", vm) { + let audit_ident: PyObjectRef = args.ident.flatten().map_or_else( + || get_argv(vm).map_or_else(|| vm.ctx.none(), Into::into), + Into::into, + ); + + audit.call( + ( + vm.ctx.new_str("syslog.openlog"), + audit_ident, + logoption, + facility, + ), + vm, + )?; + } + host_syslog::openlog(ident, logoption, facility); Ok(()) } @@ -78,6 +95,10 @@ mod syslog { None => (LOG_INFO, args.priority.try_into_value(vm)?), }; + if let Ok(audit) = vm.sys_module.get_attr("audit", vm) { + audit.call((vm.ctx.new_str("syslog.syslog"), priority, msg.clone()), vm)?; + } + if !host_syslog::is_open() { openlog(OpenLogArgs::default(), vm)?; } @@ -88,13 +109,21 @@ mod syslog { } #[pyfunction] - fn closelog() { - host_syslog::closelog(); + fn closelog(vm: &VirtualMachine) -> PyResult<()> { + if let Ok(audit) = vm.sys_module.get_attr("audit", vm) { + audit.call((vm.ctx.new_str("syslog.closelog"),), vm)?; + } + + Ok(host_syslog::closelog()) } #[pyfunction] - fn setlogmask(maskpri: i32) -> i32 { - host_syslog::setlogmask(maskpri) + fn setlogmask(maskpri: i32, vm: &VirtualMachine) -> PyResult { + if let Ok(audit) = vm.sys_module.get_attr("audit", vm) { + audit.call((vm.ctx.new_str("syslog.setlogmask"), maskpri), vm)?; + } + + Ok(host_syslog::setlogmask(maskpri)) } #[inline] From efca763448a9536af2c52fa1146ff65f5b1d367f Mon Sep 17 00:00:00 2001 From: ShaharNaveh <50263213+ShaharNaveh@users.noreply.github.com> Date: Sat, 23 May 2026 18:15:12 +0300 Subject: [PATCH 06/16] Some sys related audits --- crates/vm/src/stdlib/sys.rs | 37 ++++++++++++++++++++++++++----------- 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/crates/vm/src/stdlib/sys.rs b/crates/vm/src/stdlib/sys.rs index fe64cfbdffb..b6d1e462c65 100644 --- a/crates/vm/src/stdlib/sys.rs +++ b/crates/vm/src/stdlib/sys.rs @@ -967,25 +967,40 @@ pub mod sys { #[pyfunction] fn _getframe(offset: OptionalArg, vm: &VirtualMachine) -> PyResult { let offset = offset.into_option().unwrap_or(0); - let frames = vm.frames.borrow(); - if offset >= frames.len() { - return Err(vm.new_value_error("call stack is not deep enough")); + let frame_ref = { + let frames = vm.frames.borrow(); + if offset >= frames.len() { + return Err(vm.new_value_error("call stack is not deep enough")); + } + + let idx = frames.len() - offset - 1; + // SAFETY: the FrameRef is alive on the call stack while it's in the Vec + let py: &crate::Py = unsafe { frames[idx].as_ref() }; + py.to_owned() + }; + + if let Ok(audit) = vm.sys_module.get_attr("audit", vm) { + audit.call((vm.ctx.new_str("sys._getframe"), frame_ref.to_owned()), vm)?; } - let idx = frames.len() - offset - 1; - // SAFETY: the FrameRef is alive on the call stack while it's in the Vec - let py: &crate::Py = unsafe { frames[idx].as_ref() }; - Ok(py.to_owned()) + + Ok(frame_ref) } #[pyfunction] - fn _getframemodulename(depth: OptionalArg, vm: &VirtualMachine) -> PyObjectRef { + fn _getframemodulename( + depth: OptionalArg, + vm: &VirtualMachine, + ) -> PyResult { let depth = depth.into_option().unwrap_or(0); + if let Ok(audit) = vm.sys_module.get_attr("audit", vm) { + audit.call((vm.ctx.new_str("sys._getframemodulename"), depth), vm)?; + } // Get the frame at the specified depth let func_obj = { let frames = vm.frames.borrow(); if depth >= frames.len() { - return vm.ctx.none(); + return Ok(vm.ctx.none()); } let idx = frames.len() - depth - 1; // SAFETY: the FrameRef is alive on the call stack while it's in the Vec @@ -994,7 +1009,7 @@ pub mod sys { }; // If the frame has a function object, return its __module__ attribute - if let Some(func_obj) = func_obj { + Ok(if let Some(func_obj) = func_obj { func_obj .get_attr(identifier!(vm, __module__), vm) .unwrap_or_else( @@ -1003,7 +1018,7 @@ pub mod sys { ) } else { vm.ctx.none() - } + }) } /// Return a dictionary mapping each thread's identifier to the topmost stack frame From f5d53b01aa484d8d2345511d953ce496c3154005 Mon Sep 17 00:00:00 2001 From: ShaharNaveh <50263213+ShaharNaveh@users.noreply.github.com> Date: Sat, 23 May 2026 18:28:48 +0300 Subject: [PATCH 07/16] some marshal --- crates/vm/src/stdlib/marshal.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/crates/vm/src/stdlib/marshal.rs b/crates/vm/src/stdlib/marshal.rs index 60ecb1792f0..6e0fc4e7f5d 100644 --- a/crates/vm/src/stdlib/marshal.rs +++ b/crates/vm/src/stdlib/marshal.rs @@ -107,6 +107,14 @@ mod decl { _version, } = args; let version = _version.unwrap_or(marshal::FORMAT_VERSION as i32); + + if let Ok(audit) = vm.sys_module.get_attr("audit", vm) { + audit.call( + (vm.ctx.new_str("marshal.dumps"), value.clone(), version), + vm, + )?; + } + if !allow_code { check_no_code(&value, vm)?; } From 75b9b8942e77892d331e29852a42c500e60ea22b Mon Sep 17 00:00:00 2001 From: ShaharNaveh <50263213+ShaharNaveh@users.noreply.github.com> Date: Sat, 23 May 2026 18:35:07 +0300 Subject: [PATCH 08/16] monitoring callback --- crates/vm/src/stdlib/sys/monitoring.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/crates/vm/src/stdlib/sys/monitoring.rs b/crates/vm/src/stdlib/sys/monitoring.rs index bf113c7937f..6e61692507d 100644 --- a/crates/vm/src/stdlib/sys/monitoring.rs +++ b/crates/vm/src/stdlib/sys/monitoring.rs @@ -598,6 +598,16 @@ fn register_callback( let tool = check_valid_tool(tool_id, vm)?; let event_id = parse_single_event(event, vm)?; + if let Ok(audit) = vm.sys_module.get_attr("audit", vm) { + audit.call( + ( + vm.ctx.new_str("sys.monitoring.register_callback"), + func.clone(), + ), + vm, + )?; + } + let mut state = vm.state.monitoring.lock(); let prev = state .callbacks From 517d056c2b28ff61be36bf6dab3cda90d90a89c7 Mon Sep 17 00:00:00 2001 From: ShaharNaveh <50263213+ShaharNaveh@users.noreply.github.com> Date: Sat, 23 May 2026 18:46:14 +0300 Subject: [PATCH 09/16] Mark failing tests --- Lib/test/test_audit.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Lib/test/test_audit.py b/Lib/test/test_audit.py index 077765fcda2..7ef334d96f9 100644 --- a/Lib/test/test_audit.py +++ b/Lib/test/test_audit.py @@ -58,6 +58,7 @@ def test_block_add_hook(self): def test_block_add_hook_baseexception(self): self.do_test("test_block_add_hook_baseexception") + @unittest.expectedFailure # TODO: RUSTPYTHON def test_marshal(self): import_helper.import_module("marshal") @@ -68,26 +69,33 @@ def test_pickle(self): self.do_test("test_pickle") + @unittest.expectedFailure # TODO: RUSTPYTHON def test_monkeypatch(self): self.do_test("test_monkeypatch") + @unittest.expectedFailure # TODO: RUSTPYTHON def test_open(self): self.do_test("test_open", os_helper.TESTFN) + @unittest.expectedFailure # TODO: RUSTPYTHON def test_cantrace(self): self.do_test("test_cantrace") + @unittest.expectedFailure # TODO: RUSTPYTHON def test_mmap(self): self.do_test("test_mmap") + @unittest.expectedFailure # TODO: RUSTPYTHON def test_ctypes_call_function(self): import_helper.import_module("ctypes") self.do_test("test_ctypes_call_function") + @unittest.expectedFailure # TODO: RUSTPYTHON def test_posixsubprocess(self): import_helper.import_module("_posixsubprocess") self.do_test("test_posixsubprocess") + @unittest.expectedFailure # TODO: RUSTPYTHON def test_excepthook(self): returncode, events, stderr = self.run_python("test_excepthook") if not returncode: @@ -109,6 +117,7 @@ def test_unraisablehook(self): "RuntimeError('nonfatal-error') Exception ignored for audit hook test", ) + @unittest.expectedFailure # TODO: RUSTPYTHON def test_winreg(self): import_helper.import_module("winreg") returncode, events, stderr = self.run_python("test_winreg") @@ -136,6 +145,7 @@ def test_socket(self): self.assertEqual(events[2][0], "socket.bind") self.assertEndsWith(events[2][2], "('127.0.0.1', 8080)") + @unittest.expectedFailure # TODO: RUSTPYTHON def test_gc(self): returncode, events, stderr = self.run_python("test_gc") if returncode: @@ -165,6 +175,7 @@ def test_http(self): self.assertIn('HTTP', events[1][2]) + @unittest.expectedFailure # TODO: RUSTPYTHON def test_sqlite3(self): sqlite3 = import_helper.import_module("sqlite3") returncode, events, stderr = self.run_python("test_sqlite3") @@ -209,6 +220,7 @@ def test_sys_getframemodulename(self): self.assertEqual(actual, expected) + @unittest.expectedFailure # TODO: RUSTPYTHON def test_threading(self): returncode, events, stderr = self.run_python("test_threading") if returncode: @@ -227,6 +239,7 @@ def test_threading(self): self.assertEqual(actual, expected) + @unittest.expectedFailure # TODO: RUSTPYTHON def test_wmi_exec_query(self): import_helper.import_module("_wmi") returncode, events, stderr = self.run_python("test_wmi_exec_query") @@ -240,6 +253,7 @@ def test_wmi_exec_query(self): self.assertEqual(actual, expected) + @unittest.expectedFailure # TODO: RUSTPYTHON def test_syslog(self): syslog = import_helper.import_module("syslog") From 658e8d89ffd590349f6b267bfd59f2765c315d76 Mon Sep 17 00:00:00 2001 From: ShaharNaveh <50263213+ShaharNaveh@users.noreply.github.com> Date: Sat, 23 May 2026 18:47:56 +0300 Subject: [PATCH 10/16] clippy --- crates/vm/src/stdlib/sys.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/vm/src/stdlib/sys.rs b/crates/vm/src/stdlib/sys.rs index b6d1e462c65..aee77d7dda3 100644 --- a/crates/vm/src/stdlib/sys.rs +++ b/crates/vm/src/stdlib/sys.rs @@ -1777,9 +1777,8 @@ pub mod sys { }; if exc.class().fast_issubclass(vm.ctx.exceptions.runtime_error) { return Ok(()); - } else { - return Err(exc); } + return Err(exc); } vm.audit_hooks.borrow_mut().push(hook); From ab48fca4851b90d890f20d6dbb47edfe96154856 Mon Sep 17 00:00:00 2001 From: ShaharNaveh <50263213+ShaharNaveh@users.noreply.github.com> Date: Sat, 23 May 2026 19:45:45 +0300 Subject: [PATCH 11/16] Clippy --- crates/stdlib/src/socket.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/crates/stdlib/src/socket.rs b/crates/stdlib/src/socket.rs index 29e43118655..366de2ecc21 100644 --- a/crates/stdlib/src/socket.rs +++ b/crates/stdlib/src/socket.rs @@ -1563,15 +1563,15 @@ mod _socket { fn bind(&self, address: PyObjectRef, vm: &VirtualMachine) -> Result<(), IoOrPyException> { let sock_addr = self.extract_address(address, "bind", vm)?; - if let Some(addr) = sock_addr.as_socket() { - if let Ok(audit) = vm.sys_module.get_attr("audit", vm) { - let (ip, port) = match addr { - SocketAddr::V4(addr) => (addr.ip().to_string(), addr.port()), - SocketAddr::V6(addr) => (addr.ip().to_string(), addr.port()), - }; + if let Some(addr) = sock_addr.as_socket() + && let Ok(audit) = vm.sys_module.get_attr("audit", vm) + { + let (ip, port) = match addr { + SocketAddr::V4(addr) => (addr.ip().to_string(), addr.port()), + SocketAddr::V6(addr) => (addr.ip().to_string(), addr.port()), + }; - audit.call((vm.ctx.new_str("socket.bind"), (ip, port)), vm)?; - } + audit.call((vm.ctx.new_str("socket.bind"), (ip, port)), vm)?; } Ok(self.sock()?.bind(&sock_addr)?) From 3c8c63aa90c1fa6b60f0f76b065da7148db45ec8 Mon Sep 17 00:00:00 2001 From: ShaharNaveh <50263213+ShaharNaveh@users.noreply.github.com> Date: Sun, 24 May 2026 10:20:18 +0300 Subject: [PATCH 12/16] clippy --- crates/stdlib/src/syslog.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/stdlib/src/syslog.rs b/crates/stdlib/src/syslog.rs index fcdfdea752b..52424972a0a 100644 --- a/crates/stdlib/src/syslog.rs +++ b/crates/stdlib/src/syslog.rs @@ -114,7 +114,8 @@ mod syslog { audit.call((vm.ctx.new_str("syslog.closelog"),), vm)?; } - Ok(host_syslog::closelog()) + host_syslog::closelog(); + Ok(()) } #[pyfunction] From b4c228fdc6de61b15ccd041a874ea5d993f9d2bf Mon Sep 17 00:00:00 2001 From: ShaharNaveh <50263213+ShaharNaveh@users.noreply.github.com> Date: Sun, 24 May 2026 11:08:26 +0300 Subject: [PATCH 13/16] mark failing test --- Lib/test/test_audit.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Lib/test/test_audit.py b/Lib/test/test_audit.py index 7ef334d96f9..d01d36ad3db 100644 --- a/Lib/test/test_audit.py +++ b/Lib/test/test_audit.py @@ -315,6 +315,7 @@ def test_sys_monitoring_register_callback(self): self.assertEqual(actual, expected) + @unittest.expectedFailure # TODO: RUSTPYTHON def test_winapi_createnamedpipe(self): winapi = import_helper.import_module("_winapi") From d51345221d73ac2470441c6c44206117dff376f0 Mon Sep 17 00:00:00 2001 From: ShaharNaveh <50263213+ShaharNaveh@users.noreply.github.com> Date: Sun, 24 May 2026 11:12:46 +0300 Subject: [PATCH 14/16] mark more --- Lib/test/test_bdb.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Lib/test/test_bdb.py b/Lib/test/test_bdb.py index f15dae13eb3..590d8166b68 100644 --- a/Lib/test/test_bdb.py +++ b/Lib/test/test_bdb.py @@ -728,6 +728,7 @@ def test_until_in_caller_frame(self): with TracerRun(self) as tracer: tracer.runcall(tfunc_main) + @unittest.skipIf(hasattr(__import__("sys"), "addaudithook"), "TODO: RUSTPYTHON; Currently no conditional tracing toggle") @patch_list(sys.meta_path) def test_skip(self): # Check that tracing is skipped over the import statement in From 8d5821edfd6e378cce1fdf532cc59839e0fa8603 Mon Sep 17 00:00:00 2001 From: ShaharNaveh <50263213+ShaharNaveh@users.noreply.github.com> Date: Sun, 24 May 2026 11:19:31 +0300 Subject: [PATCH 15/16] Update `test_sys_setprofile.py` to 3.14.5 --- Lib/test/test_sys_setprofile.py | 148 +++++++++++++++++++++++++++++--- 1 file changed, 134 insertions(+), 14 deletions(-) diff --git a/Lib/test/test_sys_setprofile.py b/Lib/test/test_sys_setprofile.py index 21a09b51926..a22b728b569 100644 --- a/Lib/test/test_sys_setprofile.py +++ b/Lib/test/test_sys_setprofile.py @@ -30,9 +30,9 @@ def callback(self, frame, event, arg): if (event == "call" or event == "return" or event == "exception"): - self.add_event(event, frame) + self.add_event(event, frame, arg) - def add_event(self, event, frame=None): + def add_event(self, event, frame=None, arg=None): """Add an event to the log.""" if frame is None: frame = sys._getframe(1) @@ -43,7 +43,7 @@ def add_event(self, event, frame=None): frameno = len(self.frames) self.frames.append(frame) - self.events.append((frameno, event, ident(frame))) + self.events.append((frameno, event, ident(frame), arg)) def get_events(self): """Remove calls to add_event().""" @@ -89,11 +89,16 @@ def trace_pass(self, frame): class TestCaseBase(unittest.TestCase): - def check_events(self, callable, expected): + def check_events(self, callable, expected, check_args=False): events = capture_events(callable, self.new_watcher()) - if events != expected: - self.fail("Expected events:\n%s\nReceived events:\n%s" - % (pprint.pformat(expected), pprint.pformat(events))) + if check_args: + if events != expected: + self.fail("Expected events:\n%s\nReceived events:\n%s" + % (pprint.pformat(expected), pprint.pformat(events))) + else: + if [(frameno, event, ident) for frameno, event, ident, arg in events] != expected: + self.fail("Expected events:\n%s\nReceived events:\n%s" + % (pprint.pformat(expected), pprint.pformat(events))) class ProfileHookTestCase(TestCaseBase): @@ -119,7 +124,7 @@ def f(p): def test_caught_exception(self): def f(p): try: 1/0 - except: pass + except ZeroDivisionError: pass f_ident = ident(f) self.check_events(f, [(1, 'call', f_ident), (1, 'return', f_ident), @@ -128,7 +133,7 @@ def f(p): def test_caught_nested_exception(self): def f(p): try: 1/0 - except: pass + except ZeroDivisionError: pass f_ident = ident(f) self.check_events(f, [(1, 'call', f_ident), (1, 'return', f_ident), @@ -151,9 +156,9 @@ def f(p): def g(p): try: f(p) - except: + except ZeroDivisionError: try: f(p) - except: pass + except ZeroDivisionError: pass f_ident = ident(f) g_ident = ident(g) self.check_events(g, [(1, 'call', g_ident), @@ -182,7 +187,7 @@ def g(p): def test_raise_twice(self): def f(p): try: 1/0 - except: 1/0 + except ZeroDivisionError: 1/0 f_ident = ident(f) self.check_events(f, [(1, 'call', f_ident), (1, 'return', f_ident), @@ -191,7 +196,7 @@ def f(p): def test_raise_reraise(self): def f(p): try: 1/0 - except: raise + except ZeroDivisionError: raise f_ident = ident(f) self.check_events(f, [(1, 'call', f_ident), (1, 'return', f_ident), @@ -255,6 +260,23 @@ def g(p): (1, 'return', g_ident), ]) + def test_unfinished_generator(self): + def f(): + for i in range(2): + yield i + def g(p): + next(f()) + + f_ident = ident(f) + g_ident = ident(g) + self.check_events(g, [(1, 'call', g_ident, None), + (2, 'call', f_ident, None), + (2, 'return', f_ident, 0), + (2, 'call', f_ident, None), + (2, 'return', f_ident, None), + (1, 'return', g_ident, None), + ], check_args=True) + def test_stop_iteration(self): def f(): for i in range(2): @@ -300,7 +322,7 @@ def f(p): def test_caught_exception(self): def f(p): try: 1/0 - except: pass + except ZeroDivisionError: pass f_ident = ident(f) self.check_events(f, [(1, 'call', f_ident), (1, 'return', f_ident), @@ -415,5 +437,103 @@ def show_events(callable): pprint.pprint(capture_events(callable)) +class TestEdgeCases(unittest.TestCase): + + def setUp(self): + self.addCleanup(sys.setprofile, sys.getprofile()) + sys.setprofile(None) + + def test_reentrancy(self): + def foo(*args): + ... + + def bar(*args): + ... + + class A: + def __call__(self, *args): + pass + + def __del__(self): + sys.setprofile(bar) + + sys.setprofile(A()) + sys.setprofile(foo) + self.assertEqual(sys.getprofile(), bar) + + def test_same_object(self): + def foo(*args): + ... + + sys.setprofile(foo) + del foo + sys.setprofile(sys.getprofile()) + + def test_profile_after_trace_opcodes(self): + def f(): + ... + + sys._getframe().f_trace_opcodes = True + prev_trace = sys.gettrace() + sys.settrace(lambda *args: None) + f() + sys.settrace(prev_trace) + sys.setprofile(lambda *args: None) + f() + + def test_method_with_c_function(self): + # gh-122029 + # When we have a PyMethodObject whose im_func is a C function, we + # should record both the call and the return. f = classmethod(repr) + # is just a way to create a PyMethodObject with a C function. + class A: + f = classmethod(repr) + events = [] + sys.setprofile(lambda frame, event, args: events.append(event)) + A().f() + sys.setprofile(None) + # The last c_call is the call to sys.setprofile + self.assertEqual(events, ['c_call', 'c_return', 'c_call']) + + class B: + f = classmethod(max) + events = [] + sys.setprofile(lambda frame, event, args: events.append(event)) + # Not important, we only want to trigger INSTRUMENTED_CALL_KW + B().f(1, key=lambda x: 0) + sys.setprofile(None) + # The last c_call is the call to sys.setprofile + self.assertEqual( + events, + ['c_call', + 'call', 'return', + 'call', 'return', + 'c_return', + 'c_call' + ] + ) + + # Test CALL_FUNCTION_EX + events = [] + sys.setprofile(lambda frame, event, args: events.append(event)) + # Not important, we only want to trigger INSTRUMENTED_CALL_KW + args = (1,) + m = B().f + m(*args, key=lambda x: 0) + sys.setprofile(None) + # The last c_call is the call to sys.setprofile + # INSTRUMENTED_CALL_FUNCTION_EX has different behavior than the other + # instrumented call bytecodes, it does not unpack the callable before + # calling it. This is probably not ideal because it's not consistent, + # but at least we get a consistent call stack (no unmatched c_call). + self.assertEqual( + events, + ['call', 'return', + 'call', 'return', + 'c_call' + ] + ) + + if __name__ == "__main__": unittest.main() From b6b2f7dab23b44c28c87479b238a3272e7e8623d Mon Sep 17 00:00:00 2001 From: ShaharNaveh <50263213+ShaharNaveh@users.noreply.github.com> Date: Sun, 24 May 2026 11:21:48 +0300 Subject: [PATCH 16/16] Mark failing tests --- Lib/test/test_sys_setprofile.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Lib/test/test_sys_setprofile.py b/Lib/test/test_sys_setprofile.py index a22b728b569..813adff2a32 100644 --- a/Lib/test/test_sys_setprofile.py +++ b/Lib/test/test_sys_setprofile.py @@ -169,6 +169,7 @@ def g(p): (1, 'return', g_ident), ]) + @unittest.expectedFailure # TODO: RUSTPYTHON def test_exception_propagation(self): def f(p): 1/0 @@ -260,6 +261,7 @@ def g(p): (1, 'return', g_ident), ]) + @unittest.expectedFailure # TODO: RUSTPYTHON def test_unfinished_generator(self): def f(): for i in range(2): @@ -481,6 +483,7 @@ def f(): sys.setprofile(lambda *args: None) f() + @unittest.expectedFailure # TODO: RUSTPYTHON def test_method_with_c_function(self): # gh-122029 # When we have a PyMethodObject whose im_func is a C function, we