sipx_testkit/transaction_sequence.rs
1//! Structured event sequences for the transaction layer, and the invariants they are checked
2//! against.
3//!
4//! `sipx-sip` has four fuzz targets and all of them stop at the parser. That covers the half of
5//! the north star about adversarial *input*; the half about adversarial *timing* — what happens
6//! when messages, application calls and fired timers interleave in an order nobody wrote a test
7//! for — had nothing. This module is that instrument.
8//!
9//! # Programs, not bytes
10//!
11//! The fuzzer's bytes are decoded into a [`Program`]: a sequence of [`Event`]s over a small
12//! vocabulary, each of which the harness turns into a *well-formed* SIP message or a call on
13//! [`TransactionLayer`]. Reinterpreting the bytes as SIP instead would spend the whole budget
14//! producing messages that do not parse, which is `S-4`'s fuzz targets again with extra steps.
15//! Nothing here parses: messages are **built**, so every event reaches a state machine.
16//!
17//! The encoding is four bytes per event — opcode, target, and two operands — because libFuzzer
18//! mutates bytes and a fixed-width record keeps a byte flip to a single field instead of
19//! desynchronising the rest of the program. Every opcode byte is valid (it is taken modulo the
20//! opcode count), so no input is wasted on a decode failure.
21//!
22//! # The oracle
23//!
24//! A panic-only oracle finds almost nothing in a state machine: the machines are total, so
25//! almost any sequence "succeeds". What can go wrong is silent, so it is asserted explicitly —
26//! see [`Invariant`]. Violations are returned as data rather than panicked on, so the same code
27//! serves the fuzz target (which panics) and the regression tests (which assert).
28//!
29//! # Sans-IO
30//!
31//! No clock, no socket, no runtime, in keeping with `AGENTS.md`'s second non-negotiable. Time
32//! enters only as [`Event::FireTimer`], which is precisely what makes the timing half fuzzable.
33
34// The harness builds its fixtures from constants in this file. A fixture it cannot build is a
35// bug in the harness and must fail loudly rather than quietly drive nothing — the same reason
36// `AGENTS.md` lets test modules opt out of these lints.
37#![allow(clippy::expect_used)]
38
39use std::fmt::Write as _;
40use std::path::{Path, PathBuf};
41
42use bytes::Bytes;
43
44use sipx_sip::build::{RequestBuilder, ResponseBuilder};
45use sipx_sip::transaction::{
46 ClientState, Dispatch, Output, Reliability, ServerState, Timer, Timers, TransactionKey,
47 TransactionLayer, TuEvent,
48};
49use sipx_sip::{HeaderName, Host, HostName, Message, Method, Request, Response, StatusCode, Uri};
50
51// ------------------------------------------------------------------------------------------
52// The vocabulary
53// ------------------------------------------------------------------------------------------
54
55/// How many conversations the vocabulary can name.
56///
57/// Deliberately tiny. A large slot space would give almost every event a fresh transaction and
58/// the interesting behaviour — retransmission absorption, matching, a timer arriving for the
59/// transaction that has just gone — lives in collisions.
60pub const SLOTS: u8 = 4;
61
62/// Slots at or above this index use a branch with no RFC 3261 magic cookie, so they exercise
63/// the RFC 2543 matching fallback of `TransactionKey::Legacy`.
64pub const FIRST_LEGACY_SLOT: u8 = 2;
65
66/// Status codes the vocabulary can produce: provisional, 2xx, and non-2xx finals across the
67/// ranges the state tables branch on.
68pub const STATUSES: [u16; 8] = [100, 180, 200, 302, 404, 486, 500, 603];
69
70/// To-tags the vocabulary can produce. Three, because two are needed for a fork answering
71/// twice and the third distinguishes a legacy key.
72pub const TAGS: [&str; 3] = ["ta", "tb", "tc"];
73
74/// Every timer of RFC 3261 §17 Table 4, plus the unlettered 200 ms one.
75pub const TIMERS: [Timer; 13] = [
76 Timer::A,
77 Timer::B,
78 Timer::D,
79 Timer::E,
80 Timer::F,
81 Timer::G,
82 Timer::H,
83 Timer::I,
84 Timer::J,
85 Timer::K,
86 Timer::L,
87 Timer::M,
88 Timer::Trying100,
89];
90
91/// The methods the vocabulary can produce.
92///
93/// `ACK` and `CANCEL` earn their place: §17.2.3 folds `ACK` onto the `INVITE` it acknowledges
94/// and pointedly does not fold `CANCEL`, and getting either wrong is a call that never hangs up.
95fn method(index: u8) -> Method {
96 match index % METHOD_COUNT {
97 0 => Method::Invite,
98 1 => Method::Ack,
99 2 => Method::Bye,
100 3 => Method::Cancel,
101 4 => Method::Register,
102 _ => Method::Options,
103 }
104}
105
106/// How many methods [`method`] can return.
107const METHOD_COUNT: u8 = 6;
108
109/// The table sizes, as the `u8` the decoder needs. Checked against the tables below, because a
110/// count that drifts from its table silently narrows what the fuzzer can produce.
111const STATUS_COUNT: u8 = 8;
112const TAG_COUNT: u8 = 3;
113const TIMER_COUNT: u8 = 13;
114
115const _: () = {
116 assert!(STATUS_COUNT as usize == STATUSES.len());
117 assert!(TAG_COUNT as usize == TAGS.len());
118 assert!(TIMER_COUNT as usize == TIMERS.len());
119};
120
121/// Every `Timer` variant, matched exhaustively.
122///
123/// The const assert above proves `TIMER_COUNT` and `TIMERS` are the same length. It cannot prove the
124/// table covers the *enum*: a fourteenth variant leaves both the same size, so nothing fails and the
125/// new timer is simply never fuzzed — the drift the comment at the table says it guards against
126/// (`X-31`).
127///
128/// A match is the only construct the compiler checks for exhaustiveness. It is deliberately
129/// function-local rather than a trait impl, because what is being asserted is not "Timer has a
130/// table" but "this list of variants is complete *here*", which is the thing a global rule would
131/// silently stop being. A test (`the_table_and_the_enum_agree_row_for_row`) then ties each arm to
132/// its row, so the table and the enum cannot drift apart in either direction.
133const fn timer_row(timer: Timer) -> usize {
134 match timer {
135 Timer::A => 0,
136 Timer::B => 1,
137 Timer::D => 2,
138 Timer::E => 3,
139 Timer::F => 4,
140 Timer::G => 5,
141 Timer::H => 6,
142 Timer::I => 7,
143 Timer::J => 8,
144 Timer::K => 9,
145 Timer::L => 10,
146 Timer::M => 11,
147 Timer::Trying100 => 12,
148 }
149}
150
151#[cfg(test)]
152mod timer_coverage {
153 use super::{TIMERS, timer_row};
154
155 #[test]
156 fn the_table_and_the_enum_agree_row_for_row() {
157 // `timer_row` is an exhaustive match, so a fourteenth `Timer` fails to compile *there* and
158 // this test can only be reached by a table that names every variant. What it then catches
159 // is the two lists agreeing on order, which exhaustiveness alone cannot see: a row named in
160 // the match but filled with the wrong value would still compile.
161 let mut seen = [false; 13];
162 for (row, (timer, named)) in TIMERS.iter().zip(seen.iter_mut()).enumerate() {
163 // `timer_row` maps a variant back to this row, so the table and the match cannot
164 // disagree on order — the one drift exhaustiveness alone cannot see. Iterating both in
165 // step rather than indexing keeps the proof total: there is no index here that could be
166 // out of range for the array it walks.
167 assert_eq!(
168 timer_row(*timer),
169 row,
170 "timer_row({timer:?}) does not round-trip"
171 );
172 *named = true;
173 }
174 assert!(
175 seen.iter().all(|used| *used),
176 "a row of TIMERS is unreachable from any variant"
177 );
178 }
179}
180
181/// Distinct transaction keys the vocabulary can produce per branch space: `ACK` folds onto
182/// `INVITE`, so six methods name five keys.
183const FOLDED_METHODS: usize = 5;
184
185/// The most transactions that can be in flight at once, whatever the program does.
186///
187/// Two branch spaces (client and server), [`SLOTS`] branches in each, and five keys per branch
188/// — six methods, of which `ACK` folds onto `INVITE`. This is the bound behind
189/// [`Invariant::StoreGrowth`]: it depends on the
190/// *vocabulary* and not on the program's length, which is what "does not grow without bound
191/// over a bounded sequence" has to mean if it is to mean anything.
192/// The vocabulary's ceiling, kept because the doc below reads off it: the two `HashMap`s can hold
193/// at most `SLOTS × FOLDED_METHODS` keys each. The store-growth invariant no longer asserts against
194/// it, because it cannot fire — see the `X-31` note in `check_bound`.
195pub const MAX_LIVE_TRANSACTIONS: usize = 2 * SLOTS as usize * FOLDED_METHODS;
196
197/// Which branch space a message belongs to.
198///
199/// Requests the harness *receives* and responses it *sends* address server transactions;
200/// requests it sends and responses it receives address client ones. Giving each role its own
201/// branch keeps one key from naming a client and a server transaction at the same time, which
202/// would make every oracle below ambiguous about which machine it was talking about.
203#[derive(Debug, Clone, Copy, PartialEq, Eq)]
204enum Space {
205 Client,
206 Server,
207}
208
209impl Space {
210 fn letter(self) -> char {
211 match self {
212 Self::Client => 'c',
213 Self::Server => 's',
214 }
215 }
216}
217
218/// One step of a decoded program.
219///
220/// The three kinds the story names — a message arriving, the application asking for something,
221/// a timer firing — plus the two things a driver reports that no message can express.
222#[derive(Debug, Clone, Copy, PartialEq, Eq)]
223pub enum Event {
224 /// The application starts a client transaction.
225 SendRequest {
226 /// Which conversation, and therefore which branch.
227 slot: u8,
228 /// Index into the method vocabulary.
229 method: u8,
230 /// Whether the transport retransmits, which decides half the timer behaviour.
231 reliable: bool,
232 },
233 /// A request arrives from the network.
234 ReceiveRequest {
235 /// Which conversation, and therefore which branch.
236 slot: u8,
237 /// Index into the method vocabulary.
238 method: u8,
239 /// Whether the transport retransmits.
240 reliable: bool,
241 /// Index into [`TAGS`], for the `To` tag.
242 to_tag: u8,
243 },
244 /// A response arrives from the network, addressed at a client transaction.
245 ReceiveResponse {
246 /// Which conversation, and therefore which branch.
247 slot: u8,
248 /// Index into the method vocabulary, used for the `CSeq` method.
249 method: u8,
250 /// Index into [`STATUSES`].
251 status: u8,
252 /// Index into [`TAGS`], for the `To` tag.
253 to_tag: u8,
254 },
255 /// The application answers a server transaction.
256 SendResponse {
257 /// Which conversation, and therefore which branch.
258 slot: u8,
259 /// Index into the method vocabulary.
260 method: u8,
261 /// Index into [`STATUSES`].
262 status: u8,
263 /// Index into [`TAGS`], for the `To` tag.
264 to_tag: u8,
265 },
266 /// A timer the driver holds fires.
267 ///
268 /// The key is chosen from every key the layer has ever created, *including ones whose
269 /// transaction is gone* — a driver's timer wheel cannot cancel atomically, so a timer
270 /// firing into a retired machine is the normal case rather than the exotic one, and
271 /// [`Invariant::TimerForRemovedKey`] is what says it must be harmless.
272 FireTimer {
273 /// Index into the keys the layer has created.
274 key: u8,
275 /// Index into the timers.
276 timer: u8,
277 /// Choose from all thirteen timers rather than the ones currently armed. Off by
278 /// default so the budget mostly goes on timers a real driver would have set, and on
279 /// so that stray and never-armed ones are reachable too.
280 any: bool,
281 },
282 /// The transport reports it could not deliver for a transaction.
283 TransportError {
284 /// Index into the keys the layer has created.
285 key: u8,
286 },
287 /// The driver gives up on a server transaction the application never answered.
288 Abandon {
289 /// Index into the keys the layer has created.
290 key: u8,
291 },
292}
293
294/// How many opcodes the decoder recognises.
295const OPCODES: u8 = 7;
296
297/// Bytes per encoded event.
298const RECORD: usize = 4;
299
300/// A decoded sequence of events.
301#[derive(Debug, Clone, Default, PartialEq, Eq)]
302pub struct Program {
303 /// The events, in the order they are driven.
304 pub events: Vec<Event>,
305}
306
307impl Program {
308 /// Decode a fuzzer input.
309 ///
310 /// Total: every byte string is a program. A trailing partial record is ignored, which is
311 /// what lets libFuzzer shrink an input a byte at a time without the tail turning into
312 /// noise.
313 #[must_use]
314 pub fn decode(bytes: &[u8]) -> Self {
315 let events = bytes
316 .chunks_exact(RECORD)
317 .filter_map(|chunk| match chunk {
318 &[op, target, a, b] => Some(decode_event(op, target, a, b)),
319 _ => None,
320 })
321 .collect();
322 Self { events }
323 }
324
325 /// Encode a program back to the bytes that decode to it.
326 ///
327 /// The inverse of [`Program::decode`] on canonical inputs, which is what lets a seed
328 /// written as Rust be committed as a corpus file.
329 #[must_use]
330 pub fn encode(&self) -> Vec<u8> {
331 let mut out = Vec::with_capacity(self.events.len() * RECORD);
332 for event in &self.events {
333 out.extend_from_slice(&encode_event(*event));
334 }
335 out
336 }
337}
338
339fn decode_event(op: u8, target: u8, a: u8, b: u8) -> Event {
340 match op % OPCODES {
341 0 => Event::SendRequest {
342 slot: target % SLOTS,
343 method: a % METHOD_COUNT,
344 reliable: b & 1 != 0,
345 },
346 1 => Event::ReceiveRequest {
347 slot: target % SLOTS,
348 method: a % METHOD_COUNT,
349 reliable: b & 1 != 0,
350 to_tag: (b >> 4) % TAG_COUNT,
351 },
352 2 => Event::ReceiveResponse {
353 slot: target % SLOTS,
354 method: a % METHOD_COUNT,
355 status: b % STATUS_COUNT,
356 to_tag: (b >> 4) % TAG_COUNT,
357 },
358 3 => Event::SendResponse {
359 slot: target % SLOTS,
360 method: a % METHOD_COUNT,
361 status: b % STATUS_COUNT,
362 to_tag: (b >> 4) % TAG_COUNT,
363 },
364 4 => Event::FireTimer {
365 key: target,
366 timer: a % TIMER_COUNT,
367 any: b & 1 != 0,
368 },
369 5 => Event::TransportError { key: target },
370 _ => Event::Abandon { key: target },
371 }
372}
373
374fn encode_event(event: Event) -> [u8; RECORD] {
375 match event {
376 Event::SendRequest {
377 slot,
378 method,
379 reliable,
380 } => [0, slot, method, u8::from(reliable)],
381 Event::ReceiveRequest {
382 slot,
383 method,
384 reliable,
385 to_tag,
386 } => [1, slot, method, u8::from(reliable) | (to_tag << 4)],
387 Event::ReceiveResponse {
388 slot,
389 method,
390 status,
391 to_tag,
392 } => [2, slot, method, status | (to_tag << 4)],
393 Event::SendResponse {
394 slot,
395 method,
396 status,
397 to_tag,
398 } => [3, slot, method, status | (to_tag << 4)],
399 Event::FireTimer { key, timer, any } => [4, key, timer, u8::from(any)],
400 Event::TransportError { key } => [5, key, 0, 0],
401 Event::Abandon { key } => [6, key, 0, 0],
402 }
403}
404
405// ------------------------------------------------------------------------------------------
406// The oracle
407// ------------------------------------------------------------------------------------------
408
409/// The properties a transaction layer can break without panicking.
410///
411/// Each is something that would show up in production as a slow leak, a wedged call or a
412/// response answering the wrong request — never as a crash, which is why a panic-only fuzz
413/// target would run for a week and report nothing.
414#[derive(Debug, Clone, Copy, PartialEq, Eq)]
415pub enum Invariant {
416 /// No transaction outlives its terminal state.
417 ///
418 /// When a machine emits `Output::Terminated` the layer must have dropped it, and no
419 /// observable state may ever read `Terminated` — a transaction that reports its own death
420 /// and stays in the store is the leak this whole layer exists to avoid. Arming a timer in
421 /// the same batch that retires the transaction is the same fault seen from the other side.
422 OutlivedTermination,
423
424 /// No timer fires for a key that has been removed.
425 ///
426 /// A driver's timer wheel cannot cancel atomically: `ClearTimer` and the fired callback
427 /// race, and the transaction may be gone by the time the timer arrives. Firing into that
428 /// gap must produce nothing and above all must not resurrect the transaction. This is the
429 /// design record's "timer IDs must survive transaction termination without firing into a
430 /// dead machine", stated as something a test can fail.
431 TimerForRemovedKey,
432
433 /// The store does not grow without bound over a bounded sequence.
434 ///
435 /// Two claims. In flight, no more transactions than the vocabulary has keys — bounded by
436 /// [`MAX_LIVE_TRANSACTIONS`], not by how long the program is. And after every timer the
437 /// layer asked for has been driven to quiescence, the only transactions left are the ones
438 /// legitimately waiting on the application, because those are the only ones RFC 3261 gives
439 /// no timer of their own.
440 StoreGrowth,
441
442 /// No state is reachable that the RFC 3261 §17 tables, as amended by RFC 6026, do not name.
443 ///
444 /// `ClientState` and `ServerState` each cover two machines, so the enum alone proves
445 /// nothing: an INVITE client transaction reaching `Trying` — the *non*-INVITE waiting
446 /// state — type-checks and is meaningless. The legal set is per machine, taken from
447 /// `docs/specs/sip-transaction.md` §4.
448 UnnamedState,
449
450 /// A response to a request the layer sent must reach the transaction that sent it.
451 ///
452 /// Half of what the layer does is §17.1.3 matching, and a response that matches nothing is
453 /// a call that hangs until Timer F rather than a crash. Checked only where the harness
454 /// knows the answer: the response it builds carries the branch and `CSeq` method of the
455 /// request that created the transaction, so it must match.
456 UnroutableResponse,
457}
458
459/// One invariant broken, and where.
460#[derive(Debug, Clone)]
461pub struct Violation {
462 /// Which step of the program, or [`usize::MAX`] for the quiescence check that follows it.
463 pub step: usize,
464 /// Which invariant.
465 pub invariant: Invariant,
466 /// What was seen, in enough detail to name a defect from.
467 pub detail: String,
468}
469
470impl std::fmt::Display for Violation {
471 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
472 write!(
473 f,
474 "step {}: {:?}: {}",
475 self.step, self.invariant, self.detail
476 )
477 }
478}
479
480/// What driving a program produced.
481#[derive(Debug, Clone)]
482pub struct Run {
483 /// One line per step: the event, what the layer did with it, and the store's size.
484 ///
485 /// This is the artefact a crash report is read from, and the thing the replay test pins:
486 /// a harness whose trace is not a function of its input reports crashes nobody can
487 /// reproduce.
488 pub trace: Vec<String>,
489 /// Every invariant broken, in the order they were noticed.
490 pub violations: Vec<Violation>,
491}
492
493// ------------------------------------------------------------------------------------------
494// The driver
495// ------------------------------------------------------------------------------------------
496
497/// What the harness remembers about one key the layer has created.
498#[derive(Debug)]
499struct Tracked {
500 key: TransactionKey,
501 space: Space,
502 /// Whether this is one of the INVITE machines, which decides the legal state set.
503 invite: bool,
504 /// The timers the layer has asked for and not cleared. A `Vec` rather than a set because
505 /// `Timer` is not `Ord` and there are never more than three.
506 armed: Vec<Timer>,
507 /// `c0/INVITE`, for the trace.
508 label: String,
509}
510
511/// How many rounds the quiescence check will drive timers for before giving up.
512///
513/// Generous: a Timer A that doubles forever is retired by Timer B in the same round, so a
514/// transaction that survives this many rounds is not slow, it is stuck.
515const DRAIN_ROUNDS: usize = 64;
516
517struct Driver {
518 layer: TransactionLayer,
519 tracked: Vec<Tracked>,
520 trace: Vec<String>,
521 violations: Vec<Violation>,
522 /// The defects this run steps over.
523 ///
524 /// Always empty while [`Known`] is uninhabited, hence unread — kept, with the plumbing in
525 /// [`run_with`], because rebuilding it is what a campaign blocked behind a fresh defect
526 /// cannot afford to stop and do.
527 #[allow(dead_code)]
528 suppressed: Vec<Known>,
529}
530
531/// A defect the campaign knows about and steps over.
532///
533/// A fuzzer that crashes on the same open bug every time reports that bug forever and nothing
534/// behind it. Suppressing one is therefore necessary and also dangerous — a suppression nobody
535/// removes hides its whole class permanently — so each is named here, documented on its variant,
536/// and paired with an ignored regression test that fails until it is fixed. [`run_strict`]
537/// suppresses nothing, which is how those tests see the defect.
538///
539/// There are none at the moment: `LegacyClientResponseMatching` — a response to an RFC 2543
540/// client transaction matching nothing, because `TransactionKey::from_sent_request` derived the
541/// client key by §17.2.3's server rules — was the first and so far only entry, and `S-26` fixed
542/// it. The type stays because the next campaign will want it, and an empty enum is the honest
543/// way to say the campaign is currently suppressing nothing.
544#[derive(Debug, Clone, Copy, PartialEq, Eq)]
545#[non_exhaustive]
546pub enum Known {}
547
548/// Every defect [`run`] steps over.
549pub const KNOWN_DEFECTS: [Known; 0] = [];
550
551/// Drive a program and check it, stepping over [`KNOWN_DEFECTS`].
552///
553/// What the fuzz target calls. Never panics on a violation: it returns them, so the target can
554/// panic with a report and a regression test can assert on the list.
555#[must_use]
556pub fn run(program: &Program) -> Run {
557 run_with(program, &KNOWN_DEFECTS)
558}
559
560/// Drive a program and check it, suppressing nothing.
561///
562/// What the regression tests for the known defects call, so that "known" never quietly becomes
563/// "invisible".
564#[must_use]
565pub fn run_strict(program: &Program) -> Run {
566 run_with(program, &[])
567}
568
569/// Drive a program and check it, suppressing the named defects.
570#[must_use]
571pub fn run_with(program: &Program, suppressed: &[Known]) -> Run {
572 let mut driver = Driver {
573 layer: TransactionLayer::new(Timers::default()),
574 tracked: Vec::new(),
575 trace: Vec::new(),
576 violations: Vec::new(),
577 suppressed: suppressed.to_vec(),
578 };
579
580 for (step, event) in program.events.iter().enumerate() {
581 driver.step(step, *event);
582 driver.check_states(step);
583 driver.check_bound(step);
584 }
585 driver.drive_to_quiescence();
586
587 Run {
588 trace: driver.trace,
589 violations: driver.violations,
590 }
591}
592
593impl Driver {
594 fn step(&mut self, step: usize, event: Event) {
595 match event {
596 Event::SendRequest {
597 slot,
598 method: m,
599 reliable,
600 } => self.send_request(step, slot, m, reliable),
601 Event::ReceiveRequest {
602 slot,
603 method: m,
604 reliable,
605 to_tag,
606 } => self.receive_request(step, slot, m, reliable, to_tag),
607 Event::ReceiveResponse {
608 slot,
609 method: m,
610 status,
611 to_tag,
612 } => self.receive_response(step, slot, m, status, to_tag),
613 Event::SendResponse {
614 slot,
615 method: m,
616 status,
617 to_tag,
618 } => self.send_response(step, slot, m, status, to_tag),
619 Event::FireTimer { key, timer, any } => self.fire_timer(step, key, timer, any),
620 Event::TransportError { key } => self.transport_error(step, key),
621 Event::Abandon { key } => self.abandon(step, key),
622 }
623 }
624
625 // -- the events ------------------------------------------------------------------------
626
627 fn send_request(&mut self, step: usize, slot: u8, method_index: u8, reliable: bool) {
628 let m = method(method_index);
629 let request = build_request(Space::Client, slot, &m, None);
630 let reliability = reliability(reliable);
631 let Some((key, outputs)) = self.layer.send_request(request, reliability) else {
632 self.record(
633 step,
634 &format!("SendRequest({}) no key", label(slot, &m)),
635 "",
636 );
637 return;
638 };
639 let index = self.track(key, Space::Client, slot, &m, m == Method::Invite);
640 let outcome = format!(
641 "created client {} {}",
642 self.tracked
643 .get(index)
644 .map_or_else(String::new, |t| t.label.clone()),
645 self.apply(index, &outputs)
646 );
647 self.record(
648 step,
649 &format!(
650 "SendRequest({} {})",
651 label(slot, &m),
652 transport_name(reliable)
653 ),
654 &outcome,
655 );
656 self.check_termination(step, index, &outputs);
657 }
658
659 fn receive_request(
660 &mut self,
661 step: usize,
662 slot: u8,
663 method_index: u8,
664 reliable: bool,
665 to_tag: u8,
666 ) {
667 let m = method(method_index);
668 let request = build_request(Space::Server, slot, &m, Some(to_tag));
669 let dispatch = self
670 .layer
671 .receive(Message::Request(request), reliability(reliable));
672 let outcome = self.absorb(step, dispatch, Space::Server, slot, &m);
673 self.record(
674 step,
675 &format!(
676 "ReceiveRequest({} {})",
677 label(slot, &m),
678 transport_name(reliable)
679 ),
680 &outcome,
681 );
682 }
683
684 fn receive_response(
685 &mut self,
686 step: usize,
687 slot: u8,
688 method_index: u8,
689 status_index: u8,
690 to_tag: u8,
691 ) {
692 let m = method(method_index);
693 let status = status(status_index);
694 // The response carries the branch and CSeq of the request a `SendRequest` for this slot
695 // and method would have produced, which is what makes the matching claim below checkable.
696 let request = build_request(Space::Client, slot, &m, None);
697 let expected = TransactionKey::from_sent_request(&request);
698 let live = expected
699 .as_ref()
700 .and_then(|key| self.layer.client_state(key))
701 .is_some();
702
703 let response = build_response(&request, status, to_tag);
704 let dispatch = self
705 .layer
706 .receive(Message::Response(response), Reliability::Unreliable);
707 let matched = matches!(dispatch, Dispatch::Matched { .. });
708 let outcome = self.absorb(step, dispatch, Space::Client, slot, &m);
709
710 // The property holds on every slot, legacy ones included: §17.1.3 keys a client
711 // transaction on the branch and the `CSeq` method, which a response carries whether or
712 // not the branch has a magic cookie. Legacy slots used to be excluded here, under
713 // `Known::LegacyClientResponseMatching`, until `S-26` gave the client key its own
714 // derivation.
715 //
716 // A new suppression is consulted here: `if live && !matched && !self.suppressed.contains(…)`.
717 if live && !matched {
718 self.violate(
719 step,
720 Invariant::UnroutableResponse,
721 format!(
722 "a {} response for {} matched nothing, but its client transaction is live",
723 status.code(),
724 label(slot, &m)
725 ),
726 );
727 }
728
729 self.record(
730 step,
731 &format!("ReceiveResponse({} {})", label(slot, &m), status.code()),
732 &outcome,
733 );
734 }
735
736 fn send_response(
737 &mut self,
738 step: usize,
739 slot: u8,
740 method_index: u8,
741 status_index: u8,
742 to_tag: u8,
743 ) {
744 let m = method(method_index);
745 let status = status(status_index);
746 let request = build_request(Space::Server, slot, &m, Some(to_tag));
747 let Some(key) = TransactionKey::from_request(&request) else {
748 self.record(
749 step,
750 &format!("SendResponse({}) no key", label(slot, &m)),
751 "",
752 );
753 return;
754 };
755 let response = build_response(&request, status, to_tag);
756 let outputs = self.layer.send_response(&key, response);
757 let outcome = match self.index_of(&key) {
758 Some(index) => {
759 let label = self
760 .tracked
761 .get(index)
762 .map_or_else(String::new, |t| t.label.clone());
763 format!("{label} {}", self.apply(index, &outputs))
764 }
765 None => format!("no transaction store={}", store(&self.layer)),
766 };
767 self.record(
768 step,
769 &format!("SendResponse({} {})", label(slot, &m), status.code()),
770 &outcome,
771 );
772 if let Some(index) = self.index_of(&key) {
773 self.check_termination(step, index, &outputs);
774 }
775 }
776
777 fn fire_timer(&mut self, step: usize, key_index: u8, timer_index: u8, any: bool) {
778 if self.tracked.is_empty() {
779 self.record(step, "FireTimer(no keys)", "");
780 return;
781 }
782 let index = key_index as usize % self.tracked.len();
783 let Some(entry) = self.tracked.get(index) else {
784 return;
785 };
786 let armed = entry.armed.clone();
787 let key = entry.key.clone();
788 let label = entry.label.clone();
789 let space = entry.space;
790
791 let timer = if any || armed.is_empty() {
792 *TIMERS
793 .get(timer_index as usize % TIMERS.len())
794 .expect("the timer index is taken modulo the table")
795 } else {
796 *armed
797 .get(timer_index as usize % armed.len())
798 .expect("the armed index is taken modulo a non-empty list")
799 };
800
801 let live = self.is_live(space, &key);
802 let before = self.layer.len();
803 // A timer that fires is spent. Whatever the machine wants next it re-arms in its outputs.
804 if let Some(entry) = self.tracked.get_mut(index) {
805 entry.armed.retain(|t| *t != timer);
806 }
807 let outputs = self.layer.on_timer(&key, timer);
808 let after = self.layer.len();
809
810 if !live && (!outputs.is_empty() || before != after) {
811 self.violate(
812 step,
813 Invariant::TimerForRemovedKey,
814 format!(
815 "timer {timer:?} for {label}, whose transaction is gone, produced \
816 {} output(s) and left the store at {after:?} (was {before:?})",
817 outputs.len()
818 ),
819 );
820 }
821
822 let outcome = format!("{label} {}", self.apply(index, &outputs));
823 self.record(
824 step,
825 &format!(
826 "FireTimer({label} {timer:?}{})",
827 if live { "" } else { " stale" }
828 ),
829 &outcome,
830 );
831 self.check_termination(step, index, &outputs);
832 }
833
834 fn transport_error(&mut self, step: usize, key_index: u8) {
835 if self.tracked.is_empty() {
836 self.record(step, "TransportError(no keys)", "");
837 return;
838 }
839 let index = key_index as usize % self.tracked.len();
840 let Some(entry) = self.tracked.get(index) else {
841 return;
842 };
843 let key = entry.key.clone();
844 let label = entry.label.clone();
845 let outputs = self.layer.on_transport_error(&key);
846 let outcome = format!("{label} {}", self.apply(index, &outputs));
847 self.record(step, &format!("TransportError({label})"), &outcome);
848 self.check_termination(step, index, &outputs);
849 }
850
851 fn abandon(&mut self, step: usize, key_index: u8) {
852 if self.tracked.is_empty() {
853 self.record(step, "Abandon(no keys)", "");
854 return;
855 }
856 let index = key_index as usize % self.tracked.len();
857 let Some(entry) = self.tracked.get(index) else {
858 return;
859 };
860 let key = entry.key.clone();
861 let label = entry.label.clone();
862 let gone = self.layer.abandon(&key);
863 if let Some(entry) = self.tracked.get_mut(index).filter(|_| gone) {
864 entry.armed.clear();
865 }
866 self.record(
867 step,
868 &format!("Abandon({label})"),
869 &format!(
870 "{} store={}",
871 if gone { "dropped" } else { "absent" },
872 store(&self.layer)
873 ),
874 );
875 }
876
877 // -- bookkeeping -----------------------------------------------------------------------
878
879 /// Fold a dispatch into the model, returning its summary for the trace.
880 fn absorb(
881 &mut self,
882 step: usize,
883 dispatch: Dispatch,
884 space: Space,
885 slot: u8,
886 m: &Method,
887 ) -> String {
888 match dispatch {
889 Dispatch::Created { key, outputs } => {
890 let index = self.track(key, space, slot, m, m == &Method::Invite);
891 let label = self
892 .tracked
893 .get(index)
894 .map_or_else(String::new, |t| t.label.clone());
895 let summary = format!("created server {label} {}", self.apply(index, &outputs));
896 self.check_termination(step, index, &outputs);
897 summary
898 }
899 Dispatch::Matched { key, outputs } => match self.index_of(&key) {
900 Some(index) => {
901 let label = self
902 .tracked
903 .get(index)
904 .map_or_else(String::new, |t| t.label.clone());
905 let summary = format!("matched {label} {}", self.apply(index, &outputs));
906 self.check_termination(step, index, &outputs);
907 summary
908 }
909 None => format!("matched untracked {}", summarise(&outputs)),
910 },
911 Dispatch::Unmatched(_) => format!("unmatched store={}", store(&self.layer)),
912 }
913 }
914
915 /// Record a key the layer has created, returning its index.
916 ///
917 /// A key can be created more than once — the layer's stores are maps, so a second
918 /// transaction under the same key replaces the first — in which case the entry is reset
919 /// rather than duplicated.
920 fn track(
921 &mut self,
922 key: TransactionKey,
923 space: Space,
924 slot: u8,
925 m: &Method,
926 invite: bool,
927 ) -> usize {
928 if let Some(index) = self.index_of(&key) {
929 if let Some(entry) = self.tracked.get_mut(index) {
930 entry.invite = invite;
931 entry.armed.clear();
932 }
933 return index;
934 }
935 self.tracked.push(Tracked {
936 key,
937 space,
938 invite,
939 armed: Vec::new(),
940 label: format!("{}{slot}/{}", space.letter(), method_name(m)),
941 });
942 self.tracked.len() - 1
943 }
944
945 fn index_of(&self, key: &TransactionKey) -> Option<usize> {
946 self.tracked.iter().position(|t| &t.key == key)
947 }
948
949 /// Fold a batch of outputs into the armed-timer model and summarise it for the trace.
950 fn apply(&mut self, index: usize, outputs: &[Output]) -> String {
951 if let Some(entry) = self.tracked.get_mut(index) {
952 for output in outputs {
953 match output {
954 Output::SetTimer { timer, .. } => {
955 if !entry.armed.contains(timer) {
956 entry.armed.push(*timer);
957 }
958 }
959 Output::ClearTimer(timer) => entry.armed.retain(|t| t != timer),
960 Output::Terminated(_) => entry.armed.clear(),
961 Output::Send(_) | Output::ToTu(_) => {}
962 }
963 }
964 }
965 let state =
966 self.tracked
967 .get(index)
968 .map_or_else(String::new, |entry| match self.state_of(entry) {
969 Some(state) => format!(" state={state}"),
970 None => String::new(),
971 });
972 format!("{}{state} store={}", summarise(outputs), store(&self.layer))
973 }
974
975 // -- the invariants --------------------------------------------------------------------
976
977 /// No transaction outlives its terminal state.
978 fn check_termination(&mut self, step: usize, index: usize, outputs: &[Output]) {
979 let Some(entry) = self.tracked.get(index) else {
980 return;
981 };
982 if !outputs.iter().any(|o| matches!(o, Output::Terminated(_))) {
983 return;
984 }
985 let label = entry.label.clone();
986 let still_there = self.state_of(entry).is_some();
987 if still_there {
988 self.violate(
989 step,
990 Invariant::OutlivedTermination,
991 format!("{label} reported Terminated and is still in the store"),
992 );
993 }
994 if let Some(timer) = outputs.iter().find_map(|o| match o {
995 Output::SetTimer { timer, .. } => Some(*timer),
996 _ => None,
997 }) {
998 self.violate(
999 step,
1000 Invariant::OutlivedTermination,
1001 format!("{label} armed timer {timer:?} in the batch that retired it"),
1002 );
1003 }
1004 }
1005
1006 /// No state is reachable that the §17 tables do not name for that machine.
1007 ///
1008 /// The two non-INVITE arms name the same three states today and are kept apart anyway: each
1009 /// is a different table in `docs/specs/sip-transaction.md`, and collapsing them would mean
1010 /// the next amendment to one of them silently changed the other.
1011 #[allow(clippy::match_same_arms)]
1012 fn check_states(&mut self, step: usize) {
1013 let mut found = Vec::new();
1014 for entry in &self.tracked {
1015 let Some(state) = self.state_of(entry) else {
1016 continue;
1017 };
1018 let legal = match (entry.space, entry.invite) {
1019 // §4.1: the INVITE client machine never waits in Trying — that is the
1020 // non-INVITE machine's state — and never reaches Terminated observably.
1021 (Space::Client, true) => {
1022 ["Calling", "Proceeding", "Completed", "Accepted"].as_slice()
1023 }
1024 // §4.2: no Calling, and no Accepted — RFC 6026 adds Accepted to the INVITE
1025 // machines only, because only a 2xx to an INVITE can arrive twice.
1026 (Space::Client, false) => ["Trying", "Proceeding", "Completed"].as_slice(),
1027 // §4.3: an INVITE server transaction starts in Proceeding, never Trying.
1028 (Space::Server, true) => {
1029 ["Proceeding", "Completed", "Confirmed", "Accepted"].as_slice()
1030 }
1031 // §4.4: no Confirmed and no Accepted; there is no ACK to wait for.
1032 (Space::Server, false) => ["Trying", "Proceeding", "Completed"].as_slice(),
1033 };
1034 if !legal.contains(&state.as_str()) {
1035 found.push(format!(
1036 "{} is in {state}, which §17 does not name for {} machine",
1037 entry.label,
1038 machine_name(entry.space, entry.invite)
1039 ));
1040 }
1041 }
1042 for detail in found {
1043 self.violate(step, Invariant::UnnamedState, detail);
1044 }
1045 }
1046
1047 /// The store is bounded by the vocabulary, not by the program.
1048 fn check_bound(&mut self, step: usize) {
1049 let (client, server) = self.layer.len();
1050 let live = client + server;
1051 if live > self.tracked.len() {
1052 self.violate(
1053 step,
1054 Invariant::StoreGrowth,
1055 format!(
1056 "{live} transactions in flight, but the layer has only reported \
1057 {} keys",
1058 self.tracked.len()
1059 ),
1060 );
1061 }
1062 }
1063
1064 /// Drive every timer the layer asked for until nothing is left to fire, then say what
1065 /// survived.
1066 ///
1067 /// The only transactions RFC 3261 leaves without a timer of their own are the ones waiting
1068 /// on the application: a server transaction the TU has not answered (§17.2.2, and §17.2.1
1069 /// once the 100 has gone out), and an INVITE client transaction in Proceeding, whose
1070 /// Timer B is cancelled by the first provisional on purpose (§17.1.1.2). Anything else
1071 /// still in the store after quiescence is a transaction nothing will ever retire, which is
1072 /// the slow quiet outage this invariant is about.
1073 fn drive_to_quiescence(&mut self) {
1074 for _ in 0..DRAIN_ROUNDS {
1075 let mut fired = false;
1076 for index in 0..self.tracked.len() {
1077 let Some(entry) = self.tracked.get(index) else {
1078 continue;
1079 };
1080 if self.state_of(entry).is_none() {
1081 continue;
1082 }
1083 let key = entry.key.clone();
1084 let armed = entry.armed.clone();
1085 for timer in armed {
1086 if let Some(entry) = self.tracked.get_mut(index) {
1087 entry.armed.retain(|t| *t != timer);
1088 }
1089 let outputs = self.layer.on_timer(&key, timer);
1090 let _ = self.apply(index, &outputs);
1091 fired = true;
1092 }
1093 }
1094 if !fired {
1095 break;
1096 }
1097 }
1098
1099 let mut found = Vec::new();
1100 for entry in &self.tracked {
1101 let Some(state) = self.state_of(entry) else {
1102 continue;
1103 };
1104 let waiting_on_the_application = match entry.space {
1105 Space::Server => state == "Trying" || state == "Proceeding",
1106 Space::Client => entry.invite && state == "Proceeding",
1107 };
1108 if !waiting_on_the_application {
1109 found.push(format!(
1110 "{} is still in {state} with every timer fired; nothing will retire it",
1111 entry.label
1112 ));
1113 }
1114 }
1115 for detail in found {
1116 self.violate(usize::MAX, Invariant::StoreGrowth, detail);
1117 }
1118 self.trace.push(format!(
1119 "quiescent store={} live={:?}",
1120 store(&self.layer),
1121 self.tracked
1122 .iter()
1123 .filter_map(|entry| self.state_of(entry).map(|s| format!("{}={s}", entry.label)))
1124 .collect::<Vec<_>>()
1125 ));
1126 }
1127
1128 // -- helpers ---------------------------------------------------------------------------
1129
1130 fn state_of(&self, entry: &Tracked) -> Option<String> {
1131 match entry.space {
1132 Space::Client => self.layer.client_state(&entry.key).map(client_state_name),
1133 Space::Server => self.layer.server_state(&entry.key).map(server_state_name),
1134 }
1135 }
1136
1137 fn is_live(&self, space: Space, key: &TransactionKey) -> bool {
1138 match space {
1139 Space::Client => self.layer.client_state(key).is_some(),
1140 Space::Server => self.layer.server_state(key).is_some(),
1141 }
1142 }
1143
1144 fn record(&mut self, step: usize, event: &str, outcome: &str) {
1145 let mut line = String::new();
1146 let _ = write!(line, "{step:03} {event}");
1147 if !outcome.is_empty() {
1148 let _ = write!(line, " | {outcome}");
1149 }
1150 self.trace.push(line);
1151 }
1152
1153 fn violate(&mut self, step: usize, invariant: Invariant, detail: String) {
1154 self.violations.push(Violation {
1155 step,
1156 invariant,
1157 detail,
1158 });
1159 }
1160}
1161
1162// ------------------------------------------------------------------------------------------
1163// Building the messages
1164// ------------------------------------------------------------------------------------------
1165
1166fn reliability(reliable: bool) -> Reliability {
1167 if reliable {
1168 Reliability::Reliable
1169 } else {
1170 Reliability::Unreliable
1171 }
1172}
1173
1174fn transport_name(reliable: bool) -> &'static str {
1175 if reliable { "tcp" } else { "udp" }
1176}
1177
1178fn status(index: u8) -> StatusCode {
1179 let code = *STATUSES
1180 .get(index as usize % STATUSES.len())
1181 .expect("the status index is taken modulo the table");
1182 StatusCode::new(code).expect("the status table holds valid codes")
1183}
1184
1185/// The branch a slot's messages carry.
1186///
1187/// Slots below [`FIRST_LEGACY_SLOT`] carry the RFC 3261 magic cookie; the rest deliberately do
1188/// not, so the RFC 2543 fallback in `TransactionKey::Legacy` is reachable — those senders are
1189/// still on the public internet, and the RFC 4475 corpus has one.
1190fn branch(space: Space, slot: u8) -> String {
1191 if slot < FIRST_LEGACY_SLOT {
1192 format!("z9hG4bK-fz{}{slot}", space.letter())
1193 } else {
1194 format!("fz{}{slot}-rfc2543", space.letter())
1195 }
1196}
1197
1198fn build_request(space: Space, slot: u8, m: &Method, to_tag: Option<u8>) -> Request {
1199 let uri = Uri::sip(Host::Name(
1200 HostName::new("example.com").expect("a valid host"),
1201 ));
1202 let to = match to_tag.and_then(|index| TAGS.get(index as usize % TAGS.len())) {
1203 Some(tag) => format!("<sip:callee@example.com>;tag={tag}"),
1204 None => "<sip:callee@example.com>".to_owned(),
1205 };
1206 RequestBuilder::new(m.clone(), uri)
1207 .header(
1208 HeaderName::Via,
1209 Bytes::from(format!(
1210 "SIP/2.0/UDP host.example.net;branch={}",
1211 branch(space, slot)
1212 )),
1213 )
1214 .expect("a valid Via")
1215 .header(
1216 HeaderName::From,
1217 Bytes::from(format!("<sip:caller@example.net>;tag=f{slot}")),
1218 )
1219 .expect("a valid From")
1220 .header(HeaderName::To, Bytes::from(to))
1221 .expect("a valid To")
1222 .header(
1223 HeaderName::CallId,
1224 Bytes::from(format!("fuzz-{slot}@example.net")),
1225 )
1226 .expect("a valid Call-ID")
1227 .cseq(1, m)
1228 .expect("a valid CSeq")
1229 .max_forwards(70)
1230 .build()
1231}
1232
1233fn build_response(request: &Request, status: StatusCode, to_tag: u8) -> Response {
1234 let mut builder = ResponseBuilder::to_request(request, status, "Sequence")
1235 .expect("a response can be built for any request the harness makes");
1236 if !status.is_provisional() {
1237 // A UAS tags To on its first final response — replacing the header, not adding a
1238 // second one, which would make the response invalid.
1239 let tag = TAGS
1240 .get(to_tag as usize % TAGS.len())
1241 .expect("the tag index is taken modulo the table");
1242 builder = builder
1243 .set_header(
1244 &HeaderName::To,
1245 Bytes::from(format!("<sip:callee@example.com>;tag={tag}")),
1246 )
1247 .expect("a valid To");
1248 }
1249 builder.build()
1250}
1251
1252// ------------------------------------------------------------------------------------------
1253// Naming things, for the trace
1254// ------------------------------------------------------------------------------------------
1255
1256fn method_name(m: &Method) -> String {
1257 String::from_utf8_lossy(m.as_bytes()).into_owned()
1258}
1259
1260fn label(slot: u8, m: &Method) -> String {
1261 format!("{slot}/{}", method_name(m))
1262}
1263
1264fn machine_name(space: Space, invite: bool) -> &'static str {
1265 match (space, invite) {
1266 (Space::Client, true) => "the INVITE client",
1267 (Space::Client, false) => "the non-INVITE client",
1268 (Space::Server, true) => "the INVITE server",
1269 (Space::Server, false) => "the non-INVITE server",
1270 }
1271}
1272
1273fn client_state_name(state: ClientState) -> String {
1274 match state {
1275 ClientState::Calling => "Calling",
1276 ClientState::Trying => "Trying",
1277 ClientState::Proceeding => "Proceeding",
1278 ClientState::Completed => "Completed",
1279 ClientState::Accepted => "Accepted",
1280 ClientState::Terminated => "Terminated",
1281 }
1282 .to_owned()
1283}
1284
1285fn server_state_name(state: ServerState) -> String {
1286 match state {
1287 ServerState::Trying => "Trying",
1288 ServerState::Proceeding => "Proceeding",
1289 ServerState::Completed => "Completed",
1290 ServerState::Confirmed => "Confirmed",
1291 ServerState::Accepted => "Accepted",
1292 ServerState::Terminated => "Terminated",
1293 }
1294 .to_owned()
1295}
1296
1297fn store(layer: &TransactionLayer) -> String {
1298 let (client, server) = layer.len();
1299 format!("{client}/{server}")
1300}
1301
1302fn summarise(outputs: &[Output]) -> String {
1303 let mut parts = Vec::new();
1304 let sends = outputs
1305 .iter()
1306 .filter(|o| matches!(o, Output::Send(_)))
1307 .count();
1308 if sends > 0 {
1309 parts.push(format!("send={sends}"));
1310 }
1311 let events: Vec<&str> = outputs
1312 .iter()
1313 .filter_map(|o| match o {
1314 Output::ToTu(event) => Some(match event.as_ref() {
1315 TuEvent::Request(_) => "request",
1316 TuEvent::Response(_) => "response",
1317 TuEvent::Ack(_) => "ack",
1318 TuEvent::Timeout => "timeout",
1319 TuEvent::TransportError => "transport-error",
1320 }),
1321 _ => None,
1322 })
1323 .collect();
1324 if !events.is_empty() {
1325 parts.push(format!("tu=[{}]", events.join(",")));
1326 }
1327 let set: Vec<String> = outputs
1328 .iter()
1329 .filter_map(|o| match o {
1330 Output::SetTimer { timer, .. } => Some(format!("{timer:?}")),
1331 _ => None,
1332 })
1333 .collect();
1334 if !set.is_empty() {
1335 parts.push(format!("set=[{}]", set.join(",")));
1336 }
1337 let cleared: Vec<String> = outputs
1338 .iter()
1339 .filter_map(|o| match o {
1340 Output::ClearTimer(timer) => Some(format!("{timer:?}")),
1341 _ => None,
1342 })
1343 .collect();
1344 if !cleared.is_empty() {
1345 parts.push(format!("clear=[{}]", cleared.join(",")));
1346 }
1347 if let Some(reason) = outputs.iter().find_map(|o| match o {
1348 Output::Terminated(reason) => Some(*reason),
1349 _ => None,
1350 }) {
1351 parts.push(format!("terminated={reason:?}"));
1352 }
1353 if parts.is_empty() {
1354 "absorbed".to_owned()
1355 } else {
1356 parts.join(" ")
1357 }
1358}
1359
1360// ------------------------------------------------------------------------------------------
1361// The seed corpus
1362// ------------------------------------------------------------------------------------------
1363
1364/// A named seed program, committed to the corpus as the bytes it encodes to.
1365#[derive(Debug, Clone)]
1366pub struct Seed {
1367 /// The corpus file's name.
1368 pub name: &'static str,
1369 /// The program.
1370 pub program: Program,
1371}
1372
1373/// Where the committed seed corpus lives, relative to the repository root.
1374pub const CORPUS_PATH: &str = "crates/sipx-testkit/corpus/transaction-sequences";
1375
1376/// The committed seed corpus directory.
1377#[must_use]
1378pub fn corpus_dir() -> PathBuf {
1379 Path::new(env!("CARGO_MANIFEST_DIR"))
1380 .join("corpus")
1381 .join("transaction-sequences")
1382}
1383
1384/// Write [`seeds`] to [`corpus_dir`], one file per seed.
1385///
1386/// The corpus is generated rather than hand-written so its provenance is reproducible, the way
1387/// `scripts/import-rfc4475-corpus.sh` makes the parser corpus reproducible from the RFC. What
1388/// makes that worth anything is the test on the other side:
1389/// `the_committed_corpus_is_exactly_the_seed_programs` fails if the two ever disagree.
1390pub fn write_corpus() -> std::io::Result<usize> {
1391 let dir = corpus_dir();
1392 std::fs::create_dir_all(&dir)?;
1393 let seeds = seeds();
1394 for seed in &seeds {
1395 std::fs::write(dir.join(seed.name), seed.program.encode())?;
1396 }
1397 Ok(seeds.len())
1398}
1399
1400/// The programs the corpus is seeded from.
1401///
1402/// Seeding from nothing means the first minutes of every campaign are spent rediscovering that
1403/// a response has to follow a request. These are the scenarios of
1404/// `docs/specs/sip-transaction.md` §7 — the rows the FSM table tests already walk — rewritten
1405/// as event programs, which is the same trick CI plays on the parser targets by seeding them
1406/// with the RFC 4475 corpus. The fuzzer starts from behaviour that reaches every machine and
1407/// mutates outwards from there.
1408///
1409/// One long function on purpose: it is a table of scenarios, and a table reads best as a table.
1410#[must_use]
1411#[allow(clippy::too_many_lines)]
1412pub fn seeds() -> Vec<Seed> {
1413 // Slots 0 and 1 carry the magic cookie; 2 and 3 do not.
1414 const INVITE: u8 = 0;
1415 const ACK: u8 = 1;
1416 const BYE: u8 = 2;
1417 const CANCEL: u8 = 3;
1418 const REGISTER: u8 = 4;
1419 const OPTIONS: u8 = 5;
1420
1421 // Indices into STATUSES.
1422 const S100: u8 = 0;
1423 const S180: u8 = 1;
1424 const S200: u8 = 2;
1425 const S486: u8 = 5;
1426 const S500: u8 = 6;
1427
1428 /// Fire a named timer for the key at `key`, whether or not the model thinks it is armed.
1429 fn timer(key: u8, which: Timer) -> Event {
1430 // `timer_row`, not a `position().expect()`: a variant missing from the table is a compile
1431 // error at the match rather than a panic here, and this is what keeps the row function
1432 // load-bearing rather than decoration.
1433 let index =
1434 u8::try_from(timer_row(which)).expect("the timer table is shorter than 256 entries");
1435 Event::FireTimer {
1436 key,
1437 timer: index,
1438 any: true,
1439 }
1440 }
1441 fn send(slot: u8, method: u8) -> Event {
1442 Event::SendRequest {
1443 slot,
1444 method,
1445 reliable: false,
1446 }
1447 }
1448 fn recv(slot: u8, method: u8) -> Event {
1449 Event::ReceiveRequest {
1450 slot,
1451 method,
1452 reliable: false,
1453 to_tag: 0,
1454 }
1455 }
1456 fn answer(slot: u8, method: u8, status: u8) -> Event {
1457 Event::SendResponse {
1458 slot,
1459 method,
1460 status,
1461 to_tag: 0,
1462 }
1463 }
1464 fn reply(slot: u8, method: u8, status: u8, to_tag: u8) -> Event {
1465 Event::ReceiveResponse {
1466 slot,
1467 method,
1468 status,
1469 to_tag,
1470 }
1471 }
1472
1473 let mut seeds = Vec::new();
1474 let mut seed = |name: &'static str, events: Vec<Event>| {
1475 seeds.push(Seed {
1476 name,
1477 program: Program { events },
1478 });
1479 };
1480
1481 // §7 T1: retransmission with doubling intervals, then the timeout.
1482 seed(
1483 "t1-invite-client-retransmits-then-times-out",
1484 vec![
1485 send(0, INVITE),
1486 timer(0, Timer::A),
1487 timer(0, Timer::A),
1488 timer(0, Timer::A),
1489 timer(0, Timer::B),
1490 ],
1491 );
1492 // §7 T2: a non-2xx is acknowledged by the transaction, which then waits out Timer D.
1493 seed(
1494 "t2-invite-client-acks-a-non-2xx",
1495 vec![
1496 send(0, INVITE),
1497 reply(0, INVITE, S486, 0),
1498 reply(0, INVITE, S486, 0),
1499 timer(0, Timer::D),
1500 ],
1501 );
1502 // §7 T3 and T4: a 2xx is not acknowledged here, and a fork's second 2xx still finds a
1503 // transaction to arrive at (RFC 6026).
1504 seed(
1505 "t3-invite-client-2xx-is-not-acked-and-a-fork-answers-twice",
1506 vec![
1507 send(0, INVITE),
1508 reply(0, INVITE, S200, 0),
1509 reply(0, INVITE, S200, 1),
1510 timer(0, Timer::M),
1511 ],
1512 );
1513 // A provisional cancels Timer B on purpose: the callee may ring for longer than 64·T1.
1514 seed(
1515 "invite-client-waits-in-proceeding-with-no-timeout",
1516 vec![
1517 send(0, INVITE),
1518 reply(0, INVITE, S180, 0),
1519 timer(0, Timer::B),
1520 reply(0, INVITE, S200, 0),
1521 timer(0, Timer::M),
1522 ],
1523 );
1524 // §4.2: Timer E backs off to T2 and Timer F retires the machine from Trying.
1525 seed(
1526 "non-invite-client-backs-off-then-times-out",
1527 vec![
1528 send(1, OPTIONS),
1529 timer(0, Timer::E),
1530 timer(0, Timer::E),
1531 timer(0, Timer::E),
1532 timer(0, Timer::F),
1533 ],
1534 );
1535 seed(
1536 "non-invite-client-completes-on-a-final-response",
1537 vec![
1538 send(1, BYE),
1539 reply(1, BYE, S200, 0),
1540 reply(1, BYE, S200, 0),
1541 timer(0, Timer::K),
1542 ],
1543 );
1544 // §7 T5 and T6: retransmissions are absorbed, and answered from the last response.
1545 seed(
1546 "t5-server-absorbs-request-retransmissions",
1547 vec![
1548 recv(0, REGISTER),
1549 recv(0, REGISTER),
1550 answer(0, REGISTER, S200),
1551 recv(0, REGISTER),
1552 timer(0, Timer::J),
1553 ],
1554 );
1555 // §7 T7 and T11: the transaction answers 100 itself, then a non-2xx, then absorbs the ACK.
1556 seed(
1557 "t7-invite-server-sends-100-then-absorbs-the-ack",
1558 vec![
1559 recv(0, INVITE),
1560 timer(0, Timer::Trying100),
1561 answer(0, INVITE, S486),
1562 timer(0, Timer::G),
1563 recv(0, ACK),
1564 timer(0, Timer::I),
1565 ],
1566 );
1567 // §7 T8 and T12: the TU answers first, so no 100 goes out, and the ACK for the 2xx is the
1568 // TU's business (RFC 6026).
1569 seed(
1570 "t8-invite-server-2xx-hands-the-ack-to-the-tu",
1571 vec![
1572 recv(0, INVITE),
1573 answer(0, INVITE, S180),
1574 timer(0, Timer::Trying100),
1575 answer(0, INVITE, S200),
1576 recv(0, ACK),
1577 timer(0, Timer::L),
1578 ],
1579 );
1580 // Timer H: no ACK ever comes.
1581 seed(
1582 "invite-server-times-out-waiting-for-an-ack",
1583 vec![
1584 recv(0, INVITE),
1585 answer(0, INVITE, S500),
1586 timer(0, Timer::G),
1587 timer(0, Timer::H),
1588 ],
1589 );
1590 // §7 T9: a reliable transport sets no retransmission timers and its absorption timers fire
1591 // at once.
1592 seed(
1593 "t9-reliable-transport-sets-no-retransmission-timers",
1594 vec![
1595 Event::SendRequest {
1596 slot: 0,
1597 method: INVITE,
1598 reliable: true,
1599 },
1600 reply(0, INVITE, S486, 0),
1601 timer(0, Timer::D),
1602 Event::ReceiveRequest {
1603 slot: 1,
1604 method: OPTIONS,
1605 reliable: true,
1606 to_tag: 0,
1607 },
1608 answer(1, OPTIONS, S200),
1609 timer(1, Timer::J),
1610 ],
1611 );
1612 // §7 T13: a sender from before the magic cookie meant anything is matched by the legacy key.
1613 seed(
1614 "t13-legacy-branch-matching",
1615 vec![
1616 recv(2, OPTIONS),
1617 recv(2, OPTIONS),
1618 answer(2, OPTIONS, S200),
1619 recv(2, OPTIONS),
1620 timer(0, Timer::J),
1621 ],
1622 );
1623 // §7 T14: a CANCEL names the INVITE's branch but runs in a transaction of its own.
1624 seed(
1625 "t14-cancel-runs-in-its-own-transaction",
1626 vec![
1627 recv(0, INVITE),
1628 recv(0, CANCEL),
1629 answer(0, CANCEL, S200),
1630 answer(0, INVITE, S486),
1631 recv(0, ACK),
1632 timer(0, Timer::I),
1633 timer(1, Timer::J),
1634 ],
1635 );
1636 // The transport failing, which no message can express.
1637 seed(
1638 "transport-error-terminates-and-tells-the-tu",
1639 vec![send(0, OPTIONS), Event::TransportError { key: 0 }],
1640 );
1641 // A server transaction the application never answered, dropped by the driver — §17.2.2
1642 // gives it no timer, so nothing else would.
1643 seed(
1644 "abandon-a-server-transaction-the-application-never-answered",
1645 vec![recv(1, REGISTER), Event::Abandon { key: 0 }],
1646 );
1647 // Timers arriving after the transaction has gone: the race a driver's timer wheel cannot
1648 // close, and what `Invariant::TimerForRemovedKey` is about.
1649 seed(
1650 "stale-timers-fire-after-the-transaction-is-gone",
1651 vec![
1652 send(1, OPTIONS),
1653 reply(1, OPTIONS, S200, 0),
1654 timer(0, Timer::K),
1655 timer(0, Timer::K),
1656 timer(0, Timer::F),
1657 timer(0, Timer::E),
1658 ],
1659 );
1660 // A provisional to a non-INVITE client, which keeps Timer F running — the asymmetry with
1661 // Timer B that §17.1.2.2 states outright.
1662 seed(
1663 "non-invite-client-times-out-from-proceeding-too",
1664 vec![
1665 send(1, REGISTER),
1666 reply(1, REGISTER, S100, 0),
1667 timer(0, Timer::E),
1668 timer(0, Timer::F),
1669 ],
1670 );
1671
1672 seeds
1673}