cargo / arrayvec / audit
cargo : arrayvec @ 0.7.6
PE Patrick Elsen signed 2026-05-27 published 2026-05-27

Claims

datastructure-impl-boundsdatastructure-impl-correctdatastructure-impl-safedatastructure-impl-testedhas-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

arrayvec 0.7.6: fixed-capacity stack-allocated ArrayVec/ArrayString backed by MaybeUninit. Mirrors std::vec::Vec idioms including panic-safe Drop and BackshiftOnDrop retain. One low-severity correctness finding: extend skips ptr::write for ZSTs but still increments the length guard, causing a double-drop on non-Copy ZSTs with side-effecting Drop (reproduced against this version). Second finding: most unsafe blocks lack SAFETY comments.

Report

Subject

arrayvec provides two fixed-capacity, inline-allocated containers: ArrayVec<T, CAP> (a vector backed by an array of MaybeUninit<T>) and ArrayString<CAP> (a UTF-8 string with the same backing strategy). The capacity is a const generic, range-limited to u32::MAX, and the structures store their length inline as a u32. The crate is no_std-compatible (default feature std only enables std::io::Write and std::error::Error impls). Optional features add serde, borsh, and zeroize trait impls.

Methodology

The published crate contents were compared against the upstream Git repository at the commit recorded in .cargo_vcs_info.json (tag 0.7.6) using diff. All seven files under src/ (~1,800 lines, mostly arrayvec.rs and array_string.rs) were read in full, with explicit focus on every unsafe block, raw-pointer arithmetic site, MaybeUninit use, and Drop impl. The tests/ directory (~1,600 lines of unit and integration tests including the serde/borsh feature tests) and the .github/workflows/ci.yml configuration were surveyed.

For the suspected ZST drop-count bug found in extend_from_iter, a reproducer (tmp/zst-repro) was written and executed against this exact crate version, instrumenting a Drop-implementing ZST with AtomicUsize counters and comparing extend against push baseline.

Tools used: openvet 0.6.0 for workspace 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; cargo / rustc for compiling and running the ZST reproducer.

Results

The comparison between the published crate contents and the upstream repository shows that all source, test, and license files match byte-for-byte; the only diff is the standard cargo Cargo.toml normalisation and the cargo-emitted .cargo_vcs_info.json / Cargo.toml.orig pair.

The crate ships no binary artefacts (justifying has-binaries) and the manifest sets build = false. There is no build.rs, no [lib] proc-macro = true, and no procedural-macro attributes; cargo runs no compile- or install-time hooks (justifying has-build-exec and has-install-exec).

The source contains no network code (no std::net, reqwest, ureq), no process invocation (no std::process), no environment-variable access (no std::env), no filesystem I/O (the only reference to std::path::Path is an AsRef<Path> impl on ArrayString that performs no I/O), and no cryptographic operations or implementations. The zeroize feature delegates entirely to the zeroize crate's traits. No threads are spawned, no async runtime is used, no concurrency primitives are implemented; the only concurrency-relevant code is the unsafe impl Send/Sync for Drain which mirrors std::vec::Drain. These observations justify uses-network, uses-filesystem, uses-environment, uses-exec, uses-jit, uses-interpreter, uses-crypto, uses-concurrency, impl-crypto, impl-parser, impl-interpreter, impl-jit, impl-protocol, impl-algorithm, and impl-concurrency.

