fuzz: wave 2 — session_pumps stateful two-pump target
The wave-2 stateful target from docs/research/fuzzing.md 3.3: op sequences (arbitrary SessionSequence) over one live channels stack per exec - MuxRunner -> raw chunk stream -> the real run_demux_loop_for_client demux -> ChannelManager per side (producer Connect / consumer Accept over one duplex transport, the reverse POC's topology). The harness mirrors TunnelSession's data-plane shapes: datagram planes (codec reader + pending, the DatagramHalves mirror) and stream planes (pump_bidi against in-process peer halves, the pump_against mirror). Invariants (3.3): no-panic/no-wedge (5s reconcile join); two-pump shutdown-on-completion; lossless p2c copy count; EOF-sentinel teardown semantics (drain-before-EOF, map empties); FIFO datagram reconciliation through the real demux path (stateful framing walk, the wave-1 walk carried across ops); the 1.3 DatagramHalves.pending backlog bound (<= routed-but-unpulled remainder); decoder-state retained-bytes bound re-asserted per op. Harness-model findings (all six wave findings are harness defects, not crate bugs - the fuzzer-as-harness-checker pattern): - duplicate adopt is a legal ChannelExists no-op (documented table) - the p2c-EOF pump join is the DESIGNED half-close shape; the pump completes once the c2p source EOFs too (teardown), not before - route_payload routes into the LOCAL manager (demux direction); the producer pump writes the channel send half - the first campaign's bytes never reached the consumer; rewritten to the real protocol shape (Open allocates, session-index addressing, stateful framing model, Pull op, EOF sentinels only from teardown/sender-drop) - clear_all's return is the opener-ledger records, NOT the reaped ids (adopted channels carry no ledger entry - the alkcall 7.7 lesson re-learned); the map-empties assert is the ground truth Campaign (10-min detached, 6 runner): 228,676 execs, 32 fork jobs, oom/timeout/crash 0/0/0, exit 0. Coverage 3427 edges / 17.5k features still expanding at budget end. No crate bugs found - the two-pump contract, EOF-sentinel semantics, and the pending backlog bound are demonstrated properties. Crate addition: TunnelSession::datagram_backlog() (the DatagramHalves.pending diagnostic the 3.3 bound asserts against, in the retained_bytes() style) + a suite test pinning its rest states. Verification: main suite 136 passed; corpus replay (8 committed seeds + 2 semantic fixtures) green; clippy -D warnings + fmt clean (main, shared, wasm); wasm check clean; cargo fuzz build clean.
This commit is contained in:
1 parent
57285d23bd
commit
e3c6f6c9f8
20 files changed
+1045
-21
No files matched your search
@@ -188,6 +188,71 @@ async fn datagram_round_trip_including_empty() {
|
||||
assert!(only_channel_0(&topo));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn datagram_backlog_tracks_pending_and_drains_fifo() {
|
||||
// The §1.3/§3.3 session-shape diagnostic: `datagram_backlog()` is
|
||||
// the `DatagramHalves.pending` count — None for stream sessions,
|
||||
// 0 at rest, batched arrivals queue FIFO, and a drained queue
|
||||
// reads 0 (the wave-2 harness asserts the bound against it).
|
||||
let registry = ResourceRegistry::new();
|
||||
registry.register("dns", Substrate::Udp, "in-process").await;
|
||||
registry
|
||||
.register("echo", Substrate::Tcp, "in-process")
|
||||
.await;
|
||||
let topo = wire_forward(registry, all_substrate_dial()).await;
|
||||
|
||||
let mut session = TunnelSession::open(&topo.consumer, params("dns", Substrate::Udp))
|
||||
.await
|
||||
.expect("udp open");
|
||||
assert_eq!(
|
||||
session.datagram_backlog(),
|
||||
Some(0),
|
||||
"a fresh datagram session has an empty backlog"
|
||||
);
|
||||
let stream_session = TunnelSession::open(&topo.consumer, params("dns", Substrate::Tcp)).await;
|
||||
// This registry entry maps (dns, Tcp) nowhere; open fails typed.
|
||||
assert!(stream_session.is_err(), "unproduced substrate rejects");
|
||||
let stream_session = TunnelSession::open(&topo.consumer, params("echo", Substrate::Tcp))
|
||||
.await
|
||||
.expect("tcp open");
|
||||
assert_eq!(
|
||||
stream_session.datagram_backlog(),
|
||||
None,
|
||||
"stream sessions carry no datagram backlog"
|
||||
);
|
||||
stream_session.close().await;
|
||||
|
||||
// Backlog drain posture: two echoed datagrams land back-to-back;
|
||||
// whether the echo batches them into one chunk is timing-shaped,
|
||||
// so the suite pins only the rest state (empty after drains) —
|
||||
// the batched-arrival bound is the wave-2 harness's (§3.3).
|
||||
session.send_datagram(b"first").await.expect("send");
|
||||
session.send_datagram(b"second").await.expect("send");
|
||||
let dg1 = tokio::time::timeout(std::time::Duration::from_secs(5), session.recv_datagram())
|
||||
.await
|
||||
.expect("recv timed out")
|
||||
.expect("no io error")
|
||||
.expect("first datagram");
|
||||
let dg2 = tokio::time::timeout(std::time::Duration::from_secs(5), session.recv_datagram())
|
||||
.await
|
||||
.expect("recv timed out")
|
||||
.expect("no io error")
|
||||
.expect("second datagram");
|
||||
let mut got = [&dg1[..], &dg2[..]];
|
||||
got.sort();
|
||||
assert_eq!(got, [b"first".as_slice(), b"second".as_slice()]);
|
||||
assert_eq!(
|
||||
session.datagram_backlog(),
|
||||
Some(0),
|
||||
"the backlog drains to empty once all datagrams are pulled"
|
||||
);
|
||||
|
||||
let reaped = session.close().await;
|
||||
assert!(reaped);
|
||||
tokio::time::sleep(std::time::Duration::from_millis(200)).await;
|
||||
assert!(only_channel_0(&topo));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn wrong_substrate_operations_are_typed() {
|
||||
// The POC's WrongSubstrate shape: substrate-shaped operations fail
|
||||
|
||||
Reference in new issue
Block a user