Skip to content

(scratch) test: track RFC 4787, and state its requirements as executable contracts - #1741

Draft
daniel-noland wants to merge 220 commits into
pr/daniel-noland/fuzz-nf-probesfrom
pr/daniel-noland/spec-compliance
Draft

(scratch) test: track RFC 4787, and state its requirements as executable contracts#1741
daniel-noland wants to merge 220 commits into
pr/daniel-noland/fuzz-nf-probesfrom
pr/daniel-noland/spec-compliance

Conversation

@daniel-noland

Copy link
Copy Markdown
Collaborator

Scratch PR. Parking the specification-compliance line of work so it is pushed and reviewable;
not proposed for merge in this shape. Stacked on #1738.

What is here

Nine commits, continuing the duvet work already in the stack (RFC 4884 and RFC 5382 landed earlier).

Tracking RFC 4787 — the UDP counterpart of RFC 5382, 14 numbered requirements landing on the
same masquerade code that already carries RFC 5382 citations. Cited as the individual RFC, never as
BCP 127: that composite concatenates RFC 4787, RFC 6888 and RFC 7857, whose section numbers collide,
and duvet silently keeps only the last — 42 requirements where the three separately yield 129.

Findings recorded, not fixed. REQ-1 (mapping is address-and-port-dependent), REQ-8 (filtering is
stricter than either branch the RFC offers) and REQ-9 (no hairpinning) are one decision, not three:
this gateway is deliberately not UNSAF-traversal-friendly, or it is not deliberate and is a much
larger piece of work. REQ-13 is the same shape — this dataplane originates no ICMP error at all, and
has no MTU anywhere on the datapath to originate one from. All todo rather than exception,
because nobody has ruled.

Executable contracts. nat/src/masquerade/contract.rs states RFC 4787 REQ-12 as a
Requirement the implementation calls from a debug-only assertion and the test cited type=test
calls directly, so the two citations are provably about the same predicate. SPEC and ID are
const and checked against duvet's extracted requirements at compile time: a citation naming a
requirement its specification does not state now fails the build with E0080.

Errata. The corpus's inline-errata/ was audited. RFC 4787, 5382, 5508, 6888 and 7857 have no
errata of any status; RFC 4884 has one, and it does not touch us.

Open, and deliberately not done here

  • The .duvet/ snapshot is not regenerated — duvet is not on PATH outside the dev shell, and
    hand-editing the regression gate would defeat it. RFC 4787 REQ-12 should move to
    [!MUST,implementation,test] on the next duvet report.
  • There is no just duvet recipe yet.
  • development/code/spec-compliance.md carries a live open-questions list, expected to grow.
  • The RFC corpus itself is moving to a separate repository; nothing here depends on it at build
    time, only .duvet/ which is committed.

Checks

cargo test -p dataplane-nat 210 pass, 1 ignored. clippy and fmt clean on dataplane-nat and
dataplane.

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Aug 20, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 08f82a8c-33c3-4a1e-b6e6-565f686fe219

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Aug 20, 2026

Copy link
Copy Markdown

❌ 2 Tests Failed:

Tests completed Failed Passed Skipped
1640 2 1638 0
View the top 2 failed test(s) by shortest run time
dataplane-nat::masquerade::fuzz::a_flow_that_cannot_be_masqueraded_says_so
Stack Traces | 1.44s run time
thread 'masquerade::fuzz::a_flow_that_cannot_be_masqueraded_says_so' (158519) panicked at ..../src/masquerade/fuzz.rs:152:9:
3 flows reached the attribution assertion across 1 configurations; this property has gone vacuous
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
dataplane-routing::router::rio::tests::a_large_answer_arrives_whole
Stack Traces | 11.2s run time
thread 'router::rio::tests::a_large_answer_arrives_whole' (161420) panicked at ..../src/router/rio.rs:1035:18:
rio should answer within the read timeout: Os { code: 11, kind: WouldBlock, message: "Resource temporarily unavailable" }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

@daniel-noland
daniel-noland force-pushed the pr/daniel-noland/spec-compliance branch from cd0205c to 604bbfe Compare August 20, 2026 02:32
@daniel-noland daniel-noland added the dont-merge Do not merge this Pull Request label Aug 21, 2026
daniel-noland and others added 25 commits August 21, 2026 14:17
Cachix pays for itself only on outputs that are slow to build and reusable
across revisions.  Per-revision workspace and image outputs are neither, so
later commits mark them volatile and skip their substitute lookups.

