8000 T18 — Decide every frame with one pure verdict and enforce own-address egress at the relay by minimal-foundry[bot] · Pull Request #1700 · gominimal/minimal · GitHub
[go: up one dir, main page]

Skip to content

T18 — Decide every frame with one pure verdict and enforce own-address egress at the relay - #1700

Merged
norrietaylor merged 11 commits into
mainfrom
agent/1629-decide-every-frame-with-one-pure-verdict-and-enf
Sep 25, 2026
Merged

norrietaylor merged 11 commits into
mainfrom
agent/1629-decide-every-frame-with-one-pure-verdict-and-enf

Conversation

@minimal-foundry
Copy link
Copy Markdown
Contributor

T18 — Decide every frame with one pure verdict and enforce own-address egress at the relay

slice: S4
kind: task
covers: NET-062, NET-063, NET-064
size: L
est: 450 code, 300 tests
executor: agent
difficulty: needs care
model: strongest
diagnostics: the bundle's daemon log tail carries each rate-limited drop with the session, the destination, the protocol and the rule
observability: one rate-limited warn log line per dropped connection keyed by box and rule, with the direction, the destination and the protocol
layers:
- core:
- crates/sessions/src/core/egress.rs (new): the pure frame verdict over an owned frame summary and owned rules, letting ARP and DNS to the gateway through and dropping IPv6, with its Kani harness
- crates/sessions/src/core/mod.rs: declares the egress module
- scripts/kani.sh: expects one more sessions harness
test (deferred: kani): cargo nextest run -p sessions kani_frame_verdict_admits_nothing_undeclared
- daemon:
- crates/minimald/src/net/switch.rs: relay_tap_to_switch applies the verdict and drops without answering; only admitted frames open a UDP conntrack window
- crates/minimald/src/net/policy.rs: PolicyWarnLimiter keys its limit by box and rule
- crates/minimald/src/net/gvproxy_network.rs: passes the policy into the attach
- crates/minimald/src/net/provider.rs: network_for and the own-address network carry the whole session policy instead of ingress alone
- crates/minimald/src/session_host.rs: the launcher's ingress field becomes the session policy
- crates/minimald/src/session.rs: fills the launcher's policy
- crates/minimald/tests/netns_root_integration.rs: the attach call follows the new signature
test: cargo nextest run -p minimald disallowed_egress_dropped_not_reset
test: cargo nextest run -p minimald egress_drop_logged_rate_limited
test: cargo nextest run -p minimald allowed_egress_succeeds
test: cargo nextest run -p minimald udp_dropped_when_only_tcp_allowed
produces:
- crates/sessions/src/core/egress.rs
consumes: none
proof: An own-address box's connection to an undeclared destination drops without a reset and appears once in the log, an allowed one completes, and UDP is dropped when only TCP is allowed.
depends on:
- blocked by #1617

Proof

An own-address box's connection to an undeclared destination now dies at the session relay without any reset and with exactly one rate-limited warn line naming the box, the destination, the protocol and the rule; a declared one reaches the switch verbatim; and a UDP datagram is dropped when only TCP is allowed, its would-be reply refused too. The decision itself is one pure function in sessions::core::egress, exhaustified over a 40-byte IPv4+L4 header and at most 4 rules per dimension behind the Kani harness.

