diff --git a/Lib/test/test_free_threading/test_descr.py b/Lib/test/test_free_threading/test_descr.py new file mode 100644 index 00000000000000..237a0bfc866ffa --- /dev/null +++ b/Lib/test/test_free_threading/test_descr.py @@ -0,0 +1,46 @@ +import unittest + +from test import support +from test.support import threading_helper + + +N = 8 + + +@threading_helper.requires_working_threading() +class TestDescrQualnameRace(unittest.TestCase): + # gh-154044: reading __qualname__ on a shared descriptor for the first time + # concurrently raced on the lazy d_qualname cache. + + def race_first_access(self, descr): + results = [] + + def read(): + results.append(descr.__qualname__) + + threading_helper.run_concurrently(read, N) + self.assertEqual(len(set(results)), 1) + self.assertIsNotNone(results[0]) + + def test_slot_member_descriptors(self): + count = 100 if support.check_sanitizer(thread=True) else 300 + for _ in range(count): + class C: + __slots__ = ("value",) + self.race_first_access(C.__dict__["value"]) + + def test_builtin_descriptors(self): + kinds = {"method_descriptor", "getset_descriptor", "wrapper_descriptor"} + descrs = [ + v + for tp in (str, bytes, list, dict, set, int, float, tuple, + frozenset, bytearray) + for v in vars(tp).values() + if type(v).__name__ in kinds + ] + for descr in descrs: + self.race_first_access(descr) + + +if __name__ == "__main__": + unittest.main() diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-07-25-12-44-13.gh-issue-154044.GBa2wP.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-07-25-12-44-13.gh-issue-154044.GBa2wP.rst new file mode 100644 index 00000000000000..7338ae0210450a --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-07-25-12-44-13.gh-issue-154044.GBa2wP.rst @@ -0,0 +1,2 @@ +Fix a data race on a descriptor's ``__qualname__`` cache in the +:term:`free-threaded build`. diff --git a/Objects/descrobject.c b/Objects/descrobject.c index 568d978d27d12f..e69a29df76e076 100644 --- a/Objects/descrobject.c +++ b/Objects/descrobject.c @@ -9,6 +9,7 @@ #include "pycore_modsupport.h" // _PyArg_UnpackStack() #include "pycore_object.h" // _PyObject_GC_UNTRACK() #include "pycore_object_deferred.h" // _PyObject_SetDeferredRefcount() +#include "pycore_pyatomic_ft_wrappers.h" // FT_ATOMIC_LOAD_PTR_ACQUIRE() #include "pycore_pystate.h" // _PyThreadState_GET() #include "pycore_tuple.h" // _PyTuple_ITEMS() @@ -621,9 +622,24 @@ static PyObject * descr_get_qualname(PyObject *self, void *Py_UNUSED(ignored)) { PyDescrObject *descr = (PyDescrObject *)self; - if (descr->d_qualname == NULL) - descr->d_qualname = calculate_qualname(descr); - return Py_XNewRef(descr->d_qualname); + + PyObject *qualname = FT_ATOMIC_LOAD_PTR_ACQUIRE(descr->d_qualname); + if (qualname != NULL) { + return Py_NewRef(qualname); + } + + Py_BEGIN_CRITICAL_SECTION(self); + qualname = FT_ATOMIC_LOAD_PTR_RELAXED(descr->d_qualname); + if (qualname == NULL) { + qualname = calculate_qualname(descr); + if (qualname != NULL) { + FT_ATOMIC_STORE_PTR_RELEASE(descr->d_qualname, qualname); + } + } + Py_XINCREF(qualname); + Py_END_CRITICAL_SECTION(); + + return qualname; } static PyObject *