[3.15] gh-155519: fix data-race for Context.ctx_vars (GH-155522) (#155893)

hugovk <[email protected]>
Newsgroups gmane.comp.python.cvs
Message-ID <[email protected]>
https://github.com/python/cpython/commit/9d0e268943b54a6d1eb3418e2dee167b54d420a1
commit: 9d0e268943b54a6d1eb3418e2dee167b54d420a1
branch: 3.15
author: Miss Islington (bot) <[email protected]>
committer: hugovk <[email protected]>
date: 2026-08-19T06:31:44+03:00
summary:

[3.15] gh-155519: fix data-race for Context.ctx_vars (GH-155522) (#155893)

Co-authored-by: Neil Schemenauer <[email protected]>

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 000000000000000..02701fe25a8494e
--- /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 000000000000000..6c21837d43d5544
--- /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 4678054ff3ad743..d48543c9e023a61 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]
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.