Skip to content

gh-153419: Fix several issues around bytearray __init__#153498

Merged
vstinner merged 13 commits into
python:mainfrom
stestagg:gh-153419
Jul 24, 2026
Merged

gh-153419: Fix several issues around bytearray __init__#153498
vstinner merged 13 commits into
python:mainfrom
stestagg:gh-153419

Conversation

@stestagg

@stestagg stestagg commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

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

Resizing a bytearray to 0 length 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')

I realise that the explicit setting of bytes_object to Py_CONSTANT_EMPTY_BYTES is not strictly necessary, as this is also done by the _PyBytes_Resize too, but this seemed to mark the intent of what's happening more clearly, given we rely and assert on that exact behaviour.

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

Resizing a bytearray to 0 length 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')
stestagg added 3 commits July 11, 2026 01:33
This reverts commit c961d61.
There are several ways that bytearray_resize_lock_held gets called
and for any of them, ob_bytes_object may be NULL.

This allows us to remove the extra initialization in __init__ as
this can't be guaranteed to be called anyway.

Handle a related issue in take_bytes where in the extreme case
of the bytes resize allocation failing (on a size reduction)
then the bytearray other state fileds could be left inconsistent with
a null ob_bytes_object.

unconditionally call _canresize in __init__ as on
initial creation, this will always be true,
and if there are any exported views, then we just
always fail, even if technically an empty bytearray
doesn't get modified in this case, it's so rare, it's not worth it.
@stestagg

stestagg commented Jul 11, 2026

Copy link
Copy Markdown
Contributor Author

OK, rewritten based on @cmaloney's comments on the issue.

Some of the changes are a bit non-obvious, for example initializing ob_bytes_object at the top of bytearray_resize_lock_held, but not doing so makes the following obj->ob_start - obj->ob_bytes logic technically UB, so I figured better safe than sorry.

As it now intentionally tolerates uninitialized ob_bytes_object field, (And there was another place that set ob_bytes_object to null, so this is reasonable) I've added a bunch of tests to cover the code paths that used to crash individually, even though the actual fix is in the resize method, this seemed more robust going forward.

We have to check _canresize explicitly in init as the bytearray_resize_lock_held call doesn't always do this (if the size doesn't change).

There may be some other __init__ free-threading issues still lurking, but if there are, that's out of scope for this change really.

Comment thread Lib/test/test_bytes.py Outdated
Comment thread Lib/test/test_bytes.py Outdated
Comment thread Misc/NEWS.d/next/Core_and_Builtins/2026-07-11-01-17-57.gh-issue-153419.U8HJOJ.rst Outdated
Comment thread Objects/bytearrayobject.c
Comment thread Lib/test/test_bytes.py Outdated
Format blurb correctly, and reduce the chatter (There is a user-visible behaviour change, but I guess it's so minor it's not worth mentioning)
@stestagg

Copy link
Copy Markdown
Contributor Author

@cmaloney: are you able to take another look? Sorry for bugging you!

Comment thread Objects/bytearrayobject.c Outdated
/* If ob_bytes_object has not been initialized yet, eagerly initialize
it here so the following code can reason about state more easily,
and things like pointer comparisons are valid. */
if (obj->ob_bytes_object == NULL) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reading through https://docs.python.org/3/extending/newtypes_tutorial.html a bit it feels like should make it so tp_new does the "ob_bytes_object" is always NULL, then other places either get an allocation or assign to the empty bytes.

I think that will reduce the amount of code a bit and make it easier to get right all the corner cases.

@cmaloney cmaloney left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally looking good, like the shape. Some comments and suggestions.

Comment thread Lib/test/test_bytes.py Outdated
@@ -0,0 +1 @@
Fix several :class:`bytearray` crashes caused by calling :meth:`~object.__init__`/:meth:`~object.__new__` in unusual ways.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Fix several :class:`bytearray` crashes caused by calling :meth:`~object.__init__`/:meth:`~object.__new__` in unusual ways.
Fix multiple :class:`bytearray` crashes and reference leaks caused by skipping :meth:`~object.__init__` and broken state setup code.

Comment thread Objects/bytearrayobject.c Outdated
Comment thread Objects/bytearrayobject.c
Comment thread Lib/test/test_bytes.py Outdated
self.assertRaises(BufferError, ba.hex, S(b':'))

def test_uninitialized_instance(self):
# A bytearray created with __new__ so that __init__ is never called

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is fairly verbose at the moment, now that there is a new I think can condense a bit

@stestagg

Copy link
Copy Markdown
Contributor Author

@cmaloney I've updated the PR with the review comments, the ob_exports is technically a no-op because PyType_GenericNew calls PyType_GenericAlloc which zeroes the fields, but it's trivial safe and simple to add, so I did.

Thanks!

@cmaloney cmaloney left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two small comment nits, think this is ready for other core review.

