From 0bdc8b2ea01c2c246276a9e5fae79cfb20b6ab74 Mon Sep 17 00:00:00 2001 From: Matt Page Date: Fri, 23 Oct 2026 13:45:04 -0700 Subject: [PATCH 1/2] gh-160496: Clear inline values with atomic release stores Py_CLEAR writes NULL non-atomically, racing with the acquire load in _PyObject_TryGetInstanceAttribute. Invalidating the inline values does not exclude lookups already in flight. Use a release store before decref, preserving the clear-before-decref ordering. --- Lib/test/test_free_threading/test_dict.py | 33 +++++++++++++++++++++++ Objects/dictobject.c | 4 ++- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_free_threading/test_dict.py b/Lib/test/test_free_threading/test_dict.py index a888637fda43c8..b5ed2ccfa0865c 100644 --- a/Lib/test/test_free_threading/test_dict.py +++ b/Lib/test/test_free_threading/test_dict.py @@ -228,6 +228,39 @@ def reader_func(): self.assertEqual(count, 0) + def test_racing_replace_inline_values_and_lookup(self): + # gh-160496: replacing an unmaterialized __dict__ clears inline + # values while attribute lookup can still read them without a lock. + class C: + pass + + objects = [C() for _ in range(1000)] + for obj in objects: + obj.x = [0] + + # Inline values are invalidated only once per object, so synchronize + # the threads for each fresh instance without accessing its __dict__. + barrier = Barrier(2) + observed = [] + + def writer(): + for obj in objects: + barrier.wait() + obj.__dict__ = {"x": [1]} + barrier.wait() + + def reader(): + for obj in objects: + barrier.wait() + observed.append(getattr(obj, "x", None)) + barrier.wait() + + threading_helper.run_concurrently([writer, reader]) + for value in observed: + self.assertIn(value, ([0], [1])) + for obj in objects: + self.assertEqual(obj.x, [1]) + def test_racing_object_get_set_dict(self): e = Exception() diff --git a/Objects/dictobject.c b/Objects/dictobject.c index f00cf75f6a762d..2e0fa195f2fc1d 100644 --- a/Objects/dictobject.c +++ b/Objects/dictobject.c @@ -7802,7 +7802,9 @@ clear_inline_values(PyDictValues *values) if (values->valid) { FT_ATOMIC_STORE_UINT8(values->valid, 0); for (Py_ssize_t i = 0; i < values->capacity; i++) { - Py_CLEAR(values->values[i]); + PyObject *value = values->values[i]; + FT_ATOMIC_STORE_PTR_RELEASE(values->values[i], NULL); + Py_XDECREF(value); } } } From db83abc23c51955b86096e714cf94857b3b3e530 Mon Sep 17 00:00:00 2001 From: Matt Page Date: Fri, 23 Oct 2026 15:26:44 -0700 Subject: [PATCH 2/2] Document conflicting read --- Objects/dictobject.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Objects/dictobject.c b/Objects/dictobject.c index 2e0fa195f2fc1d..b97ca961edc11e 100644 --- a/Objects/dictobject.c +++ b/Objects/dictobject.c @@ -7803,6 +7803,8 @@ clear_inline_values(PyDictValues *values) FT_ATOMIC_STORE_UINT8(values->valid, 0); for (Py_ssize_t i = 0; i < values->capacity; i++) { PyObject *value = values->values[i]; + // The attribute read fast-path may read these without holding the + // per-object lock. FT_ATOMIC_STORE_PTR_RELEASE(values->values[i], NULL); Py_XDECREF(value); }