From 0fc508e7b8bf6c927cd7a7de5b3cb3b4330e6b84 Mon Sep 17 00:00:00 2001 From: Ivy Xu Date: Sun, 19 Jul 2026 19:24:27 +0800 Subject: [PATCH 1/3] Fix a data race when iterating a shared types.GenericAlias iterator --- .../test_free_threading/test_genericalias.py | 31 +++++++++++++++++++ ...-07-19-19-14-39.gh-issue-154043.Kych7F.rst | 2 ++ Objects/genericaliasobject.c | 13 ++++++-- 3 files changed, 43 insertions(+), 3 deletions(-) create mode 100644 Lib/test/test_free_threading/test_genericalias.py create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-07-19-19-14-39.gh-issue-154043.Kych7F.rst diff --git a/Lib/test/test_free_threading/test_genericalias.py b/Lib/test/test_free_threading/test_genericalias.py new file mode 100644 index 00000000000000..87a16cfb2fc47b --- /dev/null +++ b/Lib/test/test_free_threading/test_genericalias.py @@ -0,0 +1,31 @@ +import threading +import unittest + +from test.support import threading_helper + + +NTHREADS = 10 + + +@threading_helper.requires_working_threading() +class TestGenericAlias(unittest.TestCase): + def worker(self, it, barrier): + barrier.wait() + try: + next(it) + except StopIteration: + pass + + def test_shared_generic_alias_iter(self): + # See https://github.com/python/cpython/issues/154043 + bar = threading.Barrier(NTHREADS) + number_of_iterations = 50 + for _ in range(number_of_iterations): + shared = iter(list[int]) + threading_helper.run_concurrently( + worker_func=self.worker, nthreads=NTHREADS, args=(shared, bar) + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-07-19-19-14-39.gh-issue-154043.Kych7F.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-07-19-19-14-39.gh-issue-154043.Kych7F.rst new file mode 100644 index 00000000000000..d37e63e6116a59 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-07-19-19-14-39.gh-issue-154043.Kych7F.rst @@ -0,0 +1,2 @@ +Fix a data race when iterating a shared :class:`types.GenericAlias` iterator +from multiple threads under the :term:`free-threaded build`. diff --git a/Objects/genericaliasobject.c b/Objects/genericaliasobject.c index 4e85927def7ea7..2894e5837a02b6 100644 --- a/Objects/genericaliasobject.c +++ b/Objects/genericaliasobject.c @@ -939,17 +939,24 @@ static PyObject * ga_iternext(PyObject *op) { gaiterobject *gi = (gaiterobject*)op; - if (gi->obj == NULL) { +#ifdef Py_GIL_DISABLED + PyObject *obj = _Py_atomic_exchange_ptr(&gi->obj, NULL); +#else + PyObject* obj = gi->obj; + gi->obj = NULL; +#endif + if (obj == NULL) { PyErr_SetNone(PyExc_StopIteration); return NULL; } - gaobject *alias = (gaobject *)gi->obj; + gaobject *alias = (gaobject *)obj; PyObject *starred_alias = Py_GenericAlias(alias->origin, alias->args); if (starred_alias == NULL) { + Py_DECREF(obj); return NULL; } ((gaobject *)starred_alias)->starred = true; - Py_SETREF(gi->obj, NULL); + Py_SETREF(obj, NULL); return starred_alias; } From 8f2d85eb68b3d4d1bdc329976a19d1d17b4ef19a Mon Sep 17 00:00:00 2001 From: Ivy Xu Date: Tue, 21 Jul 2026 06:31:26 +0800 Subject: [PATCH 2/3] Move test to `test_types.py` --- .../test_free_threading/test_genericalias.py | 31 ------------------- Lib/test/test_free_threading/test_types.py | 21 +++++++++++++ 2 files changed, 21 insertions(+), 31 deletions(-) delete mode 100644 Lib/test/test_free_threading/test_genericalias.py diff --git a/Lib/test/test_free_threading/test_genericalias.py b/Lib/test/test_free_threading/test_genericalias.py deleted file mode 100644 index 87a16cfb2fc47b..00000000000000 --- a/Lib/test/test_free_threading/test_genericalias.py +++ /dev/null @@ -1,31 +0,0 @@ -import threading -import unittest - -from test.support import threading_helper - - -NTHREADS = 10 - - -@threading_helper.requires_working_threading() -class TestGenericAlias(unittest.TestCase): - def worker(self, it, barrier): - barrier.wait() - try: - next(it) - except StopIteration: - pass - - def test_shared_generic_alias_iter(self): - # See https://github.com/python/cpython/issues/154043 - bar = threading.Barrier(NTHREADS) - number_of_iterations = 50 - for _ in range(number_of_iterations): - shared = iter(list[int]) - threading_helper.run_concurrently( - worker_func=self.worker, nthreads=NTHREADS, args=(shared, bar) - ) - - -if __name__ == "__main__": - unittest.main() diff --git a/Lib/test/test_free_threading/test_types.py b/Lib/test/test_free_threading/test_types.py index 76fcf1590122f5..7be1ea49c223e1 100644 --- a/Lib/test/test_free_threading/test_types.py +++ b/Lib/test/test_free_threading/test_types.py @@ -1,9 +1,12 @@ import unittest +import threading from typing import TypeVar from test.support import threading_helper threading_helper.requires_working_threading(module=True) +NTHREADS = 10 + class TestGenericAlias(unittest.TestCase): def test_parameters_race(self): @@ -28,6 +31,24 @@ def refresh(): *[refresh for _ in range(2)], ]) + def test_shared_generic_alias_iter(self): + # See https://github.com/python/cpython/issues/154043 + def worker(it, barrier): + barrier.wait() + try: + next(it) + except StopIteration: + pass + bar = threading.Barrier(NTHREADS) + + bar = threading.Barrier(NTHREADS) + number_of_iterations = 50 + for _ in range(number_of_iterations): + shared = iter(list[int]) + threading_helper.run_concurrently( + worker_func=worker, nthreads=NTHREADS, args=(shared, bar) + ) + if __name__ == "__main__": unittest.main() From 24cd9d8f1e85d61f7490cacee0e6576da957a5be Mon Sep 17 00:00:00 2001 From: Ivy Xu Date: Tue, 21 Jul 2026 14:44:08 +0800 Subject: [PATCH 3/3] Address review --- Lib/test/test_free_threading/test_types.py | 21 --------------------- Objects/genericaliasobject.c | 3 +-- 2 files changed, 1 insertion(+), 23 deletions(-) diff --git a/Lib/test/test_free_threading/test_types.py b/Lib/test/test_free_threading/test_types.py index 7be1ea49c223e1..76fcf1590122f5 100644 --- a/Lib/test/test_free_threading/test_types.py +++ b/Lib/test/test_free_threading/test_types.py @@ -1,12 +1,9 @@ import unittest -import threading from typing import TypeVar from test.support import threading_helper threading_helper.requires_working_threading(module=True) -NTHREADS = 10 - class TestGenericAlias(unittest.TestCase): def test_parameters_race(self): @@ -31,24 +28,6 @@ def refresh(): *[refresh for _ in range(2)], ]) - def test_shared_generic_alias_iter(self): - # See https://github.com/python/cpython/issues/154043 - def worker(it, barrier): - barrier.wait() - try: - next(it) - except StopIteration: - pass - bar = threading.Barrier(NTHREADS) - - bar = threading.Barrier(NTHREADS) - number_of_iterations = 50 - for _ in range(number_of_iterations): - shared = iter(list[int]) - threading_helper.run_concurrently( - worker_func=worker, nthreads=NTHREADS, args=(shared, bar) - ) - if __name__ == "__main__": unittest.main() diff --git a/Objects/genericaliasobject.c b/Objects/genericaliasobject.c index 2894e5837a02b6..9480a0e16116ef 100644 --- a/Objects/genericaliasobject.c +++ b/Objects/genericaliasobject.c @@ -951,12 +951,11 @@ ga_iternext(PyObject *op) } gaobject *alias = (gaobject *)obj; PyObject *starred_alias = Py_GenericAlias(alias->origin, alias->args); + Py_DECREF(obj); if (starred_alias == NULL) { - Py_DECREF(obj); return NULL; } ((gaobject *)starred_alias)->starred = true; - Py_SETREF(obj, NULL); return starred_alias; }