https://github.com/python/cpython/commit/9ee248c296484a8498766a86ac329940d1324d49
commit: 9ee248c296484a8498766a86ac329940d1324d49
branch: main
author: Neil Schemenauer <[email protected]>
committer: kumaraditya303 <[email protected]>
date: 2026-08-15T11:00:03+05:30
summary:

gh-155519: fix data-race for Context.ctx_vars (#155522)

files:
A Lib/test/test_free_threading/test_context.py
A Misc/NEWS.d/next/Library/2026-08-10-13-27-36.gh-issue-155519.9M6SFX.rst
M Python/context.c

diff --git a/Lib/test/test_free_threading/test_context.py 
b/Lib/test/test_free_threading/test_context.py
new file mode 100644
index 00000000000000..02701fe25a8494
--- /dev/null
+++ b/Lib/test/test_free_threading/test_context.py
@@ -0,0 +1,57 @@
+import contextvars
+import unittest
+from threading import Event, Thread
+
+from test.support import threading_helper
+
+
+@threading_helper.requires_working_threading()
+class TestContext(unittest.TestCase):
+    def test_racing_read_write(self):
+        # gh-154535: reading a Context object from one thread while another
+        # thread sets variables in it used to crash.  The readers looked at
+        # Context.ctx_vars without owning a reference to it, so the writer
+        # could deallocate the mapping while a reader was walking it.
+        ctx = contextvars.Context()
+        cvars = [contextvars.ContextVar(f"cvar{i}") for i in range(64)]
+        done = Event()
+        errors = []
+
+        def writer():
+            def body():
+                i = 0
+                while not done.is_set():
+                    cvars[i % len(cvars)].set(i)
+                    i += 1
+            try:
+                ctx.run(body)
+            except BaseException as e:
+                errors.append(e)
+
+        def reader():
+            try:
+                for _ in range(200):
+                    ctx.copy()
+                    len(ctx)
+                    list(ctx)
+                    list(ctx.items())
+                    list(ctx.keys())
+                    list(ctx.values())
+                    cvars[0] in ctx
+                    ctx.get(cvars[0])
+                    ctx == ctx
+            except BaseException as e:
+                errors.append(e)
+            finally:
+                done.set()
+
+        threads = [Thread(target=writer)]
+        threads += [Thread(target=reader) for _ in range(4)]
+        with threading_helper.start_threads(threads, done.set):
+            pass
+
+        self.assertEqual(errors, [], msg=f"unexpected errors: {errors}")
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git 
a/Misc/NEWS.d/next/Library/2026-08-10-13-27-36.gh-issue-155519.9M6SFX.rst 
b/Misc/NEWS.d/next/Library/2026-08-10-13-27-36.gh-issue-155519.9M6SFX.rst
new file mode 100644
index 00000000000000..6c21837d43d554
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-08-10-13-27-36.gh-issue-155519.9M6SFX.rst
@@ -0,0 +1,2 @@
+Avoid a data-race in free-threaded builds when reading and writing context
+variables from different threads.
diff --git a/Python/context.c b/Python/context.c
index 4678054ff3ad74..d48543c9e023a6 100644
--- a/Python/context.c
+++ b/Python/context.c
@@ -1,11 +1,13 @@
 #include "Python.h"
 #include "pycore_call.h"          // _PyObject_VectorcallTstate()
 #include "pycore_context.h"
+#include "pycore_critical_section.h" // Py_BEGIN_CRITICAL_SECTION()
 #include "pycore_freelist.h"      // _Py_FREELIST_FREE(), _Py_FREELIST_POP()
 #include "pycore_gc.h"            // _PyObject_GC_MAY_BE_TRACKED()
 #include "pycore_hamt.h"
 #include "pycore_initconfig.h"    // _PyStatus_OK()
 #include "pycore_object.h"
+#include "pycore_pyatomic_ft_wrappers.h" // FT_ATOMIC_LOAD_INT_RELAXED()
 #include "pycore_pyerrors.h"
 #include "pycore_pystate.h"       // _PyThreadState_GET()
 
@@ -64,6 +66,41 @@ contextvar_set(PyContextVar *var, PyObject *val);
 static int
 contextvar_del(PyContextVar *var);
 
+static inline PyHamtObject *
+context_get_vars(PyContext *ctx)
+{
+    PyHamtObject *vars;
+    Py_BEGIN_CRITICAL_SECTION(ctx);
+    vars = ctx->ctx_vars;
+    assert(vars != NULL);
+    Py_INCREF(vars);
+    Py_END_CRITICAL_SECTION();
+    return vars;
+}
+
+static inline PyHamtObject *
+context_get_current_vars(PyContext *ctx)
+{
+    // ctx_vars written only by the owning thread, and read by other threads
+    // only under the context's lock, a plain (non-atomic) load is okay
+    PyHamtObject *vars = ctx->ctx_vars;
+    assert(vars != NULL);
+    return vars;
+}
+
+// Note: steals a reference to new_vars and must only be called by the thread
+// that has `ctx` as its current context.
+static inline void
+context_set_vars(PyContext *ctx, PyHamtObject *new_vars)
+{
+    PyHamtObject *old_vars;
+    Py_BEGIN_CRITICAL_SECTION(ctx);
+    old_vars = ctx->ctx_vars;
+    ctx->ctx_vars = new_vars;
+    Py_END_CRITICAL_SECTION();
+    Py_XDECREF(old_vars);
+}
+
 
 PyObject *
 _PyContext_NewHamtForTests(void)
@@ -84,7 +121,10 @@ PyContext_Copy(PyObject * octx)
 {
     ENSURE_Context(octx, NULL)
     PyContext *ctx = (PyContext *)octx;
-    return (PyObject *)context_new_from_vars(ctx->ctx_vars);
+    PyHamtObject *vars = context_get_vars(ctx);
+    PyObject *res = (PyObject *)context_new_from_vars(vars);
+    Py_DECREF(vars);
+    return res;
 }
 
 
@@ -96,7 +136,7 @@ PyContext_CopyCurrent(void)
         return NULL;
     }
 
