8000 T64 — Seal every box against the socket families its namespace does not confine by minimal-foundry[bot] · Pull Request #1780 · gominimal/minimal · GitHub
[go: up one dir, main page]

Skip to content

T64 — Seal every box against the socket families its namespace does not confine - #1780

Merged
norrietaylor merged 5 commits into
mainfrom
agent/1709-seal-every-box-against-the-socket-families-its-n
Oct 1, 2026
Merged

norrietaylor merged 5 commits into
mainfrom
agent/1709-seal-every-box-against-the-socket-families-its-n

Conversation

@minimal-foundry
@minimal-foundry minimal-foundry Bot commented Oct 1, 2026 •
Copy link
Copy Markdown
Contributor

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 none seal, refusing the namespace-bypass families (AF_VSOCK) and admitting the rest, installed for every plan that is not none; the none seal is unchanged
- crates/sandbox2/src/network.rs: every plan blocks the bypass families; none keeps blocking every outside family
test: 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 none seal for a none box
- crates/minimald/src/session_host.rs: injection is sealed for every box, not only a none box
test: 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_BLUETOOTH and AF_ALG fail with EAFNOSUPPORT everywhere; a host-address or own-address box keeps AF_UNIX, AF_INET, AF_INET6, AF_NETLINK and AF_PACKET (the last governed by the existing CAP_NET_RAW drop, not by the seal); a none box keeps only AF_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_PROCESS verdicts now apply to every box, not only a none box. 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 matches cpu_arch exactly 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:

  • The box-start seal line is info, not debug: the diag bundle carries only the info-filtered daemon log tail, so a debug line would never reach the state dump the diagnostics: field names. The per-injection line is debug as planned.
  • The shim's --seal-none-box flag is kept with a meaning for the absent case (confined seal rather than no seal), so hooks.rs and 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_families is an ungated unit test and runs in the tests lane; joined_process_refuses_namespace_bypass_families is gated on user namespaces and runs for real in the minimald-root-integration lane. See the checks on the head commit.

Verify

min task run agent-verify passed; 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 minimald test but no test file, and this harness owns the shim and injection helpers the proof needs; it also gains a vsock probe in the C caps mode.

Closes #1709

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.
@coderabbitai
coderabbitai Bot commented Oct 1, 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: 5bce3814-0883-4f51-9d99-775e284114d1

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
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Autopilot is currently an internal CodeRabbit preview.


Comment @coderabbitai help to get the list of available commands.

@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 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-1460 and :1471-1473: the doc comments now state falsehoods (the shim always installs a filter). Out of layers; a follow-up, not this PR.

Comment thread crates/sandbox2/src/lib.rs Resolved
Comment thread crates/sandbox2/src/network.rs Resolved
Comment thread crates/sandbox2/src/lib.rs Resolved
@norrietaylor
Copy link
Copy Markdown
Member

/revise ARCH-EXPERT verdict on b8ca3fc: PASS with conditions, plus the babysitter review threads. Address in this round:

  1. Family list (design condition). NET-137 says "every socket whose family the box's network namespace does not confine"; a one-family denylist meets "every" only by enumeration. Either (preferred) express the non-none seal as an allowlist of namespace-confined families (AF_UNIX, AF_INET, AF_INET6, AF_NETLINK), everything else EAFNOSUPPORT, with AF_PACKET left to the existing CAP_NET_RAW drop; or keep the denylist and add AF_BLUETOOTH and AF_ALG beside AF_VSOCK, with a code comment stating the list is an enumeration against a moving target. Update refused_families in the launch log and the unit test's simulated families to match.
  2. AT9 profile note. In the build_socket_family_filter doc comment, state that this program is the first instalment of the published seccomp profile applied inside boxes (architecture.md AT9, open gap 2), so the list is reviewable where the design promised it.
  3. Foreign-ABI kill scope (thread at crates/sandbox2/src/lib.rs:1724): the compat and x32 KILL_PROCESS verdicts now reach every box. Rewrite the stale "not something a none box ever runs legitimately" comment to state the new scope, and say in the box-start seal line's doc that a 32-bit binary in any box dies with SIGSYS.
  4. crates/sandbox2/src/network.rs:86: SocketSeal::Full must display as full, not none.
  5. crates/sandbox2/src/lib.rs:2620: rename only_a_none_plan_seals_sockets to what it now proves.
  6. crates/minimald/tests/netns_root_integration.rs:981: the "no socket-family filter" comment is stale.

Keep the none seal byte-identical. Do not touch crates/minimald/src/hooks.rs (out of layers; its stale doc comments are a follow-up). The box-start seal line stays at info.

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.
@minimal-foundry
Copy link
Copy Markdown
Contributor Author

Round 1 of 10 on a /revise note, pushed as 4da007b32357.

line exit tail
cargo nextest run -p sandbox2 every_box_refuses_namespace_bypass_families 0 Applying loadouts: default Running task runtime-implement-test-0 in session task-runtime-implement-test-0-77a6... Compiling args v0.0.1 (/home/runner/.local/state/minimal/sessions/8f67f/tree/crates/args) Compiling ot v0.0.1 (/home/runner/.local/state/minimal/sessions/8f67f/tree/crates/ot) C
cargo nextest run -p minimald joined_process_refuses_namespace_bypass_families 0 Applying loadouts: default Running task runtime-implement-test-1 in session task-runtime-implement-test-1-2af6... Compiling args v0.0.1 (/home/runner/.local/state/minimal/sessions/f4393/tree/crates/args) Compiling ot v0.0.1 (/home/runner/.local/state/minimal/sessions/f4393/tree/crates/ot) C

Verify min task run agent-verify passed.

Outside the task's layers::

  • crates/minimald/tests/netns_root_integration.rs

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

Review threads the coder judged:

  • 👍 valid — crates/sandbox2/src/lib.rs:1736: Fixed: the builder's foreign-ABI comment now states the KILL_PROCESS verdicts reach every box, the box-start seal line's doc says a 32-bit binary in any box dies with SIGSYS on its first syscall, and this account's notes carry the same so the owner accepts the compat-binary loss knowingly in the PR
  • 👍 valid — crates/sandbox2/src/network.rs: Fixed: SocketSeal::Full now displays as full, so the launch log reads socket_seal=full for the most-sealed box; each_plan_carries_its_socket_seal pins both spellings.
  • 👍 valid — crates/sandbox2/src/lib.rs: Fixed: renamed to each_plan_carries_its_socket_seal, which is what it now asserts — every plan carries a seal, the none plan's full, every other plan's confined-families.

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]

@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.

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.

@norrietaylor
norrietaylor marked this pull request as ready for review October 1, 2026 08:00
@norrietaylor
norrietaylor requested a review from a team as a code owner October 1, 2026 08:00
@minimal-foundry
minimal-foundry Bot commented Oct 1, 2026 •
Copy link
Copy Markdown
Contributor Author

Approved: no findings at 4da007b32357.

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 all five files shows only the task's own progressive commits (862ff515…f21f43ef); no recent fix is reverted.

@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 Oct 1, 2026
@norrietaylor
norrietaylor merged commit 5310904 into main Oct 1, 2026
34 checks passed
@norrietaylor
norrietaylor deleted the agent/1709-seal-every-box-against-the-socket-families-its-n branch October 1, 2026 08:35
minimal-foundry Bot added a commit that referenced this pull request Oct 1, 2026
…-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.
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.

T64 — Seal every box against the socket families its namespace does not confine

1 participant

0