Skip to content

Commit 3fe34d7

Browse files
committed
fix: harden diff calc for shallow text histories
1 parent 19f709d commit 3fe34d7

22 files changed

Lines changed: 3379 additions & 377 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/fuzz/fuzz/Cargo.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@ path = "fuzz_targets/all.rs"
3030
test = false
3131
doc = false
3232

33+
[[bin]]
34+
name = "diff_calc"
35+
path = "fuzz_targets/diff_calc.rs"
36+
test = false
37+
doc = false
38+
3339
[[bin]]
3440
name = "gc_fuzz"
3541
path = "fuzz_targets/gc_fuzz.rs"
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
#![no_main]
2+
3+
use fuzz::{
4+
actions::ActionWrapper, test_multi_sites,
5+
test_multi_sites_on_one_doc_with_peer_seed_and_targets, Action, FuzzTarget,
6+
};
7+
use libfuzzer_sys::fuzz_target;
8+
use loro::ContainerType;
9+
10+
fn lca_biased_actions(actions: Vec<Action>) -> Vec<Action> {
11+
let mut biased = Vec::with_capacity(actions.len().saturating_mul(2).min(128));
12+
for (i, action) in actions.into_iter().take(48).enumerate() {
13+
biased.push(action);
14+
15+
let site = ((i * 37) % 251) as u8;
16+
let other = site.wrapping_add(1);
17+
let version = (i as u32).wrapping_mul(97);
18+
let injected = match i % 8 {
19+
0 => Action::Sync {
20+
from: site,
21+
to: other,
22+
},
23+
1 => Action::DiffApply {
24+
from: site,
25+
to: other,
26+
},
27+
2 => Action::Checkout { site, to: version },
28+
3 => Action::ForkAt { site, to: version },
29+
4 => Action::ImportShallow { site, from: other },
30+
5 => Action::ExportShallow { site },
31+
6 => Action::StateOnlyRoundTrip { site },
32+
_ => Action::Commit { site },
33+
};
34+
biased.push(injected);
35+
}
36+
37+
biased
38+
}
39+
40+
fn run_text_diff_calc(actions: Vec<Action>) {
41+
let mut actions = lca_biased_actions(actions);
42+
test_multi_sites(5, vec![FuzzTarget::Text], &mut actions);
43+
}
44+
45+
fn run_one_doc_diff_calc(actions: Vec<Action>) {
46+
let peer_seed = peer_seed_from_actions(&actions);
47+
let mut actions = lca_biased_actions(actions);
48+
test_multi_sites_on_one_doc_with_peer_seed_and_targets(
49+
5,
50+
peer_seed,
51+
vec![ContainerType::Text],
52+
&mut actions,
53+
);
54+
}
55+
56+
fn mix_seed(seed: u64, value: u64) -> u64 {
57+
seed ^ value
58+
.wrapping_add(0x9E37_79B9_7F4A_7C15)
59+
.wrapping_add(seed << 6)
60+
.wrapping_add(seed >> 2)
61+
}
62+
63+
fn peer_seed_from_actions(actions: &[Action]) -> u64 {
64+
let mut seed = mix_seed(0xD1FF_CA1C_7E57_0001, actions.len() as u64);
65+
for action in actions.iter().take(8) {
66+
seed = match action {
67+
Action::Handle {
68+
site,
69+
target,
70+
container,
71+
action,
72+
} => {
73+
let mut seed = mix_seed(seed, 0);
74+
seed = mix_seed(seed, *site as u64);
75+
seed = mix_seed(seed, *target as u64);
76+
seed = mix_seed(seed, *container as u64);
77+
if let ActionWrapper::Generic(g) = action {
78+
seed = mix_seed(seed, g.bool as u64);
79+
seed = mix_seed(seed, g.key as u64);
80+
seed = mix_seed(seed, g.pos as u64);
81+
seed = mix_seed(seed, g.length as u64);
82+
seed = mix_seed(seed, g.prop);
83+
}
84+
seed
85+
}
86+
Action::Checkout { site, to } => mix_seed(mix_seed(seed, 1), ((*site as u64) << 32) | *to as u64),
87+
Action::Undo { site, op_len } => {
88+
mix_seed(mix_seed(seed, 2), ((*site as u64) << 32) | *op_len as u64)
89+
}
90+
Action::SyncAllUndo { site, op_len } => {
91+
mix_seed(mix_seed(seed, 3), ((*site as u64) << 32) | *op_len as u64)
92+
}
93+
Action::Sync { from, to } => {
94+
mix_seed(mix_seed(seed, 4), ((*from as u64) << 8) | *to as u64)
95+
}
96+
Action::SyncAll => mix_seed(seed, 5),
97+
Action::ForkAt { site, to } => {
98+
mix_seed(mix_seed(seed, 6), ((*site as u64) << 32) | *to as u64)
99+
}
100+
Action::DiffApply { from, to } => {
101+
mix_seed(mix_seed(seed, 7), ((*from as u64) << 8) | *to as u64)
102+
}
103+
Action::Query {
104+
site,
105+
target,
106+
query_type,
107+
} => mix_seed(
108+
mix_seed(seed, 8),
109+
((*site as u64) << 16) | ((*target as u64) << 8) | *query_type as u64,
110+
),
111+
Action::ExportShallow { site } => mix_seed(mix_seed(seed, 9), *site as u64),
112+
Action::ImportShallow { site, from } => {
113+
mix_seed(mix_seed(seed, 10), ((*site as u64) << 8) | *from as u64)
114+
}
115+
Action::StateOnlyRoundTrip { site } => mix_seed(mix_seed(seed, 11), *site as u64),
116+
Action::Commit { site } => mix_seed(mix_seed(seed, 12), *site as u64),
117+
Action::SetCommitOptions { site, origin, msg } => mix_seed(
118+
mix_seed(seed, 13),
119+
((*site as u64) << 16) | ((*origin as u64) << 8) | *msg as u64,
120+
),
121+
};
122+
}
123+
seed
124+
}
125+
126+
fuzz_target!(|actions: Vec<Action>| {
127+
if actions.is_empty() {
128+
return;
129+
}
130+
131+
let use_one_doc = match &actions[0] {
132+
Action::Handle { site, .. }
133+
| Action::Checkout { site, .. }
134+
| Action::Undo { site, .. }
135+
| Action::SyncAllUndo { site, .. }
136+
| Action::ForkAt { site, .. }
137+
| Action::Query { site, .. }
138+
| Action::ExportShallow { site }
139+
| Action::ImportShallow { site, .. }
140+
| Action::StateOnlyRoundTrip { site }
141+
| Action::Commit { site }
142+
| Action::SetCommitOptions { site, .. } => site % 2 == 1,
143+
Action::Sync { from, .. } | Action::DiffApply { from, .. } => from % 2 == 1,
144+
Action::SyncAll => false,
145+
};
146+
147+
if use_one_doc {
148+
run_one_doc_diff_calc(actions);
149+
} else {
150+
run_text_diff_calc(actions);
151+
}
152+
});

