From 003e4510690801166409744d048a55763d5f8d9d Mon Sep 17 00:00:00 2001 From: "Gregory P. Smith" Date: Thu, 22 Oct 2026 08:32:53 +0000 Subject: [PATCH] gh-160303: Fix bytearray state after a failed del b[:n] When shrinking the storage of a bytearray which has a logical offset, allocate the new buffer before modifying the object, so that bytearray_setslice_linear() can restore ob_start if the allocation fails. --- Lib/test/test_bytes.py | 20 +++++++++ ...-10-22-15-40-00.gh-issue-160303.q7Zb2K.rst | 3 ++ Objects/bytearrayobject.c | 41 +++++++++++-------- 3 files changed, 46 insertions(+), 18 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-10-22-15-40-00.gh-issue-160303.q7Zb2K.rst diff --git a/Lib/test/test_bytes.py b/Lib/test/test_bytes.py index 9c36fbed9eb19a..3a5f7511bf6815 100644 --- a/Lib/test/test_bytes.py +++ b/Lib/test/test_bytes.py @@ -1687,6 +1687,26 @@ def test_resize_error(self): ba.resize(1) self.assertEqual(ba, expected) + @support.nomemtest + def test_remove_prefix_error(self): + # If the storage cannot be shrunk after removing a prefix + # (MemoryError), the bytearray must be left unchanged. + for logical_offset in (False, True): + with self.subTest(logical_offset=logical_offset): + ba = bytearray(b'0123456789' * 4) + if logical_offset: + del ba[:3] + expected = ba.copy() + with inject_memory_error(self): + del ba[:30] + self.assertEqual(ba, expected) + with inject_memory_error(self): + ba[:30] = b'xy' + self.assertEqual(ba, expected) + # The bytearray is still usable + del ba[:30] + self.assertEqual(ba, expected[30:]) + def test_take_bytes(self): ba = bytearray(b'ab') self.assertEqual(ba.take_bytes(), b'ab') diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-10-22-15-40-00.gh-issue-160303.q7Zb2K.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-22-15-40-00.gh-issue-160303.q7Zb2K.rst new file mode 100644 index 00000000000000..d0766b48ad0275 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-22-15-40-00.gh-issue-160303.q7Zb2K.rst @@ -0,0 +1,3 @@ +Fix :class:`bytearray` reading and writing memory outside of its buffer +after ``del b[:n]`` or ``b[:n] = shorter`` failed with :exc:`MemoryError`. +The bytearray is now left unchanged. diff --git a/Objects/bytearrayobject.c b/Objects/bytearrayobject.c index dfd041b0152bdd..716342d29c96aa 100644 --- a/Objects/bytearrayobject.c +++ b/Objects/bytearrayobject.c @@ -238,28 +238,32 @@ bytearray_resize_storage(PyByteArrayObject *self, Py_ssize_t size = Py_SIZE(self); - /* Re-align data to the start of the allocation. */ - char *old_start = self->ob_start; - if (self->ob_start != self->ob_bytes) { - /* 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. + if (self->ob_start != self->ob_bytes && new_size < size) { + /* Shrink with a logical offset: copy the bytes to keep into a new + allocation. Allocate first, so that the bytearray (including + ob_start) is left unchanged on error: bytearray_setslice_linear() + relies on it to restore the bytearray after a failed "del b[:n]". */ - Py_ssize_t move = Py_MIN(new_size, size); - memmove(self->ob_bytes, self->ob_start, move); - self->ob_start = self->ob_bytes; + PyObject *bytes = PyBytes_FromStringAndSize(NULL, alloc); + if (bytes == NULL) { + return -1; + } + memcpy(PyBytes_AS_STRING(bytes), self->ob_start, new_size); + Py_SETREF(self->ob_bytes_object, bytes); } + else { + /* Re-align data to the start of the allocation. */ + if (self->ob_start != self->ob_bytes) { + /* optimization tradeoff: This is faster than a new allocation + when the number of bytes being added in a resize is small. */ + memmove(self->ob_bytes, self->ob_start, size); + self->ob_start = self->ob_bytes; + } - if (_PyBytes_ResizeKeepOnError(&self->ob_bytes_object, alloc) < 0) { - if (old_start != self->ob_bytes && new_size < size) { - // Move remaining bytes - Py_ssize_t moved = new_size; - Py_ssize_t remaining = size - moved; - memmove(self->ob_bytes + moved, old_start + moved, remaining); + if (_PyBytes_ResizeKeepOnError(&self->ob_bytes_object, alloc) < 0) { + bytearray_write_trailing_null_byte(self); + return -1; } - bytearray_write_trailing_null_byte(self); - return -1; } assert(_PyBytes_IsMutable(self->ob_bytes_object)); return 0; @@ -649,6 +653,7 @@ bytearray_setslice_linear(PyByteArrayObject *self, /* memmove() removed bytes, the bytearray object cannot be restored in its previous state. */ Py_SET_SIZE(self, Py_SIZE(self) + growth); + bytearray_write_trailing_null_byte(self); res = -1; } buf = PyByteArray_AS_STRING(self);