diff --git a/Lib/test/test_bytes.py b/Lib/test/test_bytes.py index 9c36fbed9eb19a..411ea231c6268b 100644 --- a/Lib/test/test_bytes.py +++ b/Lib/test/test_bytes.py @@ -1663,6 +1663,7 @@ def test_resize_error(self): # the bytearray must be left unchanged. offset = 3 + front = slice(None, -2) for logical_offset in (False, True): with self.subTest(logical_offset=logical_offset): # grow bytearray @@ -1687,6 +1688,29 @@ def test_resize_error(self): ba.resize(1) self.assertEqual(ba, expected) + # gh-160303: When bytes are removed at the front, they are + # removed even if shrinking the memory block fails. + ba = bytearray(b'0123456789') + if logical_offset: + del ba[:offset] + with inject_memory_error(self): + del ba[front] + self.assertEqual(ba, b'89') + ba.append(ord('a')) + del ba[:1] + self.assertEqual(ba, b'9a') + + ba = bytearray(b'0123456789') + if logical_offset: + del ba[:offset] + replacement = bytearray(b'xy') + with inject_memory_error(self): + ba[front] = replacement + self.assertEqual(ba, b'xy89') + ba.append(ord('a')) + del ba[:1] + self.assertEqual(ba, b'y89a') + 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..c6f6b74f0aa0d7 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-22-15-40-00.gh-issue-160303.q7Zb2K.rst @@ -0,0 +1,4 @@ +Fix :class:`bytearray` reading and writing memory outside of its buffer +after ``del b[:n]`` or ``b[:n] = shorter`` failed with :exc:`MemoryError`. +The bytes are now removed even if shrinking the memory block fails; +:exc:`MemoryError` is still raised. diff --git a/Objects/bytearrayobject.c b/Objects/bytearrayobject.c index dfd041b0152bdd..12c95f7acde4ea 100644 --- a/Objects/bytearrayobject.c +++ b/Objects/bytearrayobject.c @@ -266,13 +266,18 @@ bytearray_resize_storage(PyByteArrayObject *self, } +/* Resize the storage for requested_size bytes, even if it is already the + size of the bytearray. The caller must have checked that the bytearray + can be resized. */ static int -bytearray_resize_lock_held(PyObject *self, Py_ssize_t requested_size) +bytearray_resize_unchecked(PyByteArrayObject *obj, Py_ssize_t requested_size) { - _Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED(self); - PyByteArrayObject *obj = ((PyByteArrayObject *)self); + _Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED(obj); + PyObject *self = (PyObject *)obj; assert(obj->ob_bytes_object != NULL); + assert(requested_size >= 0); + assert(obj->ob_exports == 0); /* All computations are done unsigned to avoid integer overflows (see issue #22335). */ @@ -280,23 +285,8 @@ bytearray_resize_lock_held(PyObject *self, Py_ssize_t requested_size) size_t logical_offset = (size_t) (obj->ob_start - obj->ob_bytes); size_t size = (size_t) requested_size; - assert(self != NULL); - assert(PyByteArray_Check(self)); assert(logical_offset <= alloc); - if (requested_size < 0) { - PyErr_Format(PyExc_ValueError, - "Can only resize to positive sizes, got %zd", requested_size); - return -1; - } - - if (requested_size == Py_SIZE(self)) { - return 0; - } - if (!_canresize(obj)) { - return -1; - } - /* Resize to 0 resets to empty bytes (see issue #153419). */ if (requested_size == 0) { Py_SETREF(obj->ob_bytes_object, @@ -348,6 +338,30 @@ bytearray_resize_lock_held(PyObject *self, Py_ssize_t requested_size) return 0; } +static int +bytearray_resize_lock_held(PyObject *self, Py_ssize_t requested_size) +{ + _Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED(self); + PyByteArrayObject *obj = ((PyByteArrayObject *)self); + + assert(self != NULL); + assert(PyByteArray_Check(self)); + + if (requested_size < 0) { + PyErr_Format(PyExc_ValueError, + "Can only resize to positive sizes, got %zd", requested_size); + return -1; + } + + if (requested_size == Py_SIZE(self)) { + return 0; + } + if (!_canresize(obj)) { + return -1; + } + return bytearray_resize_unchecked(obj, requested_size); +} + int PyByteArray_Resize(PyObject *self, Py_ssize_t requested_size) { @@ -612,8 +626,12 @@ bytearray_setslice_linear(PyByteArrayObject *self, return -1; if (lo == 0) { - /* Shrink the buffer by advancing its logical start */ + /* Shrink the buffer by advancing its logical start. The bytes + are removed at this point; the resize below only releases + memory. */ self->ob_start -= growth; + Py_SET_SIZE(self, Py_SIZE(self) + growth); + growth = 0; /* 0 lo hi old_size | |<----avail----->|<-----tail------>| @@ -631,24 +649,16 @@ bytearray_setslice_linear(PyByteArrayObject *self, memmove(buf + lo + bytes_len, buf + hi, Py_SIZE(self) - hi); } - if (bytearray_resize_lock_held((PyObject *)self, - Py_SIZE(self) + growth) < 0) { + if (bytearray_resize_unchecked(self, Py_SIZE(self) + growth) < 0) { /* Issue #19578: Handling the memory allocation failure here is tricky here because the bytearray object has already been - modified. Depending on growth and lo, the behaviour is - different. - - If growth < 0 and lo != 0, the operation is completed, but a - MemoryError is still raised and the memory block is not - shrunk. Otherwise, the bytearray is restored in its previous - state and a MemoryError is raised. */ - if (lo == 0) { - self->ob_start += growth; - return -1; - } - /* memmove() removed bytes, the bytearray object cannot be - restored in its previous state. */ + modified: the bytes are removed and the bytearray object + cannot be restored in its previous state. + + The operation is completed, but a MemoryError is still + raised and the memory block is not shrunk. */ Py_SET_SIZE(self, Py_SIZE(self) + growth); + bytearray_write_trailing_null_byte(self); res = -1; } buf = PyByteArray_AS_STRING(self);