cargo / beef / audit
cargo : beef @ 0.5.2
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

beef 0.5.2 is a compact Cow replacement: 3-word beef::Cow (vs std's 4-word) and a 2-word beef::lean::Cow on 64-bit that packs length and capacity into one word. Small targeted unsafe for Vec::from_raw_parts round-trips; CI runs miri with strict-provenance. One low-severity correctness finding: lean::Cow borrowed constructors silently truncate length above 2^32, while owned constructors panic — an inconsistency the docs don't address.

Report

Subject

beef is a smaller, faster drop-in replacement for std::borrow::Cow. It exposes two variants: beef::Cow (wide), which is three words wide (pointer + length + an Option<NonZeroUsize> capacity, with None encoding the borrowed state); and beef::lean::Cow, which on 64-bit targets is only two words wide (pointer + a packed fat word holding both length and capacity in 32-bit halves). The smaller layout makes Cow cheaper to pass by value and to store inside enums or struct fields. The crate is no_std-compatible and the impl_serde feature adds Serde support.

Methodology

The published crate was diffed against the upstream Git checkout at the commit recorded in .cargo_vcs_info.json. All seven source files (src/lib.rs, src/generic.rs, src/wide.rs, src/lean.rs, src/traits.rs, src/serde.rs, src/lean.rs; ~1450 lines total) were read end-to-end. Every unsafe block, every unsafe trait/impl, and the Capacity impls for Wide and Lean were inspected against the invariants of Vec::from_raw_parts, String::from_utf8_unchecked, slice_from_raw_parts, and NonNull::new_unchecked. The test-generating test! macro in src/lib.rs and the MIRIFLAGS='-Zmiri-strict-provenance'-driven CI script (contents/ci/miri.sh) were noted.

Results

All source files are byte-identical between the published crate and upstream. Cargo.toml differences are limited to cargo's standard normalisation. The crate ships no binary artefacts (justifying has-binaries), no build.rs, and no proc-macros (justifying has-build-exec, has-install-exec). Optional dependencies (serde) are opt-in via documented features.

src/traits.rs defines an unsafe trait Beef: ToOwned whose contract is documented at the trait level: T::Owned must have a capacity word distinct from T, a capacity of 0 must not allocate, and the owned form must be reconstructable from a *mut T plus capacity. Implementations for str (backed by String) and [T: Clone] (backed by Vec<T>) go through Vec::from_raw_parts / String::from_utf8_unchecked and slice_from_raw_parts, each with "note on soundness" comments explaining the *const T as *mut T cast that occurs only on the borrow path (where the resulting pointer is never dereferenced as *mut). Backs uses-unsafe, unsafe-safe, unsafe-documented, unsafe-minimal, impl-datastructure, and datastructure-impl-safe.

src/generic.rs defines Cow<'a, T, U: Capacity>. The state of the Cow is encoded by querying U::maybe(fat, cap): a borrowed Cow returns None, and an owned Cow returns Some(capacity). Drop only reconstructs the owned type when capacity is present, so dropping a borrowed Cow is a no-op as expected. The Send/Sync impls have the same bounds as std::borrow::Cow and carry the inline "Safety: Same bounds as std::borrow::Cow" comment.

src/wide.rs and src/lean.rs are the two Capacity impls. Wide stores capacity as Option<NonZeroUsize> and is straightforward; Lean packs (length, capacity) into a single usize by storing length in the low 32 bits and capacity in the high 32 bits. The owned path (Capacity::store) explicitly panics if capacity exceeds u32::MAX. The borrowed path (Capacity::empty and Lean::mask_len) silently truncates length with & MASK_LO — recorded as FINDING-1, a low-severity correctness issue: in practice, byte slices above 4 GiB are rare and on 32-bit targets the issue cannot trigger, but the silent-vs-panic asymmetry with the owned path is surprising and the module docs do not explicitly warn about it. datastructure-impl-correct is therefore false.

The codebase was reviewed for cryptographic, network, filesystem, environment, exec, JIT, interpreter, and concurrency usage, and none was found, justifying uses-network, uses-filesystem, uses-environment, uses-exec, uses-crypto, uses-jit, uses-interpreter, uses-concurrency, impl-crypto, impl-parser, impl-interpreter, impl-jit, impl-protocol, impl-algorithm, impl-concurrency.

Testing is supplied by a test! macro in src/lib.rs that generates twin test suites for both wide::Cow and lean::Cow — 24 #[test] functions per variant covering borrowed and owned construction, into_owned, unwrap_borrowed (including the panic case), Clone, Hash, ordering, From<std::borrow::Cow> round-trip, Default, const_str/const_slice, and a stress_test_owned that drives 1024 iterations of clone+into_owned mutation (10 iterations under Miri). The upstream ci/miri.sh script runs cargo miri test --all-features with -Zmiri-strict-provenance, so the unsafe paths are validated under the strictest aliasing model on every commit. Together these justify has-unit-tests, unsafe-tested, and datastructure-impl-tested. No integration, fuzz, or property tests (has-integration-tests, has-fuzz-tests, has-property-tests). datastructure-impl-bounds is met: the documented lean::Cow 32-bit limit is the only deviation from std::borrow::Cow's bounds and is documented at the module level.

Conclusion

beef is a small, mature Cow replacement that uses unsafe only where strictly necessary to round-trip a String/Vec through raw parts. Its CI runs the suite under Miri with strict-provenance. One low-severity correctness finding (FINDING-1) documents an inconsistency between the owned and borrowed code paths in lean::Cow when the length exceeds 32 bits. No security or safety concerns were identified, justifying is-benign.

Findings(1)

FINDING-1 correctness low

beef::lean::Cow silently truncates length above 2^32 on borrowed inputs

beef::lean::Cow packs length and capacity into a single 64-bit "fat" word, with length in the low 32 bits and capacity in the high 32 bits. The Capacity::store impl (src/lean.rs:46-54) explicitly panics when an owned String/Vec has capacity > u32::MAX:

fn store(len: usize, capacity: usize) -> (usize, Lean) {
    if capacity & MASK_HI != 0 {
        panic!("beef::lean::Cow: Capacity out of bounds");
    }
    let fat = ((capacity & MASK_LO) << 32) | (len & MASK_LO);
    (fat, Lean)
}

However, the borrowed path silently masks the length to 32 bits without ever checking whether the value fits. The relevant sites are:

  • Capacity::empty (src/lean.rs:41-43) — (len & MASK_LO, Lean) — used by every Cow::borrowed(&'a T) call.
  • Lean::mask_len (src/lean.rs:26-29) — len & MASK_LO — used by Cow::const_str and Cow::const_slice.

If a caller constructs beef::lean::Cow::borrowed(s) from a &str (or &[T]) whose length exceeds u32::MAX, the resulting Cow reports a truncated length on Deref/AsRef/borrow. The pointer still points at the original allocation but the visible slice is len mod 2^32 bytes/elements long. There is no panic, no debug assertion, no documented warning at the call site, and no test that exercises the boundary.

The asymmetry with the owned path is the surprising part: an owned String of equivalent size panics fast; the borrowed equivalent silently misbehaves.

This is a low-severity quality/correctness finding: in practice, byte slices >4 GiB are rare (and on 32-bit targets usize is already 32 bits so the issue cannot trigger), and the module docs do say "Both length and capacity is limited to 32 bits". But the same docs only spell out the panic for the owned constructor — a reader could reasonably expect borrowed to behave symmetrically rather than truncate. Recommendation: either add a debug_assert!(len & MASK_HI == 0, ...) on the borrow paths or document the truncation behaviour explicitly. Justifies datastructure-impl-correct = false.

Annotations(4)

src/generic.rs

Generic Cow<'a, T, U: Capacity> implementation. Stores NonNull<T::PointerT> plus a usize fat field and a Capacity-impl-defined field. Drop reconstructs the owned type via T::owned_from_parts only when capacity is present. Send/Sync impls (lines 531, 539) carry the same bounds as std::borrow::Cow and a "Safety: Same bounds as std::borrow::Cow" comment. Backs uses-unsafe, unsafe-safe, unsafe-minimal, impl-datastructure.

src/lean.rs

src/lean.rs, line 41-69

    fn empty(len: usize) -> (usize, Lean) {
        (len & MASK_LO, Lean)
    }

    #[inline]
    fn store(len: usize, capacity: usize) -> (usize, Lean) {
        if capacity & MASK_HI != 0 {
            panic!("beef::lean::Cow: Capacity out of bounds");
        }

        let fat = ((capacity & MASK_LO) << 32) | (len & MASK_LO);

        (fat, Lean)
    }

    #[inline]
    fn unpack(fat: usize, _: Lean) -> (usize, usize) {
        (fat & MASK_LO, (fat & MASK_HI) >> 32)
    }

    #[inline]
    fn maybe(fat: usize, _: Lean) -> Option<Lean> {
        if fat & MASK_HI != 0 {
            Some(Lean)
        } else {
            None
        }
    }
}

Capacity impl for Lean. store (owned path) panics if capacity exceeds 2^32, but empty (borrowed path) and mask_len silently truncate length with & MASK_LO. See FINDING-1. Justifies datastructure-impl-correct = false.

src/lib.rs

src/lib.rs, line 61-285

#[rustfmt::skip]
macro_rules! test { ($tmod:ident => $cow:path) => {
    #[cfg(test)]
    mod $tmod {
        use $cow;

        #[test]
        fn borrowed_str() {
            let s = "Hello World";
            let c = Cow::borrowed(s);

            assert_eq!(s, c);
            assert_eq!(s, c.as_ref());
            assert_eq!(s, &*c);
        }

        #[test]
        fn owned_string() {
            let s = String::from("Hello World");
            let c: Cow<str> = Cow::owned(s.clone());

            assert_eq!(s, c);
        }

        #[test]
        fn into_owned() {
            let hello = "Hello World";
            let borrowed = Cow::borrowed(hello);
            let owned: Cow<str> = Cow::owned(String::from(hello));

            assert_eq!(borrowed.into_owned(), hello);
            assert_eq!(owned.into_owned(), hello);
        }

        #[test]
        fn borrowed_slice() {
            let s: &[_] = &[1, 2, 42];
            let c = Cow::borrowed(s);

            assert_eq!(s, c);
            assert_eq!(s, c.as_ref());
            assert_eq!(s, &*c);
        }

        #[test]
        fn owned_slice() {
            let s = vec![1, 2, 42];
            let c: Cow<[_]> = Cow::owned(s.clone());

            assert_eq!(s, c);
        }

        #[test]
        fn into_owned_vec() {
            let hello: &[u8] = b"Hello World";
            let borrowed = Cow::borrowed(hello);
            let owned: Cow<[u8]> = Cow::owned(hello.to_vec());

            assert_eq!(borrowed.into_owned(), hello);
            assert_eq!(owned.into_owned(), hello);
        }

        #[test]
        fn hash() {
            use std::collections::hash_map::DefaultHasher;
            use std::hash::{Hash, Hasher};

            let slice = "Hello World!";
            let borrowed = Cow::borrowed(slice);
            let owned: Cow<str> = Cow::owned(slice.to_owned());

            let hash1 = {
                let mut hasher = DefaultHasher::default();

                slice.hash(&mut hasher);

                hasher.finish()
            };

            let hash2 = {
                let mut hasher = DefaultHasher::default();

                borrowed.hash(&mut hasher);

                hasher.finish()
            };

            let hash3 = {
                let mut hasher = DefaultHasher::default();

                owned.hash(&mut hasher);

                hasher.finish()
            };

            assert_eq!(hash1, hash2);
            assert_eq!(hash1, hash3);
            assert_eq!(hash2, hash3);
        }

        #[test]
        fn ord_and_partial_ord() {
            use std::cmp::Ordering;

            macro_rules! generate_order_tests {
                ( $f:tt => $order:expr => $left:expr, $right:expr ) => {
                    assert_eq!(
                        Cow::<str>::borrowed($left).$f(&Cow::<str>::borrowed($right)),
                        $order
                    );

                    assert_eq!(
                        Cow::<str>::owned($left.to_owned())
                            .$f(&Cow::<str>::borrowed($right)),
                        $order
                    );

                    assert_eq!(
                        Cow::<str>::borrowed($left)
                            .$f(&Cow::<str>::owned($right.to_owned())),
                        $order
                    );

                    assert_eq!(
                        Cow::<str>::owned($left.to_owned())
                            .$f(&Cow::<str>::owned($right.to_owned())),
                        $order
                    );
                }
            }

            generate_order_tests!(partial_cmp => Some(Ordering::Equal) => "a", "a");
            generate_order_tests!(partial_cmp => Some(Ordering::Less) => "a", "b");
            generate_order_tests!(partial_cmp => Some(Ordering::Greater) => "b", "a");

            generate_order_tests!(cmp => Ordering::Equal => "a", "a");
            generate_order_tests!(cmp => Ordering::Less => "a", "b");
            generate_order_tests!(cmp => Ordering::Greater => "b", "a");
        }

        #[test]
        fn from_std_cow() {
            let std = std::borrow::Cow::Borrowed("Hello World");
            let beef = Cow::from(std.clone());

            assert_eq!(&*std, &*beef);
        }

        #[test]
        fn unwrap_borrowed() {
            let borrowed = Cow::borrowed("Hello");

            assert_eq!(borrowed.unwrap_borrowed(), "Hello");
        }

        #[test]
        #[should_panic]
        fn unwrap_owned() {
            let borrowed: Cow<str> = Cow::owned("Hello".to_string());

            borrowed.unwrap_borrowed();
        }

        #[test]
        fn stress_test_owned() {
            let mut expected = String::from("Hello... ");
            let mut cow: Cow<str> = Cow::borrowed("Hello... ");

            #[cfg(not(miri))]
            let iterations = 1024;
            #[cfg(miri)]
            let iterations = 10;

            for i in 0..iterations {
                if i % 3 == 0 {
                    let old = cow;
                    cow = old.clone();

                    std::mem::drop(old);
                }

                let mut owned = cow.into_owned();

                expected.push_str("Hello?.. ");
                owned.push_str("Hello?.. ");

                cow = owned.into();
            }

            assert_eq!(expected, cow.into_owned());
        }

        #[test]
        fn const_fn_str() {
            const HELLO: Cow<str> = Cow::const_str("Hello");

            assert_eq!(&*HELLO, "Hello");
        }

        #[test]
        #[cfg(feature = "const_fn")]
        fn const_fn_slice() {
            const FOO: Cow<[u8]> = Cow::const_slice(b"bar");

            assert_eq!(&*FOO, b"bar");
        }

        #[test]
        fn default_str() {
            let empty: Cow<str> = Default::default();

            assert_eq!(&*empty, "");
        }

        #[test]
        fn default_slice() {
            let empty: Cow<[u8]> = Default::default();

            assert_eq!(&*empty, b"");
        }
    }
} }

test!(test_wide => crate::wide::Cow);
test!(test_lean => crate::lean::Cow);

Module-scoped test! macro generating identical test suites for both wide::Cow and lean::Cow (24 #[test] functions per variant). The stress_test_owned test scales iterations down by 100x when running under Miri (cfg(miri) gate). Together with the ci/miri.sh script (which runs MIRIFLAGS='-Zmiri-strict-provenance' cargo miri test --all-features), justifies has-unit-tests and unsafe-tested.

src/traits.rs

Defines the unsafe trait Beef whose contract (T::Owned has an extra "capacity" word, capacity 0 means no allocation, T::Owned can be reconstructed from *mut T + capacity) is spelled out in the trait docstring. Implementations for str and [T: Clone] go through Vec::from_raw_parts / String::from_utf8_unchecked and slice_from_raw_parts, with inline "note on soundness" comments explaining the *const T as *mut T casts. Backs uses-unsafe, unsafe-safe, unsafe-documented.