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
3a95ff6 to
3f9ef7f
Compare
a5e47e3 to
c323a22
Compare
c323a22 to
fb28360
Compare
fb28360 to
e48db8d
Compare
e48db8d to
0d729b3
Compare
0d729b3 to
dac88a2
Compare
41aad4a to
f9d76cf
Compare
composition is the only unpack path now: move composeOnDiskFormat into production, point unpackCachedLayer at the cache blob directory directly, and delete the unused umoci unpackLayers path and its helpers. Also: drop the unused composeRootfs wrapper, restore the 0755 export directory mode, reuse removePath for staging and destination cleanup, migrate the unpackLayers tests to the compose path, fix the diff id mismatch test to exercise the diff id check, share the tar layer builders, and require a layered rootfs in manifest model validation.


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.validateManifestModelenforces one config diff id per manifest layer and requiresrootfs.type == "layers"(same guard the previous unpack path enforced), so a corrupt manifest fails the build with a clean error instead of indexing past the pairing (preserves the captured-fixture regression behavior). Composition is the only unpack path: the umociunpackLayerspath and its helpers are removed.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.