From 2770b9b14aae2a2cf1c77d302246423d98f5734a Mon Sep 17 00:00:00 2001 From: Lewis Date: Thu, 23 Apr 2026 22:30:13 +0300 Subject: [PATCH] feat(tranquil-store): cargo-fuzz targets with asan+ubsan Lewis: May this revision serve well! --- crates/tranquil-store/fuzz/.gitignore | 4 + crates/tranquil-store/fuzz/Cargo.toml | 52 +++++++++ .../fuzz/corpus/decode_block_record/cid_only | 1 + .../decode_block_record/zero_cid_truncated | Bin 0 -> 36 bytes .../fuzz/corpus/decode_hint_record/truncated | Bin 0 -> 16 bytes .../decode_hint_record/two_zero_records | Bin 0 -> 128 bytes .../corpus/decode_hint_record/zero_record | Bin 0 -> 64 bytes .../fuzz/corpus/gauntlet_micro/one_op | Bin 0 -> 5 bytes .../fuzz/corpus/gauntlet_micro/two_ops | Bin 0 -> 9 bytes .../metastore_key_codec/one_u64_roundtrip | Bin 0 -> 10 bytes .../fuzz/corpus/metastore_key_codec/raw_short | Bin 0 -> 2 bytes .../corpus/metastore_key_codec/raw_zeros_32 | Bin 0 -> 32 bytes .../fuzz/corpus/segment_scan/bad_magic | 1 + .../fuzz/corpus/segment_scan/header_and_zeros | Bin 0 -> 25 bytes .../fuzz/corpus/segment_scan/header_only | 1 + .../fuzz/fuzz_targets/decode_block_record.rs | 28 +++++ .../fuzz/fuzz_targets/decode_hint_record.rs | 41 +++++++ .../fuzz/fuzz_targets/gauntlet_micro.rs | 109 ++++++++++++++++++ .../fuzz/fuzz_targets/metastore_key_codec.rs | 77 +++++++++++++ .../fuzz/fuzz_targets/segment_scan.rs | 31 +++++ 20 files changed, 345 insertions(+) create mode 100644 crates/tranquil-store/fuzz/.gitignore create mode 100644 crates/tranquil-store/fuzz/Cargo.toml create mode 100644 crates/tranquil-store/fuzz/corpus/decode_block_record/cid_only create mode 100644 crates/tranquil-store/fuzz/corpus/decode_block_record/zero_cid_truncated create mode 100644 crates/tranquil-store/fuzz/corpus/decode_hint_record/truncated create mode 100644 crates/tranquil-store/fuzz/corpus/decode_hint_record/two_zero_records create mode 100644 crates/tranquil-store/fuzz/corpus/decode_hint_record/zero_record create mode 100644 crates/tranquil-store/fuzz/corpus/gauntlet_micro/one_op create mode 100644 crates/tranquil-store/fuzz/corpus/gauntlet_micro/two_ops create mode 100644 crates/tranquil-store/fuzz/corpus/metastore_key_codec/one_u64_roundtrip create mode 100644 crates/tranquil-store/fuzz/corpus/metastore_key_codec/raw_short create mode 100644 crates/tranquil-store/fuzz/corpus/metastore_key_codec/raw_zeros_32 create mode 100644 crates/tranquil-store/fuzz/corpus/segment_scan/bad_magic create mode 100644 crates/tranquil-store/fuzz/corpus/segment_scan/header_and_zeros create mode 100644 crates/tranquil-store/fuzz/corpus/segment_scan/header_only create mode 100644 crates/tranquil-store/fuzz/fuzz_targets/decode_block_record.rs create mode 100644 crates/tranquil-store/fuzz/fuzz_targets/decode_hint_record.rs create mode 100644 crates/tranquil-store/fuzz/fuzz_targets/gauntlet_micro.rs create mode 100644 crates/tranquil-store/fuzz/fuzz_targets/metastore_key_codec.rs create mode 100644 crates/tranquil-store/fuzz/fuzz_targets/segment_scan.rs 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 0000000000000000000000000000000000000000..5516e8f98aadc0ee2eed55e93388d53f5d9b7df3 GIT binary patch literal 36 KcmZQzAPfKiBme;b literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..01d633b27e8ea9b17084fc911d0c8cc43a4170a9 GIT binary patch literal 16 KcmZQzKm`B*5C8!H literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..00bc0474794e909101dcb2b0c7c809cfbe2bd665 GIT binary patch literal 128 LcmZQz7+3%R0Du4i literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..9017fd98b5f67d928cc64c59b2c025472ce74f8c GIT binary patch literal 64 LcmZQzpbP*206+i% literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..51d72fb29fa4ae1f23b57813624cfc74777a7abe GIT binary patch literal 5 KcmZQ%00IC23IGBC literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..ced67008626417873d44fa3d4a439a6a6a226493 GIT binary patch literal 9 OcmZQ#00KrvMg{-?Bmf2g literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..7b711639cbfe37165e79e7c731ed573d9d419e49 GIT binary patch literal 10 LcmZQ%fC5GU02KfO literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..09f370e38f498a462e1ca0faa724559b6630c04f GIT binary patch literal 2 JcmZQz0000200961 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..4e4e4935707a596987ec1cc32e3d0d587dbe4f04 GIT binary patch literal 32 KcmZQzzz+ZbAOHaX literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..b801da004bee3cb1666122d49d3d5b3a40bb5281 GIT binary patch literal 25 OcmWFubPZ! 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); +});