Skip to content

Commit e05c195

Browse files
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.
1 parent 3330712 commit e05c195

4 files changed

Lines changed: 227 additions & 52 deletions

File tree

‎Doc/library/sys.rst‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,13 @@ always available. Unless explicitly noted otherwise, all variables are read-only
271271

272272
.. audit-event:: sys._current_frames "" sys._current_frames
273273

274+
.. versionchanged:: 3.16
275+
Only the threads of the current interpreter are included. Previously
276+
the threads of other interpreters were reported as well, but the frame
277+
objects created for them belonged to those interpreters, which could
278+
corrupt the heap (each interpreter has its own memory arenas) and crash
279+
the process.
280+
274281
.. function:: _current_exceptions()
275282

276283
Return a dictionary mapping each thread's identifier to the topmost exception
@@ -288,6 +295,10 @@ always available. Unless explicitly noted otherwise, all variables are read-only
288295
Each value in the dictionary is now a single exception instance, rather
289296
than a 3-tuple as returned from ``sys.exc_info()``.
290297

298+
.. versionchanged:: 3.16
299+
Only the threads of the current interpreter are included, as for
300+
:func:`_current_frames`.
301+
291302
.. function:: breakpointhook()
292303

293304
This hook function is called by built-in :func:`breakpoint`. By default,

‎Lib/test/test_sys.py‎

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -563,6 +563,108 @@ def g456():
563563
leave_g.set()
564564
t.join()
565565

566+
@support.cpython_only
567+
@requires_subinterpreters
568+
@threading_helper.reap_threads
569+
@threading_helper.requires_working_threading()
570+
def test_current_frames_other_interpreters(self):
571+
# gh-158364: sys._current_frames() must not report the threads of
572+
# other interpreters. A frame object materialized for such a thread
573+
# belongs to that interpreter, and since each interpreter has its own
574+
# memory arenas, it would be deallocated by a different interpreter
575+
# than the one that allocated it, corrupting the heap.
576+
import threading
577+
578+
# Park a thread of *this* interpreter at a known place.
579+
entered_g = threading.Event()
580+
leave_g = threading.Event()
581+
582+
def park():
583+
entered_g.set()
584+
leave_g.wait(support.SHORT_TIMEOUT)
585+
586+
t = threading.Thread(target=park)
587+
t.start()
588+
entered_g.wait(support.SHORT_TIMEOUT)
589+
try:
590+
interp = interpreters.create()
591+
try:
592+
interp.exec(textwrap.dedent(f'''
593+
import sys
594+
import threading
595+
596+
frames = sys._current_frames()
597+
598+
# This interpreter reports its own threads...
599+
assert threading.get_ident() in frames, frames
600+
frame = frames[threading.get_ident()]
601+
assert frame.f_globals is globals(), frame.f_globals
602+
# ...and not the threads of the main interpreter.
603+
assert {t.ident} not in frames, frames
604+
'''))
605+
finally:
606+
interp.close()
607+
finally:
608+
# Reap the spawned thread.
609+
leave_g.set()
610+
t.join()
611+
612+
@support.cpython_only
613+
@requires_subinterpreters
614+
@threading_helper.reap_threads
615+
@threading_helper.requires_working_threading()
616+
def test_current_frames_subinterpreter_thread(self):
617+
# gh-158364: the same, but sampling here while another interpreter
618+
# runs in a different thread. The frames are kept alive until after
619+
# the other interpreter is destroyed, so that *this* interpreter is
620+
# the one that deallocates them.
621+
import threading
622+
623+
# One queue per direction, so that neither side can consume its own
624+
# message.
625+
ids = interpreters.create_queue()
626+
release = interpreters.create_queue()
627+
interp = interpreters.create()
628+
script = textwrap.dedent(f'''
629+
import threading
630+
from concurrent.interpreters import Queue
631+
632+
ids = Queue({ids.id})
633+
release = Queue({release.id})
634+
635+
def work():
636+
ids.put(threading.get_ident())
637+
release.get()
638+
639+
t = threading.Thread(target=work)
640+
t.start()
641+
t.join()
642+
''')
643+
t = threading.Thread(target=interp.exec, args=(script,), daemon=True)
644+
t.start()
645+
frames = None
646+
try:
647+
# The other interpreter's thread is now parked in release.get(),
648+
# running a frame that this interpreter must leave alone.
649+
other_id = ids.get(timeout=support.SHORT_TIMEOUT)
650+
frames = sys._current_frames()
651+
self.assertNotIn(other_id, frames)
652+
653+
# Let the other interpreter unwind and go away.
654+
release.put('go')
655+
t.join(support.SHORT_TIMEOUT)
656+
self.assertFalse(t.is_alive())
657+
finally:
658+
release.put('go')
659+
t.join(support.SHORT_TIMEOUT)
660+
try:
661+
interp.close()
662+
except interpreters.InterpreterError:
663+
pass
664+
# The other interpreter's memory is gone by now; dropping the
665+
# frames here must still be safe.
666+
frames = None
667+
566668
@threading_helper.reap_threads
567669
@threading_helper.requires_working_threading()
568670
def test_current_exceptions(self):
@@ -629,6 +731,46 @@ def g456():
629731
leave_g.set()
630732
t.join()
631733

