feat(guestd): arm nft_script as guest root before the exec gate (V3 W1) - #754
Merged
Merged
Conversation
Replace the V2b `CodeUnimplemented` branch in `supervisor.Provision` with the in-guest egress arm from the frozen microVM Runner V3 record (§(d)): a non-empty `nft_script` runs as guestd's own root, under `s.mu`, before the state transition. A failed arm returns `CodeInternal` and leaves the gate closed at `stateReady`, so `requireProvisioned` keeps refusing every exec and the host tears the VM down — the fail-closed contract. An empty script skips the arm (the §(e) hermetic test seam); the host production path never sends empty. - `armFunc func(ctx, script) error` seam on the supervisor (beside `newCredential`), production `runNftScript` spawning `/bin/sh -c <script>` with no `syscall.Credential` — a spawn path deliberately separate from exec children, bounded by `armTimeout = 120s` (OQ-5) and the RPC ctx, carrying the exit status + bounded output tail on failure (OQ-6 `CodeInternal`). - Guest image links `/bin/sh` (`bashInteractive`, zero added closure): under always-arm every microVM Start spawns `/bin/sh -c`, so a missing `/bin/sh` is a total-backend outage, not egress-only. - Proto/handler doc-comments describe the as-built arm (§(f)); no wire change. - Tests rewrite the V2b non-empty→`CodeUnimplemented` assertion into the arm contract: exact-bytes arm + `stateProvisioned`; failed arm ⇒ `CodeInternal`, gate stays closed, retry allowed; empty script skips the arm; already-provisioned refusal holds after a successful arm (no wire re-arm); a real-`/bin/sh` `exit 7` row proves the failure path (host `/bin/sh` only; the guest link is proven by the W3/KVM suite). Refs RIG-3018 Co-authored-by: Matt Wilkinson <matt@rigel.build>
|
Compass engineering docs preview: https://compass-runner-rig-3018-v3-w.compass-eng-docs.pages.dev Deployed from |
The V3 egress arm spawns `/bin/sh -c <script>` as a spawn path deliberately separate from exec children (§(d): no credential, no exec table), so it never inherits the PATH floor that `mergeEnv` applies to every exec child. guestd runs as PID 1 with no PATH, so the arm child's shell saw an empty PATH and the script's bare `nft`/`getent`/`awk` (all linked under `/bin`, guest-image/default.nix:321-323) failed `command not found` — failing the arm, failing Provision, and under always-arm (§(e)) failing EVERY microVM Start: the total-backend outage the record flags for this hazard class. Set `cmd.Env = ["PATH=" + defaultGuestPATH]` on the arm spawn so bare tools resolve against the guest rootfs userland, mirroring the exec path's floor. The host `/bin/sh` in hermetic tests always inherited a populated PATH, so this only surfaced when a real guest (PID 1, empty env) ran a real non-empty script — i.e. W1's arm and W2's always-arm script together, exactly the stacked KVM contract run (contract_microvm_test.go). Adds a hermetic regression asserting the arm child's PATH equals defaultGuestPATH (echo/redirection are shell builtins, so the captured value reflects only the env the arm sets); it fails red against the pre-fix inherited-env spawn. Refs RIG-3018 Co-authored-by: Matt Wilkinson <matt@rigel.build>
mattwilkinsonn
approved these changes
Aug 30, 2026
rigel-mintaka
added a commit
that referenced
this pull request
Aug 31, 2026
…ds netfilter (RIG-3028) The V3 in-guest egress arm (W1, #754) runs `nft` as guest root, whose first NETLINK_NETFILTER socket open requires the kernel to autoload nfnetlink / nf_tables on demand. The guest has no udev / systemd-modules-load (guestd is PID 1, record §(d)), so on-demand loading rides entirely on the kernel's request_module() usermode-helper path — which execs the binary named by /proc/sys/kernel/modprobe (default /sbin/modprobe, CONFIG_MODPROBE_PATH unset in the pinned kernel). Nothing staged that binary, so request_module was a silent no-op: the arm failed `mnl.c:66: Unable to initialize Netlink socket: Protocol not supported` (EPROTONOSUPPORT), and under §(e) always-arm this reddened every microVM Start. The shipped /lib/modules tree was necessary but not sufficient — the false OQ-3 autoload assumption CI surfaced on the W2 microvm leg. Fix: symlink /sbin/modprobe to kmod's modprobe in the rootfs (kmod, not busybox — modules are .ko.xz and CONFIG_MODULE_DECOMPRESS is unset, so the helper must decompress in userspace, the same reason the initrd already uses kmod). With the helper staged, any module the guest asks for autoloads on demand from the shipped tree via its depmod alias/dep metadata. This is the general request_module mechanism, not a fixed preload, so it also covers egress rulesets beyond the base one: a future user-defined rule pulling a new nft expression module autoloads with no guest-image change. Adds a KVM-gated red-green guard, TestInGuestEgressArmAutoloadsNetfilter: it boots a real guest and Provisions a non-empty nft_script (table + chain + `ct state` rule under `set -eu`), so Provision succeeding proves the whole chain (nfnetlink, nf_tables, nf_conntrack, nft_ct) autoloaded. Verified red on a post-W1/pre-fix rootfs (reproduces `mnl.c:66 EPROTONOSUPPORT`) and green with the fix. Refs RIG-3028 Co-authored-by: Matt Wilkinson <matt@rigel.build>
mattwilkinsonn
added a commit
that referenced
this pull request
Aug 31, 2026
…ds netfilter (RIG-3028) (#759) * fix(guest-image): stage /sbin/modprobe so in-guest egress arm autoloads netfilter (RIG-3028) The V3 in-guest egress arm (W1, #754) runs `nft` as guest root, whose first NETLINK_NETFILTER socket open requires the kernel to autoload nfnetlink / nf_tables on demand. The guest has no udev / systemd-modules-load (guestd is PID 1, record §(d)), so on-demand loading rides entirely on the kernel's request_module() usermode-helper path — which execs the binary named by /proc/sys/kernel/modprobe (default /sbin/modprobe, CONFIG_MODPROBE_PATH unset in the pinned kernel). Nothing staged that binary, so request_module was a silent no-op: the arm failed `mnl.c:66: Unable to initialize Netlink socket: Protocol not supported` (EPROTONOSUPPORT), and under §(e) always-arm this reddened every microVM Start. The shipped /lib/modules tree was necessary but not sufficient — the false OQ-3 autoload assumption CI surfaced on the W2 microvm leg. Fix: symlink /sbin/modprobe to kmod's modprobe in the rootfs (kmod, not busybox — modules are .ko.xz and CONFIG_MODULE_DECOMPRESS is unset, so the helper must decompress in userspace, the same reason the initrd already uses kmod). With the helper staged, any module the guest asks for autoloads on demand from the shipped tree via its depmod alias/dep metadata. This is the general request_module mechanism, not a fixed preload, so it also covers egress rulesets beyond the base one: a future user-defined rule pulling a new nft expression module autoloads with no guest-image change. Adds a KVM-gated red-green guard, TestInGuestEgressArmAutoloadsNetfilter: it boots a real guest and Provisions a non-empty nft_script (table + chain + `ct state` rule under `set -eu`), so Provision succeeding proves the whole chain (nfnetlink, nf_tables, nf_conntrack, nft_ct) autoloaded. Verified red on a post-W1/pre-fix rootfs (reproduces `mnl.c:66 EPROTONOSUPPORT`) and green with the fix. Refs RIG-3028 Co-authored-by: Matt Wilkinson <matt@rigel.build> * fix(guest-image): address review nits — comment precision + drop inert -f (RIG-3028) Review-round low findings on PR #759, all comment/precision (no behavior change): - boot_microvm_test.go: soften the armRuleset doc-comment — it proves a representative slice of the netfilter autoload chain (nfnetlink, nf_tables, nf_conntrack, nft_ct), not "exactly" every module the base ruleset needs (the base ruleset's interval sets + DNS rules pull further modules the same way via request_module). - boot_microvm_test.go: note the 30s Provision context is a deliberately generous CLIENT bound; the real ceiling is the server's 120s armTimeout. - guest-image/default.nix: reword the modprobe symlink comment to attribute closure inclusion to the ${pkgs.kmod} reference, not the ln flag; drop the inert -f (no prior modprobe name exists to overwrite) to match the sibling `ln -s` for /sbin/init and /lib/modules. Re-verified GREEN on real KVM after the change (rootfs still packs /sbin/modprobe -> kmod; TestInGuestEgressArmAutoloadsNetfilter passes). Refs RIG-3028 Co-authored-by: Matt Wilkinson <matt@rigel.build> --------- Co-authored-by: Matt Wilkinson <matt@rigel.build>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR is part of a stack containing 2 PRs:
mainReplace the V2b
CodeUnimplementedbranch insupervisor.Provisionwith thein-guest egress arm from the frozen microVM Runner V3 record (§(d)): a non-empty
nft_scriptruns as guestd's own root, unders.mu, before the statetransition. A failed arm returns
CodeInternaland leaves the gate closed atstateReady, sorequireProvisionedkeeps refusing every exec and the hosttears the VM down — the fail-closed contract. An empty script skips the arm (the
§(e) hermetic test seam); the host production path never sends empty.
armFunc func(ctx, script) errorseam on the supervisor (besidenewCredential), productionrunNftScriptspawning/bin/sh -c <script>withno
syscall.Credential— a spawn path deliberately separate from execchildren, bounded by
armTimeout = 120s(OQ-5) and the RPC ctx, carrying theexit status + bounded output tail on failure (OQ-6
CodeInternal)./bin/sh(bashInteractive, zero added closure): underalways-arm every microVM Start spawns
/bin/sh -c, so a missing/bin/shisa total-backend outage, not egress-only.
CodeUnimplementedassertion into the armcontract: exact-bytes arm +
stateProvisioned; failed arm ⇒CodeInternal,gate stays closed, retry allowed; empty script skips the arm;
already-provisioned refusal holds after a successful arm (no wire re-arm); a
real-
/bin/shexit 7row proves the failure path (host/bin/shonly; theguest link is proven by the W3/KVM suite).
Refs RIG-3018
Co-authored-by: Matt Wilkinson matt@rigel.build