Skip to content

Commit c961d61

Browse files
committed
gh-153419: Fix several issues around bytearray __init__
Introduce a bytearray_new function to ensure that ob_bytes_object is always set on a bytearray. Resizing a bytearray to 0 length explicitly sets the ob_bytes_object to the empty constant immortal. Add a check in the 'bytearray init from string' fast path to ensure there are no active exports. This fixes asserts/crashes on the following: - bytearray(1).__init__() - bytearray().__new__(bytearray).append(1) - a = bytearray(); b = memoryview(a); a.__init__('x', 'ascii')
1 parent 33678dc commit c961d61

3 files changed

Lines changed: 78 additions & 13 deletions

File tree

Lib/test/test_bytes.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1860,6 +1860,54 @@ def g():
18601860
alloc = b.__alloc__()
18611861
self.assertGreater(alloc, len(b))
18621862

1863+
def test_reinit_small_buffer(self):
1864+
# gh-153419: reinitializing a bytearray whose buffer allocation is
1865+
# exactly one byte used to fail a debug assertion
1866+
b = bytearray(1)
1867+
b.__init__()
1868+
self.assertEqual(b, bytearray())
1869+
1870+
def test_reinit_empty_buffer(self):
1871+
# gh-153419: Calling __init__ on a bytearray that has an active
1872+
# export should only be allowed if the bytearray is unchanged.
1873+
b = bytearray()
1874+
with memoryview(b):
1875+
self.assertRaises(BufferError, b.__init__, b"abc")
1876+
self.assertRaises(BufferError, b.__init__, "abc", "ascii")
1877+
self.assertRaises(BufferError, b.__init__, 3)
1878+
b.__init__()
1879+
b.__init__(b"")
1880+
b.__init__("", "ascii")
1881+
self.assertEqual(b, bytearray())
1882+
1883+
def test_reinit_nonempty_buffer(self):
1884+
# gh-153419: If a buffer is non-empty and has an active export
1885+
# calling __init__ on it should always fail, even if the
1886+
# new value ends up the same as the old one. This is purely for
1887+
# practical reasons rather than a design goal.
1888+
b = bytearray(b"abc")
1889+
with memoryview(b):
1890+
self.assertRaises(BufferError, b.__init__)
1891+
self.assertRaises(BufferError, b.__init__, b"xy")
1892+
self.assertRaises(BufferError, b.__init__, b"abc")
1893+
1894+
def test_new_without_init(self):
1895+
# gh-153419: a bytearray must be fully initialized by __new__
1896+
# alone to ensure that the underlying buffer is always valid.
1897+
b = bytearray.__new__(bytearray)
1898+
self.assertEqual(b, bytearray())
1899+
b.append(1)
1900+
self.assertEqual(b, bytearray(b"\x01"))
1901+
1902+
class B(bytearray):
1903+
def __init__(self, *args):
1904+
pass # does not call super().__init__()
1905+
1906+
b = B(b"ignored")
1907+
self.assertEqual(b, bytearray())
1908+
b.extend(b"abc")
1909+
self.assertEqual(b, bytearray(b"abc"))
1910+
18631911
def test_extend(self):
18641912
orig = b'hello'
18651913
a = bytearray(orig)
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix several crashes relating to calling (or not calling)
2+
``bytearray.__init__`` in unusual ways.

Objects/bytearrayobject.c

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,14 @@ bytearray_resize_lock_held(PyObject *self, Py_ssize_t requested_size)
236236
return -1;
237237
}
238238

239+
if (requested_size == 0) {
240+
/* Resizing to zero just resets to the empty constant (#153419) */
241+
Py_XSETREF(obj->ob_bytes_object,
242+
Py_GetConstant(Py_CONSTANT_EMPTY_BYTES));
243+
bytearray_reinit_from_bytes(obj, 0, 0);
244+
return 0;
245+
}
246+
239247
if (size + logical_offset <= alloc) {
240248
/* Current buffer is large enough to host the requested size,
241249
decide on a strategy. */
@@ -902,6 +910,19 @@ bytearray_ass_subscript(PyObject *op, PyObject *index, PyObject *values)
902910
return ret;
903911
}
904912

913+
static PyObject *
914+
bytearray_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
915+
{
916+
PyObject *self = PyType_GenericNew(type, args, kwds);
917+
if (self == NULL) {
918+
return NULL;
919+
}
920+
PyByteArrayObject *obj = _PyByteArray_CAST(self);
921+
obj->ob_bytes_object = Py_GetConstant(Py_CONSTANT_EMPTY_BYTES);
922+
bytearray_reinit_from_bytes(obj, 0, 0);
923+
return self;
924+
}
925+
905926
/*[clinic input]
906927
bytearray.__init__
907928
@@ -920,22 +941,13 @@ bytearray___init___impl(PyByteArrayObject *self, PyObject *arg,
920941
PyObject *it;
921942
PyObject *(*iternext)(PyObject *);
922943

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;
928-
}
929-
930944
if (Py_SIZE(self) != 0) {
931-
/* Empty previous contents (yes, do this first of all!) */
945+
/* Empty previous contents if possible (yes, do this first of all!). */
932946
if (PyByteArray_Resize((PyObject *)self, 0) < 0)
933947
return -1;
934948
}
935949

936-
/* Should be caused by first init or the resize to 0. */
937950
assert(self->ob_bytes_object == Py_GetConstantBorrowed(Py_CONSTANT_EMPTY_BYTES));
938-
assert(self->ob_exports == 0);
939951

940952
/* Make a quick exit if no first argument */
941953
if (arg == NULL) {
@@ -963,8 +975,11 @@ bytearray___init___impl(PyByteArrayObject *self, PyObject *arg,
963975
}
964976
assert(PyBytes_Check(encoded));
965977

966-
/* Most encodes return a new unique bytes, just use it as buffer. */
967-
if (_PyObject_IsUniquelyReferenced(encoded)
978+
/* Most encodes return a new unique bytes, just use it as buffer.
979+
If there are active exports, let `iconcat` handle raising the
980+
error when encoded is not empty */
981+
if (self->ob_exports == 0
982+
&& _PyObject_IsUniquelyReferenced(encoded)
968983
&& PyBytes_CheckExact(encoded))
969984
{
970985
Py_ssize_t size = Py_SIZE(encoded);
@@ -2937,7 +2952,7 @@ PyTypeObject PyByteArray_Type = {
29372952
0, /* tp_dictoffset */
29382953
bytearray___init__, /* tp_init */
29392954
PyType_GenericAlloc, /* tp_alloc */
2940-
PyType_GenericNew, /* tp_new */
2955+
bytearray_new, /* tp_new */
29412956
PyObject_Free, /* tp_free */
29422957
.tp_version_tag = _Py_TYPE_VERSION_BYTEARRAY,
29432958
};

0 commit comments

Comments
 (0)