Skip to content

Missing safety precondition on InOutBufReserved::from_raw #1499

Description

@Manishearth

Note

This finding was identified during an agentic unsafe Rust code review performed by Gemini AI, followed by human review and verification.

The Issue

In safe constructors such as InOutBufReserved::from_slices, this crate explicitly verifies and enforces that the output buffer must be at least as large as the input buffer (in_len <= out_len). This structural invariant is fundamental to the type and is required for safe methods like into_padded_blocks and get_out to partition buffers without executing out-of-bounds pointer arithmetic or constructing invalid slice references.

However, the public unchecked constructor InOutBufReserved::from_raw omits this requirement in its documented # Safety contract:

/// # Safety
/// Behavior is undefined if any of the following conditions are violated:
/// - `in_ptr` must point to a properly initialized value of type `T` and
/// must be valid for reads for `in_len * mem::size_of::<T>()` many bytes.
/// - `out_ptr` must point to a properly initialized value of type `T` and
/// must be valid for both reads and writes for `out_len * mem::size_of::<T>()`
/// many bytes.
/// - `in_ptr` and `out_ptr` must be either equal or non-overlapping.
/// - If `in_ptr` and `out_ptr` are equal, then the memory referenced by
/// them must not be accessed through any other pointer (not derived from
/// the return value) for the duration of lifetime 'a. Both read and write
/// accesses are forbidden.
/// - If `in_ptr` and `out_ptr` are not equal, then the memory referenced by
/// `out_ptr` must not be accessed through any other pointer (not derived from
/// the return value) for the duration of lifetime 'a. Both read and write
/// accesses are forbidden. The memory referenced by `in_ptr` must not be
/// mutated for the duration of lifetime `'a`, except inside an `UnsafeCell`.
/// - The total size `in_len * mem::size_of::<T>()` and
/// `out_len * mem::size_of::<T>()` must be no larger than `isize::MAX`.
#[inline(always)]
pub unsafe fn from_raw(
in_ptr: *const T,
in_len: usize,
out_ptr: *mut T,
out_len: usize,
) -> Self {

Minimal Reproduction (Miri)
// repro1.rs
use block_padding::NoPadding;
use generic_array::typenum::U16;
use inout::InOutBufReserved;

fn main() {
    let in_buf = [1u8; 64]; // 64 bytes input buffer
    let mut out_buf = [0u8; 16]; // 16 bytes output buffer (smaller than input)

    // SAFETY: All documented safety preconditions of `InOutBufReserved::from_raw` are satisfied:
    // - `in_ptr` points to valid initialized `u8`s valid for reads of 64 bytes.
    // - `out_ptr` points to valid initialized `u8`s valid for reads/writes of 16 bytes.
    // - `in_ptr` and `out_ptr` point to disjoint stack allocations.
    // - Total buffer sizes are well below `isize::MAX`.
    let reserved = unsafe {
        InOutBufReserved::from_raw(
            in_buf.as_ptr(),
            in_buf.len(),
            out_buf.as_mut_ptr(),
            out_buf.len(),
        )
    };

    // Subsequent safe public API calls trigger Undefined Behavior:
    let padded = reserved
        .into_padded_blocks::<NoPadding, U16>()
        .expect("in_len is a multiple of block size");

    // into_out() constructs a slice &[u8] of length 64 pointing to the 16-byte out_buf allocation:
    let _ub_slice = padded.into_out();
}
error: Undefined Behavior: pointer not dereferenceable: pointer must be dereferenceable for 64 bytes, but got alloc109 which is only 16 bytes from the end of the allocation
   --> /usr/local/google/home/manishearth/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/inout-0.1.4/src/reserved.rs:224:18
    |
224 |         unsafe { slice::from_raw_parts(out_ptr as *const u8, res_len) }
    |                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Undefined Behavior occurred here
    |
    = help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior
    = help: see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information
help: alloc109 was allocated here:
   --> src/bin/repro1.rs:7:9
    |
  7 |     let mut out_buf = [0u8; 16]; // 16 bytes output buffer (smaller tha...
    |         ^^^^^^^^^^^
    = note: stack backtrace:
            0: inout::PaddedInOutBuf::<'_, '_, generic_array::typenum::UInt<generic_array::typenum::UInt<generic_array::typenum::UInt<generic_array::typenum::UInt<generic_array::typenum::UInt<generic_array::typenum::UTerm, generic_array::typenum::B1>, generic_array::typenum::B0>, generic_array::typenum::B0>, generic_array::typenum::B0>, generic_array::typenum::B0>>::into_out
                at /usr/local/google/home/manishearth/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/inout-0.1.4/src/reserved.rs:224:18: 224:70
            1: main
                at src/bin/repro1.rs:29:21: 29:38

note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace

error: aborting due to 1 previous error
Suggested Fix

Add the required type invariant to the # Safety documentation of InOutBufReserved::from_raw:

  /// # Safety
  /// Behavior is undefined if any of the following conditions are violated:
+ /// - `in_len` must be less than or equal to `out_len` (`in_len <= out_len`).
  /// - `in_ptr` must point to a properly initialized value of type `T` and

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: inout (v0_1)

Overall Safety Assessment

inout (v0_1) is a no_std collection of custom pointer, reference, and buffer wrapper types (InOut, InOutBuf, InOutBufReserved, PaddedInOutBuf) designed for byte-processing and cryptographic block algorithms generic over in-place (&mut [T]) and buffer-to-buffer (&[T], &mut [T]) modes of operation.

The crate has a relatively high density of unsafe code given its compact codebase. Across 3 core source files (src/inout.rs, src/inout_buf.rs, and src/reserved.rs), there are 22 internal unsafe blocks and 3 public unsafe fn from_raw constructors. The unsafe operations predominantly involve raw pointer dereferencing (*const T, *mut T), manual slice/array pointer transformations (core::slice::from_raw_parts, slice::from_raw_parts_mut), raw memory bitwise copies (core::ptr::read, ptr::write, ptr::copy_nonoverlapping), and unchecked pointer arithmetic (ptr.add()).

Architecturally, the core pointer manipulation patterns inside the crate's safe public methods (such as .get(), .split_at(), .into_chunks(), and .xor_in2out()) are sound. They correctly uphold Rust's aliasing rules and Tree Borrows / Stacked Borrows semantics by ensuring that exclusive mutable borrows (&mut self) prevent concurrent access to referents, and iterators yield disjoint pointer referents.

However, the crate exhibits a poor safety documentation culture. None of the 17 internal unsafe blocks inside standard methods across src/inout.rs, src/inout_buf.rs, and src/reserved.rs contain // SAFETY: proof comments. Furthermore, applying rigorous proof-obligation auditing revealed a Critical contract flaw on the public constructor InOutBufReserved::from_raw (omitting the core type invariant in_len <= out_len), as well as copy-paste Fishy documentation defects across all raw constructors.

Critical Findings

1. Unsound # Safety Contract on InOutBufReserved::from_raw Allows Out-of-Bounds Buffer Access (UB) 🔴 🤦

  • Severity: 🔴 High
  • Threat Vector: 🤦 Accidental Misuse
  • Bug Type: Unsound Safety Contract

File: src/reserved.rs:64-77 (pub unsafe fn from_raw)

Description:
Under formal proof-obligation principles, the # Safety documentation of a public unsafe fn defines a contractual theorem: if a caller proves all documented safety preconditions, any subsequent sequence of operations using safe APIs on the resulting struct must be free of Undefined Behavior.

InOutBufReserved<'inp, 'out, T> maintains a fundamental internal type invariant documented in its struct comments (src/reserved.rs:15-16): "Length of the output slice is always equal or bigger than length of the input slice" (out_len >= in_len). The safe constructors (from_mut_slice and from_slices) strictly check and enforce this invariant.

However, the public unchecked constructor InOutBufReserved::from_raw fails to document in_len <= out_len in its # Safety contract:

    /// # Safety
    /// Behavior is undefined if any of the following conditions are violated:
    /// - `in_ptr` must point to a properly initialized value of type `T` and
    /// must be valid for reads for `in_len * mem::size_of::<T>()` many bytes.
    /// - `out_ptr` must point to a properly initialized value of type `T` and
    /// must be valid for both reads and writes for `out_len * mem::size_of::<T>()`
    /// many bytes.
    /// - `in_ptr` and `out_ptr` must be either equal or non-overlapping.
    /// [aliasing and isize::MAX conditions]
    pub unsafe fn from_raw(in_ptr: *const T, in_len: usize, out_ptr: *mut T, out_len: usize) -> Self

UB Scenario:
An unsafe caller adhering strictly to the documented # Safety contract can invoke InOutBufReserved::from_raw(in_ptr, 100, out_ptr, 16) where in_ptr is valid for 100 bytes and out_ptr is valid for 16 bytes.

If the caller subsequently invokes the safe method .into_padded_blocks::<P, U16>() (or .get_out()), the crate calculates blocks_len = self.in_len / 16 = 6 and calls InOutBuf::from_raw(..., blocks_len). This constructs an InOutBuf claiming 96 bytes (6 * 16) of the output buffer. Calling .into_out() on the resulting PaddedInOutBuf creates a slice &[u8] of length 96 pointing to the 16-byte out_ptr heap allocation. Constructing an out-of-bounds slice reference is immediate Undefined Behavior.

Recommended Fix:
Add the required type invariant to the # Safety docstring of InOutBufReserved::from_raw:

/// - `in_len` must be less than or equal to `out_len` (`in_len <= out_len`).

Fishy Findings

1. Copy-Paste Documentation Bugs in from_raw Safety Contracts Referring to Undefined Lifetime 'a 🟡 🤦

  • Severity: 🟡 Low
  • Threat Vector: 🤦 Accidental Misuse
  • Bug Type: Documentation Ambiguity

Files:

  • src/inout.rs:44-59 (InOut::from_raw)
  • src/inout_buf.rs:149-166 (InOutBuf::from_raw)
  • src/reserved.rs:44-62 (InOutBufReserved::from_raw)

Description:
The # Safety docstrings for all three from_raw constructors contain copy-pasted clauses stating: "for the duration of lifetime 'a. Both read and write accesses are forbidden". However, none of these functions declare or return a lifetime 'a; their struct lifetime parameters are 'inp and 'out. This creates ambiguity in caller proof obligations regarding the exact lifetime boundaries over which external aliasing restrictions and pointer liveness must hold.

Recommended Fix:
Update the docstrings to explicitly reference 'inp and 'out:

  • For input referent restrictions: "for the duration of lifetime 'inp".
  • For output referent restrictions: "for the duration of lifetime 'out".

2. Imprecise Preconditions Regarding Zero-Length Raw Pointers and Proper Alignment 🟡 🤦

  • Severity: 🟡 Low
  • Threat Vector: 🤦 Accidental Misuse
  • Bug Type: Imprecise Safety Precondition

Files: InOutBuf::from_raw (src/inout_buf.rs:168) and InOutBufReserved::from_raw (src/reserved.rs:64)

Description:
The safety documentation states that in_ptr and out_ptr "must point to a properly initialized value of type T and must be valid for reads/writes for len * mem::size_of::<T>() many bytes". When len == 0 (or in_len == 0, out_len == 0), 0 bytes are read or written. However, standard Rust slice semantics (core::slice::from_raw_parts) strictly require that slice data pointers must be non-null and properly aligned even for zero-length slices. Stating only "valid for N bytes" without explicitly requiring non-null and proper alignment for T is imprecise and could lead callers to pass dangling unaligned or null pointers when constructing empty buffers.

Recommended Fix:
Explicitly state in the # Safety sections: "Even if length is 0, in_ptr and out_ptr must be non-null and properly aligned for type T."

Missing Safety Comments

  • src/inout.rs:27: Missing // SAFETY: comment in InOut::get_in. 🔴

        // SAFETY: `self` is borrowed immutably for lifetime `'a`. For the duration of `'a`, `in_ptr` points to a properly initialized value of `T` valid for reads, as guaranteed by type invariants established upon construction.
        unsafe { &*self.in_ptr }
  • src/inout.rs:33: Missing // SAFETY: comment in InOut::get_out. 🔴

        // SAFETY: `self` is mutably borrowed for lifetime `'a`, ensuring exclusive access. For the duration of `'a`, `out_ptr` points to a properly initialized value of `T` valid for writes without aliasing other active references.
        unsafe { &mut *self.out_ptr }
  • src/inout.rs:74: Missing // SAFETY: comment in InOut::clone_in. 🔴

        // SAFETY: `self` is borrowed immutably (`&self`), preventing any concurrent mutable borrows of `out_ptr`. `in_ptr` points to a valid `T` valid for reads during `Clone::clone`.
        unsafe { (&*self.in_ptr).clone() }
  • src/inout.rs:109: Missing // SAFETY: comment in InOut::get for
    GenericArray. 🔴

        // SAFETY: `pos < N::USIZE` guarantees that `add(pos)` is within the bounds of the allocated `GenericArray<T, N>` (which is `#[repr(transparent)]` over `[T; N]`). The returned `InOut` is tied to the mutable borrow of `self` (`'a`), ensuring exclusive provenance over index `pos`.
        unsafe {
            InOut {
                in_ptr: (self.in_ptr as *const T).add(pos),
                out_ptr: (self.out_ptr as *mut T).add(pos),
                _pd: PhantomData,
            }
        }
  • src/inout.rs:139: Missing // SAFETY: comment in GenericArray<u8, N>::xor_in2out. 🔴

        // SAFETY: `self` is mutably borrowed, guaranteeing exclusive read/write access to the buffer. `in_ptr` points to a valid initialized `GenericArray<u8, N>` (which is `Copy`). `ptr::read` performs a bitwise copy, and `ptr::write` overwrites `out_ptr` (sharing exclusive raw pointer provenance) without aliasing violations.
        unsafe {
            let input = ptr::read(self.in_ptr);
  • src/inout.rs:163: Missing // SAFETY: comment in 2D
    GenericArray::xor_in2out. 🔴

        // SAFETY: `self` is exclusively borrowed (`&mut self`). `in_ptr` and `out_ptr` point to valid initialized 2D arrays of `u8` (which have no `Drop` destructors). Raw read followed by raw write to `out_ptr` is sound under exclusive raw pointer provenance.
        unsafe {
            let input = ptr::read(self.in_ptr);
  • src/inout_buf.rs:103: Missing // SAFETY: comment in InOutBuf::get. 🔴

        // SAFETY: `pos < self.len` ensures `add(pos)` remains within allocated slice bounds. Exclusive mutable borrow of `self` (`'a`) guarantees exclusive read/write access to element `pos`.
        unsafe {
            InOut {
  • src/inout_buf.rs:115: Missing // SAFETY: comment in InOutBuf::get_in.
    🔴

        // SAFETY: `in_ptr` is non-null, properly aligned for `T`, and valid for reads of `self.len` elements for lifetime `'a` (tied to `&'a self`).
        unsafe { slice::from_raw_parts(self.in_ptr, self.len) }
  • src/inout_buf.rs:121: Missing // SAFETY: comment in InOutBuf::get_out.
    🔴

        // SAFETY: `out_ptr` is non-null, properly aligned, and valid for read/write access of `self.len` elements. `&'a mut self` ensures exclusive access for lifetime `'a` without aliasing.
        unsafe { slice::from_raw_parts_mut(self.out_ptr, self.len) }
  • src/inout_buf.rs:127: Missing // SAFETY: comment in
    InOutBuf::into_out. 🔴

        // SAFETY: Consuming `self` transfers exclusive output provenance (`'out`) to the returned mutable slice `&'out mut [T]`. `out_ptr` is non-null, aligned, and valid for `self.len` elements.
        unsafe { slice::from_raw_parts_mut(self.out_ptr, self.len) }
  • src/inout_buf.rs:193: Missing // SAFETY: comment in
    InOutBuf::split_at. 🔴

        // SAFETY: `mid <= self.len` guarantees `add(mid)` is in bounds (at most 1 byte past allocation). The resulting two buffers partition `[0..mid)` and `[mid..len)` into disjoint regions.
        let (tail_in_ptr, tail_out_ptr) = unsafe { (self.in_ptr.add(mid), self.out_ptr.add(mid)) };
  • src/inout_buf.rs:221: Missing // SAFETY: comment in
    InOutBuf::into_chunks. 🔴

        // SAFETY: `GenericArray<T, N>` is `#[repr(transparent)]` over `[T; N]`, sharing exact element alignment with `T`. `chunks * N::USIZE + tail_len == self.len()`, ensuring both raw pointer casts and pointer arithmetic (`add(tail_pos)`) partition the buffer into valid disjoint regions.
        unsafe {
            let chunks = InOutBuf {
  • src/inout_buf.rs:249: Missing // SAFETY: comment in
    InOutBuf<u8>::xor_in2out. 🔴

        // SAFETY: `assert_eq!(self.len(), data.len())` ensures loop indices `i` remain in bounds for `in_ptr` and `out_ptr`. Exclusive borrow `&mut self` ensures raw read (`*in_ptr`) and raw write (`*out_ptr`) do not violate aliasing rules.
        unsafe {
            for i in 0..data.len() {
  • src/inout_buf.rs:293: Missing // SAFETY: comment in
    InOutBufIter::next. 🔴

        // SAFETY: `self.pos < self.buf.len()` ensures `add(self.pos)` is in bounds. Since `self.pos` strictly increments on each call, each yielded `InOut` points to a disjoint element index, ensuring non-overlapping mutable referents across yielded items.
        let res = unsafe {
            InOut {
  • src/reserved.rs:119: Missing // SAFETY: comment in
    InOutBufReserved::get_in. 🔴

        // SAFETY: `in_ptr` is non-null, properly aligned, and valid for reads of `self.in_len` elements for lifetime `'a`.
        unsafe { slice::from_raw_parts(self.in_ptr, self.in_len) }
  • src/reserved.rs:125: Missing // SAFETY: comment in
    InOutBufReserved::get_out. 🔴

        // SAFETY: `out_ptr` is non-null, properly aligned, and valid for read/write access of `self.out_len` elements. `&'a mut self` prevents conflicting accesses.
        unsafe { slice::from_raw_parts_mut(self.out_ptr, self.out_len) }
  • src/reserved.rs:142: Missing // SAFETY: comment in
    InOutBufReserved::into_padded_blocks. 🔴

        // SAFETY: `GenericArray<u8, BS>` has alignment 1. `blocks_len * BS <= self.in_len <= self.out_len` guarantees `in_ptr` and `out_ptr` are valid for `blocks_len` array blocks.
        let blocks = unsafe {
            InOutBuf::from_raw(

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions