test(fuzz): make the harness check what spec §7 says it checks
The fuzz oracle and its proptest twin checked much less than §7 claims (S8). The harness now: - records every frame the node attempts to send, before the transport can refuse it, so the "every frame fits mtu()" check can fail; - reads the MTU from its own u16 (input bytes 1-2), mapped into MIN_MTU..=MAX_FRAME, instead of reusing the first step tag; - reconciles drained drop counts per address: VersionMismatch/Malformed drops equal the frames that fail decode (the overflow slot makes up any difference), IdentityChanged drops never exceed frames whose `from` changed, and nothing is counted beyond the frames received; - checks no addressed observation is stored, or forwarded in an Obs frame, past its lifetime; - checks signatures round-trip from the input (an observation stored from a whole Obs frame keeps exactly its signature, and a stored signature never changes), replacing the input-independent sig_round_trip call; - checks every acked id is stored (F1) and bounds the broadcast ids offered per received summary by SEND_QUEUE_CAP (F9); - has a real adversarial Mode (input byte 0): well-formed Obs, Frag, Summary and Ack frames from 3 x MAX_PEERS addresses, with signed and max-size observations (u16 payload lengths), chosen observers (including the node's own id), ages, lifetimes and addressing, structured fragment streams that conflict, repeat or arrive out of order, replayed summaries, `from` values that change within a contact, and Acks from the peer's real id naming ids the node really sent; - adds the §10 peer-table recovery probe, which the fuzz target runs after every adversarial input. fuzz_twin.rs now runs the fuzz entry point on random bytes, generated adversarial steps, and two rewritten properties that were trivially true: summary_repeat_does_not_resend seeds broadcasts and checks a replay re-sends nothing while the next on-time summary re-sends all; peer_table_recovers_after_flood floods the table, checks it is full, and probes recovery after PEER_IDLE_MS. The recovery tests are #[ignore]d until the S1 idle sweep lands, and the fuzz target fails on the first adversarial input until then. A 180 s nightly run with the probe disabled found nothing else. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,19 +1,16 @@
|
||||
//! cargo-fuzz entry point for the `tines-core` node harness.
|
||||
//!
|
||||
//! Decodes arbitrary bytes into [`fuzz::Step`]s and drives the shared
|
||||
//! [`fuzz::Harness`] — the same oracle the proptest twin in
|
||||
//! `tines-core/tests/fuzz_twin.rs` runs, so any bug caught here is caught there
|
||||
//! too (and vice versa).
|
||||
//! [`fuzz::run_input`] reads the mode and MTU from the first three bytes and
|
||||
//! drives the shared [`fuzz::Harness`] — the same oracle the proptest twin in
|
||||
//! `tines-core/tests/fuzz_twin.rs` runs, so a property checked here is
|
||||
//! checked there too. Adversarial-mode inputs also get the §10 peer-table
|
||||
//! recovery probe.
|
||||
#![no_main]
|
||||
|
||||
libfuzzer_sys::fuzz_target!(|data: &[u8]| {
|
||||
let steps = fuzz::decode_steps(data);
|
||||
// Randomize the MTU per corpus entry so different runs probe different wire
|
||||
// bounds (the §7 "every sent frame ≤ mtu" check then catches clamps that
|
||||
// fail at a specific MTU). First byte seeds it; an empty input falls back
|
||||
// to 128.
|
||||
let mtu = 128usize + (data.first().copied().unwrap_or(0) as usize) % 512;
|
||||
if !steps.is_empty() {
|
||||
fuzz::Harness::new(mtu).run(&steps);
|
||||
if let Some(mut h) = fuzz::run_input(data) {
|
||||
if h.mode() == fuzz::Mode::Adversarial {
|
||||
h.check_peer_table_recovers();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user