ArrayVec and ArrayString are concrete data-structure implementations (justifying impl-datastructure). All operations are documented O(1) or O(n) and degrade only with input size, not adversarial ordering (justifying datastructure-impl-bounds). The crate makes pervasive use of unsafe for raw-pointer manipulation, MaybeUninit initialisation tracking, and manual Send/Sync impls (justifying uses-unsafe). Memory safety of the unsafe code was reviewed: the MaybeUninit-backed buffer, length-tracked initialisation prefix, ptr::write/ptr::read element movement, panic-safe Drop (using set_len(0) before drop_in_place in IntoIter, and ScopeExitGuard in extend_from_iter), and the BackshiftOnDrop pattern in retain all match the corresponding std::vec::Vec idioms (justifying unsafe-safe and datastructure-impl-safe). Unsafe is constrained to operations that genuinely require it (justifying unsafe-minimal). The crate ships unit tests in src/ modules and a tests/ directory with integration tests covering serde/borsh feature paths, justifying has-unit-tests and has-integration-tests. There is no fuzz/ harness and no proptest/quickcheck use, justifying has-fuzz-tests and has-property-tests. CI runs the full test suite under Miri with all features (justifying unsafe-tested and datastructure-impl-tested).

Two low-severity findings were recorded. Finding FINDING-1 (correctness) documents a ZST drop-count bug in extend_from_iter: the conditional skip of ptr::write(elt) for zero-sized types causes the local elt to be dropped at scope exit while guard.data is still incremented, leading to a second Drop call on each "slot" when the ArrayVec itself is later dropped. The bug was confirmed with a runnable reproducer against this crate version (3 constructions, 6 drops via extend; 3 constructions, 3 drops via push). The impact is limited to non-Copy ZSTs with side-effecting Drop — an uncommon pattern — and is not memory-unsafe, but it violates the Vec-like contract that each element is dropped exactly once. This justifies datastructure-impl-correct = false. Finding FINDING-2 (quality) notes that most unsafe blocks lack inline safety comments justifying their invariants, justifying unsafe-documented = false.

No malicious or deliberately harmful behaviour was found in the source, tests, or build configuration (justifying is-benign).

Conclusion

arrayvec is a well-scoped, narrow-purpose crate that closely mirrors std::vec::Vec semantics for fixed-capacity, stack-storable containers. The unsafe code is necessary, scoped tightly, and exercised under Miri in CI. The only correctness defect identified is a niche ZST double-drop in extend; consumers who do not use non-Copy ZSTs with side-effecting Drop are unaffected. The lack of inline safety comments is the main quality concern.

Findings(2)

FINDING-1 correctness low

Double-drop of non-Copy ZSTs in extend/from_iter

ArrayVec::extend (and FromIterator, try_extend_from_slice via the internal extend_from_iter) double-drops elements when T is a zero-sized type with a non-trivial Drop impl.

In src/arrayvec.rs:1080-1112, extend_from_iter skips ptr.write(elt) when mem::size_of::<T>() == 0. The local variable elt therefore goes out of scope at the end of the if let Some(elt) block and Drop is invoked. guard.data is nevertheless incremented, so on later ArrayVec::drop the call to drop_in_place on the tail slice runs Drop a second time on each logical ZST slot.

Reproduced against this exact crate version (workspace: tmp/zst-repro). Using a struct DropZst; with a counting Drop impl:

after extend(3): ctors=3 drops=3 vec.len=3
after drop(vec): ctors=3 drops=6
push baseline: after push(3): ctors=3 drops=0 vec.len=3
push net: ctors=3 drops=3

push/try_push go through ArrayVecImpl::push_unchecked (which uses ptr::write unconditionally) and are unaffected. The bug is contained to extend_from_iter.

Impact: limited. Only triggers for non-Copy ZSTs with side-effecting Drop (an uncommon pattern in practice). It is not memory-unsafe — for ZSTs there is no backing storage to corrupt — but the documented Vec-like contract that each constructed element runs Drop exactly once is violated for the affected types. Justifies datastructure-impl-correct = false.

FINDING-2 quality low

Unsafe blocks lack safety comments

The crate uses around 57 unsafe blocks across src/arrayvec.rs, src/array_string.rs, src/arrayvec_impl.rs, and src/char.rs. Only a small subset are accompanied by a safety comment justifying the invariants the unsafe code relies on:

  • src/char.rs:30 documents the encode_utf8 safety contract on the function signature.
  • src/array_string.rs:147-148 carries a // SAFETY: comment for zero_filled.
  • src/arrayvec.rs:608-617 and src/arrayvec.rs:467-470 carry block-level explanations for drain and retain.

The remaining unsafe blocks (raw-pointer arithmetic in ArrayVecImpl, set_len, try_insert, pop_at, drain_range, the Drop impls of IntoIter/Drain, From<[T; CAP]>, extend_from_iter, try_extend_from_slice, etc.) have no inline safety justification. The invariants they rely on (length within capacity, range within bounds, MaybeUninit initialisation) are inferable from context but not stated. This makes future maintenance and third-party audit more error-prone. Justifies unsafe-documented = false.

Annotations(4)

Cargo.toml

build = false, no [lib] proc-macro = true. Cargo runs no build- or install-time hooks for this crate. Optional features serde, borsh, zeroize only add trait impls; std (default) toggles no_std. uses-filesystem is still false despite the AsRef<Path> impl in array_string.rs:507-511 — it only converts &str to &Path and performs no filesystem I/O. Justifies has-build-exec, has-install-exec, uses-filesystem, uses-network, uses-environment, uses-exec.

src/arrayvec.rs

src/arrayvec.rs, line 49-55

impl<T, const CAP: usize> Drop for ArrayVec<T, CAP> {
    fn drop(&mut self) {
        self.clear();

        // MaybeUninit inhibits array's drop
    }
}

Drop for ArrayVec calls self.clear() which uses drop_in_place on the initialised prefix. MaybeUninit inhibits the array's own drop, so element drop is fully controlled by len. Justifies uses-unsafe, unsafe-safe, unsafe-minimal, impl-datastructure, datastructure-impl-safe.

src/arrayvec.rs, line 932-955

impl<T, const CAP: usize> Drop for IntoIter<T, CAP> {
    fn drop(&mut self) {
        // panic safety: Set length to 0 before dropping elements.
        let index = self.index;
        let len = self.v.len();
        unsafe {
            self.v.set_len(0);
            let elements = slice::from_raw_parts_mut(
                self.v.get_unchecked_ptr(index),
                len - index);
            ptr::drop_in_place(elements);
        }
    }
}

impl<T, const CAP: usize> Clone for IntoIter<T, CAP>
where T: Clone,
{
    fn clone(&self) -> IntoIter<T, CAP> {
        let mut v = ArrayVec::new();
        v.extend_from_slice(&self.v[self.index..]);
        v.into_iter()
    }
}

IntoIter::drop zeroes self.v.len before drop_in_place, which is panic-safe: a panicking element Drop does not cause the elements already consumed by next/next_back to be re-dropped.

src/arrayvec.rs, line 979-980

unsafe impl<'a, T: Sync, const CAP: usize> Sync for Drain<'a, T, CAP> {}
unsafe impl<'a, T: Send, const CAP: usize> Send for Drain<'a, T, CAP> {}

Drain is Send/Sync iff T is. The raw *mut ArrayVec pointer would otherwise be neither Send nor Sync; the borrow lifetime 'a of the source ArrayVec is what keeps the pointer valid. Justifies uses-concurrency = false (the impls are mirrors of std::vec::Drain, not an independent concurrency primitive).

src/arrayvec.rs, line 1080-1112

    pub(crate) unsafe fn extend_from_iter<I, const CHECK: bool>(&mut self, iterable: I)
        where I: IntoIterator<Item = T>
    {
        let take = self.capacity() - self.len();
        let len = self.len();
        let mut ptr = raw_ptr_add(self.as_mut_ptr(), len);
        let end_ptr = raw_ptr_add(ptr, take);
        // Keep the length in a separate variable, write it back on scope
        // exit. To help the compiler with alias analysis and stuff.
        // We update the length to handle panic in the iteration of the
        // user's iterator, without dropping any elements on the floor.
        let mut guard = ScopeExitGuard {
            value: &mut self.len,
            data: len,
            f: move |&len, self_len| {
                **self_len = len as LenUint;
            }
        };
        let mut iter = iterable.into_iter();
        loop {
            if let Some(elt) = iter.next() {
                if ptr == end_ptr && CHECK { extend_panic(); }
                debug_assert_ne!(ptr, end_ptr);
                if mem::size_of::<T>() != 0 {
                    ptr.write(elt);
                }
                ptr = raw_ptr_add(ptr, 1);
                guard.data += 1;
            } else {
                return; // success
            }
        }
    }

