Skip to content

Commit 3dfe214

Browse files
authored
[3.15] gh-157242: Leave bytearray unchanged if resize() fails (#157340) (#159274)
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. (cherry picked from commit 67e6be7)
1 parent 3ca92d9 commit 3dfe214

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
@@ -6,6 +6,7 @@
66

77
import array
88
import codecs
9+
import contextlib
910
import copy
1011
import functools
1112
import operator
@@ -49,6 +50,19 @@ def __index__(self):
4950
return self.value
5051

5152

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

5468
def assertTypedEqual(self, actual, expected):
@@ -1565,6 +1579,35 @@ def test_resize(self):
15651579
self.assertRaises(MemoryError, bytearray().resize, sys.maxsize)
15661580
self.assertRaises(MemoryError, bytearray(1000).resize, sys.maxsize)
15671581

1582+
def test_resize_error(self):
1583+
# gh-157242: If bytearray.resize() fails (MemoryError),
1584+
# the bytearray must be left unchanged.
1585+
1586+
offset = 3
1587+
for logical_offset in (False, True):
1588+
with self.subTest(logical_offset=logical_offset):
1589+
# grow bytearray
1590+
ba = bytearray(b'0123456789')
1591+
if logical_offset:
1592+
expected = ba[offset:]
1593+
del ba[:offset]
1594+
else:
1595+
expected = ba.copy()
1596+
with inject_memory_error(self, 0):
1597+
ba.resize(1024)
1598+
self.assertEqual(ba, expected)
1599+
1600+
# shrink bytearray
1601+
ba = bytearray(b'0123456789')
1602+
if logical_offset:
1603+
expected = ba[offset:]
1604+
del ba[:offset]
1605+
else:
1606+
expected = ba.copy()
1607+
with inject_memory_error(self, 0):
1608+
ba.resize(1)
1609+
self.assertEqual(ba, expected)
1610+
15681611
def test_take_bytes(self):
15691612
ba = bytearray(b'ab')
15701613
self.assertEqual(ba.take_bytes(), b'ab')
@@ -1629,6 +1672,28 @@ def test_take_bytes(self):
16291672
self.assertEqual(ba, bytearray(b'A'))
16301673
self.assertEqual(ord(b'c'), ord('c'))
16311674

1675+
def test_take_bytes_error(self):
1676+
# gh-157242: If bytearray.take_bytes() fails (MemoryError),
1677+
# the bytearray must be left unchanged.
1678+
1679+
for logical_offset, to_take, mem_errors in (
1680+
(True, 5, (0, 1)),
1681+
(False, 5, (0, 1)),
1682+
(True, None, (0,)),
1683+
):
1684+
for mem_error in mem_errors:
1685+
with self.subTest(logical_offset=logical_offset,
1686+
to_take=to_take, mem_error=mem_error):
1687+
ba = bytearray(b'0123456789')
1688+
if logical_offset:
1689+
expected = ba[3:]
1690+
del ba[:3]
1691+
else:
1692+
expected = ba.copy()
1693+
with inject_memory_error(self, mem_error):
1694+
ba.take_bytes(to_take)
1695+
self.assertEqual(ba, expected)
1696+
16321697
@support.cpython_only # tests an implementation detail
16331698
def test_take_bytes_optimization(self):
16341699
# 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
@@ -396,6 +396,24 @@ def test_resize(self):
396396
writer.resize(len(b'number=123456'), b'456')
397397
self.bytes_equal(writer.finish(), b'number=123456')
398398

399+
def test_resize_error(self):
400+
small_buffer = _testcapi.PyBytesWriter_small_buffer
401+
init = b'x' * (small_buffer * 2)
402+
writer = self.create_writer(len(init), init)
403+
size = len(init) + 100
404+
try:
405+
with self.assertRaises(MemoryError):
406+
_testcapi.set_nomemory(0)
407+
writer.resize(size, b'')
408+
finally:
409+
_testcapi.remove_mem_hooks()
410+
suffix = b'still working'
411+
writer.write_bytes(suffix, -1)
412+
self.bytes_equal(writer.finish(), init + suffix)
413+
414+
# Note: PyBytesWriter_Resize() leaves the buffer unchanged (no resize)
415+
# if the new size is smaller than the allocated size
416+
399417
def test_format_i(self):
400418
# Test PyBytesWriter_Format()
401419
writer = self.create_writer()
@@ -465,24 +483,6 @@ def test_example_resize(self):
465483
def test_example_highlevel(self):
466484
self.assertEqual(_testcapi.byteswriter_highlevel(), b'Hello World!')
467485

468-
def test_resize_error(self):
469-
small_buffer = _testcapi.PyBytesWriter_small_buffer
470-
init = b'x' * (small_buffer * 2)
471-
writer = self.create_writer(len(init), init)
472-
size = len(init) + 100
473-
try:
474-
with self.assertRaises(MemoryError):
475-
_testcapi.set_nomemory(0)
476-
writer.resize(size, b'')
477-
finally:
478-
_testcapi.remove_mem_hooks()
479-
suffix = b'still working'
480-
writer.write_bytes(suffix, -1)
481-
self.bytes_equal(writer.finish(), init + suffix)
482-
483-
# Note: PyBytesWriter_Resize() leaves the buffer unchanged (no resize)
484-
# if the new size is smaller than the allocated size
485-
486486

487487
class ByteArrayWriterTest(BaseWriterTest, unittest.TestCase):
488488
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
}
@@ -995,9 +1032,9 @@ bytearray___init___impl(PyByteArrayObject *self, PyObject *arg,
9951032
&& PyBytes_CheckExact(encoded)
9961033
&& _PyBytes_GET_CACHED_HASH((PyBytesObject*)encoded) == -1)
9971034
{
998-
Py_ssize_t size = Py_SIZE(encoded);
1035+
Py_ssize_t size = PyBytes_GET_SIZE(encoded);
9991036
self->ob_bytes_object = encoded;
1000-
bytearray_reinit_from_bytes(self, size, size);
1037+
bytearray_reinit_from_bytes(self, size);
10011038
return 0;
10021039
}
10031040
new = bytearray_iconcat((PyObject*)self, encoded);
@@ -1121,7 +1158,7 @@ bytearray___init___impl(PyByteArrayObject *self, PyObject *arg,
11211158
/* Append the byte */
11221159
if (Py_SIZE(self) + 1 < self->ob_alloc) {
11231160
Py_SET_SIZE(self, Py_SIZE(self) + 1);
1124-
PyByteArray_AS_STRING(self)[Py_SIZE(self)] = '\0';
1161+
bytearray_write_trailing_null_byte(self);
11251162
}
11261163
else if (PyByteArray_Resize((PyObject *)self, Py_SIZE(self)+1) < 0)
11271164
goto error;
@@ -1611,6 +1648,7 @@ bytearray_take_bytes_impl(PyByteArrayObject *self, PyObject *n)
16111648
}
16121649

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

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

16491678
// Point the bytearray towards the buffer with the remaining data.
16501679
PyObject *result = self->ob_bytes_object;
16511680
self->ob_bytes_object = remaining;
1652-
bytearray_reinit_from_bytes(self, remaining_length, remaining_length);
1681+
bytearray_reinit_from_bytes(self, remaining_length);
16531682
return result;
16541683
}
16551684

0 commit comments

Comments
 (0)