Skip to content

Soundness: Out-of-bounds pointer dereference in array_ref! and array_mut_ref! with malicious Index impl #32

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

The array_ref! and array_mut_ref! macros perform an unsafe pointer cast from a sliced result to a fixed-size array reference &[T; $len] without verifying that the runtime slice length actually matches $len.

The macros obtain a slice via &$arr[offset..offset + $len] and directly dereference slice.as_ptr() as *const [_; $len] inside as_array:

arrayref/src/lib.rs

Lines 57 to 72 in f8d0299

macro_rules! array_ref {
($arr:expr, $offset:expr, $len:expr) => {{
{
#[inline]
const unsafe fn as_array<T>(slice: &[T]) -> &[T; $len] {
&*(slice.as_ptr() as *const [_; $len])
}
let offset = $offset;
let slice = &$arr[offset..offset + $len];
#[allow(unused_unsafe)]
unsafe {
as_array(slice)
}
}
}};
}

arrayref/src/lib.rs

Lines 283 to 298 in f8d0299

macro_rules! array_mut_ref {
($arr:expr, $offset:expr, $len:expr) => {{
{
#[inline]
unsafe fn as_array<T>(slice: &mut [T]) -> &mut [T; $len] {
&mut *(slice.as_mut_ptr() as *mut [_; $len])
}
let offset = $offset;
let slice = &mut $arr[offset..offset + $len];
#[allow(unused_unsafe)]
unsafe {
as_array(slice)
}
}
}};
}

While slicing a standard slice [T] guarantees the length, the macros accept any type implementing the safe Index<Range<usize>, Output = [T]> (or IndexMut) trait.

A custom Index implementation can safely return a slice shorter than $len (such as an empty slice), causing the macro in safe caller code to construct an array reference pointing to unallocated memory or out-of-bounds elements, resulting in immediate UB.

Minimal Reproduction (Miri)
use arrayref::array_ref;

struct BadIndex;

impl std::ops::Index<std::ops::Range<usize>> for BadIndex {
    type Output = [u8];
    fn index(&self, _index: std::ops::Range<usize>) -> &[u8] {
        &[]
    }
}

fn main() {
    let bad = BadIndex;
    let r: &[u8; 5] = array_ref![bad, 0, 5];
    std::hint::black_box(r);
}
error: Undefined Behavior: pointer not dereferenceable: pointer must be dereferenceable for 5 bytes, but got alloc2 which is at or beyond the end of the allocation of size 0 bytes
  --> src/bin/repro1.rs:14:23
   |
14 |     let r: &[u8; 5] = array_ref![bad, 0, 5];
   |                       ^^^^^^^^^^^^^^^^^^^^^ 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
   = note: stack backtrace:
           0: main::as_array::<u8>
               at /usr/local/google/home/manishearth/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/arrayref-0.3.9/src/lib.rs:62:17: 62:55
           1: main
               at /usr/local/google/home/manishearth/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/arrayref-0.3.9/src/lib.rs:68:17: 68:32
   = note: this error originates in the macro `array_ref` (in Nightly builds, run with -Z macro-backtrace for more info)

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

Assert that the obtained slice runtime length matches $len before performing the unsafe array reference cast:

 macro_rules! array_ref {
     ($arr:expr, $offset:expr, $len:expr) => {{
         {
             #[inline]
             const unsafe fn as_array<T>(slice: &[T]) -> &[T; $len] {
                 &*(slice.as_ptr() as *const [_; $len])
             }
             let offset = $offset;
             let slice = &$arr[offset..offset + $len];
+            assert!(slice.len() == $len);
             #[allow(unused_unsafe)]
             unsafe {
                 as_array(slice)
             }
         }
     }};
 }

An identical assertion should also be added to array_mut_ref!.


Full Gemini Unsafe Code Audit Report

