From 86dab7c55bb85269b764886ceda1f85e3fdc7a7d Mon Sep 17 00:00:00 2001 From: Pedram Karimi <147748351+pedramkarimii@users.noreply.github.com> Date: Sat, 1 Aug 2026 13:27:14 +0330 Subject: [PATCH 1/3] gh-151627: fix thread safety of OrderedDict iterator creation (#151688) --- .../test_free_threading/test_collections.py | 27 ++++++++++++++++++- ...-06-19-08-30-00.gh-issue-151627.xY7kLm.rst | 2 ++ Objects/odictobject.c | 2 ++ 3 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Library/2026-06-19-08-30-00.gh-issue-151627.xY7kLm.rst diff --git a/Lib/test/test_free_threading/test_collections.py b/Lib/test/test_free_threading/test_collections.py index 849b0480e232fc2..a0a73c965d072a2 100644 --- a/Lib/test/test_free_threading/test_collections.py +++ b/Lib/test/test_free_threading/test_collections.py @@ -1,5 +1,5 @@ import unittest -from collections import deque +from collections import OrderedDict, deque from copy import copy from test.support import threading_helper @@ -49,5 +49,30 @@ def mutate(): ) +class TestOrderedDict(unittest.TestCase): + def test_iterator_update_clear_race(self): + # gh-151627: OrderedDict iterator construction must not race with + # concurrent clear()/update() operations that mutate the linked list. + od = OrderedDict((i, i) for i in range(100)) + + def mutate(): + for i in range(5000): + od.clear() + od.update(((i, i), (i + 1, i + 1), (i + 2, i + 2))) + + def iterate(): + for _ in range(5000): + try: + for _ in od: + pass + list(reversed(od)) + except RuntimeError: + pass + + threading_helper.run_concurrently( + [mutate, *[iterate for _ in range(8)]], + ) + + if __name__ == "__main__": unittest.main() diff --git a/Misc/NEWS.d/next/Library/2026-06-19-08-30-00.gh-issue-151627.xY7kLm.rst b/Misc/NEWS.d/next/Library/2026-06-19-08-30-00.gh-issue-151627.xY7kLm.rst new file mode 100644 index 000000000000000..2c5d39eec12347b --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-06-19-08-30-00.gh-issue-151627.xY7kLm.rst @@ -0,0 +1,2 @@ +Fix a crash in :class:`collections.OrderedDict` iterators in free-threaded +builds when the dictionary is concurrently cleared or updated. diff --git a/Objects/odictobject.c b/Objects/odictobject.c index c51fd4b7195da42..a860ade3b148e5f 100644 --- a/Objects/odictobject.c +++ b/Objects/odictobject.c @@ -1958,11 +1958,13 @@ odictiter_new(PyODictObject *od, int kind) } di->kind = kind; + Py_BEGIN_CRITICAL_SECTION(od); node = reversed ? _odict_LAST(od) : _odict_FIRST(od); di->di_current = node ? Py_NewRef(_odictnode_KEY(node)) : NULL; di->di_size = PyODict_SIZE(od); di->di_state = od->od_state; di->di_odict = (PyODictObject*)Py_NewRef(od); + Py_END_CRITICAL_SECTION(); _PyObject_GC_TRACK(di); return (PyObject *)di; From 534b6b062e53f70587840eac9b432442f66c16c6 Mon Sep 17 00:00:00 2001 From: Neil Schemenauer Date: Sat, 1 Aug 2026 03:01:26 -0700 Subject: [PATCH 2/3] Add comments about _Py_LOCK_DONT_DETACH usage. (#153817) --- Include/internal/pycore_lock.h | 5 +++++ Objects/unicodeobject.c | 9 +++++++++ 2 files changed, 14 insertions(+) diff --git a/Include/internal/pycore_lock.h b/Include/internal/pycore_lock.h index e31d8b4e5c68c92..13224a0572934b4 100644 --- a/Include/internal/pycore_lock.h +++ b/Include/internal/pycore_lock.h @@ -34,6 +34,11 @@ _PyMutex_at_fork_reinit(PyMutex *m) typedef enum _PyLockFlags { // Do not detach/release the GIL when waiting on the lock. + // + // Note that code executed while holding a mutex with this flag must + // not detach, reach a safepoint or initiate a stop-the-world pause. + // Otherwise, a non-detaching waiter may remain waiting for this mutex and + // prevent the pause from completing. _Py_LOCK_DONT_DETACH = 0, // Detach/release the GIL while waiting on the lock. diff --git a/Objects/unicodeobject.c b/Objects/unicodeobject.c index eec02f662e79059..4b4f7178ec9faf6 100644 --- a/Objects/unicodeobject.c +++ b/Objects/unicodeobject.c @@ -14726,6 +14726,15 @@ intern_common(PyInterpreterState *interp, PyObject *s /* stolen */, } #endif + // Why _Py_LOCK_DONT_DETACH is used here: waiting for the interned mutex + // must not detach the thread state. Extension code is expected to + // detach before blocking on opaque external synchronization. However, + // the lock used for C++ static initialization is hidden, making + // that difficult, and it is common for C++ extensions to call + // PyUnicode_InternFromString() from static initializers. Detaching here + // can therefore deadlock: a stop-the-world pause may prevent the lock + // owner from reattaching while the pause waits for another attached + // thread blocked on the hidden lock. FT_MUTEX_LOCK_FLAGS(INTERN_MUTEX, _Py_LOCK_DONT_DETACH); PyObject *t; { From afd10f0e28476f5ca0eaeac34ebf85dda2cdc6e5 Mon Sep 17 00:00:00 2001 From: Bhuvansh Date: Sat, 1 Aug 2026 10:21:56 +0000 Subject: [PATCH 3/3] gh-154871: Fix `asyncio.Task.get_context()` crash on uninitialized tasks (#154898) --- Lib/test/test_asyncio/test_tasks.py | 10 ++++++++++ .../2026-07-29-19-48-59.gh-issue-154871.tgZILS.rst | 2 ++ Modules/_asynciomodule.c | 6 +++++- 3 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Library/2026-07-29-19-48-59.gh-issue-154871.tgZILS.rst diff --git a/Lib/test/test_asyncio/test_tasks.py b/Lib/test/test_asyncio/test_tasks.py index 110cd0936c007c9..ad9b09857f8fd2b 100644 --- a/Lib/test/test_asyncio/test_tasks.py +++ b/Lib/test/test_asyncio/test_tasks.py @@ -2924,6 +2924,16 @@ async def coro(): with self.assertRaises(AttributeError): del task._log_destroy_pending + def test_get_context_uninitialized_segfault(self): + # https://github.com/python/cpython/issues/154871 + + class UninitializedTask(self.Task): + def __init__(self, *args, **kwargs): + pass + + task = UninitializedTask() + self.assertIsNone(task.get_context()) + @unittest.skipUnless(hasattr(futures, '_CFuture') and hasattr(tasks, '_CTask'), diff --git a/Misc/NEWS.d/next/Library/2026-07-29-19-48-59.gh-issue-154871.tgZILS.rst b/Misc/NEWS.d/next/Library/2026-07-29-19-48-59.gh-issue-154871.tgZILS.rst new file mode 100644 index 000000000000000..8f0f1096dfe6347 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-29-19-48-59.gh-issue-154871.tgZILS.rst @@ -0,0 +1,2 @@ +Fixed a crash in :meth:`asyncio.Task.get_context` +when called on an uninitialized task. diff --git a/Modules/_asynciomodule.c b/Modules/_asynciomodule.c index ec6f35f161136c1..41384b388142ccc 100644 --- a/Modules/_asynciomodule.c +++ b/Modules/_asynciomodule.c @@ -2786,7 +2786,11 @@ static PyObject * _asyncio_Task_get_context_impl(TaskObj *self) /*[clinic end generated code: output=6996f53d3dc01aef input=87c0b209b8fceeeb]*/ { - return Py_NewRef(self->task_context); + if (self->task_context) { + return Py_NewRef(self->task_context); + } + + Py_RETURN_NONE; } /*[clinic input]