GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — gh-157242: Fix PyBytesWriter_Resize() on MemoryError by vstinner · Pull Request #157243 · python/cpython

gh-157242: Fix PyBytesWriter_Resize() on MemoryError #157243

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

Sign up for GitHub

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
vstinner merged 23 commits into python:main from vstinner:writer_resize2
Sep 11, 2026

Uh oh!

There was an error while loading. Please reload this page.

Merged

gh-157242: Fix PyBytesWriter_Resize() on MemoryError #157243

Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
d0441c4
gh-157242: Leave bytearray unchanged if resize() fails
vstinner Sep 10, 2026
5f1ef32
Add NEWS entries
vstinner Sep 10, 2026
a1e64d3
Fix resize() with non-zero start
vstinner Sep 10, 2026
4b4bcfb
Reimplement _PyBytes_Resize() via _PyBytes_ResizeKeepOnError()
vstinner Sep 10, 2026
ba27fe0
Merge branch 'main' into writer_resize2
vstinner Sep 10, 2026
4d581fa
Fix resize() when shrinking the bytearray
vstinner Sep 10, 2026
f51cba5
Fix take_bytes()
vstinner Sep 10, 2026
7116757
Fix typo in test_take_bytes_error(): pass to_take argument
vstinner Sep 11, 2026
0ca5c9c
Add inject_memory_error()
vstinner Sep 11, 2026
ba1e2c8
Remove alloc parameter of bytearray_reinit_from_bytes()
vstinner Sep 11, 2026
e9e1f02
Add bytearray_realign_data_lock_held()
vstinner Sep 11, 2026
a0a3753
Don't call _PyReftracerTrack() on dangling pointer
vstinner Sep 11, 2026
53c3b51
Add test_bytes_resize_tracer()
vstinner Sep 11, 2026
fb67d23
Write trailing null bytes on MemoryError
vstinner Sep 11, 2026
6762d16
Add bytearray_write_trailing_null_byte()
vstinner Sep 11, 2026
909270c
Optimize bytearray_realign_data_lock_held()
vstinner Sep 11, 2026
0a33fd4
Fix bytearray_resize_storage()
vstinner Sep 11, 2026
dfb05bf
Add one more take_bytes() error test
vstinner Sep 11, 2026
2f1e965
no need for testing PyBytesWriter_Resize() shrinking
vstinner Sep 11, 2026
306f132
Merge branch 'main' into writer_resize2
vstinner Sep 11, 2026
a9a1423
Revert bytearray changes
vstinner Sep 11, 2026
9fa1305
Resize: set pv earlier
vstinner Sep 11, 2026
9ce6a99
Move PyBytesWriter test
vstinner Sep 11, 2026
File filter
Filter by extension

Clear filters
Conversations
Failed to load comments.
Loading

Uh oh!

There was an error while loading. Please reload this page.

Jump to
Jump to file
Failed to load files.
Loading

Uh oh!

There was an error while loading. Please reload this page.

Diff view
Diff view

Some comments aren't visible on the classic Files Changed page.

There are no files selected for viewing

2 changes: 2 additions & 0 deletions 2 Include/internal/pycore_bytesobject.h
Show comments
View file
Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ PyAPI_FUNC(PyObject *) _PyBytes_Repeat(PyObject *self, Py_ssize_t n);
*/
#define _PyBytesObject_SIZE (offsetof(PyBytesObject, ob_sval) + 1)

extern int _PyBytes_ResizeKeepOnError(PyObject **pv, Py_ssize_t newsize);

/* --- PyBytesWriter ------------------------------------------------------ */