The wrapping is separated from the decisions that use it because nixfmt
reindents an entire binding when its expression is parenthesised, so folding
these eight sites into the commits that need them buries roughly 630 lines of
pure reindentation in four otherwise small diffs.  `source-volatile` is
introduced and applied here in one place; every commit that follows is legible
without `git show -w`.

Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Daniel Noland <daniel@githedgehog.com>
Per-revision workspace and image outputs consume cache transfer and
storage while rarely substituting, crowding out slower native
dependencies that are reusable.

Mark source-volatile outputs consistently, skip their substitute
lookups, and exclude OCI assembly paths that could pull them back
through a closure.

Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Daniel Noland <daniel@githedgehog.com>
Workspace derivations remain identical across workflow-only edits,
reruns, merge-queue runs, and post-merge pushes. Marking them
source-volatile forced those runs to rebuild the workspace despite
unchanged inputs.

Keep the marker on per-revision image assembly, but let identical
workspace builds substitute. Different flags and sysroots still produce
distinct store paths.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Daniel Noland <daniel@githedgehog.com>
`just build` creates gitignored out-links under `results`, but
`lib.cleanSource` does not honor gitignore. Their store-path targets
gave local builds a source hash different from CI.

Exclude the directory so a previous local build cannot prevent reuse of
otherwise identical cached outputs.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Daniel Noland <daniel@githedgehog.com>
Every crane derivation opted out of dependency artifacts, so each
package rebuilt hundreds of third-party crates and the standard library
whenever workspace source changed.

Build shared production and test dependency artifacts instead. Separate
flag sets preserve Cargo fingerprints, while the platform-aware package
list avoids pulling excluded, WASI-incompatible dependencies into cross
builds.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Daniel Noland <daniel@githedgehog.com>
Embedding the source store path in RUSTFLAGS changed the compilation
identity of every dependency whenever workspace source changed,
defeating the shared dependency build.

Use a stable relative prefix instead. The Bolero fix makes an absolute
path unnecessary and avoids the global source symlink that raced between
worktrees.

Resolve that prefix against the worktree for coverage and filter reports
to workspace sources; otherwise llvm-cov silently includes the standard
library and native dependencies.

Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Daniel Noland <daniel@githedgehog.com>
A queued merge blocks everything behind it, so deep runs should finish
faster even when that temporarily uses more of the shared lab pool.

Run merge-queue and push matrices four entries at a time while pull
requests remain serial and cannot crowd the queue out.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Daniel Noland <daniel@githedgehog.com>
The ci:-vlab label remained in use after VLAB became opt-in, but the
workflow no longer read it. That left no way to request all deep checks
except the lab matrix.

Honor the subtractive label for the entire VLAB matrix, including
merge-ready runs, just as ci:-upgrade is honored.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Daniel Noland <daniel@githedgehog.com>
Transient registry errors occasionally discard a push after the
expensive build has completed.

Let skopeo retry individual blobs, then retry whole idempotent skopeo
and oras pushes only for known recoverable transport and status errors.
Stream output and announce retries so degradation remains visible.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Daniel Noland <daniel@githedgehog.com>
The git-derived VERSION changed the shared dependency derivation on
every commit even though third-party crates and the standard library do
not consume it.

Use a constant version for dependency builds while preserving the real
value for workspace consumers, keeping artifacts reusable across
revisions.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Daniel Noland <daniel@githedgehog.com>
The shared dependency build is only worth having if it is actually reused.
Twice now something per-commit has leaked into it -- once the workspace
source path, once the git version string -- and both times the build still
succeeded. The only symptom was a slow cache miss, which nobody notices.

Add a CI check for each leak, using the cheapest method that can see it.

The workspace source is a store path, so "does the dependency build depend
on it?" is a question about the derivation graph. Instantiate the derivation
once and look at its inputs. That answer is exact, and it names the
offending path and derivation rather than reporting only that some hash
moved. It also avoids the alternative, which is to edit a tracked file, run
the build, and restore the file from a shell trap.

The git version is a different shape of problem: it reaches the derivation
as an environment variable and never as an input path, so no graph walk can
see it. That one needs two instantiations and a comparison.