crates/fuzz/src/container/text.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ impl ActorTrait for TextActor {
8383
.unwrap()
8484
.text
8585
.to_delta();
86-
assert_eq!(value, text_h);
86+
assert_eq!(value, text_h, "peer={}", loro.peer_id());
8787
}
8888

8989
fn add_new_container(&mut self, container: Container) {

crates/fuzz/src/container/tree.rs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -333,7 +333,9 @@ impl Actionable for TreeAction {
333333
peer: before.0,
334334
counter: before.1,
335335
};
336-
super::unwrap(tree.mov_before(target, before));
336+
if let Err(LoroError::TreeError(e)) = tree.mov_before(target, before) {
337+
tracing::warn!("move before error {}", e);
338+
}
337339
None
338340
}
339341
TreeActionInner::MoveAfter { target, after } => {
@@ -345,7 +347,9 @@ impl Actionable for TreeAction {
345347
peer: after.0,
346348
counter: after.1,
347349
};
348-
super::unwrap(tree.mov_after(target, after));
350+
if let Err(LoroError::TreeError(e)) = tree.mov_after(target, after) {
351+
tracing::warn!("move after error {}", e);
352+
}
349353
None
350354
}
351355
TreeActionInner::Meta { meta: (k, v) } => {
@@ -362,7 +366,7 @@ impl Actionable for TreeAction {
362366
}
363367
TreeActionInner::MetaDelete { key } => {
364368
let meta = super::unwrap(tree.get_meta(target))?;
365-
meta.delete(key);
369+
let _ = meta.delete(key);
366370
None
367371
}
368372
TreeActionInner::MetaClear => {

crates/fuzz/src/crdt_fuzzer.rs

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,11 @@ impl CRDTFuzzer {
264264
let a_frontiers = a.loro.oplog_frontiers();
265265
let b_frontiers = b.loro.oplog_frontiers();
266266
if let Ok(diff) = a.loro.diff(&a_frontiers, &b_frontiers) {
267-
let _ = b.loro.apply_diff(diff);
267+
let before_apply = b.loro.state_frontiers();
268+
let result = b.loro.apply_diff(diff);
269+
if result.is_ok() || b.loro.state_frontiers() != before_apply {
270+
b.loro.commit();
271+
}
268272
}
269273
}
270274
Action::Query {
@@ -427,7 +431,18 @@ impl CRDTFuzzer {
427431
if let Ok(bytes) = actor.loro.export(loro::ExportMode::state_only(Some(&f))) {
428432
let new_doc = LoroDoc::new();
429433
if new_doc.import(&bytes).is_ok() {
430-
assert_eq!(new_doc.get_deep_value(), actor.loro.get_deep_value());
434+
assert_eq!(
435+
new_doc.get_deep_value(),
436+
actor.loro.get_deep_value(),
437+
"site={site} state_frontiers={:?} oplog_frontiers={:?} oplog_vv={:?} imported_frontiers={:?} imported_vv={:?} shallow_frontiers={:?} shallow_vv={:?}",
438+
actor.loro.state_frontiers(),
439+
actor.loro.oplog_frontiers(),
440+
actor.loro.oplog_vv(),
441+
new_doc.oplog_frontiers(),
442+
new_doc.oplog_vv(),
443+
new_doc.shallow_since_frontiers(),
444+
new_doc.shallow_since_vv(),
445+
);
431446
}
432447
}
433448
}
@@ -463,8 +478,8 @@ impl CRDTFuzzer {
463478
if a_shallow || b_shallow {
464479
continue;
465480
}
466-
let a_doc = &mut a.loro;
467-
let b_doc = &mut b.loro;
481+
let a_doc = &a.loro;
482+
let b_doc = &b.loro;
468483
info_span!("Attach", peer = i).in_scope(|| {
469484
a_doc.attach();
470485
});

crates/fuzz/src/lib.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,7 @@ pub use mem_kv_fuzzer::{
1515
minify_simple as kv_minify_simple, test_mem_kv_fuzzer, test_random_bytes_import,
1616
Action as KVAction,
1717
};
18-
pub use one_doc_fuzzer::test_multi_sites_on_one_doc;
18+
pub use one_doc_fuzzer::{
19+
test_multi_sites_on_one_doc, test_multi_sites_on_one_doc_with_peer_seed,
20+
test_multi_sites_on_one_doc_with_peer_seed_and_targets,
21+
};

0 commit comments

Comments
 (0)