diff --git a/Lib/test/test_free_threading/test_collections.py b/Lib/test/test_free_threading/test_collections.py index 849b0480e232fc..d5f895108d53c2 100644 --- a/Lib/test/test_free_threading/test_collections.py +++ b/Lib/test/test_free_threading/test_collections.py @@ -1,5 +1,5 @@ import unittest -from collections import deque +from collections import deque, defaultdict from copy import copy from test.support import threading_helper @@ -49,5 +49,33 @@ def mutate(): ) +class TestDefaultDict(unittest.TestCase): + def test_missing_race(self): + dd = defaultdict(int) + + def getter(): + for i in range(1000): + dd[f"{i}"] + + def setter(): + for _ in range(1000): + dd.default_factory = lambda: 0 + + threading_helper.run_concurrently([getter, setter]) + + def test_repr_race(self): + dd = defaultdict(int) + + def reprer(): + for _ in range(1000): + repr(dd) + + def setter(): + for _ in range(1000): + dd.default_factory = lambda: 0 + + threading_helper.run_concurrently([reprer, setter]) + + if __name__ == "__main__": unittest.main() diff --git a/Misc/NEWS.d/next/Library/2026-07-23-14-09-11.gh-issue-154527.QRVGB-.rst b/Misc/NEWS.d/next/Library/2026-07-23-14-09-11.gh-issue-154527.QRVGB-.rst new file mode 100644 index 00000000000000..619eef3cc0059d --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-23-14-09-11.gh-issue-154527.QRVGB-.rst @@ -0,0 +1,5 @@ +Fixed data-race in :term:`free-threaded build` in +:class:`collections.defaultdict` when calling :func:`repr` and when adding a +new entry to :class:`~collections.defaultdict` that calls +:attr:`defaultdict.default_factory +`. diff --git a/Modules/_collectionsmodule.c b/Modules/_collectionsmodule.c index 62d1dad5735ec8..61ebeb964357f1 100644 --- a/Modules/_collectionsmodule.c +++ b/Modules/_collectionsmodule.c @@ -47,10 +47,12 @@ find_module_state_by_def(PyTypeObject *type) module _collections class _tuplegetter "_tuplegetterobject *" "clinic_state()->tuplegetter_type" class _collections.deque "dequeobject *" "clinic_state()->deque_type" +class _collections.defaultdict "defdictobject *" "clinic_state()->defdict_type" [clinic start generated code]*/ -/*[clinic end generated code: output=da39a3ee5e6b4b0d input=a033cc2a8476b3f1]*/ +/*[clinic end generated code: output=da39a3ee5e6b4b0d input=9c364d64d6b7ca9a]*/ typedef struct dequeobject dequeobject; +typedef struct defdictobject defdictobject; /* We can safely assume type to be the defining class, * since tuplegetter is not a base type */ @@ -2214,27 +2216,36 @@ static PyType_Spec dequereviter_spec = { /* defaultdict type *********************************************************/ -typedef struct { +struct defdictobject { PyDictObject dict; PyObject *default_factory; -} defdictobject; +}; #define defdictobject_CAST(op) ((defdictobject *)(op)) static PyType_Spec defdict_spec; -PyDoc_STRVAR(defdict_missing_doc, -"__missing__(key) # Called by __getitem__ for missing key; pseudo-code:\n\ - if self.default_factory is None: raise KeyError((key,))\n\ - self[key] = value = self.default_factory()\n\ - return value\n\ -"); +/*[clinic input] +@critical_section +_collections.defaultdict.__missing__ as defdict_missing + + key: object + / + +Called by __getitem__ for missing key. + +Pseudo-code: + if self.default_factory is None: + raise KeyError((key,)) + self[key] = value = self.default_factory() + return value +[clinic start generated code]*/ static PyObject * -defdict_missing(PyObject *op, PyObject *key) +defdict_missing_impl(defdictobject *self, PyObject *key) +/*[clinic end generated code: output=a21796ca845265e6 input=cd46877f8d688f3e]*/ { - defdictobject *dd = defdictobject_CAST(op); - PyObject *factory = dd->default_factory; + PyObject *factory = self->default_factory; PyObject *value; if (factory == NULL || factory == Py_None) { /* XXX Call dict.__missing__(key) */ @@ -2249,7 +2260,7 @@ defdict_missing(PyObject *op, PyObject *key) if (value == NULL) return value; PyObject *result = NULL; - (void)PyDict_SetDefaultRef(op, key, value, &result); + (void)PyDict_SetDefaultRef((PyObject *)self, key, value, &result); // 'result' is NULL, or a strong reference to 'value' or 'op[key]' Py_DECREF(value); return result; @@ -2338,8 +2349,7 @@ of the dictionary's keys and values"); static PyMethodDef defdict_methods[] = { - {"__missing__", defdict_missing, METH_O, - defdict_missing_doc}, + DEFDICT_MISSING_METHODDEF {"copy", defdict_copy, METH_NOARGS, defdict_copy_doc}, {"__copy__", defdict_copy, METH_NOARGS, @@ -2371,8 +2381,9 @@ defdict_dealloc(PyObject *op) } static PyObject * -defdict_repr(PyObject *op) +defdict_repr_lock_held(PyObject *op) { + _Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED(op); defdictobject *dd = defdictobject_CAST(op); PyObject *baserepr; PyObject *defrepr; @@ -2409,6 +2420,16 @@ defdict_repr(PyObject *op) return result; } +static PyObject * +defdict_repr(PyObject *op) +{ + PyObject *return_value; + Py_BEGIN_CRITICAL_SECTION(op); + return_value = defdict_repr_lock_held(op); + Py_END_CRITICAL_SECTION(); + return return_value; +} + static PyObject* defdict_or(PyObject* left, PyObject* right) { diff --git a/Modules/clinic/_collectionsmodule.c.h b/Modules/clinic/_collectionsmodule.c.h index 6c60678a6fbd51..157abc2e665d26 100644 --- a/Modules/clinic/_collectionsmodule.c.h +++ b/Modules/clinic/_collectionsmodule.c.h @@ -565,6 +565,36 @@ deque___reversed__(PyObject *deque, PyObject *Py_UNUSED(ignored)) return deque___reversed___impl((dequeobject *)deque); } +PyDoc_STRVAR(defdict_missing__doc__, +"__missing__($self, key, /)\n" +"--\n" +"\n" +"Called by __getitem__ for missing key.\n" +"\n" +"Pseudo-code:\n" +" if self.default_factory is None:\n" +" raise KeyError((key,))\n" +" self[key] = value = self.default_factory()\n" +" return value"); + +#define DEFDICT_MISSING_METHODDEF \ + {"__missing__", (PyCFunction)defdict_missing, METH_O, defdict_missing__doc__}, + +static PyObject * +defdict_missing_impl(defdictobject *self, PyObject *key); + +static PyObject * +defdict_missing(PyObject *self, PyObject *key) +{ + PyObject *return_value = NULL; + + Py_BEGIN_CRITICAL_SECTION(self); + return_value = defdict_missing_impl((defdictobject *)self, key); + Py_END_CRITICAL_SECTION(); + + return return_value; +} + PyDoc_STRVAR(_collections__count_elements__doc__, "_count_elements($module, mapping, iterable, /)\n" "--\n" @@ -632,4 +662,4 @@ tuplegetter_new(PyTypeObject *type, PyObject *args, PyObject *kwargs) exit: return return_value; } -/*[clinic end generated code: output=f5a388add99d3d15 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=4979a785e33cb0ee input=a9049054013a1b77]*/