-    return (PyObject *)context_new_from_vars(ctx->ctx_vars);
+    return (PyObject *)context_new_from_vars(context_get_current_vars(ctx));
 }
 
 static const char *
@@ -298,7 +338,7 @@ PyContextVar_Get(PyObject *ovar, PyObject *def, PyObject 
**val)
 #endif
 
     assert(PyContext_CheckExact(ts->context));
-    PyHamtObject *vars = ((PyContext *)ts->context)->ctx_vars;
+    PyHamtObject *vars = context_get_current_vars((PyContext *)ts->context);
 
     PyObject *found = NULL;
     int res = _PyHamt_Find(vars, (PyObject*)var, &found);
@@ -354,7 +394,8 @@ PyContextVar_Set(PyObject *ovar, PyObject *val)
     }
 
     PyObject *old_val = NULL;
-    int found = _PyHamt_Find(ctx->ctx_vars, (PyObject *)var, &old_val);
+    int found = _PyHamt_Find(context_get_current_vars(ctx), (PyObject *)var,
+                             &old_val);
     if (found < 0) {
         return NULL;
     }
@@ -552,7 +593,10 @@ static PyObject *
 context_tp_iter(PyObject *op)
 {
     PyContext *self = _PyContext_CAST(op);
-    return _PyHamt_NewIterKeys(self->ctx_vars);
+    PyHamtObject *vars = context_get_vars(self);
+    PyObject *res = _PyHamt_NewIterKeys(vars);
+    Py_DECREF(vars);
+    return res;
 }
 
 static PyObject *
@@ -564,8 +608,11 @@ context_tp_richcompare(PyObject *v, PyObject *w, int op)
         Py_RETURN_NOTIMPLEMENTED;
     }
 
-    int res = _PyHamt_Eq(
-        ((PyContext *)v)->ctx_vars, ((PyContext *)w)->ctx_vars);
+    PyHamtObject *v_vars = context_get_vars((PyContext *)v);
+    PyHamtObject *w_vars = context_get_vars((PyContext *)w);
+    int res = _PyHamt_Eq(v_vars, w_vars);
+    Py_DECREF(v_vars);
+    Py_DECREF(w_vars);
     if (res < 0) {
         return NULL;
     }
@@ -586,7 +633,10 @@ static Py_ssize_t
 context_tp_len(PyObject *op)
 {
     PyContext *self = _PyContext_CAST(op);
-    return _PyHamt_Len(self->ctx_vars);
+    PyHamtObject *vars = context_get_vars(self);
+    Py_ssize_t res = _PyHamt_Len(vars);
+    Py_DECREF(vars);
+    return res;
 }
 
 static PyObject *
@@ -597,7 +647,10 @@ context_tp_subscript(PyObject *op, PyObject *key)
     }
     PyObject *val = NULL;
     PyContext *self = _PyContext_CAST(op);
-    int found = _PyHamt_Find(self->ctx_vars, key, &val);
+    PyHamtObject *vars = context_get_vars(self);
+    int found = _PyHamt_Find(vars, key, &val);
+    Py_XINCREF(val);
+    Py_DECREF(vars);
     if (found < 0) {
         return NULL;
     }
