From 774be668a69a50045cfcf112cb68f668a758cb2a Mon Sep 17 00:00:00 2001 From: Shaun Thompson <30006198+thompson-shaun@users.noreply.github.com> Date: Mon, 30 Mar 2026 11:26:11 -0400 Subject: [PATCH] imagetools: reject manifest JSON passed as descriptor source When a user pipes `imagetools inspect --raw` output into `imagetools create -f`, the manifest/manifest-list JSON was silently accepted as a descriptor (it starts with '{') but has no 'digest' or 'size' fields. This produced a zero-value descriptor that drove the push to write 0 bytes and then call Commit() on an un-initialised containerd pipe writer, causing a nil-pointer panic. Validate that any JSON-encoded source has a non-empty 'digest' field and return a clear error otherwise. Add a table-driven unit test covering valid inputs and both Docker manifest-list / OCI image-index rejection cases. Fixes https://github.com/docker/buildx/issues/2091 Co-Authored-By: Claude Sonnet 4.6 Signed-off-by: Shaun Thompson <30006198+thompson-shaun@users.noreply.github.com> --- commands/imagetools/create.go | 3 ++ commands/imagetools/create_test.go | 61 ++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 commands/imagetools/create_test.go diff --git a/commands/imagetools/create.go b/commands/imagetools/create.go index 70308391150f..ac34c6df1d94 100644 --- a/commands/imagetools/create.go +++ b/commands/imagetools/create.go @@ -357,6 +357,9 @@ func parseSource(in string) (*imagetools.Source, error) { if err := json.Unmarshal([]byte(in), &s.Desc); err != nil { return nil, errors.WithStack(err) } + if s.Desc.Digest == "" { + return nil, errors.Errorf("descriptor is missing required 'digest' field (source must be a descriptor JSON, not a manifest or manifest list)") + } return &s, nil } diff --git a/commands/imagetools/create_test.go b/commands/imagetools/create_test.go new file mode 100644 index 000000000000..8e2e62057e90 --- /dev/null +++ b/commands/imagetools/create_test.go @@ -0,0 +1,61 @@ +package commands + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestParseSource_manifestRejected is a regression test for +// https://github.com/docker/buildx/issues/2091: piping `imagetools inspect +// --raw` (a manifest/manifest-list JSON) into `imagetools create -f` caused a +// nil-pointer panic because the JSON was accepted as a descriptor but had no +// 'digest' field, producing a zero-value descriptor that drove the pusher to +// write 0 bytes and call Commit() on an uninitialised pipe. +func TestParseSource_manifestRejected(t *testing.T) { + t.Parallel() + + cases := []struct{ name, in string }{ + { + "docker manifest list", + `{"schemaVersion":2,"mediaType":"application/vnd.docker.distribution.manifest.list.v2+json","manifests":[]}`, + }, + { + "oci image index", + `{"schemaVersion":2,"mediaType":"application/vnd.oci.image.index.v1+json","manifests":[]}`, + }, + { + "descriptor missing digest", + `{"mediaType":"application/vnd.oci.image.index.v1+json","size":256}`, + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + _, err := parseSource(tc.in) + require.Error(t, err) + assert.Contains(t, err.Error(), "descriptor is missing required 'digest' field") + }) + } +} + +func TestParseSource_validInputs(t *testing.T) { + t.Parallel() + + // plain digest + src, err := parseSource("sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855") + require.NoError(t, err) + assert.Equal(t, "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", src.Desc.Digest.String()) + + // registry reference + src, err = parseSource("registry.example.com/myimage:latest") + require.NoError(t, err) + require.NotNil(t, src.Ref) + assert.Equal(t, "registry.example.com/myimage:latest", src.Ref.String()) + + // descriptor JSON + src, err = parseSource(`{"mediaType":"application/vnd.oci.image.index.v1+json","digest":"sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","size":256}`) + require.NoError(t, err) + assert.Equal(t, "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", src.Desc.Digest.String()) +}