From 92d69be9b02697089bab60a90280b4ab8256ff5d Mon Sep 17 00:00:00 2001 From: Suzu Date: Tue, 22 Sep 2026 16:31:46 +0800 Subject: [PATCH 1/3] Reuse cached OCI layers before opening sources Opening a layer's compressed reader starts a registry request even when its digest is already stored locally. Delay that request until the store has checked its existing blob so repeated pulls can reuse a verified layer without reopening its source. --- cmd/oci/store.go | 26 +++++++++++++-------- cmd/oci/store_test.go | 53 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 68 insertions(+), 11 deletions(-) diff --git a/cmd/oci/store.go b/cmd/oci/store.go index cde7e3db0..b6660d8bc 100644 --- a/cmd/oci/store.go +++ b/cmd/oci/store.go @@ -242,8 +242,7 @@ func syncDirectory(path string) error { return err } -func (s *store) writeBlob(ctx context.Context, desc v1.Descriptor, r io.ReadCloser) error { - defer r.Close() +func (s *store) writeBlob(ctx context.Context, desc v1.Descriptor, openReader func() (io.ReadCloser, error)) error { if err := ctx.Err(); err != nil { return err } @@ -277,6 +276,11 @@ func (s *store) writeBlob(ctx context.Context, desc v1.Descriptor, r io.ReadClos } else if !os.IsNotExist(err) { return err } + r, err := openReader() + if err != nil { + return err + } + defer r.Close() tmp, err := os.CreateTemp(dir, ".blob-*") if err != nil { return err @@ -333,7 +337,9 @@ func (s *store) writeBlobBytes(ctx context.Context, media types.MediaType, b []b return v1.Descriptor{}, err } desc := v1.Descriptor{MediaType: media, Digest: hash, Size: size} - return desc, s.writeBlob(ctx, desc, io.NopCloser(bytes.NewReader(b))) + return desc, s.writeBlob(ctx, desc, func() (io.ReadCloser, error) { + return io.NopCloser(bytes.NewReader(b)), nil + }) } func (s *store) publishImage(ctx context.Context, img v1.Image, platform ocispec.Platform) (v1.Descriptor, error) { @@ -360,11 +366,7 @@ func (s *store) publishImage(ctx context.Context, img v1.Image, platform ocispec if err != nil { return v1.Descriptor{}, err } - r, err := layer.Compressed() - if err != nil { - return v1.Descriptor{}, err - } - if err := s.writeBlob(ctx, v1.Descriptor{MediaType: media, Digest: digest, Size: size}, r); err != nil { + if err := s.writeBlob(ctx, v1.Descriptor{MediaType: media, Digest: digest, Size: size}, layer.Compressed); err != nil { return v1.Descriptor{}, err } } @@ -376,7 +378,9 @@ func (s *store) publishImage(ctx context.Context, img v1.Image, platform ocispec if err != nil { return v1.Descriptor{}, err } - if err := s.writeBlob(ctx, v1.Descriptor{MediaType: types.OCIConfigJSON, Digest: configHash, Size: int64(len(config))}, io.NopCloser(bytes.NewReader(config))); err != nil { + if err := s.writeBlob(ctx, v1.Descriptor{MediaType: types.OCIConfigJSON, Digest: configHash, Size: int64(len(config))}, func() (io.ReadCloser, error) { + return io.NopCloser(bytes.NewReader(config)), nil + }); err != nil { return v1.Descriptor{}, err } manifest, err := img.RawManifest() @@ -392,7 +396,9 @@ func (s *store) publishImage(ctx context.Context, img v1.Image, platform ocispec return v1.Descriptor{}, err } desc := v1.Descriptor{MediaType: media, Digest: digest, Size: int64(len(manifest)), Platform: toV1Platform(platform)} - if err := s.writeBlob(ctx, desc, io.NopCloser(bytes.NewReader(manifest))); err != nil { + if err := s.writeBlob(ctx, desc, func() (io.ReadCloser, error) { + return io.NopCloser(bytes.NewReader(manifest)), nil + }); err != nil { return v1.Descriptor{}, err } return desc, nil diff --git a/cmd/oci/store_test.go b/cmd/oci/store_test.go index 86fa6be60..ac94ee1e8 100644 --- a/cmd/oci/store_test.go +++ b/cmd/oci/store_test.go @@ -15,6 +15,8 @@ import ( "testing" v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/mutate" + "github.com/google/go-containerregistry/pkg/v1/static" "github.com/google/go-containerregistry/pkg/v1/types" ocispec "github.com/opencontainers/image-spec/specs-go/v1" ) @@ -36,6 +38,53 @@ func (r *cancelReadCloser) Read(p []byte) (int, error) { func (r *cancelReadCloser) Close() error { return nil } +type countedLayer struct { + v1.Layer + opens int + openErr error +} + +func (l *countedLayer) Compressed() (io.ReadCloser, error) { + l.opens++ + if l.openErr != nil { + return nil, l.openErr + } + return l.Layer.Compressed() +} + +func TestPublishImageReusesCachedLayers(t *testing.T) { + s := tempStore(t) + layer := &countedLayer{Layer: static.NewLayer( + buildLayerTar(t, []tarEntry{{Name: "hello", Body: "world"}}), + types.OCIUncompressedLayer, + )} + img, err := mutate.AppendLayers(platformImage(t, *toV1Platform(defaultPlatform)), layer) + if err != nil { + t.Fatal(err) + } + first, err := s.publishImage(context.Background(), img, defaultPlatform) + if err != nil { + t.Fatal(err) + } + if layer.opens != 1 { + t.Fatalf("initial layer opens = %d, want 1", layer.opens) + } + layer.openErr = errors.New("layer source unavailable") + second, err := s.publishImage(context.Background(), img, defaultPlatform) + if err != nil { + t.Fatalf("publish cached image: %v", err) + } + if second.Digest != first.Digest || layer.opens != 1 { + t.Fatalf("cached digest = %s, want %s; layer opens = %d, want 1", second.Digest, first.Digest, layer.opens) + } + if _, err := tempStore(t).publishImage(context.Background(), img, defaultPlatform); !errors.Is(err, layer.openErr) { + t.Fatalf("uncached image error = %v, want %v", err, layer.openErr) + } + if layer.opens != 2 { + t.Fatalf("uncached layer opens = %d, want 2", layer.opens) + } +} + func TestPinsPerPlatform(t *testing.T) { s := tempStore(t) arm := ocispec.Platform{OS: "linux", Architecture: "arm64"} @@ -162,7 +211,9 @@ func TestWriteBlobStopsOnCancellation(t *testing.T) { t.Fatal(err) } ctx, cancel := context.WithCancel(context.Background()) - err = s.writeBlob(ctx, v1.Descriptor{MediaType: types.OCIConfigJSON, Digest: hash, Size: size}, &cancelReadCloser{cancel: cancel}) + err = s.writeBlob(ctx, v1.Descriptor{MediaType: types.OCIConfigJSON, Digest: hash, Size: size}, func() (io.ReadCloser, error) { + return &cancelReadCloser{cancel: cancel}, nil + }) if !errors.Is(err, context.Canceled) { t.Fatalf("write error = %v", err) } From c1116df47415101e6a3c11f55279bd350701a8bf Mon Sep 17 00:00:00 2001 From: Suzu Date: Wed, 23 Sep 2026 10:48:57 +0800 Subject: [PATCH 2/3] Reuse cached OCI configs during pull Platform selection reads the config before publication, so deferring only the config write still issues a blob GET on every pull. Use verified cached config bytes for platform checks and defer source reads during publication until the manifest descriptor misses the cache. Exercise repeated pulls against a local registry with config downloads disabled. Keep platform mismatches and corrupt cached blobs rejected. --- cmd/oci/pull.go | 40 ++++++--- cmd/oci/pull_test.go | 202 +++++++++++++++++++++++++++++++++++++++++-- cmd/oci/store.go | 43 +++++++-- 3 files changed, 260 insertions(+), 25 deletions(-) diff --git a/cmd/oci/pull.go b/cmd/oci/pull.go index be773848d..36b68e1f3 100644 --- a/cmd/oci/pull.go +++ b/cmd/oci/pull.go @@ -4,6 +4,7 @@ package main import ( + "bytes" "context" "fmt" "os" @@ -43,8 +44,19 @@ func normalizeRef(ref string) (name.Reference, error) { return name.ParseReference(ref, name.WeakValidation) } -func imagePlatform(img v1.Image) (v1.Platform, error) { - config, err := img.ConfigFile() +func imagePlatform(ctx context.Context, s *store, img v1.Image) (v1.Platform, error) { + manifest, err := img.Manifest() + if err != nil { + return v1.Platform{}, err + } + raw, err := s.verifiedBlobBytes(ctx, manifest.Config) + if os.IsNotExist(err) { + raw, err = img.RawConfigFile() + } + if err != nil { + return v1.Platform{}, err + } + config, err := v1.ParseConfigFile(bytes.NewReader(raw)) if err != nil { return v1.Platform{}, err } @@ -55,7 +67,7 @@ func imagePlatform(img v1.Image) (v1.Platform, error) { }, nil } -func indexChildImage(index v1.ImageIndex, child v1.Descriptor) (v1.Image, v1.Platform, error) { +func indexChildImage(ctx context.Context, s *store, index v1.ImageIndex, child v1.Descriptor) (v1.Image, v1.Platform, error) { if !child.MediaType.IsImage() { return nil, v1.Platform{}, fmt.Errorf("unexpected index child media type %s", child.MediaType) } @@ -63,7 +75,7 @@ func indexChildImage(index v1.ImageIndex, child v1.Descriptor) (v1.Image, v1.Pla if err != nil { return nil, v1.Platform{}, err } - actual, err := imagePlatform(img) + actual, err := imagePlatform(ctx, s, img) return img, actual, err } @@ -82,7 +94,7 @@ func imagePlatformMatches(actual, requested v1.Platform) bool { actual.Variant == requested.Variant } -func selectIndexImage(index v1.ImageIndex, platform v1.Platform) (v1.Image, error) { +func selectIndexImage(ctx context.Context, s *store, index v1.ImageIndex, platform v1.Platform) (v1.Image, error) { manifest, err := index.IndexManifest() if err != nil { return nil, err @@ -91,7 +103,7 @@ func selectIndexImage(index v1.ImageIndex, platform v1.Platform) (v1.Image, erro if child.Platform == nil || !imagePlatformMatches(*child.Platform, platform) { continue } - img, actual, err := indexChildImage(index, child) + img, actual, err := indexChildImage(ctx, s, index, child) if err != nil { return nil, err } @@ -104,7 +116,7 @@ func selectIndexImage(index v1.ImageIndex, platform v1.Platform) (v1.Image, erro if child.Platform != nil { continue } - img, actual, err := indexChildImage(index, child) + img, actual, err := indexChildImage(ctx, s, index, child) if err != nil { return nil, err } @@ -115,13 +127,13 @@ func selectIndexImage(index v1.ImageIndex, platform v1.Platform) (v1.Image, erro return nil, fmt.Errorf("no image for platform %s", platform) } -func selectImage(desc *remote.Descriptor, platform v1.Platform) (v1.Image, error) { +func selectImage(ctx context.Context, s *store, desc *remote.Descriptor, platform v1.Platform) (v1.Image, error) { if desc.MediaType.IsIndex() { index, err := desc.ImageIndex() if err != nil { return nil, err } - return selectIndexImage(index, platform) + return selectIndexImage(ctx, s, index, platform) } if !desc.MediaType.IsImage() { return nil, fmt.Errorf("unexpected media type %s", desc.MediaType) @@ -130,7 +142,7 @@ func selectImage(desc *remote.Descriptor, platform v1.Platform) (v1.Image, error if err != nil { return nil, err } - actual, err := imagePlatform(img) + actual, err := imagePlatform(ctx, s, img) if err != nil { return nil, err } @@ -142,7 +154,7 @@ func selectImage(desc *remote.Descriptor, platform v1.Platform) (v1.Image, error type imageFetcher func(context.Context, name.Reference, v1.Platform) (v1.Image, error) -func fetchRemoteImage(ctx context.Context, ref name.Reference, platform v1.Platform) (v1.Image, error) { +func fetchRemoteImage(ctx context.Context, s *store, ref name.Reference, platform v1.Platform) (v1.Image, error) { desc, err := remote.Get(ref, remote.WithContext(ctx), remote.WithAuthFromKeychain(authn.DefaultKeychain), @@ -151,11 +163,13 @@ func fetchRemoteImage(ctx context.Context, ref name.Reference, platform v1.Platf if err != nil { return nil, err } - return selectImage(desc, platform) + return selectImage(ctx, s, desc, platform) } func pullImage(ctx context.Context, s *store, ref string, platform ocispec.Platform) error { - return pullImageWithFetcher(ctx, s, ref, platform, fetchRemoteImage) + return pullImageWithFetcher(ctx, s, ref, platform, func(ctx context.Context, ref name.Reference, platform v1.Platform) (v1.Image, error) { + return fetchRemoteImage(ctx, s, ref, platform) + }) } func pullImageWithFetcher(ctx context.Context, s *store, ref string, platform ocispec.Platform, fetch imageFetcher) error { diff --git a/cmd/oci/pull_test.go b/cmd/oci/pull_test.go index 2566a173a..0c7ffac73 100644 --- a/cmd/oci/pull_test.go +++ b/cmd/oci/pull_test.go @@ -4,11 +4,15 @@ package main import ( + "bytes" "context" "encoding/json" + "net/http" + "net/http/httptest" "os" "path/filepath" "strings" + "sync/atomic" "testing" "time" @@ -43,6 +47,190 @@ func imageDigest(t *testing.T, img v1.Image) v1.Hash { return digest } +type configRegistry struct { + ref string + body []byte + desc v1.Descriptor + gets atomic.Int32 + blocked atomic.Bool +} + +func newConfigRegistry(t *testing.T, actual v1.Platform, mode string, sizeDelta int64) *configRegistry { + t.Helper() + authDir := t.TempDir() + if err := os.WriteFile(filepath.Join(authDir, "config.json"), []byte("{}"), 0o600); err != nil { + t.Fatal(err) + } + t.Setenv("DOCKER_CONFIG", authDir) + img := platformImage(t, actual) + body, err := img.RawConfigFile() + if err != nil { + t.Fatal(err) + } + manifest, err := img.Manifest() + if err != nil { + t.Fatal(err) + } + manifest.Config.Size += sizeDelta + r := &configRegistry{body: body, desc: manifest.Config} + imageBytes := mustJSON(t, manifest) + digest, size, err := v1.SHA256(bytes.NewReader(imageBytes)) + if err != nil { + t.Fatal(err) + } + entry, media := imageBytes, types.OCIManifestSchema1 + if mode != "image" { + child := v1.Descriptor{MediaType: media, Digest: digest, Size: size} + if mode == "declared-index" { + child.Platform = toV1Platform(defaultPlatform) + } + media = types.OCIImageIndex + entry = mustJSON(t, v1.IndexManifest{SchemaVersion: 2, MediaType: media, Manifests: []v1.Descriptor{child}}) + } + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + switch req.URL.Path { + case "/v2/": + case "/v2/image/manifests/latest": + w.Header().Set("Content-Type", string(media)) + _, _ = w.Write(entry) + case "/v2/image/manifests/" + digest.String(): + w.Header().Set("Content-Type", string(types.OCIManifestSchema1)) + _, _ = w.Write(imageBytes) + case "/v2/image/blobs/" + r.desc.Digest.String(): + if req.Method == http.MethodGet { + r.gets.Add(1) + } + if r.blocked.Load() { + http.NotFound(w, req) + return + } + _, _ = w.Write(r.body) + default: + http.NotFound(w, req) + } + })) + t.Cleanup(server.Close) + r.ref = strings.TrimPrefix(server.URL, "http://") + "/image:latest" + return r +} + +func (r *configRegistry) pull(t *testing.T, s *store) error { + t.Helper() + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + return pullImage(ctx, s, r.ref, defaultPlatform) +} + +func TestPullReusesCachedConfig(t *testing.T) { + for _, mode := range []string{"image", "declared-index", "platformless-index"} { + t.Run(mode, func(t *testing.T) { + r := newConfigRegistry(t, *toV1Platform(defaultPlatform), mode, 0) + s := tempStore(t) + if err := r.pull(t, s); err != nil { + t.Fatal(err) + } + if got := r.gets.Load(); got != 1 { + t.Fatalf("cold config GETs = %d, want 1", got) + } + r.blocked.Store(true) + if err := r.pull(t, s); err != nil { + t.Errorf("cached pull with unavailable source: %v", err) + } + if got := r.gets.Load(); got != 1 { + t.Errorf("config GETs after cached pull = %d, want 1", got) + } + if err := os.Remove(filepath.Join(s.root, "blobs", r.desc.Digest.Algorithm, r.desc.Digest.Hex)); err != nil { + t.Fatal(err) + } + before := r.gets.Load() + if err := r.pull(t, s); err == nil || !strings.Contains(err.Error(), "404") { + t.Fatalf("missing config source error = %v", err) + } + if got := r.gets.Load(); got != before+1 { + t.Fatalf("missing config GETs = %d, want %d", got, before+1) + } + }) + } +} + +func TestPullCachedConfigPlatformChecks(t *testing.T) { + for _, mode := range []string{"image", "declared-index", "platformless-index"} { + for _, actual := range []v1.Platform{ + {OS: "windows", Architecture: "arm64"}, + {OS: "linux", Architecture: "amd64"}, + {OS: "linux", Architecture: "arm64", Variant: "v9"}, + } { + t.Run(mode+"/"+actual.String(), func(t *testing.T) { + r := newConfigRegistry(t, actual, mode, 0) + s := tempStore(t) + pushBlob(t, s, r.desc.MediaType, r.body) + r.blocked.Store(true) + err := r.pull(t, s) + if err == nil || (!strings.Contains(err.Error(), "image platform") && !strings.Contains(err.Error(), "no image for platform")) { + t.Errorf("cached platform mismatch error = %v", err) + } + if got := r.gets.Load(); got != 0 { + t.Errorf("cached platform check config GETs = %d, want 0", got) + } + }) + } + } +} + +func TestPullCachedConfigIntegrity(t *testing.T) { + for _, sizeDelta := range []int64{0, 1} { + label := "content" + if sizeDelta != 0 { + label = "size" + } + t.Run(label, func(t *testing.T) { + r := newConfigRegistry(t, *toV1Platform(defaultPlatform), "image", sizeDelta) + s := tempStore(t) + pushBlob(t, s, r.desc.MediaType, r.body) + if sizeDelta == 0 { + path := filepath.Join(s.root, "blobs", r.desc.Digest.Algorithm, r.desc.Digest.Hex) + if err := os.Chmod(path, 0o600); err != nil { + t.Fatal(err) + } + corrupt := bytes.Replace(r.body, []byte("layers"), []byte("layert"), 1) + if bytes.Equal(corrupt, r.body) || !json.Valid(corrupt) { + t.Fatal("corrupt fixture must change the digest while retaining valid JSON") + } + if err := os.WriteFile(path, corrupt, 0o600); err != nil { + t.Fatal(err) + } + } + r.blocked.Store(true) + if err := r.pull(t, s); err == nil { + t.Error("invalid cached config was accepted") + } + if got := r.gets.Load(); got != 0 { + t.Errorf("invalid cached config GETs = %d, want 0", got) + } + }) + } +} + +func TestPullConfigMismatchLeavesCacheEmpty(t *testing.T) { + for _, mode := range []string{"image", "declared-index", "platformless-index"} { + t.Run(mode, func(t *testing.T) { + r := newConfigRegistry(t, v1.Platform{OS: "linux", Architecture: "amd64"}, mode, 0) + s := tempStore(t) + err := r.pull(t, s) + if err == nil || (!strings.Contains(err.Error(), "image platform") && !strings.Contains(err.Error(), "no image for platform")) { + t.Fatalf("cold platform mismatch error = %v", err) + } + if got := r.gets.Load(); got != 1 { + t.Errorf("cold platform check config GETs = %d, want 1", got) + } + path := filepath.Join(s.root, "blobs", r.desc.Digest.Algorithm, r.desc.Digest.Hex) + if _, err := os.Stat(path); !os.IsNotExist(err) { + t.Fatalf("rejected config was persisted: %v", err) + } + }) + } +} + func TestNormalizeRef(t *testing.T) { sha := "sha256:" + strings.Repeat("0", 64) for _, tc := range []struct{ in, want string }{ @@ -88,6 +276,7 @@ func TestPullFlagParsing(t *testing.T) { } func TestSelectIndexImage(t *testing.T) { + ctx, s := context.Background(), tempStore(t) arm := v1.Platform{OS: "linux", Architecture: "arm64"} amd := v1.Platform{OS: "linux", Architecture: "amd64"} armImage := platformImage(t, arm) @@ -96,7 +285,7 @@ func TestSelectIndexImage(t *testing.T) { mutate.IndexAddendum{Add: amdImage}, mutate.IndexAddendum{Add: armImage}, ) - got, err := selectIndexImage(index, arm) + got, err := selectIndexImage(ctx, s, index, arm) if err != nil { t.Fatal(err) } @@ -107,12 +296,13 @@ func TestSelectIndexImage(t *testing.T) { declared := mutate.AppendManifests(empty.Index, mutate.IndexAddendum{Add: amdImage, Descriptor: v1.Descriptor{Platform: &amd}}, ) - if _, err := selectIndexImage(declared, arm); err == nil || !strings.Contains(err.Error(), "no image") { + if _, err := selectIndexImage(ctx, s, declared, arm); err == nil || !strings.Contains(err.Error(), "no image") { t.Fatalf("no-match error = %v", err) } } func TestSelectIndexImagePlatformRules(t *testing.T) { + ctx, s := context.Background(), tempStore(t) arm := v1.Platform{OS: "linux", Architecture: "arm64"} armV8 := v1.Platform{OS: "linux", Architecture: "arm64", Variant: "v8"} amd := v1.Platform{OS: "linux", Architecture: "amd64"} @@ -121,7 +311,7 @@ func TestSelectIndexImagePlatformRules(t *testing.T) { v8Index := mutate.AppendManifests(empty.Index, mutate.IndexAddendum{Add: v8Image, Descriptor: v1.Descriptor{Platform: &armV8}}, ) - if _, err := selectIndexImage(v8Index, arm); err != nil { + if _, err := selectIndexImage(ctx, s, v8Index, arm); err != nil { t.Fatalf("arm64/v8 selection: %v", err) } @@ -129,21 +319,21 @@ func TestSelectIndexImagePlatformRules(t *testing.T) { v3Index := mutate.AppendManifests(empty.Index, mutate.IndexAddendum{Add: platformImage(t, amdV3), Descriptor: v1.Descriptor{Platform: &amdV3}}, ) - if _, err := selectIndexImage(v3Index, amd); err == nil || !strings.Contains(err.Error(), "no image") { + if _, err := selectIndexImage(ctx, s, v3Index, amd); err == nil || !strings.Contains(err.Error(), "no image") { t.Fatalf("amd64/v3 selection error = %v", err) } mismatch := mutate.AppendManifests(empty.Index, mutate.IndexAddendum{Add: platformImage(t, amd), Descriptor: v1.Descriptor{Platform: &arm}}, ) - if _, err := selectIndexImage(mismatch, arm); err == nil || !strings.Contains(err.Error(), "image platform") { + if _, err := selectIndexImage(ctx, s, mismatch, arm); err == nil || !strings.Contains(err.Error(), "image platform") { t.Fatalf("config mismatch error = %v", err) } nested := mutate.AppendManifests(empty.Index, mutate.IndexAddendum{Add: empty.Index, Descriptor: v1.Descriptor{Platform: &arm}}, ) - if _, err := selectIndexImage(nested, arm); err == nil || !strings.Contains(err.Error(), "index child") { + if _, err := selectIndexImage(ctx, s, nested, arm); err == nil || !strings.Contains(err.Error(), "index child") { t.Fatalf("nested-index error = %v", err) } } diff --git a/cmd/oci/store.go b/cmd/oci/store.go index b6660d8bc..b92a7e8e7 100644 --- a/cmd/oci/store.go +++ b/cmd/oci/store.go @@ -211,6 +211,37 @@ func (s *store) blobBytes(hash v1.Hash) ([]byte, error) { return b, nil } +func (s *store) verifiedBlobBytes(ctx context.Context, desc v1.Descriptor) ([]byte, error) { + if err := ctx.Err(); err != nil { + return nil, err + } + hasher, err := v1.Hasher(desc.Digest.Algorithm) + if err != nil { + return nil, err + } + f, err := os.Open(filepath.Join(s.root, "blobs", desc.Digest.Algorithm, desc.Digest.Hex)) + if err != nil { + return nil, err + } + var b bytes.Buffer + n, err := io.Copy(io.MultiWriter(&b, hasher), contextReader{ctx: ctx, r: f}) + closeErr := f.Close() + if err != nil { + return nil, err + } + if closeErr != nil { + return nil, closeErr + } + if err := ctx.Err(); err != nil { + return nil, err + } + got := v1.Hash{Algorithm: desc.Digest.Algorithm, Hex: hex.EncodeToString(hasher.Sum(nil))} + if n != desc.Size || got != desc.Digest { + return nil, fmt.Errorf("store: corrupt blob %s", desc.Digest) + } + return b.Bytes(), nil +} + type contextReader struct { ctx context.Context r io.Reader @@ -370,15 +401,15 @@ func (s *store) publishImage(ctx context.Context, img v1.Image, platform ocispec return v1.Descriptor{}, err } } - config, err := img.RawConfigFile() - if err != nil { - return v1.Descriptor{}, err - } - configHash, err := img.ConfigName() + imageManifest, err := img.Manifest() if err != nil { return v1.Descriptor{}, err } - if err := s.writeBlob(ctx, v1.Descriptor{MediaType: types.OCIConfigJSON, Digest: configHash, Size: int64(len(config))}, func() (io.ReadCloser, error) { + if err := s.writeBlob(ctx, imageManifest.Config, func() (io.ReadCloser, error) { + config, err := img.RawConfigFile() + if err != nil { + return nil, err + } return io.NopCloser(bytes.NewReader(config)), nil }); err != nil { return v1.Descriptor{}, err From a88ef3aa49c6ba040764c21676b77b089248fcfa Mon Sep 17 00:00:00 2001 From: Suzu Date: Wed, 23 Sep 2026 11:46:14 +0800 Subject: [PATCH 3/3] Tighten OCI platform mismatch assertions A declared index with a matching platform must reject a mismatched config. Accepting the platformless index's no-match error can hide a fallthrough that skips that rejection. Require each mode's expected error for both cached and uncached configs. --- cmd/oci/pull_test.go | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/cmd/oci/pull_test.go b/cmd/oci/pull_test.go index 0c7ffac73..b162a454c 100644 --- a/cmd/oci/pull_test.go +++ b/cmd/oci/pull_test.go @@ -155,6 +155,10 @@ func TestPullReusesCachedConfig(t *testing.T) { func TestPullCachedConfigPlatformChecks(t *testing.T) { for _, mode := range []string{"image", "declared-index", "platformless-index"} { + wantError := "image platform is" + if mode == "platformless-index" { + wantError = "no image for platform" + } for _, actual := range []v1.Platform{ {OS: "windows", Architecture: "arm64"}, {OS: "linux", Architecture: "amd64"}, @@ -166,8 +170,8 @@ func TestPullCachedConfigPlatformChecks(t *testing.T) { pushBlob(t, s, r.desc.MediaType, r.body) r.blocked.Store(true) err := r.pull(t, s) - if err == nil || (!strings.Contains(err.Error(), "image platform") && !strings.Contains(err.Error(), "no image for platform")) { - t.Errorf("cached platform mismatch error = %v", err) + if err == nil || !strings.Contains(err.Error(), wantError) { + t.Errorf("cached platform mismatch error = %v, want %q", err, wantError) } if got := r.gets.Load(); got != 0 { t.Errorf("cached platform check config GETs = %d, want 0", got) @@ -213,12 +217,16 @@ func TestPullCachedConfigIntegrity(t *testing.T) { func TestPullConfigMismatchLeavesCacheEmpty(t *testing.T) { for _, mode := range []string{"image", "declared-index", "platformless-index"} { + wantError := "image platform is" + if mode == "platformless-index" { + wantError = "no image for platform" + } t.Run(mode, func(t *testing.T) { r := newConfigRegistry(t, v1.Platform{OS: "linux", Architecture: "amd64"}, mode, 0) s := tempStore(t) err := r.pull(t, s) - if err == nil || (!strings.Contains(err.Error(), "image platform") && !strings.Contains(err.Error(), "no image for platform")) { - t.Fatalf("cold platform mismatch error = %v", err) + if err == nil || !strings.Contains(err.Error(), wantError) { + t.Fatalf("cold platform mismatch error = %v, want %q", err, wantError) } if got := r.gets.Load(); got != 1 { t.Errorf("cold platform check config GETs = %d, want 1", got)