]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
[3.13] gh-143308: fix UAF when PickleBuffer is concurrently mutated in a callback...
authorMiss Islington (bot) <31488909+miss-islington@users.noreply.github.com>
Sat, 3 Jan 2026 23:53:59 +0000 (00:53 +0100)
committerGitHub <noreply@github.com>
Sat, 3 Jan 2026 23:53:59 +0000 (23:53 +0000)
gh-143308: fix UAF when PickleBuffer is concurrently mutated in a callback (GH-143312)
(cherry picked from commit 6c53af18f61c074d514e677b469b6201573a59da)

---------------

Co-authored-by: Aaron Wieczorek <aaronw@fastmail.com>
Co-authored-by: Aaron Wieczorek <woz@Aarons-MacBook-Pro.local>
Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com>
Lib/test/pickletester.py
Misc/NEWS.d/next/Library/2025-12-31-17-38-33.gh-issue-143308.lY8UCR.rst [new file with mode: 0644]
Modules/_pickle.c

index c0d4c8f43b9656d138ddbc7e3cbab10df0498aa8..b67a709b721133d4d546ce7d769febe014714d96 100644 (file)
@@ -3761,6 +3761,35 @@ class AbstractPickleTests:
         # 2-D, non-contiguous
         check_array(arr[::2])
 
+    def test_concurrent_mutation_in_buffer_with_bytearray(self):
+        def factory():
+            s = b"a" * 16
+            return bytearray(s), s
+        self.do_test_concurrent_mutation_in_buffer_callback(factory)
+
+    def test_concurrent_mutation_in_buffer_with_memoryview(self):
+        def factory():
+            obj = memoryview(b"a" * 32)[10:26]
+            sub = b"a" * len(obj)
+            return obj, sub
+        self.do_test_concurrent_mutation_in_buffer_callback(factory)
+
+    def do_test_concurrent_mutation_in_buffer_callback(self, factory):
+        # See: https://github.com/python/cpython/issues/143308.
+        class R:
+            def __bool__(self):
+                buf.release()
+                return True
+
+        for proto in range(5, pickle.HIGHEST_PROTOCOL + 1):
+            obj, sub = factory()
+            buf = pickle.PickleBuffer(obj)
+            buffer_callback = lambda _: R()
+
+            with self.subTest(proto=proto, obj=obj, sub=sub):
+                res = self.dumps(buf, proto, buffer_callback=buffer_callback)
+                self.assertIn(sub, res)
+
     def test_evil_class_mutating_dict(self):
         # https://github.com/python/cpython/issues/92930
         from random import getrandbits
diff --git a/Misc/NEWS.d/next/Library/2025-12-31-17-38-33.gh-issue-143308.lY8UCR.rst b/Misc/NEWS.d/next/Library/2025-12-31-17-38-33.gh-issue-143308.lY8UCR.rst
new file mode 100644 (file)
index 0000000..5db43b3
--- /dev/null
@@ -0,0 +1,3 @@
+:mod:`pickle`: fix use-after-free crashes when a :class:`~pickle.PickleBuffer`
+is concurrently mutated by a custom buffer callback during pickling.
+Patch by Bénédikt Tran and Aaron Wieczorek.
index 409b31872d5bdd324cb1e6d9dc78cd47e1557fa5..0f31fc6e70d737b36cef8a8d96a71365f5543bb1 100644 (file)
@@ -2523,53 +2523,61 @@ save_picklebuffer(PickleState *st, PicklerObject *self, PyObject *obj)
                         "PickleBuffer can only be pickled with protocol >= 5");
         return -1;
     }
-    const Py_buffer* view = PyPickleBuffer_GetBuffer(obj);
-    if (view == NULL) {
+    Py_buffer view;
+    if (PyObject_GetBuffer(obj, &view, PyBUF_FULL_RO) != 0) {
         return -1;
     }
-    if (view->suboffsets != NULL || !PyBuffer_IsContiguous(view, 'A')) {
+    if (view.suboffsets != NULL || !PyBuffer_IsContiguous(&view, 'A')) {
         PyErr_SetString(st->PicklingError,
                         "PickleBuffer can not be pickled when "
                         "pointing to a non-contiguous buffer");
-        return -1;
+        goto error;
     }
+
+    int rc = 0;
     int in_band = 1;
     if (self->buffer_callback != NULL) {
         PyObject *ret = PyObject_CallOneArg(self->buffer_callback, obj);
         if (ret == NULL) {
-            return -1;
+            goto error;
         }
         in_band = PyObject_IsTrue(ret);
         Py_DECREF(ret);
         if (in_band == -1) {
-            return -1;
+            goto error;
         }
     }
     if (in_band) {
         /* Write data in-band */
-        if (view->readonly) {
-            return _save_bytes_data(st, self, obj, (const char *)view->buf,
-                                    view->len);
+        if (view.readonly) {
+            rc = _save_bytes_data(st, self, obj, (const char *)view.buf,
+                                  view.len);
         }
         else {
-            return _save_bytearray_data(st, self, obj, (const char *)view->buf,
-                                        view->len);
+            rc = _save_bytearray_data(st, self, obj, (const char *)view.buf,
+                                      view.len);
         }
     }
     else {
         /* Write data out-of-band */
         const char next_buffer_op = NEXT_BUFFER;
         if (_Pickler_Write(self, &next_buffer_op, 1) < 0) {
-            return -1;
+            goto error;
         }
-        if (view->readonly) {
+        if (view.readonly) {
             const char readonly_buffer_op = READONLY_BUFFER;
             if (_Pickler_Write(self, &readonly_buffer_op, 1) < 0) {
-                return -1;
+                goto error;
             }
         }
     }
-    return 0;
+
+    PyBuffer_Release(&view);
+    return rc;
+
+error:
+    PyBuffer_Release(&view);
+    return -1;
 }
 
 /* A copy of PyUnicode_AsRawUnicodeEscapeString() that also translates