Both flag sets, production and test, are checked independently. `src` is
exported so the graph question can be asked from outside. The check reports
through the function's exit status, so a detected mismatch cannot be
swallowed by a command-substitution subshell.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Daniel Noland <daniel@githedgehog.com>
Clippy and doctests bypassed Nix and rebuilt dependencies in empty
runner workspaces, while the existing documentation derivation was
unused and had silently rotted.

Run all three through Nix so they share dependency artifacts and match
local CI entry points. Keep clippy on all targets, execute doctests in
the sandbox, and supply rustdoc with the same cfg declarations as rustc.

Activating those paths exposed broken documentation links, an unpatched
test-runner shebang, and source filters whose escaped regexes matched
unrelated files; repair those prerequisites as part of making the checks
real.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Daniel Noland <daniel@githedgehog.com>
Forty-one per-package clippy derivations all invalidated together
because they shared the workspace source, yet each paid the fixed cost
of unpacking dependency artifacts.

Lint the platform-aware workspace package list in one derivation while
retaining per-package targets for focused local use.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Daniel Noland <daniel@githedgehog.com>
The lab cgroup provides ten cores, but jobs used only eight because
containers cannot discover that limit reliably.

Give large derivations all ten cores. Split test_each into two five-core
jobs because its many small package derivations cannot saturate the
budget serially.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Daniel Noland <daniel@githedgehog.com>
Coverage instrumentation made one counter-heavy concurrency test
dominate the suite even though more iterations reached no additional
lines. The fuzz check also repeated optimized compilation already
covered elsewhere.

Reduce that test only under coverage instrumentation and drop fuzz from
the ordinary check matrix; sanitizers and fuzz-specific jobs retain the
heavier exercise.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Daniel Noland <daniel@githedgehog.com>
The workflow honors ci:-vlab, but its README listed ci:-upgrade as the
only subtractive label. Document the available opt-out.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Daniel Noland <daniel@githedgehog.com>
The root formatting recipe covers Rust but not Nix, allowing default.nix
to drift unnoticed during this stack.

Format it and add a focused nixfmt check. Older unformatted files under
nix remain outside the check to avoid unrelated churn.

Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Daniel Noland <daniel@githedgehog.com>
The source filter admitted all Markdown and JSON, so editing prose or a
gitignored editor configuration changed every workspace derivation even
though builds read none of it.

Allow only Markdown included by crate documentation and remove unused
JSON and Just filters. Omitting a real include now fails loudly at
compile time, while unrelated files no longer defeat substitution.

Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Daniel Noland <daniel@githedgehog.com>
Adding nixfmt to `just lint` did not add it to the workflow, whose lint
steps and failure aggregation are maintained separately. Either list can
drift silently and leave a check unenforced.

Run nixfmt in CI and add a guard that keeps the recipe dependencies,
workflow steps, and aggregated outcomes aligned.

Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Daniel Noland <daniel@githedgehog.com>
Several container derivations lacked the source-volatile marker,
allowing per-revision images and dockerTools assembly paths to reach
Cachix.

Mark every image and check their realized closures against the actual
push filter. Match dockerTools artifacts by shape so newly added or
nested images are covered without another name list.

Keep the current denylist self-checking; converting cache uploads to a
stricter allowlist remains a separate behavioral change.

Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Daniel Noland <daniel@githedgehog.com>
We recently introduced a flag FlowInfoFlags::INITIATOR to mark, in a
flow pair, which flow initiated the connection. In flow-filter's tests,
the helper function attach_flow() does not account for this, and always
create the flow as a forward flow initiating the connection. This is not
currently an issue, because flow-filter's code does not leverage this
flag yet, but it will be. In preparation for using that flat in
flow-filter, adjust tests to attach an initiating or a reply flow to
packets, depending on the case.

Signed-off-by: Quentin Monnet <qmo@qmon.net>
In the flow-filter stage, in the case of traffic for masquerade or
port-forwarding with outdated flow information attached, we try to
revalidate the flow based on flow information. To do so, we have
specific, gated entries in the flow-filter context tables, and we run
each lookup (remote then local side) in possibly two steps: once with
the gate activated, in case we're dealing with reply traffic, and once
without it, in case of forward traffic.

