https://github.com/python/cpython/commit/d095ceb0f420a22353271a8adaf5a83433d018e5
commit: d095ceb0f420a22353271a8adaf5a83433d018e5
branch: main
author: Alexey Katsman <[email protected]>
committer: encukou <[email protected]>
date: 2026-05-20T00:11:17+02:00
summary:

gh-149816: Fix UAF in Modules/_pickle.c (GH-150024)

Co-authored-by: Gregory P. Smith <[email protected]>

files:
A Misc/NEWS.d/next/Library/2026-05-18-12-42-31.gh-issue-149816.F98iME.rst
M Lib/test/test_free_threading/test_pickle.py
M Modules/_pickle.c

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..21e3ae0df57621
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-05-18-12-42-31.gh-issue-149816.F98iME.rst
@@ -0,0 +1,2 @@
+Fix a potential use after free condition in :func:`pickle.dumps` in 
free-threaded
+mode when serializing lists.
diff --git a/Modules/_pickle.c b/Modules/_pickle.c
index 15d95c658d6f90..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;
@@ -3188,14 +3188,18 @@ 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));
+
+    list_size = 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);
+        if (list_size - total == 1) {
+            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 +3214,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) {
@@ -3224,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;
 }

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to