Introduce OCI image unpack feature - #381
henrybear327 wants to merge 4 commits into
Conversation
Defer the temporary file's removal and close instead of repeating both on every failure path, and sync the directory through syncDirectory after the rename.
Reading a stored image is the reverse of pull: resolve a reference and platform to a manifest digest, load that manifest, and derive the directory its rootfs unpacks to. digestFor shares pin's walk over the per-reference index through nestedIndex. Cache directories are keyed by manifest digest under a per-kind sha256 directory, and a symlink at either parent is refused. openStoreForRead applies the format checks pull enforces without creating or repairing anything, so a lookup cannot leave a store behind at a mistyped path; checkLayout holds those checks for both callers. Rootfs containment is checked by directory identity up the ancestor chain, so a case alias of the store cannot be named as a destination. blob and refuseRootfsInStore gain their callers with unpack.
Add unpack for stored images. Without --rootfs the rootfs is published under the store by manifest digest through a rename, and only that content-addressed entry may reuse another unpack's tree when the rename is lost; a caller-named rootfs is merged in place or staged and renamed, and a lost rename is an error. moby/go-archive owns layer application, whiteouts, compression, containment, and metadata. Each header is rewritten after the previous entry has been applied, so parent symlinks already on disk resolve. Devices, FIFOs, and hardlinks to them become whiteouts. Absolute symlink targets are rebased relative to the link, and a hardlink to such a symlink is rebased at its own location. Special mode bits are cleared because ownership is never applied. Parent traversal above the root clamps there, as in go-archive and the kernel. The size field of a header-only entry is ignored, as archive/tar does, and a regular file named with a trailing slash is renamed before the writer rejects it. Directories stay accessible until all layers finish, then their modes are restored, on failure too. Staging trees are removed without following symlinks or failing on restrictive directory modes. A cancelled read is reported as cancellation even when go-archive decompresses through an unpigz child, whose exit status would otherwise replace it.
Describe the rootfs cache, the --rootfs path, what moby/go-archive owns, and the entry rewrites elfuse applies before it. Add the unpack syntax and flags to the usage page, the offline coverage to the testing page, and unpacking to the README summary.
There was a problem hiding this comment.
6 issues found across 16 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="cmd/oci/helpers_test.go">
<violation number="1" location="cmd/oci/helpers_test.go:59">
P3: When a tarEntry has an empty Name with Type==0 and Link=="", `e.Name[len(e.Name)-1]` indexes index -1 and panics the test. Guard for the empty Name before indexing, or produce a clear t.Fatal.</violation>
</file>
<file name="go.mod">
<violation number="1" location="go.mod:8">
P3: Removing gotest.tools/v3 from go.mod while its go.sum hashes remain means go.sum was not tidied. Run `go mod tidy` so the go.sum entry for the removed module is dropped, keeping go.mod/go.sum consistent.</violation>
</file>
<file name="cmd/oci/store.go">
<violation number="1" location="cmd/oci/store.go:78">
P2: When `oci-layout` is missing or malformed, `openStoreForRead` still accepts the store and `unpack` proceeds. Validate the required OCI layout metadata read-only before returning the store.</violation>
</file>
<file name="cmd/oci/unpack_test.go">
<violation number="1" location="cmd/oci/unpack_test.go:85">
P2: The dangling-symlink subtest always fails because it bypasses `unpackImage` and calls `unpackImageFresh`, whose atomic rename replaces the symlink successfully. Call `unpackImage` here so the test exercises the destination validation it intends to verify.</violation>
</file>
<file name="cmd/oci/tarfilter.go">
<violation number="1" location="cmd/oci/tarfilter.go:253">
P1: When an image uses an absolute target with leading `..`, `relativeTarget` turns `/../outside` into `../outside`, allowing a root-level symlink to escape the unpacked root. Remove leading `.` and `..` components before constructing the relative target while preserving traversal after a named component.</violation>
</file>
<file name="docs/usage.md">
<violation number="1" location="docs/usage.md:350">
P2: The quick-start guidance cannot provision the directory that `elfuse-oci unpack` just populated. `--create-sysroot PATH` mounts a separate sparsebundle over `PATH` for one `elfuse` run, hiding the unpacked files instead of making that tree case-sensitive. Tell users to unpack onto an already-mounted case-sensitive volume, or document the separate sparsebundle creation and mount workflow.</violation>
</file>
Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.
Re-trigger cubic
| prefix = strings.Repeat("../", strings.Count(dir, "/")+1) | ||
| } | ||
| // Keep target traversal intact: a/../b may cross a symlink at a. | ||
| target = strings.TrimLeft(target, "/") |
There was a problem hiding this comment.
P1: When an image uses an absolute target with leading .., relativeTarget turns /../outside into ../outside, allowing a root-level symlink to escape the unpacked root. Remove leading . and .. components before constructing the relative target while preserving traversal after a named component.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At cmd/oci/tarfilter.go, line 253:
<comment>When an image uses an absolute target with leading `..`, `relativeTarget` turns `/../outside` into `../outside`, allowing a root-level symlink to escape the unpacked root. Remove leading `.` and `..` components before constructing the relative target while preserving traversal after a named component.</comment>
<file context>
@@ -0,0 +1,321 @@
+ prefix = strings.Repeat("../", strings.Count(dir, "/")+1)
+ }
+ // Keep target traversal intact: a/../b may cross a symlink at a.
+ target = strings.TrimLeft(target, "/")
+ if target == "" {
+ target = "."
</file context>
| return nil, fmt.Errorf("store: %s is not a directory", root) | ||
| } | ||
| s := &store{root: root} | ||
| if err := s.checkLayout(); err != nil { |
There was a problem hiding this comment.
P2: When oci-layout is missing or malformed, openStoreForRead still accepts the store and unpack proceeds. Validate the required OCI layout metadata read-only before returning the store.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At cmd/oci/store.go, line 78:
<comment>When `oci-layout` is missing or malformed, `openStoreForRead` still accepts the store and `unpack` proceeds. Validate the required OCI layout metadata read-only before returning the store.</comment>
<file context>
@@ -53,6 +59,184 @@ func openStore(root string) (*store, error) {
+ return nil, fmt.Errorf("store: %s is not a directory", root)
+ }
+ s := &store{root: root}
+ if err := s.checkLayout(); err != nil {
+ if errors.Is(err, errNoMarker) {
+ return nil, fmt.Errorf("store: %s is not an elfuse OCI store", root)
</file context>
| } | ||
| var err error | ||
| captureOutput(t, func() { | ||
| err = unpackImageFresh(context.Background(), s, manifestOf(t, s, d), dest) |
There was a problem hiding this comment.
P2: The dangling-symlink subtest always fails because it bypasses unpackImage and calls unpackImageFresh, whose atomic rename replaces the symlink successfully. Call unpackImage here so the test exercises the destination validation it intends to verify.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At cmd/oci/unpack_test.go, line 85:
<comment>The dangling-symlink subtest always fails because it bypasses `unpackImage` and calls `unpackImageFresh`, whose atomic rename replaces the symlink successfully. Call `unpackImage` here so the test exercises the destination validation it intends to verify.</comment>
<file context>
@@ -0,0 +1,648 @@
+ }
+ var err error
+ captureOutput(t, func() {
+ err = unpackImageFresh(context.Background(), s, manifestOf(t, s, d), dest)
+ })
+ if err != nil {
</file context>
| err = unpackImageFresh(context.Background(), s, manifestOf(t, s, d), dest) | |
| err = unpackImage(context.Background(), s, "bad:1", manifestOf(t, s, d), dest) |
| A default APFS volume folds case, which a Linux rootfs does not expect. Provision | ||
| the sysroot with `--create-sysroot` as described under | ||
| [Dynamic Linking And Sysroots](#dynamic-linking-and-sysroots) when unpacking a | ||
| distribution rootfs for real use. |
There was a problem hiding this comment.
P2: The quick-start guidance cannot provision the directory that elfuse-oci unpack just populated. --create-sysroot PATH mounts a separate sparsebundle over PATH for one elfuse run, hiding the unpacked files instead of making that tree case-sensitive. Tell users to unpack onto an already-mounted case-sensitive volume, or document the separate sparsebundle creation and mount workflow.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At docs/usage.md, line 350:
<comment>The quick-start guidance cannot provision the directory that `elfuse-oci unpack` just populated. `--create-sysroot PATH` mounts a separate sparsebundle over `PATH` for one `elfuse` run, hiding the unpacked files instead of making that tree case-sensitive. Tell users to unpack onto an already-mounted case-sensitive volume, or document the separate sparsebundle creation and mount workflow.</comment>
<file context>
@@ -343,26 +343,35 @@ does not require Go.
+build/elfuse --sysroot ~/debian-rootfs /bin/sh
+A default APFS volume folds case, which a Linux rootfs does not expect. Provision
+the sysroot with --create-sysroot as described under
+Dynamic Linking And Sysroots when unpacking a
</file context>
</details>
```suggestion
A default APFS volume folds case, which a Linux rootfs does not expect. Unpack
onto an already-mounted case-sensitive APFS volume (for example, a sparsebundle)
before using it as the sysroot; `--create-sysroot` provisions a separate volume
for an `elfuse` run and cannot convert this already-unpacked directory.
| hdr.Typeflag = tar.TypeSymlink | ||
| hdr.Linkname = e.Link | ||
| hdr.Size = 0 | ||
| case e.Name[len(e.Name)-1] == '/': |
There was a problem hiding this comment.
P3: When a tarEntry has an empty Name with Type==0 and Link=="", e.Name[len(e.Name)-1] indexes index -1 and panics the test. Guard for the empty Name before indexing, or produce a clear t.Fatal.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At cmd/oci/helpers_test.go, line 59:
<comment>When a tarEntry has an empty Name with Type==0 and Link=="", `e.Name[len(e.Name)-1]` indexes index -1 and panics the test. Guard for the empty Name before indexing, or produce a clear t.Fatal.</comment>
<file context>
@@ -10,27 +10,59 @@ import (
+ hdr.Typeflag = tar.TypeSymlink
+ hdr.Linkname = e.Link
+ hdr.Size = 0
+ case e.Name[len(e.Name)-1] == '/':
+ hdr.Typeflag = tar.TypeDir
+ if e.Mode == 0 {
</file context>
| require ( | ||
| github.com/alecthomas/kong v1.16.1 | ||
| github.com/google/go-containerregistry v0.21.7 | ||
| github.com/moby/go-archive v0.3.3 |
There was a problem hiding this comment.
P3: Removing gotest.tools/v3 from go.mod while its go.sum hashes remain means go.sum was not tidied. Run go mod tidy so the go.sum entry for the removed module is dropped, keeping go.mod/go.sum consistent.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At go.mod, line 8:
<comment>Removing gotest.tools/v3 from go.mod while its go.sum hashes remain means go.sum was not tidied. Run `go mod tidy` so the go.sum entry for the removed module is dropped, keeping go.mod/go.sum consistent.</comment>
<file context>
@@ -5,16 +5,21 @@ go 1.25.0
require (
github.com/alecthomas/kong v1.16.1
github.com/google/go-containerregistry v0.21.7
+ github.com/moby/go-archive v0.3.3
github.com/opencontainers/image-spec v1.1.1
)
</file context>
Summary by cubic
Adds the
unpackcommand toelfuse-ociso stored images can be extracted into a rootfs for use withelfuse --sysroot; previously images could only be pulled and stored.--rootfs, the rootfs is cached in the store by manifest digest, staged in a sibling temp directory, and published by rename; completed entries are reused and abandoned staging trees are swept after a day.--rootfs DIR, an existing directory is merged in place, an absent one is staged and renamed, and destinations inside the store are refused, including case aliases of it.moby/go-archivefor whiteouts, hardlinks, compression, and metadata; ownership is never applied.pull's store format checks without creating or repairing the store.Written for commit dd87cca. Summary will update on new commits.