Comment thread Objects/bytearrayobject.c Outdated
Comment thread Objects/bytearrayobject.c
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. */

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this quite matches the current state? The non-null is always handled in the tp_new; the resize is just preventing a reallocation which would leave the existing exports and pointers into unowned memory

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was probably a badly worded comment, as per your previous, the _canresize() isn't strictly needed here, because PyByteArray_Resize checks this before doing an actual resize.

The benefit of putting it here is that the processing of the first argument (source in the docs) is a bit easier if we eagerly check _canresize(). The non-null in the comment is referring to that (because when source is null, we don't do anything more than resize()).

I'm open to suggestions here :)

Fix capitalization in comment

Co-authored-by: Cody Maloney <cmaloney@users.noreply.github.com>

@vstinner vstinner left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall, the change looks good to me. Here is a first review :-)

Comment thread Lib/test/test_bytes.py Outdated
def test_no_init_called(self):
# A bytearray created without calling bytearray.__init__
# should not crash the interpreter (see gh-153419).
def uninitialized():

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hum, "uninitialized" name is misleading. Maybe before the fix, the object could be "uninitialized". But with this fix, the object is now well defined. I suggest renaming the function to "bytearray_new()".

Comment thread Objects/bytearrayobject.c
_Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED(self);
PyByteArrayObject *obj = ((PyByteArrayObject *)self);

assert(obj->ob_bytes_object != NULL);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you mind to apply this sanity check to _PyBytes_Resize()? The empty bytes string singleton must never be resized.

diff --git a/Objects/bytesobject.c b/Objects/bytesobject.c
index f63185e1428..264ce0d9a10 100644
--- a/Objects/bytesobject.c
+++ b/Objects/bytesobject.c
@@ -3381,6 +3381,7 @@ _PyBytes_Resize(PyObject **pv, Py_ssize_t newsize)
         return (*pv == NULL) ? -1 : 0;
     }
 
+    assert(v != bytes_get_empty());
 #ifdef Py_TRACE_REFS
     _Py_ForgetReference(v);
 #endif

Comment thread Objects/bytearrayobject.c Outdated
if (self == NULL) {
return NULL;
}
PyByteArrayObject *obj = _PyByteArray_CAST(self);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer to rename self to op and rename obj to self.

Comment thread Objects/bytearrayobject.c Outdated
if (PyByteArray_Resize((PyObject *)self, 0) < 0) {
return -1;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: you can remove this empty line, since these 2 assertions are related to PyByteArray_Resize() :-)

Comment thread Objects/bytearrayobject.c
}

if (_PyBytes_Resize(&self->ob_bytes_object, to_take) == -1) {
self->ob_bytes_object = Py_GetConstant(Py_CONSTANT_EMPTY_BYTES);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not 100% sure that _PyBytes_Resize() always set self->ob_bytes_object to NULL on error. Also, this private function can change in the future. So I would prefer to add this assertion to make sure that there is no refleak:

Suggested change
self->ob_bytes_object = Py_GetConstant(Py_CONSTANT_EMPTY_BYTES);
assert(self->ob_bytes_object == NULL);
self->ob_bytes_object = Py_GetConstant(Py_CONSTANT_EMPTY_BYTES);

stestagg added 2 commits July 24, 2026 15:27
 - Add assert to bytes object resize
    (ensure we're not resizing the empty constant)
 - Test helper rename
 - Flip obj<->self usage in bytearray_new
 - Assert PyBytes_Resize failure sets ob_bytes_object to NULL

@vstinner vstinner left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Thanks for the updates.

@vstinner
vstinner enabled auto-merge (squash) July 24, 2026 15:11
@vstinner vstinner added the needs backport to 3.15 pre-release feature fixes, bugs and security fixes label Jul 24, 2026
@vstinner

Copy link
Copy Markdown
Member

It would be interesting to backport these changes to 3.13 and 3.14, but I expect that it should be done manually to solve merge conflicts.

@vstinner
vstinner merged commit d5c1b29 into python:main Jul 24, 2026
59 checks passed
@miss-islington-app

Copy link
Copy Markdown

Thanks @stestagg for the PR, and @vstinner for merging it 🌮🎉.. I'm working now to backport this PR to: 3.15.
🐍🍒⛏🤖

@bedevere-app

bedevere-app Bot commented Jul 24, 2026

Copy link
Copy Markdown

GH-154622 is a backport of this pull request to the 3.15 branch.

@bedevere-app bedevere-app Bot removed the needs backport to 3.15 pre-release feature fixes, bugs and security fixes label Jul 24, 2026
@stestagg

Copy link
Copy Markdown
Contributor Author

LGTM. Thanks for the updates.

@vstinner - Thanks for the review, and finding the issues!

vstinner pushed a commit that referenced this pull request Jul 24, 2026
…3498) (#154622)

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 d5c1b29)

Co-authored-by: Steve Stagg <stestagg@gmail.com>
Co-authored-by: Cody Maloney <cmaloney@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants