Skip to content

Unsoundness: uninitialised buffer is passed to the wrapped Read implementation #9

Description

@icedracon

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions