cargo / arrayref / audit
cargo : arrayref @ 0.3.9
PE Patrick Elsen signed 2026-05-27 published 2026-05-27

Claims

has-binarieshas-build-exechas-fuzz-testshas-install-exechas-integration-testshas-property-testshas-unit-testsimpl-algorithmimpl-concurrencyimpl-cryptoimpl-datastructureimpl-interpreterimpl-jitimpl-parserimpl-protocolis-benignunsafe-documentedunsafe-minimalunsafe-safeunsafe-testeduses-concurrencyuses-cryptouses-environmentuses-execuses-filesystemuses-interpreteruses-jituses-networkuses-unsafe

Summary

arrayref 0.3.9: no_std macros that produce &[T; N]/&mut [T; N] views into wider slices via raw-pointer casts. Safe slicing or type-level length checks gate every cast; quickcheck property tests cover both in- and out-of-bounds offsets. Four low-severity quality/correctness findings (missing SAFETY comments, fragile subtraction-before-assert in .. variants, #![deny(warnings)] coupling to compiler churn, multi-evaluation of macro arguments). Safe to deploy.

Report

Subject

arrayref is a small, no-std Rust crate exposing four macros (array_ref!, array_refs!, mut_array_refs!, array_mut_ref!) for obtaining fixed-size array references (&[T; N] or &mut [T; N]) into a larger slice or array. The motivation is to let APIs that need a statically-sized window over a buffer accept &[T; N] parameters while still being callable from code that holds the data as a wider slice, without paying repeated runtime bounds-checks.

Methodology

The published crate was diffed against the upstream Git working tree recorded in .cargo_vcs_info.json with diff -r; the only delta is the cargo-normalised Cargo.toml plus Cargo.lock, Cargo.toml.orig, and .cargo_vcs_info.json that cargo adds at publish time. The single source file (src/lib.rs, 504 lines including tests), all three examples, and the CI configuration in .github/workflows/rust.yml and .travis.yml were read in full. The audit focused on:

  • the soundness of the raw-pointer casts inside each macro's expansion, in particular whether the macros' validation always runs before any unchecked dereference;
  • whether the macros' arithmetic on offsets and lengths can be made to overflow or wrap into a soundness bug;
  • the coverage of the embedded test suite, including the quickcheck property tests for both in-bounds and out-of-bounds offsets.

The test suite was not executed and no code was compiled.

Tools used: openvet 0.6.0 for workspace creation and audit data management; diff (GNU diffutils) for byte-level comparison between contents/ and vcs/; git (2.51) for the upstream checkout; grep/ripgrep for capability surveys.

Results

The published crate matches the upstream Git tree byte-for-byte across code, licence, and examples; the cargo-added artefacts are publish-conventional.

The crate ships no binary artefacts, no build.rs, and no proc-macro, justifying has-binaries, has-build-exec, and has-install-exec. The only std:: references are extern crate std and use std::vec::Vec gated on #[cfg(test)]; the runtime path is strictly no_std and contains no I/O, no concurrency primitives, no cryptography, no process or interpreter execution, justifying uses-network, uses-filesystem, uses-environment, uses-exec, uses-jit, uses-interpreter, uses-crypto, uses-concurrency, and all of impl-crypto, impl-parser, impl-interpreter, impl-jit, impl-protocol, impl-datastructure, impl-algorithm, impl-concurrency.

All four macros expand to unsafe blocks containing raw-pointer casts (*const T as *const [T; N], *mut T as *mut [T; N]) and, for the variable-length variants, core::slice::from_raw_parts{_mut} calls, justifying uses-unsafe. Each macro performs validation before any unchecked dereference:

  • array_ref! and array_mut_ref! form the source slice via &$arr[offset..offset + $len], which bounds-checks at runtime; wraparound of offset + $len is itself caught by the slice indexer because the wrapped end becomes smaller than the start.
  • array_refs!/mut_array_refs! fixed variants take their input as &[T; $($len +)* 0], so the length condition is enforced at type level and any mismatch is a compile-time error.
  • The .. variants compute MIN_LEN at compile time via saturating_add, assert MIN_LEN < usize::MAX (catching saturated user input), and assert a.len() >= MIN_LEN before any pointer-arithmetic step.

This justifies unsafe-safe. unsafe-minimal also holds: each unsafe block is the minimum needed for a pointer cast or from_raw_parts that cannot be expressed in safe Rust. unsafe-tested is justified by the quickcheck property tests in mod test covering both successful and panicking offsets for array_ref! and array_mut_ref!, alongside the hand-written should_panic test and the exhaustive coverage of the split-and-.. variants.

Four low-severity findings were recorded. FINDING-1 notes that none of the unsafe blocks carries a // Safety: comment, leaving the soundness argument implicit; this is the reason unsafe-documented is asserted false. FINDING-2 flags a fragile ordering in the .. variants where a.len() - MIN_LEN runs immediately before the assert that would prove the subtraction in-bounds; soundness still holds because the assert fires before the subtraction's wrapped result is consumed, but the order should be swapped. FINDING-3 flags #![deny(warnings)] as a maintenance hazard against future compiler releases. FINDING-4 notes that $len/$offset substitution in the simple macros evaluates the argument multiple times, which is invisible for the idiomatic literal/const argument and panics loudly under most misuse.

No malicious patterns, obfuscation, hidden binaries, or unexpected side effects were found; the crate's behaviour matches the documented macro expansions, justifying is-benign.

Conclusion

arrayref is a single-purpose crate doing one job carefully. The unsafe it contains is necessary to express the intended cast, and the review found no soundness issue. All findings are low-severity quality items.

Findings(4)

FINDING-1 quality low

Unsafe blocks lack Safety: comments

All four macros expand to unsafe { ... } blocks that perform raw-pointer casts (slice.as_ptr() as *const [T; N], p as *mut [T; N]) and call core::slice::from_raw_parts{_mut}. None of the eight unsafe-containing sites in src/lib.rs carries a // Safety: comment justifying the invariants the cast relies on (length match between source slice and target array type, non-overlap for the multi-output mut_array_refs! variants, validity of pointer-arithmetic offsets).

The invariants are in practice maintained: array_ref!/array_mut_ref! slice via &$arr[offset..offset + $len] first (which bounds-checks at runtime), and array_refs!/mut_array_refs! rely on the input being typed as &[T; N] for a statically computed N or assert a.len() >= MIN_LEN before forming any reference. Adding // Safety: blocks would make this auditable inline rather than requiring reconstruction by the reader. This is the reason unsafe-documented is asserted false.

FINDING-2 quality low

var_len subtraction precedes the length assert in .. variants

In the .. arms of array_refs! (src/lib.rs:115-116) and mut_array_refs! (src/lib.rs:212-213), let var_len = a.len() - MIN_LEN; runs before assert!(a.len() >= MIN_LEN);. In debug builds the subtraction itself panics on underflow, which masks the assert; in release builds with overflow-checks off the subtraction silently wraps, producing a giant var_len, but the immediately-following assert! still fires before var_len is consumed by slice::from_raw_parts, so soundness is preserved.

The current ordering is therefore safe in practice but reads as accidental and is fragile under future edits (any code that observed var_len between the subtraction and the assert would become unsound). Swapping the two lines would make the invariant local.

FINDING-3 quality low

#![deny(warnings)] couples crate compilation to compiler-warning churn

src/lib.rs:28 sets #![deny(warnings)] at crate level. Any new lint introduced or promoted in a future rustc release will hard-error this crate against downstream consumers' cargo build even when nothing in the source has changed. The standard guidance is to scope deny(warnings) to CI runs only (e.g. via RUSTFLAGS in the workflow), keeping the crate buildable on newer toolchains. This is purely a maintenance concern, not a correctness or safety issue.

FINDING-4 correctness low

Macro arguments are evaluated multiple times

array_ref!($arr, $offset, $len) substitutes $len three times into the expansion: once in the return type of the inner as_array (&[T; $len]), once in the bounds-check slice (&$arr[offset..offset + $len]), and once in the pointer-cast target (*const [_; $len]). array_mut_ref! does the same. If a caller passes an effectful or non-pure expression for $len (or for $offset in the slicing expression), it will be evaluated several times with potentially different values.

Idiomatic use passes a literal or a const for $len, in which case this is invisible. But $len is documented only as "the length" without a constness requirement, and a user passing e.g. a function call would silently get misbehaviour rather than a compile error. A let-binding inside the macro would close this footgun for the offset; the length argument is typically required to be a const because it appears in a type position. Recording as low-severity correctness because the misuse path is narrow and the failure mode is loud (panic in the slicing or in the array-pointer cast).

Annotations(1)

src/lib.rs

src/lib.rs, line 56-72

#[macro_export]
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)
            }
        }
    }};
}

