Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
20 changes: 20 additions & 0 deletions Lib/test/test_bytes.py
Original file line number Diff line number Diff line change
Expand Up @@ -1687,6 +1687,26 @@ def test_resize_error(self):
ba.resize(1)
self.assertEqual(ba, expected)

@support.nomemtest
def test_remove_prefix_error(self):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can these cases be moved to test_resize_error? That already has some shrink errors.

# 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')
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
41 changes: 23 additions & 18 deletions Objects/bytearrayobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
Loading