Skip to content

debug abort/ref leak calling __init__(<str>, <enc>) on a one-byte bytearray #153419

Description

@stestagg

Crash report

What happened?

The debug assert is triggered by:

bytearray(1).__init__()

For the refleak, we need:

b = bytearray((1, ))
b.__init__("x", "ascii")

But this hits the above assert before leaking the ref, and in a non-debug build, you leak a 1-byte Bytes object each time, so detecting this is hard without custom c hooks.

The issue is that bytearray___init___impl, if ob_bytes_object is not NULL, calls PyByteArray_Resize((PyObject *)self, 0) to reset the backing bytes object:

if (Py_SIZE(self) != 0) {
/* Empty previous contents (yes, do this first of all!) */
if (PyByteArray_Resize((PyObject *)self, 0) < 0)
return -1;
}

assuming that this resets the bytes object to be the Py_CONSTANT_EMPTY_BYTES object by virtue of being resized to 0. BUT:

PyByteArray_Resize has a fast path for when the requested size < the allocated size, but >= alloc / 2
(when alloc == 1, then 0 >= alloc / 2):

if (size + logical_offset <= alloc) {
/* Current buffer is large enough to host the requested size,
decide on a strategy. */
if (size < alloc / 2) {
/* Major downsize; resize down to exact size */
alloc = size;
}
else {
/* Minor downsize; quick exit */
Py_SET_SIZE(self, size);
/* Add mid-buffer null; end provided by bytes. */
PyByteArray_AS_STRING(self)[size] = '\0'; /* Trailing null */
return 0;
}
}

In this case, the underlying bytes object is re-used with size set to 0, which is not the same as Py_CONSTANT_EMPTY_BYTES.

However, bytearray___init___impl immediately then asserts:
`assert(self->ob_bytes_object == Py_GetConstantBorrowed(Py_CONSTANT_EMPTY_BYTES));
which causes the above issue.

For the refleak, we have to continue a bit further down this function, because we're left with a usable, but empty bytes object, which is fine as-is, but if a unicode object is passed in, there's another fast path for when the string is encoded:

if (_PyObject_IsUniquelyReferenced(encoded)
&& PyBytes_CheckExact(encoded))
{
Py_ssize_t size = Py_SIZE(encoded);
self->ob_bytes_object = encoded;

This unilaterally overwrites the ob_bytes_object which leaks the above bytes object reference. (Code assumes that the ob_bytes_object is the immortal Py_CONSTANT_EMPTY_BYTES, so this would be safe if the assert were true).

I'm not sure of the best fix here, in some ways, none of this is very important, because leaking tiny 1-byte Py_Bytes objects when you call __init__ on an object is pretty minor. and the resize fast path seems reasonable, it would be nice if there were a more explicit/simple bytearray_empty() or _clear() method that could be used with a simpler implementation, or we just remove the assert and decref the ob_bytes_object in the case where a unicode object is passed in?

CPython versions tested on:

CPython main branch

Operating systems tested on:

Linux

Output from running 'python -VV' on the command line:

Python 3.16.0a0 (heads/main-dirty:f932a74, Jul 8 2026, 14:33:20) [Clang 22.1.6 ]

Linked PRs

Metadata

Metadata

Assignees

No one assigned

    Labels

    3.15pre-release feature fixes, bugs and security fixes3.16new features, bugs and security fixesinterpreter-core(Objects, Python, Grammar, and Parser dirs)type-crashA hard crash of the interpreter, possibly with a core dump

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions