diff --git a/Lib/test/test_sys.py b/Lib/test/test_sys.py index 786832d3e35b0eb..fd6536536e4cd16 100644 --- a/Lib/test/test_sys.py +++ b/Lib/test/test_sys.py @@ -587,6 +587,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): @@ -654,6 +686,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 000000000000000..14ab4ef12becae6 --- /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 07c69b92d527dce..4b434193e30c78f 100644 --- a/Python/pystate.c +++ b/Python/pystate.c @@ -2547,37 +2547,34 @@ _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) { - PyThreadState *t; - for (t = i->threads.head; t != NULL; t = t->next) { - _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); + PyThreadState *t; + for (t = interp->threads.head; t != NULL; t = t->next) { + _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; @@ -2587,7 +2584,7 @@ _PyThread_CurrentFrames(void) done: HEAD_UNLOCK(runtime); - _PyEval_StartTheWorldAll(runtime); + _PyEval_StartTheWorld(interp); return result; } @@ -2613,36 +2610,33 @@ _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) { - PyThreadState *t; - for (t = i->threads.head; t != NULL; t = t->next) { - _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; - } + PyThreadState *t; + for (t = interp->threads.head; t != NULL; t = t->next) { + _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; @@ -2652,7 +2646,7 @@ _PyThread_CurrentExceptions(void) done: HEAD_UNLOCK(runtime); - _PyEval_StartTheWorldAll(runtime); + _PyEval_StartTheWorld(interp); return result; }