Visitar URL original
Fix crashes found hunting the last open fuzzing record by youknowone · Pull Request #8524 · RustPython/RustPython · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
f24691e
specialize: check the member descriptor's type before caching its slo…
youknowone Aug 14, 2026
40e67e1
socket: reserve recv()'s buffer fallibly
youknowone Aug 14, 2026
40a839b
types: count the __call__ and __get__ slot dispatches as recursion
youknowone Aug 14, 2026
bb744d6
typevar: show a ParamSpecArgs origin by its repr
youknowone Aug 14, 2026
7e4b96e
Do not hold a lock across a call back into Python
youknowone Aug 14, 2026
6e47646
Validate memoryview.cast() arguments and export negative strides corr…
youknowone Aug 14, 2026
2432fd7
Charge native recursion to the stack, not to the frame limit
youknowone Aug 14, 2026
02a6193
Report a size that cannot be allocated instead of aborting on it
youknowone Aug 14, 2026
ff52fe0
marshal: answer allow_code where a code object is written or read
youknowone Aug 14, 2026
d7cf971
Do not lock an object while running code that can reach it
youknowone Aug 14, 2026
61b989b
Stop asserting a pbkdf2 message that depends on the width of a C long
youknowone Aug 14, 2026
c9449a6
Publish and read the same pointer for a thread's top frame
youknowone Aug 15, 2026
ee782e0
Decide stop-the-world parking under the thread registry lock
youknowone Aug 15, 2026
fab2b3b
Keep an atexit callback alive while it is being compared
youknowone Aug 15, 2026
e3dcd8e
Hold atexit entries in PyRc rather than Arc
youknowone Aug 15, 2026
773ae23
Do not hold a buffer's storage while waiting for a peer
youknowone Aug 15, 2026
125d5aa
Do not hold a buffer's storage while waiting to hand it over
youknowone Aug 15, 2026
62ed618
Check select()'s descriptor limit while the sequence is walked
youknowone Aug 15, 2026
c65fe42
Bound native recursion where the C stack cannot be measured
youknowone Aug 15, 2026
00b64c3
Report the failure to allocate pbkdf2's key and Take.readinto's scratch
youknowone Aug 15, 2026
7c40cb9
Read the frozen-code tuple length as a signed length
youknowone Aug 15, 2026
ce43008
Make the snippets from the fuzzer sweep assert what they check
youknowone Aug 15, 2026
3d69690
Ask for a marshal container's room instead of assuming it
youknowone Aug 15, 2026
366ef3e
Compare the blocking-buffer snippet against something
youknowone Aug 15, 2026
ab0d4ff
Refuse array.frombytes() a source whose items are not bytes
youknowone Aug 15, 2026
7fad3b0
Tell apart the ways marshal data can be bad
youknowone Aug 15, 2026
a7f7021
Hash the collector's tables by address rather than by SipHash
youknowone Aug 16, 2026
691d6ca
Keep the collection's candidates and their counts in one table
youknowone Aug 16, 2026
3139b95
gc: collect referents into one buffer instead of a vector per object
youknowone Aug 16, 2026
dce42a3
memoryview and struct: match the checks and errors of the reference
youknowone Aug 16, 2026
5a5ba17
io: decide the readinto path by file type, not by seekability
youknowone Aug 16, 2026
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
io: decide the readinto path by file type, not by seekability
FileIO.readinto wrote straight into the caller's buffer, holding its write
borrow, when the fd was seekable; otherwise it read aside into scratch and
copied. Seekability stood in for "this read answers without waiting on a
peer", which a pipe on Windows breaks: lseek on one succeeds, so the pipe
took the borrow-holding path and every other thread touching that bytearray
waited for the peer.

host_io::reads_without_waiting answers it directly -- seekability elsewhere,
GetFileType() == FILE_TYPE_DISK on Windows.

The regression snippet times each operation separately, so a failure names
the one that waited; it asserts the transfer is still in flight before
checking the export; and the socket case fills the connection until it
refuses rather than assuming a size that outruns it, which SO_SNDBUF on an
already-connected pair does not settle.

Assisted-by: Claude
  • Loading branch information
youknowone committed Aug 16, 2026
commit 5a5ba17e147817be69a79c5df43ab283f45f36bb
23 changes: 23 additions & 0 deletions crates/host_env/src/io.rs
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,29 @@ pub fn is_seekable(fd: crt_fd::Borrowed<'_>) -> bool {
os::seek_fd(fd, 0, libc::SEEK_CUR).is_ok()
}

/// Whether a read from `fd` answers from data the file already holds, rather
/// than waiting for whoever writes the other end.
///
/// Seeking answers this everywhere but Windows, where a pipe seeks too --
/// `lseek` on one succeeds and reports a position, so a reader that took
/// seekability for an answer would wait on a peer while holding whatever it
/// holds for the length of the call.
#[cfg(not(windows))]
pub fn reads_without_waiting(fd: crt_fd::Borrowed<'_>) -> bool {
is_seekable(fd)
}

