Skip to content

Commit 314253c

Browse files
committed
bytearray_resize_lock_held always initializes ob_bytes_object if NULL
There are several ways that bytearray_resize_lock_held gets called and for any of them, ob_bytes_object may be NULL. This allows us to remove the extra initialization in __init__ as this can't be guaranteed to be called anyway. Handle a related issue in take_bytes where in the extreme case of the bytes resize allocation failing (on a size reduction) then the bytearray other state fileds could be left inconsistent with a null ob_bytes_object. unconditionally call _canresize in __init__ as on initial creation, this will always be true, and if there are any exported views, then we just always fail, even if technically an empty bytearray doesn't get modified in this case, it's so rare, it's not worth it.
1 parent b54ca0f commit 314253c

2 files changed

Lines changed: 109 additions & 11 deletions

File tree

Lib/test/test_bytes.py

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
import test.string_tests
2626
import test.list_tests
2727
from test.support import bigaddrspacetest, MAX_Py_ssize_t
28-
from test.support.script_helper import assert_python_failure
28+
from test.support.script_helper import assert_python_failure, assert_python_ok
2929

3030

3131
if sys.flags.bytes_warning:
@@ -2198,6 +2198,85 @@ def __len__(self):
21982198
self.assertRaises(BufferError, ba.hex, S(b':'))
21992199

22002200

2201+
class ByteArrayInitialization1Test(unittest.TestCase):
2202+
# A bytearray created with __new__ so that __init__ is never called
2203+
# (often as a side-effect of a subclass not calling super().__init__).
2204+
# Is left with ob_bytes_object == NULL. It's easy for implementation
2205+
# code to not realise that ob_bytes_object can be NULL, so these tests
2206+
# verify a set of code paths that have historically crashed or asserted
2207+
# (see gh-153419)
2208+
2209+
def _check(self, stmt, expected):
2210+
code = textwrap.dedent(f"""
2211+
a = bytearray.__new__(bytearray)
2212+
{stmt}
2213+
print(list(a))
2214+
""")
2215+
rc, out, err = assert_python_ok('-c', code)
2216+
self.assertEqual(out.decode().strip(), expected)
2217+
2218+
def test_append(self):
2219+
self._check("a.append(1)", "[1]")
2220+
2221+
def test_insert(self):
2222+
self._check("a.insert(0, 1)", "[1]")
2223+
2224+
def test_extend_bytes(self):
2225+
self._check("a.extend(b'x')", "[120]")
2226+
2227+
def test_extend_iterable(self):
2228+
self._check("a.extend([1, 2, 3])", "[1, 2, 3]")
2229+
2230+
def test_iadd(self):
2231+
self._check("a += b'x'", "[120]")
2232+
2233+
def test_slice_assign(self):
2234+
self._check("a[:] = b'xyz'", "[120, 121, 122]")
2235+
2236+
def test_resize(self):
2237+
self._check("a.resize(4)", "[0, 0, 0, 0]")
2238+
2239+
def test_init_int(self):
2240+
self._check("a.__init__(5)", "[0, 0, 0, 0, 0]")
2241+
2242+
def test_init_bytes(self):
2243+
self._check("a.__init__(b'xyz')", "[120, 121, 122]")
2244+
2245+
def test_reinit_length1(self):
2246+
# There is a shortcut taken when resizing, where alloc/2 < newsize.
2247+
# In this case, the existing buffer is reused, rather than reset
2248+
# If this happens when newsize == 0 and alloc == 1, then various
2249+
# code assumptions can be violated. This test should catch those
2250+
# in debug builds. (see gh-153419)
2251+
code = textwrap.dedent("""
2252+
a = bytearray(1)
2253+
a.__init__()
2254+
print(list(a))
2255+
""")
2256+
rc, out, err = assert_python_ok('-c', code)
2257+
self.assertEqual(out.decode().strip(), repr([]))
2258+
2259+
def test_take_bytes_all(self):
2260+
self._check("a.take_bytes()", "[]")
2261+
2262+
def test_take_bytes_zero(self):
2263+
self._check("a.take_bytes(0)", "[]")
2264+
2265+
def test_reinit_with_view(self):
2266+
code = textwrap.dedent("""
2267+
a = bytearray()
2268+
v = memoryview(a)
2269+
try:
2270+
a.__init__("x", "ascii")
2271+
except BufferError:
2272+
print("PASS")
2273+
else:
2274+
print("NO EXCEPTION")
2275+
""")
2276+
rc, out, err = assert_python_ok('-c', code)
2277+
self.assertEqual(out.decode().strip(), "PASS")
2278+
2279+
22012280
class AssortedBytesTest(unittest.TestCase):
22022281
#
22032282
# Test various combinations of bytes and bytearray

