diff --git a/Lib/test/test_free_threading/test_set.py b/Lib/test/test_free_threading/test_set.py index 9dd3d68d5dad13..80475460111274 100644 --- a/Lib/test/test_free_threading/test_set.py +++ b/Lib/test/test_free_threading/test_set.py @@ -148,6 +148,61 @@ def read_set(): for t in threads: t.join() + @threading_helper.reap_threads + def test_length_hint_used_race(self): + NUM_ITERS = 10 + NUM_THREADS = 10 + NUM_LOOPS = 2_000 + + for _ in range(NUM_ITERS): + s = set(range(2000)) + it = iter(s) + + def worker(): + for i in range(NUM_LOOPS): + it.__length_hint__() + s.add(i) + s.discard(i - 1) + + threading_helper.run_concurrently(worker, nthreads=NUM_THREADS) + + @threading_helper.reap_threads + def test_length_hint_exhaust_race(self): + NUM_ITERS = 10 + NUM_THREADS = 10 + + for _ in range(NUM_ITERS): + s = set(range(256)) + it = iter(s) + + def worker(): + while True: + it.__length_hint__() + try: + next(it) + except StopIteration: + break + + threading_helper.run_concurrently(worker, nthreads=NUM_THREADS) + + @threading_helper.reap_threads + def test_iternext_concurrent_exhaust_race(self): + NUM_ITERS = 10 + NUM_THREADS = 10 + + for _ in range(NUM_ITERS): + s = set(range(64)) + it = iter(s) + + def worker(): + while True: + try: + next(it) + except StopIteration: + break + + threading_helper.run_concurrently(worker, nthreads=NUM_THREADS) + @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..1d4a4075c95e44 --- /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 potential races in set iterators (``__length_hint__`` and iteration) in free-threaded builds. diff --git a/Objects/setobject.c b/Objects/setobject.c index 5d4d1812282eed..fb9064d9f74613 100644 --- a/Objects/setobject.c +++ b/Objects/setobject.c @@ -1056,8 +1056,22 @@ 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 + PySetObject *so = si->si_set; + assert(so != NULL); + + Py_BEGIN_CRITICAL_SECTION2(op, so); + if (si->si_pos >= 0 && si->si_used == so->used) { len = si->len; + } + Py_END_CRITICAL_SECTION2(); +#else + if (si->si_set != NULL && si->si_used == si->si_set->used) { + len = si->len; + } +#endif + return PyLong_FromSsize_t(len); } @@ -1089,7 +1103,8 @@ static PyMethodDef setiter_methods[] = { {NULL, NULL} /* sentinel */ }; -static PyObject *setiter_iternext(PyObject *self) +static PyObject * +setiter_iternext(PyObject *self) { setiterobject *si = (setiterobject*)self; PyObject *key = NULL; @@ -1097,9 +1112,14 @@ static PyObject *setiter_iternext(PyObject *self) setentry *entry; PySetObject *so = si->si_set; - if (so == NULL) +#ifdef Py_GIL_DISABLED + assert(so != NULL); +#else + if (so == NULL) { return NULL; - assert (PyAnySet_Check(so)); + } +#endif + assert(PyAnySet_Check(so)); Py_ssize_t so_used = FT_ATOMIC_LOAD_SSIZE_RELAXED(so->used); Py_ssize_t si_used = FT_ATOMIC_LOAD_SSIZE_RELAXED(si->si_used); @@ -1110,9 +1130,20 @@ static PyObject *setiter_iternext(PyObject *self) return NULL; } +#ifdef Py_GIL_DISABLED + Py_BEGIN_CRITICAL_SECTION2(self, so); +#else Py_BEGIN_CRITICAL_SECTION(so); +#endif + i = si->si_pos; - assert(i>=0); +#ifdef Py_GIL_DISABLED + if (i < 0) { + /* iterator already exhausted */ + goto done; + } +#endif + entry = so->table; mask = so->mask; while (i <= mask && (entry[i].key == NULL || entry[i].key == dummy)) { @@ -1120,16 +1151,32 @@ static PyObject *setiter_iternext(PyObject *self) } if (i <= mask) { key = Py_NewRef(entry[i].key); + si->si_pos = i + 1; + si->len--; + } + else { + /* exhausted */ + si->si_pos = -1; + si->len = 0; +#ifndef Py_GIL_DISABLED + si->si_set = NULL; +#endif } + +#ifdef Py_GIL_DISABLED +done: + Py_END_CRITICAL_SECTION2(); + return key; +#else Py_END_CRITICAL_SECTION(); - si->si_pos = i+1; + if (key == NULL) { - si->si_set = NULL; + /* exhausted */ Py_DECREF(so); return NULL; } - si->len--; return key; +#endif } PyTypeObject PySetIter_Type = {