From 0279ee2bea4165994e4d09e2d1590568586abe3b Mon Sep 17 00:00:00 2001 From: Christian Aurich Zanettini Martins Date: Mon, 12 Oct 2026 13:00:39 -0300 Subject: [PATCH] gh-159244: Fix crash in dict() on a concurrently mutated key/value pair In the free-threaded build, dict() and dict.update() checked that a list key/value pair had two items and then read them with borrowed references without holding its lock, so another thread could empty the list in between and the items were read from a freed or NULL array. Read the size and both items under Py_BEGIN_CRITICAL_SECTION_SEQUENCE_FAST, as bytes.join() and str.join() already do. --- Lib/test/test_free_threading/test_dict.py | 25 ++++++++++++++++++- ...-10-12-16-00-34.gh-issue-159244.ThiobE.rst | 3 +++ Objects/dictobject.c | 12 +++++---- 3 files changed, 34 insertions(+), 6 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-10-12-16-00-34.gh-issue-159244.ThiobE.rst 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);