From eb37a3e11a748e09a4b3deec72bd4bf21b27b630 Mon Sep 17 00:00:00 2001 From: Arthur Carcano Date: Tue, 12 May 2026 14:44:54 +0000 Subject: [PATCH] Fuzzing for K2VItem Crdt (#1438) Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/1438 Reviewed-by: Alex --- fuzz/Cargo.toml | 9 ++++++- fuzz/fuzz_targets/k2v_item_crdt.rs | 36 +++++++++++++++++++++++++ src/model/k2v/item_table.rs | 43 +++++++++++++++++++++++++----- 3 files changed, 80 insertions(+), 8 deletions(-) create mode 100644 fuzz/fuzz_targets/k2v_item_crdt.rs diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 15385d2f..635a99b4 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -14,7 +14,7 @@ libfuzzer-sys = { workspace = true } garage_db.workspace = true garage_table.workspace = true garage_util.workspace = true -garage_model = { workspace = true, default-features = false, features = ["arbitrary"] } +garage_model = { workspace = true, default-features = false, features = ["arbitrary", "k2v"] } [[bin]] name = "version_crdt" @@ -64,3 +64,10 @@ path = "fuzz_targets/bucket_alias_crdt.rs" test = false doc = false bench = false + +[[bin]] +name = "k2v_item_crdt" +path = "fuzz_targets/k2v_item_crdt.rs" +test = false +doc = false +bench = false diff --git a/fuzz/fuzz_targets/k2v_item_crdt.rs b/fuzz/fuzz_targets/k2v_item_crdt.rs new file mode 100644 index 00000000..9d5b9c18 --- /dev/null +++ b/fuzz/fuzz_targets/k2v_item_crdt.rs @@ -0,0 +1,36 @@ +#![no_main] + +use std::collections::BTreeMap; + +use garage_fuzz::check_crdt_laws; +use garage_model::k2v::item_table::{DvvsEntry, DvvsValue, K2VItem}; +use libfuzzer_sys::fuzz_target; + +// Timestamps are encoded as `(ts << 32) | shift` so that items built with different +// shifts (0, 1, 2) have disjoint timestamp spaces that still interleave in the sorted merge. +fn make(raw: BTreeMap)>, shift: u32) -> K2VItem { + let shift = shift as u64; + let items = raw + .into_iter() + .map(|(node, (t_discard, values))| { + let entry = DvvsEntry::from_raw( + (t_discard as u64) << 32 | shift, + values + .into_iter() + .map(|(ts, v)| ((ts as u64) << 32 | shift, v)) + .collect(), + ); + (node, entry) + }) + .collect(); + K2VItem::with_raw_items(items) +} + +fuzz_target!(|inputs: ( + BTreeMap)>, + BTreeMap)>, + BTreeMap)>, +)| { + let (a, b, c) = inputs; + check_crdt_laws(make(a, 0), make(b, 1), make(c, 2)); +}); diff --git a/src/model/k2v/item_table.rs b/src/model/k2v/item_table.rs index 9378a1d1..0b7706e3 100644 --- a/src/model/k2v/item_table.rs +++ b/src/model/k2v/item_table.rs @@ -45,6 +45,7 @@ mod v08 { } #[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)] + #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] pub enum DvvsValue { Value(#[serde(with = "serde_bytes")] Vec), Deleted, @@ -131,9 +132,26 @@ impl K2VItem { ent.discard(); } } + + pub fn with_raw_items(items: BTreeMap) -> Self { + let mut item = K2VItem { + partition: K2VItemPartition { + bucket_id: [0u8; 32].into(), + partition_key: String::new(), + }, + sort_key: String::new(), + items, + }; + item.discard(); + item + } } impl DvvsEntry { + pub fn from_raw(t_discard: u64, values: Vec<(u64, DvvsValue)>) -> Self { + DvvsEntry { t_discard, values } + } + fn max_time(&self) -> u64 { self.values .iter() @@ -162,15 +180,26 @@ impl Crdt for K2VItem { impl Crdt for DvvsEntry { fn merge(&mut self, other: &Self) { - self.t_discard = std::cmp::max(self.t_discard, other.t_discard); - self.discard(); - - let t_max = self.max_time(); - for (vt, vv) in other.values.iter() { - if *vt > t_max { - self.values.push((*vt, vv.clone())); + let mut slf = std::mem::take(&mut self.values).into_iter().peekable(); + let mut otr = other.values.iter().peekable(); + while let (Some((slf_t, _)), Some((otr_t, _))) = (slf.peek(), otr.peek()) { + match slf_t.cmp(otr_t) { + std::cmp::Ordering::Less => { + self.values.push(slf.next().unwrap()); + } + std::cmp::Ordering::Equal => { + self.values.push(slf.next().unwrap()); + otr.next(); + } + std::cmp::Ordering::Greater => { + self.values.push(otr.next().unwrap().clone()); + } } } + self.values.extend(slf); + self.values.extend(otr.cloned()); + self.t_discard = std::cmp::max(self.t_discard, other.t_discard); + self.discard(); } }