Skip to content

Commit 51dd1ca

Browse files
committed
gh-160303: Keep ob_start instead of allocating in del b[:n]
Move ob_start and the size before the shrinking resize, so a failed shrink leaves the bytes removed and the larger block in place, as the non-front path already does. Split the checks out of bytearray_resize_lock_held() so the slice code can resize directly. Move the tests into test_resize_error.
1 parent 7298647 commit 51dd1ca

3 files changed

Lines changed: 83 additions & 73 deletions

File tree

‎Lib/test/test_bytes.py‎

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1663,6 +1663,7 @@ def test_resize_error(self):
16631663
# the bytearray must be left unchanged.
16641664

16651665
offset = 3
1666+
front = slice(None, -2)
16661667
for logical_offset in (False, True):
16671668
with self.subTest(logical_offset=logical_offset):
16681669
# grow bytearray
@@ -1687,25 +1688,28 @@ def test_resize_error(self):
16871688
ba.resize(1)
16881689
self.assertEqual(ba, expected)
16891690

1690-
@support.nomemtest
1691-
def test_remove_prefix_error(self):
1692-
# If the storage cannot be shrunk after removing a prefix
1693-
# (MemoryError), the bytearray must be left unchanged.
1694-
for logical_offset in (False, True):
1695-
with self.subTest(logical_offset=logical_offset):
1696-
ba = bytearray(b'0123456789' * 4)
1691+
# gh-160303: When bytes are removed at the front, they are
1692+
# removed even if shrinking the memory block fails.
1693+
ba = bytearray(b'0123456789')
16971694
if logical_offset:
1698-
del ba[:3]
1699-
expected = ba.copy()
1695+
del ba[:offset]
17001696
with inject_memory_error(self):
1701-
del ba[:30]
1702-
self.assertEqual(ba, expected)
1697+
del ba[front]
1698+
self.assertEqual(ba, b'89')
1699+
ba.append(ord('a'))
1700+
del ba[:1]
1701+
self.assertEqual(ba, b'9a')
1702+
1703+
ba = bytearray(b'0123456789')
1704+
if logical_offset:
1705+
del ba[:offset]
1706+
replacement = bytearray(b'xy')
17031707
with inject_memory_error(self):
1704-
ba[:30] = b'xy'
1705-
self.assertEqual(ba, expected)
1706-
# The bytearray is still usable
1707-
del ba[:30]
1708-
self.assertEqual(ba, expected[30:])
1708+
ba[front] = replacement
1709+
self.assertEqual(ba, b'xy89')
1710+
ba.append(ord('a'))
1711+
del ba[:1]
1712+
self.assertEqual(ba, b'y89a')
17091713

17101714
def test_take_bytes(self):
17111715
ba = bytearray(b'ab')
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
Fix :class:`bytearray` reading and writing memory outside of its buffer
22
after ``del b[:n]`` or ``b[:n] = shorter`` failed with :exc:`MemoryError`.
3-
The bytearray is now left unchanged.
3+
The bytes are now removed even if shrinking the memory block fails;
4+
:exc:`MemoryError` is still raised.

‎Objects/bytearrayobject.c‎