array_ref! macro: bounds-checks via &$arr[offset..offset + $len] first, then casts the resulting slice pointer to *const [_; $len]. Soundness rests on the slice length being exactly $len after the safe slicing operation. offset + $len overflow is caught by the slice indexing (a wrapped end value smaller than offset makes start > end and panics). Justifies uses-unsafe, unsafe-safe, unsafe-minimal. No I/O imports across src/, justifying uses-network, uses-filesystem, uses-environment, uses-exec, uses-concurrency, uses-crypto, uses-jit, uses-interpreter, impl-crypto, impl-parser, impl-interpreter, impl-jit, impl-protocol, impl-datastructure, impl-algorithm, impl-concurrency, has-build-exec, has-install-exec, has-binaries.

src/lib.rs, line 104-159

#[macro_export]
macro_rules! array_refs {
    ( $arr:expr, $( $pre:expr ),* ; .. ;  $( $post:expr ),* ) => {{
        {
            use core::slice;
            #[inline]
            #[allow(unused_assignments)]
            #[allow(clippy::eval_order_dependence)]
            const unsafe fn as_arrays<T>(a: &[T]) -> ( $( &[T; $pre], )* &[T],  $( &[T; $post], )*) {
                const MIN_LEN: usize = 0usize $( .saturating_add($pre) )* $( .saturating_add($post) )*;
                assert!(MIN_LEN < usize::MAX, "Your arrays are too big, are you trying to hack yourself?!");
                let var_len = a.len() - MIN_LEN;
                assert!(a.len() >= MIN_LEN);
                let mut p = a.as_ptr();
                ( $( {
                    let aref = & *(p as *const [T; $pre]);
                    p = p.add($pre);
                    aref
                }, )* {
                    let sl = slice::from_raw_parts(p as *const T, var_len);
                    p = p.add(var_len);
                    sl
                }, $( {
                    let aref = & *(p as *const [T; $post]);
                    p = p.add($post);
                    aref
                }, )*)
            }
            let input = $arr;
            #[allow(unused_unsafe)]
            unsafe {
                as_arrays(input)
            }
        }
    }};
    ( $arr:expr, $( $len:expr ),* ) => {{
        {
            #[inline]
            #[allow(unused_assignments)]
            #[allow(clippy::eval_order_dependence)]
            const unsafe fn as_arrays<T>(a: &[T; $( $len + )* 0 ]) -> ( $( &[T; $len], )* ) {
                let mut p = a.as_ptr();
                ( $( {
                    let aref = &*(p as *const [T; $len]);
                    p = p.offset($len as isize);
                    aref
                }, )* )
            }
            let input = $arr;
            #[allow(unused_unsafe)]
            unsafe {
                as_arrays(input)
            }
        }
    }}
}

