Skip to content

feat(microvm): virtio-fs mount-ns isolation + volume-quota verification (RIG-2497) - #912

Open
rigel-mintaka wants to merge 3 commits into
mainfrom
compass-runner/rig-2497-microvm-v6-isolation-quota
Open

feat(microvm): virtio-fs mount-ns isolation + volume-quota verification (RIG-2497)#912
rigel-mintaka wants to merge 3 commits into
mainfrom
compass-runner/rig-2497-microvm-v6-isolation-quota

Conversation

@rigel-mintaka

Copy link
Copy Markdown
Contributor

V6 of the frozen microVM Runner backend design (record § Plan > V6): per-session virtio-fs isolation proven under real KVM, plus the D7 verify-never-assign volume-quota preflight. Executes against the frozen record (docs/designs/infra/runtime/compass-elastic-session-runtime/microvm-runner.md, D7 + Approach (d)).

What lands

  • Quota verification (D7: verify, never assign). VolumeQuota{Bytes,Inodes} (the expected bound), verifyVolumeQuota(path, want, probe) → (QuotaReading, error), and MicroVMConfig.QuotaRequired (appended, additive). The Runner never assigns quota (that needs CAP_SYS_ADMIN it lacks); it only verifies an operator-provisioned project quota is active — a rootless, read-only check wired into verifyMicroVMSupport as step 5. When QuotaRequired is set (multi-tenant profile) an absent quota fails startup naming the volume + fix; unset (Dogfood single-tenant) it logs utilization and proceeds.
  • The rootless quota read is statfs(2)-derived, not quotactl. quotactl(Q_XGETQUOTA, PRJQUOTA) EPERMs rootless (the kernel gates a non-self quota id on CAP_SYS_ADMIN); FS_IOC_FSGETXATTR yields only the project-id label, not whether enforcement is live. On XFS and ext4 the kernel rewrites a project-quota'd directory's statfs totals to the project's limit+usage, so path-totals < mount-root-totals is the kernel reporting an enforced quota — answered unprivileged, with the utilization in the same call.
  • The isolation-proving KVM suite. Real guests booted under KVM: traversal-confined (dot-dot, symlink-to-host, absolute-path, deep-dot-dot — all blocked), cross-session-unreachable (two live guests; A cannot reach B's volume by any vector), and host-ownership parity (guest-authored files land host-side uid:gid matching podman's --userns=keep-id target). The real ENOSPC/EDQUOT-in-guest leg is honestly gated (requireQuotaFS): it skips where no quota'd filesystem + root exist (a COMPASS_REQUIRE_QUOTA_FS=1 hard-fail switch for the CI/managed profile), never fake-passing. The verification logic is proven by the hermetic unit tests, which run everywhere.

Load-bearing fix: guest writes to /workspace were broken

