diff --git a/Lib/test/test_sys.py b/Lib/test/test_sys.py index da1bd381dd182e..1ccdcf5a187bbb 100644 --- a/Lib/test/test_sys.py +++ b/Lib/test/test_sys.py @@ -563,6 +563,38 @@ def g456(): leave_g.set() t.join() + @support.cpython_only + @requires_subinterpreters + @threading_helper.requires_working_threading() + def test_current_frames_other_interpreters(self): + # gh-158364: sys._current_frames() would access frames of another + # interpreter and crash + import threading + + entered = threading.Event() + left = threading.Event() + + def park(): + entered.set() + left.wait() + + t = threading.Thread(target=park) + with threading_helper.start_threads([t], unlock=left.set): + entered.wait() + interp = interpreters.create() + try: + interp.exec(f"""if True: + import sys + import threading + + frames = sys._current_frames() + assert threading.get_ident() in frames, frames + assert frames[threading.get_ident()].f_globals is globals() + assert {t.ident} not in frames, frames + """) + finally: + interp.close() + @threading_helper.reap_threads @threading_helper.requires_working_threading() def test_current_exceptions(self): @@ -629,6 +661,39 @@ def g456(): leave_g.set() t.join() + @support.cpython_only + @requires_subinterpreters + @threading_helper.requires_working_threading() + def test_current_exceptions_other_interpreters(self): + # gh-158364: sys._current_exceptions() would hand out exceptions of + # another interpreter and crash + import threading + + entered = threading.Event() + left = threading.Event() + + def hold(): + # The thread has to be handling an exception, otherwise + # sys._current_exceptions() has nothing to report for it. + try: + raise ValueError + except ValueError: + entered.set() + left.wait() + + t = threading.Thread(target=hold) + with threading_helper.start_threads([t], unlock=left.set): + entered.wait() + interp = interpreters.create() + try: + interp.exec(f"""if True: + import sys + + assert {t.ident} not in sys._current_exceptions() + """) + finally: + interp.close() + def test_attributes(self): self.assertIsInstance(sys.api_version, int) self.assertIsInstance(sys.argv, list) diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-09-28-18-58-27.gh-issue-158364.jBqmTs.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-09-28-18-58-27.gh-issue-158364.jBqmTs.rst new file mode 100644 index 00000000000000..14ab4ef12becae --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-09-28-18-58-27.gh-issue-158364.jBqmTs.rst @@ -0,0 +1,3 @@ +Fix crash when :func:`sys._current_frames` or +:func:`sys._current_exceptions` is called while another interpreter is +running. diff --git a/Python/pystate.c b/Python/pystate.c index 737bf0f216bd34..76ad958478a3ca 100644 --- a/Python/pystate.c +++ b/Python/pystate.c @@ -2801,36 +2801,33 @@ _PyThread_CurrentFrames(void) return NULL; } - /* for i in all interpreters: - * for t in all of i's thread states: - * if t's frame isn't NULL, map t's id to its frame + /* for t in all of the current interpreter's thread states: + * if t's frame isn't NULL, map t's id to its frame * Because these lists can mutate even when the GIL is held, we * need to grab head_mutex for the duration. */ - _PyEval_StopTheWorldAll(runtime); + PyInterpreterState *interp = tstate->interp; + _PyEval_StopTheWorld(interp); HEAD_LOCK(runtime); - PyInterpreterState *i; - for (i = runtime->interpreters.head; i != NULL; i = i->next) { - _Py_FOR_EACH_TSTATE_UNLOCKED(i, t) { - _PyInterpreterFrame *frame = t->current_frame; - frame = _PyFrame_GetFirstComplete(frame); - if (frame == NULL) { - continue; - } - PyObject *id = PyLong_FromUnsignedLong(t->thread_id); - if (id == NULL) { - goto fail; - } - PyObject *frameobj = (PyObject *)_PyFrame_GetFrameObject(frame); - if (frameobj == NULL) { - Py_DECREF(id); - goto fail; - } - int stat = PyDict_SetItem(result, id, frameobj); + _Py_FOR_EACH_TSTATE_UNLOCKED(interp, t) { + _PyInterpreterFrame *frame = t->current_frame; + frame = _PyFrame_GetFirstComplete(frame); + if (frame == NULL) { + continue; + } + PyObject *id = PyLong_FromUnsignedLong(t->thread_id); + if (id == NULL) { + goto fail; + } + PyObject *frameobj = (PyObject *)_PyFrame_GetFrameObject(frame); + if (frameobj == NULL) { Py_DECREF(id); - if (stat < 0) { - goto fail; - } + goto fail; + } + int stat = PyDict_SetItem(result, id, frameobj); + Py_DECREF(id); + if (stat < 0) { + goto fail; } } goto done; @@ -2840,7 +2837,7 @@ _PyThread_CurrentFrames(void) done: HEAD_UNLOCK(runtime); - _PyEval_StartTheWorldAll(runtime); + _PyEval_StartTheWorld(interp); return result; } @@ -2866,35 +2863,32 @@ _PyThread_CurrentExceptions(void) return NULL; } - /* for i in all interpreters: - * for t in all of i's thread states: - * if t's frame isn't NULL, map t's id to its frame + /* for t in all of the current interpreter's thread states: + * if t's frame isn't NULL, map t's id to its exception * Because these lists can mutate even when the GIL is held, we * need to grab head_mutex for the duration. */ - _PyEval_StopTheWorldAll(runtime); + PyInterpreterState *interp = tstate->interp; + _PyEval_StopTheWorld(interp); HEAD_LOCK(runtime); - PyInterpreterState *i; - for (i = runtime->interpreters.head; i != NULL; i = i->next) { - _Py_FOR_EACH_TSTATE_UNLOCKED(i, t) { - _PyErr_StackItem *err_info = _PyErr_GetTopmostException(t); - if (err_info == NULL) { - continue; - } - PyObject *id = PyLong_FromUnsignedLong(t->thread_id); - if (id == NULL) { - goto fail; - } - PyObject *exc = err_info->exc_value; - assert(exc == NULL || - exc == Py_None || - PyExceptionInstance_Check(exc)); - - int stat = PyDict_SetItem(result, id, exc == NULL ? Py_None : exc); - Py_DECREF(id); - if (stat < 0) { - goto fail; - } + _Py_FOR_EACH_TSTATE_UNLOCKED(interp, t) { + _PyErr_StackItem *err_info = _PyErr_GetTopmostException(t); + if (err_info == NULL) { + continue; + } + PyObject *id = PyLong_FromUnsignedLong(t->thread_id); + if (id == NULL) { + goto fail; + } + PyObject *exc = err_info->exc_value; + assert(exc == NULL || + exc == Py_None || + PyExceptionInstance_Check(exc)); + + int stat = PyDict_SetItem(result, id, exc == NULL ? Py_None : exc); + Py_DECREF(id); + if (stat < 0) { + goto fail; } } goto done; @@ -2904,7 +2898,7 @@ _PyThread_CurrentExceptions(void) done: HEAD_UNLOCK(runtime); - _PyEval_StartTheWorldAll(runtime); + _PyEval_StartTheWorld(interp); return result; }