Repository navigation
Add FFI session event log fuzz target - #2005
Conversation
Coverage Report for CI Build 40384290794Coverage remained the same at 87.471%Details
Uncovered ChangesNo uncovered changes found. Coverage RegressionsNo coverage regressions found. Coverage Stats
💛 - Coveralls |
ab13be4 to
e9a1fda
Compare
benalleng
left a comment
There was a problem hiding this comment.
I'd like to see more seeds since they are spliced into this fuzzer so anything e don't list in the seed is pretty much unreacheable. Some examples;
{"GotReplyableError":{"error_code":<n>,"message":"m","extra":{}}}
{"Closed":"Aborted"}
{"Closed":"FallbackBroadcasted"}
{"Closed":{"Success":"0000000000000000000000000000000000000000000000000000000000000000"}} {"Closed":{"Unrecognized":"0000000000000000000000000000000000000000000000000000000000000000"}}
{"Cancelled":[]}
{"Closed":"Aborted"}
perhaps these could be fed as an events.jsonl as a group of events since these are single lines to be fed as a bank of seeds to be spliced in.
| _ => {} | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
I think we should add a type-change mutator.
fn typeflip_leaf(value: &mut Value) {
*value = match value {
Value::Null => Value::Bool(false),
Value::Bool(b) => Value::from(u8::from(*b)),
Value::Number(n) => Value::String(n.to_string()),
Value::String(s) => match s.parse::<u64>() {
Ok(n) => Value::from(n),
Err(_) => s
.parse::<f64>()
.ok()
.and_then(serde_json::Number::from_f64)
.map(Value::Number)
.unwrap_or(Value::Null),
},
_ => Value::Null,
};
}
Also we can add a removal mutation to remove entries entirely.
fn remove_nth_entry(value: &mut Value, n: &mut usize) -> bool {
if as_bytes(value).is_some() {
return false;
}
match value {
Value::Object(map) => {
if *n < map.len() {
let key = map.keys().nth(*n).expect("index below len").clone();
map.remove(&key);
return true;
}
*n -= map.len();
map.values_mut().any(|v| remove_nth_entry(v, n))
}
Value::Array(items) => {
if *n < items.len() {
items.remove(*n);
return true;
}
*n -= items.len();
items.iter_mut().any(|v| remove_nth_entry(v, n))
}
_ => false,
}
}
Bindings persist session events as JSON in host-side storage and replay them through replay_receiver_event_log and replay_sender_event_log on resume, so both the events and their order originate outside Rust. Feed the input as a JSON-lines event log through the FFI JSON persister traits, assert that every accepted event survives a to_json/from_json round trip unchanged, and query the session history of any log that replays. libFuzzer cannot produce a valid PSBT, key or OHTTP config inside JSON on its own, so a custom mutator starts from complete receiver and sender seed sessions and mutates them event by event: hex and byte-array leaves are mutated in decoded form to get past the encoding check, and events are dropped, repeated, reordered or spliced in from the seeds to drive replay through transitions a well-behaved persister never produces.
2f3630f to
2048b64
Compare
|
|
I think too, I rebased and keep failing |
benalleng
left a comment
There was a problem hiding this comment.
The unused dep warnings were a red herring the real problem is the nix flake not being able to properly parse .jsonl files.
You need to add this to the flake.nix
// /flake.nix lines 120
|| (builtins.match ".*fuzz_targets/.*\\.jsonl$" path != null)
2048b64 to
190a31a
Compare
Adds
ffi-session-event-log, the first fuzz target for payjoin-ffi. It feeds a host-provided event log through the FFI JSON persisters and replays it, for both receiver and sender.Bindings persist session events as JSON in host-side storage and hand them back through
JsonReceiverSessionPersister/JsonSenderSessionPersisteron resume. Both the events and their order come from outside Rust, and nothing guarantees the host returns what it saved.What the fuzzer controls
The input is a JSON-lines event log, one event per line, as a persister would return from
load. The same log is replayed as a receiver session and as a sender session.A first version that only ran the input through
from_jsonstalled at 478 coverage counters within a minute: libFuzzer can't build a valid PSBT, key or OHTTP config inside JSON on its own, so almost every input died in serde. A custom mutator fixes that:include_str!, so the target bootstraps from an empty corpus. Every prefix of both seeds replays successfully.u64.Invariants
Beyond not panicking, the target asserts that every event accepted by
ReceiverSessionEvent::from_jsonorSenderSessionEvent::from_jsonsurvives ato_json/from_jsonround trip unchanged, compared through the coreSessionEvent. Otherwise a resumed session would replay a different history than the one saved.What is covered
ReceiverSessionEventandSenderSessionEventfrom_json/to_json,replay_receiver_event_log,replay_sender_event_log, and on a successful replay the session history accessors:pj_uri,pj_param,fallback_txandstatus.Not covered
OhttpKeys::decode,InputPairconstructors,SenderBuilder::new). I plan to cover them in follow ups.Tests
Three unit tests cover the empty input, non-UTF-8 input, and both seed logs. They run with:
cargo test --manifest-path fuzz/Cargo.toml --bin ffi-session-event-logNothing in CI runs the fuzz crate's tests today, so this is a manual check for now.
Fuzzing results
From an empty corpus on x86_64 Linux, 600 seconds, default
max_len:This is a smoke run, not a campaign. Coverage was still growing when it ended. Throughput is about 4500 exec/s.
The full receiver seed is about 9.4 KB, above libFuzzer's default 4096 byte limit, so with default settings the mutator only uses session prefixes and the late receiver states are underexplored. Longer runs should pass
-max_len=16384.Addresses part of #1267.
Assigning @benalleng to review.
Disclosure: co-authored by Claude
Pull Request Checklist
Please confirm the following before requesting review: