P11 slice 113 — post-registration gating fuzzing#
Date: 2026-06-11
Closes: testing-plan §4 fuzzing gap — post_registration gating ("Golden-only
behaviors lacking fuzz"). golden_post_registration.rs exists; there is no proptest.
Scope#
Fuzz the pre-/post-registration command gate — the highest-value remaining §4 gap
after MODE (already done) and nick-collision (slice 112). This is the security boundary
that decides which commands a connection may run before it has completed NICK+USER.
The gate lives in two layers:
Session::dispatch(leveva/src/session.rs): phase-independent carve-outs —QUIT(closes in either phase),CAP(phase-independent), andAWAYonly when the connection negotiated thedraft/pre-awaycap. Then thepending_completeguard, then the phase match:Registered→command::dispatch,Registering→Registration::apply.Registration::apply(leveva/src/registration.rs): the pre-registration verb match —NICK/USER/PASSadvance state,QUITisPending, everything else →451 ERR_NOTREGISTERED.
The pre-registration exempt set is exactly {NICK, USER, PASS, CAP, QUIT} (plus AWAY
only with the pre_away cap, which the fuzzed sessions never negotiate). Every other verb
— known post-reg command or junk alike — yields 451 before registration and never
451 after registration (a known verb dispatches; an unknown verb → 421).
Why a proptest (not a differential)#
The oracle also 451s unregistered clients, but the registration-error numeric skeleton
is already pinned against the oracle by registration_error_skeleton_differential.rs
(slice 105). The §4 gap is specifically the missing fuzz of the gate's monotonicity, so
this is a model-lockstep proptest, not a new differential. The model is trivial and
independent: registered = (seen_valid_nick && seen_valid_user); gated verbs 451 iff
!registered.
Locked decisions#
- Open server fixture:
allow { host "*@*"; class "c" }with no password, so a validNICK+USERregisters synchronously (no auth pipeline →ctxbuilt withNone; no PASS wall; no CAP negotiation in the fuzzed streams). - A "gated verb" is any
[A-Z]{1,12}token not in the exempt set. This is broader than a hand-picked post-reg verb pool: it also covers unknown verbs (which still451pre-reg and421post-reg — never451), exercising the gate's generality. - Headline invariant (P2) is the before/after pairing — the same gated verb is
451before registration and not-451after. This is both the security property and its inverse.
Tests#
Unit (leveva/src/registration.rs, inverse-invariant) — 2#
stray_commands_never_open_registration— feeding a sequence of non-registration commands throughapplyonly ever returnsReply([451]), neverComplete, andis_registered()staysfalse(the gate never opens spuriously — the inverse of "NICK+USER completes").pass_then_stray_still_refuses_and_does_not_register—PASSalone advances nothing observable; a following stray still451s and the connection is still unregistered (PASS is exempt from451but is not a registration trigger on its own).
Proptest (leveva/tests/post_registration_proptest.rs) — 4#
gated_verb_before_registration_is_451_and_does_not_register— fresh session, random gated verb + args → output contains451, session stays unregistered, well-formed CRLF.the_gate_flips_once_same_verb_451_before_not_after— the headline + inverse: session A (unregistered)451s the verb; session B (registered, welcome drained) feeds the same verb+args and the output does not contain451.registration_completes_once_order_independent_strays_stay_gated—Kstrays, thenNICK/USERin a random order separated by more strays, thenJstrays: every stray before both of NICK+USER land451s and leaves the session unregistered; after both, registered; exactly one001across the whole stream; a trailing stray no longer451s.arbitrary_command_bytes_never_panic_either_phase— arbitrary bytes as a command line fed to a fresh and to a registered session never panic and yield only well-formed CRLF lines.
Divergences#
None — leveva-native characterization (no leveva source change expected; the gate already behaves correctly, it was simply never fuzzed). Joins the coverage-closing characterization slices (100/102/109/110-char/111/112).
Gate#
cargo test -p leveva --lib (+2 units) green; the new post_registration_proptest binary
green; golden_post_registration still green; new files clippy-clean; cargo build --workspace 0 warnings.