#[cfg(windows)]
pub fn reads_without_waiting(fd: crt_fd::Borrowed<'_>) -> bool {
use std::os::windows::io::AsRawHandle;
use windows_sys::Win32::Storage::FileSystem::{FILE_TYPE_DISK, GetFileType};

let Ok(handle) = crt_fd::as_handle(fd) else {
return false;
};
unsafe { GetFileType(handle.as_raw_handle() as _) == FILE_TYPE_DISK }
}

pub fn validate_whence(whence: i32) -> bool {
let standard = (0..=2).contains(&whence);
#[cfg(any(target_os = "dragonfly", target_os = "freebsd", target_os = "linux"))]
Expand Down
4 changes: 4 additions & 0 deletions crates/host_env/src/io_unsupported.rs
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,10 @@ pub fn is_seekable(_fd: crt_fd::Borrowed<'_>) -> bool {
false
}

pub fn reads_without_waiting(_fd: crt_fd::Borrowed<'_>) -> bool {
false
}

pub fn validate_whence(whence: i32) -> bool {
(0..=2).contains(&whence)
}
Expand Down
3 changes: 2 additions & 1 deletion crates/vm/src/stdlib/_io.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5861,9 +5861,10 @@ mod fileio {

let handle = zelf.get_fd(vm)?;

if zelf.seekable(vm)? {
if host_io::reads_without_waiting(handle) {
// The read answers from the file itself, so it returns without
// waiting on anyone; write where the caller asked directly.
// Seekability is not the question -- a pipe on Windows seeks.
let mut buf = obj.borrow_buf_mut();
return Self::read_once_into(zelf, handle, &mut buf, vm);
}
Expand Down
56 changes: 43 additions & 13 deletions extra_tests/snippets/stdlib_io_blocking_buffer.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,22 @@


def measure(buf, expected_len, writable):
"""Time the operations on `buf` that do not need the peer."""
start = time.monotonic()
assert len(buf) == expected_len, len(buf)
assert isinstance(bytes(buf), bytes)
"""Time each operation on `buf` that does not need the peer, separately, so
a failure names the one that waited rather than the group."""
elapsed = {}

def timed(name, operation):
start = time.monotonic()
value = operation()
elapsed[name] = time.monotonic() - start
return value

assert timed("len", lambda: len(buf)) == expected_len, len(buf)
assert isinstance(timed("bytes", lambda: bytes(buf)), bytes)
if writable:
buf[0] = buf[0]
gc.collect()
return time.monotonic() - start
timed("setitem", lambda: buf.__setitem__(0, buf[0]))
timed("gc.collect", gc.collect)
return elapsed


def run(buf, blocking_call, release_peer, writable):
Expand All @@ -51,9 +59,13 @@ def peer():
time.sleep(0.2) # the transfer is now waiting on its peer

elapsed = measure(buf, expected_len, writable)
assert elapsed < SLACK, "waited %.2fs on the peer" % elapsed
waited = ["%s %.2fs" % item for item in elapsed.items() if item[1] >= SLACK]
assert not waited, "waited on the peer: " + ", ".join(waited)

# The export is still held either way, so the buffer cannot be resized.
# The transfer is still in flight, so its export is still held and the
# buffer cannot be resized. An operating system that took the whole
# transfer without a peer leaves nothing here to observe.
assert not result, "the transfer finished without its peer"
try:
buf.append(0)
except BufferError:
Expand Down Expand Up @@ -124,12 +136,26 @@ def drain():
if hasattr(socket, "socketpair"):
left, right = socket.socketpair()
try:
left.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 4096)
source = bytearray(4 * 1024 * 1024)
# How much a connection holds before it makes the sender wait is the
# operating system's to decide, and asking for a small send buffer does
# not settle it -- a socketpair is already connected, and on Windows it
# is a loopback pair whose receiver has a window of its own. So fill it
# until it refuses rather than guess a size that outruns it.
left.setblocking(False)
filled = 0
while True:
try:
filled += left.send(bytes(1 << 16))
except (BlockingIOError, InterruptedError):
break
left.setblocking(True)

source = bytearray(1 << 16)
received = []

def receive():
while sum(received) < len(source):
wanted = filled + len(source)
while sum(received) < wanted:
chunk = right.recv(1 << 16)
if not chunk:
break
Expand All @@ -138,7 +164,11 @@ def receive():
reader = threading.Thread(target=receive, daemon=True)
run(source, left.sendall, reader.start, writable=True)
reader.join()
assert sum(received) == len(source), (sum(received), len(source))
assert sum(received) == filled + len(source), (
sum(received),
filled,
len(source),
)
finally:
left.close()
right.close()
Expand Down
Loading