[!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.

Unsafe Rust Review: arrayref (v0_3)

Overall Safety Assessment

The arrayref crate provides macros to extract array references from slices. It contains a small amount of unsafe code inside these macros, which performs pointer casts to convert slice pointers to array references.

The unsafe code has some safety issues:

  • There is a critical soundness bug in the array_ref! and array_mut_ref! macros. They assume that slicing a type yields a slice of the exact expected length. However, because Index is a safe trait, a custom implementation can return a shorter slice, resulting in out-of-bounds reads/writes when dereferencing the casted pointer.
  • There are no # Safety documentation or // SAFETY: comments on any of the unsafe boundaries or operations.
  • The macros rely on left-to-right evaluation order of tuple elements to advance pointers safely, which is guaranteed by the Rust Reference but is fragile and undocumented.

Due to the soundness bug, this crate cannot be considered fully sound in the presence of custom Index implementations.

Critical Findings

Soundness Bug in array_ref! and array_mut_ref! Macros 🔴 🤸

  • Priority: 🔴 High
  • Threat Vector: 🤸 Deliberate Contortion
  • Bug Type: Out-of-Bounds Pointer Dereference

Description:
The macros array_ref! and array_mut_ref! cast a slice &[T] (or &mut [T]) to an array reference &[T; $len] (or &mut [T; $len]).
The slice is obtained via:
let slice = &$arr[offset..offset + $len];
And the cast is:
&*(slice.as_ptr() as *const [_; $len])

The macro assumes that slice.len() == $len. While this is guaranteed for standard slices ([T]), the macro can be called on any type $arr that implements Index<Range<usize>, Output = [T]>. Because Index is a safe trait, a user can provide an implementation that returns a slice of a different length (e.g., an empty slice &[]). If this happens, the macro will dereference a pointer pointing to fewer than $len elements, leading to Undefined Behavior (out-of-bounds read or write).

Proof of Concept:

struct BadIndex;
impl std::ops::Index<std::ops::Range<usize>> for BadIndex {
    type Output = [u8];
    fn index(&self, _index: std::ops::Range<usize>) -> &[u8] {
        &[] // Returns an empty slice regardless of the range
    }
}

fn main() {
    let bad = BadIndex;
    // This is safe code but triggers UB (Segmentation fault) when printed:
    let r: &[u8; 5] = array_ref![bad, 0, 5];
    println!("r = {:?}", r);
}

Remedy:
Add an assertion in the macros to verify that the slice's runtime length matches the expected length before performing the unsafe cast:

let slice = &$arr[offset..offset + $len];
assert_eq!(slice.len(), $len);

Fishy Findings

Fragile Reliance on Tuple Evaluation Order 🟡 🤦

  • Severity: 🟡 Low
  • Threat Vector: 🤦 Accidental Misuse
  • Bug Type: Fragile Evaluation Order Reliance

Description:
The array_refs! and mut_array_refs! macros use a mutable pointer p that is modified sequentially during the construction of a tuple:

( $( {
    let aref = & *(p as *const [T; $pre]);
    p = p.add($pre);
    aref
}, )* ... )

This assumes that the expressions inside the tuple are evaluated from left to right, so that p is advanced correctly before the next array reference is created.
While the Rust Reference guarantees left-to-right evaluation order for tuple expressions, relying on side effects across tuple elements to guarantee memory safety is fragile. If the compiler were to evaluate them in a different order (e.g. due to optimization bugs or future language changes), it would result in overlapping or out-of-bounds references, which is UB.
This reliance is not documented or commented in the code.

Missing Safety Comments

Missing Safety Comments on as_array and Pointer Casts 🔴

The crate lacks any safety comments. The following comments should be added:

  1. In array_ref! (and similarly in array_mut_ref!):
            /// # Safety
            ///
            /// The caller must ensure that `slice.len() >= $len`.
            const unsafe fn as_array<T>(slice: &[T]) -> &[T; $len] {
                // SAFETY:
                // - `slice.as_ptr()` points to a valid allocation of at least `slice.len()` elements (AXIOM: standard library slice contract).
                // - By the safety contract of this function, `slice` contains at least `$len` elements (PRECONDITION).
                // - Therefore, casting the pointer to `*const [T; $len]` and dereferencing it is valid (AXIOM: primitive pointer dereference).
                // - The returned reference's lifetime is bound to the input `slice`'s lifetime (TYPE FACT).
                &*(slice.as_ptr() as *const [_; $len])
            }
  1. In array_refs! (and similarly in mut_array_refs!):
            /// # Safety
            ///
            /// The caller must ensure that `a.len() >= MIN_LEN`.
            const unsafe fn as_arrays<T>(a: &[T]) -> ( $( &[T; $pre], )* &[T],  $( &[T; $post], )*) {
                const MIN_LEN: usize = 0usize $( .saturating_add($pre) )* $( .saturating_add($post) )*;
                // ...
                let mut p = a.as_ptr();
                ( $( {
                    // SAFETY:
                    // - `p` is derived from `a.as_ptr()`.
                    // - Since we asserted `a.len() >= MIN_LEN`, the pointer `p` remains within the bounds of the valid slice `a`
                    //   for this offset (LOCAL FACT, PRECONDITION).
                    // - The cast to `*const [T; $pre]` is valid because there are at least `$pre` elements remaining in the slice.
                    let aref = & *(p as *const [T; $pre]);
                    // SAFETY: `p` is advanced by `$pre` elements, which is safe because `p + $pre` is within or at most one element past the end of the slice (AXIOM: pointer arithmetic).
                    p = p.add($pre);
                    aref
                }, )* ... )
            }

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