From 039f60582e67e59ea226f7fa680df327b9bd0539 Mon Sep 17 00:00:00 2001 From: Christian Aurich Zanettini Martins Date: Sat, 24 Oct 2026 08:02:10 -0300 Subject: [PATCH] [3.15] 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 2180613f689c897..35b221d447e75c9 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: @@ -437,5 +437,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 1c51305a499aae6..5caf02f9e0c69fe 100644 --- a/Objects/dictobject.c +++ b/Objects/dictobject.c @@ -4066,7 +4066,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; @@ -4088,7 +4088,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 " @@ -4098,10 +4104,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);