diff --git a/Lib/test/test_free_threading/test_set.py b/Lib/test/test_free_threading/test_set.py index 9dd3d68d5dad13..5d8abe72d607c2 100644 --- a/Lib/test/test_free_threading/test_set.py +++ b/Lib/test/test_free_threading/test_set.py @@ -148,6 +148,62 @@ def read_set(): for t in threads: t.join() + def test_length_hint_used_race(self): + s = set(range(2000)) + it = iter(s) + + NUM_LOOPS = 50_000 + barrier = Barrier(2) + + def reader(): + barrier.wait() + for _ in range(NUM_LOOPS): + it.__length_hint__() + + def writer(): + barrier.wait() + i = 0 + for _ in range(NUM_LOOPS): + s.add(i) + s.discard(i - 1) + i += 1 + + t1 = Thread(target=reader) + t2 = Thread(target=writer) + t1.start(); t2.start() + t1.join(); t2.join() + + def test_length_hint_exhaust_race(self): + NUM_LOOPS = 10_000 + INNER_HINTS = 20 + barrier = Barrier(2) + box = {"it": None} + + def exhauster(): + for _ in range(NUM_LOOPS): + s = set(range(256)) + box["it"] = iter(s) + barrier.wait() # start together + try: + while True: + next(box["it"]) + except StopIteration: + pass + barrier.wait() # end iteration + + def reader(): + for _ in range(NUM_LOOPS): + barrier.wait() + it = box["it"] + for _ in range(INNER_HINTS): + it.__length_hint__() + barrier.wait() + + t1 = Thread(target=reader) + t2 = Thread(target=exhauster) + t1.start(); t2.start() + t1.join(); t2.join() + @threading_helper.requires_working_threading() class SmallSetTest(RaceTestBase, unittest.TestCase): diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-01-31-03-40-28.gh-issue-144356.otfq_X.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-01-31-03-40-28.gh-issue-144356.otfq_X.rst new file mode 100644 index 00000000000000..d5d67ad1e8dbb3 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-01-31-03-40-28.gh-issue-144356.otfq_X.rst @@ -0,0 +1 @@ +Fix a data race in ``set_iterator.__length_hint__`` under ``Py_GIL_DISABLED``. diff --git a/Objects/setobject.c b/Objects/setobject.c index 5d4d1812282eed..d0291b98ebfb72 100644 --- a/Objects/setobject.c +++ b/Objects/setobject.c @@ -1056,8 +1056,23 @@ setiter_len(PyObject *op, PyObject *Py_UNUSED(ignored)) { setiterobject *si = (setiterobject*)op; Py_ssize_t len = 0; - if (si->si_set != NULL && si->si_used == si->si_set->used) +#ifdef Py_GIL_DISABLED + PyObject *so_obj = FT_ATOMIC_LOAD_PTR_ACQUIRE(si->si_set); + if (so_obj != NULL) { + /* Turn borrowed si->si_set into a strong ref safely. */ + if (_Py_TryIncrefCompare((PyObject **)&si->si_set, so_obj)) { + PySetObject *so = (PySetObject *)so_obj; + if (si->si_used == FT_ATOMIC_LOAD_SSIZE_RELAXED(so->used)) { + len = si->len; + } + Py_DECREF(so_obj); + } + } +#else + if (si->si_set != NULL && si->si_used == si->si_set->used) { len = si->len; + } +#endif return PyLong_FromSsize_t(len); } @@ -1124,7 +1139,11 @@ static PyObject *setiter_iternext(PyObject *self) Py_END_CRITICAL_SECTION(); si->si_pos = i+1; if (key == NULL) { +#ifdef Py_GIL_DISABLED + FT_ATOMIC_STORE_PTR_RELEASE(si->si_set, NULL); +#else si->si_set = NULL; +#endif Py_DECREF(so); return NULL; }