From defaac1b4f4710d202b97030c41e6b544c47a759 Mon Sep 17 00:00:00 2001 From: Arthur Carcano Date: Mon, 4 May 2026 17:07:35 +0200 Subject: [PATCH 1/5] Use PartialEq instead of crdt_state --- fuzz/fuzz_targets/mpu_crdt.rs | 26 +++++--------------------- fuzz/fuzz_targets/version_crdt.rs | 26 +++++--------------------- 2 files changed, 10 insertions(+), 42 deletions(-) diff --git a/fuzz/fuzz_targets/mpu_crdt.rs b/fuzz/fuzz_targets/mpu_crdt.rs index 82d6b613..3051f2f4 100644 --- a/fuzz/fuzz_targets/mpu_crdt.rs +++ b/fuzz/fuzz_targets/mpu_crdt.rs @@ -27,10 +27,6 @@ fn make_mpu(deleted: bool, parts: Vec<(MpuPartKey, MpuPart)>) -> MultipartUpload mpu } -fn crdt_state(mpu: &MultipartUpload) -> (bool, &[(MpuPartKey, MpuPart)]) { - (mpu.deleted.get(), mpu.parts.items()) -} - fuzz_target!(|inputs: ( (bool, Vec<(MpuPartKey, MpuPart)>), (bool, Vec<(MpuPartKey, MpuPart)>), @@ -45,14 +41,10 @@ fuzz_target!(|inputs: ( { let mut a2 = a.clone(); a2.merge(&a.clone()); - assert_eq!( - crdt_state(&a2), - crdt_state(&a), - "merge is not idempotent: {a2:#?} != {a:#?}" - ); + assert_eq!(a2, a, "merge is not idempotent: {a2:#?} != {a:#?}"); } - // Commutativity: crdt_state(merge(a, b)) == crdt_state(merge(b, a)) + // Commutativity: merge(a, b) == merge(b, a) let ab = { let mut t = a.clone(); t.merge(&b); @@ -63,13 +55,9 @@ fuzz_target!(|inputs: ( t.merge(&a); t }; - assert_eq!( - crdt_state(&ab), - crdt_state(&ba), - "merge is not commutative: {ab:#?} != {ba:#?}" - ); + assert_eq!(ab, ba, "merge is not commutative: {ab:#?} != {ba:#?}"); - // Associativity: crdt_state(merge(merge(a, b), c)) == crdt_state(merge(a, merge(b, c))) + // Associativity: merge(merge(a, b), c) == merge(a, merge(b, c)) let ab_c = { let mut t = ab.clone(); t.merge(&c); @@ -85,9 +73,5 @@ fuzz_target!(|inputs: ( t.merge(&bc); t }; - assert_eq!( - crdt_state(&ab_c), - crdt_state(&a_bc), - "merge is not associative: {ab_c:#?} != {a_bc:#?}" - ); + assert_eq!(ab_c, a_bc, "merge is not associative: {ab_c:#?} != {a_bc:#?}"); }); diff --git a/fuzz/fuzz_targets/version_crdt.rs b/fuzz/fuzz_targets/version_crdt.rs index 4be47666..87fc7368 100644 --- a/fuzz/fuzz_targets/version_crdt.rs +++ b/fuzz/fuzz_targets/version_crdt.rs @@ -28,10 +28,6 @@ fn make_version(deleted: bool, mut blocks: Vec<(VersionBlockKey, VersionBlock)>) v } -fn crdt_state(v: &Version) -> (bool, &[(VersionBlockKey, VersionBlock)]) { - (v.deleted.get(), v.blocks.items()) -} - fuzz_target!(|inputs: ( (bool, Vec<(VersionBlockKey, VersionBlock)>), (bool, Vec<(VersionBlockKey, VersionBlock)>), @@ -46,14 +42,10 @@ fuzz_target!(|inputs: ( { let mut a2 = a.clone(); a2.merge(&a.clone()); - assert_eq!( - crdt_state(&a2), - crdt_state(&a), - "merge is not idempotent: {a2:#?} != {a:#?}" - ); + assert_eq!(a2, a, "merge is not idempotent: {a2:#?} != {a:#?}"); } - // Commutativity: crdt_state(merge(a, b)) == crdt_state(merge(b, a)) + // Commutativity: merge(a, b) == merge(b, a) let ab = { let mut t = a.clone(); t.merge(&b); @@ -64,13 +56,9 @@ fuzz_target!(|inputs: ( t.merge(&a); t }; - assert_eq!( - crdt_state(&ab), - crdt_state(&ba), - "merge is not commutative: {ab:#?} != {ba:#?}" - ); + assert_eq!(ab, ba, "merge is not commutative: {ab:#?} != {ba:#?}"); - // Associativity: crdt_state(merge(merge(a, b), c)) == crdt_state(merge(a, merge(b, c))) + // Associativity: merge(merge(a, b), c) == merge(a, merge(b, c)) let ab_c = { let mut t = ab.clone(); t.merge(&c); @@ -86,9 +74,5 @@ fuzz_target!(|inputs: ( t.merge(&bc); t }; - assert_eq!( - crdt_state(&ab_c), - crdt_state(&a_bc), - "merge is not associative: {ab_c:#?} != {a_bc:#?}" - ); + assert_eq!(ab_c, a_bc, "merge is not associative: {ab_c:#?} != {a_bc:#?}"); }); From e996f34887ebc9045e7d5b329ddc3ab5e0632c64 Mon Sep 17 00:00:00 2001 From: Arthur Carcano Date: Mon, 4 May 2026 17:23:31 +0200 Subject: [PATCH 2/5] Factor the crdt test code --- fuzz/fuzz_targets/mpu_crdt.rs | 44 ++---------------------------- fuzz/fuzz_targets/version_crdt.rs | 44 ++---------------------------- fuzz/src/lib.rs | 45 +++++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 84 deletions(-) create mode 100644 fuzz/src/lib.rs diff --git a/fuzz/fuzz_targets/mpu_crdt.rs b/fuzz/fuzz_targets/mpu_crdt.rs index 3051f2f4..86eb9900 100644 --- a/fuzz/fuzz_targets/mpu_crdt.rs +++ b/fuzz/fuzz_targets/mpu_crdt.rs @@ -1,7 +1,7 @@ #![no_main] +use garage_fuzz::check_crdt_laws; use garage_model::s3::mpu_table::{MpuPart, MpuPartKey, MultipartUpload}; -use garage_table::crdt::Crdt; use libfuzzer_sys::fuzz_target; /// Build a MultipartUpload from an arbitrary deleted flag and parts list, using a fixed @@ -33,45 +33,5 @@ fuzz_target!(|inputs: ( (bool, Vec<(MpuPartKey, MpuPart)>) )| { let ((d1, p1), (d2, p2), (d3, p3)) = inputs; - let a = make_mpu(d1, p1); - let b = make_mpu(d2, p2); - let c = make_mpu(d3, p3); - - // Idempotency: merge(a, a) == a - { - let mut a2 = a.clone(); - a2.merge(&a.clone()); - assert_eq!(a2, a, "merge is not idempotent: {a2:#?} != {a:#?}"); - } - - // Commutativity: merge(a, b) == merge(b, a) - let ab = { - let mut t = a.clone(); - t.merge(&b); - t - }; - let ba = { - let mut t = b.clone(); - t.merge(&a); - t - }; - assert_eq!(ab, ba, "merge is not commutative: {ab:#?} != {ba:#?}"); - - // Associativity: merge(merge(a, b), c) == merge(a, merge(b, c)) - let ab_c = { - let mut t = ab.clone(); - t.merge(&c); - t - }; - let bc = { - let mut t = b.clone(); - t.merge(&c); - t - }; - let a_bc = { - let mut t = a.clone(); - t.merge(&bc); - t - }; - assert_eq!(ab_c, a_bc, "merge is not associative: {ab_c:#?} != {a_bc:#?}"); + check_crdt_laws(make_mpu(d1, p1), make_mpu(d2, p2), make_mpu(d3, p3)); }); diff --git a/fuzz/fuzz_targets/version_crdt.rs b/fuzz/fuzz_targets/version_crdt.rs index 87fc7368..b14c6120 100644 --- a/fuzz/fuzz_targets/version_crdt.rs +++ b/fuzz/fuzz_targets/version_crdt.rs @@ -1,7 +1,7 @@ #![no_main] +use garage_fuzz::check_crdt_laws; use garage_model::s3::version_table::{Version, VersionBacklink, VersionBlock, VersionBlockKey}; -use garage_table::crdt::Crdt; use libfuzzer_sys::fuzz_target; /// Build a Version from an arbitrary deleted flag and block list, using a fixed uuid/backlink @@ -34,45 +34,5 @@ fuzz_target!(|inputs: ( (bool, Vec<(VersionBlockKey, VersionBlock)>) )| { let ((d1, b1), (d2, b2), (d3, b3)) = inputs; - let a = make_version(d1, b1); - let b = make_version(d2, b2); - let c = make_version(d3, b3); - - // Idempotency: merge(a, a) == a - { - let mut a2 = a.clone(); - a2.merge(&a.clone()); - assert_eq!(a2, a, "merge is not idempotent: {a2:#?} != {a:#?}"); - } - - // Commutativity: merge(a, b) == merge(b, a) - let ab = { - let mut t = a.clone(); - t.merge(&b); - t - }; - let ba = { - let mut t = b.clone(); - t.merge(&a); - t - }; - assert_eq!(ab, ba, "merge is not commutative: {ab:#?} != {ba:#?}"); - - // Associativity: merge(merge(a, b), c) == merge(a, merge(b, c)) - let ab_c = { - let mut t = ab.clone(); - t.merge(&c); - t - }; - let bc = { - let mut t = b.clone(); - t.merge(&c); - t - }; - let a_bc = { - let mut t = a.clone(); - t.merge(&bc); - t - }; - assert_eq!(ab_c, a_bc, "merge is not associative: {ab_c:#?} != {a_bc:#?}"); + check_crdt_laws(make_version(d1, b1), make_version(d2, b2), make_version(d3, b3)); }); diff --git a/fuzz/src/lib.rs b/fuzz/src/lib.rs new file mode 100644 index 00000000..1715d1e9 --- /dev/null +++ b/fuzz/src/lib.rs @@ -0,0 +1,45 @@ +use garage_table::crdt::Crdt; +use std::fmt::Debug; + +pub fn check_crdt_laws(a: T, b: T, c: T) +where + T: Crdt + PartialEq + Clone + Debug, +{ + // Idempotency: merge(a, a) == a + { + let mut a2 = a.clone(); + a2.merge(&a.clone()); + assert_eq!(a2, a, "merge is not idempotent: {a2:#?} != {a:#?}"); + } + + // Commutativity: merge(a, b) == merge(b, a) + let ab = { + let mut t = a.clone(); + t.merge(&b); + t + }; + let ba = { + let mut t = b.clone(); + t.merge(&a); + t + }; + assert_eq!(ab, ba, "merge is not commutative: {ab:#?} != {ba:#?}"); + + // Associativity: merge(merge(a, b), c) == merge(a, merge(b, c)) + let ab_c = { + let mut t = ab.clone(); + t.merge(&c); + t + }; + let bc = { + let mut t = b.clone(); + t.merge(&c); + t + }; + let a_bc = { + let mut t = a.clone(); + t.merge(&bc); + t + }; + assert_eq!(ab_c, a_bc, "merge is not associative: {ab_c:#?} != {a_bc:#?}"); +} From 28a75d723455c3fab90c4a1720802b3e968e64a3 Mon Sep 17 00:00:00 2001 From: Arthur Carcano Date: Mon, 4 May 2026 17:23:59 +0200 Subject: [PATCH 3/5] Add LXs corrolary --- fuzz/src/lib.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/fuzz/src/lib.rs b/fuzz/src/lib.rs index 1715d1e9..6abd894b 100644 --- a/fuzz/src/lib.rs +++ b/fuzz/src/lib.rs @@ -25,6 +25,14 @@ where }; assert_eq!(ab, ba, "merge is not commutative: {ab:#?} != {ba:#?}"); + // LX's corrolary: merge(merge(a,b),b) = merge(a,b) + let ab_b = { + let mut t = ab.clone(); + t.merge(&b); + t + }; + assert_eq!(ab,ab_b); + // Associativity: merge(merge(a, b), c) == merge(a, merge(b, c)) let ab_c = { let mut t = ab.clone(); From 382981642d24fa9b65b5a928d21f18dccda19663 Mon Sep 17 00:00:00 2001 From: Arthur Carcano Date: Mon, 4 May 2026 17:26:13 +0200 Subject: [PATCH 4/5] Remove uneeded clones --- fuzz/src/lib.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/fuzz/src/lib.rs b/fuzz/src/lib.rs index 6abd894b..4405c511 100644 --- a/fuzz/src/lib.rs +++ b/fuzz/src/lib.rs @@ -8,7 +8,7 @@ where // Idempotency: merge(a, a) == a { let mut a2 = a.clone(); - a2.merge(&a.clone()); + a2.merge(&a); assert_eq!(a2, a, "merge is not idempotent: {a2:#?} != {a:#?}"); } @@ -35,17 +35,17 @@ where // Associativity: merge(merge(a, b), c) == merge(a, merge(b, c)) let ab_c = { - let mut t = ab.clone(); + let mut t = ab; t.merge(&c); t }; let bc = { - let mut t = b.clone(); + let mut t = b; t.merge(&c); t }; let a_bc = { - let mut t = a.clone(); + let mut t = a; t.merge(&bc); t }; From 0eb7d61829376100c7254108d5c728bd91f3413b Mon Sep 17 00:00:00 2001 From: Arthur Carcano Date: Tue, 5 May 2026 10:41:21 +0200 Subject: [PATCH 5/5] cargo fmt --- fuzz/fuzz_targets/version_crdt.rs | 6 +++++- fuzz/src/lib.rs | 7 +++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/fuzz/fuzz_targets/version_crdt.rs b/fuzz/fuzz_targets/version_crdt.rs index b14c6120..cb0649bc 100644 --- a/fuzz/fuzz_targets/version_crdt.rs +++ b/fuzz/fuzz_targets/version_crdt.rs @@ -34,5 +34,9 @@ fuzz_target!(|inputs: ( (bool, Vec<(VersionBlockKey, VersionBlock)>) )| { let ((d1, b1), (d2, b2), (d3, b3)) = inputs; - check_crdt_laws(make_version(d1, b1), make_version(d2, b2), make_version(d3, b3)); + check_crdt_laws( + make_version(d1, b1), + make_version(d2, b2), + make_version(d3, b3), + ); }); diff --git a/fuzz/src/lib.rs b/fuzz/src/lib.rs index 4405c511..288ccced 100644 --- a/fuzz/src/lib.rs +++ b/fuzz/src/lib.rs @@ -31,7 +31,7 @@ where t.merge(&b); t }; - assert_eq!(ab,ab_b); + assert_eq!(ab, ab_b); // Associativity: merge(merge(a, b), c) == merge(a, merge(b, c)) let ab_c = { @@ -49,5 +49,8 @@ where t.merge(&bc); t }; - assert_eq!(ab_c, a_bc, "merge is not associative: {ab_c:#?} != {a_bc:#?}"); + assert_eq!( + ab_c, a_bc, + "merge is not associative: {ab_c:#?} != {a_bc:#?}" + ); }