You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This finding was identified during an agentic unsafe Rust code review performed by Gemini AI, followed by human review and verification.
The Issue
ReadReady::num_ready_bytes for std::fs::File constructs an owned std::fs::File wrapper directly from self's raw OS file descriptor or handle via from_raw_filelike to determine remaining bytes without modifying current stream position:
According to RFC 3128, std::fs::File assumes exclusive ownership over the lifecycle of its underlying OS file descriptor or handle.
The implementation invokes tmp.seek(...) on line 76 and only afterwards calls std::mem::forget(tmp) on line 77. If tmp.seek (or any thread profiling or hooking mechanism) panics or unwinds before line 77 is reached, tmp will be dropped during stack unwinding. Its destructor (File::drop) will close the underlying OS file descriptor or handle while self (and any cloned File instances or borrowed handles across concurrent threads) remains alive and believes it owns the open descriptor.
This is probably minor, but worth fixing.
Suggested Fix
Wrap the owned file wrapper immediately in std::mem::ManuallyDrop upon creation, ensuring its destructor can never execute regardless of unwinding:
- let mut tmp = unsafe { std::fs::File::from_raw_filelike(self.as_raw_filelike()) };+ let mut tmp = std::mem::ManuallyDrop::new(unsafe { std::fs::File::from_raw_filelike(self.as_raw_filelike()) });
let current = tmp.seek(SeekFrom::Current(0));
- std::mem::forget(tmp);
return Ok(metadata.len() - current?);
Note
The full audit report below also contains additional minor findings (such as missing safety comments or undocumented FFI assumptions) that are probably worth fixing as well but not the primary goal of this issue. The audit report has not been human-reviewed, it may contain misleading claims.
Full Gemini Codebase Audit Report Appendix
Unsafe Rust Review: system_interface (v0_27)
Overall Safety Assessment
system_interface (v0_27) provides extension traits (FileIoExt, IoExt, ReadReady, IsReadWrite) aimed at extending standard library I/O types (std::fs::File, std::net::TcpStream, Stdin, etc.) with POSIX/Windows-specific capabilities like vectored reads at offsets, immediate read readiness queries, and peek operations.
The crate contains a moderate density of unsafe code distributed across Windows FFI calls (PeekNamedPipe, ioctlsocket, send, recv), OS handle conversions (from_raw_filelike), and raw pointer slicing polyfills (advance and advance_mut).
From a safety documentation perspective, the crate exhibits limited safety documentation: unsafe blocks generally lack formal // SAFETY: proof comments, and private unsafe fn helpers omit # Safety docstrings detailing their contracts. More significantly, auditing revealed two Critical findings: (1) an I/O Safety violation where unwinding during File::seek causes a double-close vulnerability on borrowed file descriptors, and (2) an intentional bad pointer dereference (usize::MAX) passed to Windows recv() to test socket shutdown state.
Critical Findings
1. Violation of I/O Safety (RFC 3128) via Unwound Double-Close in ReadReady::num_ready_bytes 🔴 🧪
Description: To determine the remaining bytes in a regular file without modifying its current stream position, the implementation constructs an owned std::fs::File wrapper (tmp) directly from self's raw OS file descriptor/handle via from_raw_filelike. It subsequently invokes tmp.seek(...) on line 76 and only afterwards calls std::mem::forget(tmp) on line 77. Under Rust's I/O safety conventions (RFC 3128), std::fs::File assumes exclusive ownership over the lifecycle of its underlying file descriptor/handle. If tmp.seek (or any thread interruption/hooking mechanism) panics or unwinds before line 77 is reached, tmp will be dropped during stack unwinding. Its destructor (File::drop) will close the underlying OS file descriptor/handle while self (and any cloned File instances or borrowed handles across concurrent threads) remains alive and believes it owns the open descriptor. This leads to use-after-close or file descriptor misdelivery vulnerabilities when the OS reassigns the closed descriptor number to a subsequent open file in another thread.
Remediation: Wrap the owned file wrapper immediately in std::mem::ManuallyDrop upon creation, ensuring its destructor can never execute regardless of unwinding:
letmut tmp = std::mem::ManuallyDrop::new(unsafe{ std::fs::File::from_raw_filelike(self.as_raw_filelike())});let current = tmp.seek(SeekFrom::Current(0));returnOk(metadata.len() - current?);
2. ASan-Fatal Bad Pointer Dereference and Brittle Exception Trap Reliance in raw_socket_is_read_write 🔴 ⚠️
Priority: 🔴 High
Threat Vector: ⚠️ Accidental Misuse
Bug Type: Invalid Pointer Dereference
Location:src/io/is_read_write.rs:144
Code:
// Detect read shutdown. A normal zero-length `recv` does block, so// use deliberately invalid pointer, as we get different error codes in// the case of a shut-down stream.let read_result = unsafe{recv(socket, usize::MAXas*mut_,1,MSG_PEEK)};
Description: To distinguish between an open blocking socket with no pending network packets and a socket shut down for reading (WSAESHUTDOWN), the implementation passes an unallocated, unaligned bogus address (usize::MAX as *mut _ / 0xFFFFFFFFFFFFFFFF) with length 1 to the Windows recv() Winsock system call. The author included a helpful code comment explaining the intent: a normal zero-length recv would block, so an invalid pointer is intentionally passed to leverage error code differences (WSAEFAULT, 10014) on shut-down streams. However, relying on the OS/kernel to safely catch invalid pointer access violations presents serious issues:
Dynamic Analysis & Sanitizer Incompatibility: Under AddressSanitizer (ASan), Valgrind, Dr. Memory, or API-hooking endpoint detection and response (EDR) agents, passing 0xFFFFFFFFFFFFFFFF with len = 1 to recv() is immediately trapped as an illegal memory access or heap buffer overflow attempt, crashing the process with a fatal sanitizer abort.
Undefined Behavior in Foreign Function Contracts: In C / POSIX / Winsock specifications, passing an invalid pointer to recv when length > 0 is undefined behavior under foreign call contracts. Assuming foreign syscall wrappers will safely convert bad pointers into EFAULT / WSAEFAULT rather than exhibiting undefined behavior or corrupting state violates safety preconditions.
Indefinite Thread Hanging: If layered service providers (LSPs) or network drivers block waiting for network packets before validating user-mode buffer pointers, calling is_read_write() on any idle open socket will hang the calling thread indefinitely.
Remediation: Allocate a valid 1-byte stack buffer (let mut buf = [0u8; 1];) and pass buf.as_mut_ptr() with MSG_PEEK. Alternatively, use non-blocking socket queries (ioctlsocket with FIONREAD or WSAPoll / select).
Fishy Findings
1. Undocumented Preconditions on unsafe fn _reopen Helpers and Unsound Safe Abstraction 🟠 ⚠️
Description: The functions _reopen, _reopen_write, and _reopen_append are marked unsafe fn because they delegate to cap_fs_ext::Reopen::reopen, which is an unsafe fn on Windows. Reopening a file handle by object ID or path on Windows requires strict assumptions regarding filesystem capabilities (e.g., OpenFileById support) and absence of concurrent handle redirection or sharing mode conflicts. However, none of these unsafe fn declarations possess # Safety docstrings defining their safety obligations. Furthermore, the private helper fn reopen wraps _reopen in a completely safe function accepting any Filelike: AsFilelike. Calling reopen from safe trait methods (read_at, read_exact_at, read_vectored_at) without documenting or proving why cap_fs_ext's safety obligations are unconditionally satisfied for all std::fs::File objects leaves an unverified gap in the crate's safety architecture.
Location:src/fs/file_io_ext.rs:362, 394 and src/io/io_ext.rs:168, 200
Description: The polyfill functions advance and advance_mut—which perform raw pointer arithmetic (ptr.add) and lifetime reconstruction (slice::from_raw_parts) on IoSlice and IoSliceMut—are duplicated line-for-line across the filesystem (fs) and I/O (io) modules. Maintaining duplicate copies of non-trivial unsafe pointer manipulation increases auditing burden and creates risk of divergence if soundness patches are applied to only one module.
Missing Safety Comments
The codebase contains 18 locations where unsafe operations lack formal safety comments or docstrings. Below are the exact file:line locations along with rigorous proposed proof obligations:
// SAFETY: `first` is a valid `IoSlice` whose length satisfies `accumulated_len + first.len() > n`, ensuring `advance_by < first.len()`. `ptr` points to the contiguous slice memory, so `ptr.add(advance_by)` remains within the same allocated object. `len` is `first.len() - advance_by > 0`. `slice::from_raw_parts` preserves the valid lifetime `'a` and alignment (1 byte for `u8`) of the original slice.
// SAFETY: `first` is uniquely borrowed (`&mut`) and `advance_by < first.len()`. `ptr.add(advance_by)` remains within the bounds of the allocated buffer. `slice::from_raw_parts_mut` creates a non-aliasing mutable subslice of length `len = first.len() - advance_by` preserving lifetime `'a`.
src/fs/file_io_ext.rs:1007 🔴
Context:unsafe { _reopen(&file) } inside reopen.
Proposed Proof Comment:
// SAFETY: `file` is a valid open `std::fs::File` handle. Reopening it via `_reopen` is safe because `file` represents a regular filesystem object supporting reopening by object ID.
/// # Safety/// `file` must be a valid open file handle pointing to a filesystem object that supports reopening by object ID or path without causing undefined handle redirection behavior.
/// # Safety/// `file` must be a valid open file handle pointing to a filesystem object that supports reopening for write access without violating OS sharing constraints.
// SAFETY: `first` is a valid `IoSlice` and `advance_by < first.len()`. `ptr.add(advance_by)` remains within the allocated memory object. `slice::from_raw_parts` reconstructs a valid subslice for lifetime `'a`.
```rust
// SAFETY: `first` is uniquely borrowed and `advance_by < first.len()`. `ptr.add(advance_by)` remains within the allocated buffer. `slice::from_raw_parts_mut` creates a non-aliasing mutable subslice for lifetime `'a`.
```
src/io/io_ext.rs:313 🔴
Context:unsafe { PeekNamedPipe(...) } inside IoExt::peek for std::fs::File on Windows.
Proposed Proof Comment:
```rust
// SAFETY: `self.as_raw_handle()` is a valid OS handle. `buf.as_mut_ptr()` points to a buffer of at least `buf.len()` bytes, and `len` is clamped to `min(buf.len(), u32::MAX)`. `bytes_read.as_mut_ptr()` points to a valid allocated `MaybeUninit<u32>` on the stack. All null pointers passed for optional parameters are accepted by `PeekNamedPipe`.
```
```rust
// SAFETY: `res != 0` indicates that `PeekNamedPipe` succeeded, which guarantees that the number of bytes read was written into `bytes_read`.
```
src/io/read_ready.rs:75 🔴
Context:unsafe { std::fs::File::from_raw_filelike(...) } inside ReadReady::num_ready_bytes for std::fs::File.
Proposed Proof Comment:
```rust
// SAFETY: `self.as_raw_filelike()` is a valid open file descriptor/handle. (Note: To prevent I/O safety violations on unwinding before `mem::forget`, this instance must be wrapped in `ManuallyDrop`).
```
src/io/read_ready.rs:240 🔴
Context:unsafe { ioctlsocket(...) } inside ReadReady::num_ready_bytes for TcpStream on Windows.
Proposed Proof Comment:
```rust
// SAFETY: `self.as_raw_socket()` is a valid Winsock `SOCKET`. `arg.as_mut_ptr()` points to a valid allocated `MaybeUninit<c_ulong>` on the stack. `FIONREAD` is a valid ioctl command that writes the readable byte count into `arg`.
```
src/io/read_ready.rs:241 🔴
Context:unsafe { arg.assume_init() } inside ReadReady::num_ready_bytes for TcpStream on Windows.
Proposed Proof Comment:
```rust
// SAFETY: `ioctlsocket` returning 0 indicates success, guaranteeing that `arg` was initialized with the readable byte count.
```
```rust
// SAFETY: `socket` is passed by value. Passing a null buffer pointer with length 0 to `send` is permitted by Winsock to probe socket write status without sending data.
```
```rust
// SAFETY: (Note: Unsound as written due to `usize::MAX` pointer). When passing a valid 1-byte buffer pointer `buf.as_mut_ptr()`, calling `recv` with `MSG_PEEK` safely probes socket read availability without consuming data.
```
tests/sys_common/io.rs:9 🟡
Context:unsafe { tempdir(...) } inside tmpdir.
Proposed Proof Comment:
```rust
// SAFETY: Called within test execution context where ambient filesystem authority is available and permitted.
```
Note
This finding was identified during an agentic unsafe Rust code review performed by Gemini AI, followed by human review and verification.
The Issue
ReadReady::num_ready_bytesforstd::fs::Fileconstructs an ownedstd::fs::Filewrapper directly fromself's raw OS file descriptor or handle viafrom_raw_fileliketo determine remaining bytes without modifying current stream position:system-interface/src/io/read_ready.rs
Lines 75 to 78 in 8ee8c82
According to RFC 3128,
std::fs::Fileassumes exclusive ownership over the lifecycle of its underlying OS file descriptor or handle.The implementation invokes
tmp.seek(...)on line 76 and only afterwards callsstd::mem::forget(tmp)on line 77. Iftmp.seek(or any thread profiling or hooking mechanism) panics or unwinds before line 77 is reached,tmpwill be dropped during stack unwinding. Its destructor (File::drop) will close the underlying OS file descriptor or handle whileself(and any clonedFileinstances or borrowed handles across concurrent threads) remains alive and believes it owns the open descriptor.This is probably minor, but worth fixing.
Suggested Fix
Wrap the owned file wrapper immediately in
std::mem::ManuallyDropupon creation, ensuring its destructor can never execute regardless of unwinding:Note
The full audit report below also contains additional minor findings (such as missing safety comments or undocumented FFI assumptions) that are probably worth fixing as well but not the primary goal of this issue. The audit report has not been human-reviewed, it may contain misleading claims.
Full Gemini Codebase Audit Report Appendix
Unsafe Rust Review:
system_interface(v0_27)Overall Safety Assessment
system_interface(v0_27) provides extension traits (FileIoExt,IoExt,ReadReady,IsReadWrite) aimed at extending standard library I/O types (std::fs::File,std::net::TcpStream,Stdin, etc.) with POSIX/Windows-specific capabilities like vectored reads at offsets, immediate read readiness queries, and peek operations.The crate contains a moderate density of
unsafecode distributed across Windows FFI calls (PeekNamedPipe,ioctlsocket,send,recv), OS handle conversions (from_raw_filelike), and raw pointer slicing polyfills (advanceandadvance_mut).From a safety documentation perspective, the crate exhibits limited safety documentation:
unsafeblocks generally lack formal// SAFETY:proof comments, and privateunsafe fnhelpers omit# Safetydocstrings detailing their contracts. More significantly, auditing revealed two Critical findings: (1) an I/O Safety violation where unwinding duringFile::seekcauses a double-close vulnerability on borrowed file descriptors, and (2) an intentional bad pointer dereference (usize::MAX) passed to Windowsrecv()to test socket shutdown state.Critical Findings
1. Violation of I/O Safety (RFC 3128) via Unwound Double-Close in
ReadReady::num_ready_bytes🔴 🧪Priority: 🔴 High
Threat Vector: 🧪 Contrived Setup
Bug Type: I/O Safety Violation
Location:
src/io/read_ready.rs:75-77Code:
Description: To determine the remaining bytes in a regular file without modifying its current stream position, the implementation constructs an owned
std::fs::Filewrapper (tmp) directly fromself's raw OS file descriptor/handle viafrom_raw_filelike. It subsequently invokestmp.seek(...)on line 76 and only afterwards callsstd::mem::forget(tmp)on line 77. Under Rust's I/O safety conventions (RFC 3128),std::fs::Fileassumes exclusive ownership over the lifecycle of its underlying file descriptor/handle. Iftmp.seek(or any thread interruption/hooking mechanism) panics or unwinds before line 77 is reached,tmpwill be dropped during stack unwinding. Its destructor (File::drop) will close the underlying OS file descriptor/handle whileself(and any clonedFileinstances or borrowed handles across concurrent threads) remains alive and believes it owns the open descriptor. This leads to use-after-close or file descriptor misdelivery vulnerabilities when the OS reassigns the closed descriptor number to a subsequent open file in another thread.Remediation: Wrap the owned file wrapper immediately in
std::mem::ManuallyDropupon creation, ensuring its destructor can never execute regardless of unwinding:2. ASan-Fatal Bad Pointer Dereference and Brittle Exception Trap Reliance in⚠️
raw_socket_is_read_write🔴Priority: 🔴 High
Threat Vector:⚠️ Accidental Misuse
Bug Type: Invalid Pointer Dereference
Location:
src/io/is_read_write.rs:144Code:
Description: To distinguish between an open blocking socket with no pending network packets and a socket shut down for reading (
WSAESHUTDOWN), the implementation passes an unallocated, unaligned bogus address (usize::MAX as *mut _/0xFFFFFFFFFFFFFFFF) with length1to the Windowsrecv()Winsock system call. The author included a helpful code comment explaining the intent: a normal zero-lengthrecvwould block, so an invalid pointer is intentionally passed to leverage error code differences (WSAEFAULT, 10014) on shut-down streams. However, relying on the OS/kernel to safely catch invalid pointer access violations presents serious issues:0xFFFFFFFFFFFFFFFFwithlen = 1torecv()is immediately trapped as an illegal memory access or heap buffer overflow attempt, crashing the process with a fatal sanitizer abort.recvwhen length > 0 is undefined behavior under foreign call contracts. Assuming foreign syscall wrappers will safely convert bad pointers intoEFAULT/WSAEFAULTrather than exhibiting undefined behavior or corrupting state violates safety preconditions.is_read_write()on any idle open socket will hang the calling thread indefinitely.let mut buf = [0u8; 1];) and passbuf.as_mut_ptr()withMSG_PEEK. Alternatively, use non-blocking socket queries (ioctlsocketwithFIONREADorWSAPoll/select).Fishy Findings
1. Undocumented Preconditions on⚠️
unsafe fn _reopenHelpers and Unsound Safe Abstraction 🟠Priority: 🟠 Medium
Threat Vector:⚠️ Accidental Misuse
Bug Type: Unsound Safe Abstraction
Location:
src/fs/file_io_ext.rs:1005-1037Code:
Description: The functions
_reopen,_reopen_write, and_reopen_appendare markedunsafe fnbecause they delegate tocap_fs_ext::Reopen::reopen, which is anunsafe fnon Windows. Reopening a file handle by object ID or path on Windows requires strict assumptions regarding filesystem capabilities (e.g.,OpenFileByIdsupport) and absence of concurrent handle redirection or sharing mode conflicts. However, none of theseunsafe fndeclarations possess# Safetydocstrings defining their safety obligations. Furthermore, the private helperfn reopenwraps_reopenin a completely safe function accepting anyFilelike: AsFilelike. Callingreopenfrom safe trait methods (read_at,read_exact_at,read_vectored_at) without documenting or proving whycap_fs_ext's safety obligations are unconditionally satisfied for allstd::fs::Fileobjects leaves an unverified gap in the crate's safety architecture.2. Code Duplication of Subtle Unsafe Pointer Slicing Polyfills 🟡⚠️
Priority: 🟡 Low
Threat Vector:⚠️ Accidental Misuse
Bug Type: Code Duplication
Location:
src/fs/file_io_ext.rs:362, 394andsrc/io/io_ext.rs:168, 200Description: The polyfill functions
advanceandadvance_mut—which perform raw pointer arithmetic (ptr.add) and lifetime reconstruction (slice::from_raw_parts) onIoSliceandIoSliceMut—are duplicated line-for-line across the filesystem (fs) and I/O (io) modules. Maintaining duplicate copies of non-trivialunsafepointer manipulation increases auditing burden and creates risk of divergence if soundness patches are applied to only one module.Missing Safety Comments
The codebase contains 18 locations where
unsafeoperations lack formal safety comments or docstrings. Below are the exact file:line locations along with rigorous proposed proof obligations:src/fs/file_io_ext.rs:362🔴Context:
unsafe { ptr = ptr.add(advance_by); ... }insideadvance.Proposed Proof Comment:
// SAFETY: `first` is a valid `IoSlice` whose length satisfies `accumulated_len + first.len() > n`, ensuring `advance_by < first.len()`. `ptr` points to the contiguous slice memory, so `ptr.add(advance_by)` remains within the same allocated object. `len` is `first.len() - advance_by > 0`. `slice::from_raw_parts` preserves the valid lifetime `'a` and alignment (1 byte for `u8`) of the original slice.src/fs/file_io_ext.rs:394🔴Context:
unsafe { ptr = ptr.add(advance_by); ... }insideadvance_mut.Proposed Proof Comment:
// SAFETY: `first` is uniquely borrowed (`&mut`) and `advance_by < first.len()`. `ptr.add(advance_by)` remains within the bounds of the allocated buffer. `slice::from_raw_parts_mut` creates a non-aliasing mutable subslice of length `len = first.len() - advance_by` preserving lifetime `'a`.src/fs/file_io_ext.rs:1007🔴Context:
unsafe { _reopen(&file) }insidereopen.Proposed Proof Comment:
// SAFETY: `file` is a valid open `std::fs::File` handle. Reopening it via `_reopen` is safe because `file` represents a regular filesystem object supporting reopening by object ID.src/fs/file_io_ext.rs:1011🔴Context:
unsafe fn _reopen(file: &fs::File) -> io::Result<fs::File>Proposed Docstring:
src/fs/file_io_ext.rs:1019🔴Context:
unsafe { _reopen_write(&file) }insidereopen_write.Proposed Proof Comment:
// SAFETY: `file` is a valid open `std::fs::File` handle supporting reopening for write access.src/fs/file_io_ext.rs:1023🔴Context:
unsafe fn _reopen_write(file: &fs::File) -> io::Result<fs::File>Proposed Docstring:
src/fs/file_io_ext.rs:1031🔴Context:
unsafe { _reopen_append(&file) }insidereopen_append.Proposed Proof Comment:
// SAFETY: `file` is a valid open `std::fs::File` handle supporting reopening for append access.src/fs/file_io_ext.rs:1035🔴Context:
unsafe fn _reopen_append(file: &fs::File) -> io::Result<fs::File>Proposed Docstring:
src/io/io_ext.rs:168🔴Context:
unsafe { ptr = ptr.add(advance_by); ... }insideadvance.Proposed Proof Comment:
// SAFETY: `first` is a valid `IoSlice` and `advance_by < first.len()`. `ptr.add(advance_by)` remains within the allocated memory object. `slice::from_raw_parts` reconstructs a valid subslice for lifetime `'a`.src/io/io_ext.rs:200🔴Context:
unsafe { ptr = ptr.add(advance_by); ... }insideadvance_mut.Proposed Proof Comment:
src/io/io_ext.rs:313🔴Context:
unsafe { PeekNamedPipe(...) }insideIoExt::peekforstd::fs::Fileon Windows.Proposed Proof Comment:
src/io/io_ext.rs:326🔴Context:
unsafe { bytes_read.assume_init() }insideIoExt::peek.Proposed Proof Comment:
src/io/read_ready.rs:75🔴Context:
unsafe { std::fs::File::from_raw_filelike(...) }insideReadReady::num_ready_bytesforstd::fs::File.Proposed Proof Comment:
src/io/read_ready.rs:240🔴Context:
unsafe { ioctlsocket(...) }insideReadReady::num_ready_bytesforTcpStreamon Windows.Proposed Proof Comment:
src/io/read_ready.rs:241🔴Context:
unsafe { arg.assume_init() }insideReadReady::num_ready_bytesforTcpStreamon Windows.Proposed Proof Comment:
src/io/is_read_write.rs:131🟡Context:
unsafe { send(...) }insideraw_socket_is_read_write.Proposed Proof Comment:
src/io/is_read_write.rs:144🟡Context:
unsafe { recv(...) }insideraw_socket_is_read_write.Proposed Proof Comment:
tests/sys_common/io.rs:9🟡Context:
unsafe { tempdir(...) }insidetmpdir.Proposed Proof Comment: