]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
[3.15] gh-153419: Fix several issues around bytearray __init__ (GH-153498) (#154622)
authorMiss Islington (bot) <31488909+miss-islington@users.noreply.github.com>
Fri, 24 Jul 2026 17:25:56 +0000 (19:25 +0200)
committerGitHub <noreply@github.com>
Fri, 24 Jul 2026 17:25:56 +0000 (19:25 +0200)
gh-153419: Fix several issues around bytearray __init__ (GH-153498)

Introduce a bytearray_new() function to ensure that
ob_bytes_object is always set on a bytearray.

Resizing a bytearray to 0 length now 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')
(cherry picked from commit d5c1b29658063d1ef9d66515d8e4cb9929126be7)

Co-authored-by: Steve Stagg <stestagg@gmail.com>
Co-authored-by: Cody Maloney <cmaloney@users.noreply.github.com>
Lib/test/test_bytes.py
Misc/NEWS.d/next/Core_and_Builtins/2026-07-11-01-17-57.gh-issue-153419.U8HJOJ.rst [new file with mode: 0644]
Objects/bytearrayobject.c
Objects/bytesobject.c

index e211c3d15a4ed2018413fe6a5090eb850389d911..e3e703fef1eb5d607c8c83bd47d8dcb1ec3b226c 100644 (file)
@@ -2197,6 +2197,46 @@ class ByteArrayTest(BaseBytesTest, unittest.TestCase):
 
         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 (file)
index 0000000..4459ec8
--- /dev/null
@@ -0,0 +1 @@
+Fix multiple :class:`bytearray` crashes and reference leaks caused by skipping :meth:`~object.__init__` and broken state setup code.
index ca7956579e80bb6db9ffc4c8a5029e00388e7624..70e9e87210b60b87d3194cb2814fd5f3f81fff0c 100644 (file)
@@ -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,
 };
index 1135770549c01741fda22a22147277f341d0c65f..33dc66eec6299079f1986f5d69528b622b8d53d0 100644 (file)
@@ -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);