diff --git a/Lib/test/test_bytes.py b/Lib/test/test_bytes.py index e211c3d15a4ed2..e3e703fef1eb5d 100644 --- a/Lib/test/test_bytes.py +++ b/Lib/test/test_bytes.py @@ -2197,6 +2197,46 @@ def __len__(self): self.assertRaises(BufferError, ba.hex, S(b':')) + def test_no_init_called(self): + # A bytearray created without calling bytearray.__init__ + # should not crash the interpreter (see gh-153419). + def bytearray_new(): + return bytearray.__new__(bytearray) + + bytearray_new().insert(0, 1) + bytearray_new().extend(b"x") + bytearray_new().extend([1, 2, 3]) + bytearray_new().resize(4) + bytearray_new().__init__(5) + bytearray_new().__init__(b"xyz") + bytearray_new().take_bytes() + bytearray_new().take_bytes(0) + + a = bytearray_new() + a.append(1) + + a = bytearray_new() + a += b"x" + + a = bytearray_new() + a[:] = b"xyz" + + def test_reinit_length(self): + # There is a shortcut taken when resizing, where alloc/2 < newsize. + # In this case, the existing buffer is reused, rather than reset. + # If this happens when newsize == 0 and alloc == 1, then various + # code assumptions can be violated. This test should catch those + # in debug builds. (see gh-153419) + a = bytearray(1) + a.__init__() + self.assertEqual(a, b"") + + def test_reinit_with_view(self): + a = bytearray() + with memoryview(a): + self.assertRaises(BufferError, a.__init__, "x", "ascii") + self.assertEqual(a, b"") + class AssortedBytesTest(unittest.TestCase): # diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-07-11-01-17-57.gh-issue-153419.U8HJOJ.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-07-11-01-17-57.gh-issue-153419.U8HJOJ.rst new file mode 100644 index 00000000000000..4459ec835395ea --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-07-11-01-17-57.gh-issue-153419.U8HJOJ.rst @@ -0,0 +1 @@ +Fix multiple :class:`bytearray` crashes and reference leaks caused by skipping :meth:`~object.__init__` and broken state setup code. diff --git a/Objects/bytearrayobject.c b/Objects/bytearrayobject.c index 696ddad8efaae5..ca1cf0beeb7d9f 100644 --- a/Objects/bytearrayobject.c +++ b/Objects/bytearrayobject.c @@ -213,6 +213,9 @@ bytearray_resize_lock_held(PyObject *self, Py_ssize_t requested_size) { _Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED(self); PyByteArrayObject *obj = ((PyByteArrayObject *)self); + + assert(obj->ob_bytes_object != NULL); + /* All computations are done unsigned to avoid integer overflows (see issue #22335). */ size_t alloc = (size_t) obj->ob_alloc; @@ -236,6 +239,14 @@ bytearray_resize_lock_held(PyObject *self, Py_ssize_t requested_size) return -1; } + /* Resize to 0 resets to empty bytes (see issue #153419). */ + if (requested_size == 0) { + Py_SETREF(obj->ob_bytes_object, + Py_GetConstant(Py_CONSTANT_EMPTY_BYTES)); + bytearray_reinit_from_bytes(obj, 0, 0); + return 0; + } + if (size + logical_offset <= alloc) { /* Current buffer is large enough to host the requested size, decide on a strategy. */ @@ -902,6 +913,20 @@ bytearray_ass_subscript(PyObject *op, PyObject *index, PyObject *values) return ret; } +static PyObject * +bytearray_new(PyTypeObject *type, PyObject *args, PyObject *kwds) +{ + PyObject *op = PyType_GenericNew(type, args, kwds); + if (op == NULL) { + return NULL; + } + PyByteArrayObject *self = _PyByteArray_CAST(op); + self->ob_bytes_object = Py_GetConstant(Py_CONSTANT_EMPTY_BYTES); + bytearray_reinit_from_bytes(self, 0, 0); + self->ob_exports = 0; + return op; +} + /*[clinic input] bytearray.__init__ @@ -920,20 +945,16 @@ bytearray___init___impl(PyByteArrayObject *self, PyObject *arg, PyObject *it; PyObject *(*iternext)(PyObject *); - /* First __init__; set ob_bytes_object so ob_bytes is always non-null. */ - if (self->ob_bytes_object == NULL) { - self->ob_bytes_object = Py_GetConstant(Py_CONSTANT_EMPTY_BYTES); - bytearray_reinit_from_bytes(self, 0, 0); - self->ob_exports = 0; + /* Disallow any __init__ call if the object is not resizable (has exports) + to make the handling of non-null `source` init values simpler. */ + if (!_canresize(self)) { + return -1; } - if (Py_SIZE(self) != 0) { - /* Empty previous contents (yes, do this first of all!) */ - if (PyByteArray_Resize((PyObject *)self, 0) < 0) - return -1; + /* Empty any previous contents (do this first of all!). */ + if (PyByteArray_Resize((PyObject *)self, 0) < 0) { + return -1; } - - /* Should be caused by first init or the resize to 0. */ assert(self->ob_bytes_object == Py_GetConstantBorrowed(Py_CONSTANT_EMPTY_BYTES)); assert(self->ob_exports == 0); @@ -1607,6 +1628,9 @@ bytearray_take_bytes_impl(PyByteArrayObject *self, PyObject *n) } if (_PyBytes_Resize(&self->ob_bytes_object, to_take) == -1) { + assert(self->ob_bytes_object == NULL); + self->ob_bytes_object = Py_GetConstant(Py_CONSTANT_EMPTY_BYTES); + bytearray_reinit_from_bytes(self, 0, 0); Py_DECREF(remaining); return NULL; } @@ -2937,7 +2961,7 @@ PyTypeObject PyByteArray_Type = { 0, /* tp_dictoffset */ bytearray___init__, /* tp_init */ PyType_GenericAlloc, /* tp_alloc */ - PyType_GenericNew, /* tp_new */ + bytearray_new, /* tp_new */ PyObject_Free, /* tp_free */ .tp_version_tag = _Py_TYPE_VERSION_BYTEARRAY, }; diff --git a/Objects/bytesobject.c b/Objects/bytesobject.c index f63185e14284b1..ef35dad82e8aae 100644 --- a/Objects/bytesobject.c +++ b/Objects/bytesobject.c @@ -3380,6 +3380,7 @@ _PyBytes_Resize(PyObject **pv, Py_ssize_t newsize) Py_DECREF(v); return (*pv == NULL) ? -1 : 0; } + assert(v != bytes_get_empty()); #ifdef Py_TRACE_REFS _Py_ForgetReference(v);