Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
6f8d490
feat(clock): reject wall-clock reads during virtual tests
daniel-noland Aug 24, 2026
914dfb9
build(semgrep): forbid clock reads during Drop
daniel-noland Aug 24, 2026
cffdff6
feat(tracectl): timestamp logs with the active test clock
daniel-noland Aug 24, 2026
a93c743
test(dataplane): check flows across scheduled time advances
daniel-noland Aug 24, 2026
7bf8caf
feat(clock): propagate test clocks to spawned threads
daniel-noland Aug 24, 2026
cbabca0
fix(dataplane): poll timer tasks during fuzzing
daniel-noland Aug 25, 2026
6f70748
fix(nat): make NAT properties reachable by the fuzzer
daniel-noland Aug 25, 2026
ceca5a0
build(just): compare the effective fuzz sanitizer
daniel-noland Aug 25, 2026
883fb0f
fix(net): expose flow-info properties as fuzz targets
daniel-noland Aug 25, 2026
da90379
build: enable Bolero's std feature explicitly
daniel-noland Aug 26, 2026
6fa72da
fix(net): register each header shard as a fuzz target
daniel-noland Aug 25, 2026
e537369
fix(fuzz): give shared properties selectable target names
daniel-noland Aug 25, 2026
25d8b65
build(just): restore flags hidden by cargo-bolero
daniel-noland Aug 25, 2026
391f974
build: compose instrumentation independently of profiles
daniel-noland Aug 25, 2026
120ae1d
build: make fuzz-instrumented sysroots link
daniel-noland Aug 25, 2026
0533c32
fix(clock): keep nested clock guards armed
daniel-noland Aug 27, 2026
785cfed
fix(concurrency): remove imports left by the fuzz refactor
daniel-noland Aug 27, 2026
5afdeeb
style(dataplane): stop bypassing the concurrency facade
daniel-noland Aug 28, 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
6 changes: 3 additions & 3 deletions .github/workflows/dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ jobs:
uses: *gate
with:
labels: "test/all-profiles"
on-value: '["debug", "release", "fuzz"]'
on-value: '["debug", "release", "checked"]'
off-value: '["debug"]'

# Lab jobs require release images but not other release/fuzz checks.
Expand Down Expand Up @@ -202,9 +202,9 @@ jobs:
matrix:
profile: "${{ fromJSON(needs.plan.outputs.profiles) }}"
exclude:
# Fuzz repeats the release compile here; coverage, sanitizers, and
# `checked` repeats the release compile here; coverage, sanitizers, and
# fuzzing jobs already exercise that profile.
- profile: "fuzz"
- profile: "checked"
steps:
- *checkout

Expand Down
27 changes: 27 additions & 0 deletions .semgrep/rules/no-clock-read-in-drop.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
rules:
- id: rust-no-clock-read-in-drop
languages: [rust]
severity: ERROR
message: |
Do not read the clock from a `Drop` implementation.

Once anything in the process has paused the virtual clock, tokio routes
every read through the calling thread's runtime context. A `Drop` that
runs during thread-local teardown may find that context already
destroyed, and tokio's response is a panic inside a destructor -- which
aborts the process rather than failing the test.

Take the reading before the value is dropped and pass it in, or record
the instant when the value is created.
paths:
exclude:
- .codeql/tests/
- clock/src/
patterns:
- pattern-inside: |
fn drop(&mut self) {
...
}
- pattern-either:
- pattern: clock::now()
- pattern: clock::system_now()
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ overflow-checks = false
codegen-units = 1
rpath = true

