diff --git a/Lib/test/test_free_threading/test_dict.py b/Lib/test/test_free_threading/test_dict.py index 4a812275143bc4d..89f3ae778db812a 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: @@ -356,5 +356,28 @@ def writer(): threading_helper.run_concurrently([reader, writer]) + 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 000000000000000..954cccf38c43676 --- /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 eed9bd92c55bf2b..ed36d3c15ca27f2 100644 --- a/Objects/dictobject.c +++ b/Objects/dictobject.c @@ -4115,7 +4115,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; @@ -4137,7 +4137,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 " @@ -4147,10 +4153,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);