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]