From 963497bce473147a224a43ab76c6c7dae8fc5a86 Mon Sep 17 00:00:00 2001 From: mintaka Date: Sun, 30 Aug 2026 19:01:54 -0400 Subject: [PATCH 1/2] fix(guest-image): stage /sbin/modprobe so in-guest egress arm autoloads netfilter (RIG-3028) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../runtime/microvm/boot_microvm_test.go | 79 +++++++++++++++++++ guest-image/default.nix | 32 ++++++-- 2 files changed, 106 insertions(+), 5 deletions(-) diff --git a/go/internal/runtime/microvm/boot_microvm_test.go b/go/internal/runtime/microvm/boot_microvm_test.go index 4fba5b181..342418b21 100644 --- a/go/internal/runtime/microvm/boot_microvm_test.go +++ b/go/internal/runtime/microvm/boot_microvm_test.go @@ -24,6 +24,9 @@ import ( "testing" "time" + "connectrpc.com/connect" + + compassv1 "github.com/RigelBuild/compass/go/internal/gen/compass/v1" "github.com/RigelBuild/compass/go/internal/microvmtest" ) @@ -182,6 +185,82 @@ func TestFullBoot(t *testing.T) { } } +// armRuleset is a minimal but representative in-guest egress arm: it creates the +// inet table + a conntrack-stateful output rule, forcing exactly the netfilter +// autoload chain the real base ruleset needs — the NETLINK_NETFILTER socket +// (nfnetlink), the nf_tables subsystem, and the `ct state` expression +// (nf_conntrack + nft_ct). It is `set -eu` so any nft failing aborts non-zero, +// exactly as EgressPolicy.NftScript()'s base ruleset does. Kept as a local +// literal, not a runtime.EgressPolicy call, because this package must not import +// internal/runtime (config.go: no runtime dep, no cycle). +const armRuleset = `set -eu +nft add table inet compass_egress +nft add chain inet compass_egress output '{ type filter hook output priority 0 ; policy drop ; }' +nft add rule inet compass_egress output ct state established,related accept` + +// TestInGuestEgressArmAutoloadsNetfilter is the RIG-3028 proof: on a real guest +// boot, Provision with a NON-EMPTY nft_script (the §(d) in-guest arm W1 landed) +// must succeed. The arm runs `/bin/sh -c