Objects/bytearrayobject.c

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,15 @@ bytearray_resize_lock_held(PyObject *self, Py_ssize_t requested_size)
213213
{
214214
_Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED(self);
215215
PyByteArrayObject *obj = ((PyByteArrayObject *)self);
216+
217+
/* If ob_bytes_object has not been initialized yet, eagerly initialize
218+
it here so the following code can reason about state more easily,
219+
and things like pointer comparisons are valid. */
220+
if (obj->ob_bytes_object == NULL) {
221+
obj->ob_bytes_object = Py_GetConstant(Py_CONSTANT_EMPTY_BYTES);
222+
bytearray_reinit_from_bytes(obj, 0, 0);
223+
}
224+
216225
/* All computations are done unsigned to avoid integer overflows
217226
(see issue #22335). */
218227
size_t alloc = (size_t) obj->ob_alloc;
@@ -236,6 +245,15 @@ bytearray_resize_lock_held(PyObject *self, Py_ssize_t requested_size)
236245
return -1;
237246
}
238247

248+
/* When resizing to 0, always reset to the empty-bytes constant to avoid
249+
complexity in the realloc path below (see gh-153419). */
250+
if (requested_size == 0) {
251+
Py_XSETREF(obj->ob_bytes_object,
252+
Py_GetConstant(Py_CONSTANT_EMPTY_BYTES));
253+
bytearray_reinit_from_bytes(obj, 0, 0);
254+
return 0;
255+
}
256+
239257
if (size + logical_offset <= alloc) {
240258
/* Current buffer is large enough to host the requested size,
241259
decide on a strategy. */
@@ -920,20 +938,17 @@ bytearray___init___impl(PyByteArrayObject *self, PyObject *arg,
920938
PyObject *it;
921939
PyObject *(*iternext)(PyObject *);
922940

923-
/* First __init__; set ob_bytes_object so ob_bytes is always non-null. */
924-
if (self->ob_bytes_object == NULL) {
925-
self->ob_bytes_object = Py_GetConstant(Py_CONSTANT_EMPTY_BYTES);
926-
bytearray_reinit_from_bytes(self, 0, 0);
927-
self->ob_exports = 0;
941+
if (!_canresize(self)) {
942+
return -1;
928943
}
929944

930-
if (Py_SIZE(self) != 0) {
931-
/* Empty previous contents (yes, do this first of all!) */
932-
if (PyByteArray_Resize((PyObject *)self, 0) < 0)
933-
return -1;
945+
/* Empty any previous contents (do this first of all!).
946+
Also initializes ob_bytes_object if needed */
947+
if (PyByteArray_Resize((PyObject *)self, 0) < 0) {
948+
return -1;
934949
}
935950

936-
/* Should be caused by first init or the resize to 0. */
951+
/* PyByteArray_Resize(,0) should always leave the empty bytes constant */
937952
assert(self->ob_bytes_object == Py_GetConstantBorrowed(Py_CONSTANT_EMPTY_BYTES));
938953
assert(self->ob_exports == 0);
939954

@@ -1607,6 +1622,10 @@ bytearray_take_bytes_impl(PyByteArrayObject *self, PyObject *n)
16071622
}
16081623

16091624
if (_PyBytes_Resize(&self->ob_bytes_object, to_take) == -1) {
1625+
/* _PyBytes_Resize has made ob_bytes_object NULL here
1626+
we need to ensure that the other byte array fields are consistent. */
1627+
self->ob_bytes_object = Py_GetConstant(Py_CONSTANT_EMPTY_BYTES);
1628+
bytearray_reinit_from_bytes(self, 0, 0);
16101629
Py_DECREF(remaining);
16111630
return NULL;
16121631
}

0 commit comments

Comments
 (0)