array_refs! macro: two variants. The .. variant operates on a slice with runtime length-check (see FINDING-2). The fixed variant uses a function input of type &[T; $( $len + )* 0], making the type system enforce that the user-provided lengths sum to the array's compile-time size; this turns the soundness condition (no read past the array end) into a type error rather than a runtime check.

src/lib.rs, line 201-256

#[macro_export]
macro_rules! mut_array_refs {
    ( $arr:expr, $( $pre:expr ),* ; .. ;  $( $post:expr ),* ) => {{
        {
            use core::slice;
            #[inline]
            #[allow(unused_assignments)]
            #[allow(clippy::eval_order_dependence)]
            unsafe fn as_arrays<T>(a: &mut [T]) -> ( $( &mut [T; $pre], )* &mut [T],  $( &mut [T; $post], )*) {
                const MIN_LEN: usize = 0usize $( .saturating_add($pre) )* $( .saturating_add($post) )*;
                assert!(MIN_LEN < usize::MAX, "Your arrays are too big, are you trying to hack yourself?!");
                let var_len = a.len() - MIN_LEN;
                assert!(a.len() >= MIN_LEN);
                let mut p = a.as_mut_ptr();
                ( $( {
                    let aref = &mut *(p as *mut [T; $pre]);
                    p = p.add($pre);
                    aref
                }, )* {
                    let sl = slice::from_raw_parts_mut(p as *mut T, var_len);
                    p = p.add(var_len);
                    sl
                }, $( {
                    let aref = &mut *(p as *mut [T; $post]);
                    p = p.add($post);
                    aref
                }, )*)
            }
            let input = $arr;
            #[allow(unused_unsafe)]
            unsafe {
                as_arrays(input)
            }
        }
    }};
    ( $arr:expr, $( $len:expr ),* ) => {{
        {
            #[inline]
            #[allow(unused_assignments)]
            #[allow(clippy::eval_order_dependence)]
            unsafe fn as_arrays<T>(a: &mut [T; $( $len + )* 0 ]) -> ( $( &mut [T; $len], )* ) {
                let mut p = a.as_mut_ptr();
                ( $( {
                    let aref = &mut *(p as *mut [T; $len]);
                    p = p.add($len);
                    aref
                }, )* )
            }
            let input = $arr;
            #[allow(unused_unsafe)]
            unsafe {
                as_arrays(input)
            }
        }
    }};
}

