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]

Reply via email to