diff --git a/Lib/test/test_free_threading/test_dict.py b/Lib/test/test_free_threading/test_dict.py index 55272a00c3ad50..b600f56687c915 100644 --- a/Lib/test/test_free_threading/test_dict.py +++ b/Lib/test/test_free_threading/test_dict.py @@ -5,7 +5,7 @@ from ast import Or from functools import partial -from threading import Barrier, Thread +from threading import Barrier, Event, Thread from unittest import TestCase try: @@ -290,5 +290,28 @@ def reader(): with threading_helper.start_threads([t1, t2]): pass + def test_racing_update_from_mutating_pair(self): + # gh-159244: dict() must not read the items of a key/value pair + # that another thread empties concurrently. + pair = [object(), object()] + done = Event() + + def writer(): + try: + for _ in range(10000): + pair.clear() + pair.extend((object(), object())) + finally: + done.set() + + def reader(): + while not done.is_set(): + try: + dict([pair]) + except ValueError: + pass + + threading_helper.run_concurrently([writer] + [reader] * 4) + if __name__ == "__main__": unittest.main() diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-10-12-16-00-34.gh-issue-159244.ThiobE.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-12-16-00-34.gh-issue-159244.ThiobE.rst new file mode 100644 index 00000000000000..954cccf38c4367 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-12-16-00-34.gh-issue-159244.ThiobE.rst @@ -0,0 +1,3 @@ +Fix a crash in :class:`dict` and :meth:`dict.update` in the +:term:`free-threaded build` when another thread concurrently mutates a list +used as a key/value pair. Patch by Christian Aurich Zanettini Martins. diff --git a/Objects/dictobject.c b/Objects/dictobject.c index e285f44ca92ddf..c579ff47692f7b 100644 --- a/Objects/dictobject.c +++ b/Objects/dictobject.c @@ -3808,7 +3808,7 @@ merge_from_seq2_lock_held(PyObject *d, PyObject *seq2, int override) return -1; for (i = 0; ; ++i) { - PyObject *key, *value; + PyObject *key = NULL, *value = NULL; Py_ssize_t n; fast = NULL; @@ -3830,7 +3830,13 @@ merge_from_seq2_lock_held(PyObject *d, PyObject *seq2, int override) } goto Fail; } + Py_BEGIN_CRITICAL_SECTION_SEQUENCE_FAST(item); n = PySequence_Fast_GET_SIZE(fast); + if (n == 2) { + key = Py_NewRef(PySequence_Fast_GET_ITEM(fast, 0)); + value = Py_NewRef(PySequence_Fast_GET_ITEM(fast, 1)); + } + Py_END_CRITICAL_SECTION_SEQUENCE_FAST(); if (n != 2) { PyErr_Format(PyExc_ValueError, "dictionary update sequence element #%zd " @@ -3840,10 +3846,6 @@ merge_from_seq2_lock_held(PyObject *d, PyObject *seq2, int override) } /* Update/merge with this (key, value) pair. */ - key = PySequence_Fast_GET_ITEM(fast, 0); - value = PySequence_Fast_GET_ITEM(fast, 1); - Py_INCREF(key); - Py_INCREF(value); if (override) { if (setitem_lock_held((PyDictObject *)d, key, value) < 0) { Py_DECREF(key);