From 20ce55f6f33ccce10762760a2d9163f352ce96a3 Mon Sep 17 00:00:00 2001 From: Himesh Rupchandani Date: Tue, 29 Sep 2026 08:21:39 +0530 Subject: [PATCH] gh-158364: Don't report other interpreters' threads in sys._current_frames() sys._current_frames() materialized a PyFrameObject for every thread of every interpreter. A frame object created for a thread of another interpreter belongs to that interpreter: it is stored in its _PyInterpreterFrame.frame_obj and deallocated when that interpreter pops the frame, while the calling interpreter holds the reference from the returned dict and drops it at some arbitrary later time. Since PEP 684 gives each interpreter its own obmalloc arenas, the block ends up being freed by a different interpreter than the one that allocated it, which corrupts the heap and typically aborts the process inside free(). sys._current_exceptions() has the same problem: it hands out references to exception objects owned by other interpreters. Both functions now only report the threads of the calling interpreter. This matches PyUnstable_DumpTracebackThreads() (used by faulthandler), which already only dumps the threads of one interpreter. As a consequence, only the current interpreter's world needs to be stopped. --- Lib/test/test_sys.py | 65 ++++++++++++ ...-09-28-18-58-27.gh-issue-158364.jBqmTs.rst | 3 + Python/pystate.c | 98 +++++++++---------- 3 files changed, 114 insertions(+), 52 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-09-28-18-58-27.gh-issue-158364.jBqmTs.rst diff --git a/Lib/test/test_sys.py b/Lib/test/test_sys.py index da1bd381dd182e4..1ccdcf5a187bbb4 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 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 737bf0f216bd34e..76ad958478a3ca9 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; }