-
Notifications
You must be signed in to change notification settings - Fork 4
tests: add fuzzing to our test suite #52
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
6e09c68
cb6fa4e
ac844ad
9a12bbb
10d8446
3a327bc
18cc64b
4f3ff0c
f6a87c1
7534682
2c1f94d
44cc267
33b0a22
cfb767b
36dccc0
2552540
afeaf31
3c7cfec
4324017
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -56,6 +56,45 @@ jobs: | |||||||||||||
| toolchain: ${{ steps.msrv.outputs.value }} | ||||||||||||||
| components: rustfmt | ||||||||||||||
| - run: just ci_mode=0 ci-test-msrv # Ignore warnings in MSRV | ||||||||||||||
| fuzz: | ||||||||||||||
| runs-on: ubuntu-latest | ||||||||||||||
|
|
||||||||||||||
| env: | ||||||||||||||
| # The number of seconds to run the fuzz target. | ||||||||||||||
| FUZZ_TIME: 60 | ||||||||||||||
|
|
||||||||||||||
| strategy: | ||||||||||||||
| matrix: | ||||||||||||||
| include: | ||||||||||||||
| - fuzz_target: fastpfor_cpp | ||||||||||||||
| - fuzz_target: fastpfor_rust | ||||||||||||||
|
|
||||||||||||||
| steps: | ||||||||||||||
| - uses: actions/checkout@v6 | ||||||||||||||
| with: {persist-credentials: false, submodules: recursive} | ||||||||||||||
|
||||||||||||||
| with: {persist-credentials: false, submodules: recursive} | |
| with: {persist-credentials: false, submodules: recursive} | |
| - if: github.event_name != 'release' && github.event_name != 'workflow_dispatch' | |
| uses: Swatinem/rust-cache@v2 |
Copilot
AI
Feb 13, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The cargo fuzz commands are being run from the repository root, but they should be run from the fuzz directory. cargo-fuzz expects to be run from the directory containing the fuzz Cargo.toml. Add working-directory: fuzz to these steps or use cd fuzz && cargo fuzz ... to ensure the commands run in the correct directory.
| - run: cargo fuzz build --target x86_64-unknown-linux-gnu ${{ matrix.fuzz_target }} | |
| - run: cargo fuzz run --target x86_64-unknown-linux-gnu ${{ matrix.fuzz_target }} -- -max_total_time=${{ env.FUZZ_TIME }} | |
| - run: cargo fuzz build --target x86_64-unknown-linux-gnu ${{ matrix.fuzz_target }} | |
| working-directory: fuzz | |
| - run: cargo fuzz run --target x86_64-unknown-linux-gnu ${{ matrix.fuzz_target }} -- -max_total_time=${{ env.FUZZ_TIME }} | |
| working-directory: fuzz |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| # Fuzzing artifacts | ||
| artifacts/ | ||
| corpus/ | ||
| coverage/ | ||
|
|
||
| # Build artifacts | ||
| target/ | ||
| Cargo.lock | ||
|
|
||
| # Crash/timeout/slow inputs that should be committed | ||
| # are stored in artifacts/ but we ignore by default | ||
| # Uncomment specific patterns if you want to track some artifacts | ||
| # !artifacts/important-crash |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| [package] | ||
| name = "fastpfor-fuzz" | ||
| version = "0.0.0" | ||
| publish = false | ||
| edition = "2024" | ||
|
|
||
| [package.metadata] | ||
| cargo-fuzz = true | ||
|
|
||
| [dependencies] | ||
| libfuzzer-sys = "0.4" | ||
| arbitrary = { version = "1", features = ["derive"] } | ||
| fastpfor = { path = "..", features = ["cpp", "rust"] } | ||
|
|
||
| # Prevent this from interfering with workspaces | ||
| [workspace] | ||
| members = ["."] | ||
|
|
||
| [[bin]] | ||
| name = "fastpfor_rust" | ||
| path = "fuzz_targets/fastpfor_rust.rs" | ||
| test = false | ||
| doc = false | ||
|
|
||
| [[bin]] | ||
| name = "fastpfor_cpp" | ||
| path = "fuzz_targets/fastpfor_cpp.rs" | ||
| test = false | ||
| doc = false |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| # Fuzzing FastPFOR | ||
|
|
||
| This directory contains a fuzz test for the FastPFOR compression codec to find bugs, panics, and data corruption issues. | ||
|
|
||
| ## Why Fuzz FastPFOR? | ||
|
|
||
| The FastPFOR codec is the core compression algorithm. Fuzzing helps catch: | ||
| - Data corruption during compress/decompress roundtrips | ||
| - Panics on edge case inputs | ||
| - Buffer overflows or underflows | ||
| - Incorrect handling of different block sizes (128 vs 256) | ||
| - Issues with boundary conditions (empty data, very large values, etc.) | ||
|
|
||
| ## Known Issues Found | ||
|
|
||
| The fuzzer has already discovered the following issues: | ||
|
|
||
| ### Data Loss with Small Inputs (Issue #1) | ||
|
|
||
| **Input:** Single element array `[0]` with block size 128 | ||
|
|
||
| **Issue:** FastPFOR silently drops data that doesn't fit into complete blocks. When the input length is less than the block size (128 or 256), `greatest_multiple(input_length, block_size)` returns 0, causing the codec to: | ||
| 1. Write 0 to the output header | ||
| 2. Skip compression entirely | ||
| 3. On decompression, return 0 elements instead of the original input | ||
|
|
||
| **Expected:** Should either: | ||
| - Compress partial blocks correctly, or | ||
| - Return an error indicating input is too small, or | ||
| - Document this limitation clearly | ||
|
|
||
| This is a **data corruption bug** where the codec claims success but loses data. | ||
|
|
||
| ## Prerequisites | ||
|
|
||
| Install cargo-fuzz and switch to nightly Rust: | ||
|
|
||
| ```bash | ||
| cargo install cargo-fuzz | ||
| rustup install nightly | ||
| ``` | ||
|
|
||
| ## Running the Fuzzer | ||
|
|
||
| ```bash | ||
| cd fuzz | ||
| cargo +nightly fuzz run fastpfor_rust | ||
| ``` | ||
|
|
||
| Run for a specific duration (e.g., 60 seconds): | ||
|
|
||
| ```bash | ||
| cargo +nightly fuzz run fastpfor_rust -- -max_total_time=60 | ||
| ``` | ||
|
CommanderStorm marked this conversation as resolved.
|
||
|
|
||
| ## What It Tests | ||
|
|
||
| The fuzzer: | ||
| 1. Generates random sequences of u32 integers | ||
| 2. Randomly selects block size (128 or 256) | ||
| 3. Compresses the data with FastPFOR | ||
| 4. Decompresses the result | ||
| 5. Verifies the output matches the original input exactly | ||
|
|
||
| This ensures the codec is lossless and doesn't corrupt data under any input pattern. | ||
|
|
||
| ## If a Crash Is Found | ||
|
|
||
| Crashes are saved to `fuzz/artifacts/fastpfor_rust/`. To reproduce: | ||
|
|
||
| ```bash | ||
| cargo +nightly fuzz run fastpfor_rust fuzz/artifacts/fastpfor_rust/crash-<hash> | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,122 @@ | ||
| #![no_main] | ||
|
|
||
| use fastpfor::cpp::*; | ||
| use libfuzzer_sys::fuzz_target; | ||
|
|
||
| type BoxedCodec = Box<dyn Codec32>; | ||
|
|
||
| fuzz_target!(|data: FuzzInput| { | ||
| let codec = BoxedCodec::from(data.codec); | ||
| let input = data.data; | ||
|
|
||
| // Allocate output buffer with generous size | ||
| let mut output = vec![0u32; input.len() * 2 + 1024]; | ||
|
|
||
| // Compress the data | ||
| let enc_slice = codec.encode32(&input, &mut output).unwrap(); | ||
|
|
||
| // Now decompress | ||
| let mut decoded = vec![0u32; input.len() * 2 + 1024]; | ||
| let dec_slice = codec.decode32(&enc_slice, &mut decoded).unwrap(); | ||
|
|
||
| // Verify roundtrip | ||
| if dec_slice.len() + input.len() < 200 { | ||
| assert_eq!(input, dec_slice, "Decompressed output mismatches"); | ||
| } else { | ||
| assert_eq!(dec_slice.len(), input.len(), "Decompressed length mismatch"); | ||
| for (i, (&original, &decoded)) in input.iter().zip(dec_slice.iter()).enumerate() { | ||
| assert_eq!( | ||
| original, decoded, | ||
| "Mismatch at position {i}: expected {original}, got {decoded}" | ||
| ); | ||
| } | ||
| } | ||
| assert_eq!(dec_slice.len(), input.len()); | ||
| }); | ||
|
|
||
| #[derive(arbitrary::Arbitrary, Debug)] | ||
| struct FuzzInput { | ||
| data: Vec<u32>, | ||
| codec: FuzzCodec, | ||
| } | ||
|
|
||
| #[derive(Clone, Copy, Eq, PartialEq, arbitrary::Arbitrary, Debug)] | ||
| enum FuzzCodec { | ||
| BP32Codec, | ||
| CopyCodec, | ||
| FastBinaryPacking8Codec, | ||
| FastPFor128Codec, | ||
| FastPFor256Codec, | ||
| FastBinaryPacking16Codec, | ||
| FastBinaryPacking32Codec, | ||
| MaskedVByteCodec, | ||
| NewPForCodec, | ||
| OptPForCodec, | ||
| PFor2008Codec, | ||
| PForCodec, | ||
| SimdBinaryPackingCodec, | ||
| SimdFastPFor128Codec, | ||
| SimdFastPFor256Codec, | ||
| SimdGroupSimpleCodec, | ||
| SimdGroupSimpleRingBufCodec, | ||
| SimdNewPForCodec, | ||
| SimdOptPForCodec, | ||
| SimdPForCodec, | ||
| SimdSimplePForCodec, | ||
| // Simple16Codec, // cannot encode arbitrary bytes | ||
| // Simple8bCodec, // cannot encode arbitrary bytes | ||
| // Simple8bRleCodec, // cannot encode arbitrary bytes | ||
| // Simple9Codec, // cannot encode arbitrary bytes | ||
| // Simple9RleCodec, // cannot encode arbitrary bytes | ||
| // SimplePForCodec, // cannot encode arbitrary bytes | ||
| // SnappyCodec, // Conditional with #ifdef | ||
| StreamVByteCodec, | ||
| VByteCodec, | ||
| VarIntCodec, | ||
| // VarIntG8iuCodec, // Conditional with #ifdef | ||
| VarIntGbCodec, | ||
| // VsEncodingCodec, // This is leaking memory | ||
| } | ||
|
|
||
| impl From<FuzzCodec> for BoxedCodec { | ||
| fn from(codec: FuzzCodec) -> Self { | ||
| match codec { | ||
| FuzzCodec::BP32Codec => Box::new(BP32Codec::default()), | ||
| FuzzCodec::CopyCodec => Box::new(CopyCodec::default()), | ||
| FuzzCodec::FastBinaryPacking8Codec => Box::new(FastBinaryPacking8Codec::default()), | ||
| FuzzCodec::FastPFor128Codec => Box::new(FastPFor128Codec::default()), | ||
| FuzzCodec::FastPFor256Codec => Box::new(FastPFor256Codec::default()), | ||
| FuzzCodec::FastBinaryPacking16Codec => Box::new(FastBinaryPacking16Codec::default()), | ||
| FuzzCodec::FastBinaryPacking32Codec => Box::new(FastBinaryPacking32Codec::default()), | ||
| FuzzCodec::MaskedVByteCodec => Box::new(MaskedVByteCodec::default()), | ||
| FuzzCodec::NewPForCodec => Box::new(NewPForCodec::default()), | ||
| FuzzCodec::OptPForCodec => Box::new(OptPForCodec::default()), | ||
| FuzzCodec::PFor2008Codec => Box::new(PFor2008Codec::default()), | ||
| FuzzCodec::PForCodec => Box::new(PForCodec::default()), | ||
| FuzzCodec::SimdBinaryPackingCodec => Box::new(SimdBinaryPackingCodec::default()), | ||
| FuzzCodec::SimdFastPFor128Codec => Box::new(SimdFastPFor128Codec::default()), | ||
| FuzzCodec::SimdFastPFor256Codec => Box::new(SimdFastPFor256Codec::default()), | ||
| FuzzCodec::SimdGroupSimpleCodec => Box::new(SimdGroupSimpleCodec::default()), | ||
| FuzzCodec::SimdGroupSimpleRingBufCodec => { | ||
| Box::new(SimdGroupSimpleRingBufCodec::default()) | ||
| } | ||
| FuzzCodec::SimdNewPForCodec => Box::new(SimdNewPForCodec::default()), | ||
| FuzzCodec::SimdOptPForCodec => Box::new(SimdOptPForCodec::default()), | ||
| FuzzCodec::SimdPForCodec => Box::new(SimdPForCodec::default()), | ||
| FuzzCodec::SimdSimplePForCodec => Box::new(SimdSimplePForCodec::default()), | ||
| // FuzzCodec::Simple16Codec => Box::new(Simple16Codec::default()), | ||
| // FuzzCodec::Simple8bCodec => Box::new(Simple8bCodec::default()), | ||
| // FuzzCodec::Simple8bRleCodec => Box::new(Simple8bRleCodec::default()), | ||
| // FuzzCodec::Simple9Codec => Box::new(Simple9Codec::default()), | ||
| // FuzzCodec::Simple9RleCodec => Box::new(Simple9RleCodec::default()), | ||
| // FuzzCodec::SimplePForCodec => Box::new(SimplePForCodec::default()), | ||
| // FuzzCodec::SnappyCodec => Box::new(SnappyCodec::default()), | ||
| FuzzCodec::StreamVByteCodec => Box::new(StreamVByteCodec::default()), | ||
| FuzzCodec::VByteCodec => Box::new(VByteCodec::default()), | ||
| FuzzCodec::VarIntCodec => Box::new(VarIntCodec::default()), | ||
| // FuzzCodec::VarIntG8iuCodec => Box::new(VarIntG8iuCodec::default()), | ||
| FuzzCodec::VarIntGbCodec => Box::new(VarIntGbCodec::default()), | ||
| // FuzzCodec::VsEncodingCodec => Box::new(VsEncodingCodec::default()), | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| #![no_main] | ||
|
|
||
| use std::io::Cursor; | ||
|
|
||
| use fastpfor::rust::{BLOCK_SIZE_256, DEFAULT_PAGE_SIZE, FastPFOR, Integer}; | ||
| use libfuzzer_sys::fuzz_target; | ||
|
|
||
| fuzz_target!(|input_data: Vec<u32>| { | ||
| let mut codec = FastPFOR::new(DEFAULT_PAGE_SIZE, BLOCK_SIZE_256); | ||
|
|
||
|
CommanderStorm marked this conversation as resolved.
|
||
| // Limit input size to avoid timeouts | ||
| let input_data: Vec<u32> = input_data.into_iter().take(10_000).collect(); | ||
|
|
||
| // Allocate output buffer with generous size | ||
| let mut compressed = vec![0u32; input_data.len() * 2 + 1024]; | ||
|
|
||
| // Compress the data | ||
| let mut output_offset = Cursor::new(0); | ||
| codec | ||
| .compress( | ||
| &input_data, | ||
| input_data.len() as u32, | ||
| &mut Cursor::new(0), | ||
| &mut compressed, | ||
| &mut output_offset, | ||
| ) | ||
| .unwrap(); | ||
| let compressed_size = output_offset.position() as u32; | ||
| if !input_data.is_empty() { | ||
| assert!(compressed_size != 0, "compression should not be empty"); | ||
| } | ||
|
|
||
| // Now decompress | ||
| let mut decompressed = vec![0u32; input_data.len()]; | ||
| let mut output_offset = Cursor::new(0); | ||
|
|
||
| codec | ||
| .uncompress( | ||
| &compressed, | ||
| compressed_size, | ||
| &mut Cursor::new(0), | ||
| &mut decompressed, | ||
| &mut output_offset, | ||
| ) | ||
| .unwrap(); | ||
| let decompressed_length = output_offset.position() as usize; | ||
|
|
||
| // Verify roundtrip | ||
| if decompressed_length + input_data.len() < 200 { | ||
| assert_eq!( | ||
| input_data, | ||
| decompressed[..decompressed_length], | ||
| "Decompressed length mismatch: expected {}, got {decompressed_length}", | ||
| input_data.len() | ||
| ); | ||
| } else { | ||
| for (i, (&original, &decoded)) in input_data.iter().zip(decompressed.iter()).enumerate() { | ||
| assert_eq!( | ||
| original, decoded, | ||
| "Mismatch at position {}: expected {}, got {}", | ||
| i, original, decoded | ||
| ); | ||
| } | ||
| } | ||
| assert_eq!(decompressed_length, input_data.len()); | ||
| }); | ||
Uh oh!
There was an error while loading. Please reload this page.