From 5457fa55fbfe15fe3f33544ca4752d51142e6fef Mon Sep 17 00:00:00 2001 From: Pedro Nobre Date: Tue, 22 Sep 2026 07:30:41 +0100 Subject: [PATCH 1/7] refactor: re-implement fuzzing via libfuzzer --- .github/workflows/rust.yml | 12 +- .gitignore | 1 - fuzz/.gitignore | 4 + fuzz/Cargo.toml | 21 +- fuzz/README.md | 47 ++- fuzz/fuzz_targets/smallvec_ops.rs | 467 ++++++++++++++++++------------ fuzz/in/stub | 1 - fuzz/travis-fuzz.sh | 19 -- 8 files changed, 340 insertions(+), 232 deletions(-) create mode 100644 fuzz/.gitignore delete mode 100644 fuzz/in/stub delete mode 100755 fuzz/travis-fuzz.sh diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 2a836946..67d655fd 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -20,13 +20,12 @@ jobs: include: - name: stable toolchain: stable - fuzz: true - name: beta toolchain: beta - fuzz: true - name: nightly toolchain: nightly nightly: true + fuzz: true - name: MSRV toolchain: "1.86.0" - name: no_std @@ -36,10 +35,6 @@ jobs: steps: - uses: actions/checkout@v7 - - name: Install packages for fuzzing - if: matrix.fuzz - run: sudo apt-get update -y && sudo apt-get install -y binutils-dev libunwind8-dev libcurl4-openssl-dev libelf-dev libdw-dev cmake gcc libiberty-dev - - name: Install toolchain uses: dtolnay/rust-toolchain@master with: @@ -85,10 +80,9 @@ jobs: if: matrix.nightly run: rustup component add miri && cargo miri test --verbose --all-features - - name: fuzz + - name: Run Fuzzer if: matrix.fuzz - working-directory: fuzz - run: ./travis-fuzz.sh + run: cargo install cargo-fuzz && cargo fuzz run smallvec_ops -- -max_total_time=60 build_result: name: homu build finished diff --git a/.gitignore b/.gitignore index 858f0d52..af93bbc0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,2 @@ target -/fuzz/hfuzz_target /.vscode diff --git a/fuzz/.gitignore b/fuzz/.gitignore new file mode 100644 index 00000000..1a45eee7 --- /dev/null +++ b/fuzz/.gitignore @@ -0,0 +1,4 @@ +target +corpus +artifacts +coverage diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index ebaf78b6..bc60f71f 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -1,26 +1,23 @@ [package] name = "smallvec-fuzz" version = "0.1.0" -authors = ["Dawid Ciężarkiewicz "] -edition = "2021" +authors = ["The Servo Project Developers"] publish = false +edition = "2024" [package.metadata] cargo-fuzz = true -[features] -afl_fuzz = ["afl"] -honggfuzz_fuzz = ["honggfuzz"] - - [dependencies] -honggfuzz = { version = "0.5.47", optional = true } -afl = { version = "0.4", optional = true } -smallvec = { path = ".." } +arbitrary = { version = "1.4.2", features = ["derive"] } +libfuzzer-sys = "0.4" -[workspace] -members = ["."] +[dependencies.smallvec] +path = ".." [[bin]] name = "smallvec_ops" path = "fuzz_targets/smallvec_ops.rs" +test = false +doc = false +bench = false diff --git a/fuzz/README.md b/fuzz/README.md index 32305709..e9528a87 100644 --- a/fuzz/README.md +++ b/fuzz/README.md @@ -1,12 +1,47 @@ -# Fuzzer for smallvec +# SmallVec Differential Fuzzer -Based on fuzzing in [rust-bitcoin](https://github.com/rust-bitcoin/rust-bitcoin/tree/c8ac25219a09bf9d017f1b05abe3e746e2136f73/fuzz) +Differential fuzzer comparing `smallvec::SmallVec` invariants and operations directly against `std::vec::Vec`. -## Running manually with afl +It tests structural equivalence across multiple inline capacity configurations (`N = 0, 1, 2, 7, 8`) to stress inline-to-heap spilling boundaries, buffer alignment and iterator drop mechanics. +## Quick start + +This fuzzer uses `libFuzzer` via `cargo-fuzz`. + +### Prerequisites + +Install `cargo-fuzz` (requires a nightly Rust toolchain): + +```sh +cargo +nightly install cargo-fuzz ``` -cargo afl build --release --bin smallvec_ops --features afl && cargo afl fuzz -i in -o out target/release/smallvec_ops + +### Running the fuzzer + +Run the target with standard `libFuzzer` options: + +```sh +cargo +nightly fuzz run smallvec_ops ``` -# Useful links: -* https://rust-fuzz.github.io/book/afl.html +### Reproducing a Ccash + +If the fuzzer finds an invariant mismatch or panic, reproduce it against a saved crash artifact: + +```sh +cargo +nightly fuzz run smallvec_ops artifacts/smallvec_ops/crash- +``` + +### Generating Coverage Reports + +Generating coverage requires the `llvm-tools-preview` component: + +```sh +cargo +nightly rustup component add llvm-tools-preview +``` + +Then run coverage against the target: + +```sh +cargo +nightly fuzz coverage smallvec_ops +``` diff --git a/fuzz/fuzz_targets/smallvec_ops.rs b/fuzz/fuzz_targets/smallvec_ops.rs index 991793dc..5a5b13f3 100644 --- a/fuzz/fuzz_targets/smallvec_ops.rs +++ b/fuzz/fuzz_targets/smallvec_ops.rs @@ -1,237 +1,336 @@ //! Simple fuzzer testing all available `SmallVec` operations -use smallvec::SmallVec; -// There's no point growing too much, so try not to grow -// over this size. -const CAP_GROWTH: usize = 256; +#![no_main] -macro_rules! next_usize { - ($b:ident) => { - $b.next().unwrap_or(0) as usize - }; +use { + arbitrary::Arbitrary, + libfuzzer_sys::fuzz_target, + smallvec::SmallVec, + std::fmt::Debug +}; + +/// A generic wrapper that bounds data generated via `arbitrary`. +/// Default cap is 255. +#[derive(Debug, Clone)] +pub struct Bounded(pub T); + +// Bounded `usize` between `0..=CAP` +impl<'a, const CAP: usize> Arbitrary<'a> for Bounded { + #[inline] + fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { + Ok(Bounded(u.int_in_range(0..=CAP)?)) + } } -macro_rules! next_u8 { - ($b:ident) => { - $b.next().unwrap_or(0) - }; +// Bounded `Vec` whose length is between `0..=CAP` +impl<'a, T, const CAP: usize> Arbitrary<'a> for Bounded, CAP> +where T: Arbitrary<'a> +{ + fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { + let len = u.int_in_range(0..=CAP)?; + let vec = u + .arbitrary_iter()? + .take(len) + .collect::, _>>()?; + Ok(Bounded(vec)) + } } -fn black_box_iter(i: impl Iterator) { - // print to work as a black_box - print!("{}", i.fold(0u8, |acc, e| acc.wrapping_add(e))); +#[inline] +fn choose_range( + u: &mut arbitrary::Unstructured, + len: usize +) -> arbitrary::Result> { + let start = u.int_in_range(0..=len)?; + let end = u.int_in_range(start..=len)?; + Ok(start..end) } -fn black_box_slice(s: &[u8]) { - black_box_iter(s.iter().copied()) +#[derive(Arbitrary, Debug)] +enum Op { + New, + WithCapacity(Bounded), + FromVec, + FromSlice(Bounded>), + Push(usize), + Pop, + Grow(Bounded), + Reserve(Bounded), + ReserveExact(Bounded), + ShrinkToFit, + Truncate, + SwapRemove, + Clear, + Remove, + Insert { val: usize }, + Drain, + Splice { items: Bounded> }, + RetainEven, + Dedup, + ExtendFromSlice(Bounded>), + ExtendFromWithin, + Resize { new_len: Bounded, val: usize } } -fn black_box_mut_slice(s: &mut [u8]) { - s.iter_mut().map(|e| *e = e.wrapping_add(1)).count(); - black_box_iter((s as &[u8]).iter().copied()) +/// Helper to assert equivalence of all structural invariants of `SmallVec` +/// against `alloc::Vec` +fn assert_invariants( + small_vec: &mut SmallVec, + std_vec: &mut Vec +) { + // Length and content equivalence + assert_eq!(small_vec.len(), std_vec.len(), "`len()` mismatch"); + assert_eq!( + small_vec.is_empty(), + std_vec.is_empty(), + "`is_empty()` mismatch" + ); + assert_eq!( + small_vec.as_slice(), + std_vec.as_slice(), + "`as_slice()` mismatch" + ); + assert_eq!( + small_vec.as_mut_slice(), + std_vec.as_mut_slice(), + "`as_mut_slice()` mismatch" + ); + + // Capacity & spilling invariants + assert!( + small_vec.capacity() >= small_vec.len(), + "`capacity()` is smaller than `len()`" + ); + assert!( + small_vec.capacity() >= N, + "`capacity()` is smaller than inline size `N`" + ); + assert_eq!( + small_vec.spilled(), + small_vec.capacity() > N, + "`spilled()` doesn't equal to `capacity() > N`" + ); + + // Indexing and bounds invariants + for i in 0..small_vec.len() { + assert_eq!( + small_vec[i], std_vec[i], + "`small_vec[{i}]` doesn't match `std_vec[{i}]`" + ); + assert_eq!( + small_vec.get(i), + std_vec.get(i), + "`small_vec.get({i})` doesn't match `std_vec.get({i})`" + ); + } + assert_eq!( + small_vec.get(small_vec.len()), + None, + "out-of-bounds `get()` did not return `None`" + ); + + // Iterator invariants + assert!( + small_vec.iter().eq(std_vec.iter()), + "iterator yield mismatch" + ); + assert!( + small_vec.iter().rev().eq(std_vec.iter().rev()), + "reverse iterator yield mismatch" + ); + assert!( + small_vec + .clone() + .into_iter() + .eq(std_vec.clone().into_iter()), + "`into_iter()` yield mismatch" + ); + assert_eq!( + small_vec.iter().size_hint(), + std_vec.iter().size_hint(), + "`size_hint()` mismatch" + ); + assert_eq!( + small_vec.iter().len(), + std_vec.iter().len(), + "`ExactSizeIterator::len()` mismatch" + ); + + // Clone invariant + assert_eq!(small_vec.clone(), *std_vec, "clone mismatch"); } -fn do_test(data: &[u8]) -> SmallVec { - let mut v = SmallVec::::new(); +fn test_with_inline_cap( + u: &mut arbitrary::Unstructured, + ops: &[Op] +) -> arbitrary::Result<()> { + // We let `T` be `usize` instead of `u8` because, albeit less efficient, + // this incurs potential memory misalignment which should be properly + // handled by the library. - let mut bytes = data.iter().copied(); + let mut small_vec = SmallVec::::new(); + let mut std_vec = Vec::::new(); - while let Some(op) = bytes.next() { - match op % 27 { - 0 => { - v = SmallVec::new(); + for op in ops { + match op { + Op::New => { + small_vec = SmallVec::new(); + std_vec = Vec::new(); } - 1 => { - v = SmallVec::with_capacity(next_usize!(bytes)); + Op::WithCapacity(cap) => { + small_vec = SmallVec::with_capacity(cap.0); + std_vec = Vec::with_capacity(cap.0); } - 2 => { - v = SmallVec::from_vec(v.to_vec()); + Op::FromVec => { + small_vec = SmallVec::from_vec(small_vec.into_vec()); + // No-op on `Vec` } - 3 => { - black_box_iter(v.drain(..)); + Op::FromSlice(data) => { + small_vec = SmallVec::from(data.0.as_slice()); + std_vec = data.0.clone(); } - 4 => { - if v.len() < CAP_GROWTH { - v.push(next_u8!(bytes)) - } + Op::Push(val) => { + small_vec.push(*val); + std_vec.push(*val); } - 5 => { - v.pop(); + Op::Pop => { + assert_eq!(small_vec.pop(), std_vec.pop(), "`pop()` mismatch"); } - 6 => v.grow(next_usize!(bytes) + v.len()), - 7 => { - if v.len() < CAP_GROWTH { - v.reserve(next_usize!(bytes)) + Op::Grow(target) => { + small_vec.grow(target.0); + // Mimic `SmallVec::grow` on `Vec` + if target.0 > std_vec.capacity() { + let additional = target.0 - std_vec.len(); + std_vec.reserve(additional); } } - 8 => { - if v.len() < CAP_GROWTH { - v.reserve_exact(next_usize!(bytes)) - } + Op::Reserve(amount) => { + small_vec.reserve(amount.0); + std_vec.reserve(amount.0); } - 9 => v.shrink_to_fit(), - 10 => v.truncate(next_usize!(bytes)), - 11 => black_box_slice(v.as_slice()), - 12 => black_box_mut_slice(v.as_mut_slice()), - 13 => { - if !v.is_empty() { - v.swap_remove(next_usize!(bytes) % v.len()); - } + Op::ReserveExact(amount) => { + small_vec.reserve_exact(amount.0); + std_vec.reserve_exact(amount.0); + } + Op::ShrinkToFit => { + small_vec.shrink_to_fit(); + std_vec.shrink_to_fit(); } - 14 => { - v.clear(); + Op::Truncate => { + let len = u.int_in_range(0..=small_vec.len())?; + small_vec.truncate(len); + std_vec.truncate(len); } - 15 => { - if !v.is_empty() { - v.remove(next_usize!(bytes) % v.len()); + Op::SwapRemove => { + if !small_vec.is_empty() { + let idx = u.choose_index(small_vec.len())?; + assert_eq!( + small_vec.swap_remove(idx), + std_vec.swap_remove(idx), + "`swap_remove()` mismatch" + ); } } - 16 => { - let insert_pos = next_usize!(bytes) % (v.len() + 1); - v.insert(insert_pos, next_u8!(bytes)); + Op::Clear => { + small_vec.clear(); + std_vec.clear(); } - 17 => { - let insert_pos = next_usize!(bytes) % (v.len() + 1); - let how_many = next_usize!(bytes); - let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { - v.splice(insert_pos..insert_pos, (0..how_many).map(|_| bytes.next().unwrap())); - })); - - if result.is_err() { - assert!(bytes.next().is_none()); + Op::Remove => { + if !small_vec.is_empty() { + let idx = u.choose_index(small_vec.len())?; + assert_eq!( + small_vec.remove(idx), + std_vec.remove(idx), + "`remove()` mismatch" + ); } } - 18 => { - v = SmallVec::from_vec(v.into_vec()); + Op::Insert { + val + } => { + let idx = u.int_in_range(0..=small_vec.len())?; + small_vec.insert(idx, *val); + std_vec.insert(idx, *val); } + Op::Drain => { + let len = small_vec.len(); + let range = choose_range(u, len)?; - 19 => { - let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { - v.retain_mut(|e| { - let alt_e = bytes.next().unwrap(); - let retain = *e >= alt_e; - *e = e.wrapping_add(alt_e); - retain - }); - })); + let small_vec_drained = small_vec.drain(range.clone()); + let std_vec_drained = std_vec.drain(range); - if result.is_err() { - assert!(bytes.next().is_none()); - } - } - 20 => { - v.dedup(); + assert!( + small_vec_drained.eq(std_vec_drained), + "`drain()` yield mismatch" + ); } + Op::Splice { + items + } => { + let len = small_vec.len(); + let range = choose_range(u, len)?; - 21 => { - let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { - v.dedup_by(|a, b| { - let substitute = bytes.next().unwrap(); - let dedup = a == b; - *a = a.wrapping_add(substitute); - *b = b.wrapping_add(substitute); - dedup - }); - })); + let small_vec_spliced = small_vec.splice(range.clone(), items.0.clone()); + let std_vec_spliced = std_vec.splice(range, items.0.clone()); - if result.is_err() { - assert!(bytes.next().is_none()); - } + assert!( + small_vec_spliced.eq(std_vec_spliced), + "`splice()` yield mismatch" + ); } - 22 => { - v = SmallVec::from(data); + Op::RetainEven => { + small_vec.retain(|e| e % 2 == 0); + std_vec.retain(|e| e % 2 == 0); } - - 23 => { - if v.len() < CAP_GROWTH { - v.extend_from_slice(data) - } + Op::Dedup => { + small_vec.dedup(); + std_vec.dedup(); } - - 24 => { - let a = next_usize!(bytes) % (v.len() + 1); - let b = next_usize!(bytes) % (v.len() + 1); - let (start, end) = (a.min(b), a.max(b)); - v.extend_from_within(start..end); + Op::ExtendFromSlice(items) => { + small_vec.extend_from_slice(&items.0); + std_vec.extend_from_slice(&items.0); } + Op::ExtendFromWithin => { + let len = small_vec.len(); + let range = choose_range(u, len)?; - 25 => { - if v.len() < CAP_GROWTH { - v.resize(next_usize!(bytes), next_u8!(bytes)); - } + small_vec.extend_from_within(range.clone()); + std_vec.extend_from_within(range); } - 26 => { - v = smallvec::from_elem(next_u8!(bytes), next_usize!(bytes)); + Op::Resize { + new_len, + val + } => { + small_vec.resize(new_len.0, *val); + std_vec.resize(new_len.0, *val); } - _ => panic!("booo"), } + + assert_invariants(&mut small_vec, &mut std_vec); } - v -} -fn do_test_all(data: &[u8]) { - do_test::<0>(data); - do_test::<1>(data); - do_test::<2>(data); - do_test::<7>(data); - do_test::<8>(data); + Ok(()) } -#[cfg(feature = "afl")] -fn main() { - afl::fuzz!(|data| { - // Remove the panic hook so we can actually catch panic - // See https://github.com/rust-fuzz/afl.rs/issues/150 - std::panic::set_hook(Box::new(|_| {})); - do_test_all(data); - }); -} +fn run_test(mut u: arbitrary::Unstructured) -> arbitrary::Result<()> { + let ops = Vec::::arbitrary(&mut u)?; + let dynamic_entropy = u.take_rest(); -#[cfg(feature = "honggfuzz")] -fn main() { - loop { - honggfuzz::fuzz!(|data| { - // Remove the panic hook so we can actually catch panic - // See https://github.com/rust-fuzz/afl.rs/issues/150 - std::panic::set_hook(Box::new(|_| {})); - do_test_all(data); - }); - } -} + let run_test = |test_func: fn(&mut arbitrary::Unstructured, &[Op]) -> arbitrary::Result<()>| { + test_func(&mut arbitrary::Unstructured::new(dynamic_entropy), &ops) + }; -#[cfg(test)] -mod tests { - fn extend_vec_from_hex(hex: &str, out: &mut Vec) { - let mut b = 0; - for (idx, c) in hex.as_bytes().iter().enumerate() { - b <<= 4; - match *c { - b'A'..=b'F' => b |= c - b'A' + 10, - b'a'..=b'f' => b |= c - b'a' + 10, - b'0'..=b'9' => b |= c - b'0', - b'\n' => {} - b' ' => {} - _ => panic!("Bad hex"), - } - if (idx & 1) == 1 { - out.push(b); - b = 0; - } - } - } + run_test(test_with_inline_cap::<0>)?; + run_test(test_with_inline_cap::<1>)?; + run_test(test_with_inline_cap::<2>)?; + run_test(test_with_inline_cap::<7>)?; + run_test(test_with_inline_cap::<8>)?; - #[test] - fn duplicate_crash() { - let mut a = Vec::new(); - // paste the output of `xxd -p ` here and run `cargo test` - extend_vec_from_hex( - r#" - 646e21f9f910f90200f9d9f9c7030000def9000010646e2af9f910f90264 - 6e21f9f910f90200f9d9f9c7030000def90000106400f9f9d9f9c7030000 - def90000106400f9d9f9e7f1000000d9f9e7f1000000f9 - "#, - &mut a, - ); - super::do_test_all(&a); - } + Ok(()) } + +fuzz_target!(|data: &[u8]| { + let u = arbitrary::Unstructured::new(data); + + let _ = run_test(u); +}); diff --git a/fuzz/in/stub b/fuzz/in/stub deleted file mode 100644 index 587be6b4..00000000 --- a/fuzz/in/stub +++ /dev/null @@ -1 +0,0 @@ -x diff --git a/fuzz/travis-fuzz.sh b/fuzz/travis-fuzz.sh deleted file mode 100755 index ff44d253..00000000 --- a/fuzz/travis-fuzz.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/bash -set -e -cargo install --force honggfuzz --version "^0.5.47" -for TARGET in fuzz_targets/*; do - FILENAME=$(basename $TARGET) - FILE="${FILENAME%.*}" - if [ -d hfuzz_input/$FILE ]; then - HFUZZ_INPUT_ARGS="-f hfuzz_input/$FILE/input" - fi - HFUZZ_BUILD_ARGS="--features honggfuzz_fuzz" HFUZZ_RUN_ARGS="--run_time 30 --exit_upon_crash -v $HFUZZ_INPUT_ARGS" cargo hfuzz run $FILE - - if [ -f hfuzz_workspace/$FILE/HONGGFUZZ.REPORT.TXT ]; then - cat hfuzz_workspace/$FILE/HONGGFUZZ.REPORT.TXT - for CASE in hfuzz_workspace/$FILE/SIG*; do - cat $CASE | xxd -p - done - exit 1 - fi -done From 9a5b99555714568a2db5e6dab428c25620197380 Mon Sep 17 00:00:00 2001 From: Pedro Nobre Date: Tue, 22 Sep 2026 18:04:21 +0100 Subject: [PATCH 2/7] refactor: added fuzz package to the workspace; implemented some small requested changes --- Cargo.lock | 62 +++++++++++++++++++++++++++++++ Cargo.toml | 6 +++ fuzz/Cargo.toml | 4 +- fuzz/README.md | 47 ----------------------- fuzz/fuzz_targets/smallvec_ops.rs | 12 ++---- 5 files changed, 74 insertions(+), 57 deletions(-) delete mode 100644 fuzz/README.md diff --git a/Cargo.lock b/Cargo.lock index e56831b8..6ab62b8e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -43,6 +43,9 @@ name = "arbitrary" version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] [[package]] name = "autocfg" @@ -104,6 +107,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ad534f4357a5264cce5019c989cf66a4f0dc4e0d1b1d15f8aacec0ff7360273" dependencies = [ "find-msvc-tools", + "jobserver", + "libc", "shlex", ] @@ -277,6 +282,17 @@ dependencies = [ "thiserror", ] +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "either" version = "1.18.0" @@ -350,6 +366,17 @@ dependencies = [ "slab", ] +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi", +] + [[package]] name = "half" version = "2.7.1" @@ -392,6 +419,16 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom", + "libc", +] + [[package]] name = "js-sys" version = "0.3.104" @@ -409,6 +446,16 @@ version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" +[[package]] +name = "libfuzzer-sys" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9fd2f41a1cba099f79a0b6b6c35656cf7c03351a7bae8ff0f28f25270f929d2" +dependencies = [ + "arbitrary", + "cc", +] + [[package]] name = "malloc_size_of" version = "0.1.1" @@ -513,6 +560,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "rayon" version = "1.12.0" @@ -658,6 +711,15 @@ dependencies = [ "serde_test", ] +[[package]] +name = "smallvec-fuzz" +version = "0.0.0" +dependencies = [ + "arbitrary", + "libfuzzer-sys", + "smallvec", +] + [[package]] name = "syn" version = "2.0.119" diff --git a/Cargo.toml b/Cargo.toml index 258897f4..c548fd91 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -77,3 +77,9 @@ harness = false [package.metadata.docs.rs] all-features = true rustdoc-args = ["--cfg", "docsrs"] + +[workspace] +members = [ + ".", + "fuzz", +] diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index bc60f71f..2fbb612f 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "smallvec-fuzz" -version = "0.1.0" +version = "0.0.0" authors = ["The Servo Project Developers"] publish = false edition = "2024" @@ -9,7 +9,7 @@ edition = "2024" cargo-fuzz = true [dependencies] -arbitrary = { version = "1.4.2", features = ["derive"] } +arbitrary = { version = "1.4", features = ["derive"] } libfuzzer-sys = "0.4" [dependencies.smallvec] diff --git a/fuzz/README.md b/fuzz/README.md deleted file mode 100644 index e9528a87..00000000 --- a/fuzz/README.md +++ /dev/null @@ -1,47 +0,0 @@ -# SmallVec Differential Fuzzer - -Differential fuzzer comparing `smallvec::SmallVec` invariants and operations directly against `std::vec::Vec`. - -It tests structural equivalence across multiple inline capacity configurations (`N = 0, 1, 2, 7, 8`) to stress inline-to-heap spilling boundaries, buffer alignment and iterator drop mechanics. - -## Quick start - -This fuzzer uses `libFuzzer` via `cargo-fuzz`. - -### Prerequisites - -Install `cargo-fuzz` (requires a nightly Rust toolchain): - -```sh -cargo +nightly install cargo-fuzz -``` - -### Running the fuzzer - -Run the target with standard `libFuzzer` options: - -```sh -cargo +nightly fuzz run smallvec_ops -``` - -### Reproducing a Ccash - -If the fuzzer finds an invariant mismatch or panic, reproduce it against a saved crash artifact: - -```sh -cargo +nightly fuzz run smallvec_ops artifacts/smallvec_ops/crash- -``` - -### Generating Coverage Reports - -Generating coverage requires the `llvm-tools-preview` component: - -```sh -cargo +nightly rustup component add llvm-tools-preview -``` - -Then run coverage against the target: - -```sh -cargo +nightly fuzz coverage smallvec_ops -``` diff --git a/fuzz/fuzz_targets/smallvec_ops.rs b/fuzz/fuzz_targets/smallvec_ops.rs index 5a5b13f3..9f19bbaf 100644 --- a/fuzz/fuzz_targets/smallvec_ops.rs +++ b/fuzz/fuzz_targets/smallvec_ops.rs @@ -62,9 +62,9 @@ enum Op { SwapRemove, Clear, Remove, - Insert { val: usize }, + Insert(usize), Drain, - Splice { items: Bounded> }, + Splice(Bounded>), RetainEven, Dedup, ExtendFromSlice(Bounded>), @@ -245,9 +245,7 @@ fn test_with_inline_cap( ); } } - Op::Insert { - val - } => { + Op::Insert(val) => { let idx = u.int_in_range(0..=small_vec.len())?; small_vec.insert(idx, *val); std_vec.insert(idx, *val); @@ -264,9 +262,7 @@ fn test_with_inline_cap( "`drain()` yield mismatch" ); } - Op::Splice { - items - } => { + Op::Splice(items) => { let len = small_vec.len(); let range = choose_range(u, len)?; From 49c6685447ddf6a83e0c66685cb05ae751167e13 Mon Sep 17 00:00:00 2001 From: Pedro Nobre Date: Tue, 22 Sep 2026 18:51:04 +0100 Subject: [PATCH 3/7] refactor: change fuzz file name --- fuzz/Cargo.toml | 4 ++-- fuzz/fuzz_targets/{smallvec_ops.rs => main.rs} | 0 2 files changed, 2 insertions(+), 2 deletions(-) rename fuzz/fuzz_targets/{smallvec_ops.rs => main.rs} (100%) diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 2fbb612f..d6f417f0 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -16,8 +16,8 @@ libfuzzer-sys = "0.4" path = ".." [[bin]] -name = "smallvec_ops" -path = "fuzz_targets/smallvec_ops.rs" +name = "main" +path = "fuzz_targets/main.rs" test = false doc = false bench = false diff --git a/fuzz/fuzz_targets/smallvec_ops.rs b/fuzz/fuzz_targets/main.rs similarity index 100% rename from fuzz/fuzz_targets/smallvec_ops.rs rename to fuzz/fuzz_targets/main.rs From ce7b5a6a990600da0328ab31a5a79bbc7f1c5e6c Mon Sep 17 00:00:00 2001 From: Pedro Nobre Date: Wed, 23 Sep 2026 14:38:39 +0100 Subject: [PATCH 4/7] refactor: change fuzz bin on ci; make resize a tuple; remove redundant invariant assertions on fuzzing --- .github/workflows/rust.yml | 2 +- fuzz/fuzz_targets/main.rs | 30 ++---------------------------- src/lib.rs | 5 +++-- 3 files changed, 6 insertions(+), 31 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 67d655fd..eca2e0e6 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -82,7 +82,7 @@ jobs: - name: Run Fuzzer if: matrix.fuzz - run: cargo install cargo-fuzz && cargo fuzz run smallvec_ops -- -max_total_time=60 + run: cargo install cargo-fuzz && cargo fuzz run main -- -max_total_time=60 build_result: name: homu build finished diff --git a/fuzz/fuzz_targets/main.rs b/fuzz/fuzz_targets/main.rs index 9f19bbaf..1a8bd1e3 100644 --- a/fuzz/fuzz_targets/main.rs +++ b/fuzz/fuzz_targets/main.rs @@ -69,7 +69,7 @@ enum Op { Dedup, ExtendFromSlice(Bounded>), ExtendFromWithin, - Resize { new_len: Bounded, val: usize } + Resize(Bounded, usize) } /// Helper to assert equivalence of all structural invariants of `SmallVec` @@ -90,11 +90,6 @@ fn assert_invariants( std_vec.as_slice(), "`as_slice()` mismatch" ); - assert_eq!( - small_vec.as_mut_slice(), - std_vec.as_mut_slice(), - "`as_mut_slice()` mismatch" - ); // Capacity & spilling invariants assert!( @@ -111,24 +106,6 @@ fn assert_invariants( "`spilled()` doesn't equal to `capacity() > N`" ); - // Indexing and bounds invariants - for i in 0..small_vec.len() { - assert_eq!( - small_vec[i], std_vec[i], - "`small_vec[{i}]` doesn't match `std_vec[{i}]`" - ); - assert_eq!( - small_vec.get(i), - std_vec.get(i), - "`small_vec.get({i})` doesn't match `std_vec.get({i})`" - ); - } - assert_eq!( - small_vec.get(small_vec.len()), - None, - "out-of-bounds `get()` did not return `None`" - ); - // Iterator invariants assert!( small_vec.iter().eq(std_vec.iter()), @@ -293,10 +270,7 @@ fn test_with_inline_cap( small_vec.extend_from_within(range.clone()); std_vec.extend_from_within(range); } - Op::Resize { - new_len, - val - } => { + Op::Resize(new_len, val) => { small_vec.resize(new_len.0, *val); std_vec.resize(new_len.0, *val); } diff --git a/src/lib.rs b/src/lib.rs index 5434d44f..461f2f1b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -882,14 +882,15 @@ impl SmallVec { .unwrap_or_else(SmallVecError::handle); } - #[cold] pub fn try_grow(&mut self, new_capacity: usize) -> Result<(), SmallVecError> { if Self::IS_ZST { return Ok(()); } let (len, on_heap) = self.len.parts(); - assert!(new_capacity >= len); + if new_capacity < len { + return Ok(()); + } if new_capacity > Self::inline_size() { // SAFETY: we checked all the preconditions From 5e71e1b5ef8dd7474da769831904bdcfa498bf84 Mon Sep 17 00:00:00 2001 From: Alejandro Vaz Date: Thu, 24 Sep 2026 13:19:07 +0200 Subject: [PATCH 5/7] Delete .github/workflows/rust.yml Signed-off-by: Alejandro Vaz --- .github/workflows/rust.yml | 98 -------------------------------------- 1 file changed, 98 deletions(-) delete mode 100644 .github/workflows/rust.yml diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml deleted file mode 100644 index eca2e0e6..00000000 --- a/.github/workflows/rust.yml +++ /dev/null @@ -1,98 +0,0 @@ -name: Rust - -on: - push: - branches: [v2] - pull_request: - merge_group: - types: [checks_requested] - workflow_dispatch: - -env: - CARGO_TERM_COLOR: always - -jobs: - build: - name: Build and test (${{ matrix.name }}) - runs-on: ubuntu-latest - strategy: - matrix: - include: - - name: stable - toolchain: stable - - name: beta - toolchain: beta - - name: nightly - toolchain: nightly - nightly: true - fuzz: true - - name: MSRV - toolchain: "1.86.0" - - name: no_std - toolchain: stable - target: thumbv7m-none-eabi - no_std: true - steps: - - uses: actions/checkout@v7 - - - name: Install toolchain - uses: dtolnay/rust-toolchain@master - with: - toolchain: ${{ matrix.toolchain }} - target: ${{ matrix.target }} - - - name: Style check - if: matrix.nightly - run: rustup component add rustfmt && cargo fmt --all --check - - - name: Clippy check - if: matrix.nightly - run: rustup component add clippy && cargo clippy --all-features --all-targets -- -D warnings - - - name: Build - run: cargo build --verbose - - - name: Run tests - if: ${{ !matrix.no_std }} - run: cargo test --verbose - - - name: Cargo test no default features - if: matrix.nightly - run: cargo test --verbose --no-default-features - - - name: Cargo test all features - if: matrix.nightly - run: cargo test --verbose --all-features - - - name: Cargo doc all features - if: matrix.nightly - run: cargo doc --all-features --verbose - - - name: Cargo bench no default features - if: matrix.nightly - run: cargo clean && cargo bench --verbose --no-default-features - - - name: Cargo bench all features - if: matrix.nightly - run: cargo clean && cargo bench --verbose --all-features - - - name: miri - if: matrix.nightly - run: rustup component add miri && cargo miri test --verbose --all-features - - - name: Run Fuzzer - if: matrix.fuzz - run: cargo install cargo-fuzz && cargo fuzz run main -- -max_total_time=60 - - build_result: - name: homu build finished - runs-on: ubuntu-latest - needs: build - - steps: - - name: Mark the job as successful - run: exit 0 - if: success() - - name: Mark the job as unsuccessful - run: exit 1 - if: "!success()" From d8f36d60e1a258d11251ad6fffc0325f608b9dce Mon Sep 17 00:00:00 2001 From: Pedro Nobre Date: Fri, 25 Sep 2026 06:10:00 +0100 Subject: [PATCH 6/7] test: fuzzing now seems to pass --- src/lib.rs | 23 +++++++++++++++++++---- tests/main.rs | 2 +- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index ea702ece..d52ce5fb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -467,6 +467,12 @@ impl SmallVec { } else { let mut vec = ManuallyDrop::new(vec); let length = vec.len(); + + // A heap-allocated `SmallVec` must always observe the invariant + // that `cap > N`. + if vec.capacity() <= N { + vec.reserve(N + 1 - length); + } let cap = vec.capacity(); // SAFETY: vec.capacity is not `0` (checked above), so the pointer // can not dangle and thus specifically cannot be null. @@ -886,14 +892,15 @@ impl SmallVec { .unwrap_or_else(SmallVecError::handle); } - #[cold] pub fn try_grow(&mut self, new_capacity: usize) -> Result<(), SmallVecError> { if Self::IS_ZST { return Ok(()); } let (length, on_heap) = self.length.parts(); - assert!(new_capacity >= length); + if new_capacity <= length { + return Ok(()); + } if new_capacity > Self::inline_size() { // SAFETY: we checked all the preconditions @@ -941,11 +948,18 @@ impl SmallVec { #[inline] pub fn try_reserve(&mut self, additional: usize) -> Result<(), SmallVecError> { if additional > self.capacity() - self.len() { - let new_capacity = self + let required = self .len() .checked_add(additional) - .and_then(usize::checked_next_power_of_two) .ok_or(SmallVecError::CapacityOverflow)?; + + let double_cap = self.capacity().saturating_mul(2); + + let new_capacity = required + .max(double_cap) + .checked_next_power_of_two() + .ok_or(SmallVecError::CapacityOverflow)?; + self.try_grow(new_capacity) } else { Ok(()) @@ -1894,6 +1908,7 @@ impl SmallVec { self.reserve(lower.saturating_add(1)); } unsafe { + // heap-buffer-overflow happening here core::ptr::write(self.as_mut_ptr().add(length), element); // Since next() executes user code which can panic we have to // bump the length after each step. diff --git a/tests/main.rs b/tests/main.rs index 5060f73a..c3c1d370 100644 --- a/tests/main.rs +++ b/tests/main.rs @@ -462,11 +462,11 @@ fn append() { } #[test] -#[should_panic(expected = "new_capacity >= length")] fn invalid_grow() { let mut v: SmallVec = SmallVec::new(); v.extend(0..8); v.grow(5); + assert_eq!(v.capacity(), 8); } #[test] From aa9ea2116abb5c5e0389039c07976915e805be98 Mon Sep 17 00:00:00 2001 From: Pedro Nobre Date: Fri, 25 Sep 2026 06:29:52 +0100 Subject: [PATCH 7/7] fix: this comment stayed from my debugging session, oops --- src/lib.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 953bc672..eaa9f189 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1641,7 +1641,6 @@ impl SmallVec { self.reserve(lower.saturating_add(1)); } unsafe { - // heap-buffer-overflow happening here core::ptr::write(self.as_mut_ptr().add(length), element); // Since next() executes user code which can panic we have to // bump the length after each step.