Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
096d24f
test(masquerade): Model-check the pools across a config change
daniel-noland Aug 5, 2026
823929a
fix(masquerade): Stop the allocator deadlocking against its own pool …
daniel-noland Aug 6, 2026
ee5dbf2
fix(masquerade): Keep printing the pool from wedging it too
daniel-noland Aug 6, 2026
4a8cc4a
build(fuzz): Add just recipes for libfuzzer campaigns
daniel-noland Aug 5, 2026
694ecfb
fix(masquerade): Only fall through to the next region on exhaustion
daniel-noland Aug 5, 2026
c86a190
fix(masquerade): Refuse an unmappable address instead of panicking
daniel-noland Aug 5, 2026
cc136fc
test(masquerade): Close the gap in the concurrent uniqueness oracle
daniel-noland Aug 5, 2026
090a2f7
fix(masquerade): Make an allocation a lease only one thing can hold
daniel-noland Aug 6, 2026
2ae8334
fix(masquerade): Stretch the flow timeouts when the tests run emulated
daniel-noland Aug 6, 2026
2efa890
test(masquerade): Cover two VPCs sharing a private prefix, end to end
daniel-noland Aug 6, 2026
4cf0eae
fix(masquerade): Honour every port-forwarding claim on an address
daniel-noland Aug 6, 2026
bfa935e
fix(masquerade): Claim port-forwarded ports in the public space
daniel-noland Aug 6, 2026
fb73cfa
fix(masquerade): Pass over an address that has no port to give
daniel-noland Aug 6, 2026
44862a6
fix(masquerade): Keep the count of usable port blocks honest
daniel-noland Aug 6, 2026
2053688
fix(masquerade): Tell a block claimed in full apart from a bookkeepin…
daniel-noland Aug 6, 2026
d5ac16a
fix(masquerade): Keep addresses port forwarding has used up out of th…
daniel-noland Aug 6, 2026
99eb70a
test(masquerade): Give the unusable-address property a real oracle
daniel-noland Aug 6, 2026
c03e7f5
test(masquerade): Model the masquerade-over-forwarded-ports fixture o…
daniel-noland Aug 6, 2026
67f2ea5
test(masquerade): Union every claim in the reserved-ports oracle
daniel-noland Aug 6, 2026
5726905
test(masquerade): Cover a port freed while its neighbours are still held
daniel-noland Aug 6, 2026
4027f95
test(masquerade): Hold what a racing reservation is given
daniel-noland Aug 6, 2026
9654b43
test(masquerade): Stop the concurrent model publishing generations th…
daniel-noland Aug 6, 2026
f60f8f0
fix(masquerade): Answer a reservation that raced its block's release
daniel-noland Aug 6, 2026
5a47ebc
refactor(masquerade): Find a pool without relying on disjoint prefixes
daniel-noland Aug 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
**.profraw
**/__fuzz__/**
# libfuzzer writes one of these per worker into the working directory when
# `just fuzz` is given -j; the corpus itself lives under __fuzz__.
fuzz-*.log
# qemu-user core dumps from SIGABRT under emulated tests.
**/qemu_*.core
result*
Expand Down
69 changes: 68 additions & 1 deletion development/code/running-tests.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,75 @@ change this.
The major downside is that these processes are very computationally intensive and can take a long time to run.
In fact, the [afl] fuzzer runs until you terminate it.

## Running a real fuzzing campaign

To run a target under [libfuzzer], which is coverage guided and explores far deeper than the random
driver the test suite uses, list the targets and pick one:

```shell
just fuzz-list -p dataplane-nat
just fuzz 'masquerade::apalloc::region::bolero_tests::decompose_properties' 10min -p dataplane-nat
```

The duration defaults to `60s`; anything after it is forwarded to `cargo bolero test`. As a sense of
the difference, a property that manages a few thousand cases per second under `just test` reaches
several hundred thousand per minute here, because libfuzzer mutates towards inputs that reach new
code rather than sampling blindly.

Findings are written to a `__fuzz__` directory beside the test. That directory is gitignored: the
corpus is a local artifact that seeds later runs on the same machine, not something to commit.

Pass `-j` to spread the campaign over more cores, which is the cheapest way to reach deeper:

```shell
just fuzz 'some::module::tests::some_property' 10min -p some-package -j 60
```

Each worker then writes a `fuzz-<n>.log` into the directory you ran from, rather than into
`__fuzz__`. Those are gitignored too, and are only worth reading when a run reports a crash.

### Sanitizers

`cargo bolero` builds with the `fuzz` profile and links [AddressSanitizer] unless told otherwise, so
a plain `just fuzz` is already an asan campaign. To swap sanitizers, set the same `sanitize`
variable the rest of the justfile uses:

```shell
just sanitize=thread fuzz 'some::module::tests::some_property' 5min -p some-package
```

