From 7ec1c72678432a3bb5854fc61ffe9dc6fbb05650 Mon Sep 17 00:00:00 2001 From: chenzeyan54-commits Date: Thu, 24 Sep 2026 04:27:15 +0800 Subject: [PATCH] fix(#663): port the new fuzzer to v1 port the new fuzzer introduced with #658 in v2 to the v1 branch, it should be a copy-paste + update CI if the fuzzers finds any inconsistencies, it's ... --- fuzz/fuzz_targets/smallvec_ops.rs | 437 +++++++++++++++++------------- 1 file changed, 253 insertions(+), 184 deletions(-) diff --git a/fuzz/fuzz_targets/smallvec_ops.rs b/fuzz/fuzz_targets/smallvec_ops.rs index 991793dc..8b3a4c6e 100644 --- a/fuzz/fuzz_targets/smallvec_ops.rs +++ b/fuzz/fuzz_targets/smallvec_ops.rs @@ -1,237 +1,306 @@ //! 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(usize), + Drain, + Splice(Bounded>), + RetainEven, + Dedup, + ExtendFromSlice(Bounded>), + ExtendFromWithin, + Resize(Bounded, 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" + ); + + // 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`" + ); + + // 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); +}); \ No newline at end of file