Skip to content

Commit 67e6be7

Browse files
authored
gh-157242: Leave bytearray unchanged if resize() fails (#157340)
If bytearray.resize() or bytearray.take_bytes() fails, leave the bytearray unchanged. Add PyBytesWriter_Resize() error test on bytearray.
1 parent f5dd52d commit 67e6be7

4 files changed

Lines changed: 152 additions & 55 deletions

File tree

Lib/test/test_bytes.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"""
66

77
import array
8+
import contextlib
89
import operator
910
import os
1011
import re
@@ -48,6 +49,19 @@ def __index__(self):
4849
return self.value
4950

5051

52+
@contextlib.contextmanager
53+
def inject_memory_error(testcase, start):
54+
# Raise SkipTest if _testcapi extension module is missing
55+
_testcapi = import_helper.import_module('_testcapi')
56+
57+
with testcase.assertRaises(MemoryError):
58+
try:
59+
_testcapi.set_nomemory(start)
60+
yield
61+
finally:
62+
_testcapi.remove_mem_hooks()
63+
64+
5165
class BaseBytesTest:
5266

5367
def assertTypedEqual(self, actual, expected):
@@ -1555,6 +1569,35 @@ def test_resize(self):
15551569
self.assertRaises(MemoryError, bytearray().resize, sys.maxsize)
15561570
self.assertRaises(MemoryError, bytearray(1000).resize, sys.maxsize)
15571571

1572+
def test_resize_error(self):
1573+
# gh-157242: If bytearray.resize() fails (MemoryError),
1574+
# the bytearray must be left unchanged.
1575+
1576+
offset = 3
1577+
for logical_offset in (False, True):
1578+
with self.subTest(logical_offset=logical_offset):
1579+
# grow bytearray
1580+
ba = bytearray(b'0123456789')
1581+
if logical_offset:
1582+
expected = ba[offset:]
1583+
del ba[:offset]
1584+
else:
1585+
expected = ba.copy()
1586+
with inject_memory_error(self, 0):
1587+
ba.resize(1024)
1588+
self.assertEqual(ba, expected)
1589+
1590+
# shrink bytearray
1591+
ba = bytearray(b'0123456789')
1592+
if logical_offset:
1593+
expected = ba[offset:]
1594+
del ba[:offset]
1595+
else:
1596+
expected = ba.copy()
1597+
with inject_memory_error(self, 0):
1598+
ba.resize(1)
1599+
self.assertEqual(ba, expected)
1600+
15581601
def test_take_bytes(self):
15591602
ba = bytearray(b'ab')
15601603
self.assertEqual(ba.take_bytes(), b'ab')
@@ -1619,6 +1662,28 @@ def test_take_bytes(self):
16191662
self.assertEqual(ba, bytearray(b'A'))
16201663
self.assertEqual(ord(b'c'), ord('c'))
16211664

1665+
def test_take_bytes_error(self):
1666+
# gh-157242: If bytearray.take_bytes() fails (MemoryError),
1667+
# the bytearray must be left unchanged.
1668+
1669+
for logical_offset, to_take, mem_errors in (
1670+
(True, 5, (0, 1)),
1671+
(False, 5, (0, 1)),
1672+
(True, None, (0,)),
1673+
):
1674+
for mem_error in mem_errors:
1675+
with self.subTest(logical_offset=logical_offset,
1676+
to_take=to_take, mem_error=mem_error):
1677+
ba = bytearray(b'0123456789')
1678+
if logical_offset:
1679+
expected = ba[3:]
1680+
del ba[:3]
1681+
else:
1682+
expected = ba.copy()
1683+
with inject_memory_error(self, mem_error):
1684+
ba.take_bytes(to_take)
1685+
self.assertEqual(ba, expected)
1686+
16221687
@support.cpython_only # tests an implementation detail
16231688
def test_take_bytes_optimization(self):
16241689
# Validate optimization around taking lots of little chunks out of a

Lib/test/test_capi/test_bytes.py

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,24 @@ def test_resize(self):
389389
writer.resize(len(b'number=123456'), b'456')
390390
self.assertEqual(writer.finish(), self.result_type(b'number=123456'))
391391

392+
def test_resize_error(self):
393+
small_buffer = _testcapi.PyBytesWriter_small_buffer
394+
init = b'x' * (small_buffer * 2)
395+
writer = self.create_writer(len(init), init)
396+
size = len(init) + 100
397+
try:
398+
with self.assertRaises(MemoryError):
399+
_testcapi.set_nomemory(0)
400+
writer.resize(size, b'')
401+
finally:
402+
_testcapi.remove_mem_hooks()
403+
suffix = b'still working'
404+
writer.write_bytes(suffix, -1)
405+
self.assertEqual(writer.finish(), self.result_type(init + suffix))
406+
407+
# Note: PyBytesWriter_Resize() leaves the buffer unchanged (no resize)
408+
# if the new size is smaller than the allocated size
409+
392410
def test_format_i(self):
393411
# Test PyBytesWriter_Format()
394412
writer = self.create_writer()
@@ -446,24 +464,6 @@ def test_example_resize(self):
446464
def test_example_highlevel(self):
447465
self.assertEqual(_testcapi.byteswriter_highlevel(), b'Hello World!')
448466

449-
def test_resize_error(self):
450-
small_buffer = _testcapi.PyBytesWriter_small_buffer
451-
init = b'x' * (small_buffer * 2)
452-
writer = self.create_writer(len(init), init)
453-
size = len(init) + 100
454-
try:
455-
with self.assertRaises(MemoryError):
456-
_testcapi.set_nomemory(0)
457-
writer.resize(size, b'')
458-
finally:
459-
_testcapi.remove_mem_hooks()
460-
suffix = b'still working'
461-
writer.write_bytes(suffix, -1)
462-
self.assertEqual(writer.finish(), self.result_type(init + suffix))
463-
464-
# Note: PyBytesWriter_Resize() leaves the buffer unchanged (no resize)
465-
# if the new size is smaller than the allocated size
466-
467467

468468
class ByteArrayWriterTest(BaseWriterTest, unittest.TestCase):
469469
result_type = bytearray
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
If :meth:`bytearray.resize` or :meth:`bytearray.take_bytes` fails, leave the
2+
:class:`bytearray` unchanged, instead of clearing it. Patch by Victor
3+
Stinner.

Objects/bytearrayobject.c

Lines changed: 66 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,24 @@ _getbytevalue(PyObject* arg, int *value)
4343
return 1;
4444
}
4545

46+
static inline void
47+
bytearray_write_trailing_null_byte(PyByteArrayObject *self)
48+
{
49+
char *data = PyByteArray_AS_STRING(self);
50+
Py_ssize_t size = PyByteArray_GET_SIZE(self);
51+
data[size] = '\0';
52+
}
53+
54+
4655
static void
47-
bytearray_reinit_from_bytes(PyByteArrayObject *self, Py_ssize_t size,
48-
Py_ssize_t alloc)
56+
bytearray_reinit_from_bytes(PyByteArrayObject *self, Py_ssize_t size)
4957
{
58+
Py_ssize_t alloc = PyBytes_GET_SIZE(self->ob_bytes_object);
59+
assert(0 <= size && size <= alloc);
60+
5061
/* Only the empty bytes may be immortal. */
5162
assert((alloc == 0) == _Py_IsImmortal(self->ob_bytes_object));
63+
5264
self->ob_bytes = self->ob_start = PyBytes_AS_STRING(self->ob_bytes_object);
5365
Py_SET_SIZE(self, size);
5466
FT_ATOMIC_STORE_SSIZE_RELAXED(self->ob_alloc, alloc);
@@ -185,7 +197,7 @@ PyByteArray_FromStringAndSize(const char *bytes, Py_ssize_t size)
185197
Py_DECREF(new);
186198
return NULL;
187199
}
188-
bytearray_reinit_from_bytes(new, size, size);
200+
bytearray_reinit_from_bytes(new, size);
189201
if (bytes != NULL && size > 0) {
190202
memcpy(new->ob_bytes, bytes, size);
191203
}
@@ -211,6 +223,43 @@ PyByteArray_AsString(PyObject *self)
211223
return PyByteArray_AS_STRING(self);
212224
}
213225

226+
227+
static int
228+
bytearray_resize_storage(PyByteArrayObject *self,
229+
Py_ssize_t new_size, Py_ssize_t alloc)
230+
{
231+
_Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED(self);
232+
assert(1 <= new_size && new_size <= alloc);
233+
234+
Py_ssize_t size = Py_SIZE(self);
235+
236+
/* Re-align data to the start of the allocation. */
237+
char *old_start = self->ob_start;
238+
if (self->ob_start != self->ob_bytes) {
239+
/* optimization tradeoff: This is faster than a new allocation when
240+
the number of bytes being removed in a resize is small; for
241+
large size changes it may be better to just make a new bytes
242+
object as _PyBytes_Resize will do a malloc + memcpy internally.
243+
*/
244+
Py_ssize_t move = Py_MIN(new_size, size);
245+
memmove(self->ob_bytes, self->ob_start, move);
246+
self->ob_start = self->ob_bytes;
247+
}
248+
249+
if (_PyBytes_ResizeKeepOnError(&self->ob_bytes_object, alloc) < 0) {
250+
if (old_start != self->ob_bytes && new_size < size) {
251+
// Move remaining bytes
252+
Py_ssize_t moved = new_size;
253+
Py_ssize_t remaining = size - moved;
254+
memmove(self->ob_bytes + moved, old_start + moved, remaining);
255+
}
256+
bytearray_write_trailing_null_byte(self);
257+
return -1;
258+
}
259+
return 0;
260+
}
261+
262+
214263
static int
215264
bytearray_resize_lock_held(PyObject *self, Py_ssize_t requested_size)
216265
{
@@ -246,7 +295,7 @@ bytearray_resize_lock_held(PyObject *self, Py_ssize_t requested_size)
246295
if (requested_size == 0) {
247296
Py_SETREF(obj->ob_bytes_object,
248297
Py_GetConstant(Py_CONSTANT_EMPTY_BYTES));
249-
bytearray_reinit_from_bytes(obj, 0, 0);
298+
bytearray_reinit_from_bytes(obj, 0);
250299
return 0;
251300
}
252301

@@ -261,7 +310,7 @@ bytearray_resize_lock_held(PyObject *self, Py_ssize_t requested_size)
261310
/* Minor downsize; quick exit */
262311
Py_SET_SIZE(self, size);
263312
/* Add mid-buffer null; end provided by bytes. */
264-
PyByteArray_AS_STRING(self)[size] = '\0'; /* Trailing null */
313+
bytearray_write_trailing_null_byte(_PyByteArray_CAST(self));
265314
return 0;
266315
}
267316
}
@@ -281,28 +330,16 @@ bytearray_resize_lock_held(PyObject *self, Py_ssize_t requested_size)
281330
return -1;
282331
}
283332

284-
/* Re-align data to the start of the allocation. */
285-
if (logical_offset > 0) {
286-
/* optimization tradeoff: This is faster than a new allocation when
287-
the number of bytes being removed in a resize is small; for large
288-
size changes it may be better to just make a new bytes object as
289-
_PyBytes_Resize will do a malloc + memcpy internally. */
290-
memmove(obj->ob_bytes, obj->ob_start,
291-
Py_MIN(requested_size, Py_SIZE(self)));
333+
if (bytearray_resize_storage(obj, requested_size, (Py_ssize_t)alloc) < 0) {
334+
return -1;
292335
}
293336

294-
int ret = _PyBytes_Resize(&obj->ob_bytes_object, alloc);
295-
if (ret == -1) {
296-
obj->ob_bytes_object = Py_GetConstant(Py_CONSTANT_EMPTY_BYTES);
297-
size = alloc = 0;
298-
}
299-
bytearray_reinit_from_bytes(obj, size, alloc);
337+
bytearray_reinit_from_bytes(obj, size);
300338
if (alloc != size) {
301339
/* Add mid-buffer null; end provided by bytes. */
302-
obj->ob_bytes[size] = '\0';
340+
bytearray_write_trailing_null_byte(obj);
303341
}
304-
305-
return ret;
342+
return 0;
306343
}
307344

308345
int
@@ -928,7 +965,7 @@ bytearray_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
928965
}
929966
PyByteArrayObject *self = _PyByteArray_CAST(op);
930967
self->ob_bytes_object = Py_GetConstant(Py_CONSTANT_EMPTY_BYTES);
931-
bytearray_reinit_from_bytes(self, 0, 0);
968+
bytearray_reinit_from_bytes(self, 0);
932969
self->ob_exports = 0;
933970
return op;
934971
}
@@ -994,9 +1031,9 @@ bytearray___init___impl(PyByteArrayObject *self, PyObject *arg,
9941031
if (_PyObject_IsUniquelyReferenced(encoded)
9951032
&& PyBytes_CheckExact(encoded))
9961033
{
997-
Py_ssize_t size = Py_SIZE(encoded);
1034+
Py_ssize_t size = PyBytes_GET_SIZE(encoded);
9981035
self->ob_bytes_object = encoded;
999-
bytearray_reinit_from_bytes(self, size, size);
1036+
bytearray_reinit_from_bytes(self, size);
10001037
return 0;
10011038
}
10021039
new = bytearray_iconcat((PyObject*)self, encoded);
@@ -1120,7 +1157,7 @@ bytearray___init___impl(PyByteArrayObject *self, PyObject *arg,
11201157
/* Append the byte */
11211158
if (Py_SIZE(self) + 1 < self->ob_alloc) {
11221159
Py_SET_SIZE(self, Py_SIZE(self) + 1);
1123-
PyByteArray_AS_STRING(self)[Py_SIZE(self)] = '\0';
1160+
bytearray_write_trailing_null_byte(self);
11241161
}
11251162
else if (PyByteArray_Resize((PyObject *)self, Py_SIZE(self)+1) < 0)
11261163
goto error;
@@ -1610,6 +1647,7 @@ bytearray_take_bytes_impl(PyByteArrayObject *self, PyObject *n)
16101647
}
16111648

16121649
Py_ssize_t remaining_length = size - to_take;
1650+
16131651
// optimization: If taking less than leaving, just copy the small to_take
16141652
// portion out and move ob_start.
16151653
if (to_take < remaining_length) {
@@ -1631,24 +1669,15 @@ bytearray_take_bytes_impl(PyByteArrayObject *self, PyObject *n)
16311669
memcpy(PyBytes_AS_STRING(remaining), self->ob_start + to_take,
16321670
remaining_length);
16331671

1634-
// If the bytes are offset inside the buffer must first align.
1635-
if (self->ob_start != self->ob_bytes) {
1636-
memmove(self->ob_bytes, self->ob_start, to_take);
1637-
self->ob_start = self->ob_bytes;
1638-
}
1639-
1640-
if (_PyBytes_Resize(&self->ob_bytes_object, to_take) == -1) {
1641-
assert(self->ob_bytes_object == NULL);
1642-
self->ob_bytes_object = Py_GetConstant(Py_CONSTANT_EMPTY_BYTES);
1643-
bytearray_reinit_from_bytes(self, 0, 0);
1672+
if (bytearray_resize_storage(self, to_take, to_take) < 0) {
16441673
Py_DECREF(remaining);
16451674
return NULL;
16461675
}
16471676

16481677
// Point the bytearray towards the buffer with the remaining data.
16491678
PyObject *result = self->ob_bytes_object;
16501679
self->ob_bytes_object = remaining;
1651-
bytearray_reinit_from_bytes(self, remaining_length, remaining_length);
1680+
bytearray_reinit_from_bytes(self, remaining_length);
16521681
return result;
16531682
}
16541683

0 commit comments

Comments
 (0)