Now that we added a flag to flow information to tell whether each flow
in a pair is the initiating, or reply-direction flow, we don't actually
need to do these two-step lookups ever: we can infer the direction
(forward or reply) from the packet from the presence or absence of the
flag. Let's do this, and remove the unnecessary lookups. We also need to
slightly adjust the related fuzz tests.

Signed-off-by: Quentin Monnet <qmo@qmon.net>
Debug binaries retained the complete Rust toolchain through their
standard-library source paths, adding roughly 2.4 GB to the closure.
They also carried a sizable DWARF index that neither packaged debugger
consumes.

Point those paths at the much smaller rust-src component, omit unused
prebuilt documentation, and remove .debug_names. Source browsing and
symbols remain available while the resulting diagnostic images become
practical to store and transfer.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Daniel Noland <daniel@githedgehog.com>
A core collected from the lab is useful only with the exact unstripped
binaries and sources that produced it. A general debugging toolbox
cannot reconstruct that relationship after the release has moved on.

Provide a purpose-built gdb image alongside each build and teach it
Rust's standard-library types without retaining rustc. This keeps
post-mortem debugging reproducible while avoiding unrelated
live-debugging tools.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Daniel Noland <daniel@githedgehog.com>
Post-mortem inspection and live debugging need different tools. The core
viewer cannot offer an editor-driven session, while bugstalker
understands Rust layouts and can expose the running dataplane through
the Debug Adapter Protocol.

Track bugstalker upstream for its current remote DAP support and package
it separately with the matching binaries and sources. Keeping the image
single-purpose avoids making every diagnostic artifact carry every
debugger.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Daniel Noland <daniel@githedgehog.com>
daniel-noland and others added 29 commits August 23, 2026 13:25
Two things already lean on this. `bolero` diagnoses by shrinking and replaying, so a
pipeline that answered differently the second time would shrink towards nothing and report
a case that does not fail. And it is what makes recording every case unnecessary: a
failure can be re-run with as much instrumentation as it takes.

Verified across processes too, by digesting a fixed scenario: 3525008ad91c215a four times.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Inline generate-and-judge works while there is one conversation at a time; it cannot be
interleaved, because the loop producing packets would also have to be in the middle of
judging them.

Proven against the most demanding existing property rather than a new easy one, so that
the trait's shape is decided by what is already needed. Both of that property's break
tests still fail it, now carrying the conversation's history rather than a line number.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…sfied

The first property here about superposition rather than about a stage, and it asserts
nothing new: each conversation knows what it sent, so the joint claim is that every one of
them was satisfied.

A poll draws from several loads rather than one, because the shape that matters is a reply
for one conversation sharing a burst with another's request -- and the harness counts how
often that actually happened, since a schedule that never mixed would pass while testing
what we already had.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A request/response load can never offer a scheduler more than one packet, so a run of them
is a run of singleton bursts however generous the schedule. A blast is what makes `take`
mean anything, and what puts several packets of one flow into a burst alongside somebody
else's traffic.

The interleaving guards now count what each burst actually carried rather than what its
poll named, which is the difference between a coverage guard and one that cannot fail.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Hand-aimed addresses work while the fixture is fixed and stop the moment it is not: a
generated configuration exposes prefixes nobody wrote down, and traffic aimed by hand
would miss every one of them.

The derivation reads what a configuration offers, not what the dataplane should do with
it; each load still judges only its own traffic. Counting derived loads per flavour rather
than in total, because skipping an entire expose kind is the likeliest way for this to be
quietly wrong.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Implements the operation algebra for the overlay, which the design note recorded as
still unbuilt.

The per-kind draw counters are load-bearing rather than decoration: they caught two
separate ways the vocabulary went dead while every property stayed green. Both are
written down where the mistake was made.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Closes the loop the config algebra was built for: the configuration, the traffic it
implies, and the order that traffic runs in are now one draw.

Three harness assumptions that a written fixture had been holding up failed here,
one of them at about a run in ten. Each is a case where the configuration already
knew the answer and the derivation had hard-coded it; the general form is in
development/code/property-testing.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
First step of the multi-worker work: find out whether the pipeline can be driven
inside a model-checked execution at all. It cannot, and the obstacle is not DPDK's
opacity but one process-global lock, which the module docs set out.

