Repository navigation
T42 — Escape into the VM reaches only the declared union, end to end - #1923
norrietaylor merged 16 commits into
Conversation
Add the escape_reaches_only_declared_union case to scripts/session-e2e.sh (NET-085 at the VM boundary). Two resident boxes with disjoint declared egress bound the union — box A declares 10.0.0.0/8, box B 192.168.0.0/16, both the fabric — and a root escapee's paths out are pinned on both sides: in the box, no CAP_NET_RAW or CAP_NET_ADMIN in either capability set (NET-083) and no IPv6 interface or route (the guest boots with ipv6.disable=1, NET-082); at the host-side egress gate's landing edge, a spoofer speaking the shuttle's own protocol and wearing each address in turn reaches nothing beyond the resident union plus the baseline set, which `min session policy` shows beside the rules (NET-130). The cross-half and beyond-union attempts are refused with the gate's own lines naming source, rule, and destination, an out-of-plan source is refused outright, and the one interim admit — a made-up in-plan lease, the spoofer's liveness bracket — is recorded flip-stably with the line that names it. The refused arms send only the SYN: a claim would admit at the gate and attribute its connection's frames, whose end withdraws the row at the worn address (NET-133). The case self-skips on the native lane, which has no VM boundary to cross; the KVM and macOS lanes run it.
…o-the-vm-reaches-only-the-declared-uni
|
Important Review skippedBot user detected. To trigger a single review, invoke the ⚙️ Run configuration
You can disable this status message by setting the Use the checkbox below for a quick retry:
Comment |
…o-the-vm-reaches-only-the-declared-uni # Conflicts: # scripts/session-e2e.sh
…o-the-vm-reaches-only-the-declared-uni
|
Round 1 of 6 on a conflict with the base branch, pushed as Resolved conflicts in:
Verify Converted to a draft: something above needs a person before review. At the branch head the escape_reaches_only_declared_union e2e case stands beside main's new unpublished_port_refused_on_vm_host case, both wired into the lane order, dispatch and usage: on a VM-backed lane it drives root inside the VM to the host-side egress gate wearing each resident box's address in turn and pins that nothing beyond the resident union of declared egress plus the node-plane baseline set is reached, that no box holds CAP_NET_RAW or CAP_NET_ADMIN (the raw-socket route around the relay is gone, so what the relay forwards is bounded by its lease check), that no IPv6 route exists in the guest, and that min session policy shows the baseline set beside each box's effective rules. Notes from the agent: Conflict round only: the directive listed no review threads. Resolved the two hunks in scripts/session-e2e.sh where the branch's escape_reaches_only_declared_union case met main's new unpublished_port_refused_on_vm_host (T70, a4cad26) in the dispatch list and the usage list; kept both sides everywhere (lane order, dispatch, usage, header docs) and completed the merge with commit fb13b2e. Verified the merge cost the escape case nothing: git diff of the script shows no escape-case line touched, every string the proof greps (registration records, egress-unregistered/unknown-source, egress-infrastructure-destination, egress-undeclared-subnet, the policy display's baseline-set lines) is intact in the merged crates, and the merged tree compiles (the pair built during the test run). shellcheck on the merged script is clean. The merge commit also carries main's own crate changes (minimald exec/net, sandbox2 install hint, the switch crate's refusal builder) — those are the base's content, not this task's work; the escape proof's surfaces live in minvmd, which neither side changed. The proof's VM-lane half runs on the KVM and macOS CI lanes via the whole-lane order; on a bare native box it self-skips with exit 0, which is the outcome a fresh sandbox wit…[truncated] |
|
/revise Review at 417f7b2. The refused arms are sound: each requires an absent listener marker plus a positive gate log line from this case's own daemon. Fix the items below and keep the case's scope (one case in
Run |
…o-the-vm-reaches-only-the-declared-uni
esu_fail was called by the port probe before its definition: under set -uo pipefail without -e a 'command not found' is a status the script carries on from, so the probe's failure path could not stop the case. esu_host_log, esu_case_log and esu_fail now stand above the port probe, and the copies after the listener wait are gone.
A lane that reaches the case with minvmd or E2E_VM set but no MINVMD_GVPROXY_BIN is not a target without the proof — it is a broken lane: every VM target exports the switch binary, because minvmd boots the switch and the host-side egress gate beside it from that path, so such a lane could not boot a gate at all and the NET-085 bound would silently never be tested. It now fails; the native minimald skip stays a skip.
The three loop variables the case's for-loops write were leaking as globals past the case's end. sid_var is the dangerous one: the proxy-source case dispatches right after this one and writes the same name, so a stale esu_ session id left behind here would shadow its next read. esu_box, esu_set and sid_var, plus the arms' window carriers esu_between, esu_prior and esu_beyond_rule, are now local to the proof function.
The IPv6 emptiness check ignored the exec's exit status, so a failed read — and a v6-less guest — both read as empty output and the arm passed without proving anything. The read now emits v6-begin and v6-end around the two tables, and the assertion demands both markers with nothing between them; a failed exec names its error instead.
The refused arms matched their gate line on source and rule, then checked the destination on whichever line tail picked — a line another flow's frame wrote under the same source and rule could pass, and on the two rules whose lines carry no destination at all (DropLimiter::emit writes source and rule only) nothing but source and rule was ever checked. The destination match now sits inside the grep for the two cross arms, and the beyond and outside arms pin their line to the arm's own window: the count must grow past the baseline captured before the arm, so a stale line cannot pass and a spent rate-limit window fails the arm by name.
The spoofer's reader dropped the bytes of a partly-read frame when its deadline cut the read short, so the next call started reading mid-frame and the flow misparsed or lost the very ACK that answers its push. The buffer now persists across calls the way the in-crate harness's own reader holds its partial bytes (crates/minvmd/tests/vm_escape_integration.rs), and each call resumes the frame it was in.
UNREGISTERED_SOURCE_PHASE is Announced on this build, so an in-plan lease no row holds is admitted to every destination — including one beyond the resident union. The case's claims read as though that flow were bounded too. A new arm sends a made-up lease toward 198.51.100.7 and pins whichever line the gate writes: under the shipped interim the flow is ADMITTED and the arm records it as the known interim gap, never as bounded; once T66 (#1711) flips the constant the same arm reads the per-box default's refusal. The closing OK line and the header now claim only the bound the case proves: a RESIDENT box's address reaches only the declared union plus the baseline set, and the made-up lease's admit is named as the gap.
|
The runtime will not revise this pull request again on its own: round 3 would exceed the cap of 1 (the last trigger was a conflict with the base branch). A person decides now. Remove |
|
The runtime will not revise this pull request again on its own: round 3 would exceed the cap of 1 (the last trigger was a failed check suite). A person decides now. Remove |
|
The runtime will not revise this pull request again on its own: round 3 would exceed the cap of 1 (the last trigger was a failed check suite). A person decides now. Remove |
|
Un-drafting. The draft came from the Worker's verify step running clippy 1.98 against main's own code (agent-runtime#278); this PR changes only |
|
Reviewed: 1 finding, none blocking at Address the comments on the diff. Clobber: none: the only edits to pre-existing text are two comment rewordings on |
|
Round 1 of 1 on the runtime's review findings: the coder changed nothing. Revise round 1: judged the single unresolved thread (esu_host_log's log-file selection, scripts/session-e2e.sh:10354-10357) invalid and changed nothing, so the branch is byte-identical to the reviewed head ddff31e. The test line above was run here and exited 0, but this box has no /dev/kvm, no minvmd, and no switch binary, so only the case's native-host skip guard executed; the VM-backed proof itself still owes its evidence to the runner's VM lane, which the runner re-runs. No conflicts file was present and no other finding was raised. Review threads the coder judged:
|
|
The runtime will not revise this pull request again on its own: round 2 would exceed the cap of 1 (the last trigger was a failed check suite). A person decides now. Remove |
# Conflicts: # scripts/session-e2e.sh
|
The runtime will not revise this pull request again on its own: round 2 would exceed the cap of 1 (the last trigger was a failed check suite). A person decides now. Remove |
There was a problem hiding this comment.
Approved at 08de25d. ARCH PASS, minimal-review concluded on this head with no open threads, and the Linux lanes are green. The commitlint red is the fixer's old merge commit subject; commitlint isn't a required check, and the squash title is conventional.
T42 — Escape into the VM reaches only the declared union, end to end
slice: S9
kind: integration
covers: none
size: M
est: 60 code, 160 tests
executor: agent
difficulty: routine
model: standard
diagnostics: the VM host daemon log from the run shows each drop, and the guest route table is printed by the case
observability: the case prints each attempt from the spoofer with the gate's verdict
layers:
- e2e:
- scripts/session-e2e.sh: adds the case escape_reaches_only_declared_union
test: ./scripts/session-e2e.sh escape_reaches_only_declared_ 8000 union
produces: none
consumes: none
proof: root inside the VM spoofing another box's address reaches nothing beyond the resident union plus the baseline set; no box holds CAP_NET_RAW; the relay rejects foreign sources; no IPv6 route in the guest; the baseline set shows beside the effective rules.
depends on:
- blocked by #1627
- blocked by #1640
- blocked by #1650
- blocked by #1651
- blocked by #1630
- blocked by #1641
- blocked by #1659
- blocked by #1708
- blocked by #1709
- blocked by #1710
- blocked by #1711
Proof
The session e2e lane carries the
escape_reaches_only_declared_unioncase. On a VM-backed lane (KVM, or macOS with no flag at all) it boots two boxes with disjoint declared egress. A root escapee spoofs a resident box's address, and the case prints each attempt beside the host egress gate's verdict on it:ipv6.disable=1is on the boot line, and the IPv6 tables are read between sentinels and asserted empty.min session policyshows the node-plane baseline set beside its effective rules.The case claims NET-085 for resident-address spoofing only: spoofing a resident box's address reaches only the declared union plus the baseline set.
Known interim gap: an in-plan address that no box's row holds (a made-up lease) is still admitted under the shipped unregistered-source phase. The case asserts that admit, keyed to the gate's own start-up phase line, and prints it as a known gap. Once the phase drops unregistered sources, the same arm asserts the drop line and an absent marker instead. NET-085 stays open on #1925 (T89).
"The relay rejects foreign sources" is proved by the unit test
relay_rejects_non_lease_source(crates/minimald/src/net/switch.rs), not by this e2e case: a box without CAP_NET_RAW cannot inject foreign-source frames.After round 2, the branch was rebuilt by hand from the round's commit, which had failed to push (stale base). The rebuild merged main and keyed the made-up lease's far-side arm to the gate's phase.
Tests
./scripts/session-e2e.sh escape_reaches_only_declared_unionVerify
min task run agent-verifyexited 101, so this PR is a draft.Base
The base branch moved while the coder worked; it was merged in before the tests ran.
Closes #1664