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:
glm-5.3-flash committed 2026-09-28 10:12:09 +00:00
1 parent 57285d23bd
commit e3c6f6c9f8
20 files changed
+1045 -21

No files matched your search

+65
View File
@@ -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