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 000000000000000..237a0bfc866ffaa --- /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 000000000000000..7338ae0210450a7 --- /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 568d978d27d12f9..c84af34464a55fc 100644 --- a/Objects/descrobject.c +++ b/Objects/descrobject.c @@ -621,9 +621,14 @@ static PyObject * descr_get_qualname(PyObject *self, void *Py_UNUSED(ignored)) { PyDescrObject *descr = (PyDescrObject *)self; - if (descr->d_qualname == NULL) + PyObject *qualname; + Py_BEGIN_CRITICAL_SECTION(self); + if (descr->d_qualname == NULL) { descr->d_qualname = calculate_qualname(descr); - return Py_XNewRef(descr->d_qualname); + } + qualname = Py_XNewRef(descr->d_qualname); + Py_END_CRITICAL_SECTION(); + return qualname; } static PyObject *