Skip to content

Commit 20ce55f

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 20ce55f

3 files changed

Lines changed: 114 additions & 52 deletions

File tree

‎Lib/test/test_sys.py‎

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

566+
@support.cpython_only
567+
@requires_subinterpreters
568+
@threading_helper.requires_working_threading()
569+
def test_current_frames_other_interpreters(self):
570+
# gh-158364: sys._current_frames() would access frames of another
571+
# interpreter and crash
572+
import threading
573+
574+
entered = threading.Event()
575+
left = threading.Event()
576+
577+
def park():
578+
entered.set()
579+
left.wait()
580+
581+
t = threading.Thread(target=park)
582+
with threading_helper.start_threads([t], unlock=left.set):
583+
entered.wait()
584+
interp = interpreters.create()
585+
try:
586+
interp.exec(f"""if True:
587+
import sys
588+
import threading
589+
590+
frames = sys._current_frames()
591+
assert threading.get_ident() in frames, frames
592+
assert frames[threading.get_ident()].f_globals is globals()
593+
assert {t.ident} not in frames, frames
594+
""")
595+
finally:
596+
interp.close()
597+
566598
@threading_helper.reap_threads
567599
@threading_helper.requires_working_threading()
568600
def test_current_exceptions(self):
@@ -629,6 +661,39 @@ def g456():
629661
leave_g.set()
630662
t.join()
631663

664+
@support.cpython_only
665+
@requires_subinterpreters
666+
@threading_helper.requires_working_threading()
667+
def test_current_exceptions_other_interpreters(self):
668+
# gh-158364: sys._current_exceptions() would hand out exceptions of
669+
# another interpreter and crash
670+
import threading
671+
672+
entered = threading.Event()
673+
left = threading.Event()
674+
675+
def hold():
676+
# The thread has to be handling an exception, otherwise
677+
# sys._current_exceptions() has nothing to report for it.
678+
try:
679+
raise ValueError
680+
except ValueError:
681+
entered.set()
682+
left.wait()
683+
684+
t = threading.Thread(target=hold)
685+
with threading_helper.start_threads([t], unlock=left.set):
686+
entered.wait()
687+
interp = interpreters.create()
688+
try:
689+
interp.exec(f"""if True:
690+
import sys
691+
692+
assert {t.ident} not in sys._current_exceptions()
693+
""")
694+
finally:
695+
interp.close()
696+
632697
def test_attributes(self):
633698
self.assertIsInstance(sys.api_version, int)
634699
self.assertIsInstance(sys.argv, list)
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix crash when :func:`sys._current_frames` or
2+
:func:`sys._current_exceptions` is called while another interpreter is
3+
running.

‎Python/pystate.c‎

Lines changed: 46 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -2801,36 +2801,33 @@ _PyThread_CurrentFrames(void)
28012801
return NULL;
28022802
}
28032803

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
2804+
/* for t in all of the current interpreter's thread states:
2805+
* if t's frame isn't NULL, map t's id to its frame
28072806
* Because these lists can mutate even when the GIL is held, we
28082807
* need to grab head_mutex for the duration.
28092808
*/
2810-
_PyEval_StopTheWorldAll(runtime);
2809+
PyInterpreterState *interp = tstate->interp;
2810+
_PyEval_StopTheWorld(interp);
28112811
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);
2812+
_Py_FOR_EACH_TSTATE_UNLOCKED(interp, t) {
2813+
_PyInterpreterFrame *frame = t->current_frame;
2814+
frame = _PyFrame_GetFirstComplete(frame);
2815+
if (frame == NULL) {
2816+
continue;
2817+
}
2818+
PyObject *id = PyLong_FromUnsignedLong(t->thread_id);
2819+
if (id == NULL) {
2820+
goto fail;
2821+
}
2822+
PyObject *frameobj = (PyObject *)_PyFrame_GetFrameObject(frame);
2823+
if (frameobj == NULL) {
28302824
Py_DECREF(id);
2831-
if (stat < 0) {
2832-
goto fail;
2833-
}
2825+
goto fail;
2826+
}
2827+
int stat = PyDict_SetItem(result, id, frameobj);
2828+
Py_DECREF(id);
2829+
if (stat < 0) {
2830+
goto fail;
28342831
}
28352832
}
28362833
goto done;
@@ -2840,7 +2837,7 @@ _PyThread_CurrentFrames(void)
28402837

28412838
done:
28422839
HEAD_UNLOCK(runtime);
2843-
_PyEval_StartTheWorldAll(runtime);
2840+
_PyEval_StartTheWorld(interp);
28442841
return result;
28452842
}
28462843

@@ -2866,35 +2863,32 @@ _PyThread_CurrentExceptions(void)
28662863
return NULL;
28672864
}
28682865

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
2866+
/* for t in all of the current interpreter's thread states:
2867+
* if t's frame isn't NULL, map t's id to its exception
28722868
* Because these lists can mutate even when the GIL is held, we
28732869
* need to grab head_mutex for the duration.
28742870
*/
2875-
_PyEval_StopTheWorldAll(runtime);
2871+
PyInterpreterState *interp = tstate->interp;
2872+
_PyEval_StopTheWorld(interp);
28762873
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-
}
2874+
_Py_FOR_EACH_TSTATE_UNLOCKED(interp, t) {
2875+
_PyErr_StackItem *err_info = _PyErr_GetTopmostException(t);
2876+
if (err_info == NULL) {
2877+
continue;
2878+
}
2879+
PyObject *id = PyLong_FromUnsignedLong(t->thread_id);
2880+
if (id == NULL) {
2881+
goto fail;
2882+
}
2883+
PyObject *exc = err_info->exc_value;
2884+
assert(exc == NULL ||
2885+
exc == Py_None ||
2886+
PyExceptionInstance_Check(exc));
2887+
2888+
int stat = PyDict_SetItem(result, id, exc == NULL ? Py_None : exc);
2889+
Py_DECREF(id);
2890+
if (stat < 0) {
2891+
goto fail;
28982892
}
28992893
}
29002894
goto done;
@@ -2904,7 +2898,7 @@ _PyThread_CurrentExceptions(void)
29042898

29052899
done:
29062900
HEAD_UNLOCK(runtime);
2907-
_PyEval_StartTheWorldAll(runtime);
2901+
_PyEval_StartTheWorld(interp);
29082902
return result;
29092903
}
29102904

0 commit comments

Comments
 (0)