[3.14] gh-143308: fix UAF when PickleBuffer is concurrently mutated in a callback (GH-143312) (#143396)

gh-143308: fix UAF when PickleBuffer is concurrently mutated in a callback (GH-143312)
(cherry picked from commit 6c53af18f6)

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

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>
This commit is contained in:
Miss Islington (bot) 2026-01-03 23:27:35 +01:00 committed by GitHub
parent 6eafcaee64
commit c99d87db1f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 55 additions and 15 deletions

View file

@ -4090,6 +4090,35 @@ def check_array(arr):
# 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

View file

@ -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.

View file

@ -2561,53 +2561,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