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 ]