Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Re-check the destination of a memoryview slice assignment
Acquiring the source runs `__buffer__`, which can release the destination view,
so check the released flag again once the source is in hand and before the
structures are compared, as `copy_single` does.

Build the sliced destination as a view that counts as no export, the way a
`Py_buffer dest = *view` copy does. Holding one kept the exporter unresizable
for the length of the assignment, so a source that released the view and then
resized the exporter met a `BufferError` instead of the assignment reporting the
released view.

Assisted-by: Claude
  • Loading branch information
youknowone committed Aug 15, 2026
commit f3abad88bf8f8ac00d671896b003574f8ad9166f
20 changes: 19 additions & 1 deletion crates/vm/src/builtins/memory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,21 @@ impl PyMemoryView {
}
}

/// A view for a temporary that never reaches Python. It counts as no export,
/// so the exporter stays exactly as resizable as it already was, the way a
/// `Py_buffer dest = *view` copy does.
#[must_use]
fn borrowed_view(&self) -> Self {
Self {
buffer: self.buffer.detached(),
released: AtomicCell::new(false),
restricted: AtomicCell::new(false),
format_spec: self.format_spec.clone(),
desc: self.desc.clone(),
hash: OnceCell::new(),
}
}

fn try_not_released(&self, vm: &VirtualMachine) -> PyResult<()> {
if self.released.load() {
Err(vm.new_value_error("operation forbidden on released memoryview object"))
Expand Down Expand Up @@ -582,7 +597,7 @@ impl Py<PyMemoryView> {
return Err(vm.new_not_implemented_error("sub-view are not implemented"));
}

let mut dest = self.new_view();
let mut dest = self.borrowed_view();
dest.init_slice(slice, 0, vm)?;
dest.init_len();

Expand All @@ -599,6 +614,9 @@ impl Py<PyMemoryView> {

// PyObject_GetBuffer(value, &src, PyBUF_FULL_RO)
let src = PyBuffer::try_from_object(vm, src)?;
// Acquiring the source ran `__buffer__`, which can release this view.
// copy_single: CHECK_RELEASED_INT_AGAIN
self.try_not_released(vm)?;

if !is_equiv_structure(&src.desc, &dest.desc) {
return Err(vm.new_value_error(
Expand Down
40 changes: 40 additions & 0 deletions extra_tests/snippets/builtin_memoryview.py
Original file line number Diff line number Diff line change
Expand Up @@ -443,3 +443,43 @@ def __release_buffer__(self, view):


test_restricted_view_compares()


def test_release_during_slice_assignment():
# copy_single: acquiring the source runs __buffer__, which can release the
# destination view.
ba = bytearray(b"abcd")
mv = memoryview(ba)

class Src:
def __buffer__(self, flags):
mv.release()
return memoryview(b"WXYZ")

try:
mv[:] = Src()
raise AssertionError("wrote through a released view")
except ValueError as e:
assert "released memoryview" in str(e), e
assert bytes(ba) == b"abcd"

# The destination of a slice assignment counts as no export, so the source
# may resize the exporter once it has released the view.
ba = bytearray(b"abcd")
mv = memoryview(ba)

class Shrink:
def __buffer__(self, flags):
mv.release()
ba.clear()
return memoryview(b"WXYZ")

try:
mv[:] = Shrink()
raise AssertionError("wrote through a released view")
except ValueError as e:
assert "released memoryview" in str(e), e
assert bytes(ba) == b""


test_release_during_slice_assignment()
Loading