From: Miss Islington (bot) <31488909+miss-islington@users.noreply.github.com> Date: Sun, 17 May 2026 09:30:00 +0000 (+0200) Subject: [3.14] gh-146452: Fix pickle segfault on concurrent mutation of dict in pickle (GH... X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=f380f85e6105180ab1c2e40afdbd14f2e12e082a;p=thirdparty%2FPython%2Fcpython.git [3.14] gh-146452: Fix pickle segfault on concurrent mutation of dict in pickle (GH-146470) (#149940) gh-146452: Fix pickle segfault on concurrent mutation of dict in pickle (GH-146470) (cherry picked from commit e62a61177f8b793d787e337034a740ca75c1ab44) Co-authored-by: Farhan Saif Co-authored-by: Kumar Aditya --- diff --git a/Lib/test/test_free_threading/test_pickle.py b/Lib/test/test_free_threading/test_pickle.py new file mode 100644 index 000000000000..85a644dc72ec --- /dev/null +++ b/Lib/test/test_free_threading/test_pickle.py @@ -0,0 +1,44 @@ +import pickle +import threading +import unittest + +from test.support import threading_helper + + +@threading_helper.requires_working_threading() +class TestPickleFreeThreading(unittest.TestCase): + + def test_pickle_dumps_with_concurrent_dict_mutation(self): + # gh-146452: Pickling a dict while another thread mutates it + # used to segfault. batch_dict_exact() iterated dict items via + # PyDict_Next() which returns borrowed references, and a + # concurrent pop/replace could free the value before Py_INCREF + # got to it. + shared = {str(i): list(range(20)) for i in range(50)} + + def dumper(): + for _ in range(1000): + try: + pickle.dumps(shared) + except RuntimeError: + # "dictionary changed size during iteration" is expected + pass + + def mutator(): + for j in range(1000): + key = str(j % 50) + shared[key] = list(range(j % 20)) + if j % 10 == 0: + shared.pop(key, None) + shared[key] = [j] + + threads = [] + for _ in range(10): + threads.append(threading.Thread(target=dumper)) + threads.append(threading.Thread(target=mutator)) + + with threading_helper.start_threads(threads): + pass + +if __name__ == "__main__": + unittest.main() diff --git a/Misc/NEWS.d/next/Library/2026-03-26-09-30-00.gh-issue-146452.Y2N6qZ8J.rst b/Misc/NEWS.d/next/Library/2026-03-26-09-30-00.gh-issue-146452.Y2N6qZ8J.rst new file mode 100644 index 000000000000..99f3cce33497 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-03-26-09-30-00.gh-issue-146452.Y2N6qZ8J.rst @@ -0,0 +1,2 @@ +Fix segfault in :mod:`pickle` when pickling a dictionary concurrently +mutated by another thread in the free-threaded build. diff --git a/Modules/_pickle.c b/Modules/_pickle.c index a1cf258c7083..75e1c4dea85e 100644 --- a/Modules/_pickle.c +++ b/Modules/_pickle.c @@ -3353,7 +3353,7 @@ batch_dict(PickleState *state, PicklerObject *self, PyObject *iter, PyObject *or * Note that this currently doesn't work for protocol 0. */ static int -batch_dict_exact(PickleState *state, PicklerObject *self, PyObject *obj) +batch_dict_exact_impl(PickleState *state, PicklerObject *self, PyObject *obj) { PyObject *key = NULL, *value = NULL; int i; @@ -3424,6 +3424,18 @@ error: return -1; } +/* gh-146452: Wrap the dict iteration in a critical section to prevent + concurrent mutation from invalidating PyDict_Next() iteration state. */ +static int +batch_dict_exact(PickleState *state, PicklerObject *self, PyObject *obj) +{ + int ret; + Py_BEGIN_CRITICAL_SECTION(obj); + ret = batch_dict_exact_impl(state, self, obj); + Py_END_CRITICAL_SECTION(); + return ret; +} + static int save_dict(PickleState *state, PicklerObject *self, PyObject *obj) {