struct PyBytesWriter {
Expand Down
59 changes: 59 additions & 0 deletions 59 Lib/test/test_bytes.py
Show comments
View file
Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -1555,6 +1555,46 @@ def test_resize(self):
self.assertRaises(MemoryError, bytearray().resize, sys.maxsize)
self.assertRaises(MemoryError, bytearray(1000).resize, sys.maxsize)

def test_resize_error(self):
# gh-157242: If bytearray.resize() fails (memory allocation failure),
# the bytearray must be left unchanged.
_testcapi = import_helper.import_module('_testcapi')

# Simple bytearray
data = b'some data'
ba = bytearray(data)
with self.assertRaises(MemoryError):
try:
_testcapi.set_nomemory(0)
Comment thread
vstinner marked this conversation as resolved.
Resolved

Uh oh!

There was an error while loading. Please reload this page.

ba.resize(1024)
finally:
_testcapi.remove_mem_hooks()
self.assertEqual(ba, bytearray(data))

# growing bytearray with non-zero logical start
ba = bytearray(b'0123456789')
expected = ba[3:]
del ba[:3]
with self.assertRaises(MemoryError):
try:
_testcapi.set_nomemory(0)
ba.resize(1024)
finally:
_testcapi.remove_mem_hooks()
self.assertEqual(ba, expected)

# shrink bytearray with non-zero logical start
ba = bytearray(b'0123456789')
expected = ba[3:]
del ba[:3]
with self.assertRaises(MemoryError):
try:
_testcapi.set_nomemory(0)
ba.resize(1)
finally:
_testcapi.remove_mem_hooks()
self.assertEqual(ba, expected)

def test_take_bytes(self):
ba = bytearray(b'ab')
self.assertEqual(ba.take_bytes(), b'ab')
Expand Down Expand Up @@ -1619,6 +1659,25 @@ def test_take_bytes(self):
self.assertEqual(ba, bytearray(b'A'))
self.assertEqual(ord(b'c'), ord('c'))

def test_take_bytes_error(self):
# gh-157242: If bytearray.take_bytes() fails (memory allocation
# failure), the bytearray must be left unchanged.
_testcapi = import_helper.import_module('_testcapi')

for mem_error in (0, 1):
for to_take in (5, None):
with self.subTest(mem_error=mem_error, to_take=to_take):
ba = bytearray(b'0123456789')
expected = ba[3:]
del ba[:3]
with self.assertRaises(MemoryError):
try:
_testcapi.set_nomemory(mem_error)
ba.take_bytes(5)
Comment thread
vstinner marked this conversation as resolved.
Resolved

Uh oh!

There was an error while loading. Please reload this page.

finally:
_testcapi.remove_mem_hooks()
self.assertEqual(ba, expected)

@support.cpython_only # tests an implementation detail
def test_take_bytes_optimization(self):
# Validate optimization around taking lots of little chunks out of a
Expand Down
15 changes: 15 additions & 0 deletions 15 Lib/test/test_capi/test_bytes.py
Show comments
View file
Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,21 @@ def test_resize(self):
writer.resize(len(b'number=123456'), b'456')
self.assertEqual(writer.finish(), self.result_type(b'number=123456'))

def test_resize_error(self):
small_buffer = _testcapi.PyBytesWriter_small_buffer
init = b'x' * (small_buffer * 2)
writer = self.create_writer(len(init), init)
size = len(init) + 100
try:
with self.assertRaises(MemoryError):
_testcapi.set_nomemory(0)
writer.resize(size, b'')
finally:
_testcapi.remove_mem_hooks()
suffix = b'still working'
writer.write_bytes(suffix, -1)
self.assertEqual(writer.finish(), self.result_type(init + suffix))

def test_format_i(self):
# Test PyBytesWriter_Format()
writer = self.create_writer()
Expand Down
3 changes: 3 additions & 0 deletions 3 Misc/NEWS.d/next/C_API/2026-09-10-02-51-26.gh-issue-157242.tfXIsf.rst
Show comments
View file
Open in desktop
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Fix :c:func:`PyBytesWriter_Resize` to handle properly memory allocation
failure. Leave the writer unchanged on error, instead of leaving it in an
inconsistent state. Patch by Victor Stinner.
3 changes: 3 additions & 0 deletions 3 Misc/NEWS.d/next/Core_and_Builtins/2026-09-10-02-50-14.gh-issue-157242.LsqOUJ.rst
Show comments
View file
Open in desktop
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
If :meth:`bytearray.resize` or :meth:`bytearray.take_bytes` fails, leave the
:class:`bytearray` unchanged, instead of clearing it. Patch by Victor
Stinner.
76 changes: 50 additions & 26 deletions 76 Objects/bytearrayobject.c
Show comments
View file
Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -281,28 +281,39 @@ bytearray_resize_lock_held(PyObject *self, Py_ssize_t requested_size)
return -1;
}

