Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,9 @@ jobs:
strategy:
matrix:
include:
- fuzz_target: fastpfor_cpp
- fuzz_target: fastpfor_rust
- fuzz_target: cpp_roundtrip
- fuzz_target: rust_compress_oracle
- fuzz_target: rust_decompress_oracle

steps:
- uses: actions/checkout@v6
Expand Down
14 changes: 10 additions & 4 deletions fuzz/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,19 @@ fastpfor = { path = "..", features = ["cpp", "rust"] }
members = ["."]

[[bin]]
name = "fastpfor_rust"
path = "fuzz_targets/fastpfor_rust.rs"
name = "cpp_roundtrip"
path = "fuzz_targets/cpp_roundtrip.rs"
test = false
doc = false

[[bin]]
name = "fastpfor_cpp"
path = "fuzz_targets/fastpfor_cpp.rs"
name = "rust_compress_oracle"
path = "fuzz_targets/rust_compress_oracle.rs"
test = false
doc = false

[[bin]]
name = "rust_decompress_oracle"
path = "fuzz_targets/rust_decompress_oracle.rs"
test = false
doc = false
62 changes: 25 additions & 37 deletions fuzz/README.md
Original file line number Diff line number Diff line change
@@ -1,36 +1,17 @@
# Fuzzing FastPFOR

