Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions Lib/test/test_pickle.py
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,38 @@ class CPicklingErrorTests(PyPicklingErrorTests):
class CPicklerTests(PyPicklerTests):
pickler = _pickle.Pickler
unpickler = _pickle.Unpickler
def test_release_in_callback_keepalive(self):
Comment thread
picnixz marked this conversation as resolved.
Outdated
Comment thread
picnixz marked this conversation as resolved.
Outdated
base = bytearray(b'A' * 16)
pb = pickle.PickleBuffer(base)

class Evil:
def __bool__(self):
pb.release()
return True

def callback(p):
return Evil()

for proto in range(5, pickle.HIGHEST_PROTOCOL + 1):
result = self.dumps(pb, proto, buffer_callback=callback)
self.assertIn(b'A' * 16, result)

def test_release_in_callback_complex_keepalive(self):
base = bytearray(b'A' * 32)
view = memoryview(base)[10:26]
pb = pickle.PickleBuffer(view)

class Evil:
def __bool__(self):
pb.release()
return True

def callback(p):
return Evil()

for proto in range(5, pickle.HIGHEST_PROTOCOL + 1):
result = self.dumps(pb, proto, buffer_callback=callback)
self.assertIn(b'A' * 16, result)

class CPersPicklerTests(PyPersPicklerTests):
pickler = _pickle.Pickler
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Fixed Use-After-Free in _pickle.c by incrementing PickleBuffer exports
during pickling.
Comment thread
picnixz marked this conversation as resolved.
Outdated
29 changes: 22 additions & 7 deletions Modules/_pickle.c
Original file line number Diff line number Diff line change
Expand Up @@ -2646,43 +2646,58 @@ save_picklebuffer(PickleState *st, PicklerObject *self, PyObject *obj)
"pointing to a non-contiguous buffer");
Comment on lines 2645 to 2646
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of curiosity, but do we have a test for this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Neither of the tests added test for non-contiguous, but should be straightforward to add a test against a buffer with strides...

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just wanted to be sure that this branch was covered by existing tests or by other tests. We can increase the coverage in a follow-up PR.

return -1;
}

int rc = 0;
Py_buffer keepalive_view; // to ensure that 'view' is kept alive
if (PyObject_GetBuffer(view->obj, &keepalive_view, PyBUF_FULL_RO) != 0) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could not we simply apply PyObject_GetBuffer() to the PickleBuffer object itself? We can then get rid of PyPickleBuffer_GetBuffer().

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potentially! I'd want to test it. @picnixz what do you think? Since this hasn't been merged yet now would seem to be the time.

Copy link
Copy Markdown
Member

@picnixz picnixz Jan 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should work. PyPickleBuffer_GetBuffer just returns a (borrowed) ref so it doesn't get any buffer on it. I think it should work so feel free to check!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This worked great, thanks for the suggestion @serhiy-storchaka. @picnixz The change was a little more involved than expected, but by doing this I was able to remove a redundant view, update all the view-> calls, and the exit: label. All tests pass and confirmed that the patch still holds.

return -1;
}

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,
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,
rc = _save_bytearray_data(st, self, obj, (const char *)view->buf,
view->len);
}
goto exit;
}
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) {
const char readonly_buffer_op = READONLY_BUFFER;
if (_Pickler_Write(self, &readonly_buffer_op, 1) < 0) {
return -1;
goto error;
}
}
}
return 0;

exit:
PyBuffer_Release(&keepalive_view);
return rc;

error:
PyBuffer_Release(&keepalive_view);
return -1;
}

/* A copy of PyUnicode_AsRawUnicodeEscapeString() that also translates
Expand Down
Loading