Keep a bytearray exported while it formats with % (#9147)

bytearray.__mod__ copied the format and ran the arguments' __repr__,
__str__, __bytes__ and __index__ without holding an export, so a callback
could clear or extend the format and formatting went on with the old
copy:

    fmt = bytearray(b"%a end")
    class S:
        def __repr__(self):
            fmt.clear()
            return "E"
    fmt % S()   # bytearray(b'E end'), fmt left empty

CPython holds a buffer on the format for the whole call, so the resize
fails with BufferError and the format is left as it was. Taking an export
for the duration of __mod__ does the same here, the way bytearray.join
already does for its separator.

This lets test_mod_concurrent_mutation in test_bytes run again.

Assisted-by: Claude Code:claude-opus-5-5
This commit is contained in:
Luan Taraschi authored and GitHub committed 2026-10-15 22:06:16 +09:00
1 parent 901431963b
commit bf404e0b7c
2 files changed
+3 -2

No files matched your search

-1
View File
@@ -1393,7 +1393,6 @@ class ByteArrayTest(BaseBytesTest, unittest.TestCase):
except OSError:
pass
@unittest.skip("TODO: RUSTPYTHON; segfault")
def test_mod_concurrent_mutation(self):
# Prevent crash in __mod__ when formatting mutates the bytearray.
# Regression test for https://github.com/python/cpython/issues/142557.
+3 -1
View File
@@ -454,7 +454,9 @@ impl PyByteArray {
fn __mod__(&self, values: PyObjectRef, vm: &VirtualMachine) -> PyResult<Self> {
// Formatting calls the values' conversion methods, which can reach
// this bytearray, so the format is taken by value.
// this bytearray, so the format is taken by value. It also stays
// exported until formatting ends, so resizing it raises BufferError.
let _export = self.begin_export();
let format = self.inner().clone();
let formatted = format.cformat(values, vm)?;
Ok(formatted.into())