This directory contains a fuzz test for the FastPFOR compression codec to find bugs, panics, and data corruption issues.
This directory contains fuzz tests 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
The FastPFOR codec is a core compression algorithm. Fuzzing helps catch:
- Implementation discrepancies between Rust and C++
- Data corruption during compress/decompress cycles
- 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:
Expand All @@ -40,34 +21,41 @@ cargo install cargo-fuzz
rustup install nightly
```

## Running the Fuzzer
## Running the Fuzzers

Run the oracle-based compression fuzzer:

```bash
cd fuzz
cargo +nightly fuzz run fastpfor_rust
cargo +nightly fuzz run rust_compress_oracle
# or
cargo +nightly fuzz run rust_decompress_oracle
# or
cargo +nightly fuzz run rust_roundtrip_oracle
Comment thread
CommanderStorm marked this conversation as resolved.
Outdated
Comment thread
CommanderStorm marked this conversation as resolved.
Outdated
```

Run for a specific duration (e.g., 60 seconds):

```bash
cargo +nightly fuzz run fastpfor_rust -- -max_total_time=60
cargo +nightly fuzz run rust_compress_oracle -- -max_total_time=60
```

## What It Tests
Run with specific number of iterations:

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.
```bash
cargo +nightly fuzz run rust_compress_oracle -- -runs=1000
```

## If a Crash Is Found

Crashes are saved to `fuzz/artifacts/fastpfor_rust/`. To reproduce:
Crashes are saved to `fuzz/artifacts/<target_name>/`. To reproduce:

```bash
cargo +nightly fuzz run <target_name> fuzz/artifacts/<target_name>/crash-<hash>
```

For example:

```bash
cargo +nightly fuzz run fastpfor_rust fuzz/artifacts/fastpfor_rust/crash-<hash>
cargo +nightly fuzz run rust_compress_oracle fuzz/artifacts/rust_compress_oracle/crash-abc123
```
119 changes: 119 additions & 0 deletions fuzz/fuzz_targets/common.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
use fastpfor::cpp;
use fastpfor::rust;

pub type BoxedCppCodec = Box<dyn cpp::Codec32>;

#[derive(arbitrary::Arbitrary)]
pub struct FuzzInput<C> {
pub data: Vec<u32>,
pub codec: C,
}

impl<C: std::fmt::Debug> std::fmt::Debug for FuzzInput<C> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("FuzzInput<C>")
.field("data_length", &self.data.len())
.field("codec", &self.codec)
.finish()
}
}

#[derive(arbitrary::Arbitrary, Clone, Copy, PartialEq, Eq, Debug)]
pub enum RustCodec {
FastPFOR256,
FastPFOR128,
VariableByte,
JustCopy,
}

impl From<RustCodec> for rust::Codec {
fn from(codec: RustCodec) -> Self {
use rust::*;
match codec {
RustCodec::FastPFOR256 => Codec::from(FastPFOR::new(DEFAULT_PAGE_SIZE, BLOCK_SIZE_256)),
RustCodec::FastPFOR128 => Codec::from(FastPFOR::new(DEFAULT_PAGE_SIZE, BLOCK_SIZE_128)),
RustCodec::VariableByte => Codec::from(VariableByte::new()),
RustCodec::JustCopy => Codec::from(JustCopy::new()),
}
}
}

#[derive(Clone, Copy, Eq, PartialEq, arbitrary::Arbitrary, Debug)]
pub enum CppCodec {
BP32,
Copy,
FastBinaryPacking8,
FastPFor128,
FastPFor256,
FastBinaryPacking16,
FastBinaryPacking32,
MaskedVByte,
NewPFor,
OptPFor,
PFor2008,
PFor,
SimdBinaryPacking,
SimdFastPFor128,
SimdFastPFor256,
SimdGroupSimple,
SimdGroupSimpleRingBuf,
SimdNewPFor,
SimdOptPFor,
SimdPFor,
SimdSimplePFor,
// Simple16, // cannot encode arbitrary bytes
// Simple8b, // cannot encode arbitrary bytes
// Simple8bRle, // cannot encode arbitrary bytes
// Simple9, // cannot encode arbitrary bytes
// Simple9Rle, // cannot encode arbitrary bytes
// SimplePFor, // cannot encode arbitrary bytes
// Snappy, // Conditional with #ifdef
StreamVByte,
VByte,
VarInt,
// VarIntG8iu, // Conditional with #ifdef
VarIntGb,
// VsEncoding, // This is leaking memory
}

impl From<CppCodec> for BoxedCppCodec {
fn from(codec: CppCodec) -> Self {
use cpp::*;
match codec {
CppCodec::BP32 => Box::new(BP32Codec::default()),
CppCodec::Copy => Box::new(CopyCodec::default()),
CppCodec::FastBinaryPacking8 => Box::new(FastBinaryPacking8Codec::default()),
CppCodec::FastPFor128 => Box::new(FastPFor128Codec::default()),
CppCodec::FastPFor256 => Box::new(FastPFor256Codec::default()),
CppCodec::FastBinaryPacking16 => Box::new(FastBinaryPacking16Codec::default()),
CppCodec::FastBinaryPacking32 => Box::new(FastBinaryPacking32Codec::default()),
CppCodec::MaskedVByte => Box::new(MaskedVByteCodec::default()),
CppCodec::NewPFor => Box::new(NewPForCodec::default()),
CppCodec::OptPFor => Box::new(OptPForCodec::default()),
CppCodec::PFor2008 => Box::new(PFor2008Codec::default()),
CppCodec::PFor => Box::new(PForCodec::default()),
CppCodec::SimdBinaryPacking => Box::new(SimdBinaryPackingCodec::default()),
CppCodec::SimdFastPFor128 => Box::new(SimdFastPFor128Codec::default()),
CppCodec::SimdFastPFor256 => Box::new(SimdFastPFor256Codec::default()),
CppCodec::SimdGroupSimple => Box::new(SimdGroupSimpleCodec::default()),
CppCodec::SimdGroupSimpleRingBuf => Box::new(SimdGroupSimpleRingBufCodec::default()),
CppCodec::SimdNewPFor => Box::new(SimdNewPForCodec::default()),
CppCodec::SimdOptPFor => Box::new(SimdOptPForCodec::default()),
CppCodec::SimdPFor => Box::new(SimdPForCodec::default()),
CppCodec::SimdSimplePFor => Box::new(SimdSimplePForCodec::default()),
// CppCodec::Simple16 => Box::new(Simple16Codec::default()),
// CppCodec::Simple8b => Box::new(Simple8bCodec::default()),
// CppCodec::Simple8bRle => Box::new(Simple8bRleCodec::default()),
// CppCodec::Simple9 => Box::new(Simple9Codec::default()),
// CppCodec::Simple9Rle => Box::new(Simple9RleCodec::default()),
// CppCodec::SimplePFor => Box::new(SimplePForCodec::default()),
// CppCodec::Snappy => Box::new(SnappyCodec::default()),
CppCodec::StreamVByte => Box::new(StreamVByteCodec::default()),
CppCodec::VByte => Box::new(VByteCodec::default()),
CppCodec::VarInt => Box::new(VarIntCodec::default()),
// CppCodec::VarIntG8iu => Box::new(VarIntG8iuCodec::default()),
CppCodec::VarIntGb => Box::new(VarIntGbCodec::default()),
// CppCodec::VsEncoding => Box::new(VsEncodingCodec::default()),
}
}
}
33 changes: 33 additions & 0 deletions fuzz/fuzz_targets/cpp_roundtrip.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#![no_main]

use libfuzzer_sys::fuzz_target;
mod common;
use common::*;

fuzz_target!(|data: FuzzInput<CppCodec>| {
let codec = BoxedCppCodec::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}"
);
}
}
Comment thread
CommanderStorm marked this conversation as resolved.
});
Loading