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..765ebb6b523feae 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) @@ -2652,6 +2677,73 @@ 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 + monitoring_enabled = False + + 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 + 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 to create the executor. + 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_offset + if code is run.__code__ and isinstance(exc, StopIteration): + 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.assertEqual(callback_offset, executor_offset) + 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/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. diff --git a/Python/bytecodes.c b/Python/bytecodes.c index 42ad83daa1ea461..3650092ddb62a77 100644 --- a/Python/bytecodes.c +++ b/Python/bytecodes.c @@ -3818,7 +3818,10 @@ dummy_func( if (item == NULL) { if (_PyErr_Occurred(tstate)) { if (_PyErr_ExceptionMatches(tstate, PyExc_StopIteration)) { - _PyEval_MonitorRaise(tstate, frame, frame->instr_ptr); + int err = _PyEval_MonitorRaise(tstate, frame, frame->instr_ptr); + if (err < 0) { + ERROR_NO_POP(); + } _PyErr_Clear(tstate); } else { diff --git a/Python/ceval.c b/Python/ceval.c index 223106065c603c4..700b13444cdd767 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,10 @@ _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); + int err = _PyEval_MonitorRaise(tstate, frame, frame->instr_ptr); + if (err < 0) { + return PyStackRef_ERROR; + } _PyErr_Clear(tstate); } else { diff --git a/Python/executor_cases.c.h b/Python/executor_cases.c.h index bc85ccd9ddb4fc1..3a5e91c68c48969 100644 --- a/Python/executor_cases.c.h +++ b/Python/executor_cases.c.h @@ -15032,8 +15032,12 @@ if (_PyErr_ExceptionMatches(tstate, PyExc_StopIteration)) { assert(stack_pointer == _PyFrame_GetStackPointer(frame)); _PyFrame_StackPointerValidate(frame); - _PyEval_MonitorRaise(tstate, frame, frame->instr_ptr); + int err = _PyEval_MonitorRaise(tstate, frame, frame->instr_ptr); _PyFrame_StackPointerInvalidate(frame); + if (err < 0) { + SET_CURRENT_CACHED_VALUES(0); + JUMP_TO_ERROR(); + } assert(stack_pointer == _PyFrame_GetStackPointer(frame)); _PyFrame_StackPointerValidate(frame); _PyErr_Clear(tstate);