734+
@support.cpython_only
735+
@requires_subinterpreters
736+
@threading_helper.reap_threads
737+
@threading_helper.requires_working_threading()
738+
def test_current_exceptions_other_interpreters(self):
739+
# gh-158364: like test_current_frames_other_interpreters(), but for
740+
# sys._current_exceptions(): the returned dict would hold a reference
741+
# to an exception owned by another interpreter.
742+
import threading
743+
744+
entered_g = threading.Event()
745+
leave_g = threading.Event()
746+
747+
def hold():
748+
try:
749+
raise ValueError('held in another thread')
750+
except ValueError:
751+
entered_g.set()
752+
leave_g.wait(support.SHORT_TIMEOUT)
753+
754+
t = threading.Thread(target=hold)
755+
t.start()
756+
entered_g.wait(support.SHORT_TIMEOUT)
757+
try:
758+
interp = interpreters.create()
759+
try:
760+
interp.exec(textwrap.dedent(f'''
761+
import sys
762+
763+
excs = sys._current_exceptions()
764+
765+
assert {t.ident} not in excs, excs
766+
'''))
767+
finally:
768+
interp.close()
769+
finally:
770+
# Reap the spawned thread.
771+
leave_g.set()
772+
t.join()
773+
632774
def test_attributes(self):
633775
self.assertIsInstance(sys.api_version, int)
634776
self.assertIsInstance(sys.argv, list)
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
Fix heap corruption (typically an abort or crash in the C library's
2+
``free()``) when :func:`sys._current_frames` or
3+
:func:`sys._current_exceptions` is called while another interpreter is
4+
running. Both functions no longer report the threads of other
5+
interpreters: a frame object materialized for such a thread belongs to
6+
that interpreter, and since each interpreter has its own memory arenas it
7+
was being deallocated by a different interpreter than the one that
8+
allocated it.

‎Python/pystate.c‎

Lines changed: 66 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -2792,6 +2792,9 @@ _PyThread_CurrentFrames(void)
27922792
{
27932793
_PyRuntimeState *runtime = &_PyRuntime;
27942794
PyThreadState *tstate = current_fast_get();
2795+
2796+
_Py_EnsureTstateNotNULL(tstate);
2797+
27952798
if (_PySys_Audit(tstate, "sys._current_frames", NULL) < 0) {
27962799
return NULL;
27972800
}
@@ -2801,36 +2804,45 @@ _PyThread_CurrentFrames(void)
28012804
return NULL;
28022805
}
28032806

2804-
/* for i in all interpreters:
2805-
* for t in all of i's thread states:
2806-
* if t's frame isn't NULL, map t's id to its frame
2807+
/* for t in all of the current interpreter's thread states:
2808+
* if t's frame isn't NULL, map t's id to its frame
28072809
* Because these lists can mutate even when the GIL is held, we
28082810
* need to grab head_mutex for the duration.
2811+
*
2812+
* Threads of other interpreters are not included (gh-158364). A frame
2813+
* object materialized for such a thread belongs to that interpreter: it
2814+
* is stored in its _PyInterpreterFrame.frame_obj and deallocated when
2815+
* that interpreter pops the frame, while the caller drops the reference
2816+
* held by the returned dict at some arbitrary later time. Since PEP 684
2817+
* gives each interpreter its own obmalloc arenas, the block can be freed
2818+
* by a different interpreter than the one that allocated it, corrupting
2819+
* the heap. Handing out such a frame is unsafe for other reasons too: it
2820+
* holds references to the other interpreter's objects, and that
2821+
* interpreter may be finalized while the caller still holds the frame.
2822+
* Code running in each interpreter sees its own threads.
28092823
*/
2810-
_PyEval_StopTheWorldAll(runtime);
2824+
PyInterpreterState *interp = tstate->interp;
2825+
_PyEval_StopTheWorld(interp);
28112826
HEAD_LOCK(runtime);
2812-
PyInterpreterState *i;
2813-
for (i = runtime->interpreters.head; i != NULL; i = i->next) {
2814-
_Py_FOR_EACH_TSTATE_UNLOCKED(i, t) {
2815-
_PyInterpreterFrame *frame = t->current_frame;
2816-
frame = _PyFrame_GetFirstComplete(frame);
2817-
if (frame == NULL) {
2818-
continue;
2819-
}
2820-
PyObject *id = PyLong_FromUnsignedLong(t->thread_id);
2821-
if (id == NULL) {
2822-
goto fail;
2823-
}
2824-
PyObject *frameobj = (PyObject *)_PyFrame_GetFrameObject(frame);
2825-
if (frameobj == NULL) {
2826-
Py_DECREF(id);
2827-
goto fail;
2828-
}
2829-
int stat = PyDict_SetItem(result, id, frameobj);
2827+
_Py_FOR_EACH_TSTATE_UNLOCKED(interp, t) {
2828+
_PyInterpreterFrame *frame = t->current_frame;
2829+
frame = _PyFrame_GetFirstComplete(frame);
2830+
if (frame == NULL) {
2831+
continue;
2832+
}
2833+
PyObject *id = PyLong_FromUnsignedLong(t->thread_id);
2834+
if (id == NULL) {
2835+
goto fail;
2836+
}
2837+
PyObject *frameobj = (PyObject *)_PyFrame_GetFrameObject(frame);
2838+
if (frameobj == NULL) {
28302839
Py_DECREF(id);
2831-
if (stat < 0) {
2832-
goto fail;
2833-
}
2840+
goto fail;
2841+
}
2842+
int stat = PyDict_SetItem(result, id, frameobj);
2843+
Py_DECREF(id);
2844+
if (stat < 0) {
2845+
goto fail;
28342846
}
28352847
}
28362848
goto done;
@@ -2840,7 +2852,7 @@ _PyThread_CurrentFrames(void)
28402852

28412853
done:
28422854
HEAD_UNLOCK(runtime);
2843-
_PyEval_StartTheWorldAll(runtime);
2855+
_PyEval_StartTheWorld(interp);
28442856
return result;
28452857
}
28462858

@@ -2866,35 +2878,37 @@ _PyThread_CurrentExceptions(void)
28662878
return NULL;
28672879
}
28682880

