Repository navigation
debug abort/ref leak calling __init__(<str>, <enc>) on a one-byte bytearray #153419
Description
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:
cpython/Objects/bytearrayobject.c
Lines 930 to 934 in cd9994e
| 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):
cpython/Objects/bytearrayobject.c
Lines 239 to 253 in cd9994e
| 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:
cpython/Objects/bytearrayobject.c
Lines 967 to 971 in cd9994e
| 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 ]
Activity
cmaloney commentedon Jul 9, 2026
Nice find!
My leaning here is resize to zero should always replace with the pre-allocated empty bytes object which should make the assert pass and the later code not leak (the empty bytes object is immortal). I can write a patch for that today or happy to review one from you.
stestagg commentedon Jul 9, 2026
Thanks @cmaloney! useful feedback, I can have a go at a PR this evening
stestagg commentedon Jul 10, 2026
@cmaloney: Ok, I am looking at this, but it's a small can of worms, there are actually three separate bugs I found relating to this:
__init__()with an active export also aborts() (or does bad things)bytearray.__new__()never inits the underlyign bytes ob, so we get:I'm proposing to kill three birds with one refactor here by:
self->ob_bytes_object = Py_GetConstant(Py_CONSTANT_EMPTY_BYTES); etcto a new tp_new implPyByteArray_Resize(.., 0)on initPyByteArray_Resizeexplicitly init toPy_CONSTANT_EMPTY_BYTESwhensize == 0reinit_from_bytes/iconcatbecause technically there might be an export of the empty bytearray active.cmaloney commentedon Jul 10, 2026
Resize and re-init should raise an exception if there are exports by calling
_canresize. Could allow re-init with exactly the same size but would need a use case where that provides enough value for the extra codecpython/Objects/bytearrayobject.c
Lines 113 to 122 in c22e9c9
In CPython typically only one of
__init__or__new__is implemented on an object (some details in https://docs.python.org/3/c-api/typeobj.html#c.PyTypeObject.tp_new). It is intentional thatob_bytesis NULL until the first__init__call. The scope is limited by https://docs.python.org/3/c-api/lifecycle.html.stestagg commentedon Jul 11, 2026
@cmaloney - Thanks for the feedback, I've updated the PR accordingly.
The tp_new/tp_init thing is interesting, it feels wrong to me to leave strictly internal pointer fields uninitialized, but this new version just ensures that all the code paths expect that!
cmaloney commentedon Jul 11, 2026
Yea, it's definitely CPython specific coding patterns. I picked up some of those pieces in https://github.com/python/cpython/pull/140128/changes#r2478883438 which I suspect will be helpful in updating this PR. Definitely recommend doing an AddressSanitizer build / run. Left some comments on the PR.
flameastro commentedon Jul 11, 2026
gh-153419: Fix several issues around bytearray __init__ (#153498)
[3.15] gh-153419: Fix several issues around bytearray __init__ (GH-15…
StanFromIreland commentedon Jul 28, 2026
This looks complete so I'm closing, please let me know if that's not the case and I'll reopen.
cmaloney commentedon Jul 29, 2026
To close out one lingering thread: On the main PR was an ask if we needed to backport to 3.14. No backport to 3.14 is needed.
In a local branch I backported the test change to 3.14 excluding the
take_byespart and it passes there / init problems were introduced with my change to contain aPyBytesObject. Those are in 3.15+ and resolved with this PR + existing backport to 3.15.vstinner commentedon Aug 7, 2026
Oh ok. I planned to backport this fix to stable branches in my TODO list. So cool, I don't have to do it :-)
pythongh-153419: Fix several issues around bytearray __init__ (python…