From 76fc92eda11b9bb323b9669629207e0b3f818411 Mon Sep 17 00:00:00 2001 From: Arpit Jain Date: Thu, 16 Jul 2026 12:28:52 +0900 Subject: [PATCH] compose: guard x-nerdctl-* extension type assertions The compose image verify/pull/push paths read x-nerdctl-* extension values out of the parsed compose file and assert them to string without checking the type. compose-go accepts any value under an x-* key, so a malformed entry like `x-nerdctl-verify: 123` loads fine and then panics the CLI with "interface conversion: interface {} is int, not string" instead of being ignored or reported. Use the comma-ok form at every extension site (compose up in pkg/cmd/compose, compose pull and push in pkg/composer) so a non-string value falls back to the default rather than crashing. Adds a regression test that drives the real compose-go loader with an int-valued x-nerdctl-verify and checks the provider defaults to none. Signed-off-by: Arpit Jain --- pkg/cmd/compose/compose.go | 24 +++++++------- pkg/cmd/compose/compose_test.go | 55 +++++++++++++++++++++++++++++++++ pkg/composer/pull.go | 24 +++++++------- pkg/composer/push.go | 8 ++--- 4 files changed, 83 insertions(+), 28 deletions(-) create mode 100644 pkg/cmd/compose/compose_test.go diff --git a/pkg/cmd/compose/compose.go b/pkg/cmd/compose/compose.go index fd4e2cfa466..146c4997b99 100644 --- a/pkg/cmd/compose/compose.go +++ b/pkg/cmd/compose/compose.go @@ -162,27 +162,27 @@ func New(client *containerd.Client, globalOptions types.GlobalCommandOptions, op func imageVerifyOptionsFromCompose(ps *serviceparser.Service) types.ImageVerifyOptions { var opt types.ImageVerifyOptions - if verifier, ok := ps.Unparsed.Extensions[serviceparser.ComposeVerify]; ok { - opt.Provider = verifier.(string) + if verifier, ok := ps.Unparsed.Extensions[serviceparser.ComposeVerify].(string); ok { + opt.Provider = verifier } else { opt.Provider = "none" } // for cosign, if key is given, use key mode, otherwise use keyless mode. - if keyVal, ok := ps.Unparsed.Extensions[serviceparser.ComposeCosignPublicKey]; ok { - opt.CosignKey = keyVal.(string) + if keyVal, ok := ps.Unparsed.Extensions[serviceparser.ComposeCosignPublicKey].(string); ok { + opt.CosignKey = keyVal } - if ciVal, ok := ps.Unparsed.Extensions[serviceparser.ComposeCosignCertificateIdentity]; ok { - opt.CosignCertificateIdentity = ciVal.(string) + if ciVal, ok := ps.Unparsed.Extensions[serviceparser.ComposeCosignCertificateIdentity].(string); ok { + opt.CosignCertificateIdentity = ciVal } - if cirVal, ok := ps.Unparsed.Extensions[serviceparser.ComposeCosignCertificateIdentityRegexp]; ok { - opt.CosignCertificateIdentityRegexp = cirVal.(string) + if cirVal, ok := ps.Unparsed.Extensions[serviceparser.ComposeCosignCertificateIdentityRegexp].(string); ok { + opt.CosignCertificateIdentityRegexp = cirVal } - if coiVal, ok := ps.Unparsed.Extensions[serviceparser.ComposeCosignCertificateOidcIssuer]; ok { - opt.CosignCertificateOidcIssuer = coiVal.(string) + if coiVal, ok := ps.Unparsed.Extensions[serviceparser.ComposeCosignCertificateOidcIssuer].(string); ok { + opt.CosignCertificateOidcIssuer = coiVal } - if coirVal, ok := ps.Unparsed.Extensions[serviceparser.ComposeCosignCertificateOidcIssuerRegexp]; ok { - opt.CosignCertificateOidcIssuerRegexp = coirVal.(string) + if coirVal, ok := ps.Unparsed.Extensions[serviceparser.ComposeCosignCertificateOidcIssuerRegexp].(string); ok { + opt.CosignCertificateOidcIssuerRegexp = coirVal } return opt } diff --git a/pkg/cmd/compose/compose_test.go b/pkg/cmd/compose/compose_test.go new file mode 100644 index 00000000000..7d943bc3a34 --- /dev/null +++ b/pkg/cmd/compose/compose_test.go @@ -0,0 +1,55 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package compose + +import ( + "testing" + + "gotest.tools/v3/assert" + + "github.com/containerd/nerdctl/v2/pkg/composer/serviceparser" + "github.com/containerd/nerdctl/v2/pkg/testutil" +) + +// TestImageVerifyOptionsFromComposeNonStringExtension makes sure a non-string +// x-nerdctl-verify value in a compose file does not crash the CLI. compose-go +// accepts any value under an x-* key, so a user typo like `x-nerdctl-verify: 123` +// used to reach an unguarded type assertion and panic with +// "interface conversion: interface {} is int, not string". +func TestImageVerifyOptionsFromComposeNonStringExtension(t *testing.T) { + const dockerComposeYAML = ` +services: + app: + image: alpine:latest + x-nerdctl-verify: 123 +` + comp := testutil.NewComposeDir(t, dockerComposeYAML) + defer comp.CleanUp() + + project, err := testutil.LoadProject(comp.YAMLFullPath(), comp.ProjectName(), nil) + assert.NilError(t, err) + + svcConfig, err := project.GetService("app") + assert.NilError(t, err) + + ps, err := serviceparser.Parse(project, svcConfig) + assert.NilError(t, err) + + opt := imageVerifyOptionsFromCompose(ps) + // A non-string verify value is ignored and falls back to the default. + assert.Equal(t, "none", opt.Provider) +} diff --git a/pkg/composer/pull.go b/pkg/composer/pull.go index 758d342f49e..ae65c01d479 100644 --- a/pkg/composer/pull.go +++ b/pkg/composer/pull.go @@ -52,23 +52,23 @@ func (c *Composer) pullServiceImage(ctx context.Context, image string, platform if po.Quiet { args = append(args, "--quiet") } - if verifier, ok := ps.Unparsed.Extensions[serviceparser.ComposeVerify]; ok { - args = append(args, "--verify="+verifier.(string)) + if verifier, ok := ps.Unparsed.Extensions[serviceparser.ComposeVerify].(string); ok { + args = append(args, "--verify="+verifier) } - if publicKey, ok := ps.Unparsed.Extensions[serviceparser.ComposeCosignPublicKey]; ok { - args = append(args, "--cosign-key="+publicKey.(string)) + if publicKey, ok := ps.Unparsed.Extensions[serviceparser.ComposeCosignPublicKey].(string); ok { + args = append(args, "--cosign-key="+publicKey) } - if certificateIdentity, ok := ps.Unparsed.Extensions[serviceparser.ComposeCosignCertificateIdentity]; ok { - args = append(args, "--cosign-certificate-identity="+certificateIdentity.(string)) + if certificateIdentity, ok := ps.Unparsed.Extensions[serviceparser.ComposeCosignCertificateIdentity].(string); ok { + args = append(args, "--cosign-certificate-identity="+certificateIdentity) } - if certificateIdentityRegexp, ok := ps.Unparsed.Extensions[serviceparser.ComposeCosignCertificateIdentityRegexp]; ok { - args = append(args, "--cosign-certificate-identity-regexp="+certificateIdentityRegexp.(string)) + if certificateIdentityRegexp, ok := ps.Unparsed.Extensions[serviceparser.ComposeCosignCertificateIdentityRegexp].(string); ok { + args = append(args, "--cosign-certificate-identity-regexp="+certificateIdentityRegexp) } - if certificateOidcIssuer, ok := ps.Unparsed.Extensions[serviceparser.ComposeCosignCertificateOidcIssuer]; ok { - args = append(args, "--cosign-certificate-oidc-issuer="+certificateOidcIssuer.(string)) + if certificateOidcIssuer, ok := ps.Unparsed.Extensions[serviceparser.ComposeCosignCertificateOidcIssuer].(string); ok { + args = append(args, "--cosign-certificate-oidc-issuer="+certificateOidcIssuer) } - if certificateOidcIssuerRegexp, ok := ps.Unparsed.Extensions[serviceparser.ComposeCosignCertificateOidcIssuerRegexp]; ok { - args = append(args, "--cosign-certificate-oidc-issuer-regexp="+certificateOidcIssuerRegexp.(string)) + if certificateOidcIssuerRegexp, ok := ps.Unparsed.Extensions[serviceparser.ComposeCosignCertificateOidcIssuerRegexp].(string); ok { + args = append(args, "--cosign-certificate-oidc-issuer-regexp="+certificateOidcIssuerRegexp) } if c.Options.Experimental { diff --git a/pkg/composer/push.go b/pkg/composer/push.go index 5f384601863..02f69277a9c 100644 --- a/pkg/composer/push.go +++ b/pkg/composer/push.go @@ -48,11 +48,11 @@ func (c *Composer) pushServiceImage(ctx context.Context, image string, platform if platform != "" { args = append(args, "--platform="+platform) } - if signer, ok := ps.Unparsed.Extensions[serviceparser.ComposeSign]; ok { - args = append(args, "--sign="+signer.(string)) + if signer, ok := ps.Unparsed.Extensions[serviceparser.ComposeSign].(string); ok { + args = append(args, "--sign="+signer) } - if privateKey, ok := ps.Unparsed.Extensions[serviceparser.ComposeCosignPrivateKey]; ok { - args = append(args, "--cosign-key="+privateKey.(string)) + if privateKey, ok := ps.Unparsed.Extensions[serviceparser.ComposeCosignPrivateKey].(string); ok { + args = append(args, "--cosign-key="+privateKey) } if c.Options.Experimental { args = append(args, "--experimental")