mut_array_refs! macro: mirrors array_refs! but produces &mut [T; N] tuples. The macro intentionally hands out multiple &mut references derived from raw-pointer arithmetic over the same backing buffer; soundness rests on the offsets being non-overlapping, which holds because each p = p.add($pre) advances by exactly the size of the slot just borrowed and the offsets are driven by user-supplied lengths whose total is bounded by either the array's compile-time size (fixed variant) or the asserted a.len() >= MIN_LEN runtime check (.. variant).

src/lib.rs, line 282-298

#[macro_export]
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)
            }
        }
    }};
}

array_mut_ref! macro: identical structure to array_ref! but for &mut slicing. Same soundness argument: safe slicing first, then cast pointer to fixed-size array type.

src/lib.rs, line 300-504

#[allow(clippy::all)]
#[cfg(test)]
mod test {

    extern crate quickcheck;

    use std::vec::Vec;

    // use super::*;

    #[test]
    #[should_panic]
    fn checks_bounds() {
        let foo: [u8; 11] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
        let bar = array_ref!(foo, 1, 11);
        println!("I am checking that I can dereference bar[0] = {}", bar[0]);
    }

    #[test]
    fn simple_case_works() {
        fn check(expected: [u8; 3], actual: &[u8; 3]) {
            for (e, a) in (&expected).iter().zip(actual.iter()) {
                assert_eq!(e, a)
            }
        }
        let mut foo: [u8; 11] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
        {
            let bar = array_ref!(foo, 2, 3);
            check([2, 3, 4], bar);
        }
        check([0, 1, 2], array_ref!(foo, 0, 3));
        fn zero2(x: &mut [u8; 2]) {
            x[0] = 0;
            x[1] = 0;
        }
        zero2(array_mut_ref!(foo, 8, 2));
        check([0, 0, 10], array_ref!(foo, 8, 3));
    }