2869-
/* for i in all interpreters:
2870-
* for t in all of i's thread states:
2871-
* if t's frame isn't NULL, map t's id to its frame
2881+
/* for t in all of the current interpreter's thread states:
2882+
* if t's frame isn't NULL, map t's id to its exception
28722883
* Because these lists can mutate even when the GIL is held, we
28732884
* need to grab head_mutex for the duration.
2885+
*
2886+
* Threads of other interpreters are not included, for the same reason as
2887+
* in _PyThread_CurrentFrames(): the returned dict would hold a reference
2888+
* to an exception owned by another interpreter, which can then be
2889+
* deallocated by whichever interpreter drops it last (gh-158364).
28742890
*/
2875-
_PyEval_StopTheWorldAll(runtime);
2891+
PyInterpreterState *interp = tstate->interp;
2892+
_PyEval_StopTheWorld(interp);
28762893
HEAD_LOCK(runtime);
2877-
PyInterpreterState *i;
2878-
for (i = runtime->interpreters.head; i != NULL; i = i->next) {
2879-
_Py_FOR_EACH_TSTATE_UNLOCKED(i, t) {
2880-
_PyErr_StackItem *err_info = _PyErr_GetTopmostException(t);
2881-
if (err_info == NULL) {
2882-
continue;
2883-
}
2884-
PyObject *id = PyLong_FromUnsignedLong(t->thread_id);
2885-
if (id == NULL) {
2886-
goto fail;
2887-
}
2888-
PyObject *exc = err_info->exc_value;
2889-
assert(exc == NULL ||
2890-
exc == Py_None ||
2891-
PyExceptionInstance_Check(exc));
2892-
2893-
int stat = PyDict_SetItem(result, id, exc == NULL ? Py_None : exc);
2894-
Py_DECREF(id);
2895-
if (stat < 0) {
2896-
goto fail;
2897-
}
2894+
_Py_FOR_EACH_TSTATE_UNLOCKED(interp, t) {
2895+
_PyErr_StackItem *err_info = _PyErr_GetTopmostException(t);
2896+
if (err_info == NULL) {
2897+
continue;
2898+
}
2899+
PyObject *id = PyLong_FromUnsignedLong(t->thread_id);
2900+
if (id == NULL) {
2901+
goto fail;
2902+
}
2903+
PyObject *exc = err_info->exc_value;
2904+
assert(exc == NULL ||
2905+
exc == Py_None ||
2906+
PyExceptionInstance_Check(exc));
2907+
2908+
int stat = PyDict_SetItem(result, id, exc == NULL ? Py_None : exc);
2909+
Py_DECREF(id);
2910+
if (stat < 0) {
2911+
goto fail;
28982912
}
28992913
}
29002914
goto done;
@@ -2904,7 +2918,7 @@ _PyThread_CurrentExceptions(void)
29042918

29052919
done:
29062920
HEAD_UNLOCK(runtime);
2907-
_PyEval_StartTheWorldAll(runtime);
2921+
_PyEval_StartTheWorld(interp);
29082922
return result;
29092923
}
29102924

0 commit comments

Comments
 (0)