From 420d550a9e0df323ce01e0324dfb6053468c4814 Mon Sep 17 00:00:00 2001 From: Paul Gaiduk Date: Wed, 25 Mar 2026 18:53:24 +0100 Subject: [PATCH] pkg build: fix nil pointer dereference when releasing image only in registry When an image exists in the registry but not in local cache and a release tag is requested, FindDescriptor returns nil causing a panic at build.go:588. This was a regression introduced in 4129cc799 which removed the early return for missing local cache images. Fix by pulling the image into local cache when the descriptor is nil and a release is needed. Also guard the targetDocker block against nil descriptors, and fix the FindDescriptor mock to return nil,nil (matching the real implementation) instead of an error. Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Paul Gaiduk --- src/cmd/linuxkit/pkglib/build.go | 17 +++++- src/cmd/linuxkit/pkglib/build_test.go | 74 ++++++++++++++++++++++++++- 2 files changed, 88 insertions(+), 3 deletions(-) diff --git a/src/cmd/linuxkit/pkglib/build.go b/src/cmd/linuxkit/pkglib/build.go index d29b808dc..cf5d15846 100644 --- a/src/cmd/linuxkit/pkglib/build.go +++ b/src/cmd/linuxkit/pkglib/build.go @@ -509,7 +509,7 @@ func (p Pkg) Build(bos ...BuildOpt) error { // if requested docker, load the image up // we will store images with arch suffix, i.e. -amd64 // if one of the arch equals with system, we will add tag without suffix - if bo.targetDocker { + if bo.targetDocker && desc != nil { for _, platform := range bo.platforms { ref, err := reference.Parse(p.FullTag()) if err != nil { @@ -575,6 +575,21 @@ func (p Pkg) Build(bos ...BuildOpt) error { return nil } + if desc == nil { + // Image exists in registry but not in local cache. Pull it so we can create the release tag. + _, _ = fmt.Fprintf(writer, "Pulling %s into local cache for release...\n", p.FullTag()) + if err := c.ImagePull(&ref, bo.platforms, false); err != nil { + return fmt.Errorf("unable to pull image for release: %v", err) + } + desc, err = c.FindDescriptor(&ref) + if err != nil { + return err + } + if desc == nil { + return fmt.Errorf("unable to find image descriptor in local cache for %s after pull, cannot release", p.FullTag()) + } + } + relTag, err := p.ReleaseTag(bo.release) if err != nil { return err diff --git a/src/cmd/linuxkit/pkglib/build_test.go b/src/cmd/linuxkit/pkglib/build_test.go index d4974e442..177cc7b67 100644 --- a/src/cmd/linuxkit/pkglib/build_test.go +++ b/src/cmd/linuxkit/pkglib/build_test.go @@ -240,13 +240,24 @@ type cacheMocker struct { enableIndexWrite bool images map[string][]v1.Descriptor hashes map[string][]byte + registryImages map[string][]v1.Descriptor } func (c *cacheMocker) ImagePull(ref *reference.Spec, platforms []imagespec.Platform, alwaysPull bool) error { if !c.enableImagePull { return errors.New("ImagePull disabled") } - // make some random data for a layer + image := ref.String() + // simulate pulling from registry: copy registry descriptors into local cache + if c.registryImages != nil { + if descs, ok := c.registryImages[image]; ok { + for _, d := range descs { + c.appendImage(image, d) + } + return nil + } + } + // fallback: make some random data for a layer b := make([]byte, 256) _, _ = rand.Read(b) descs, err := c.imageWriteStream(bytes.NewReader(b)) @@ -277,6 +288,19 @@ func (c *cacheMocker) ImageInCache(ref *reference.Spec, trustedRef, architecture } func (c *cacheMocker) ImageInRegistry(ref *reference.Spec, trustedRef, architecture string) (bool, error) { + if c.registryImages == nil { + return false, nil + } + image := ref.String() + descs, ok := c.registryImages[image] + if !ok { + return false, nil + } + for _, d := range descs { + if d.Platform != nil && d.Platform.Architecture == architecture { + return true, nil + } + } return false, nil } @@ -445,7 +469,7 @@ func (c *cacheMocker) FindDescriptor(ref *reference.Spec) (*v1.Descriptor, error if desc, ok := c.images[name]; ok && len(desc) > 0 { return &desc[0], nil } - return nil, fmt.Errorf("not found %s", name) + return nil, nil } func (c *cacheMocker) NewSource(ref *reference.Spec, platform *imagespec.Platform, descriptor *v1.Descriptor) spec.ImageSource { return cacheMockerSource{c, ref, platform, descriptor} @@ -581,6 +605,52 @@ func TestBuild(t *testing.T) { } } +// TestPushReleaseImageInRegistry tests that push+release works when the image +// already exists in the registry but not in the local cache. This is a +// regression test for a nil pointer dereference introduced in 4129cc79. +func TestPushReleaseImageInRegistry(t *testing.T) { + cacheDir := "somecachedir" + // simulate an image that exists in the registry for amd64 + // FullTag() for org:"foo", image:"bar" with no tag set is "docker.io/foo/bar:latest" + registryDescs := map[string][]v1.Descriptor{ + "docker.io/foo/bar:latest": { + { + MediaType: types.OCIManifestSchema1, + Size: 100, + Digest: v1.Hash{Algorithm: "sha256", Hex: "aabbccdd"}, + Platform: &v1.Platform{Architecture: "amd64", OS: "linux"}, + }, + }, + } + c := &cacheMocker{ + enablePush: true, + enabledDescriptorWrite: true, + enableImagePull: true, + enableIndexWrite: true, + registryImages: registryDescs, + } + runner := &dockerMocker{supportContexts: true} + p := Pkg{org: "foo", image: "bar", hash: "abc", arches: []string{"amd64"}, commitHash: "HEAD", dockerfile: "testdata/Dockerfile"} + opts := []BuildOpt{ + WithBuildCacheDir(cacheDir), + WithBuildPush(), + WithRelease("snapshot"), + WithBuildDocker(runner), + WithBuildCacheProvider(c), + WithBuildOutputWriter(io.Discard), + WithBuildPlatforms(imagespec.Platform{OS: "linux", Architecture: "amd64"}), + } + err := p.Build(opts...) + if err != nil { + t.Fatalf("expected no error, got: %v", err) + } + // verify the release tag was written to cache + relTag := "docker.io/foo/bar:snapshot" + if _, ok := c.images[relTag]; !ok { + t.Errorf("expected release tag %q to be written to cache", relTag) + } +} + // testCheckBuildRun check the output of a build run func testCheckBuildRun(build buildLog, platforms map[string]bool) error { platform := build.platform