extend_from_iter skips ptr.write(elt) when size_of::<T>() == 0 but still increments guard.data. For ZSTs with a side-effecting Drop, the local elt is dropped at scope exit and drop_in_place later runs Drop again on each tracked slot. See FINDING-1.

src/char.rs

src/char.rs, line 32-55

pub unsafe fn encode_utf8(ch: char, ptr: *mut u8, len: usize) -> Result<usize, EncodeUtf8Error>
{
    let code = ch as u32;
    if code < MAX_ONE_B && len >= 1 {
        ptr.add(0).write(code as u8);
        return Ok(1);
    } else if code < MAX_TWO_B && len >= 2 {
        ptr.add(0).write((code >> 6 & 0x1F) as u8 | TAG_TWO_B);
        ptr.add(1).write((code & 0x3F) as u8 | TAG_CONT);
        return Ok(2);
    } else if code < MAX_THREE_B && len >= 3 {
        ptr.add(0).write((code >> 12 & 0x0F) as u8 | TAG_THREE_B);
        ptr.add(1).write((code >>  6 & 0x3F) as u8 | TAG_CONT);
        ptr.add(2).write((code & 0x3F) as u8 | TAG_CONT);
        return Ok(3);
    } else if len >= 4 {
        ptr.add(0).write((code >> 18 & 0x07) as u8 | TAG_FOUR_B);
        ptr.add(1).write((code >> 12 & 0x3F) as u8 | TAG_CONT);
        ptr.add(2).write((code >>  6 & 0x3F) as u8 | TAG_CONT);
        ptr.add(3).write((code & 0x3F) as u8 | TAG_CONT);
        return Ok(4);
    };
    Err(EncodeUtf8Error)
}

encode_utf8 is an internal helper invoked by ArrayString::try_push (src/array_string.rs:230-243). The safety contract that ptr is writable for len bytes is upheld by the caller, which passes self.capacity() - len as len and self.as_mut_ptr().add(len) as ptr while len < capacity. The byte-count branches encode the input char correctly per UTF-8 (char excludes surrogates and values > U+10FFFF by construction).

src/lib.rs

src/lib.rs, line 33-51

macro_rules! assert_capacity_limit {
    ($cap:expr) => {
        if std::mem::size_of::<usize>() > std::mem::size_of::<LenUint>() {
            if $cap > LenUint::MAX as usize {
                panic!("ArrayVec: largest supported capacity is u32::MAX")
            }
        }
    }
}

macro_rules! assert_capacity_limit_const {
    ($cap:expr) => {
        if std::mem::size_of::<usize>() > std::mem::size_of::<LenUint>() {
            if $cap > LenUint::MAX as usize {
                [/*ArrayVec: largest supported capacity is u32::MAX*/][$cap]
            }
        }
    }
}

Capacity is range-limited to u32::MAX. assert_capacity_limit! is checked at runtime in ArrayVec::new/ArrayString::new/ArrayString::zero_filled; assert_capacity_limit_const! triggers a compile-time error via out-of-bounds array indexing in new_const. Both checks no-op on 32-bit targets where size_of::<usize>() <= size_of::<u32>(). Justifies datastructure-impl-bounds. No imports of std::net, std::process, threading, crypto, jit, or interpreter, 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-algorithm, impl-concurrency. CI runs the full test suite under Miri with all features, justifying unsafe-tested, datastructure-impl-tested, has-unit-tests, has-integration-tests.