Repository navigation
T64 — Seal every box against the socket families its namespace does not confine - #1780
Conversation
A network namespace confines AF_INET and AF_INET6 but not AF_VSOCK, so a process in any box could reach the host over vsock whatever its network mode. Every NetPlan now carries a SocketSeal: the none plan keeps its full seal (every family but AF_UNIX), every other plan is sealed against the bypass families only and keeps its inet sockets. The seal picks the filter, the launch installs it for every plan, and the launch log names the seal applied.
The socket-family filter installed at launch is inherited by children of the filtered process only, so the nsenter shim already reinstalls it for none boxes; now it installs a seal for every box — the bypass-family one by default, the none plan's full seal under --seal-none-box — and the session host derives the marker from the plan's seal instead of the network mode. A debug line per injection names the seal applied; a root integration proof joins a running host-address box and asserts the injected process is refused AF_VSOCK while its inet stream sockets keep working.
|
Important Review skippedBot user detected. To trigger a single review, invoke the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Advanced Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Comment |
There was a problem hiding this comment.
Babysitter review at b8ca3fc. The seal matches the plan block and NET-137 as written in #1707: one BPF builder with a swapped verdict tail, none byte-identical, bypass filter installed for every other plan at launch and through the shim on injection, both plan test lines present and running in the tests and minimald-root-integration lanes. The out-of-layer netns_root_integration.rs change is justified (the plan names a -p minimald test but no test file, and this harness owns the shim helpers). No blocking defect.
Items outside the diff hunks, so noted here rather than inline:
- NIT
crates/minimald/tests/netns_root_integration.rs:981: "no socket-family filter" is stale; a host-plan box now carries the bypass-family filter. - LOW
crates/minimald/src/hooks.rs:1457-1460and:1471-1473: the doc comments now state falsehoods (the shim always installs a filter). Out of layers; a follow-up, not this PR.
|
/revise ARCH-EXPERT verdict on b8ca3fc: PASS with conditions, plus the babysitter review threads. Address in this round:
Keep the |
A one-family denylist met "every socket whose family the box's network namespace does not confine" only by enumeration, so the non-`none` seal is now an allowlist of the families the namespace confines — unix, inet, inet6, netlink, packet — with everything else refused `EAFNOSUPPORT`, `AF_VSOCK` included, without any of them named. `AF_PACKET` stays admitted by the seal, its refusal the missing `CAP_NET_RAW` no box holds (NET-083), so that proof still reads EPERM. The `none` seal's program is byte-identical; both seals now build from one allowlist tail. With the review round: the full seal displays as `full` (it read as the plan name `none`, which reads as no seal); the foreign-ABI and x32 `KILL_PROCESS` verdicts — which now reach every box — say so in the builder comment and the box-start seal line, a 32-bit binary in any box dying with SIGSYS on its first syscall; the builder's doc states this program is the first instalment of the seccomp profile applied inside boxes (architecture.md AT9, open gap 2); the plan-seal unit test is renamed to what it proves; the stale "no socket-family filter" comments in the capability proof are rewritten.
The strict clippy gate's cast_sign_loss read the probe loop's i32-to-u32 cast as one of a runtime value. The same cast of the known-positive family numbers already passes where it sits in a const, the shape the seal's own admitted list uses, so the loop walks a const array of u32 and the cast leaves the loop body.
…-box-against-the-socket-families-its-n
|
Round 1 of 10 on a /revise note, pushed as
Verify Outside the task's
Converted to a draft: something above needs a person before review. Review threads the coder judged:
A process in any box, whatever its network mode, is refused a socket of a family its network namespace does not confine: the non-none seal is now an allowlist of the namespace-confined families (AF_UNIX, AF_INET, AF_INET6, AF_NETLINK, AF_PACKET) that refuses everything else with EAFNOSUPPORT, AF_VSOCK and every other bypass family included, while an own-address or host-address box keeps its inet sockets (AF_PACKET's refusal stays the missing CAP_NET_RAW of NET-083, not the seal); a process the daemon joins into a running box carries the same refusal, the shim reinstating the box's seal. The none seal's program is byte-identical to round 1. Notes from the agent: Revise round 1, directive items 1-6 all addressed; both test lines re-run green at the final head (2bbfa39 + one follow-up commit; the runtime's own committer landed the main change mid-session as 2bbfa39 and I added the clippy-strict const hoist on top). Item 1 took the preferred option: the non-none seal expressed as an allowlist of namespace-confined families, AF_PACKET kept admitted so its refusal remains the existing CAP_NET_RAW drop (the NET-083 proof's raw_socket_errno is an AF_INET SOCK_RAW attempt and still reads EPERM; sandbox2's boxes_lack_cap_net_raw passes). The none seal's BPF program is instruction-for-instruction identical: the builder now emits an allowlist tail for both seals, and the none seal's single-family list reproduces the round-1 bytes exactly (socketpair jump k=4 included). refused_families for the confined seal reads 'every family but unix, inet, inet6, netlink, packet' in the launch log, and the unit test's simulated families gained AF_NETLINK/AF_PACKET admitted plus AF_BLUETOOTH/AF_ALG refused. just clippy-strict: 1 hit remains, pre-existing from round 1 (cast_sign_loss at crates/sandbox2/src/lib.rs:2889, the nr as u32 in the probe closure) — reported per the revise-round rule, not fixed; the hit my round introduced was fixed by hoisting the bypass-family numbers i…[truncated] |
There was a problem hiding this comment.
Round 1 at 4da007b verified: the confined seal is an allowlist (AF_UNIX, AF_INET, AF_INET6, AF_NETLINK, AF_PACKET admitted; everything else EAFNOSUPPORT), the BPF jump table checked by hand for both seals, the none program byte-identical to main, the AT9 note present, Full displays as full, the test renamed, stale comments fixed, hooks.rs untouched. ARCH-EXPERT verdict PASS with conditions, all met here; the stderr-side SIGSYS reason recording is the design's visibility ask and is tracked with the follow-ups. Merge once minimal-review concludes on this head and the lanes are green.
|
Approved: no findings at 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: |
…-host-address-box-in-its-own-classifie Carry the classifier-leaf placement through the socket-seal refactor (#1780): every box now installs its seal filter in the same pre-exec closure that joins its leaf, the launch's seal marker comes from the plan's seal, and injections both join the leaf and reinstall the seal.
T64 — Seal every box against the socket families its namespace does not confine
slice: S9
kind: task
covers: NET-137
size: S
est: 140 code, 120 tests
executor: agent
difficulty: routine
model: standard
diagnostics: the daemon's state dump names the seal each box runs under
observability: one debug log line per box start and per injection naming the seal applied
layers:
- sandbox:
- crates/sandbox2/src/lib.rs: a second socket-family filter beside the
noneseal, refusing the namespace-bypass families (AF_VSOCK) and admitting the rest, installed for every plan that is notnone; thenoneseal is unchanged- crates/sandbox2/src/network.rs: every plan blocks the bypass families;
nonekeeps blocking every outside familytest: cargo nextest run -p sandbox2 every_box_refuses_namespace_bypass_families
- daemon:
- crates/minimald/src/nsenter.rs: the injection seal installs the bypass-family filter for every network mode and the full
noneseal for anonebox- crates/minimald/src/session_host.rs: injection is sealed for every box, not only a
noneboxtest: cargo nextest run -p minimald joined_process_refuses_namespace_bypass_families
produces: none
consumes: none
proof: A process in any box, whatever its network mode, is refused a socket of a family its network namespace does not confine, AF_VSOCK first, while an own-address or host-address box keeps its inet sockets; a process the daemon joins into a running box carries the same refusal.
Proof
A process in any box, whatever its network mode, is refused a socket of a family its network namespace does not confine:
AF_VSOCK,AF_BLUETOOTHandAF_ALGfail withEAFNOSUPPORTeverywhere; a host-address or own-address box keepsAF_UNIX,AF_INET,AF_INET6,AF_NETLINKandAF_PACKET(the last governed by the existingCAP_NET_RAWdrop, not by the seal); anonebox keeps onlyAF_UNIX, with a seccomp program byte-identical to the one on main. A process the daemon injects into a running box carries the same seal through the shim, and the launch log names the seal each box runs under.The confined seal is an allowlist of namespace-confined families, not a denylist, so "every family the namespace does not confine" holds without enumerating the kernel's families. The program is the first instalment of the published seccomp profile applied inside boxes (architecture AT9, open gap 2); the builder's doc comment says so.
Behaviour change to accept knowingly: the foreign-ABI and x32
SECCOMP_RET_KILL_PROCESSverdicts now apply to every box, not only anonebox. A 32-bit binary in a host-address or own-address dev box dies with SIGSYS on its first sys 8000 call where it ran before. The design's package model matchescpu_archexactly and ships no foreign-ABI binaries into a box, so this is outside the contract rather than against it; a user-supplied 32-bit binary is the case that changes. The box-start seal line names the scope.Deviations from the plan block, both deliberate:
info, notdebug: the diag bundle carries only the info-filtered daemon log tail, so a debug line would never reach the state dump thediagnostics:field names. The per-injection line isdebugas planned.--seal-none-boxflag is kept with a meaning for the absent case (confined seal rather than no seal), sohooks.rsand its argv tests needed no change. Its doc comments now describe the old contract; that is #1782, outside this task's layers.Tests
Both plan test lines pass in the runtime's test step on every round.
every_box_refuses_namespace_bypass_familiesis an ungated unit test and runs in thetestslane;joined_process_refuses_namespace_bypass_familiesis gated on user namespaces and runs for real in theminimald-root-integrationlane. See the checks on the head commit.Verify
min task run agent-verifypassed; the PR's CI lanes are green on the head commit.Out of layer
crates/minimald/tests/netns_root_integration.rs: the plan's daemon test line names a-p minimaldtest but no test file, and this harness owns the shim and injection helpers the proof needs; it also gains a vsock probe in the Ccapsmode.Closes #1709