Compose VM rootfs from shared layer blobs - #457
Conversation
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 2b465b6. Configure here.
| continue | ||
| } | ||
| clearExisting(filepath.Join(dest, dir, hidden)) | ||
| continue |
There was a problem hiding this comment.
Whiteouts escape compose root
High Severity
Opaque and whiteout handling joins header.Name onto dest with filepath.Join and never runs safeJoin, unlike regular entries. A layer whose whiteout path contains .. can make clearDirContents / clearExisting delete files outside the compose tree during image build, including host paths under the build parent.
Reviewed by Cursor Bugbot for commit 2b465b6. Configure here.
| continue | ||
| } | ||
| clearExisting(filepath.Join(dest, dir, hidden)) | ||
| continue |
There was a problem hiding this comment.
Empty whiteout deletes parent
Medium Severity
A whiteout whose name is exactly .wh. yields an empty hidden target. After skipping only . and .., clearExisting runs on the parent directory and RemoveAlls it, so a single marker can wipe the containing directory instead of hiding one entry.
Reviewed by Cursor Bugbot for commit 2b465b6. Configure here.
2b465b6 to
fb40cc1
Compare
5675147 to
6bde47c
Compare
6bde47c to
abdceaf
Compare
abdceaf to
0d9f270
Compare
0d9f270 to
de83eaa
Compare
2093a74 to
92dd458
Compare
92dd458 to
5ec53e4
Compare
5ec53e4 to
e290067
Compare
e290067 to
c69e90b
Compare
d4801d7 to
f24fed3
Compare
cb42b2d to
3a95ff6
Compare
The production composition path builds one merged rootfs per image by applying the manifest's ordered layer blobs from the shared OCI cache in tar order: whiteout and opaque-directory markers are interpreted as each layer is applied, entries replace conflicting types safely, and hardlink targets resolve against the accumulated tree. The result is exported to the same single read-only disk the guest already mounts as its lower filesystem, with the writable overlay upper untouched. Attaching one disk per layer was rejected: the guest init mounts exactly one read-only lower plus one writable upper, and QEMU microvm virtio-mmio slots bound the device count, so a per-layer-disk layout cannot be supported uniformly across cloud-hypervisor, Firecracker, QEMU, and vz. Sharing happens at the content-addressed blob and layer-artifact level instead, and every hypervisor keeps its existing disk contract. Composition also enforces the config diff-id pairing integrity check, so corrupt manifests fail the build with a clean error instead of panicking; the umoci unpack remains as a fallback when no manifest model exists.
3a95ff6 to
3f9ef7f
Compare


summary
Stacked stage of the image-storage project: the production composition path.
composeRootfsbuilds one merged rootfs per image by applying the manifest's ordered layer blobs from the shared OCI cache in tar order. Whiteout and opaque-directory markers are interpreted as each layer is applied; entries replace conflicting types safely; hardlink targets resolve against the accumulated tree. The merged tree is exported to the same single read-only disk the guest mounts as its lower filesystem.validateModelPairingenforces one config diff id per manifest layer, so a corrupt manifest fails the build with a clean error instead of indexing past the pairing (preserves the captured-fixture regression behavior). The umoci unpack remains a fallback when no manifest model exists.extractTarEntrynow clears non-empty directories and type-conflicting entries before writing, so a layer can replace a directory with a file (and vice versa).why this path
The guest init mounts exactly one read-only lower (
/dev/vda) and one writable upper (/dev/vdb), and QEMU microvm virtio-mmio slots bound how many devices can be attached. A one-disk-per-layer layout therefore cannot be supported uniformly across cloud-hypervisor, Firecracker, QEMU, and vz, so it was rejected rather than attached blindly. Sharing happens at the content-addressed blob and layer-artifact level; every hypervisor keeps its existing disk contract (vda read-only rootfs, vdb writable overlay), and guest init and hypervisor config are unchanged.validation
fsck.erofs --extract.go test ./lib/images ./lib/paths ./lib/builds ./lib/scopes ./cmd/api/api: green except Docker Hub pull tests (anonymous rate limit in this environment) and VM lifecycle tests, which require network-bridge privileges (verified failing identically on unmodifiedmain).Note
Medium Risk
Switches the primary rootfs build path for normal images and changes tar extraction semantics shared with layer unpacking; guest disk contract is unchanged and umoci fallback limits blast radius.
Overview
Image export now merges manifest-ordered layer blobs from the shared OCI cache into one staging rootfs (
composeRootfs/applyLayerToDir) instead of always using umoci unpack when a manifest model is available. Whiteout and opaque-directory markers are applied as each layer is streamed, so the composed tree is meant for a single read-only guest disk without relying on overlayfs to interpret.wh.*files.validateModelPairingrejects configs whererootfs.diff_idslength mismatches manifest layer count (same integrity message as the umoci path). If no model or empty layers, umociunpackLayersremains the fallback.extractTarEntrynow usesclearExisting(includingRemoveAllfor directories) so later layer entries can replace a path with a different type—e.g. directory → file—fixing composition cases likereplacedirin the new tests.Tests cover whiteout/opaque/replacement ordering, missing blobs, and erofs export +
fsck.erofs --extracton the composed tree.Reviewed by Cursor Bugbot for commit 2b465b6. Configure here.