fuzz - #1755
Draft
daniel-noland wants to merge 14 commits into
Draft
Conversation
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: trueComment |
daniel-noland
commented
Aug 26, 2026
Comment on lines
+414
to
+415
| source: IpAddr, | ||
| destination: IpAddr, |
Collaborator
Author
There was a problem hiding this comment.
better to avoid the use of IpAddr if we can just use an IpAddress generic instead. Cleaner
daniel-noland
commented
Aug 26, 2026
| /// | ||
| /// Panics if the two addresses are of different families. Resolution never mixes them, since an | ||
| /// expose is of one family throughout and the peer prefix is chosen to match. | ||
| pub(crate) fn build( |
Collaborator
Author
There was a problem hiding this comment.
this function should likely be rebuilt using the pat.rs / view.rs mechanics we use elsewhere
daniel-noland
force-pushed
the
pr/daniel-noland/fuzz-config-generators
branch
from
August 26, 2026 17:30
641ac94 to
020c301
Compare
daniel-noland
force-pushed
the
pr/daniel-noland/fuzz-nf-probes
branch
from
August 26, 2026 17:30
1017959 to
c0bc094
Compare
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
daniel-noland
force-pushed
the
pr/daniel-noland/fuzz-config-generators
branch
from
August 26, 2026 19:36
020c301 to
ec84ef3
Compare
daniel-noland
force-pushed
the
pr/daniel-noland/fuzz-nf-probes
branch
2 times, most recently
from
August 26, 2026 20:41
caeff74 to
fc901ba
Compare
daniel-noland
force-pushed
the
pr/daniel-noland/fuzz-config-generators
branch
from
August 26, 2026 21:02
23da889 to
8d7de8c
Compare
daniel-noland
force-pushed
the
pr/daniel-noland/fuzz-nf-probes
branch
from
August 26, 2026 21:02
fc901ba to
3d43e5b
Compare
daniel-noland
force-pushed
the
pr/daniel-noland/fuzz-config-generators
branch
from
August 26, 2026 21:13
8d7de8c to
17e5812
Compare
daniel-noland
force-pushed
the
pr/daniel-noland/fuzz-nf-probes
branch
from
August 26, 2026 21:13
3d43e5b to
357270f
Compare
daniel-noland
force-pushed
the
pr/daniel-noland/fuzz-config-generators
branch
from
August 27, 2026 01:29
17e5812 to
5514278
Compare
daniel-noland
force-pushed
the
pr/daniel-noland/fuzz-nf-probes
branch
from
August 27, 2026 01:29
357270f to
ffec6ba
Compare
daniel-noland
force-pushed
the
pr/daniel-noland/fuzz-config-generators
branch
from
August 27, 2026 01:41
5514278 to
a1f62fc
Compare
daniel-noland
force-pushed
the
pr/daniel-noland/fuzz-nf-probes
branch
from
August 27, 2026 01:41
ffec6ba to
f51c0fc
Compare
daniel-noland
force-pushed
the
pr/daniel-noland/fuzz-config-generators
branch
from
August 27, 2026 04:32
a1f62fc to
99c2ea7
Compare
daniel-noland
force-pushed
the
pr/daniel-noland/fuzz-nf-probes
branch
from
August 27, 2026 04:32
f51c0fc to
96cbbc7
Compare
daniel-noland
force-pushed
the
pr/daniel-noland/fuzz-config-generators
branch
from
August 27, 2026 05:10
99c2ea7 to
3927db1
Compare
daniel-noland
force-pushed
the
pr/daniel-noland/fuzz-nf-probes
branch
2 times, most recently
from
August 27, 2026 17:59
0b54681 to
4748ecd
Compare
daniel-noland
force-pushed
the
pr/daniel-noland/fuzz-config-generators
branch
from
August 27, 2026 18:29
01c6a98 to
b137c88
Compare
daniel-noland
force-pushed
the
pr/daniel-noland/fuzz-nf-probes
branch
2 times, most recently
from
August 27, 2026 19:33
281d3bc to
d61d365
Compare
daniel-noland
force-pushed
the
pr/daniel-noland/fuzz-nf-probes
branch
from
August 28, 2026 04:22
876a902 to
773d31a
Compare
daniel-noland
force-pushed
the
pr/daniel-noland/fuzz-config-generators
branch
2 times, most recently
from
August 28, 2026 05:11
5f049ba to
8387397
Compare
daniel-noland
force-pushed
the
pr/daniel-noland/fuzz-nf-probes
branch
from
August 28, 2026 05:11
773d31a to
95022cf
Compare
daniel-noland
force-pushed
the
pr/daniel-noland/fuzz-nf-probes
branch
from
August 28, 2026 05:42
f759872 to
e9fba51
Compare
daniel-noland
force-pushed
the
pr/daniel-noland/fuzz-config-generators
branch
from
August 28, 2026 06:40
6fd7f93 to
26db9f0
Compare
daniel-noland
force-pushed
the
pr/daniel-noland/fuzz-nf-probes
branch
from
August 28, 2026 06:40
2065c3b to
43a361f
Compare
daniel-noland
force-pushed
the
pr/daniel-noland/fuzz-config-generators
branch
from
August 28, 2026 07:07
26db9f0 to
fbb8ebe
Compare
daniel-noland
force-pushed
the
pr/daniel-noland/fuzz-nf-probes
branch
from
August 28, 2026 07:07
43a361f to
81f2482
Compare
daniel-noland
force-pushed
the
pr/daniel-noland/fuzz-config-generators
branch
from
August 28, 2026 07:31
fbb8ebe to
adb8eae
Compare
daniel-noland
force-pushed
the
pr/daniel-noland/fuzz-nf-probes
branch
from
August 28, 2026 07:31
81f2482 to
e681b25
Compare
daniel-noland
force-pushed
the
pr/daniel-noland/fuzz-config-generators
branch
from
August 28, 2026 07:43
adb8eae to
5ebb19b
Compare
daniel-noland
force-pushed
the
pr/daniel-noland/fuzz-nf-probes
branch
from
August 28, 2026 07:43
e681b25 to
f44e856
Compare
daniel-noland
force-pushed
the
pr/daniel-noland/fuzz-config-generators
branch
from
August 28, 2026 09:14
5ebb19b to
214e324
Compare
daniel-noland
force-pushed
the
pr/daniel-noland/fuzz-nf-probes
branch
from
August 28, 2026 09:14
f44e856 to
7e7ba95
Compare
daniel-noland
force-pushed
the
pr/daniel-noland/fuzz-config-generators
branch
from
August 28, 2026 17:15
214e324 to
71e4ecc
Compare
daniel-noland
force-pushed
the
pr/daniel-noland/fuzz-nf-probes
branch
from
August 28, 2026 17:15
7e7ba95 to
0472824
Compare
A design note for testing the config-driven dataplane, plus links to it from the
code guidelines and the property-testing guide.
Nothing in it is implemented. Its value is mostly in the approaches it rejects,
because each of those looks obviously right at first and is a dead end for a
reason worth keeping:
* Generating configuration values directly. A `TypeGenerator` over the config
types yields syntactically valid, semantically impossible configurations --
colliding VNIs, peerings between VPCs that do not exist -- so the validator
refuses nearly all of them and a coverage-guided fuzzer spends its budget
exploring rejection paths. Filtering does not help, because the generator
would then have to encode the validator's rules, leaving two copies to keep
in agreement. Build configurations from an algebra of valid operations
instead, and preconditions become unrepresentable rather than checked.
* A shadow model as the oracle. It grows into a second dataplane, drifts from
the first, and has to be rewritten whenever the real one is refactored.
Operations emit claims about observable behaviour instead.
* Reimplementing rule selection, or discovering precedence by ablation. Both
are unnecessary: `acl/src/reference/` already answers "which rule should have
won, and which did it shadow" in one pass, and the vocabulary in
`match-action` is general enough to serve every function that consults a
table. That reference scales with the match vocabulary rather than with the
feature set, which is what keeps it from rotting.
* Following the algebraic notation toward rigour. There is no inverse for
"transmit a session", and the nearest thing to one advances the clock until
transients decay. Chasing that ends in rebuilding a temporal logic. We want
to find defects, not prove their absence, so the notation is a naming scheme
for test shapes and nothing more.
* Putting the oracles at the boundary of the whole pipeline. An ACL that drops
traffic before the router sees it hides the router completely, and the
expectation becomes a cross-product over domains. Contracts belong to
individual network functions; pipeline behaviour is their composition.
The recurring theme is that an oracle derived from the same source as the
implementation cannot see that source being wrong, and that the way out is always
to find something genuinely independent -- a parser, a transport protocol, a
second walk over the same data.
Two constraints are recorded as requirements on work that has not started yet,
because both are cheap to honour in advance and expensive to retrofit: the
generation-propagation logic of the planned network-function DAG has to be a pure
state machine over a small hashable state, and a match-action rule has to name its
action completely enough to serve as the specification for it.
The note is a record under revision rather than settled doctrine; its open
questions are live, and several of them are questions about this repository that
nobody has answered yet.
Signed-off-by: Daniel Noland <daniel@githedgehog.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`StaticNatExpose` draws one expose, and one expose builds a table with one rule in
it. A property about a *lookup* wants several, because one rule gives a
longest-prefix match nothing to choose between.
Repeating the single-expose generator does not work. Two independent draws are
refused by a manifest almost every time, for two separate reasons:
* **Overlap.** Every expose is laid out from the same two bases -- 10.0.0.0 for
the private side and 172.16.0.0 for the public one -- so two of them cover the
same addresses and validation refuses the pair.
* **Address family.** A peering's manifests must agree on one family, so a v4
expose beside a v6 one is refused as well.
Both belong in the generator rather than in each caller, since both are facts
about what a manifest accepts. `StaticNatExposes` draws the family once and places
each expose in a block of its own, `BLOCK_STRIDE` apart -- wider than the widest
span one expose can occupy, so distinct blocks cannot collide whatever the draw.
Measured on the static NAT network function properties that motivated this, which
draw between one and three exposes: repeating `StaticNatExpose` validated 33% of
configurations, one block per expose 59%, one block and one family 100%. So two
thirds of the fuzzing budget was being spent building configurations that were
thrown away -- and, worse than the waste, multi-expose configurations were nearly
unreachable. The interesting case was the one being skipped.
`StaticNatExpose` is unchanged and still draws a single expose, so the existing
callers in `nat` and `mgmt` are untouched.
Signed-off-by: Daniel Noland <daniel@githedgehog.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The fuzzing so far has been small, focused and intrusive: it reaches into a
structure, exercises it directly, and asserts something about that structure. This
is the first of the other kind -- configure a network function, put generated
packets through it, and assert properties that would hold of any static NAT rather
than of this one.
Packets have to be drawn relative to the configuration. `Packet` has a
`TypeGenerator`, and pointing it at a generated NAT configuration is useless:
every packet misses every table, the fuzzer explores the miss path, and the run is
vacuous while looking enormous. This is the same failure the design note rejects
for configuration values, one level down, so it takes the same answer. The
configuration is a **parameter to resolution**, not a predicate to filter against.
A `ProbeSpec` is drawn with no reference to any configuration -- it is a handful of
indices -- and `resolve` interprets it against the built `Fabric`. Resolution is
total, so no draw is discarded and no rejection loop skews the distribution.
`acl-filter`'s `ProbeSpec` resolves against a built overlay the same way; this
generalises the shape to a stage that takes real packets.
The arrival state is the stage's precondition. `StaticNat` sits mid-pipeline and
assumes its predecessors annotated the packet: two vpc discriminants, the overlay
flag, and the flags saying which directions of translation are wanted. Nothing
says so in the type system -- `process` passes silently over a packet that lacks
them. `Arrival` writes it down once, which is what the design note asks for when it
puts contracts on network functions rather than on the pipeline: the assumption
travels with the stage. `masquerade`'s tests hand-roll the same thing as a mock
stage, and that is the drift this avoids.
`setup::config_driven` already proves the *mapping* right by enumeration; nothing
there touches a packet, its metadata, or `StaticNat`. These cover the half where
the decisions live, and none of them needs an oracle -- each is a metamorphic
relation or an invariant, so nothing here is a second copy of `RangeBuilder`:
* **round trip** -- a translated source comes back. The outbound packet is
rewritten by the local vpc's table, built from the local side of the peering;
the reply is rewritten by the peer's table, built from the remote side, by a
different code path. Whatever the first did, the second must undo. This is the
one property that ties the two halves together.
* **injectivity** -- distinct sources stay distinct, through the stage rather
than through the table. A collision is a tenant isolation defect.
* **frame** -- translating the source touches nothing else. The generated
exposes carry no port ranges, so a rewritten port would be a mapping reaching
further than it was configured to.
* **permission** -- nothing is translated that did not ask. Covers every reason:
not requested, already done, annotations missing or naming something absent,
source not exposed.
* **attribution** -- a packet that cannot be looked up is dropped with a
`DoneReason`, not passed silently. A silent pass forwards untranslated traffic
under a configuration that never mentioned it.
* **marking** -- a packet whose address changed carries `src_natted` and
`checksum_refresh`. Without the second it goes out with a checksum for an
address it no longer has, and is discarded by the receiver rather than by
anything that could report it.
`Packet::enforce` removes a dropped packet from the output, so probes carry `keep`:
without it a drop and a pass-through are the same event from outside, and
attribution could not be stated at all.
Every property counts the draws that reached its assertion and fails the run if
too few did, because the failure that matters is an assertion that stops running
rather than one that is wrong. That floor is a **ratio** -- at least one reaching
draw per two configurations built, plus a small absolute minimum -- rather than an
absolute count, because an absolute count measures how fast the machine was. A
property that reaches a couple of thousand draws on its own reaches a few dozen
under coverage instrumentation beside nine hundred other tests, and a floor tuned
to the fast case then fails for a reason that has nothing to do with the code
under test. Both counts scale with the iteration budget, so their ratio does not,
and a property that has genuinely stopped reaching its assertion still collapses
the ratio to zero -- which is the only thing the guard was ever for.
Signed-off-by: Daniel Noland <daniel@githedgehog.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…sses
Static NAT permits a port range on a prefix, and a prefix that carries one takes
the mapping down a second path entirely: `NatTableValue::Pat` and
`PortAddrTranslationValue` rather than `NatTableValue::Nat` and
`AddrTranslationValue`. The expose generator produced no port ranges, so that path
had no configuration-driven coverage at all.
The rule makes it the harder path. Validation asks that the two sides cover the
same **total**, counting addresses times ports, so a `/32` carrying 64 ports is a
legal answer to a `/30` carrying 16, and the mapping has to run across both
dimensions at once. That asymmetry is the reason the path exists, so
`StaticNatExposes::with_ports` draws it on purpose: one total per expose, divided
into addresses and ports independently per side, with both port ranges starting at
a drawn offset so a mapping that quietly assumes they begin at the same port fails
here.
Worth noting what is legal for static NAT and not for port forwarding, which
requires the two prefix lengths and the two port counts to match individually. The
two flavours do not share this rule and must not share a generator.
An address on its own is no longer a thing the configuration maps -- the
address-and-port pair is. So `Endpoint` replaces the bare address, carrying the
range its prefix declares, and a probe draws its port from that range rather than
freely, or it would miss. Three consequences:
* the reply in the round trip must be addressed to the **translated** port, since
that is the port the peer was contacted from;
* injectivity sweeps every pair rather than every address -- an address-only
sweep checks a diagonal of the space and calls it injective; and
* the frame differs between the paths. With no port range the transport ports are
part of the frame and must survive untouched; with one they are part of what is
being translated, and only the destination and protocol remain.
One property per flavour rather than one over a mix, following the same reasoning
as the NAT flavour properties in `mgmt`: a mixed property reaches each path
eventually, one that asks for a path reaches it every time and says in its name
which one failed. The two suites are mutually isolated -- a defect in
`PortAddrTranslationValue::get_entry` is invisible to the address properties and
vice versa -- which is what earns the extra properties their place rather than
re-running the same paths under new names.
Signed-off-by: Daniel Noland <daniel@githedgehog.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The second network function, and the first stateful one. Static NAT's answer for a
packet is fixed by its tables; masquerade's is whatever the allocator handed out
the first time it saw the flow, kept in the flow table and reused after. Every
property here is really about that state being kept consistently, which is a
different subject from anything the allocator's own tests can reach.
A probe is a flow, not a packet. The first packet of a flow allocates and writes a
flow entry; the second finds that entry and reuses it. Different code, and the
interesting properties relate the two. So `Probe::packet` hands back a fresh
packet on every call rather than being consumed once, because sending the same
flow twice is how the hot path is reached at all.
The stage order is load-bearing. `FlowLookup` attaches a flow entry only to a
packet whose `dst_vpcd` is **absent**, and the flow filter that sets `dst_vpcd`
runs *after* it. `Masquerade` then requires `dst_vpcd` to be present. So the
annotation has to arrive between the two stages -- not before both, not after.
Stamping both up front, the way the static NAT harness does, crashes nothing: it
quietly gives no packet any flow state, sends every packet down the allocation
path, and makes a flow look re-allocated on each packet. That is the sharper form
of the arrival-state point from the static NAT work. A network function's
precondition is not always a stamp a test can apply in one go -- here part of it is
supplied by a stage that must run *after* another stage that requires its absence,
and no test that ignores the ordering describes the real thing.
The three prerequisites the design note lists for comparing a stateful stage at
all, handled rather than assumed:
* **Seeded non-determinism** -- `set_randomize(false)`, or two fabrics built from
one configuration disagree on every flow.
* **Timers** -- rather than fake a clock, every property completes inside one flow
lifetime, so none depends on expiry either happening or not. Expiry is a
separate subject and wants the explicitly driven clock the note asks for, not a
wall clock a property happens to outrun.
* **Projections, not state** -- nothing here inspects the allocator or the flow
table. Every assertion is over what came out of the pipeline.
The properties also need a tokio runtime, since `FlowTable::insert` spawns a
per-flow expiry timer. The existing tests get one from `#[tokio::test]`; a bolero
body is synchronous, so it enters a runtime instead.
None of the properties predicts which address and port a flow will be given --
that is the allocator's business and predicting it would be a second copy of it:
* **reversibility** -- the reply comes back to where the flow started. Unlike
static NAT there is no second table built from the other side of the peering:
the reverse translation exists only because the forward packet recorded it. A
forward translation not faithfully recorded is a connection that never gets an
answer.
* **stability** -- a flow keeps the translation it was first given. A stage that
re-allocated would produce a legal-looking packet every time, and the
connection would break in a way no allocator-level test could see, because the
two allocations are individually correct.
* **exclusivity** -- two live flows never share a translation. Distinct source
ports as well as addresses, since masquerade collapses many private addresses
onto few public ones and the port is what keeps them apart after.
* **containment** -- every translation lands inside a range the configuration
named. The one property that consults the configuration, and legitimately: a
membership test, not a prediction of which member. An address from outside the
declared set is unroutable, so the flow is a blackhole that looks like success
from inside the box.
* **permission** and **attribution** -- as for static NAT. Permission matters more
here, because a translation is not merely applied but *recorded*: a packet
masqueraded without permission leaves an entry behind that keeps translating
its successors.
Reversibility is deliberately blind to an allocation outside the declared range:
it asserts the reverse undoes the forward, which stays true when both use the same
wrong address. Containment is what covers that.
`MasqueradeExposes` gets the same treatment `StaticNatExposes` did, for the same
two reasons -- `MasqueradeExpose` draws its base index freely, so two exposes
collide whenever their index ranges intersect, and independent draws mix address
families. One slot of four indices per expose, family drawn once.
The vacuity guard is a ratio rather than an absolute count, for the reason
recorded with the static NAT properties.
Signed-off-by: Daniel Noland <daniel@githedgehog.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…nction
`acl-filter/src/fuzz.rs` has the strongest oracle in this codebase: it evaluates
the validated configuration directly and compares that against the lowered tables,
so a lowering mistake cannot hide behind the thing it produced. What it never
touches is a packet. Every probe there is a `PacketSummary` handed straight to
`lookup`.
Two pieces of production code sit between a packet and that summary, and neither
had any coverage from a generated configuration:
* **`PacketSummary::try_from`**, which reads the five-tuple and both
discriminants out of the headers; and
* **`AclFilter::process_packet`**, which turns a verdict into a fate --
`DoneReason::AclDropped`, `invalidate_flows`, and the `is_overlay` gate
deciding whether any of it happens.
A field misread in the first of those is invisible to every existing property,
because none of them builds the packet that would be misread. This re-points the
existing generators rather than writing new ones. The `OverlaySpec` and
`ProbeSpec` are unchanged; a probe now becomes a packet and the answer is read off
the packet's fate. The oracle is the same `oracle_resolved_action`, asked the same
question, so this is a differential test over the packet path rather than a second
ACL.
* **stage verdict** -- a packet the configuration denies is dropped with
`AclDropped`; one it allows survives untouched. This tests the extraction
implicitly: a field read from the wrong place makes the stage judge a different
tuple from the one the oracle judged, and they disagree wherever that field
decides the answer.
* **summary round trip** -- the five-tuple read back is the one the packet was
built with. Direct rather than implicit, so it also catches the misread that
happens to be harmless for the ruleset drawn.
* **missing discriminant** -- a packet naming no destination vpc is refused as
`Unroutable`. An ACL is indexed by the vpc pair, so such a packet cannot be
judged at all, and letting it through applies no policy whatsoever.
* **underlay gate** -- traffic that is not overlay traffic is left alone. No ACL
in the configuration describes it.
Only TCP and UDP become packets. A probe drawing ICMP or an arbitrary next header
is counted and skipped rather than approximated, because a packet whose headers did
not match the summary it came from would make every disagreement meaningless. The
same goes for the generator's `CrossVersion` stray, which asks for a v4 source with
a v6 destination -- there is no such packet, and that case stays with the
summary-level properties where it belongs.
The vacuity guard here counts denials as well as arrivals: a run that only ever saw
permits would pass while the drop path -- the only path where the stage does
anything -- went entirely unexercised. Around a quarter of probes are denied in
practice. It is a ratio rather than an absolute count, for the reason recorded with
the static NAT properties.
Signed-off-by: Daniel Noland <daniel@githedgehog.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ting denials Two defects in one helper, both of which `nat`'s two copies of it already avoid. `cargo bolero` runs the test binary once with `CARGO_BOLERO_SELECT` set to find out which targets it holds; `check!()` registers and returns without drawing, so `report` ran with every count at zero and the vacuity guard refused the *selection*. These targets could not be fuzzed at all. The denial ratio is documented as catching a run that only ever saw permits. Three of the four properties bumped `denied` on the same line as `reached`, which reduces it to `reached * 20 >= reached`; `underlay_traffic_is_not_judged` did so having just asserted the packet was *not* denied. They now use a report that measures arrivals and does not claim to measure anything else. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Daniel Noland <daniel@githedgehog.com>
`Fabric::public` is what `a_translation_stays_inside_the_public_range` tests membership against, and it was computed from the *unvalidated* exposes. Validation collapses exclusion prefixes, so the raw `as_range` is a superset of what the allocator's pool is built from: a translation to an address the operator explicitly excluded would have passed. Latent, because the masquerade generator emits no exclusions -- which is the only reason the two agree, and not something the property should depend on. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Daniel Noland <daniel@githedgehog.com>
`fix(acl-filter): Let the acl properties be selected` says `nat`'s two copies of this helper already avoid the defect. Only one does, and by accident: `static_nat::fuzz` puts `check!()` at the body scope of `drive_*`, so bolero's `return` under `CARGO_BOLERO_SELECT` leaves the function before `report` runs. Here `check!()` is inside a `with_runtime` closure, so the `return` leaves only the closure and the vacuity guard fires on every count at zero. All six masquerade properties therefore failed `cargo bolero`'s target enumeration and could not be fuzzed at all. Signed-off-by: Daniel Noland <daniel@githedgehog.com>
… too The commit before last made this argument for masquerade and left the identical code in static NAT: validation collapses exclusion prefixes, so the raw expose lists are supersets of what the tables were built from. It is worse on this side. `private` is what `every_source` sweeps, so an excluded address in it fails `a_translated_source_comes_back` and `distinct_sources_stay_distinct` against a correct implementation -- a property that lies rather than one that misses. Latent either way: the generator emits no exclusions, which is the only reason the two agree. Both walks now take the offering vpc alone. `overlay_with_exposes` gives the peer a manifest of its own, and folding it in would have swept its 256 addresses as sources this configuration maps. Signed-off-by: Daniel Noland <daniel@githedgehog.com>
`packet_for` clamps port 0 to 1, because 0 is not a port either transport can carry. The oracle was still asked about the drawn summary, so its verdict on port 0 was compared against the stage's verdict on port 1 -- and a ruleset that distinguishes the two fails a correct implementation. `expected_summary` already existed for exactly this, and was used only by the round-trip property. Using it here restates nothing: that the packet carries this summary is what the round-trip property establishes. Signed-off-by: Daniel Noland <daniel@githedgehog.com>
The same guard as the masquerade one two commits back, and the same reason. Backported from `fix(nat): Let the nat properties be fuzzed at all` on pr/daniel-noland/driven-clock, whose other two files do not exist yet here. Not load-bearing today: `check!()` sits at the body scope of the `drive_*` helpers, so its `return` leaves the helper before `report` runs. It is here so that wrapping a property body in a closure later cannot quietly make these targets unselectable again, which is exactly how masquerade acquired the fault. Signed-off-by: Daniel Noland <daniel@githedgehog.com>
`bolero::check!()` names its target after the function it is written in, so the six drivers shared by these eleven tests registered six targets named after functions that are not tests -- listed by `cargo bolero list`, resolvable by nothing. Expanding the driver at each test site is what makes the name a test's. The case bodies are unchanged; only where the macro expands moved. Signed-off-by: Daniel Noland <daniel@githedgehog.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> (cherry picked from commit c00f66d)
The comment three lines above these asserts says an absolute floor measures how fast the machine was rather than anything about the property. The asserts carried one anyway, and coverage instrumentation duly failed it: `a_flow_that_cannot_be_masqueraded_says_so` reached 3 flows across 1 configuration, which satisfies `reached * 2 >= built` and misses `reached >= 8`. The ratio is the part that means something, and a property that has stopped reaching its assertion collapses to zero, which `reached > 0` catches. Signed-off-by: Daniel Noland <daniel@githedgehog.com>
daniel-noland
force-pushed
the
pr/daniel-noland/fuzz-config-generators
branch
from
August 28, 2026 17:33
71e4ecc to
440a6fa
Compare
daniel-noland
force-pushed
the
pr/daniel-noland/fuzz-nf-probes
branch
from
August 28, 2026 17:33
0472824 to
c49f3b8
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.