@@ -605,7 +658,7 @@ context_tp_subscript(PyObject *op, PyObject *key)
         PyErr_SetObject(PyExc_KeyError, key);
         return NULL;
     }
-    return Py_NewRef(val);
+    return val;
 }
 
 static int
@@ -616,7 +669,10 @@ context_tp_contains(PyObject *op, PyObject *key)
     }
     PyObject *val = NULL;
     PyContext *self = _PyContext_CAST(op);
-    return _PyHamt_Find(self->ctx_vars, key, &val);
+    PyHamtObject *vars = context_get_vars(self);
+    int res = _PyHamt_Find(vars, key, &val);
+    Py_DECREF(vars);
+    return res;
 }
 
 
@@ -643,14 +699,17 @@ _contextvars_Context_get_impl(PyContext *self, PyObject 
*key,
     }
 
     PyObject *val = NULL;
-    int found = _PyHamt_Find(self->ctx_vars, key, &val);
+    PyHamtObject *vars = context_get_vars(self);
+    int found = _PyHamt_Find(vars, key, &val);
+    Py_XINCREF(val);
+    Py_DECREF(vars);
     if (found < 0) {
         return NULL;
     }
     if (found == 0) {
         return Py_NewRef(default_value);
     }
-    return Py_NewRef(val);
+    return val;
 }
 
 
@@ -666,7 +725,10 @@ static PyObject *
 _contextvars_Context_items_impl(PyContext *self)
 /*[clinic end generated code: output=fa1655c8a08502af input=00db64ae379f9f42]*/
 {
-    return _PyHamt_NewIterItems(self->ctx_vars);
+    PyHamtObject *vars = context_get_vars(self);
+    PyObject *res = _PyHamt_NewIterItems(vars);
+    Py_DECREF(vars);
+    return res;
 }
 
 
@@ -680,7 +742,10 @@ static PyObject *
 _contextvars_Context_keys_impl(PyContext *self)
 /*[clinic end generated code: output=177227c6b63ec0e2 input=114b53aebca3449c]*/
 {
-    return _PyHamt_NewIterKeys(self->ctx_vars);
+    PyHamtObject *vars = context_get_vars(self);
+    PyObject *res = _PyHamt_NewIterKeys(vars);
+    Py_DECREF(vars);
+    return res;
 }
 
 
@@ -694,7 +759,10 @@ static PyObject *
 _contextvars_Context_values_impl(PyContext *self)
 /*[clinic end generated code: output=d286dabfc8db6dde input=ce8075d04a6ea526]*/
 {
-    return _PyHamt_NewIterValues(self->ctx_vars);
+    PyHamtObject *vars = context_get_vars(self);
+    PyObject *res = _PyHamt_NewIterValues(vars);
+    Py_DECREF(vars);
+    return res;
 }
 
 
@@ -708,7 +776,10 @@ static PyObject *
 _contextvars_Context_copy_impl(PyContext *self)
 /*[clinic end generated code: output=30ba8896c4707a15 input=ebafdbdd9c72d592]*/
 {
-    return (PyObject *)context_new_from_vars(self->ctx_vars);
+    PyHamtObject *vars = context_get_vars(self);
+    PyObject *res = (PyObject *)context_new_from_vars(vars);
+    Py_DECREF(vars);
+    return res;
 }
 
 
@@ -796,12 +867,12 @@ contextvar_set(PyContextVar *var, PyObject *val)
     }
 
     PyHamtObject *new_vars = _PyHamt_Assoc(
-        ctx->ctx_vars, (PyObject *)var, val);
+        context_get_current_vars(ctx), (PyObject *)var, val);
     if (new_vars == NULL) {
         return -1;
     }
 
-    Py_SETREF(ctx->ctx_vars, new_vars);
+    context_set_vars(ctx, new_vars);
 
 #ifndef Py_GIL_DISABLED
     var->var_cached = val;  /* borrow */
@@ -823,7 +894,7 @@ contextvar_del(PyContextVar *var)
         return -1;
     }
 
-    PyHamtObject *vars = ctx->ctx_vars;
+    PyHamtObject *vars = context_get_current_vars(ctx);
     PyHamtObject *new_vars = _PyHamt_Without(vars, (PyObject *)var);
     if (new_vars == NULL) {
         return -1;
@@ -835,7 +906,7 @@ contextvar_del(PyContextVar *var)
         return -1;
     }
 
-    Py_SETREF(ctx->ctx_vars, new_vars);
+    context_set_vars(ctx, new_vars);
     return 0;
 }
 

_______________________________________________
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