Lines changed: 61 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -238,69 +238,55 @@ bytearray_resize_storage(PyByteArrayObject *self,
238238

239239
Py_ssize_t size = Py_SIZE(self);
240240

241-
if (self->ob_start != self->ob_bytes && new_size < size) {
242-
/* Shrink with a logical offset: copy the bytes to keep into a new
243-
allocation. Allocate first, so that the bytearray (including
244-
ob_start) is left unchanged on error: bytearray_setslice_linear()
245-
relies on it to restore the bytearray after a failed "del b[:n]".
241+
/* Re-align data to the start of the allocation. */
242+
char *old_start = self->ob_start;
243+
if (self->ob_start != self->ob_bytes) {
244+
/* optimization tradeoff: This is faster than a new allocation when
245+
the number of bytes being removed in a resize is small; for
246+
large size changes it may be better to just make a new bytes
247+
object as _PyBytes_Resize will do a malloc + memcpy internally.
246248
*/
247-
PyObject *bytes = PyBytes_FromStringAndSize(NULL, alloc);
248-
if (bytes == NULL) {
249-
return -1;
250-
}
251-
memcpy(PyBytes_AS_STRING(bytes), self->ob_start, new_size);
252-
Py_SETREF(self->ob_bytes_object, bytes);
249+
Py_ssize_t move = Py_MIN(new_size, size);
250+
memmove(self->ob_bytes, self->ob_start, move);
251+
self->ob_start = self->ob_bytes;
253252
}
254-
else {
255-
/* Re-align data to the start of the allocation. */
256-
if (self->ob_start != self->ob_bytes) {
257-
/* optimization tradeoff: This is faster than a new allocation
258-
when the number of bytes being added in a resize is small. */
259-
memmove(self->ob_bytes, self->ob_start, size);
260-
self->ob_start = self->ob_bytes;
261-
}
262253

263-
if (_PyBytes_ResizeKeepOnError(&self->ob_bytes_object, alloc) < 0) {
264-
bytearray_write_trailing_null_byte(self);
265-
return -1;
254+
if (_PyBytes_ResizeKeepOnError(&self->ob_bytes_object, alloc) < 0) {
255+
if (old_start != self->ob_bytes && new_size < size) {
256+
// Move remaining bytes
257+
Py_ssize_t moved = new_size;
258+
Py_ssize_t remaining = size - moved;
259+
memmove(self->ob_bytes + moved, old_start + moved, remaining);
266260
}
261+
bytearray_write_trailing_null_byte(self);
262+
return -1;
267263
}
268264
assert(_PyBytes_IsMutable(self->ob_bytes_object));
269265
return 0;
270266
}
271267

272268

269+
/* Resize the storage for requested_size bytes, even if it is already the
270+
size of the bytearray. The caller must have checked that the bytearray
271+
can be resized. */
273272
static int
274-
bytearray_resize_lock_held(PyObject *self, Py_ssize_t requested_size)
273+
bytearray_resize_unchecked(PyByteArrayObject *obj, Py_ssize_t requested_size)
275274
{
276-
_Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED(self);
277-
PyByteArrayObject *obj = ((PyByteArrayObject *)self);
275+
_Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED(obj);
276+
PyObject *self = (PyObject *)obj;
278277

279278
assert(obj->ob_bytes_object != NULL);
279+
assert(requested_size >= 0);
280+
assert(obj->ob_exports == 0);
280281

281282
/* All computations are done unsigned to avoid integer overflows
282283
(see issue #22335). */
283284
size_t alloc = (size_t) obj->ob_alloc;
284285
size_t logical_offset = (size_t) (obj->ob_start - obj->ob_bytes);
285286
size_t size = (size_t) requested_size;
286287

287-
assert(self != NULL);
288-
assert(PyByteArray_Check(self));
289288
assert(logical_offset <= alloc);
290289

291-
if (requested_size < 0) {
292-
PyErr_Format(PyExc_ValueError,
293-
"Can only resize to positive sizes, got %zd", requested_size);
294-
return -1;
295-
}
296-
297-
if (requested_size == Py_SIZE(self)) {
298-
return 0;
299-
}
300-
if (!_canresize(obj)) {
301-
return -1;
302-
}
303-
304290
/* Resize to 0 resets to empty bytes (see issue #153419). */
305291
if (requested_size == 0) {
306292
Py_SETREF(obj->ob_bytes_object,
@@ -352,6 +338,30 @@ bytearray_resize_lock_held(PyObject *self, Py_ssize_t requested_size)
352338
return 0;
353339
}
354340

341+
static int
342+
bytearray_resize_lock_held(PyObject *self, Py_ssize_t requested_size)
343+
{
344+
_Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED(self);
345+
PyByteArrayObject *obj = ((PyByteArrayObject *)self);
346+
347+
assert(self != NULL);
348+
assert(PyByteArray_Check(self));
349+
350+
if (requested_size < 0) {
351+
PyErr_Format(PyExc_ValueError,
352+
"Can only resize to positive sizes, got %zd", requested_size);
353+
return -1;
354+
}
355+
356+
if (requested_size == Py_SIZE(self)) {
357+
return 0;
358+
}
359+
if (!_canresize(obj)) {
360+
return -1;
361+
}
362+
return bytearray_resize_unchecked(obj, requested_size);
363+
}
364+
355365
int
356366
PyByteArray_Resize(PyObject *self, Py_ssize_t requested_size)
357367
{
@@ -616,8 +626,12 @@ bytearray_setslice_linear(PyByteArrayObject *self,
616626
return -1;
617627

618628
if (lo == 0) {
619-
/* Shrink the buffer by advancing its logical start */
629+
/* Shrink the buffer by advancing its logical start. The bytes
630+
are removed at this point; the resize below only releases
631+
memory. */
620632
self->ob_start -= growth;
633+
Py_SET_SIZE(self, Py_SIZE(self) + growth);
634+
growth = 0;
621635
/*
622636
0 lo hi old_size
623637
| |<----avail----->|<-----tail------>|
@@ -635,23 +649,14 @@ bytearray_setslice_linear(PyByteArrayObject *self,
635649
memmove(buf + lo + bytes_len, buf + hi,
636650
Py_SIZE(self) - hi);
637651
}
638-
if (bytearray_resize_lock_held((PyObject *)self,
639-
Py_SIZE(self) + growth) < 0) {
652+
if (bytearray_resize_unchecked(self, Py_SIZE(self) + growth) < 0) {
640653
/* Issue #19578: Handling the memory allocation failure here is
641654
tricky here because the bytearray object has already been
642-
modified. Depending on growth and lo, the behaviour is
643-
different.
644-
645-
If growth < 0 and lo != 0, the operation is completed, but a
646-
MemoryError is still raised and the memory block is not
647-
shrunk. Otherwise, the bytearray is restored in its previous
648-
state and a MemoryError is raised. */
649-
if (lo == 0) {
650-
self->ob_start += growth;
651-
return -1;
652-
}
653-
/* memmove() removed bytes, the bytearray object cannot be
654-
restored in its previous state. */
655+
modified: the bytes are removed and the bytearray object
656+
cannot be restored in its previous state.
657+
658+
The operation is completed, but a MemoryError is still
659+
raised and the memory block is not shrunk. */
655660
Py_SET_SIZE(self, Py_SIZE(self) + growth);
656661
bytearray_write_trailing_null_byte(self);
657662
res = -1;

0 commit comments

Comments
 (0)