    #[test]
    fn check_array_ref_5() {
        fn f(data: Vec<u8>, offset: usize) -> quickcheck::TestResult {
            // Compute the following, with correct results even if the sum would overflow:
            //   if data.len() < offset + 5
            if data.len() < 5 || data.len() - 5 < offset {
                return quickcheck::TestResult::discard();
            }
            let out = array_ref!(data, offset, 5);
            quickcheck::TestResult::from_bool(out.len() == 5)
        }
        quickcheck::quickcheck(f as fn(Vec<u8>, usize) -> quickcheck::TestResult);
    }

    #[test]
    fn check_array_ref_out_of_bounds_5() {
        fn f(data: Vec<u8>, offset: usize) -> quickcheck::TestResult {
            // Compute the following, with correct results even if the sum would overflow:
            //   if data.len() >= offset + 5
            if data.len() >= 5 && data.len() - 5 >= offset {
                return quickcheck::TestResult::discard();
            }
            quickcheck::TestResult::must_fail(move || {
                array_ref!(data, offset, 5);
            })
        }
        quickcheck::quickcheck(f as fn(Vec<u8>, usize) -> quickcheck::TestResult);
    }

    #[test]
    fn check_array_mut_ref_7() {
        fn f(mut data: Vec<u8>, offset: usize) -> quickcheck::TestResult {
            // Compute the following, with correct results even if the sum would overflow:
            //   if data.len() < offset + 7
            if data.len() < 7 || data.len() - 7 < offset {
                return quickcheck::TestResult::discard();
            }
            let out = array_mut_ref!(data, offset, 7);
            out[6] = 3;
            quickcheck::TestResult::from_bool(out.len() == 7)
        }
        quickcheck::quickcheck(f as fn(Vec<u8>, usize) -> quickcheck::TestResult);
    }

    #[test]
    fn check_array_mut_ref_out_of_bounds_32() {
        fn f(mut data: Vec<u8>, offset: usize) -> quickcheck::TestResult {
            // Compute the following, with correct results even if the sum would overflow:
            //   if data.len() >= offset + 32
            if data.len() >= 32 && data.len() - 32 >= offset {
                return quickcheck::TestResult::discard();
            }
            quickcheck::TestResult::must_fail(move || {
                array_mut_ref!(data, offset, 32);
            })
        }
        quickcheck::quickcheck(f as fn(Vec<u8>, usize) -> quickcheck::TestResult);
    }

    #[test]
    fn test_5_array_refs() {
        let mut data: [usize; 128] = [0; 128];
        for i in 0..128 {
            data[i] = i;
        }
        let data = data;
        let (a, b, c, d, e) = array_refs!(&data, 1, 14, 3, 100, 10);
        assert_eq!(a.len(), 1 as usize);
        assert_eq!(b.len(), 14 as usize);
        assert_eq!(c.len(), 3 as usize);
        assert_eq!(d.len(), 100 as usize);
        assert_eq!(e.len(), 10 as usize);
        assert_eq!(a, array_ref![data, 0, 1]);
        assert_eq!(b, array_ref![data, 1, 14]);
        assert_eq!(c, array_ref![data, 15, 3]);
        assert_eq!(e, array_ref![data, 118, 10]);
    }