/* Re-align data to the start of the allocation. */
if (logical_offset > 0) {
/* optimization tradeoff: This is faster than a new allocation when
the number of bytes being removed in a resize is small; for large
size changes it may be better to just make a new bytes object as
_PyBytes_Resize will do a malloc + memcpy internally. */
memmove(obj->ob_bytes, obj->ob_start,
Py_MIN(requested_size, Py_SIZE(self)));
}
if (logical_offset == 0 || requested_size >= Py_SIZE(self)) {
/* Re-align data to the start of the allocation. */
if (logical_offset > 0) {
/* optimization tradeoff: This is faster than a new allocation when
the number of bytes being removed in a resize is small; for large
size changes it may be better to just make a new bytes object as
_PyBytes_Resize will do a malloc + memcpy internally. */
memmove(obj->ob_bytes, obj->ob_start, Py_SIZE(self));
Comment thread
vstinner marked this conversation as resolved.
Resolved

Uh oh!

There was an error while loading. Please reload this page.

obj->ob_start = obj->ob_bytes;
}

int ret = _PyBytes_Resize(&obj->ob_bytes_object, alloc);
if (ret == -1) {
obj->ob_bytes_object = Py_GetConstant(Py_CONSTANT_EMPTY_BYTES);
size = alloc = 0;
if (_PyBytes_ResizeKeepOnError(&obj->ob_bytes_object, alloc) < 0) {
return -1;
}
}
else {
// Using memmove() would be unsafe, since _PyBytes_ResizeKeepOnError()
// failure code path would be unable to restore the bytearray to its
// previous state.
PyObject *resized = PyBytes_FromStringAndSize(NULL, requested_size);
if (resized == NULL) {
return -1;
}
memcpy(PyBytes_AS_STRING(resized), obj->ob_start, requested_size);
Py_SETREF(obj->ob_bytes_object, resized);
}

bytearray_reinit_from_bytes(obj, size, alloc);
if (alloc != size) {
/* Add mid-buffer null; end provided by bytes. */
obj->ob_bytes[size] = '\0';
}

return ret;
return 0;
}

int
Expand Down Expand Up @@ -1610,6 +1621,7 @@ bytearray_take_bytes_impl(PyByteArrayObject *self, PyObject *n)
}

Py_ssize_t remaining_length = size - to_take;

