From fb9ada105354b1d7be73303d90632baa9cbc1863 Mon Sep 17 00:00:00 2001 From: 1ndahous3 <9205230+1ndahous3@users.noreply.github.com> Date: Wed, 14 Oct 2026 13:23:49 +0300 Subject: [PATCH 1/2] Reuse unlocked buffers in os.write Assisted-by: Codex:gpt-6 --- crates/vm/src/stdlib/os.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/vm/src/stdlib/os.rs b/crates/vm/src/stdlib/os.rs index d096157d092..1e846d9fa61 100644 --- a/crates/vm/src/stdlib/os.rs +++ b/crates/vm/src/stdlib/os.rs @@ -460,9 +460,9 @@ pub(super) mod _os { #[pyfunction] fn write(fd: crt_fd::Borrowed<'_>, data: ArgBytesLike, vm: &VirtualMachine) -> PyResult { - let owned = data.with_ref(|b| b.to_vec()); + let buffer = data.borrow_buf_unlocked(vm)?; loop { - match vm.allow_threads(|| crt_fd::write(fd, &owned)) { + match vm.allow_threads(|| crt_fd::write(fd, &buffer)) { Ok(n) => return Ok(n), Err(e) if e.raw_os_error() == Some(libc::EINTR) => { vm.check_signals()?; From 29338d7418f298ff7b71fa37cad47e76c5bbc76e Mon Sep 17 00:00:00 2001 From: 1ndahous3 <9205230+1ndahous3@users.noreply.github.com> Date: Wed, 14 Oct 2026 14:16:04 +0300 Subject: [PATCH 2/2] Snapshot buffers without immutable storage Assisted-by: Codex:gpt-6 --- crates/common/src/borrow.rs | 7 ++--- crates/vm/src/function/buffer.rs | 11 ++++--- crates/vm/src/stdlib/_ctypes/function.rs | 38 ++++++++++++++++++++++++ 3 files changed, 46 insertions(+), 10 deletions(-) diff --git a/crates/common/src/borrow.rs b/crates/common/src/borrow.rs index ebf69fde71d..0f9903b6d22 100644 --- a/crates/common/src/borrow.rs +++ b/crates/common/src/borrow.rs @@ -39,10 +39,9 @@ impl_from!('a, T, BorrowedValue<'a, T>, impl<'a, T: ?Sized> BorrowedValue<'a, T> { /// Whether reaching the value holds a lock that other threads wait on. /// - /// An immutable object hands out a plain reference and answers `false`; - /// one whose storage can change hands out a guard. A caller about to wait - /// for something unrelated -- a peer, a file, a signal -- can use this to - /// decide whether it may keep the borrow for the duration. + /// A plain reference answers `false`, including references to externally + /// managed memory. This does not guarantee that the storage is immutable + /// or safe to borrow while another thread runs. #[must_use] pub const fn is_locked(&self) -> bool { !matches!(self, Self::Ref(_)) diff --git a/crates/vm/src/function/buffer.rs b/crates/vm/src/function/buffer.rs index 51f9dc28ac5..528692fd765 100644 --- a/crates/vm/src/function/buffer.rs +++ b/crates/vm/src/function/buffer.rs @@ -1,7 +1,7 @@ use crate::{ AsObject, PyObject, PyObjectRef, PyResult, TryFromBorrowedObject, TryFromObject, VirtualMachine, - builtins::{PyStr, PyStrRef}, + builtins::{PyBytes, PyStr, PyStrRef}, common::borrow::{BorrowedValue, BorrowedValueMut}, protocol::{BufferFlags, PyBuffer}, }; @@ -55,13 +55,12 @@ impl ArgBytesLike { /// `borrow_buf` may answer with a lock that every other thread writing to /// the same object waits on, and a thread waiting on a lock never reaches /// a safepoint, so keeping one across a wait for a peer, a pipe or a - /// signal stops the world from being stopped at all. Bytes reached that - /// way are copied out first. Bytes that lock nothing -- an immutable - /// object's -- are borrowed where they lie, which is all CPython holds in - /// either case. + /// signal stops the world from being stopped at all. Only storage owned + /// by `bytes` can be borrowed across that wait. Other exporters are copied: + /// an unlocked or readonly buffer may still alias writable raw memory. pub fn borrow_buf_unlocked(&self, vm: &VirtualMachine) -> PyResult> { let borrowed = self.borrow_buf(); - if !borrowed.is_locked() { + if !borrowed.is_locked() && self.source_object().downcast_ref::().is_some() { return Ok(UnlockedBuf::Borrowed(borrowed)); } let mut copy = Vec::new(); diff --git a/crates/vm/src/stdlib/_ctypes/function.rs b/crates/vm/src/stdlib/_ctypes/function.rs index 4c9b46a9313..689d13b6e81 100644 --- a/crates/vm/src/stdlib/_ctypes/function.rs +++ b/crates/vm/src/stdlib/_ctypes/function.rs @@ -2020,3 +2020,41 @@ impl Py { self.callable.clone() } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::{TryFromObject, class::PyClassImpl, function::ArgBytesLike}; + + #[test] + fn unlocked_raw_buffers_are_copied_even_when_readonly() { + crate::Interpreter::without_stdlib(Default::default()).enter(|vm| { + let _class = RawMemoryBuffer::make_static_type(); + for readonly in [false, true] { + let mut storage = *b"before"; + let view = memoryview_at_impl( + storage.as_mut_ptr() as usize, + storage.len() as isize, + readonly, + vm, + ) + .unwrap(); + let buffer = ArgBytesLike::try_from_object(vm, view).unwrap(); + let snapshot = buffer.borrow_buf_unlocked(vm).unwrap(); + assert_ne!(snapshot.as_ptr(), storage.as_ptr()); + storage.copy_from_slice(b"after!"); + assert_eq!(&*snapshot, b"before"); + } + + let bytes = vm.ctx.new_bytes(b"bytes".to_vec()); + let view = crate::builtins::PyMemoryView::from_object(bytes.as_object(), vm) + .unwrap() + .into_pyobject(vm); + for obj in [bytes.clone().into(), view] { + let buffer = ArgBytesLike::try_from_object(vm, obj).unwrap(); + let borrowed = buffer.borrow_buf_unlocked(vm).unwrap(); + assert_eq!(borrowed.as_ptr(), bytes.as_bytes().as_ptr()); + } + }); + } +}