    #[test]
    fn test_5_array_refs_dotdot() {
        let mut data: [usize; 128] = [0; 128];
        for i in 0..128 {
            data[i] = i;
        }
        let data = data;
        let (a, b, c, d, e) = array_refs!(&data, 1, 14, 3; ..; 10);
        assert_eq!(a.len(), 1 as usize);
        assert_eq!(b.len(), 14 as usize);
        assert_eq!(c.len(), 3 as usize);
        assert_eq!(d.len(), 100 as usize);
        assert_eq!(e.len(), 10 as usize);
        assert_eq!(a, array_ref![data, 0, 1]);
        assert_eq!(b, array_ref![data, 1, 14]);
        assert_eq!(c, array_ref![data, 15, 3]);
        assert_eq!(e, array_ref![data, 118, 10]);
    }

    #[test]
    fn test_5_mut_xarray_refs() {
        let mut data: [usize; 128] = [0; 128];
        {
            // temporarily borrow the data to modify it.
            let (a, b, c, d, e) = mut_array_refs!(&mut data, 1, 14, 3, 100, 10);
            assert_eq!(a.len(), 1 as usize);
            assert_eq!(b.len(), 14 as usize);
            assert_eq!(c.len(), 3 as usize);
            assert_eq!(d.len(), 100 as usize);
            assert_eq!(e.len(), 10 as usize);
            *a = [1; 1];
            *b = [14; 14];
            *c = [3; 3];
            *d = [100; 100];
            *e = [10; 10];
        }
        assert_eq!(&[1; 1], array_ref![data, 0, 1]);
        assert_eq!(&[14; 14], array_ref![data, 1, 14]);
        assert_eq!(&[3; 3], array_ref![data, 15, 3]);
        assert_eq!(&[10; 10], array_ref![data, 118, 10]);
    }

    #[test]
    fn test_5_mut_xarray_refs_with_dotdot() {
        let mut data: [usize; 128] = [0; 128];
        {
            // temporarily borrow the data to modify it.
            let (a, b, c, d, e) = mut_array_refs!(&mut data, 1, 14, 3; ..; 10);
            assert_eq!(a.len(), 1 as usize);
            assert_eq!(b.len(), 14 as usize);
            assert_eq!(c.len(), 3 as usize);
            assert_eq!(d.len(), 100 as usize);
            assert_eq!(e.len(), 10 as usize);
            *a = [1; 1];
            *b = [14; 14];
            *c = [3; 3];
            *e = [10; 10];
        }
        assert_eq!(&[1; 1], array_ref![data, 0, 1]);
        assert_eq!(&[14; 14], array_ref![data, 1, 14]);
        assert_eq!(&[3; 3], array_ref![data, 15, 3]);
        assert_eq!(&[10; 10], array_ref![data, 118, 10]);
    }

    #[forbid(clippy::ptr_offset_with_cast)]
    #[test]
    fn forbidden_clippy_lints_do_not_fire() {
        let mut data = [0u8; 32];
        let _ = array_refs![&data, 8; .. ;];
        let _ = mut_array_refs![&mut data, 8; .. ; 10];
    }

    #[test]
    fn single_arg_refs() {
        let mut data = [0u8; 8];
        let (_,) = array_refs![&data, 8];
        let (_,) = mut_array_refs![&mut data, 8];

        let (_, _) = array_refs![&data, 4; ..;];
        let (_, _) = mut_array_refs![&mut data, 4; ..;];

        let (_, _) = array_refs![&data,; ..; 4];
        let (_, _) = mut_array_refs![&mut data,; ..; 4];

        let (_,) = array_refs![&data,; ..;];
        let (_,) = mut_array_refs![&mut data,; ..;];
    }
} // mod test

Tests live inline in the crate under #[cfg(test)] mod test. Coverage includes panicking-bounds tests (checks_bounds), positive cases for both 1D and split macros, and quickcheck-based property tests covering in-bounds and out-of-bounds offsets for both immutable and mutable variants (check_array_ref_5, check_array_ref_out_of_bounds_5, check_array_mut_ref_7, check_array_mut_ref_out_of_bounds_32). This justifies has-unit-tests, has-property-tests, and unsafe-tested. No tests/ directory or fuzz harness is shipped (has-integration-tests = false, has-fuzz-tests = false).