From 836fac878ef3a0f30109acfe10b0db7b4483bd2f Mon Sep 17 00:00:00 2001 From: adhinaik13 <22je1052@iitism.ac.in> Date: Mon, 26 Oct 2026 17:07:01 +0530 Subject: [PATCH 1/6] gh-160676: Propagate monitoring callback errors from for loops --- Include/internal/pycore_ceval.h | 2 +- Lib/test/test_monitoring.py | 25 +++++++++++++++++++++++++ Python/bytecodes.c | 4 +++- Python/ceval.c | 10 ++++++---- Python/executor_cases.c.h | 8 ++++---- 5 files changed, 39 insertions(+), 10 deletions(-) diff --git a/Include/internal/pycore_ceval.h b/Include/internal/pycore_ceval.h index a0fea389b6502c0..f5482480a5f76cc 100644 --- a/Include/internal/pycore_ceval.h +++ b/Include/internal/pycore_ceval.h @@ -314,7 +314,7 @@ PyObject * _PyEval_ImportNameWithImport( PyObject *locals, PyObject *name, PyObject *fromlist, PyObject *level); PyAPI_FUNC(PyObject *)_PyEval_MatchClass(PyThreadState *tstate, PyObject *subject, PyObject *type, Py_ssize_t nargs, PyObject *kwargs); PyAPI_FUNC(PyObject *)_PyEval_MatchKeys(PyThreadState *tstate, PyObject *map, PyObject *keys); -PyAPI_FUNC(void) _PyEval_MonitorRaise(PyThreadState *tstate, _PyInterpreterFrame *frame, _Py_CODEUNIT *instr); +PyAPI_FUNC(int) _PyEval_MonitorRaise(PyThreadState *tstate, _PyInterpreterFrame *frame, _Py_CODEUNIT *instr); PyAPI_FUNC(bool) _PyEval_NoToolsForUnwind(PyThreadState *tstate, _PyInterpreterFrame *frame); PyAPI_FUNC(int) _PyEval_UnpackIterableStackRef(PyThreadState *tstate, PyObject *v, int argcnt, int argcntafter, _PyStackRef *sp); PyAPI_FUNC(void) _PyEval_FrameClearAndPop(PyThreadState *tstate, _PyInterpreterFrame *frame); diff --git a/Lib/test/test_monitoring.py b/Lib/test/test_monitoring.py index 24cc51f4b4053ab..82a479ea38ef3e3 100644 --- a/Lib/test/test_monitoring.py +++ b/Lib/test/test_monitoring.py @@ -895,6 +895,31 @@ def implicit_stop_iteration(iterator=None): # This will record a STOP_ITERATION event for the StopIteration. self.check_events(implicit_stop_iteration, expected, recorders=recorders) + def test_raise_callback_stop_iteration_in_for(self): + class Empty: + def __iter__(self): + return self + + def __next__(self): + raise StopIteration + + def run(): + for _ in Empty(): + pass + + def on_raise(code, offset, exc): + if code is run.__code__ and isinstance(exc, StopIteration): + raise RuntimeError("monitor callback failed") + + try: + sys.monitoring.register_callback(TEST_TOOL, E.RAISE, on_raise) + sys.monitoring.set_events(TEST_TOOL, E.RAISE) + with self.assertRaisesRegex(RuntimeError, "monitor callback failed"): + run() + finally: + sys.monitoring.set_events(TEST_TOOL, 0) + sys.monitoring.register_callback(TEST_TOOL, E.RAISE, None) + initial = [ ("raise", ZeroDivisionError), ("handled", ZeroDivisionError) diff --git a/Python/bytecodes.c b/Python/bytecodes.c index 42ad83daa1ea461..bb9a529f88d094d 100644 --- a/Python/bytecodes.c +++ b/Python/bytecodes.c @@ -3818,7 +3818,9 @@ dummy_func( if (item == NULL) { if (_PyErr_Occurred(tstate)) { if (_PyErr_ExceptionMatches(tstate, PyExc_StopIteration)) { - _PyEval_MonitorRaise(tstate, frame, frame->instr_ptr); + if (_PyEval_MonitorRaise(tstate, frame, frame->instr_ptr) < 0) { + ERROR_NO_POP(); + } _PyErr_Clear(tstate); } else { diff --git a/Python/ceval.c b/Python/ceval.c index 223106065c603c4..d3a70c51234b023 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -2542,14 +2542,14 @@ _PyEval_UnpackIterableStackRef(PyThreadState *tstate, PyObject *v, -void +int _PyEval_MonitorRaise(PyThreadState *tstate, _PyInterpreterFrame *frame, _Py_CODEUNIT *instr) { if (no_tools_for_local_event(tstate, frame, PY_MONITORING_EVENT_RAISE)) { - return; + return 0; } - do_monitor_exc(tstate, frame, instr, PY_MONITORING_EVENT_RAISE); + return do_monitor_exc(tstate, frame, instr, PY_MONITORING_EVENT_RAISE); } bool @@ -3834,7 +3834,9 @@ _PyStackRef _PyForIter_VirtualIteratorNext(PyThreadState* tstate, _PyInterpreter if (next == NULL) { if (_PyErr_Occurred(tstate)) { if (_PyErr_ExceptionMatches(tstate, PyExc_StopIteration)) { - _PyEval_MonitorRaise(tstate, frame, frame->instr_ptr); + if (_PyEval_MonitorRaise(tstate, frame, frame->instr_ptr) < 0) { + return PyStackRef_ERROR; + } _PyErr_Clear(tstate); } else { diff --git a/Python/executor_cases.c.h b/Python/executor_cases.c.h index bc85ccd9ddb4fc1..a0289934d9bf9c9 100644 --- a/Python/executor_cases.c.h +++ b/Python/executor_cases.c.h @@ -15030,10 +15030,10 @@ if (item == NULL) { if (_PyErr_Occurred(tstate)) { if (_PyErr_ExceptionMatches(tstate, PyExc_StopIteration)) { - assert(stack_pointer == _PyFrame_GetStackPointer(frame)); - _PyFrame_StackPointerValidate(frame); - _PyEval_MonitorRaise(tstate, frame, frame->instr_ptr); - _PyFrame_StackPointerInvalidate(frame); + if (_PyEval_MonitorRaise(tstate, frame, frame->instr_ptr) < 0) { + SET_CURRENT_CACHED_VALUES(0); + JUMP_TO_ERROR(); + } assert(stack_pointer == _PyFrame_GetStackPointer(frame)); _PyFrame_StackPointerValidate(frame); _PyErr_Clear(tstate); From 65a641876908b74d435a8d8d314e9fb125e6c983 Mon Sep 17 00:00:00 2001 From: Vankudothu Adhi Naik <22je1052@iitism.ac.in> Date: Mon, 26 Oct 2026 17:28:17 +0530 Subject: [PATCH 2/6] gh-160676: Add NEWS entry for monitoring callback error fix --- .../2026-10-26-18-00-00.gh-issue-160676.AdhN7x.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-10-26-18-00-00.gh-issue-160676.AdhN7x.rst diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-10-26-18-00-00.gh-issue-160676.AdhN7x.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-26-18-00-00.gh-issue-160676.AdhN7x.rst new file mode 100644 index 000000000000000..3564d77709bad4d --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-26-18-00-00.gh-issue-160676.AdhN7x.rst @@ -0,0 +1,2 @@ +Fix an issue where exceptions raised by a ``sys.monitoring`` callback for +``StopIteration`` could be swallowed by a ``for`` loop. From 0ba9059c6e4aa7c585eb9b4497a02ca8093eb4be Mon Sep 17 00:00:00 2001 From: adhinaik13 <22je1052@iitism.ac.in> Date: Mon, 26 Oct 2026 23:38:42 +0530 Subject: [PATCH 3/6] gh-160676: Cover tier-2 monitoring callback errors --- Lib/test/test_monitoring.py | 31 +++++++++++++++++++++++++++++++ Python/bytecodes.c | 3 ++- Python/ceval.c | 3 ++- Python/executor_cases.c.h | 6 +++++- 4 files changed, 40 insertions(+), 3 deletions(-) diff --git a/Lib/test/test_monitoring.py b/Lib/test/test_monitoring.py index 82a479ea38ef3e3..72952e4df5ed0db 100644 --- a/Lib/test/test_monitoring.py +++ b/Lib/test/test_monitoring.py @@ -2677,6 +2677,37 @@ def test_func(x): class TestTier2Optimizer(CheckEvents): + @requires_specialization + def test_raise_callback_stop_iteration_in_for_tier2(self): + _testinternalcapi = import_helper.import_module("_testinternalcapi") + _opcode = import_helper.import_module("_opcode") + threshold = _testinternalcapi.SPECIALIZATION_THRESHOLD + callback_executor = None + + def run(): + for i in range(threshold + 1): + sys.monitoring.set_events( + TEST_TOOL, + E.RAISE if i == threshold else 0, + ) + + def on_raise(code, offset, exc): + nonlocal callback_executor + if code is run.__code__ and isinstance(exc, StopIteration): + callback_executor = _opcode.get_executor(code, offset) + raise RuntimeError("monitor callback failed") + + try: + sys.monitoring.register_callback(TEST_TOOL, E.RAISE, on_raise) + with self.assertRaisesRegex(RuntimeError, "monitor callback failed"): + run() + self.assertIsNotNone(callback_executor) + uops = [op[0] for op in callback_executor] + self.assertIn("_ITER_NEXT_INLINE", uops) + finally: + sys.monitoring.set_events(TEST_TOOL, 0) + sys.monitoring.register_callback(TEST_TOOL, E.RAISE, None) + def test_monitoring_already_opimized_loop(self): _testinternalcapi = import_helper.import_module("_testinternalcapi") diff --git a/Python/bytecodes.c b/Python/bytecodes.c index bb9a529f88d094d..3650092ddb62a77 100644 --- a/Python/bytecodes.c +++ b/Python/bytecodes.c @@ -3818,7 +3818,8 @@ dummy_func( if (item == NULL) { if (_PyErr_Occurred(tstate)) { if (_PyErr_ExceptionMatches(tstate, PyExc_StopIteration)) { - if (_PyEval_MonitorRaise(tstate, frame, frame->instr_ptr) < 0) { + int err = _PyEval_MonitorRaise(tstate, frame, frame->instr_ptr); + if (err < 0) { ERROR_NO_POP(); } _PyErr_Clear(tstate); diff --git a/Python/ceval.c b/Python/ceval.c index d3a70c51234b023..700b13444cdd767 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -3834,7 +3834,8 @@ _PyStackRef _PyForIter_VirtualIteratorNext(PyThreadState* tstate, _PyInterpreter if (next == NULL) { if (_PyErr_Occurred(tstate)) { if (_PyErr_ExceptionMatches(tstate, PyExc_StopIteration)) { - if (_PyEval_MonitorRaise(tstate, frame, frame->instr_ptr) < 0) { + int err = _PyEval_MonitorRaise(tstate, frame, frame->instr_ptr); + if (err < 0) { return PyStackRef_ERROR; } _PyErr_Clear(tstate); diff --git a/Python/executor_cases.c.h b/Python/executor_cases.c.h index a0289934d9bf9c9..3a5e91c68c48969 100644 --- a/Python/executor_cases.c.h +++ b/Python/executor_cases.c.h @@ -15030,7 +15030,11 @@ if (item == NULL) { if (_PyErr_Occurred(tstate)) { if (_PyErr_ExceptionMatches(tstate, PyExc_StopIteration)) { - if (_PyEval_MonitorRaise(tstate, frame, frame->instr_ptr) < 0) { + assert(stack_pointer == _PyFrame_GetStackPointer(frame)); + _PyFrame_StackPointerValidate(frame); + int err = _PyEval_MonitorRaise(tstate, frame, frame->instr_ptr); + _PyFrame_StackPointerInvalidate(frame); + if (err < 0) { SET_CURRENT_CACHED_VALUES(0); JUMP_TO_ERROR(); } From 2fc84ddb6ee356d4c8052bf9d42926251dc96f84 Mon Sep 17 00:00:00 2001 From: adhinaik13 <22je1052@iitism.ac.in> Date: Tue, 27 Oct 2026 00:07:21 +0530 Subject: [PATCH 4/6] gh-160676: Fix tier-2 monitoring regression test --- Lib/test/test_monitoring.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/Lib/test/test_monitoring.py b/Lib/test/test_monitoring.py index 72952e4df5ed0db..b842a6be5c31860 100644 --- a/Lib/test/test_monitoring.py +++ b/Lib/test/test_monitoring.py @@ -2684,12 +2684,23 @@ def test_raise_callback_stop_iteration_in_for_tier2(self): threshold = _testinternalcapi.SPECIALIZATION_THRESHOLD callback_executor = None + class Iterator: + def __init__(self): + self.count = 0 + + def __iter__(self): + return self + + def __next__(self): + if self.count < threshold: + self.count += 1 + return self.count + sys.monitoring.set_events(TEST_TOOL, E.RAISE) + raise StopIteration + def run(): - for i in range(threshold + 1): - sys.monitoring.set_events( - TEST_TOOL, - E.RAISE if i == threshold else 0, - ) + for _ in Iterator(): + pass def on_raise(code, offset, exc): nonlocal callback_executor From 4bc8ddfe23f430489e12e809efb30ccf261f6aed Mon Sep 17 00:00:00 2001 From: adhinaik13 <22je1052@iitism.ac.in> Date: Tue, 27 Oct 2026 00:58:49 +0530 Subject: [PATCH 5/6] test: cover tier-2 iterator monitoring errors --- Lib/test/test_monitoring.py | 40 ++++++++++++++++++++++++++++++------- 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/Lib/test/test_monitoring.py b/Lib/test/test_monitoring.py index b842a6be5c31860..0cc4e6c0133148f 100644 --- a/Lib/test/test_monitoring.py +++ b/Lib/test/test_monitoring.py @@ -2682,7 +2682,7 @@ def test_raise_callback_stop_iteration_in_for_tier2(self): _testinternalcapi = import_helper.import_module("_testinternalcapi") _opcode = import_helper.import_module("_opcode") threshold = _testinternalcapi.SPECIALIZATION_THRESHOLD - callback_executor = None + monitoring_enabled = False class Iterator: def __init__(self): @@ -2695,26 +2695,52 @@ def __next__(self): if self.count < threshold: self.count += 1 return self.count - sys.monitoring.set_events(TEST_TOOL, E.RAISE) + if monitoring_enabled: + sys.monitoring.set_events(TEST_TOOL, E.RAISE) raise StopIteration def run(): for _ in Iterator(): pass + # Warm up the loop before enabling monitoring, so the callback run + # executes the tier-2 executor rather than creating it. + run() + code = run.__code__ + executor_offset = None + executor = None + for offset in range(0, len(code.co_code), 2): + try: + candidate = _opcode.get_executor(code, offset) + except ValueError: + continue + except RuntimeError as exc: + if "Executors are not available in this build" not in str(exc): + raise + self.skipTest("requires tier-2 executor support") + if candidate is not None: + uops = [op[0] for op in candidate] + if "_ITER_NEXT_INLINE" in uops: + executor_offset = offset + executor = candidate + break + if executor is None: + self.skipTest("requires _ITER_NEXT_INLINE executor") + + callback_offset = None + def on_raise(code, offset, exc): - nonlocal callback_executor + nonlocal callback_offset if code is run.__code__ and isinstance(exc, StopIteration): - callback_executor = _opcode.get_executor(code, offset) + callback_offset = offset raise RuntimeError("monitor callback failed") + monitoring_enabled = True try: sys.monitoring.register_callback(TEST_TOOL, E.RAISE, on_raise) with self.assertRaisesRegex(RuntimeError, "monitor callback failed"): run() - self.assertIsNotNone(callback_executor) - uops = [op[0] for op in callback_executor] - self.assertIn("_ITER_NEXT_INLINE", uops) + self.assertEqual(callback_offset, executor_offset) finally: sys.monitoring.set_events(TEST_TOOL, 0) sys.monitoring.register_callback(TEST_TOOL, E.RAISE, None) From f2fb76a841478f875cbd3edf1c9d7f13ec114f77 Mon Sep 17 00:00:00 2001 From: adhinaik13 <22je1052@iitism.ac.in> Date: Tue, 27 Oct 2026 13:10:45 +0530 Subject: [PATCH 6/6] Simplify tier-two monitoring test comment --- Lib/test/test_monitoring.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Lib/test/test_monitoring.py b/Lib/test/test_monitoring.py index 0cc4e6c0133148f..765ebb6b523feae 100644 --- a/Lib/test/test_monitoring.py +++ b/Lib/test/test_monitoring.py @@ -2703,8 +2703,7 @@ def run(): for _ in Iterator(): pass - # Warm up the loop before enabling monitoring, so the callback run - # executes the tier-2 executor rather than creating it. + # Warm up the loop before enabling monitoring to create the executor. run() code = run.__code__ executor_offset = None