Fix bytearray membership racing with resizing (#9091)

* Fix bytearray membership racing with resizing

Search exact bytes and int needles under the data read lock instead of
taking a temporary export. This prevents a concurrent slice replacement
from receiving BufferError for a callback-free membership operation.

Preserve export protection for generic index/buffer callbacks and actual
memoryviews. Add Python coverage for these contracts.

AI assistance: Codex/OpenAI diagnosed the race, implemented and reviewed
the patch and regression snippet, and ran the reported validation. The
runtime did not expose an exact model version.

Assisted-by: Codex:model-version-unavailable

* Handle bool needles and consolidate bytearray regression coverage

Include exact builtin bool in the callback-free integer membership path.
Keep int subclasses and generic conversion paths under export protection.
Move this PR's regression coverage into the existing bytearray snippet
and cover True/False membership as requested in review.

AI assistance: Codex/OpenAI implemented and independently reviewed the
review follow-up and ran the reported validation. The runtime did not
expose an exact model version.

Assisted-by: Codex:model-version-unavailable
This commit is contained in:
youknowdot authored and GitHub committed 2026-10-13 05:19:27 +00:00
1 parent 8f47ced2c7
commit e09846b4eb
2 files changed
+100 -2

No files matched your search

+17 -2
View File
@@ -1,6 +1,6 @@
//! Implementation of the python bytearray object. //! Implementation of the python bytearray object.
use super::{ use super::{
PositionIterInternal, PyBytes, PyDictRef, PyGenericAlias, PyStrRef, PyTuple, PyTupleRef, PositionIterInternal, PyBytes, PyDictRef, PyGenericAlias, PyInt, PyStrRef, PyTuple, PyTupleRef,
PyType, PyTypeRef, iter::builtins_iter, locked_next, PyType, PyTypeRef, iter::builtins_iter, locked_next,
}; };
use crate::{ use crate::{
@@ -12,7 +12,7 @@ use crate::{
bytes_inner::{ bytes_inner::{
ByteInnerFindOptions, ByteInnerHexOptions, ByteInnerNewOptions, ByteInnerPaddingOptions, ByteInnerFindOptions, ByteInnerHexOptions, ByteInnerNewOptions, ByteInnerPaddingOptions,
ByteInnerReplaceOptions, ByteInnerSplitOptions, ByteInnerStripOptions, ByteInnerSub, ByteInnerReplaceOptions, ByteInnerSplitOptions, ByteInnerStripOptions, ByteInnerSub,
ByteInnerTranslateOptions, BytesJoin, DecodeArgs, PyBytesInner, bytes_decode, ByteInnerTranslateOptions, ByteOr, BytesJoin, DecodeArgs, PyBytesInner, bytes_decode,
}, },
class::PyClassImpl, class::PyClassImpl,
common::{ common::{
@@ -317,6 +317,21 @@ impl PyByteArray {
} }
fn __contains__(&self, needle: PyObjectRef, vm: &VirtualMachine) -> PyResult<bool> { fn __contains__(&self, needle: PyObjectRef, vm: &VirtualMachine) -> PyResult<bool> {
// Exact builtins cannot call Python while preparing the needle. The
// read lock alone protects their search; a temporary export would make
// a concurrent resize fail instead of waiting for that lock.
if let Some(bytes) = needle.downcast_ref_if_exact::<PyBytes>(vm) {
return Ok(self.borrow_buf().contains_str(bytes.as_bytes()));
}
let int = if needle.class().is(vm.ctx.types.bool_type) {
needle.downcast_ref::<PyInt>()
} else {
needle.downcast_ref_if_exact::<PyInt>(vm)
};
if let Some(int) = int {
let byte = int.as_bigint().byte_or(vm)?;
return Ok(self.borrow_buf().contains(&byte));
}
// Converting the needle runs Python, which must not resize this bytearray. // Converting the needle runs Python, which must not resize this bytearray.
self.exports.fetch_add(1, Ordering::Release); self.exports.fetch_add(1, Ordering::Release);
scopeguard::defer! { self.exports.fetch_sub(1, Ordering::Release); } scopeguard::defer! { self.exports.fetch_sub(1, Ordering::Release); }
+83
View File
@@ -1504,3 +1504,86 @@ def test_bytearray_callback_exports():
test_bytearray_callback_exports() test_bytearray_callback_exports()
def test_bytearray_contains_builtin_and_callback_exports():
for value in (b"", b"a", b"ab\x00\xff", b"\x01"):
target = bytearray(value)
for needle in (
b"",
b"a",
b"ab",
b"z",
b"\x00",
b"\xff",
0,
97,
255,
False,
True,
):
assert (needle in target) == (needle in value)
assert target in target
def must_raise_buffer_error(action):
try:
action()
except BufferError:
return
raise AssertionError("resize was not rejected")
target = bytearray(b"a")
view = memoryview(target)
try:
assert b"a" in target
assert 97 in target
must_raise_buffer_error(lambda: target.append(0))
target[:] = b"b"
assert b"b" in target
assert 98 in target
finally:
view.release()
target.append(0)
assert target == b"b\x00"
class IndexNeedle:
def __index__(self):
must_raise_buffer_error(lambda: target.append(0))
target[0] = 98
return 98
class BytesNeedle(bytes):
__index__ = IndexNeedle.__index__
for needle in (IndexNeedle(), BytesNeedle(b"\xdd")):
target[:] = b"a"
assert needle in target
assert target == b"b"
target.append(0)
assert target == b"b\x00"
for needle in (-1, 256):
with assert_raises(ValueError):
needle in target
events = [0, 0]
class BufferNeedle:
def __buffer__(self, flags):
must_raise_buffer_error(lambda: target.append(0))
target[0] = 98
events[0] += 1
return memoryview(b"b")
def __release_buffer__(self, view):
must_raise_buffer_error(lambda: target.append(0))
events[1] += 1
target[:] = b"a"
assert BufferNeedle() in target
assert events == [1, 1]
target.append(0)
assert target == b"b\x00"
test_bytearray_contains_builtin_and_callback_exports()