Notes from the agent: Deferred (lane: kani): cargo nextest run -p sessions kani_frame_verdict_admits_nothing_undeclared — the harness is written in crates/sessions/src/core/egress.rs (#cfg(kani), iff against a restated declared-expression, unwind bound 4) and scripts/kani.sh now expects the seventh sessions harness, but cargo-kani is not installed on this box so nothing was run for it. Out of layers, disclosed: crates/minimald/src/exec.rs — one assertion string in a_task_takes_the_network_of_its_session follows the Debug field that provider.rs renamed (has_ingress -> has_policy); the full-crate run caught it and the fix is committed as 2fca306. The netns root proofs (#[ignore], MINIMALD_NETNS_TEST-gated) now attach their Ptasks through a gated relay and declare the ports their listeners sit on (9009 for ptask-to-ptask, 80 for the ingress proof), so they exercise the enforcement the production attach applies; they compile but need root + gvproxy and were not run here. attach_to_switch/attach_to_switch_vsock keep an Option-typed gate so the daemon's own relay (guest.rs) and task networks (exec.rs -> network_for) still pass None and stay ungated, unchanged.

Tests

line exit tail
cargo nextest run -p minimald disallowed_egress_dropped_not_reset 0 Applying loadouts: default Running task runtime-implement-test-0 in session task-runtime-implement-test-0-64a6... Compiling args v0.0.1 (/home/runner/.local/state/minimal/sessions/6e3c8/tree/crates/args) Compiling ot v0.0.1 (/home/runner/.local/state/minimal/sessions/6e3c8/tree/crates/ot) C
cargo nextest run -p minimald egress_drop_logged_rate_limited 0 Applying loadouts: default Running task runtime-implement-test-1 in session task-runtime-implement-test-1-9b18... Compiling stdlib v0.0.20 (/home/runner/.local/state/minimal/sessions/7c954/tree/crates/stdlib) Compiling version v0.6.0 (/home/runner/.local/state/minimal/sessions/7c954/tree/crate
cargo nextest run -p minimald allowed_egress_succeeds 0 Applying loadouts: default Running task runtime-implement-test-2 in session task-runtime-implement-test-2-0bce... Compiling stdlib v0.0.20 (/home/runner/.local/state/minimal/sessions/aa809/tree/crates/stdlib) Compiling version v0.6.0 (/home/runner/.local/state/minimal/sessions/aa809/tree/crate
cargo nextest run -p minimald udp_dropped_when_only_tcp_allowed 0 Applying loadouts: default Running task runtime-implement-test-3 in session task-runtime-implement-test-3-5e4b... Compiling stdlib v0.0.20 (/home/runner/.local/state/minimal/sessions/14d3e/tree/crates/stdlib) Compiling version v0.6.0 (/home/runner/.local/state/minimal/sessions/14d3e/tree/crate

Verify

min task run agent-verify passed ([tasks.verify]).

Deferred

The plan marked these test lines deferred: the executor lacks the lane, so nothing here ran them and they still owe evidence.

line lane
cargo nextest run -p sessions kani_frame_verdict_admits_nothing_undeclared kani

Out of scope

These files are outside the task's layers:, so this PR is a draft:

  • crates/minimald/src/exec.rs

Closes #1629

NET-062, NET-063, NET-064 reduce to one admit-or-drop decision over an
owned frame summary and an owned rule set, so that is what core/egress
now is: summarize an Ethernet frame into its family, IPv4 destination,
protocol, and L4 destination port, then match it against the box's
compiled rules.

The families decided without rules are fixed: ARP is a declared path for
every box, IPv6 is dropped as a family (no v1 admission path exists,
NET-082/NET-136), any other EtherType drops (VLAN tags included), and a
frame too short to read is never declared. IPv4 is decided by
allow_protocols, then deny_subnets carved out of allow_subnets, with
one carve-out that precedes them all: UDP to the resolver Minimal owns
for the box, at its address and port, is admitted under any declaration
including deny-all (NET-079, NET-134).

The decision is exhaustive behind a Kani harness over a 40-byte
IPv4+L4 header and at most 4 rules per dimension, stated as an iff
against the declared half so neither arm can silently become
unreachable; scripts/kani.sh expects the seventh sessions harness.
NET-062, NET-063 and NET-064 land where a box's traffic actually
leaves: relay_tap_to_switch decides every frame with the pure verdict
in sessions::core::egress before it reaches the shared switch, drops
without answering (nothing is written back toward the box), and says
so once per box per rule per minute. Only a frame the verdict admits
opens a UDP conntrack window, so an undeclared datagram can no longer
punch a hole in the inbound gate.

The gate the relay runs is now the session's whole policy:
IngressGate becomes SessionGate, carrying the compiled egress rules
beside the inbound default-block, the shared conntrack, and one
PolicyWarnLimiter for both legs and the NET-004 notice. The limiter
keys its rate limit by box and rule (a flood from one box neither
silences a neighbour's single drop nor a second rule's first line),
and the warn line renders the transport the drop carried via the new
Proto enum (tcp/udp/icmp by name, ip-N by number, ipv6/none for the
drops with no L4 header to name).

The attach chain follows: network_for and the launcher carry
SessionPolicy instead of the ingress half alone, so the egress
declaration a session validated at launch reaches its relay. The netns
proofs' Ptasks attach through a gated relay and declare the ports
their listeners sit on, driving the same wiring a session's own
attach does.
The task-network proof asserts the provider's Debug render; the
gate's field is the whole session policy now, so the render it
checks follows: has_ingress -> has_policy.
@coderabbitai
coderabbitai Bot commented Sep 25, 2026 •
Copy link
Copy Markdown
Contributor

Important

Review skipped

Bot user detected.

To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: dee46d8e-1237-408d-bc1b-5465453ab2f9

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.

@norrietaylor
Copy link
Copy Markdown
Member

Babysitter review of T18 against the plan block in #1629, at 2fca306.

Verdict: revise once, then ready. All ten layers: intents are met. summarize and verdict in sessions::core::egress do no I/O, read no clock and allocate nothing per frame; rules compile once per gate; the only enforcement site is relay_tap_to_switch, where a drop skips the frame and writes nothing back toward the box, so nothing resets; the UDP conntrack window opens only after admission; the daemon's own relay and task networks stay ungated. All four test lines drive a real spawn_relay through a socketpair and exit 0, none self-skips. The single out-of-layer file, crates/minimald/src/exec.rs, is a one-line test string forced by the has_ingress to has_policy rename, so the draft is procedural and I will un-draft once the round below lands.

Medium crates/minimald/src/net/policy.rs:552-565 with crates/minimald/src/net/switch.rs:928: PolicyWarnLimiter now keys on (box, rule) and never evicts, while the ingress caller passes "no ingress mapping for {proto} dst port {dst_port}" as the rule. A peer scanning ports gets one warn line per port per minute per box, up to the whole port space, and the nested map grows for the life of the session. Before this PR it was one line per minute per gate. Egress callers use fixed rule strings and are fine.

Low

  • verdict_ipv4 admits DNS to the resolver over UDP only, and a test pins TCP/53 as dropped. A box with tight rules loses the DNS-over-TCP fallback for truncated answers. Whether that is what NET-079/NET-134 mean is a spec call, not a code defect.
  • The Kani harness restates the verdict as its "declared" check and fixes the frame type to IPv4; ARP, IPv6, other-family and truncated frames are proven by unit tests only.
  • crates/minimald/src/session.rs:2159-2162 says validate_policy rejects either policy half on any other mode; it rejects egress only on NoNet, and host-address egress is allowed (NET-120).
  • Admitted UDP frames are parsed twice (summarize, then parse_ipv4_l4), no allocation, so a nit.

@norrietaylor
Copy link
Copy Markdown
Member

/revise One fix in files the task already owns:

  1. crates/minimald/src/net/switch.rs:928: the ingress drop must not put the port into the limiter's rule key. Pass a fixed rule string such as "no ingress mapping" and keep {proto} and {dst_port} in the structured fields the warn already carries (remote_addr has the source; add the destination port as its own field if warn lacks one). The intent is one warn line per box per gate per minute regardless of how many ports a peer probes, and a PolicyWarnLimiter map whose size is bounded by the number of distinct rule strings.
  2. Add a unit test beside the existing limiter tests in crates/minimald/src/net/policy.rs that two inbound drops to different ports within WARN_MIN_INTERVAL yield exactly one emission.
  3. crates/minimald/src/session.rs:2159-2162: correct the comment. validate_policy rejects egress only on NoNet; a host-address box may carry egress rules (NET-120), and only the ingress half is own-address-only.

Do not touch anything else. Keep the four test lines and the Kani line passing.

@norrietaylor
Copy link
Copy Markdown
Member

/revise Round 1 died in the runner after two hours with nothing pushed; the branch is now merged with main at 72cbde3 (one test-literal conflict in switch.rs, plus dynamic_ingress: None on the two new IngressPolicy literals in the netns test). Three fixes in files the task already owns, smallest first:

  1. Kani lane is red. crates/sessions/src/core/egress.rs:792 and :808 use if !kani::any(), which Kani 0.68 rejects with E0282 (type annotations needed). Write kani::any::<bool>() in both places. Run scripts/kani.sh locally before pushing; that is the failing check.
  2. crates/minimald/src/net/switch.rs:928: the ingress drop must not put the port into the limiter's rule key. Pass a fixed rule string such as "no ingress mapping" and keep {proto} and {dst_port} in the structured fields the warn already carries (add the destination port as its own field if warn lacks one). Intent: one warn line per box per gate per minute regardless of how many ports a peer probes, and a PolicyWarnLimiter map bounded by the number of distinct rule strings. Add a unit test beside the existing limiter tests in crates/minimald/src/net/policy.rs that two inbound drops to different ports within WARN_MIN_INTERVAL yield exactly one emission.
  3. crates/minimald/src/session.rs:2159-2162: correct the comment. validate_policy rejects egress only on NoNet; a host-address box may carry egress rules (NET-120), and only the ingress half is own-address-only.

Do not touch anything else. Keep the four test lines and the Kani line passing. Push early: commit and push after fix 1 before starting fix 2, so a session that runs out of time still delivers the lane fix.

@norrietaylor
Copy link
Copy Markdown
Member

Babysitter: round 2 ran out its two-hour budget with nothing pushed, the same as round 1, so the three items in the /revise note were applied directly on the branch instead of a third round: c9d4f7c (kani::any::<bool>() in both bounded generators), 76611af (fixed "no ingress mapping" rule key, dst_port as a structured field on PolicyWarnLimiter::warn, unit test warn_limiter_keys_ingress_drops_by_rule_not_port), c9d2f3a (the policy-validation comment now says egress is rejected only on NoNet and only ingress forwards are own-address-only).

Verified on macOS: cargo fmt --check clean, cargo test -p sessions 507 passed. minimald does not build on macOS, so the kani, tests, and root-integration lanes are the proof for the second and third commits. needs-human removed from the task. Un-draft and merge gate follow green lanes and the bot review.

norrietaylor and others added 4 commits September 25, 2026 06:49
The frame-verdict proof scans rule lists of symbolic length (at most 4
per dimension) but carried no unwind bound, so CBMC unwound the scans
without end: the kani lane ran the full 60-minute job timeout on
c9d2f3a and failed. With #[kani::unwind(5)] the harness verifies in
about 7 minutes locally (kani 0.68.0, cadical); the other nine
harnesses still solve in seconds.
The frame-verdict proof built each rule list with a symbolic element
count (n <= 4 assumed, pushed inside a 4-iteration loop), so every scan
CBMC unwound had a trip count it could not read off the code, and the
solve outlived the kani lane: run 36136625090 died on the job's
60-minute timeout, then runs 36143464955 and 36159156083 were each
cancelled nine minutes in with the harness still grinding. An unwind
bound does not help, because the blow-up is the symbolic length, not
the unwinding.

The harness now keeps every list at a concrete length (undeclared,
declared empty, or two symbolic rules) and pins every frame byte the
decision does not read — the MACs and the L4 around the port — leaving
symbolic exactly the fragment offset, the protocol, the destination
and the L4 destination port, at an unwind bound of 4. The property is
unchanged and still an iff over the same summary and rules: a frame is
admitted exactly when the rules declare it, resolver carve-out first.

Refs: #1700
Locally, with the affordable shape from the previous commit, the
harness verifies — and fails one unwinding assertion on the way:
comparing [u8; 4] addresses lowers to CBMC's builtin memcmp, a 4-trip
loop, so a bound of 4 trips the assertion even though every loop the
harness itself writes (rule scans over lists of at most two rules) has
exited by its third check. The bound has to clear every trip the
compiler generates, not only the ones the source shows, so it is now 6.

With that, scripts/kani.sh passes on a 4-core host: sessions 7/7
harnesses (the frame verdict in 40s, the other six in well under a
second) and rcache 3/3, where before this round the harness never came
back inside the lane's budget at all.

Refs: #1700
@minimal-foundry
Copy link
Copy Markdown
Contributor Author

Round 4 of 10 on a failed check suite, pushed as 12770c6afd9c.

line exit tail
cargo nextest run -p minimald disallowed_egress_dropped_not_reset 0 Applying loadouts: default Running task runtime-implement-test-0 in session task-runtime-implement-test-0-2aeb... Compiling ot v0.0.1 (/home/runner/.local/state/minimal/sessions/27914/tree/crates/ot) Compiling args v0.0.1 (/home/runner/.local/state/minimal/sessions/27914/tree/crates/args) C
cargo nextest run -p minimald egress_drop_logged_rate_limited 0 Applying loadouts: default Running task runtime-implement-test-1 in session task-runtime-implement-test-1-4d77... Compiling stdlib v0.0.20 (/home/runner/.local/state/minimal/sessions/f3383/tree/crates/stdlib) Compiling version v0.6.0 (/home/runner/.local/state/minimal/sessions/f3383/tree/crate
cargo nextest run -p minimald allowed_egress_succeeds 0 Applying loadouts: default Running task runtime-implement-test-2 in session task-runtime-implement-test-2-c91a... Compiling stdlib v0.0.20 (/home/runner/.local/state/minimal/sessions/6c76b/tree/crates/stdlib) Compiling version v0.6.0 (/home/runner/.local/state/minimal/sessions/6c76b/tree/crate
cargo nextest run -p minimald udp_dropped_when_only_tcp_allowed 0 Applying loadouts: default Running task runtime-implement-test-3 in session task-runtime-implement-test-3-2bf2... Compiling stdlib v0.0.20 (/home/runner/.local/state/minimal/sessions/92507/tree/crates/stdlib) Compiling version v0.6.0 (/home/runner/.local/state/minimal/sessions/92507/tree/crate

Verify min task run agent-verify exited 1.

Applying loadouts: default
Running task runtime-implement-verify in session task-runtime-implement-verify-591f...
error: could not create home directory: '/home/runner/.local/state/minimal/sessions/98f6d/home/.rustup': Read-only file system (os error 30)

Outside the task's layers::

  • crates/minimald/src/exec.rs

Converted to a draft: something above needs a person before review.

The four egress behaviours hold at the branch head — an own-address box's connection to an undeclared destination drops without a reset and is logged once, an allowed one completes, and UDP is dropped when only TCP is allowed (each named minimald test passes) — and the pure frame verdict behind them is now proved inside the kani lane's budget: the frame-verdict harness verifies in about 40 seconds with zero failing checks, and scripts/kani.sh is green at 7 sessions and 3 rcache harnesses, where before this round the lane never finished at all (a 60-minute job timeout, then two runs cancelled nine minutes in).

Notes from the agent: Round 4 addressed the two failed checks in the directive (ci-kani-success / kani) inside the layer file crates/sessions/src/core/egress.rs, #[cfg(kani)] module only, in two commits. (1) 678f919: the harness built each rule list with a SYMBOLIC element count (n <= 4 assumed, pushed in a 4-iteration loop), so every rule scan CBMC unwound had a trip count it could not read off the code and the solve outlived the lane — run 36136625090 died on the job's 60-minute timeout, runs 36143464955 and 36159156083 were cancelled 8.5-9 minutes in. The harness now pins that cost away: rule lists are None | Some(vec![]) | Some(two symbolic rules) at concrete lengths, and the frame keeps symbolic exactly the fragment offset, protocol, destination and L4 destination port, pinning the MACs, EtherType, version/IHL (4/5) and the L4 around the port. The previous round's #[kani::unwind(5)] could not have fixed this: the blow-up is the symbolic length, not the unwinding. (2) 12770c6: with the affordable shape the proof actually completes and exposes the real remaining defect — comparing [u8; 4] addresses lowers to CBMC's builtin memcmp, a 4-trip loop, so the unwind bound must clear compiler-generated loops too; bound 6 (was 4). The harness name, its iff property and the restatement are unchanged, so scripts/kani.sh's 'expect sessions 7' still counts…[truncated]

@norrietaylor
norrietaylor marked this pull request as ready for review September 25, 2026 19:34
@norrietaylor
norrietaylor requested a review from a team as a code owner September 25, 2026 19:34
@minimal-foundry
minimal-foundry Bot commented Sep 25, 2026 •
Copy link
Copy Markdown
Contributor Author

Approved: no findings at 12770c6afd9c.

Nothing to do.

Verdict APPROVE, posted as a comment: the App cannot review its own pull request; the check and the label carry it.

Clobber: none: git log on the touched files shows no reverted fix (the only recent commits are this PR's own fix(sessions) bound-adjustment history).

@minimal-foundry minimal-foundry Bot left a comment
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minimal-review — approved

Verdict APPROVE — No findings.

@minimal-foundry minimal-foundry Bot added the review:approved minimal-review approved the current head label Sep 25, 2026
@norrietaylor norrietaylor left a comment
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Babysitter review against T18's plan block: the pure verdict in crates/sessions/src/core/egress.rs (summarize + verdict, resolver carve-out first, three declared dimensions conjunctively) applied in the relay; the ingress rule-key fix; the main merge; and the kani harness pinned to the fields the decision reads with two symbolic rules per dimension at unwind 6, verified locally in 6.7 s and green on the CI lane. minimal-review approved with no findings, CodeRabbit posted, no open threads. Merging.

@norrietaylor
norrietaylor merged commit 1cdf5aa into main Sep 25, 2026
33 checks passed
@norrietaylor
norrietaylor deleted the agent/1629-decide-every-frame-with-one-pure-verdict-and-enf branch September 25, 2026 19:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

review:approved minimal-review approved the current head

Projects

None yet

Development

Successfully merging this pull request may close these issues.

T18 — Decide every frame with one pure verdict and enforce own-address egress at the relay

1 participant

0