]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-149816: Fix UAF in Modules/_pickle.c (GH-150024)
authorAlexey Katsman <alex2007508@gmail.com>
Tue, 19 May 2026 22:11:17 +0000 (15:11 -0700)
committerGitHub <noreply@github.com>
Tue, 19 May 2026 22:11:17 +0000 (00:11 +0200)
Co-authored-by: Gregory P. Smith <68491+gpshead@users.noreply.github.com>
Lib/test/test_free_threading/test_pickle.py
Misc/NEWS.d/next/Library/2026-05-18-12-42-31.gh-issue-149816.F98iME.rst [new file with mode: 0644]
Modules/_pickle.c

index 85a644dc72ecb44026871513a368941972740aca..45ea1bf5f26465a37990fd2b2318628faf9d818a 100644 (file)
@@ -40,5 +40,39 @@ class TestPickleFreeThreading(unittest.TestCase):
         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 (file)
index 0000000..21e3ae0
--- /dev/null
@@ -0,0 +1,2 @@
+Fix a potential use after free condition in :func:`pickle.dumps` in free-threaded
+mode when serializing lists.
index 15d95c658d6f9062111b08869a8fee4d5f89e34f..253ba7f743ec715dd652fddc8e9de3c2df0d18a1 100644 (file)
@@ -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;
 }