https://github.com/python/cpython/commit/db929014053f9a7a3a43c8f7032955d9095c3e5e
commit: db929014053f9a7a3a43c8f7032955d9095c3e5e
branch: main
author: Kumar Aditya <[email protected]>
committer: kumaraditya303 <[email protected]>
date: 2026-09-02T23:26:04+05:30
summary:
gh-144446: Fix thread safety of gi_frame, cr_frame and ag_frame in
free-threading (#156037)
files:
A
Misc/NEWS.d/next/Core_and_Builtins/2026-08-19-06-43-54.gh-issue-144446.k3QzXa.rst
M Include/internal/pycore_interpframe.h
M Lib/test/test_free_threading/test_generators.py
M Objects/genobject.c
M Python/ceval.c
M Python/frame.c
diff --git a/Include/internal/pycore_interpframe.h
b/Include/internal/pycore_interpframe.h
index 9809cd292995f0b..812e1a28debef44 100644
--- a/Include/internal/pycore_interpframe.h
+++ b/Include/internal/pycore_interpframe.h
@@ -7,6 +7,7 @@
#include "pycore_code.h" // _PyCode_CODE()
#include "pycore_interpframe_structs.h" // _PyInterpreterFrame
+#include "pycore_pyatomic_ft_wrappers.h" // FT_ATOMIC_LOAD_PTR_ACQUIRE()
#include "pycore_stackref.h" // PyStackRef_AsPyObjectBorrow()
#include "pycore_stats.h" // CALL_STAT_INC()
@@ -344,7 +345,7 @@ _PyFrame_GetFrameObject(_PyInterpreterFrame *frame)
{
assert(!_PyFrame_IsIncomplete(frame));
- PyFrameObject *res = frame->frame_obj;
+ PyFrameObject *res = FT_ATOMIC_LOAD_PTR_ACQUIRE(frame->frame_obj);
if (res != NULL) {
return res;
}
diff --git a/Lib/test/test_free_threading/test_generators.py
b/Lib/test/test_free_threading/test_generators.py
index 382503eebd123f1..7b4fd4d10a79a09 100644
--- a/Lib/test/test_free_threading/test_generators.py
+++ b/Lib/test/test_free_threading/test_generators.py
@@ -155,3 +155,64 @@ def closer():
done.set()
threading_helper.run_concurrently([reader, closer])
+
+ def test_gi_frame_teardown_race(self):
+ ROUNDS = 20000
+
+ def gen():
+ yield 1
+
+ barrier = Barrier(2)
+ shared = {}
+ captured = []
+
+ def reader():
+ for _ in range(ROUNDS):
+ barrier.wait()
+ frame = shared['gen'].gi_frame
+ if frame is not None:
+ captured.append(frame)
+ barrier.wait()
+
+ def driver():
+ for _ in range(ROUNDS):
+ g = gen()
+ next(g)
+ shared['gen'] = g
+ barrier.wait()
+ try:
+ next(g)
+ except StopIteration:
+ pass
+ barrier.wait()
+
+ threading_helper.run_concurrently([reader, driver])
+ shared.clear()
+ for frame in captured:
+ self.assertIsNotNone(frame.f_lineno)
+
+ def test_concurrent_gi_frame(self):
+ frames = set()
+ def gen():
+ for i in range(10000):
+ yield i
+
+ g = gen()
+ done = threading.Event()
+
+ def runner():
+ for _ in g:
+ pass
+ done.set()
+
+ def reader():
+ while not done.is_set():
+ frame = g.gi_frame
+ if frame:
+ frame.f_code
+ frame.f_locals
+ frames.add(frame)
+ self.assertIsNone(g.gi_frame)
+
+ threading_helper.run_concurrently([runner, reader])
+ self.assertEqual(len(frames), 1)
diff --git
a/Misc/NEWS.d/next/Core_and_Builtins/2026-08-19-06-43-54.gh-issue-144446.k3QzXa.rst
b/Misc/NEWS.d/next/Core_and_Builtins/2026-08-19-06-43-54.gh-issue-144446.k3QzXa.rst
new file mode 100644
index 000000000000000..66d2ef0391a45a3
--- /dev/null
+++
b/Misc/NEWS.d/next/Core_and_Builtins/2026-08-19-06-43-54.gh-issue-144446.k3QzXa.rst
@@ -0,0 +1,3 @@
+Fix thread safety of the :attr:`!generator.gi_frame`,
+:attr:`!coroutine.cr_frame` and :attr:`!agen.ag_frame` attributes in the
+free-threading build.
diff --git a/Objects/genobject.c b/Objects/genobject.c
index 6a96bc27d9a950d..c313002c723e317 100644
--- a/Objects/genobject.c
+++ b/Objects/genobject.c
@@ -170,7 +170,9 @@ gen_clear_frame(PyGenObject *gen)
_PyInterpreterFrame *frame = &gen->gi_iframe;
_PyThreadState_UpdateLastProfiledFrame(_PyThreadState_GET(), frame,
frame->previous);
frame->previous = NULL;
+ Py_BEGIN_CRITICAL_SECTION(gen);
_PyFrame_ClearExceptCode(frame);
+ Py_END_CRITICAL_SECTION();
_PyErr_ClearExcState(&gen->gi_exc_state);
}
@@ -960,8 +962,17 @@ _gen_getframe(PyGenObject *gen, const char *const name)
if (FRAME_STATE_FINISHED(frame_state)) {
Py_RETURN_NONE;
}
- // TODO: still not thread-safe with free threading
- return _Py_XNewRef((PyObject *)_PyFrame_GetFrameObject(&gen->gi_iframe));
+ PyObject *frame = NULL;
+ Py_BEGIN_CRITICAL_SECTION(gen);
+ frame_state = FT_ATOMIC_LOAD_INT8_RELAXED(gen->gi_frame_state);
+ if (FRAME_STATE_FINISHED(frame_state)) {
+ frame = Py_None;
+ }
+ else {
+ frame = _Py_XNewRef((PyObject
*)_PyFrame_GetFrameObject(&gen->gi_iframe));
+ }
+ Py_END_CRITICAL_SECTION();
+ return frame;
}
static PyObject *
diff --git a/Python/ceval.c b/Python/ceval.c
index 3f636c3416e1207..3a859c05a037240 100644
--- a/Python/ceval.c
+++ b/Python/ceval.c
@@ -1996,9 +1996,11 @@ clear_gen_frame(PyThreadState *tstate,
_PyInterpreterFrame * frame)
assert(tstate->exc_info == &gen->gi_exc_state);
tstate->exc_info = gen->gi_exc_state.previous_item;
gen->gi_exc_state.previous_item = NULL;
- assert(frame->frame_obj == NULL || frame->frame_obj->f_frame == frame);
frame->previous = NULL;
+ Py_BEGIN_CRITICAL_SECTION(gen);
+ assert(frame->frame_obj == NULL || frame->frame_obj->f_frame == frame);
_PyFrame_ClearExceptCode(frame);
+ Py_END_CRITICAL_SECTION();
_PyErr_ClearExcState(&gen->gi_exc_state);
// gh-143939: There must not be any escaping calls between setting
// the generator return kind and returning from _PyEval_EvalFrame.
diff --git a/Python/frame.c b/Python/frame.c
index ba8222417d208c2..79522b00b47f118 100644
--- a/Python/frame.c
+++ b/Python/frame.c
@@ -20,7 +20,6 @@ _PyFrame_Traverse(_PyInterpreterFrame *frame, visitproc
visit, void *arg)
PyFrameObject *
_PyFrame_MakeAndSetFrameObject(_PyInterpreterFrame *frame)
{
- assert(frame->frame_obj == NULL);
PyObject *exc = PyErr_GetRaisedException();
PyFrameObject *f = _PyFrame_New_NoTrack(_PyFrame_GetCode(frame));
@@ -37,10 +36,18 @@ _PyFrame_MakeAndSetFrameObject(_PyInterpreterFrame *frame)
// Notice that _PyFrame_New_NoTrack() can potentially raise a MemoryError,
// but it won't allocate a traceback until the frame unwinds, so we are
safe
// here.
- assert(frame->frame_obj == NULL);
assert(frame->owner != FRAME_OWNED_BY_FRAME_OBJECT);
f->f_frame = frame;
+#ifdef Py_GIL_DISABLED
+ PyFrameObject *expected = NULL;
+ if (!_Py_atomic_compare_exchange_ptr(&frame->frame_obj, &expected, f)) {
+ Py_DECREF(f);
+ return expected;
+ }
+#else
+ assert(frame->frame_obj == NULL);
frame->frame_obj = f;
+#endif
return f;
}
@@ -113,9 +120,9 @@ _PyFrame_ClearExceptCode(_PyInterpreterFrame *frame)
// GH-99729: Clearing this frame can expose the stack (via finalizers).
It's
// crucial that this frame has been unlinked, and is no longer visible:
assert(_PyThreadState_GET()->current_frame != frame);
- if (frame->frame_obj) {
- PyFrameObject *f = frame->frame_obj;
- frame->frame_obj = NULL;
+ PyFrameObject *f = FT_ATOMIC_LOAD_PTR_RELAXED(frame->frame_obj);
+ if (f != NULL) {
+ FT_ATOMIC_STORE_PTR_RELAXED(frame->frame_obj, NULL);
if (!_PyObject_IsUniquelyReferenced((PyObject *)f)) {
take_ownership(f, frame);
Py_DECREF(f);
_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]