diff --git a/crates/tranquil-store/fuzz/.gitignore b/crates/tranquil-store/fuzz/.gitignore new file mode 100644 index 0000000..0b0c39a --- /dev/null +++ b/crates/tranquil-store/fuzz/.gitignore @@ -0,0 +1,4 @@ +target/ +artifacts/ +coverage/ +Cargo.lock diff --git a/crates/tranquil-store/fuzz/Cargo.toml b/crates/tranquil-store/fuzz/Cargo.toml new file mode 100644 index 0000000..67b93e4 --- /dev/null +++ b/crates/tranquil-store/fuzz/Cargo.toml @@ -0,0 +1,52 @@ +[package] +name = "tranquil-store-fuzz" +version = "0.0.0" +publish = false +edition = "2024" + +[package.metadata] +cargo-fuzz = true + +[dependencies] +libfuzzer-sys = "0.4" +arbitrary = { version = "1", features = ["derive"] } +tranquil-store = { path = "..", features = ["test-harness"] } +tempfile = "3" +tokio = { version = "1", features = ["rt", "time", "macros", "sync"] } + +[[bin]] +name = "decode_block_record" +path = "fuzz_targets/decode_block_record.rs" +test = false +doc = false +bench = false + +[[bin]] +name = "decode_hint_record" +path = "fuzz_targets/decode_hint_record.rs" +test = false +doc = false +bench = false + +[[bin]] +name = "segment_scan" +path = "fuzz_targets/segment_scan.rs" +test = false +doc = false +bench = false + +[[bin]] +name = "metastore_key_codec" +path = "fuzz_targets/metastore_key_codec.rs" +test = false +doc = false +bench = false + +[[bin]] +name = "gauntlet_micro" +path = "fuzz_targets/gauntlet_micro.rs" +test = false +doc = false +bench = false + +[workspace] diff --git a/crates/tranquil-store/fuzz/corpus/decode_block_record/cid_only b/crates/tranquil-store/fuzz/corpus/decode_block_record/cid_only new file mode 100644 index 0000000..5625cce --- /dev/null +++ b/crates/tranquil-store/fuzz/corpus/decode_block_record/cid_only @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/crates/tranquil-store/fuzz/corpus/decode_block_record/zero_cid_truncated b/crates/tranquil-store/fuzz/corpus/decode_block_record/zero_cid_truncated new file mode 100644 index 0000000..5516e8f Binary files /dev/null and b/crates/tranquil-store/fuzz/corpus/decode_block_record/zero_cid_truncated differ diff --git a/crates/tranquil-store/fuzz/corpus/decode_hint_record/truncated b/crates/tranquil-store/fuzz/corpus/decode_hint_record/truncated new file mode 100644 index 0000000..01d633b Binary files /dev/null and b/crates/tranquil-store/fuzz/corpus/decode_hint_record/truncated differ diff --git a/crates/tranquil-store/fuzz/corpus/decode_hint_record/two_zero_records b/crates/tranquil-store/fuzz/corpus/decode_hint_record/two_zero_records new file mode 100644 index 0000000..00bc047 Binary files /dev/null and b/crates/tranquil-store/fuzz/corpus/decode_hint_record/two_zero_records differ diff --git a/crates/tranquil-store/fuzz/corpus/decode_hint_record/zero_record b/crates/tranquil-store/fuzz/corpus/decode_hint_record/zero_record new file mode 100644 index 0000000..9017fd9 Binary files /dev/null and b/crates/tranquil-store/fuzz/corpus/decode_hint_record/zero_record differ diff --git a/crates/tranquil-store/fuzz/corpus/gauntlet_micro/one_op b/crates/tranquil-store/fuzz/corpus/gauntlet_micro/one_op new file mode 100644 index 0000000..51d72fb Binary files /dev/null and b/crates/tranquil-store/fuzz/corpus/gauntlet_micro/one_op differ diff --git a/crates/tranquil-store/fuzz/corpus/gauntlet_micro/two_ops b/crates/tranquil-store/fuzz/corpus/gauntlet_micro/two_ops new file mode 100644 index 0000000..ced6700 Binary files /dev/null and b/crates/tranquil-store/fuzz/corpus/gauntlet_micro/two_ops differ diff --git a/crates/tranquil-store/fuzz/corpus/metastore_key_codec/one_u64_roundtrip b/crates/tranquil-store/fuzz/corpus/metastore_key_codec/one_u64_roundtrip new file mode 100644 index 0000000..7b71163 Binary files /dev/null and b/crates/tranquil-store/fuzz/corpus/metastore_key_codec/one_u64_roundtrip differ diff --git a/crates/tranquil-store/fuzz/corpus/metastore_key_codec/raw_short b/crates/tranquil-store/fuzz/corpus/metastore_key_codec/raw_short new file mode 100644 index 0000000..09f370e Binary files /dev/null and b/crates/tranquil-store/fuzz/corpus/metastore_key_codec/raw_short differ diff --git a/crates/tranquil-store/fuzz/corpus/metastore_key_codec/raw_zeros_32 b/crates/tranquil-store/fuzz/corpus/metastore_key_codec/raw_zeros_32 new file mode 100644 index 0000000..4e4e493 Binary files /dev/null and b/crates/tranquil-store/fuzz/corpus/metastore_key_codec/raw_zeros_32 differ diff --git a/crates/tranquil-store/fuzz/corpus/segment_scan/bad_magic b/crates/tranquil-store/fuzz/corpus/segment_scan/bad_magic new file mode 100644 index 0000000..c5cb6be --- /dev/null +++ b/crates/tranquil-store/fuzz/corpus/segment_scan/bad_magic @@ -0,0 +1 @@ +BADX \ No newline at end of file diff --git a/crates/tranquil-store/fuzz/corpus/segment_scan/header_and_zeros b/crates/tranquil-store/fuzz/corpus/segment_scan/header_and_zeros new file mode 100644 index 0000000..b801da0 Binary files /dev/null and b/crates/tranquil-store/fuzz/corpus/segment_scan/header_and_zeros differ diff --git a/crates/tranquil-store/fuzz/corpus/segment_scan/header_only b/crates/tranquil-store/fuzz/corpus/segment_scan/header_only new file mode 100644 index 0000000..0633631 --- /dev/null +++ b/crates/tranquil-store/fuzz/corpus/segment_scan/header_only @@ -0,0 +1 @@ +TQEV \ No newline at end of file diff --git a/crates/tranquil-store/fuzz/fuzz_targets/decode_block_record.rs b/crates/tranquil-store/fuzz/fuzz_targets/decode_block_record.rs new file mode 100644 index 0000000..6704ff7 --- /dev/null +++ b/crates/tranquil-store/fuzz/fuzz_targets/decode_block_record.rs @@ -0,0 +1,28 @@ +#![no_main] + +use std::path::Path; + +use libfuzzer_sys::fuzz_target; +use tranquil_store::blockstore::{BlockOffset, decode_block_record}; +use tranquil_store::{FaultConfig, OpenOptions, SimulatedIO, StorageIO}; + +fuzz_target!(|data: &[u8]| { + let sim = SimulatedIO::new(0, FaultConfig::none()); + let opts = OpenOptions { + read: true, + write: true, + create: true, + truncate: false, + }; + let fd = match sim.open(Path::new("/fuzz/block.tqb"), opts) { + Ok(fd) => fd, + Err(_) => return, + }; + if !data.is_empty() { + let _ = sim.write_all_at(fd, 0, data); + let _ = sim.sync(fd); + } + let file_size = data.len() as u64; + let _ = decode_block_record(&sim, fd, BlockOffset::new(0), file_size); + let _ = sim.close(fd); +}); diff --git a/crates/tranquil-store/fuzz/fuzz_targets/decode_hint_record.rs b/crates/tranquil-store/fuzz/fuzz_targets/decode_hint_record.rs new file mode 100644 index 0000000..66f67aa --- /dev/null +++ b/crates/tranquil-store/fuzz/fuzz_targets/decode_hint_record.rs @@ -0,0 +1,41 @@ +#![no_main] + +use std::path::Path; + +use libfuzzer_sys::fuzz_target; +use tranquil_store::blockstore::{HintOffset, decode_hint_record}; +use tranquil_store::{FaultConfig, OpenOptions, SimulatedIO, StorageIO}; + +fuzz_target!(|data: &[u8]| { + let sim = SimulatedIO::new(0, FaultConfig::none()); + let opts = OpenOptions { + read: true, + write: true, + create: true, + truncate: false, + }; + let fd = match sim.open(Path::new("/fuzz/hint.tqh"), opts) { + Ok(fd) => fd, + Err(_) => return, + }; + if !data.is_empty() { + let _ = sim.write_all_at(fd, 0, data); + let _ = sim.sync(fd); + } + let file_size = data.len() as u64; + let cursor = std::cell::Cell::new(0u64); + std::iter::from_fn(|| { + if cursor.get() >= file_size { + return None; + } + match decode_hint_record(&sim, fd, HintOffset::new(cursor.get()), file_size) { + Ok(Some(_)) => { + cursor.set(cursor.get() + 64); + Some(()) + } + _ => None, + } + }) + .for_each(|()| {}); + let _ = sim.close(fd); +}); diff --git a/crates/tranquil-store/fuzz/fuzz_targets/gauntlet_micro.rs b/crates/tranquil-store/fuzz/fuzz_targets/gauntlet_micro.rs new file mode 100644 index 0000000..5680477 --- /dev/null +++ b/crates/tranquil-store/fuzz/fuzz_targets/gauntlet_micro.rs @@ -0,0 +1,109 @@ +#![no_main] + +use std::sync::OnceLock; + +use arbitrary::{Arbitrary, Unstructured}; +use libfuzzer_sys::fuzz_target; +use tokio::runtime::Runtime; +use tranquil_store::blockstore::GroupCommitConfig; +use tranquil_store::gauntlet::{ + CollectionName, DidSpaceSize, Gauntlet, GauntletConfig, InvariantSet, IoBackend, KeySpaceSize, + MaxFileSize, Op, OpCount, OpInterval, OpStream, OpWeights, RecordKey, RestartPolicy, + RetentionMaxSecs, RunLimits, Seed, ShardCount, SizeDistribution, StoreConfig, ValueBytes, + ValueSeed, WallMs, WorkloadModel, WriterConcurrency, +}; + +#[derive(Arbitrary, Debug)] +enum FuzzOp { + Add { rkey: u8, value: u16 }, + Delete { rkey: u8 }, + Compact, + Checkpoint, + Read { rkey: u8 }, + ReadBlock { value: u16 }, +} + +const COLLECTION: &str = "app.bsky.feed.post"; +const MAX_OPS: usize = 128; + +fn to_op(fuzz_op: FuzzOp) -> Op { + match fuzz_op { + FuzzOp::Add { rkey, value } => Op::AddRecord { + collection: CollectionName(COLLECTION.to_string()), + rkey: RecordKey(format!("k{rkey:03}")), + value_seed: ValueSeed(u32::from(value)), + }, + FuzzOp::Delete { rkey } => Op::DeleteRecord { + collection: CollectionName(COLLECTION.to_string()), + rkey: RecordKey(format!("k{rkey:03}")), + }, + FuzzOp::Compact => Op::Compact, + FuzzOp::Checkpoint => Op::Checkpoint, + FuzzOp::Read { rkey } => Op::ReadRecord { + collection: CollectionName(COLLECTION.to_string()), + rkey: RecordKey(format!("k{rkey:03}")), + }, + FuzzOp::ReadBlock { value } => Op::ReadBlock { + value_seed: ValueSeed(u32::from(value)), + }, + } +} + +fn tiny_config() -> GauntletConfig { + GauntletConfig { + seed: Seed(0), + io: IoBackend::Real, + workload: WorkloadModel { + weights: OpWeights::default(), + size_distribution: SizeDistribution::Fixed(ValueBytes(64)), + collections: vec![CollectionName(COLLECTION.to_string())], + key_space: KeySpaceSize(256), + did_space: DidSpaceSize(8), + retention_max_secs: RetentionMaxSecs(3600), + }, + op_count: OpCount(0), + invariants: InvariantSet::REFCOUNT_CONSERVATION + | InvariantSet::REACHABILITY + | InvariantSet::READ_AFTER_WRITE, + limits: RunLimits { + max_wall_ms: Some(WallMs(2_000)), + }, + restart_policy: RestartPolicy::EveryNOps(OpInterval(32)), + store: StoreConfig { + max_file_size: MaxFileSize(4096), + group_commit: GroupCommitConfig::default(), + shard_count: ShardCount(1), + }, + eventlog: None, + writer_concurrency: WriterConcurrency(1), + } +} + +fn shared_runtime() -> &'static Runtime { + static RUNTIME: OnceLock = OnceLock::new(); + RUNTIME.get_or_init(|| { + tokio::runtime::Builder::new_current_thread() + .enable_time() + .build() + .expect("build tokio runtime") + }) +} + +fuzz_target!(|data: &[u8]| { + if data.is_empty() { + return; + } + let mut u = Unstructured::new(data); + let ops: Vec = match Vec::::arbitrary(&mut u) { + Ok(ops) => ops.into_iter().take(MAX_OPS).collect(), + Err(_) => return, + }; + if ops.is_empty() { + return; + } + let stream = OpStream::from_vec(ops.into_iter().map(to_op).collect()); + + let cfg = tiny_config(); + let gauntlet = Gauntlet::new(cfg).expect("build gauntlet"); + let _ = shared_runtime().block_on(gauntlet.run_with_ops(stream)); +}); diff --git a/crates/tranquil-store/fuzz/fuzz_targets/metastore_key_codec.rs b/crates/tranquil-store/fuzz/fuzz_targets/metastore_key_codec.rs new file mode 100644 index 0000000..795830f --- /dev/null +++ b/crates/tranquil-store/fuzz/fuzz_targets/metastore_key_codec.rs @@ -0,0 +1,77 @@ +#![no_main] + +use arbitrary::Arbitrary; +use libfuzzer_sys::fuzz_target; +use tranquil_store::metastore::encoding::{KeyBuilder, KeyReader}; + +#[derive(Arbitrary, Debug, PartialEq, Eq)] +enum Field { + U64(u64), + I64(i64), + U32(u32), + U16(u16), + Bool(bool), + Bytes(Vec), + String(String), +} + +fn append(builder: KeyBuilder, field: &Field) -> KeyBuilder { + match field { + Field::U64(v) => builder.u64(*v), + Field::I64(v) => builder.i64(*v), + Field::U32(v) => builder.u32(*v), + Field::U16(v) => builder.u16(*v), + Field::Bool(v) => builder.bool(*v), + Field::Bytes(v) => builder.bytes(v), + Field::String(v) => builder.string(v), + } +} + +fn consume(reader: &mut KeyReader<'_>, field: &Field) -> bool { + match field { + Field::U64(v) => reader.u64() == Some(*v), + Field::I64(v) => reader.i64() == Some(*v), + Field::U32(v) => reader.u32() == Some(*v), + Field::U16(v) => reader.u16() == Some(*v), + Field::Bool(v) => reader.bool() == Some(*v), + Field::Bytes(v) => reader.bytes().as_deref() == Some(v.as_slice()), + Field::String(v) => reader.string().as_deref() == Some(v.as_str()), + } +} + +#[derive(Arbitrary, Debug)] +enum Mode { + Roundtrip(Vec), + Raw(Vec), +} + +fuzz_target!(|mode: Mode| { + match mode { + Mode::Roundtrip(fields) => { + let encoded1 = fields.iter().fold(KeyBuilder::new(), append).build(); + + let mut reader = KeyReader::new(encoded1.as_slice()); + let all_match = fields.iter().all(|f| consume(&mut reader, f)); + assert!(all_match, "roundtrip decode failed"); + assert!(reader.is_empty(), "trailing bytes after decode"); + + let encoded2 = fields.iter().fold(KeyBuilder::new(), append).build(); + assert_eq!( + encoded1.as_slice(), + encoded2.as_slice(), + "encoding not deterministic", + ); + } + Mode::Raw(data) => { + let mut reader = KeyReader::new(&data); + let _ = reader.u64(); + let _ = reader.i64(); + let _ = reader.u32(); + let _ = reader.u16(); + let _ = reader.bool(); + let _ = reader.bytes(); + let _ = reader.string(); + let _ = reader.tag(); + } + } +}); diff --git a/crates/tranquil-store/fuzz/fuzz_targets/segment_scan.rs b/crates/tranquil-store/fuzz/fuzz_targets/segment_scan.rs new file mode 100644 index 0000000..9921fad --- /dev/null +++ b/crates/tranquil-store/fuzz/fuzz_targets/segment_scan.rs @@ -0,0 +1,31 @@ +#![no_main] + +use std::path::Path; + +use libfuzzer_sys::fuzz_target; +use tranquil_store::eventlog::SegmentReader; + +const FUZZ_MAX_PAYLOAD: u32 = 1 << 20; +use tranquil_store::{FaultConfig, OpenOptions, SimulatedIO, StorageIO}; + +fuzz_target!(|data: &[u8]| { + let sim = SimulatedIO::new(0, FaultConfig::none()); + let opts = OpenOptions { + read: true, + write: true, + create: true, + truncate: false, + }; + let fd = match sim.open(Path::new("/fuzz/segment.tqe"), opts) { + Ok(fd) => fd, + Err(_) => return, + }; + if !data.is_empty() { + let _ = sim.write_all_at(fd, 0, data); + let _ = sim.sync(fd); + } + if let Ok(reader) = SegmentReader::open(&sim, fd, FUZZ_MAX_PAYLOAD) { + reader.for_each(|_result| {}); + } + let _ = sim.close(fd); +});