// optimization: If taking less than leaving, just copy the small to_take
// portion out and move ob_start.
if (to_take < remaining_length) {
Expand All @@ -1631,18 +1643,30 @@ bytearray_take_bytes_impl(PyByteArrayObject *self, PyObject *n)
memcpy(PyBytes_AS_STRING(remaining), self->ob_start + to_take,
remaining_length);

// If the bytes are offset inside the buffer must first align.
if (self->ob_start != self->ob_bytes) {
memmove(self->ob_bytes, self->ob_start, to_take);
self->ob_start = self->ob_bytes;
}
size_t logical_offset = (size_t) (self->ob_start - self->ob_bytes);
if (logical_offset == 0 || remaining_length == 0) {
// If the bytes are offset inside the buffer must first align.
if (logical_offset != 0) {
memmove(self->ob_bytes, self->ob_start, to_take);
self->ob_start = self->ob_bytes;
}

if (_PyBytes_Resize(&self->ob_bytes_object, to_take) == -1) {
assert(self->ob_bytes_object == NULL);
self->ob_bytes_object = Py_GetConstant(Py_CONSTANT_EMPTY_BYTES);
bytearray_reinit_from_bytes(self, 0, 0);
Py_DECREF(remaining);
return NULL;
if (_PyBytes_ResizeKeepOnError(&self->ob_bytes_object, to_take) == -1) {
Py_DECREF(remaining);
return NULL;
}
}
else {
// Using memmove() would be unsafe, since _PyBytes_ResizeKeepOnError()
Comment thread
vstinner marked this conversation as resolved.
Resolved

Uh oh!

There was an error while loading. Please reload this page.

// failure code path would be unable to restore the bytearray to its
// previous state.
PyObject *resized = PyBytes_FromStringAndSize(NULL, to_take);
if (resized == NULL) {
Py_DECREF(remaining);
return NULL;
}
memcpy(PyBytes_AS_STRING(resized), self->ob_start, to_take);
Py_SETREF(self->ob_bytes_object, resized);
}

// Point the bytearray towards the buffer with the remaining data.
Expand Down
77 changes: 50 additions & 27 deletions 77 Objects/bytesobject.c
Show comments
View file
Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -3344,69 +3344,91 @@ PyBytes_ConcatAndDel(PyObject **pv, PyObject *w)
does *not* include that), and a trailing \0 byte is stored.
*/

// Similar to _PyBytes_Resize(), but leaves the object unchanged on error.
int
_PyBytes_Resize(PyObject **pv, Py_ssize_t newsize)
_PyBytes_ResizeKeepOnError(PyObject **pv, Py_ssize_t newsize)
{
PyObject *v;
PyBytesObject *sv;
v = *pv;
PyObject *v = *pv;
PyObject *result;

if (!PyBytes_Check(v) || newsize < 0) {
*pv = 0;
Py_DECREF(v);
PyErr_BadInternalCall();
return -1;
}

Py_ssize_t oldsize = PyBytes_GET_SIZE(v);
if (oldsize == newsize) {
/* return early if newsize equals to v->ob_size */
return 0;
}

if (oldsize == 0) {
*pv = _PyBytes_FromSize(newsize, 0);
result = _PyBytes_FromSize(newsize, 0);
if (result == NULL) {
return -1;
}
*pv = result;
Py_DECREF(v);
return (*pv == NULL) ? -1 : 0;
return 0;
}

if (newsize == 0) {
*pv = bytes_get_empty();
*pv = bytes_get_empty(); // cannot fail
Py_DECREF(v);
return 0;
}

if (!_PyObject_IsUniquelyReferenced(v)) {
// Allocate and then copy so we don't get a shared immortal
// one-character singleton!
*pv = _PyBytes_FromSize(newsize, 0);
if (*pv) {
memcpy(PyBytes_AS_STRING(*pv), PyBytes_AS_STRING(v),
Py_MIN(oldsize, newsize));
result = _PyBytes_FromSize(newsize, 0);
if (!result) {
return -1;
}

memcpy(PyBytes_AS_STRING(result), PyBytes_AS_STRING(v),
Py_MIN(oldsize, newsize));
*pv = result;
Py_DECREF(v);
return (*pv == NULL) ? -1 : 0;
return 0;
}

assert(v != bytes_get_empty());
result = (PyObject *)PyObject_Realloc(v, PyBytesObject_SIZE + newsize);
if (result == NULL) {
PyErr_NoMemory();
return -1;
}

#ifdef Py_TRACE_REFS
_Py_ForgetReference(v);
#endif
_PyReftracerTrack(v, PyRefTracer_DESTROY);
Comment thread
vstinner marked this conversation as resolved.
Resolved

Uh oh!

There was an error while loading. Please reload this page.

*pv = (PyObject *)
PyObject_Realloc(v, PyBytesObject_SIZE + newsize);
if (*pv == NULL) {
#ifdef Py_REF_DEBUG
_Py_DecRefTotal(_PyThreadState_GET());
#endif
PyObject_Free(v);
PyErr_NoMemory();
return -1;
}
_Py_NewReferenceNoTotal(*pv);
sv = (PyBytesObject *) *pv;

v = result;
_Py_NewReferenceNoTotal(v);
PyBytesObject *sv = (PyBytesObject *)v;
Py_SET_SIZE(sv, newsize);
sv->ob_sval[newsize] = '\0';
set_ob_shash(sv, -1); /* invalidate cached hash value */
*pv = v;
return 0;
}


int
_PyBytes_Resize(PyObject **pv, Py_ssize_t newsize)
{
int res = _PyBytes_ResizeKeepOnError(pv, newsize);
if (res < 0) {
PyObject *v = *pv;
*pv = NULL;
Py_DECREF(v);
}
return res;
}


/*********************** Bytes Iterator ****************************/

typedef struct {
Expand Down Expand Up @@ -3644,7 +3666,8 @@ byteswriter_resize(PyBytesWriter *writer, Py_ssize_t size, int resize)
}
}
else {
if (_PyBytes_Resize(&writer->obj, size)) {
if (_PyBytes_ResizeKeepOnError(&writer->obj, size)) {
assert(writer->obj != NULL);
return -1;
}
}
Expand Down
Loading
Oops, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

Add this suggestion to a batch that can be applied as a single commit. This suggestion is invalid because no changes were made to the code. Suggestions cannot be applied while the pull request is closed. Suggestions cannot be applied while viewing a subset of changes. Only one suggestion per line can be applied in a batch. Add this suggestion to a batch that can be applied as a single commit. Applying suggestions on deleted lines is not supported. You must change the existing code in this line in order to create a valid suggestion. Outdated suggestions cannot be applied. This suggestion has been applied or marked resolved. Suggestions cannot be applied from pending reviews. Suggestions cannot be applied on multi-line comments. Suggestions cannot be applied while the pull request is queued to merge. Suggestion cannot be applied right now. Please check back later.
You are visitor 01402520 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.