From 10850b8045ac421a5d0d81ef9147401057b4ad2c Mon Sep 17 00:00:00 2001 From: mpage Date: Mon, 26 Oct 2026 07:16:46 -0700 Subject: [PATCH] gh-160496: Clear inline values with atomic release stores (GH-160510) (cherry picked from commit a9049ee4fb62138e4aa03c06f2e705a03677f92e) Co-authored-by: mpage --- Lib/test/test_free_threading/test_dict.py | 33 +++++++++++++++++++++++ Objects/dictobject.c | 6 ++++- 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_free_threading/test_dict.py b/Lib/test/test_free_threading/test_dict.py index 35b221d447e75c..d4b50e37c2416b 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 5caf02f9e0c69f..d21d47d153c3bc 100644 --- a/Objects/dictobject.c +++ b/Objects/dictobject.c @@ -7731,7 +7731,11 @@ 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]; + // 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); } } }