The parity test surfaced a real defect masked because no prior test wrote to the volume: under --sandbox=namespace alone, rootless virtiofsd could not become namespace-root (Couldn't set the process uid as root: -1), so it could not chown a newly created inode to the requesting guest id — every guest create on the share failed EINVAL. The share was effectively read-only to the guest.

The fix is the record's named mechanism (§(d): "virtiofsd does its own uid/gid translation via that userns"): --uid-map/--gid-map map a subordinate id to namespace-root (so the daemon can chown) and the agent id to the invoking host (uid, gid). gid maps to the host gid, not the uid — the guest agent runs uid==gid, but a host user's gid differs (e.g. 1000:100), and collapsing gid onto uid is exactly the parity break the test detects. Rootless throughout (a /etc/subuid subordinate id, no CAP_SYS_ADMIN); --uid-map over --translate-uid also keeps POSIX ACLs available on the share.

BootConfig.AgentUID carries the guest uid (zero disables the mapping, preserving the V2a spike harness); Create threads spec.UID through.

Verification

All gates green on a real-KVM box (Intel, nested virt): build+vet (default + -tags microvm, darwin cross-build of the non-linux stub), hermetic -race suite, the KVM isolation suite (real boots, COMPASS_REQUIRE_MICROVM=1), full tagged regression (boot/contract/egress/Q-budget unregressed), golangci-lint (tagged + untagged) 0 issues, nilaway clean (default + microvm).

No OTel metric registration (V7 owns the metric set — V6 only exposes QuotaReading.UsedRatio()), no teardown/reap (V7), no benchmark (V8). go/internal/vfs untouched. ContainerRuntime interface unchanged.

Spec-impact: none. Refs RIG-2497

Co-authored-by: Matt Wilkinson matt@rigel.build

@linear-code

linear-code Bot commented Sep 6, 2026

Copy link
Copy Markdown

RIG-2497

@trunk-io

trunk-io Bot commented Sep 6, 2026

Copy link
Copy Markdown

Merging to main in this repository is managed by Trunk.

  • To merge this pull request, check the box to the left or comment /trunk merge below.

After your PR is submitted to the merge queue, this comment will be automatically updated with its status. If the PR fails, failure details will also be posted here

@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown

Compass engineering docs preview: https://compass-runner-rig-2497-micr.compass-eng-docs.pages.dev

Deployed from compass-runner/rig-2497-microvm-v6-isolation-quota at 3b798cc.

rigel-mintaka added a commit that referenced this pull request Sep 6, 2026
…, test non-vacuity (RIG-2497)

Addresses the round-1 review of #912 (2 highs + 6 mediums + 4 lows fixed; the third high — `--uid-map` vs `--translate-uid` capability surface — is a design fork filed as RIG-3330, with its record-faithful hardening landed here).

## Fail-open fixes (the two acted highs)

- **QuotaRequired was dead code.** `MicroVMConfig.QuotaRequired` was never wired to any operator knob, so the D7 quota gate could never fire in production — it was settable only from tests. Added `--microvm-quota-required` (+ `$COMPASS_MICROVM_QUOTA_REQUIRED`, via a new `boolOrEnv` that *refuses* an unparseable value rather than silently reading false) and threaded it into the config literal. `selectEngine` split out a testable `backendConfig()` seam; new `backend_flags_test.go` pins flag/env/precedence end to end.
- **verifyQuota probed the wrong filesystem.** It targeted `RunRoot` (the short `/tmp` socket dir), not the session-volume filesystem (the durable D9 volume, arriving as `Mount.HostPath`). Added `MicroVMConfig.VolumeRoot` (+ `--microvm-volume-root`) and pointed the check at it. When `QuotaRequired` is set and `VolumeRoot` is unknown, startup now **fails closed** with a named error instead of reporting a verdict about a filesystem it never probed.

## id-map hardening (RIG-3330 interim, no mechanism swap)

Kept the record's named `--uid-map`/`--gid-map` mechanism; added `--modcaps=-mknod` (the one capability with no legitimate use on a workspace share) and an explicit security-posture comment stating the namespace-scoped capability set the ns-uid-0 mapping confers (bounded to the volume subtree by the mount-ns pivot_root). The narrower `--translate-uid` alternative is tracked as fork RIG-3330.

## Correctness + test-adequacy fixes

- **subuid base was hardcoded 100000** — a silent EINVAL boot failure on any host whose `/etc/subuid` range starts elsewhere. Now parsed from the invoking user's `/etc/subuid` entry (behind a pure seam), with a named error when absent, and added as a startup preflight axis so it fails legibly at startup, not at first boot's opaque socket-wait. New `launch_idmap_test.go` pins the exact argv (injecting a non-100000 base so a regression is caught).
- **XFS inode-only quota false-negative** — the mount-root `f_files` is a dynamic estimate (`fakeinos` shrinks as the fs fills); added a tolerance margin + documented the dynamism so a genuinely-quota'd volume is not read as unbounded.
- **mountRoot false-positive** — a stat-blocked ancestor was swallowed and could report a bogus active quota (a sibling project's bound); now an inconclusive probe that fails closed under `QuotaRequired`.
- **Cross-session test vacuity** — the `ls` row asserted content that `ls` never prints, and the sweep row depended on `grep`, which the guest image does not ship, so it exited 127 having never searched. Rewrote each row's success condition to match what its command emits (a bash+awk sweep printing `path:line`, needle via env, self-`/proc` matches excluded), added the non-zero-exit assertion to every confined row, and added a control test proving the sweep finds a planted canary.
- **Hardcoded exec uid** `"1000"` → `agentuid.AgentUID` in the isolation, egress, and vsock-gateway microVM suites (a routine const change would otherwise silently make every escape attempt run as an unmapped uid and pass green).
- **Parity leg** now hard-fails on malformed `stat` output instead of silently skipping the host→guest direction, and its error text names the flags the code actually uses.

Lows: corrected the non-Linux stub's rationale, documented `UsedRatio`'s projected-vs-whole-fs dual meaning (and log it only when a quota is active), flattened the virtiofsd argv construction, and de-duplicated the quota rationale headers to a single source.

## Verification

All gates green on a real-KVM box: build+vet (default + `-tags microvm` + darwin `!linux` stub), hermetic `-race` (runtime + microvm + cmd), the KVM isolation suite (real boots — the stricter cross-session sweep genuinely walks the tree and failed twice on real self-match artifacts during development, proving it no longer passes vacuously), golangci-lint (tagged + untagged) 0 issues, nilaway clean (default + microvm).

Spec-impact: none. Refs RIG-2497

Co-authored-by: Matt Wilkinson <matt@rigel.build>
@rigel-mintaka
rigel-mintaka force-pushed the compass-runner/rig-2497-microvm-v6-isolation-quota branch from bc65d7d to a2aa9ec Compare September 6, 2026 02:25
rigel-mintaka and others added 3 commits September 6, 2026 00:07
…on (RIG-2497)

V6 of the frozen microVM Runner backend design (record § Plan > V6): per-session virtio-fs isolation proven under real KVM, plus the D7 verify-never-assign volume-quota preflight. Executes against the frozen record (`docs/designs/infra/runtime/compass-elastic-session-runtime/microvm-runner.md`, D7 + Approach (d)).

## What lands

- **Quota verification (D7: verify, never assign).** `VolumeQuota{Bytes,Inodes}` (the expected bound), `verifyVolumeQuota(path, want, probe) → (QuotaReading, error)`, and `MicroVMConfig.QuotaRequired` (appended, additive). The Runner never *assigns* quota (that needs `CAP_SYS_ADMIN` it lacks); it only *verifies* an operator-provisioned project quota is active — a rootless, read-only check wired into `verifyMicroVMSupport` as step 5. When `QuotaRequired` is set (multi-tenant profile) an absent quota fails startup naming the volume + fix; unset (Dogfood single-tenant) it logs utilization and proceeds.
- **The rootless quota read is `statfs(2)`-derived, not `quotactl`.** `quotactl(Q_XGETQUOTA, PRJQUOTA)` EPERMs rootless (the kernel gates a non-self quota id on `CAP_SYS_ADMIN`); `FS_IOC_FSGETXATTR` yields only the project-id label, not whether enforcement is live. On XFS and ext4 the kernel rewrites a project-quota'd directory's `statfs` totals to the project's limit+usage, so path-totals < mount-root-totals *is* the kernel reporting an enforced quota — answered unprivileged, with the utilization in the same call.
- **The isolation-proving KVM suite.** Real guests booted under KVM: traversal-confined (dot-dot, symlink-to-host, absolute-path, deep-dot-dot — all blocked), cross-session-unreachable (two live guests; A cannot reach B's volume by any vector), and host-ownership parity (guest-authored files land host-side `uid:gid` matching podman's `--userns=keep-id` target). The real ENOSPC/EDQUOT-in-guest leg is honestly gated (`requireQuotaFS`): it skips where no quota'd filesystem + root exist (a `COMPASS_REQUIRE_QUOTA_FS=1` hard-fail switch for the CI/managed profile), never fake-passing. The verification *logic* is proven by the hermetic unit tests, which run everywhere.

## Load-bearing fix: guest writes to /workspace were broken

The parity test surfaced a real defect masked because no prior test wrote to the volume: under `--sandbox=namespace` alone, rootless virtiofsd could not become namespace-root (`Couldn't set the process uid as root: -1`), so it could not chown a newly created inode to the requesting guest id — **every** guest create on the share failed `EINVAL`. The share was effectively read-only to the guest.

The fix is the record's named mechanism (§(d): "virtiofsd does its own uid/gid translation via that userns"): `--uid-map`/`--gid-map` map a subordinate id to namespace-root (so the daemon can chown) and the agent id to the invoking host `(uid, gid)`. gid maps to the host *gid*, not the uid — the guest agent runs `uid==gid`, but a host user's gid differs (e.g. `1000:100`), and collapsing gid onto uid is exactly the parity break the test detects. Rootless throughout (a `/etc/subuid` subordinate id, no `CAP_SYS_ADMIN`); `--uid-map` over `--translate-uid` also keeps POSIX ACLs available on the share.

`BootConfig.AgentUID` carries the guest uid (zero disables the mapping, preserving the V2a spike harness); `Create` threads `spec.UID` through.

## Verification

All gates green on a real-KVM box (Intel, nested virt): build+vet (default + `-tags microvm`, darwin cross-build of the non-linux stub), hermetic `-race` suite, the KVM isolation suite (real boots, `COMPASS_REQUIRE_MICROVM=1`), full tagged regression (boot/contract/egress/Q-budget unregressed), golangci-lint (tagged + untagged) 0 issues, nilaway clean (default + microvm).

No OTel metric registration (V7 owns the metric set — V6 only exposes `QuotaReading.UsedRatio()`), no teardown/reap (V7), no benchmark (V8). `go/internal/vfs` untouched. `ContainerRuntime` interface unchanged.

Spec-impact: none. Refs RIG-2497

Co-authored-by: Matt Wilkinson <matt@rigel.build>
…, test non-vacuity (RIG-2497)

Addresses the round-1 review of #912 (2 highs + 6 mediums + 4 lows fixed; the third high — `--uid-map` vs `--translate-uid` capability surface — is a design fork filed as RIG-3330, with its record-faithful hardening landed here).

## Fail-open fixes (the two acted highs)

- **QuotaRequired was dead code.** `MicroVMConfig.QuotaRequired` was never wired to any operator knob, so the D7 quota gate could never fire in production — it was settable only from tests. Added `--microvm-quota-required` (+ `$COMPASS_MICROVM_QUOTA_REQUIRED`, via a new `boolOrEnv` that *refuses* an unparseable value rather than silently reading false) and threaded it into the config literal. `selectEngine` split out a testable `backendConfig()` seam; new `backend_flags_test.go` pins flag/env/precedence end to end.
- **verifyQuota probed the wrong filesystem.** It targeted `RunRoot` (the short `/tmp` socket dir), not the session-volume filesystem (the durable D9 volume, arriving as `Mount.HostPath`). Added `MicroVMConfig.VolumeRoot` (+ `--microvm-volume-root`) and pointed the check at it. When `QuotaRequired` is set and `VolumeRoot` is unknown, startup now **fails closed** with a named error instead of reporting a verdict about a filesystem it never probed.

## id-map hardening (RIG-3330 interim, no mechanism swap)

Kept the record's named `--uid-map`/`--gid-map` mechanism; added `--modcaps=-mknod` (the one capability with no legitimate use on a workspace share) and an explicit security-posture comment stating the namespace-scoped capability set the ns-uid-0 mapping confers (bounded to the volume subtree by the mount-ns pivot_root). The narrower `--translate-uid` alternative is tracked as fork RIG-3330.

## Correctness + test-adequacy fixes

- **subuid base was hardcoded 100000** — a silent EINVAL boot failure on any host whose `/etc/subuid` range starts elsewhere. Now parsed from the invoking user's `/etc/subuid` entry (behind a pure seam), with a named error when absent, and added as a startup preflight axis so it fails legibly at startup, not at first boot's opaque socket-wait. New `launch_idmap_test.go` pins the exact argv (injecting a non-100000 base so a regression is caught).
- **XFS inode-only quota false-negative** — the mount-root `f_files` is a dynamic estimate (`fakeinos` shrinks as the fs fills); added a tolerance margin + documented the dynamism so a genuinely-quota'd volume is not read as unbounded.
- **mountRoot false-positive** — a stat-blocked ancestor was swallowed and could report a bogus active quota (a sibling project's bound); now an inconclusive probe that fails closed under `QuotaRequired`.
- **Cross-session test vacuity** — the `ls` row asserted content that `ls` never prints, and the sweep row depended on `grep`, which the guest image does not ship, so it exited 127 having never searched. Rewrote each row's success condition to match what its command emits (a bash+awk sweep printing `path:line`, needle via env, self-`/proc` matches excluded), added the non-zero-exit assertion to every confined row, and added a control test proving the sweep finds a planted canary.
- **Hardcoded exec uid** `"1000"` → `agentuid.AgentUID` in the isolation, egress, and vsock-gateway microVM suites (a routine const change would otherwise silently make every escape attempt run as an unmapped uid and pass green).
- **Parity leg** now hard-fails on malformed `stat` output instead of silently skipping the host→guest direction, and its error text names the flags the code actually uses.

Lows: corrected the non-Linux stub's rationale, documented `UsedRatio`'s projected-vs-whole-fs dual meaning (and log it only when a quota is active), flattened the virtiofsd argv construction, and de-duplicated the quota rationale headers to a single source.

## Verification

All gates green on a real-KVM box: build+vet (default + `-tags microvm` + darwin `!linux` stub), hermetic `-race` (runtime + microvm + cmd), the KVM isolation suite (real boots — the stricter cross-session sweep genuinely walks the tree and failed twice on real self-match artifacts during development, proving it no longer passes vacuously), golangci-lint (tagged + untagged) 0 issues, nilaway clean (default + microvm).

Spec-impact: none. Refs RIG-2497

Co-authored-by: Matt Wilkinson <matt@rigel.build>
…ness, mount-point quota degeneracy (RIG-2497)

Round 2 of the #912 review returned 2 highs / 4 mediums / 5 lows, all folded here. The two highs are defects the round-1 fix code itself introduced or left latent, and both fail in the direction that matters: silently, on a host that differs from this one.

## Fail-silent fixes (the highs)

- **`/etc/subgid` is a separate allocation.** Round 1 correctly stopped hardcoding the subordinate base, but read only `/etc/subuid` and fed that one value into *both* `--uid-map` and `--gid-map` — while `newgidmap` validates the gid range against `/etc/subgid`, an independently-editable file (`usermod --add-subuids` and `--add-subgids` are separate flags; the code's own remediation text conceded they can diverge). A host whose two ranges differ would die in virtiofsd's gid map at first boot with no useful diagnostic. Both files are now read independently through a path-injected core, the two arms carry their own bases, and `VerifySubordinateIDRange` resolves both so a missing subgid range fails at *startup* naming the file and the fix. The false "allocated in lockstep" claim is gone. This box cannot observe the bug (both files read `mattw:100000:65536`), so the regression test injects divergent bases as fixtures rather than reading the host.
- **Readiness ignored liveness.** `waitForSockets` polled only for socket-path existence, and virtiofsd binds its socket *before* the id-map step that can fail — so any mapping failure left the socket on disk, readiness returned nil, and cloud-hypervisor was started against a dead virtiofsd, surfacing as an inscrutable vhost-user error instead of virtiofsd's own message. Readiness is now liveness-aware and its error carries the daemon's name and log tail.

  Doing this properly required fixing the supervision model underneath it: only the VMM had a reaper, virtiofsd and passt were `Wait`ed lazily during teardown, and liveness was tracked by an atomic set by whichever path happened to `Wait` first — so a poll-loop check would have raced a second `Wait`. There is now exactly one reaper per child, installed at spawn, with `waitErr` published before the `exited` channel closes as the happens-before edge. `Running()` checks exit *first*, since a zombie still answers `Signal(0)`.

## Correctness

- **Quota verification refused startup on a correctly-provisioned host.** `mountRoot` never checked whether the path it was handed *is* the mount point — `mountRoot("/tmp") == "/tmp"` — so when the volume root is the mount point of a dedicated quota'd filesystem (the natural production layout), the probe compared a filesystem against itself and `Active()` was false by construction. That case is now a distinct inconclusive verdict naming the fix (point `--microvm-volume-root` at a subdirectory), not a false negative. The former "positive control" would have passed an identity-returning implementation; it now asserts the walk actually climbs and converges from two depths.
- The empty-path error still named the retired `--microvm-runroot` knob, with its own test pinning the stale text — a defect regression-locked by its test. Both corrected.

## Test cost and honesty

- The cross-session sweep forked one `awk` per file across the whole guest filesystem: ~72s against a hard 120s exec cap, i.e. a flake waiting for a loaded box. Batched (one `awk` per 200 files) and scoped to the trees that discriminate, it now runs in milliseconds. Batching made the existing control partly vacuous — it never fills a batch — so a cross-batch test covers the mid-loop flush and the accumulator, with needles planted in both the first and final batch. Every confined row is now timed against the cap so creep is caught rather than rediscovered.
- The quota-fill leg wrote the entire project limit plus 64MiB, which against a realistic quota would blow the exec cap and report a timeout instead of the `EDQUOT` verdict it exists to prove. It now fills only remaining headroom plus margin, and refuses with a named reason when the volume is too large to be a purpose-sized test quota.

Lows: pinned the `--modcaps` literal in a shared constant (virtiofsd silently accepts misspelled capability names, so the argv assertion is the only guard), reworded the provisioning hint so `100000` reads as an example rather than canonical, and documented `Active()`'s deliberate byte/inode asymmetry. `boolOrEnv`'s precedence is unchanged and now documented as a contract — an explicit `--microvm-quota-required=false` does not override an env true, which fails toward leaving the multi-tenant gate on — with a test pinning that direction.

## Verification

Gates green on a real-KVM box: build + vet (default, `-tags microvm`, darwin `!linux` stub), hermetic `-race`, the KVM suite under `COMPASS_REQUIRE_MICROVM=1` (real boots), golangci-lint 0 issues both tag sets, nilaway clean both. The load-bearing tests are mutation-verified: reverting the liveness check to path-existence-only reproduces the finding's exact failure, and an identity-returning `mountRoot` fails the strengthened control while passing the old one.

Spec-impact: none. Refs RIG-2497

Co-authored-by: Matt Wilkinson <matt@rigel.build>
@rigel-mintaka
rigel-mintaka force-pushed the compass-runner/rig-2497-microvm-v6-isolation-quota branch from a2aa9ec to 3b798cc Compare September 6, 2026 04:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant