From 6e4987c4a28dbfad386cc04730e21a01cb22eaf7 Mon Sep 17 00:00:00 2001 From: Alexey Katsman Date: Mon, 18 May 2026 12:26:27 -0700 Subject: [PATCH 1/3] gh-149816: Fix UAF in Modules/_pickle.c Get a strong reference atomically for list item instead of 2 operations. --- Lib/test/test_free_threading/test_pickle.py | 34 +++++++++++++++++++ ...-05-18-12-42-31.gh-issue-149816.F98iME.rst | 1 + Modules/_pickle.c | 15 +++++--- 3 files changed, 45 insertions(+), 5 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-05-18-12-42-31.gh-issue-149816.F98iME.rst diff --git a/Lib/test/test_free_threading/test_pickle.py b/Lib/test/test_free_threading/test_pickle.py index 85a644dc72ecb4..45ea1bf5f26465 100644 --- a/Lib/test/test_free_threading/test_pickle.py +++ b/Lib/test/test_free_threading/test_pickle.py @@ -40,5 +40,39 @@ def mutator(): with threading_helper.start_threads(threads): pass + def test_pickle_dumps_with_concurrent_list_mutations(self): + # gh-149816: Pickling a list while another thread mutates it + # used to be a UAF in free-threaded mode. batch_list_exact() + # used PyList_GET_ITEM (borrowed) followed by Py_INCREF, and a + # concurrent replace/pop could free the item between those two + # operations. + shared = [list(range(20)) for _ in range(50)] + + def dumper(): + for _ in range(1000): + try: + pickle.dumps(shared) + except (RuntimeError, IndexError): + pass + + def mutator(): + for i in range(1000): + idx = i % 50 + shared[idx] = list(range(i % 20)) + if i % 10 == 0: + try: + shared.pop() + except IndexError: + pass + shared.append([i]) + + 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-05-18-12-42-31.gh-issue-149816.F98iME.rst b/Misc/NEWS.d/next/Library/2026-05-18-12-42-31.gh-issue-149816.F98iME.rst new file mode 100644 index 00000000000000..7ad7bd998e13fa --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-05-18-12-42-31.gh-issue-149816.F98iME.rst @@ -0,0 +1 @@ +Fix a race condition in ``pickle.dumps`` method in free-threaded mode. diff --git a/Modules/_pickle.c b/Modules/_pickle.c index 15d95c658d6f90..b7e337fa996c2e 100644 --- a/Modules/_pickle.c +++ b/Modules/_pickle.c @@ -3188,14 +3188,16 @@ batch_list_exact(PickleState *state, PicklerObject *self, PyObject *obj) assert(obj != NULL); assert(self->proto > 0); assert(PyList_CheckExact(obj)); - assert(PyList_GET_SIZE(obj)); /* Write in batches of BATCHSIZE. */ total = 0; do { if (PyList_GET_SIZE(obj) - total == 1) { - item = PyList_GET_ITEM(obj, total); - Py_INCREF(item); + item = PyList_GetItemRef(obj, total); + if (item == NULL) { + _PyErr_FormatNote("when serializing %T item %zd", obj, total); + return -1; + } int err = save(state, self, item, 0); Py_DECREF(item); if (err < 0) { @@ -3210,8 +3212,11 @@ batch_list_exact(PickleState *state, PicklerObject *self, PyObject *obj) if (_Pickler_Write(self, &mark_op, 1) < 0) return -1; while (total < PyList_GET_SIZE(obj)) { - item = PyList_GET_ITEM(obj, total); - Py_INCREF(item); + item = PyList_GetItemRef(obj, total); + if (item == NULL) { + _PyErr_FormatNote("when serializing %T item %zd", obj, total); + return -1; + } int err = save(state, self, item, 0); Py_DECREF(item); if (err < 0) { From 9e2e0ba481df8ca4b1a32ab09daafce254f29f2f Mon Sep 17 00:00:00 2001 From: "Gregory P. Smith" <68491+gpshead@users.noreply.github.com> Date: Mon, 18 May 2026 16:13:52 -0700 Subject: [PATCH 2/3] reword news --- .../Library/2026-05-18-12-42-31.gh-issue-149816.F98iME.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2026-05-18-12-42-31.gh-issue-149816.F98iME.rst b/Misc/NEWS.d/next/Library/2026-05-18-12-42-31.gh-issue-149816.F98iME.rst index 7ad7bd998e13fa..21e3ae0df57621 100644 --- a/Misc/NEWS.d/next/Library/2026-05-18-12-42-31.gh-issue-149816.F98iME.rst +++ b/Misc/NEWS.d/next/Library/2026-05-18-12-42-31.gh-issue-149816.F98iME.rst @@ -1 +1,2 @@ -Fix a race condition in ``pickle.dumps`` method in free-threaded mode. +Fix a potential use after free condition in :func:`pickle.dumps` in free-threaded +mode when serializing lists. From d1e3981f5115bfaad27b604baa01a0ab94a3f4fa Mon Sep 17 00:00:00 2001 From: Alexey Katsman Date: Mon, 18 May 2026 16:24:18 -0700 Subject: [PATCH 3/3] gh-149816: Check for changed list size in Modules/_pickle.c --- Modules/_pickle.c | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/Modules/_pickle.c b/Modules/_pickle.c index b7e337fa996c2e..253ba7f743ec71 100644 --- a/Modules/_pickle.c +++ b/Modules/_pickle.c @@ -3179,7 +3179,7 @@ static int batch_list_exact(PickleState *state, PicklerObject *self, PyObject *obj) { PyObject *item = NULL; - Py_ssize_t this_batch, total; + Py_ssize_t this_batch, total, list_size; const char append_op = APPEND; const char appends_op = APPENDS; @@ -3189,10 +3189,12 @@ batch_list_exact(PickleState *state, PicklerObject *self, PyObject *obj) assert(self->proto > 0); assert(PyList_CheckExact(obj)); + list_size = PyList_GET_SIZE(obj); + /* Write in batches of BATCHSIZE. */ total = 0; do { - if (PyList_GET_SIZE(obj) - total == 1) { + if (list_size - total == 1) { item = PyList_GetItemRef(obj, total); if (item == NULL) { _PyErr_FormatNote("when serializing %T item %zd", obj, total); @@ -3229,8 +3231,14 @@ batch_list_exact(PickleState *state, PicklerObject *self, PyObject *obj) } if (_Pickler_Write(self, &appends_op, 1) < 0) return -1; + if (PyList_GET_SIZE(obj) != list_size) { + PyErr_Format( + PyExc_RuntimeError, + "list changed size during iteration"); + return -1; + } - } while (total < PyList_GET_SIZE(obj)); + } while (total < list_size); return 0; }