Hi, and thanks for the crate.
While auditing container/IO crates with Miri I ran into a soundness issue in
RevBufReader. Reporting it here since the repo has no security policy; happy
to move it somewhere private if you'd prefer.
Issue
RevBufReader::with_capacity reserves the buffer and calls set_len on it
without initialising the memory:
// src/lib.rs:145
let mut buffer = Vec::with_capacity(capacity);
buffer.set_len(capacity);
That buffer is later handed to the wrapped reader:
// src/lib.rs:401
self.inner.read_exact(&mut self.buf[..length])?;
So any user-supplied Read implementation receives a &mut [u8] pointing at
uninitialised memory. std::io::Read::read documents that passing an
uninitialised buffer is not safe, and only recommends that implementations
avoid reading buf rather than forbidding it — so a reader that inspects its
buffer is within contract, and this is UB.
No unsafe is needed on the caller's side to reach it.
Reproducer
use std::io::{Read, Seek, SeekFrom};
use rev_buf_reader::RevBufReader;
struct PeekingReader { data: Vec<u8>, pos: usize }
impl Read for PeekingReader {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
let mut acc = 0u64;
for b in buf.iter() { acc = acc.wrapping_add(*b as u64); }
std::hint::black_box(acc);
let n = buf.len().min(self.data.len() - self.pos);
buf[..n].copy_from_slice(&self.data[self.pos..self.pos + n]);
self.pos += n;
Ok(n)
}
}
impl Seek for PeekingReader {
fn seek(&mut self, from: SeekFrom) -> std::io::Result<u64> {
let len = self.data.len() as i64;
let target = match from {
SeekFrom::Start(n) => n as i64,
SeekFrom::End(n) => len + n,
SeekFrom::Current(n) => self.pos as i64 + n,
};
if target < 0 {
return Err(std::io::Error::new(std::io::ErrorKind::InvalidInput, "negative seek"));
}
self.pos = (target as usize).min(self.data.len());
Ok(self.pos as u64)
}
}
#[test]
fn uninit() {
let inner = PeekingReader { data: (0u8..64).collect(), pos: 0 };
let mut r = RevBufReader::with_capacity(4096, inner);
let mut out = Vec::new();
let _ = r.read_to_end(&mut out);
}
cargo +nightly miri test:
error: Undefined Behavior: reading memory at alloc53897[0x0..0x1],
but memory is uninitialized at [0x0..0x1],
and this operation requires initialized memory
= note: stack backtrace:
0: <PeekingReader as std::io::Read>::read
3: <RevBufReader<PeekingReader> as std::io::BufRead>::fill_buf
at rev_buf_reader-0.3.0/src/lib.rs:401:13
Reproduces under both Stacked Borrows and Tree Borrows.
Note on the read_initializer feature
The initialisation path in with_capacity is gated behind
feature = "read_initializer", which needs the #![feature(read_initializer)]
nightly gate. That language feature has since been removed from the compiler,
so the guard can no longer be enabled on any current toolchain — every user on
stable takes the uninitialised path.
Suggested fix
The minimal correct change is to zero the buffer:
let buffer = vec![0u8; capacity];
std::io::BufReader shipped exactly this for years before moving to
BorrowedBuf/read_buf. If you'd like, I'm glad to open a PR.
I'd also like to file a RustSec advisory for this once you've had a chance to
confirm — let me know if you'd prefer to coordinate on timing.
Hi, and thanks for the crate.
While auditing container/IO crates with Miri I ran into a soundness issue in
RevBufReader. Reporting it here since the repo has no security policy; happyto move it somewhere private if you'd prefer.
Issue
RevBufReader::with_capacityreserves the buffer and callsset_lenon itwithout initialising the memory:
That buffer is later handed to the wrapped reader:
So any user-supplied
Readimplementation receives a&mut [u8]pointing atuninitialised memory.
std::io::Read::readdocuments that passing anuninitialised buffer is not safe, and only recommends that implementations
avoid reading
bufrather than forbidding it — so a reader that inspects itsbuffer is within contract, and this is UB.
No
unsafeis needed on the caller's side to reach it.Reproducer
cargo +nightly miri test:Reproduces under both Stacked Borrows and Tree Borrows.
Note on the
read_initializerfeatureThe initialisation path in
with_capacityis gated behindfeature = "read_initializer", which needs the#![feature(read_initializer)]nightly gate. That language feature has since been removed from the compiler,
so the guard can no longer be enabled on any current toolchain — every user on
stable takes the uninitialised path.
Suggested fix
The minimal correct change is to zero the buffer:
std::io::BufReadershipped exactly this for years before moving toBorrowedBuf/read_buf. If you'd like, I'm glad to open a PR.I'd also like to file a RustSec advisory for this once you've had a chance to
confirm — let me know if you'd prefer to coordinate on timing.