[profile.fuzz]
[profile.checked]
inherits = "release"
opt-level = 2
debug-assertions = true
Expand Down
166 changes: 84 additions & 82 deletions acl/tests/property_predicate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -211,100 +211,102 @@ where
}
const MIN_ASSERTED_HITS: u64 = 20;
const MIN_ASSERTED_MISSES: u64 = 20;
fn run_property<A, T>(
name_prefix: &str,
install_dpdk: impl Fn(String, &FiveTupleRule<A>) -> T + core::panic::RefUnwindSafe,
) where
A: KeyAddr,
PrefixSpec<A>: FieldHit<A> + FieldMiss<A> + IsUniversal,
T: Lookup<FiveTuple<A>, Verdict>,
RawRule<A>: TypeGenerator,
{
let asserted_hits = AtomicU64::new(0);
let asserted_misses = AtomicU64::new(0);
macro_rules! run_property {
($a:ty, $name_prefix:expr, $install_dpdk:expr) => {{
let asserted_hits = AtomicU64::new(0);
let asserted_misses = AtomicU64::new(0);

bolero::check!()
.with_type::<(RawRule<A>, Box<[u8]>, Box<[u8]>)>()
.for_each(|(raw, hit_bytes, miss_bytes)| {
let rule = build_rule(raw);
let dpdk = install_dpdk(unique_name(name_prefix), &rule);
let reference = ReferenceTable::<FiveTuple<A>, Verdict>::new(vec![RefRule::new(
rule.into_backend_fields::<Erased>(),
Verdict::Drop,
)]);

let hits = HitsGen { rule };
let n_hits = sweep(&hits, hit_bytes, |k| {
assert!(rule.accepts(k), "hits gen produced a rejected key: {k:?}");
assert_eq!(reference.lookup(k), Some(&Verdict::Drop));
assert_eq!(dpdk.lookup(k), Some(&Verdict::Drop));
});
asserted_hits.fetch_add(n_hits, Ordering::Relaxed);
bolero::check!()
.with_type::<(RawRule<$a>, Box<[u8]>, Box<[u8]>)>()
.for_each(|(raw, hit_bytes, miss_bytes)| {
let rule = build_rule(raw);
let dpdk = $install_dpdk(unique_name($name_prefix), &rule);
let reference = ReferenceTable::<FiveTuple<$a>, Verdict>::new(vec![RefRule::new(
rule.into_backend_fields::<Erased>(),
Verdict::Drop,
)]);

if !rule.is_universal() {
let misses = MissesGen { rule };
let n_misses = sweep(&misses, miss_bytes, |k| {
assert!(
!rule.accepts(k),
"misses gen produced an accepted key: {k:?}",
);
assert_eq!(reference.lookup(k), None);
assert_eq!(dpdk.lookup(k), None);
let hits = HitsGen { rule };
let n_hits = sweep(&hits, hit_bytes, |k| {
assert!(rule.accepts(k), "hits gen produced a rejected key: {k:?}");
assert_eq!(reference.lookup(k), Some(&Verdict::Drop));
assert_eq!(dpdk.lookup(k), Some(&Verdict::Drop));
});
asserted_misses.fetch_add(n_misses, Ordering::Relaxed);
}
});
asserted_hits.fetch_add(n_hits, Ordering::Relaxed);

let h = asserted_hits.load(Ordering::Relaxed);
let m = asserted_misses.load(Ordering::Relaxed);
assert!(
h >= MIN_ASSERTED_HITS,
"asserted only {h} hits (< {MIN_ASSERTED_HITS}); generator may have gone inert",
);
assert!(
m >= MIN_ASSERTED_MISSES,
"asserted only {m} misses (< {MIN_ASSERTED_MISSES}); generator may have gone inert",
);
if !rule.is_universal() {
let misses = MissesGen { rule };
let n_misses = sweep(&misses, miss_bytes, |k| {
assert!(
!rule.accepts(k),
"misses gen produced an accepted key: {k:?}",
);
assert_eq!(reference.lookup(k), None);
assert_eq!(dpdk.lookup(k), None);
});
asserted_misses.fetch_add(n_misses, Ordering::Relaxed);
}
});

let h = asserted_hits.load(Ordering::Relaxed);
let m = asserted_misses.load(Ordering::Relaxed);
assert!(
h >= MIN_ASSERTED_HITS,
"asserted only {h} hits (< {MIN_ASSERTED_HITS}); generator may have gone inert",
);
assert!(
m >= MIN_ASSERTED_MISSES,
"asserted only {m} misses (< {MIN_ASSERTED_MISSES}); generator may have gone inert",
);
}};
}

#[test]
#[dpdk::with_eal]
fn property_v4() {
run_property::<Ipv4Addr, FiveTupleTableV4<Verdict>>("prop_v4", |name, rule| {
install_table(
&name,
NonZero::new(2).expect("nonzero"),
vec![
RuleSpec::<FiveTuple<Ipv4Addr>, Verdict>::new(
Priority::new(1).expect("nonzero priority"),
CategoryMask::new(1).expect("nonzero mask"),
rule.into_backend_fields::<Dpdk>(),
Verdict::Drop,
)
.expect("RuleSpec"),
],
)
.expect("install_table")
});
run_property!(
Ipv4Addr,
"prop_v4",
|name: String, rule: &FiveTupleRule<Ipv4Addr>| {
install_table(
&name,
NonZero::new(2).expect("nonzero"),
vec![
RuleSpec::<FiveTuple<Ipv4Addr>, Verdict>::new(
Priority::new(1).expect("nonzero priority"),
CategoryMask::new(1).expect("nonzero mask"),
rule.into_backend_fields::<Dpdk>(),
Verdict::Drop,
)
.expect("RuleSpec"),
],
)
.expect("install_table")
}
);
}

#[test]
#[dpdk::with_eal]
fn property_v6() {
run_property::<Ipv6Addr, FiveTupleTableV6<Verdict>>("prop_v6", |name, rule| {
install_table(
&name,
NonZero::new(2).expect("nonzero"),
vec![
RuleSpec::<FiveTuple<Ipv6Addr>, Verdict>::new(
Priority::new(1).expect("nonzero priority"),
CategoryMask::new(1).expect("nonzero mask"),
rule.into_backend_fields::<Dpdk>(),
Verdict::Drop,
)
.expect("RuleSpec"),
],
)
.expect("install_table")
});
run_property!(
Ipv6Addr,
"prop_v6",
|name: String, rule: &FiveTupleRule<Ipv6Addr>| {
install_table(
&name,
NonZero::new(2).expect("nonzero"),
vec![
RuleSpec::<FiveTuple<Ipv6Addr>, Verdict>::new(
Priority::new(1).expect("nonzero priority"),
CategoryMask::new(1).expect("nonzero mask"),
rule.into_backend_fields::<Dpdk>(),
Verdict::Drop,
)
.expect("RuleSpec"),
],
)
.expect("install_table")
}
);
}
6 changes: 3 additions & 3 deletions ci.just
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ check-doctest profile:
check-docs profile:
just {{ _lab }} profile={{ profile }} docs

sanitize san profile="fuzz":
sanitize san profile="checked":
just {{ if profile == "debug" { _lab } else { _lab-lto } }} profile={{ profile }} sanitize={{ san }} test

test-each profile="debug":
Expand All @@ -95,10 +95,10 @@ coverage profile="debug":
just {{ if profile == "debug" { _lab } else { _lab-lto } }} profile={{ profile }} instrument=coverage coverage-archive

# Optimized fuzz builds let schedule explorers cover more interleavings.
shuttle profile="fuzz":
shuttle profile="checked":
just {{ if profile == "debug" { _lab } else { _lab-lto } }} profile={{ profile }} features=shuttle test

loom profile="fuzz":
loom profile="checked":
just {{ if profile == "debug" { _lab } else { _lab-lto } }} profile={{ profile }} features=loom test

wasm:
Expand Down
6 changes: 6 additions & 0 deletions clock/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,9 @@ virtual = ["dep:tokio"]

[dependencies]
tokio = { workspace = true, optional = true, features = ["test-util", "time"] }

[dev-dependencies]
concurrency = { workspace = true }

[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(wall_clock)'] }
39 changes: 39 additions & 0 deletions clock/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Open Network Fabric Authors

use std::process::Command;
use std::{env, fs, path::PathBuf};

fn main() {
println!("cargo::rerun-if-env-changed=RUSTC_BOOTSTRAP");
println!("cargo::rustc-check-cfg=cfg(has_spawn_hook)");

let out = PathBuf::from(env::var_os("OUT_DIR").expect("cargo sets OUT_DIR"));
let probe = out.join("spawn_hook_probe.rs");
if fs::write(
&probe,
"#![feature(thread_spawn_hook)]\n\
pub fn probe() { std::thread::add_spawn_hook(|_| || {}); }\n",
)
.is_err()
{
return;
}

let rustc = env::var_os("RUSTC").unwrap_or_else(|| "rustc".into());
let accepted = Command::new(rustc)
.args(["--crate-type=lib", "--emit=metadata", "-o"])
.arg(out.join("spawn_hook_probe.rmeta"))
.arg(&probe)
.status()
.is_ok_and(|status| status.success());

if accepted {
println!("cargo::rustc-cfg=has_spawn_hook");
} else {
println!(
"cargo::warning=thread_spawn_hook is unavailable, so a test that drives the clock \
cannot check the threads it spawns. Set RUSTC_BOOTSTRAP=1 (the dev shell does)."
);
}
}
Loading
Loading