feat: add Cloud Hypervisor filesystem write planner - #7660
Conversation
Add a pure, strongly typed planner that computes how a filesystem.allowWrite allowlist would narrow validated Cloud Hypervisor directory exports. The planner classifies each export as unrestricted, read-only, fully writable, or selectively writable, and returns canonical host/guest overlay paths so a later integration can mount them without re-resolving symlinks. It only removes write access: read-only exports are never widened and no host path outside an existing read-write export is ever exposed. The planner is inert. It is not wired into runtime execution and filesystem.allowWrite remains rejected for the Cloud Hypervisor runtime. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Adds an inert Cloud Hypervisor planner for future filesystem.allowWrite enforcement.
Changes:
- Introduces typed export and overlay planning.
- Adds focused planner tests.
- Documents the currently unwired foundation.
Show a summary per file
| File | Description |
|---|---|
src/cloud-hypervisor/filesystem-write-policy.ts |
Implements write-policy planning. |
src/cloud-hypervisor/filesystem-write-policy.test.ts |
Tests planner semantics and validation. |
docs/cloud-hypervisor-foundation.md |
Documents the inert planner. |
Review details
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
- Files reviewed: 3/3 changed files
- Comments generated: 1
- Review effort level: Balanced
| if (isPathAtOrBelow(entry.target, allowedPath)) { | ||
| matched.add(allowedPath); | ||
| return { export: entry, disposition: 'writable', effectiveMode: 'rw', internal, overlays: [] }; |
✅ Coverage Check PassedOverall Coverage
📁 Per-file Coverage Changes (1 files)
✨ New Files (1 files)
Coverage comparison generated by |
A selective export previously reported a single effectiveMode of "ro", which would be wrong for the guest mount. virtio-fs submounts are attached through d_automount, and finish_automount() calls do_add_mount(..., path->mnt->mnt_flags | MNT_SHRINKABLE), so an announced submount inherits MNT_READONLY from its parent mount. A guest-level MS_RDONLY on a composite tree would therefore block writes to every writable node below it. Replace effectiveMode with hostRootMode and guestMountMode. Read-only enforcement is a host-side property: a selective export stages a read-only host backing tree root while the guest mount stays read-write so the overlays remain writable. Also clarify that overlay guestPath is only lexically normalized while hostPath is realpath-canonical. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
|
✅ Copilot review passed with no inline comments. @lpcox Add the |
|
@copilot address review feedback |
Fixed in 2a79de8: the full-export match now requires an exact match against the export target instead of |
|
📰 VERDICT: Smoke Docker Sbx has concluded. All systems operational. This is a developing story. 🎤
|
|
🔌 Smoke Services — All services reachable! ✅
|
|
✅ Smoke Copilot BYOK completed. Copilot BYOK mode operational. 🔓
|
|
✅ Smoke Claude passed
|
|
Chroot tests passed! Smoke Chroot - All security and functionality tests succeeded.
|
|
🛡️ Smoke Copilot Network Isolation confirmed the egress allowlist is enforced. ✅ Warning Firewall blocked 1 domainThe following domain was blocked by the firewall during workflow execution:
To allow these domains, add them to the network:
allowed:
- defaults
- "example.com"See Network Configuration for more information.
|
|
✅ Smoke Copilot BYOK AOAI (Entra) completed. Copilot AOAI BYOK (Entra) mode operational. 🔓
|
|
❌ Contribution Check failed. Please review the logs for details.
|
|
📰 VERDICT: Smoke Copilot has concluded. All systems operational. This is a developing story. 🎤
|
|
✅ Build Test Suite completed successfully!
|
|
✅ Smoke Copilot BYOK AOAI (api-key) completed. Copilot AOAI BYOK (api-key) mode operational. 🔓
|
|
📡 Smoke OTel Tracing completed. All tracing scenarios validated. ✅
|
|
🚀 Security Guard has started processing this pull request |
|
✨ The prophecy is fulfilled... Smoke Codex has completed its mystical journey. The stars align. 🌟 Warning Firewall blocked 3 domainsThe following domains were blocked by the firewall during workflow execution:
To allow these domains, add them to the network:
allowed:
- defaults
- "msfeed2.pkgs.visualstudio.com"
- "msfeed25.pkgs.visualstudio.com"
- "registry.npmjs.org"See Network Configuration for more information.
|
|
✨ The prophecy is fulfilled... Smoke Codex has completed its mystical journey. The stars align. 🌟 Warning Firewall blocked 3 domainsThe following domains were blocked by the firewall during workflow execution:
To allow these domains, add them to the network:
allowed:
- defaults
- "msfeed2.pkgs.visualstudio.com"
- "msfeed25.pkgs.visualstudio.com"
- "registry.npmjs.org"See Network Configuration for more information.
|
|
Chroot tests passed! Smoke Chroot - All security and functionality tests succeeded.
|
|
✅ Smoke Copilot BYOK AOAI (api-key) completed. Copilot AOAI BYOK (api-key) mode operational. 🔓
|
|
❌ Smoke Gemini reports failed. Facets need polishing...
|
|
🔌 Smoke Services — All services reachable! ✅
|
|
✅ Smoke Copilot BYOK completed. Copilot BYOK mode operational. 🔓
|
|
✅ Contribution Check completed successfully!
|
|
Smoke Test: Copilot Engine — @lpcox
Overall: PASS
|
|
EGRESS_RESULT allow=pass deny=pass ✅ Allowed domain (github.com) reachable: Overall status: PASS cc @lpcox Warning Firewall blocked 1 domainThe following domain was blocked by the firewall during workflow execution:
To allow these domains, add them to the network:
allowed:
- defaults
- "example.com"See Network Configuration for more information.
|
Smoke Test: Claude Engine Validation
Overall result: PASS
|
Docker Sbx Smoke Test
Recent merged PRs:
Overall: PASS cc @lpcox
|
Smoke Test: Services Connectivity
Overall: FAIL —
|
Smoke Test Results: Copilot BYOK (Direct) Mode ✅
Overall Status: PASS
|
|
Merged PR titles:
Warning Firewall blocked 3 domainsThe following domains were blocked by the firewall during workflow execution:
To allow these domains, add them to the network:
allowed:
- defaults
- "msfeed2.pkgs.visualstudio.com"
- "msfeed25.pkgs.visualstudio.com"
- "registry.npmjs.org"See Network Configuration for more information.
|
Chroot Version Comparison
Result: Not all tests passed — Node.js version mismatch between host and chroot environment.
|
📡 OTel Tracing Smoke Test Results
Overall: ✅ All core scenarios pass. No regressions detected in OTEL module init, span/token attribute logic, env var propagation, or token-tracker integration.
|
|
feat: add Cloud Hypervisor filesystem write planner
|
|
@lpcox
|
🏗️ Build Test Suite Results
Overall: 8/8 ecosystems passed — PASS Note: Maven initially failed with All ecosystems and projects completed successfully.
|
Resolves one conflict in docs/cloud-hypervisor-foundation.md, where main's write-policy planner (#7660) and this branch's host mount-tree enforcement each added a section immediately before "## Limitations". Both are kept. The planner section is a `###` under "## Guest and workspace", so it stays first to remain under its parent heading, and the mount-tree section follows as a new `##`. The two now cross-reference each other: the planner decides which paths stay writable and emits `hostRootMode`/`guestMountMode`, while the mount-tree layer stages the host tree that enforces it. Both remain inert and independent of each other. Also documents that the propagation check rejects `master:` and `propagate_from:` as well as `shared:`, matching the slave-mount fix on this branch. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6cda9102-6304-49e1-a578-40a583561e2c
* feat: support filesystem allowWrite with Cloud Hypervisor Wires the write-policy planner (#7660) into the host mount-tree enforcement layer (#7661) so `filesystem.allowWrite` is actually enforced by the Cloud Hypervisor microVM runtime. Both foundations landed deliberately inert; this is the runtime integration. `filesystem-write-enforcement.ts` is the only place the two meet. It plans the policy in a dedicated startup stage before the boot loop, so an invalid allowlist aborts before virtiofsd or the guest launches, and translates each planner disposition into the merged mount-tree API: - policy absent: no enforcement argument at all, byte-identical legacy behaviour and original export objects; - unrestricted/fully writable: guest `rw`, no plan; - fully read-only: guest `ro`, staged host root `ro`, zero-overlay plan; - selectively writable: guest `rw`, staged host root `ro`, one overlay per allowed path. A zero-overlay read-only export still gets a plan rather than falling back to the legacy single bind plus remount, so a narrowed export always uses the recursively-verified staged tree. No `internalTags` are passed: Cloud Hypervisor has no analogue of Docker's always-writable log and session-state binds, and exempting `tmp-gh-aw` would defeat the narrowing a policy like `allowWrite: ["/tmp/gh-aw/agent"]` expresses. Because the host tree is the boundary, a selective export is never mounted read-only guest-side. `validateCloudHypervisorExports()` accepts a read-only `workspace` only when a plan for that tag exists, so a read-only workspace nothing enforces is still rejected. Unknown plan tags stay fail-closed and planner validation is not duplicated. Removes only the Cloud Hypervisor rejection from `filesystem-policy.ts`; sbx and Docker-in-Docker still fail closed. Adds live-KVM smoke cases proving an allowed directory and file write persists to the host while sibling, parent, create, truncate, rename, and delete outside the allowlist fail, plus empty-allowlist narrowing and a fail-closed abort. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * fix: gate compose write policy by runtime and repair allowWrite smoke probes Two review blockers on the Cloud Hypervisor allowWrite integration. 1. CRITICAL — `filesystem.allowWrite` leaked into compose generation. `writeConfigs()` -> `generateDockerCompose()` -> `buildAgentVolumes()` called Docker's `applyFilesystemWritePolicy()` unconditionally. Compose still builds an agent service object for microVM runtimes (so infra containers can wire depends_on edges) even though it is omitted from the emitted file, so a Cloud Hypervisor policy was evaluated against compose bind mounts the agent never uses. Guest paths such as `/workspace/allowed` -- and the motivating `/tmp/gh-aw/agent` -- are not backed by any host bind mount, so they threw "not an existing path within a writable host mount" during writeConfigs(), long before the Cloud Hypervisor planner ran. Adds `resolveComposeFilesystemAllowWrite()`, which returns the policy only when `runtimeUsesComposeAgent()` is true, and routes both consumers through it: the `applyFilesystemWritePolicy()` call in volume-builder and the `dropUnbackedHostHomeOverlays()` toggle in optional-services. Docker and gVisor behaviour is unchanged; sbx still fails closed in filesystem-policy. Audited every other `filesystemAllowWrite` reader: the remaining ones are the type declaration, config mapping, the runtime compatibility gate, and the Cloud Hypervisor planner itself, all correct. 2. HIGH — the live smoke truncate probe killed the guest shell. The guest runs BusyBox ash, where a redirection failure on a POSIX *special* builtin is fatal. `! : > /workspace/input.txt` therefore exited the shell before the rename and delete probes ran, while the host post-checks still passed vacuously -- a write never attempted also never changes anything. Reproduced in busybox: the old chain exits 1 and never prints the rename/delete markers; `printf ''` continues correctly. Every write-denial probe is now a regular builtin or external command, wrapped in a subshell so a fatal error stays contained, and followed by a sentinel. A new `assert_sentinels` helper requires all of them after the case, so a shell that dies partway can no longer look like a pass. Regression coverage: top-level `generateDockerCompose` tests proving a Cloud Hypervisor config with `/workspace/allowed` neither throws nor rewrites any emitted compose volume, while Docker/gVisor still narrow mounts and still fail closed on an unbacked path; plus script-contract tests banning special-builtin write probes and requiring each sentinel to be both emitted and asserted. Full suite 319 suites / 5068 tests passing. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Summary
Adds a pure, strongly typed policy-planning foundation for
filesystem.allowWriteon the Cloud Hypervisor runtime. This PR is inert: nothing is wired into runtime execution,src/cloud-hypervisor/virtiofsd.tsis untouched, andfilesystem.allowWriteis still rejected for the Cloud Hypervisor runtime bysrc/filesystem-policy.ts. No behavior change.New module:
src/cloud-hypervisor/filesystem-write-policy.tsSemantics
undefined→restricted: false, every export keeps its declared mode (disposition: 'unrestricted').[]→ every writable non-internal export becomes read-only; AWF-owned exports named ininternalTagsstay writable (mirrors the always-writable Docker mounts)..., exist on the host, and resolve beneath an existing read-write export target.src/services/agent-volumes/filesystem-write-policy.ts.kind: 'file' | 'directory')./above/workspace) is not itself a path reachable within the export, so it goes through the same host resolution as any other candidate and is rejected if it does not resolve.internalTags) read-write export are validated and consumed rather than reported as unmatched, but emit no overlay because the whole export is already writable. A nested internal path still passes the same existence / realpath-equality / file-or-directory checks, so/tmp/gh-aw/missingor a symlink escape under an internal export is still rejected.filesystem.allowWritewording:filesystem.allowWrite path must be absolute without '..': …andfilesystem.allowWrite path is not an existing path within a writable Cloud Hypervisor export: …(all unmatched paths reported at once).Host root mode vs guest mount mode
Read-only enforcement for a selectively writable export is a host-side property, so each plan entry carries two modes rather than one:
hostRootModeguestMountModeunrestrictedread-onlyrorowritablerwrwselectiverorwA
selectiveexport must not be mounted read-only in the guest. virtio-fs submounts are attached throughd_automount→fuse_dentry_automount()→fc_mount()→finish_automount(), andfinish_automount()calls:(
fs/namespace.c) — the announced submount inherits the parent mount'smnt_flags, includingMNT_READONLY. (fs_context_for_submount()passessb_flags = 0, so the submount superblock is notSB_RDONLY, but the inherited per-mountMNT_READONLYstill denies writes.) A guest-levelMS_RDONLYon a composite tree would therefore block writes to every writable node beneath it. The read-only host backing tree root — the same host-side read-only bindvirtiofsd.tsalready builds forroexports — is what denies writes outside the overlays.Output shape
A later integration PR can distinguish the cases and get the paths directly:
CloudHypervisorExportWritePlan:disposition(unrestricted|read-only|writable|selective),hostRootMode,guestMountMode,internal,overlays.CloudHypervisorWritableOverlay:exportTag,guestPath,hostPath,relativePath,kind. Both paths are absolute, but canonical in different senses —guestPathis only lexically normalized (the guest filesystem does not exist at planning time), whilehostPathis realpath-canonical and verified not to escape the export source.CloudHypervisorFilesystemWritePlan:restricted, normalizedallowedPaths, per-exportexports, and a flattenedoverlayslist.Tests
src/cloud-hypervisor/filesystem-write-policy.test.ts— 22 focused cases covering: undefined, empty list, internal tag exemption, full-export allowance, strict-ancestor rejection, nested directory, existing file, source/target translation, duplicate/descendant normalization, relative and..rejection, nonexistent path, symlink escape, read-only export (both target and nested path), unmatched path, multi-path error aggregation, deepest-export resolution, deeper-ro non-widening, multiple overlays per export, and the host-root/guest-mount mode invariant (an overlay-bearing export is alwayshostRootMode: 'ro'+guestMountMode: 'rw', and arwhost root is never published to aroguest mount).An
internal exportsblock covers the internal cases specifically: exact internal target, valid nested internal path (consumed, no overlay), missing nested internal path (rejected), strict ancestor of the internal target (rejected), and symlink escape under the internal export (rejected).Validation
npx jest src/cloud-hypervisor→ 13 suites, 172 tests passednpm test→ 316 suites, 4989 tests passednpm run build(tsc) → cleannpx eslinton the new files → 0 errors (only the pre-existingsecurity/detect-non-literal-fs-filenamewarnings that the sibling Docker policy module also emits)Docs
docs/cloud-hypervisor-foundation.mdgains a "Write-policy planning (inert)" subsection describing the planner, the host-VFS enforcement rationale for the two modes, and the path-canonicality distinction.