From 5253b1835f613948707e6517a08d5c803696fe70 Mon Sep 17 00:00:00 2001 From: Christian Aurich Zanettini Martins Date: Sat, 24 Oct 2026 08:02:10 -0300 Subject: [PATCH] [3.14] gh-159244: Fix crash in dict() on a concurrently mutated key/value pair (GH-159245) 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. (cherry picked from commit eaab974e8a40a58c2207afd69df0147982fab29d) Co-authored-by: Christian Aurich Zanettini Martins --- 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 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);