Skip to content

gh-160303: Fix bytearray state after a failed del b[:n] - #160304

Merged
gpshead merged 4 commits into
python:mainfrom
gpshead:scanfix-bytearrayobjec-qrz6idtc
Nov 11, 2026
Merged

gpshead merged 4 commits into
python:mainfrom
gpshead:scanfix-bytearrayobjec-qrz6idtc

Conversation

@gpshead

@gpshead gpshead commented Oct 22, 2026 •

Copy link
Copy Markdown
Member

After a failed del b[:n], bytearray_setslice_linear() moved ob_start back, but bytearray_resize_storage() had already realigned the data and reset ob_start, so the bytearray then started n bytes before its buffer. The front-delete path now moves ob_start and the size before the shrinking resize and no longer restores anything, so a failed shrink leaves the bytes removed and the larger block in place, raising MemoryError as the non-front path already does. The checks in bytearray_resize_lock_held() are split from a new bytearray_resize_unchecked() so the slice code can call it directly without changing other callers, and the non-front failure path now writes the trailing null byte it was missing.

Fixes #160303

ANT-2026-QRZ6IDTC

When shrinking the storage of a bytearray which has a logical offset,
allocate the new buffer before modifying the object, so that
bytearray_setslice_linear() can restore ob_start if the allocation fails.

@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.

@vstinner recently made it so that failed allocations keep the pre-existing bytes (#157242). That means we should always have a valid bytes. Backported to 3.15 (3.14 has a different internal bytearray structure).

bytearray implements a fast delete at front by having an ob_start member which can be incremented. Also fast delete at end: Leave ob_alloc as is and call Py_SET_SIZE to set the new size. The "effective" bytes being smaller than the allocation is okay.

The bug report sounds like we're getting that pointer manipulation off? I would strongly prefer not allocating a new bytes here with PyBytes_FromStringAndSize

Comment thread Lib/test/test_bytes.py Resolved
@gpshead gpshead added the sprint label Oct 23, 2026
@github-project-automation github-project-automation Bot moved this to Todo in Sprint Oct 23, 2026
@gpshead gpshead moved this from Todo to In Progress in Sprint Oct 25, 2026
Move ob_start and the size before the shrinking resize, so a failed
shrink leaves the bytes removed and the larger block in place, as the
non-front path already does.  Split the checks out of
bytearray_resize_lock_held() so the slice code can resize directly.
Move the tests into test_resize_error.
@gpshead

gpshead commented Nov 1, 2026 •

Copy link
Copy Markdown
Member Author

Reworked to keep ob_start and avoid the extra allocation: the delete now moves ob_start and the size before the resize. A failed shrink leaves the bytes removed and the larger block in place, raising MemoryError as the non-front path already does.

... we could have a failed shrink not raise at all, since the larger block is valid? I still like the error myself. del implies someone was hoping to release memory. Silently not doing so is presumably only going to happen in a high memory pressure situation? Being silent would then just mean that something else fails soon after.

@gpshead
gpshead requested a review from cmaloney November 1, 2026 00:04
Comment thread Lib/test/test_bytes.py Resolved
Comment thread Objects/bytearrayobject.c Resolved
Comment thread Objects/bytearrayobject.c Resolved
…hecked resize

Only the front path of bytearray_setslice_linear() calls
bytearray_resize_unchecked(): it has already advanced ob_start and reduced
the size, so bytearray_resize_lock_held() would return early without
releasing memory. Other slices go through bytearray_resize_lock_held()
again and keep the failure fixup to themselves.

test_resize_error now also covers slices in the middle and at the end,
and checks the null byte after the data with float().

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
@gpshead
gpshead requested a review from cmaloney November 10, 2026 05:03
@gpshead gpshead added the 🔨 test-with-buildbots Test PR w/ buildbots; report in status section label Nov 10, 2026
@bedevere-bot

Copy link
Copy Markdown

🤖 New build scheduled with the buildbot fleet by @gpshead for commit 8601525 🤖

Results will be shown at:

https://buildbot.python.org/all/#/grid?branch=refs%2Fpull%2F160304%2Fmerge

If you want to schedule another build, you need to add the 🔨 test-with-buildbots label again.

@bedevere-bot bedevere-bot removed the 🔨 test-with-buildbots Test PR w/ buildbots; report in status section label Nov 10, 2026
@gpshead
gpshead merged commit 35e2c5a into python:main Nov 11, 2026
182 of 188 checks passed
@github-project-automation github-project-automation Bot moved this from In Progress to Done in Sprint Nov 11, 2026
@miss-islington-app

Copy link
Copy Markdown

Thanks @gpshead for the PR 🌮🎉.. I'm working now to backport this PR to: 3.15.
🐍🍒⛏🤖

@bedevere-app

bedevere-app Bot commented Nov 11, 2026

Copy link
Copy Markdown

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

@bedevere-app bedevere-app Bot removed the needs backport to 3.15 bugs and security fixes label Nov 11, 2026
gpshead added a commit that referenced this pull request Nov 11, 2026
…0304) (#161836)

After a failed `del b[:n]`, `bytearray_setslice_linear()` moved `ob_start` back, but `bytearray_resize_storage()` had already realigned the data and reset `ob_start`, so the bytearray then started `n` bytes before its buffer. The front-delete path now moves `ob_start` and the size before the shrinking resize and no longer restores anything, so a failed shrink leaves the bytes removed and the larger block in place, raising `MemoryError` as the non-front path already does. The checks in `bytearray_resize_lock_held()` are split from a new `bytearray_resize_unchecked()` so the slice code can call it directly without changing other callers, and the non-front failure path now writes the trailing null byte it was missing.

Fixes #160303

ANT-2026-QRZ6IDTC

<!-- gh-issue-number: gh-160303 -->
* Issue: gh-160303
<!-- /gh-issue-number -->

(cherry picked from commit 35e2c5a)

Co-authored-by: Gregory P. Smith <68491+gpshead@users.noreply.github.com>
orsenthil pushed a commit to orsenthil/cpython that referenced this pull request Nov 12, 2026
…GH-160304)

* pythongh-160303: Fix bytearray state after a failed del b[:n]

When shrinking the storage of a bytearray which has a logical offset,
allocate the new buffer before modifying the object, so that
bytearray_setslice_linear() can restore ob_start if the allocation fails.

* pythongh-160303: Keep ob_start instead of allocating in del b[:n]

Move ob_start and the size before the shrinking resize, so a failed
shrink leaves the bytes removed and the larger block in place, as the
non-front path already does.  Split the checks out of
bytearray_resize_lock_held() so the slice code can resize directly.
Move the tests into test_resize_error.

* pythongh-160303: Test failed shrinks not at the front; narrow the unchecked resize

Only the front path of bytearray_setslice_linear() calls
bytearray_resize_unchecked(): it has already advanced ob_start and reduced
the size, so bytearray_resize_lock_held() would return early without
releasing memory. Other slices go through bytearray_resize_lock_held()
again and keep the failure fixup to themselves.

test_resize_error now also covers slices in the middle and at the end,
and checks the null byte after the data with float().
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

bytearray reads and writes outside its buffer after del b[:n] fails with MemoryError

3 participants