The three-case lock test exists to make that attributable rather than to assert
anything interesting on its own. Its failing third case is written out rather than
run: shuttle aborts the process instead of failing a test, so a live one would take
the suite down with it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A `concurrency::sync` mutex in a `static` typechecks under loom and shuttle and then
aborts the process, because the primitive belongs to the execution that created it
and a `OnceLock` outlives every execution. `OnceLock` is what the concurrency crate
recommends for the separate problem that `Mutex::new` is not `const fn` there, so
the advice and the hazard point in opposite directions and nothing says so.

This unblocks driving the pipeline under shuttle: every `Fabric` builds ACL contexts
and so took this lock. Revisit if loom and shuttle ever make `Mutex::new` const.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The facade documented `OnceLock` as the workaround for `Mutex::new` not being
`const fn` under the model checkers. It resolves that compile error and leaves a
worse one, which nothing said: the second execution to take the lock aborts the
process. `dpdk` followed the advice and was the only instance.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A pipeline per thread needs one reader per thread. The readers this already
published cannot serve: they hold `NonNull` table pointers and a `Cell` counter, and
the fib readers cache `Rc<UnsafeCell<FibGroup>>`, so none of them is `Send` or
`Sync` -- nor is `RouterTables`. A factory crosses the boundary and each thread makes
its own reader, which is how `start_router` feeds its workers.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…and a worker

A `Fabric` interleaved writer creation with stage assembly, so one configuration
could only ever produce one pipeline and no property here could be about more
than one worker. Which of the three parts may cross a thread boundary, and why
only that one, is on `Blueprint`.

The `nat` and `routing` re-exports are part of the same change: a blueprint has
to name the reader-factory types in a field, and outside those crates they could
not be named at all.
…ator

The first property here that runs in the arrangement production runs in: two
pipelines, one allocator, one flow table.

Break-tested against a lost update in the port-block claim. The measurement that
says shuttle rather than the sanitizer is what catches it -- and what shuttle
still cannot see -- is recorded on the test.
`table_name` promised process uniqueness and, under a model checker, did not
deliver it. The counter was the concurrency facade's atomic, which belongs to
the execution that created it; `rte_acl`'s registry is process-global and
resets for nobody. Found by `dataplane::packet_processor::fuzz::model`, which
lowers many configurations per process and so was the first thing to notice.
bolero becomes the outer loop over the same draw the single-threaded generated
property uses, so a shape reachable on one thread is reachable on two by
construction.

Measured against a deliberate allocator defect: this property does not catch it
and the targeted one does. Both the measurement and the reason are on the test,
because "the generated property covers that too" is the assumption it refutes.
Tests can now hold a recording that dumps the spans and events leading to a
panic. It cannot be asserted on: there is no way to read the trace back into the
program, and the module explains at length why that is the feature rather than a
missing one.

Three things about it were measured rather than assumed, and are written down
where the next person will need them: which targets drown a dump under a model
checker, that `#[instrument]` on the packet path captures whole `Packet` values,
and that a scoped subscriber cannot exist under loom or shuttle at all.
The arrangement receive-side steering normally puts the gateway in, and the one
no property here has ever been in: a reply's tuple is the request's reversed, so
the return traffic of a flow opened on one worker routinely lands on another.

The oracle is the existing `Conversation::judge_reply`, reused rather than
restated. What the break test does and does not establish is recorded on the
test, because it does not isolate what the property is named for.
Both handlers re-read the same `RwLock`, and so does the `Display` behind the
`logfmt()` in their debug lines, so the guard made those recursive acquisitions.
`parking_lot` gives no recursion guarantee, and writers on this lock are ordinary
data-path traffic; one of the two acquisitions being a log line means the window
only opens when tracing is turned up.

Found by the new model property below, which shuttle refused outright. Real
threads had passed it every time.
Production's third thread. Every property before this froze the tables before
sending a packet, so left-right under `FibTableReader` -- and the per-thread
`Rc<UnsafeCell<FibGroup>>` cache behind it -- had never been asked to publish
while a reader was mid-lookup.

`Fleet` stops owning the `RouterTables` to make it possible: a fleet is shared by
reference with every worker, so whoever wants to change a route cannot also be
borrowing it through one.
The route-churn property states a frame condition, so it can only ever say
that publishing did no harm. Aiming the churn at the route under test needs a
claim with a lower bound as well, and the fib's in-place next-hop update --
which writes through an `UnsafeCell` every route already points at -- is the
part of it whose safety argument had never been exercised.
The overlay half of config-apply-under-load. `Fleet::reconfigure` re-stores
through the writers a running worker already reads, which is what production
does and what rebuilding a fleet would not be.

Written twice: the first shape overlapped nothing on the plain backend and
passed for that reason. See the doc comment -- rounds are what make the
passing case mean something, and the two discarded break tests are recorded
because each looked conclusive and was not.
The design note proposed checking completeness against real configurations.
That answers the wrong question -- the algebra's address plan is a function of
its handles, so no real configuration is expressible and the report is "0%
reachable". Per degree of freedom instead, with the fuzzer supplying the
evidence and the survey's exhaustive destructuring stopping the build if the
schema grows a field nobody classified.

Reports thirteen of twenty-seven degrees of freedom fixed. Nothing acted on
yet; the note names the four worth doing first.
… touch

The frame condition over a configuration that actually changes, with the frame
taken from the algebra rather than guessed at. `X => A.X` from one draw split
at its last operation.

Round 1 races the change and is counted, not asserted: it fails about one run
in twenty, and the doc comment records what was measured and what is still
unknown. Rounds after it race a re-enactment and carry the full claim.

Two harness faults found on the way and fixed here, both of which made earlier
properties in this module prove less than they appeared to: a worker panicking
between barriers hung the run instead of failing it, and the genid was pinned
at 1 across enactments, which told the nat allocator no configuration ever
changed and manufactured two symptoms that are not the dataplane's.
`Fleet` published neither: the nat allocator was told generation one on every
enactment, and the stages were never told at all, because `Blueprint::worker`
built its pipeline without `set_data`. The second also meant the flow
revalidation path -- the code that exists for a configuration changing under
live flows -- was unreachable from every property in this module.

Also separates the two workers' five-tuples by disjoint halves of the port
space rather than by an offset, which two drawn ports could cancel; `enact`
performs config-apply one step at a time, for attributing a disturbance to
one; and the ordering now follows `mgmt`, where the generation is published
last on purpose.

Found by `just fuzz` on this target. The random engine `cargo test` runs
cannot get here: it was the coverage-guided corpus that produced the crashing
inputs these were diagnosed from.
Prompted by `-Cunsafe-allow-abi-mismatch=sanitizer` looking suspicious, which
it is. Nothing changed; the note records what each path covers and names the
experiment that decides whether the flag is vestigial or is hiding something.
`-Cunsafe-allow-abi-mismatch=sanitizer` disabled rustc's check that every
crate agrees about sanitizer flags -- the check that refuses a
half-instrumented link. `tests.pkg.dataplane` builds and links without it
under `--argstr sanitize thread`, so there was nothing to excuse: `std` is
rebuilt from source by `-Zbuild-std` under the same sanitizer as the rest.

The sysroot now records what it was built for, and `just fuzz` refuses when
that disagrees with the sanitizer it was asked for. The two knobs are
independent and a mismatch produced a binary whose green run meant nothing.
…s against

A short burst right after the barrier measured when the allocator swap landed
rather than what it did: `Everything` spends about a millisecond rebuilding
rte_acl before reaching the allocator, by which time the burst was over, so it
looked fifty times safer than the masquerade step alone. Sustained, the two
are the same order.

Also models the router-config await `mgmt` performs between the allocator swap
and the generation publish, which turns out to change nothing -- the doc
comment records that and what thread sanitizer said, which was nothing, on a
build that instruments dpdk and rebuilds std.
The transient the step instrument attributed to the masquerade store is a
dataplane defect, not a harness one: an allocator swap can give one public tuple
to two live flows, and a reply then reaches the wrong tenant conversation.

`set_randomize(false)` was the last deviation this harness had from
`mgmt::apply_masquerade_config` and was the obvious suspect. It is not the
answer -- the instrument now runs both settings, with the running allocator
primed so the swap under test is random-to-random.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`Flavour` has no static-nat or port-forwarding member, so those two rows
measured a writer storing an empty table. Recorded because the table is cited
elsewhere as evidence that only the masquerade store disturbs traffic.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@daniel-noland
daniel-noland force-pushed the pr/daniel-noland/spec-compliance branch from 976db44 to d49c061 Compare August 23, 2026 19:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dont-merge Do not merge this Pull Request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants