Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ linker resolved against an external sysroot via `--sysroot`.

## OCI Images

`elfuse-oci` is a separate Go binary that pulls OCI images into a local
OCI image layout. It does not add container isolation. See
`elfuse-oci` is a separate Go binary that pulls and unpacks OCI images. It
does not add container isolation. See
[docs/usage.md](docs/usage.md#oci-images) and
[docs/oci-images.md](docs/oci-images.md).

Expand Down Expand Up @@ -159,7 +159,7 @@ The build signs `build/elfuse` before use. Override the signing identity with
`make check` flow, the QEMU and Rosetta cross-check matrices, and
fixture handling.
- [docs/oci-images.md](docs/oci-images.md): the `elfuse-oci` store,
pull behavior, and validation.
pull and unpack behavior, and validation.
- [docs/filenames.md](docs/filenames.md): how a guest filename becomes a
name on disk and back: case folding and normalization on the sysroot
volume, the escape encoding, and the length limits both systems impose.
Expand Down
9 changes: 9 additions & 0 deletions cmd/oci/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,12 @@ func (cf *commonFlags) openStore() (*store, ocispec.Platform, error) {
s, err := openStore(root)
return s, platform, err
}

func (cf *commonFlags) openStoreForRead() (*store, ocispec.Platform, error) {
root, platform, err := cf.values()
if err != nil {
return nil, ocispec.Platform{}, err
}
s, err := openStoreForRead(root)
return s, platform, err
}
85 changes: 81 additions & 4 deletions cmd/oci/helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,27 +10,59 @@ import (
"context"
"encoding/json"
"io"
"math/rand"
"os"
"path/filepath"
"strings"
"testing"
"time"

v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/types"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
)

type tarEntry struct {
Name string
Body string
Name string
Body string
Link string
Mode int64
Type byte
Major int64
ModTime time.Time
}

func buildLayerTar(t *testing.T, entries []tarEntry) []byte {
t.Helper()
var b bytes.Buffer
tw := tar.NewWriter(&b)
for _, e := range entries {
hdr := &tar.Header{Name: e.Name, Mode: 0o644, Size: int64(len(e.Body)), Typeflag: tar.TypeReg}
hdr := &tar.Header{Name: e.Name, Mode: e.Mode, Size: int64(len(e.Body)), Typeflag: tar.TypeReg, ModTime: e.ModTime}
if hdr.Mode == 0 {
hdr.Mode = 0o644
}
// tar.Writer rounds ModTime to whole seconds unless Format is set, and
// only PAX keeps the fraction.
if e.ModTime.Nanosecond() != 0 {
hdr.Format = tar.FormatPAX
}
switch {
case e.Type != 0:
hdr.Typeflag = e.Type
hdr.Size = 0
hdr.Linkname = e.Link
hdr.Devmajor = e.Major
case e.Link != "":
hdr.Typeflag = tar.TypeSymlink
hdr.Linkname = e.Link
hdr.Size = 0
case e.Name[len(e.Name)-1] == '/':

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the issue still valid? Why?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@henrybear327 Yes, it still stands as a code-level hazard, though it's defensive rather than something the current tests hit.

The switch is unchanged in the current file: when e.Type == 0 and e.Link == "" (both the zero values), control falls to case e.Name[len(e.Name)-1] == '/', and tarEntry{Name: ""} (a plain body with no type/link) indexes [-1] and panics. Every call site in the current suite passes a non-empty Name, so it won't fire today — but the helper's contract doesn't enforce that, and a future test author adding an entry with just a Body (the struct's most natural bare use) would get a confusing index-out-of-range panic instead of a clear message.

Cheapest fix is to make the empty case explicit before the indexing, e.g.:

case e.Name == "":
    // clear t.Fatal, or fall through to a regular file with a generated name

That preserves the existing behavior for real entries and turns the footgun into a readable failure. I'd keep it as P3 either way.

Tip: get faster answers by chatting with cubic’s review copilot in the review UI

hdr.Typeflag = tar.TypeDir
if e.Mode == 0 {
hdr.Mode = 0o755
}
hdr.Size = 0
}
if err := tw.WriteHeader(hdr); err != nil {
t.Fatal(err)
}
Expand All @@ -47,7 +79,10 @@ func buildLayerTar(t *testing.T, entries []tarEntry) []byte {
func gzipBytes(t *testing.T, b []byte) []byte {
t.Helper()
var z bytes.Buffer
zw := gzip.NewWriter(&z)
zw, err := gzip.NewWriterLevel(&z, gzip.BestSpeed)
if err != nil {
t.Fatal(err)
}
if _, err := zw.Write(b); err != nil {
t.Fatal(err)
}
Expand Down Expand Up @@ -220,3 +255,45 @@ func mustContain(t *testing.T, got string, wants ...string) {
}
}
}

func manifestOf(t *testing.T, s *store, digest string) ocispec.Manifest {
t.Helper()
manifest, err := s.manifestFor(context.Background(), digest)
if err != nil {
t.Fatal(err)
}
return manifest
}

func runCaptured(t *testing.T, args ...string) (string, error) {
t.Helper()
var err error
_, stderr := captureOutput(t, func() { err = run(args) })
return stderr, err
}

func unpackFresh(t *testing.T, s *store, digest string) string {
t.Helper()
dest := filepath.Join(t.TempDir(), "rootfs")
if err := unpackFreshTo(t, s, digest, dest, false); err != nil {
t.Fatal(err)
}
return dest
}

func unpackFreshTo(t *testing.T, s *store, digest, dest string, cached bool) (err error) {
t.Helper()
t.Cleanup(func() { _ = removeRootfsTree(dest) })
captureOutput(t, func() {
err = unpackImageFresh(context.Background(), s, manifestOf(t, s, digest), dest, cached)
})
return err
}

// incompressibleBody returns seeded random bytes, which gzip cannot shrink.
func incompressibleBody(seed, n int) string {
r := rand.New(rand.NewSource(int64(seed)))
b := make([]byte, n)
r.Read(b)
return string(b)
}
3 changes: 2 additions & 1 deletion cmd/oci/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ func main() {
}

type cli struct {
Pull pullCommand `cmd:"" help:"Pull an image into the local store"`
Pull pullCommand `cmd:"" help:"Pull an image into the local store"`
Unpack unpackCommand `cmd:"" help:"Unpack a stored image into a rootfs"`
}

func newParser(stdout, stderr io.Writer, target *cli) (*kong.Kong, error) {
Expand Down
20 changes: 11 additions & 9 deletions cmd/oci/main_command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ func TestUsageAndErrors(t *testing.T) {
if err == nil {
t.Fatal("missing command must fail")
}
mustContain(t, stdout, "Usage: elfuse-oci <command>", "pull")
mustContain(t, stdout, "Usage: elfuse-oci <command>", "pull", "unpack")
if stderr != "" {
t.Fatalf("parse error wrote to stderr: %q", stderr)
}
Expand All @@ -24,18 +24,20 @@ func TestUsageAndErrors(t *testing.T) {
if err == nil || !strings.Contains(err.Error(), "unexpected argument bogus") {
t.Fatalf("unknown command error = %v", err)
}
mustContain(t, stdout, "Usage: elfuse-oci <command>", "pull")
mustContain(t, stdout, "Usage: elfuse-oci <command>", "pull", "unpack")
if stderr != "" {
t.Fatalf("unknown command wrote to stderr: %q", stderr)
}

stdout, stderr = captureOutput(t, func() { err = run([]string{"pull", "--nope", "x"}) })
if err == nil || !strings.Contains(err.Error(), "unknown flag --nope") {
t.Fatalf("unknown flag error = %v", err)
}
mustContain(t, stdout, "Usage: elfuse-oci pull", "--platform", "--store", "--timeout")
if stderr != "" {
t.Fatalf("unknown flag wrote to stderr: %q", stderr)
for cmd, flag := range map[string]string{"pull": "--timeout", "unpack": "--rootfs"} {
stdout, stderr = captureOutput(t, func() { err = run([]string{cmd, "--nope", "x"}) })
if err == nil || !strings.Contains(err.Error(), "unknown flag --nope") {
t.Fatalf("%s: unknown flag error = %v", cmd, err)
}
mustContain(t, stdout, "Usage: elfuse-oci "+cmd, "--platform", "--store", flag)
if stderr != "" {
t.Fatalf("%s: unknown flag wrote to stderr: %q", cmd, stderr)
}
}
}

Expand Down
Loading