[ThreadSanitizer] only reports on a target that actually spawns threads, so it is worth the extra
cost on a concurrency suite and close to pointless on a single-threaded property. It also takes
much longer to get going, because thread instrumentation changes the ABI: `just` therefore adds
`--build-std` for it, since a std left uninstrumented fails the build on a mismatch against `core`.

A sanitizer is not free. Instrumentation costs roughly a factor of four in executions per second,
so it is worth spending some of a campaign with none at all, reaching deeper into the input space
in exchange for only catching what the test's own assertions catch:

```shell
just sanitize=NONE fuzz 'some::module::tests::some_property' 30min -p some-package
```

The two are complementary: asan for memory errors the assertions cannot see, `NONE` for depth.

The suite as a whole can also be run under either sanitizer with the standard runner, which is what
CI's `sanitize/fuzz/*` jobs do:

```shell
just profile=fuzz sanitize=thread test
just profile=fuzz sanitize=address test
```

That covers far more code than a single fuzz target, but only with the brief random driver rather
than a real campaign. The two are complementary.

> [!NOTE]
> Dedicated `just` recipes for running full fuzz campaigns (with libfuzzer/afl) are planned for a future PR.
> `just fuzz` passes `--rustc-bootstrap`, because libfuzzer wants a nightly compiler for its
> sanitizer coverage flags while the pinned toolchain is stable. An [afl] recipe is still to come.

[AddressSanitizer]: https://clang.llvm.org/docs/AddressSanitizer.html
[ThreadSanitizer]: https://clang.llvm.org/docs/ThreadSanitizer.html

[README.md]: ../../README.md
[afl]: https://github.com/AFLplusplus/AFLplusplus
Expand Down
26 changes: 26 additions & 0 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,32 @@ test package="tests.all" *args: (build (if package == "tests.all" { "tests.all"
declare -r target="{{ if package == "tests.all" { "tests.all" } else { "tests.pkg." + package } }}"
cargo nextest run --archive-file results/${target}/*.tar.zst --workspace-remap $(pwd) {{ filter }}

# List the bolero targets `just fuzz` can run. Args go to `cargo bolero list`
[script]
fuzz-list *args="":
{{ _just_debuggable_ }}
cargo bolero list {{ _cargo_feature_flags }} {{ args }}

# Fuzz one bolero target under libfuzzer. See development/code/running-tests.md
[script]
fuzz target time="60s" *args="":
{{ _just_debuggable_ }}
# libfuzzer wants a nightly compiler for its sanitizer coverage flags, while the
# pinned toolchain is stable; --rustc-bootstrap bridges that. cargo-bolero already
# builds with the fuzz profile and links AddressSanitizer unless told otherwise, so
# a plain `just fuzz` is already an asan run. Findings land in a gitignored
# `__fuzz__` directory beside the test.
#
# `sanitize=thread` additionally rebuilds std: thread instrumentation changes the
# ABI, so a std left uninstrumented fails the build on a mismatch against `core`.
# asan does not need that, and skipping the std rebuild keeps it far quicker.
# `sanitize=NONE` drops instrumentation altogether, which buys roughly four times
# the executions per second in exchange for only catching what the test asserts.
cargo bolero test '{{ target }}' --rustc-bootstrap -T '{{ time }}' \
{{ if sanitize != "" { "--sanitizer " + sanitize } else { "" } }} \
{{ if sanitize == "thread" { "--build-std" } else { "" } }} \
{{ _cargo_feature_flags }} {{ args }}

# Build and run the criterion benches. The rte_acl benches are gated behind the
# `dpdk` feature, so run `just features=dpdk bench` to exercise them; a plain
# `just bench` builds them as empty `main()` and only runs the reference benches.
Expand Down
26 changes: 26 additions & 0 deletions nat/src/masquerade/allocation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,32 @@ pub enum AllocatorError {
NoPoolFound,
}

impl AllocatorError {
/// Whether this error says the space simply ran out, rather than that something is wrong.
///
/// A caller holding several allocators over disjoint space may move on to the next one when
/// this holds, and only then: any other error is about the allocator rather than about how
/// full it is, and would be buried by a later success. The classification is the one
/// [`DoneReason`] already draws, where exactly these become `NatOutOfResources`.
/// The match is exhaustive on purpose: a new error has to be classified here rather than
/// silently defaulting to one side of it.
#[must_use]
pub fn is_exhaustion(&self) -> bool {
match self {
AllocatorError::NoFreeIp
| AllocatorError::NoPortBlock
| AllocatorError::NoFreePort(_) => true,
AllocatorError::PortAllocationFailed(_)
| AllocatorError::PortReservationFailed(_)
| AllocatorError::UnsupportedProtocol(_)
| AllocatorError::MissingDiscriminant
| AllocatorError::InternalIssue(_)
| AllocatorError::Denied
| AllocatorError::NoPoolFound => false,
}
}
}

impl From<&AllocatorError> for DoneReason {
fn from(error: &AllocatorError) -> Self {
match error {
Expand Down
Loading
Loading