diff --git a/bake/bake.go b/bake/bake.go index 290e8670d590..30eac7bc72b6 100644 --- a/bake/bake.go +++ b/bake/bake.go @@ -23,6 +23,7 @@ import ( "github.com/docker/buildx/bake/hclparser" "github.com/docker/buildx/build" "github.com/docker/buildx/util/buildflags" + "github.com/docker/buildx/util/ocilayout" "github.com/docker/buildx/util/osutil" "github.com/docker/buildx/util/platformutil" "github.com/docker/buildx/util/progress" @@ -1614,7 +1615,7 @@ func remoteURLWithSubdir(remoteURL, subdir string) string { return base + "#" + ref + ":" + subdir } -func collectLocalPaths(t build.Inputs) []string { +func collectLocalPaths(t build.Inputs) ([]string, error) { var out []string if t.ContextState == nil { if v, ok := isLocalPath(t.ContextPath); ok { @@ -1630,11 +1631,18 @@ func collectLocalPaths(t build.Inputs) []string { if v.State != nil { continue } + if ref, ok, err := ocilayout.Parse(v.Path); ok { + if err != nil { + return nil, errors.Wrapf(err, "invalid OCI layout context %q", v.Path) + } + out = append(out, ref.Path) + continue + } if v, ok := isLocalPath(v.Path); ok { out = append(out, v) } } - return out + return out, nil } func isLocalPath(p string) (string, bool) { diff --git a/bake/entitlements.go b/bake/entitlements.go index b42ba1ab7bb9..43ad6c82f84b 100644 --- a/bake/entitlements.go +++ b/bake/entitlements.go @@ -162,7 +162,11 @@ func (c EntitlementConf) check(bo build.Options, expected *EntitlementConf) erro rwPaths := map[string]struct{}{} roPaths := map[string]struct{}{} - for _, p := range collectLocalPaths(bo.Inputs) { + localPaths, err := collectLocalPaths(bo.Inputs) + if err != nil { + return err + } + for _, p := range localPaths { roPaths[p] = struct{}{} } @@ -200,8 +204,8 @@ func (c EntitlementConf) check(bo build.Options, expected *EntitlementConf) erro } for _, secret := range bo.SecretSpecs { - if secret.FilePath != "" { - roPaths[secret.FilePath] = struct{}{} + if filePath := secret.ResolveSource().FilePath; filePath != "" { + roPaths[filePath] = struct{}{} } } @@ -216,7 +220,6 @@ func (c EntitlementConf) check(bo build.Options, expected *EntitlementConf) erro } } - var err error expected.FSRead, err = findMissingPaths(c.FSRead, roPaths) if err != nil { return err @@ -231,9 +234,19 @@ func (c EntitlementConf) check(bo build.Options, expected *EntitlementConf) erro } func (c EntitlementConf) Prompt(ctx context.Context, isRemote bool, out io.Writer) error { + return c.prompt(ctx, isRemote, out, true) +} + +func (c EntitlementConf) Check(isRemote bool) error { + return c.prompt(context.Background(), isRemote, io.Discard, false) +} + +func (c EntitlementConf) prompt(ctx context.Context, isRemote bool, out io.Writer, interactive bool) error { var term bool - if _, err := console.ConsoleFromFile(os.Stdin); err == nil { - term = true + if interactive { + if _, err := console.ConsoleFromFile(os.Stdin); err == nil { + term = true + } } var msgs []string @@ -365,6 +378,13 @@ func (c EntitlementConf) Prompt(ctx context.Context, isRemote bool, out io.Write if fsEntitlementsEnabled && !fsEntitlementsSet && len(msgsFS) != 0 { fmt.Fprintf(out, "To disable filesystem entitlements checks, you can set BUILDX_BAKE_ENTITLEMENTS_FS=0 .\n\n") } + if !interactive { + requiredFlags := flags + if fsEntitlementsEnabled { + requiredFlags = slices.Concat(requiredFlags, flagsFS) + } + return errors.Errorf("additional privileges requested: pass %q to grant requested privileges", strings.Join(requiredFlags, " ")) + } if term { fmt.Fprintf(out, "Do you want to grant requested privileges and continue? [y/N] ") diff --git a/bake/entitlements_test.go b/bake/entitlements_test.go index d1f67fe43362..c96795a98939 100644 --- a/bake/entitlements_test.go +++ b/bake/entitlements_test.go @@ -7,6 +7,7 @@ import ( "os" "path/filepath" "slices" + "strings" "testing" "github.com/docker/buildx/build" @@ -107,6 +108,13 @@ func TestValidateEntitlements(t *testing.T) { require.NoError(t, err) expWd, err := filepath.EvalSymlinks(wd) require.NoError(t, err) + credentialPath := filepath.Join(dir1, "credential") + require.NoError(t, os.WriteFile(credentialPath, []byte("test credential"), 0600)) + expCredentialPath, err := filepath.EvalSymlinks(credentialPath) + require.NoError(t, err) + layoutLink := filepath.Join(dir1, "layout-link") + require.NoError(t, os.Symlink(dir2, layoutLink)) + t.Setenv("BUILDX_TEST_SECRET_SOURCE", "test credential") tcases := []struct { name string @@ -223,6 +231,70 @@ func TestValidateEntitlements(t *testing.T) { FSRead: []string{wd, dir1}, }, }, + { + name: "secret-id-file-fallback-requires-read", + opt: build.Options{ + SecretSpecs: []*buildflags.Secret{{ID: credentialPath}}, + }, + conf: EntitlementConf{FSRead: []string{wd}}, + expected: EntitlementConf{ + FSRead: []string{expCredentialPath}, + }, + }, + { + name: "secret-id-file-fallback-allowed", + opt: build.Options{ + SecretSpecs: []*buildflags.Secret{{ID: credentialPath}}, + }, + conf: EntitlementConf{FSRead: []string{wd, dir1}}, + }, + { + name: "secret-id-env-fallback-needs-no-read", + opt: build.Options{ + SecretSpecs: []*buildflags.Secret{{ID: "BUILDX_TEST_SECRET_SOURCE"}}, + }, + conf: EntitlementConf{FSRead: []string{wd}}, + }, + { + name: "explicit-secret-env-needs-no-read", + opt: build.Options{ + SecretSpecs: []*buildflags.Secret{{ID: credentialPath, Env: "BUILDX_TEST_SECRET_SOURCE"}}, + }, + conf: EntitlementConf{FSRead: []string{wd}}, + }, + { + name: "oci-layout-named-context-requires-read", + opt: build.Options{ + Inputs: build.Inputs{NamedContexts: map[string]build.NamedContext{ + "layout": {Path: "oci-layout://" + dir1 + ":latest"}, + }}, + }, + conf: EntitlementConf{FSRead: []string{wd}}, + expected: EntitlementConf{ + FSRead: []string{expDir1}, + }, + }, + { + name: "oci-layout-named-context-allowed", + opt: build.Options{ + Inputs: build.Inputs{NamedContexts: map[string]build.NamedContext{ + "layout": {Path: "oci-layout://" + dir1 + ":latest"}, + }}, + }, + conf: EntitlementConf{FSRead: []string{wd, dir1}}, + }, + { + name: "oci-layout-named-context-symlink-requires-destination", + opt: build.Options{ + Inputs: build.Inputs{NamedContexts: map[string]build.NamedContext{ + "layout": {Path: "oci-layout://" + layoutLink + ":latest"}, + }}, + }, + conf: EntitlementConf{FSRead: []string{wd, dir1}}, + expected: EntitlementConf{ + FSRead: []string{expDir2}, + }, + }, { name: "SecretFromEscapeLink", opt: build.Options{ @@ -441,6 +513,61 @@ func TestPromptLocalOutputDeleteCannotBeDisabledWithFSEntitlements(t *testing.T) require.Contains(t, out.String(), "--allow=buildx.local.delete") } +func TestCheckEntitlements(t *testing.T) { + t.Run("all entitlement types", func(t *testing.T) { + t.Setenv("BUILDX_BAKE_ENTITLEMENTS_FS", "1") + + err := EntitlementConf{ + NetworkHost: true, + SecurityInsecure: true, + Devices: &EntitlementsDevicesConf{ + Devices: map[string]struct{}{"vendor.com/device=foo": {}}, + }, + FSRead: []string{t.TempDir()}, + FSWrite: []string{t.TempDir()}, + SSH: true, + LocalOutputDelete: true, + }.Check(false) + require.Error(t, err) + require.True(t, strings.HasPrefix(err.Error(), `additional privileges requested: pass "`), err.Error()) + for _, flag := range []string{ + "--allow=network.host", + "--allow=security.insecure", + "--allow=device=vendor.com/device=foo", + "--allow=fs.read=", + "--allow=fs.write=", + "--allow=ssh", + "--allow=buildx.local.delete", + } { + require.ErrorContains(t, err, flag) + } + }) + + t.Run("filesystem checks disabled", func(t *testing.T) { + t.Setenv("BUILDX_BAKE_ENTITLEMENTS_FS", "0") + + err := EntitlementConf{ + FSRead: []string{t.TempDir()}, + FSWrite: []string{t.TempDir()}, + SSH: true, + }.Check(false) + require.NoError(t, err) + }) + + t.Run("filesystem setting does not disable other checks", func(t *testing.T) { + t.Setenv("BUILDX_BAKE_ENTITLEMENTS_FS", "0") + + err := EntitlementConf{ + NetworkHost: true, + FSRead: []string{t.TempDir()}, + SSH: true, + }.Check(false) + require.ErrorContains(t, err, "--allow=network.host") + require.NotContains(t, err.Error(), "--allow=fs.read=") + require.NotContains(t, err.Error(), "--allow=ssh") + }) +} + func TestGroupSamePaths(t *testing.T) { tests := []struct { name string diff --git a/commands/bake.go b/commands/bake.go index 2924177f24d0..ff03f4fd6b43 100644 --- a/commands/bake.go +++ b/commands/bake.go @@ -341,8 +341,8 @@ func runBake(ctx context.Context, dockerCli command.Cli, targets []string, in ba return err } if progressMode == progressui.RawJSONMode { - if exp.LocalOutputDelete { - return errors.Errorf("additional privileges requested: pass %q to grant requested privileges", "--allow="+string(bake.EntitlementKeyBuildxLocalDelete)) + if err := exp.Check(url != ""); err != nil { + return err } } else { if err := exp.Prompt(ctx, url != "", &syncWriter{w: dockerCli.Err(), wait: printer.Wait}); err != nil { diff --git a/go.mod b/go.mod index 7f8aba175b44..bc9cc9886e49 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/docker/buildx -go 1.26.3 +go 1.26.8 require ( github.com/Masterminds/semver/v3 v3.4.0 @@ -9,7 +9,7 @@ require ( github.com/aws/aws-sdk-go-v2/config v1.32.39 github.com/compose-spec/compose-go/v2 v2.14.0 github.com/containerd/console v1.0.5 - github.com/containerd/containerd/v2 v2.3.4 + github.com/containerd/containerd/v2 v2.3.6 github.com/containerd/continuity v0.5.0 github.com/containerd/errdefs v1.0.0 github.com/containerd/log v0.1.0 @@ -29,7 +29,7 @@ require ( github.com/hashicorp/hcl/v2 v2.24.0 github.com/in-toto/in-toto-golang v0.11.0 github.com/mitchellh/hashstructure/v2 v2.0.2 - github.com/moby/buildkit v0.33.0 + github.com/moby/buildkit v0.33.1 github.com/moby/moby/api v1.55.0 github.com/moby/moby/client v0.5.0 github.com/moby/policy-helpers v0.0.0-20260901104222-dd6c5499c491 @@ -60,7 +60,7 @@ require ( go.opentelemetry.io/otel/sdk v1.45.0 go.opentelemetry.io/otel/trace v1.45.0 go.yaml.in/yaml/v3 v3.0.5 - golang.org/x/crypto v0.55.0 + golang.org/x/crypto v0.56.0 golang.org/x/mod v0.40.0 golang.org/x/sync v0.22.0 golang.org/x/sys v0.47.0 diff --git a/go.sum b/go.sum index 6c3a5b710ff3..41cd1af81150 100644 --- a/go.sum +++ b/go.sum @@ -118,8 +118,8 @@ github.com/containerd/console v1.0.5 h1:R0ymNeydRqH2DmakFNdmjR2k0t7UPuiOV/N/27/q github.com/containerd/console v1.0.5/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= github.com/containerd/containerd/api v1.11.1 h1:h8nfoDW9+fNsC/9TwiAHj8B1GzXKtR4eFtkhi/X5RLU= github.com/containerd/containerd/api v1.11.1/go.mod h1:CaQFRu+N1MtbgL6JDOJLUB1hCKESU1lD6MuTJhgtdlw= -github.com/containerd/containerd/v2 v2.3.4 h1:c2PJo/9UGVdiiw8SwrxuLxWGY+9b3jQ6Xp9zntneIvI= -github.com/containerd/containerd/v2 v2.3.4/go.mod h1:a30D8fWZJ1Uzx/2WpjLbLsxBkq9He41pe8ENW+QZ3LY= +github.com/containerd/containerd/v2 v2.3.6 h1:huoqZXaDW1x1kgPvFC4rMwxv92vUJnqgq7I5b1rSddM= +github.com/containerd/containerd/v2 v2.3.6/go.mod h1:qaUSWKk9EchqbudSbhv1MxI8dXKGYHLpZtMQdkEkVZ4= github.com/containerd/continuity v0.5.0 h1:7a85HZpCSs+1Zps0Ee3DPSuAWY+0SJM1JNM51nlEVDg= github.com/containerd/continuity v0.5.0/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= @@ -393,8 +393,8 @@ github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4 github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/moby/buildkit v0.33.0 h1:zBbt1FiMcTB/oFg1iCNcKa83k5Rn8MGcVjXFIcfYhuQ= -github.com/moby/buildkit v0.33.0/go.mod h1:uNKSZnfMk1aSa18JiCR5BT68M/3jIDGo6XuAByUK3ek= +github.com/moby/buildkit v0.33.1 h1:UUrdifmRdRadykO+f5l8BT1CseUXst2sJHV7AmkjjxE= +github.com/moby/buildkit v0.33.1/go.mod h1:584wW8T/WG4O+lpXUCoDaWEc5e+rTGC3iP+l9mNcX8E= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/go-archive v0.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8= @@ -634,8 +634,8 @@ go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= go.yaml.in/yaml/v4 v4.0.0-rc.4 h1:UP4+v6fFrBIb1l934bDl//mmnoIZEDK0idg1+AIvX5U= go.yaml.in/yaml/v4 v4.0.0-rc.4/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0= -golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M= -golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis= +golang.org/x/crypto v0.56.0 h1:GUh5Ii4J5jtcseSMiRqr1jXCNHoxjeV9Fmekc2oLy6Y= +golang.org/x/crypto v0.56.0/go.mod h1:OMW5y6CY9l38uPLmxU6l6pwcXp1obtLo3e6gT7gQR2I= golang.org/x/mod v0.40.0 h1:hUv+3cXcdRHz08UmSiOob7sadHig73uo5bkXxQ/tvUs= golang.org/x/mod v0.40.0/go.mod h1:0/weTWkPWGBikyTWAX3dkjVztMmBA5hM0DH6BElSupE= golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To= diff --git a/tests/bake.go b/tests/bake.go index 279bc3c1143e..fab9f018a9cb 100644 --- a/tests/bake.go +++ b/tests/bake.go @@ -69,6 +69,7 @@ var bakeTests = []func(t *testing.T, sb integration.Sandbox){ testBakeDefinitionNotExistingSubdirNoParallel, testBakeDefinitionNotExistingOutsideNoParallel, testBakeDefinitionExistingOutsideNoParallel, + testBakeRawJSONEntitlementsNoParallel, testBakeDefinitionSymlinkOutsideNoParallel, testBakeDefinitionSymlinkOutsideGrantedNoParallel, testBakeSSHPathNoParallel, @@ -1853,6 +1854,48 @@ target "default" { } } +func testBakeRawJSONEntitlementsNoParallel(t *testing.T, sb integration.Sandbox) { + t.Setenv("BUILDX_BAKE_ENTITLEMENTS_FS", "1") + dockerfile := []byte(` +FROM scratch +COPY foo /foo + `) + dirSrc := tmpdir( + t, + fstest.CreateFile("Dockerfile", dockerfile, 0600), + fstest.CreateFile("foo", []byte("foo"), 0600), + ) + dirDest := t.TempDir() + bakefile := fmt.Appendf(nil, ` +target "default" { + context = %q + output = ["type=local,dest=%s"] +} +`, dirSrc, dirDest) + dirSpec := tmpdir( + t, + fstest.CreateFile("docker-bake.hcl", bakefile, 0600), + ) + + cmd := buildxCmd(sb, withDir(dirSpec), withArgs("bake", "--progress=rawjson")) + out, err := cmd.CombinedOutput() + require.Error(t, err, string(out)) + require.Contains(t, string(out), "additional privileges requested") + require.Contains(t, string(out), "--allow=fs.read=") + require.Contains(t, string(out), "--allow=fs.write=") + require.NoFileExists(t, filepath.Join(dirDest, "foo")) + + cmd = buildxCmd(sb, withDir(dirSpec), withArgs( + "bake", + "--progress=rawjson", + "--allow=fs.read="+dirSrc, + "--allow=fs.write="+dirDest, + )) + out, err = cmd.CombinedOutput() + require.NoError(t, err, string(out)) + require.FileExists(t, filepath.Join(dirDest, "foo")) +} + func testBakeDefinitionSymlinkOutsideNoParallel(t *testing.T, sb integration.Sandbox) { for _, ent := range []bool{true, false} { t.Run(fmt.Sprintf("ent=%v", ent), func(t *testing.T) { diff --git a/util/buildflags/secrets.go b/util/buildflags/secrets.go index 994fc416a129..4ce97e6d86dc 100644 --- a/util/buildflags/secrets.go +++ b/util/buildflags/secrets.go @@ -2,6 +2,7 @@ package buildflags import ( "encoding/json" + "os" "strings" "github.com/pkg/errors" @@ -35,6 +36,20 @@ type Secret struct { Env string `json:"env,omitempty"` } +// ResolveSource returns a copy with the source selected using BuildKit's +// default secret source rules. +func (s *Secret) ResolveSource() *Secret { + resolved := *s + if resolved.Env == "" && resolved.FilePath == "" { + if _, ok := os.LookupEnv(resolved.ID); ok { + resolved.Env = resolved.ID + } else { + resolved.FilePath = resolved.ID + } + } + return &resolved +} + func (s *Secret) Equal(other *Secret) bool { return s.ID == other.ID && s.FilePath == other.FilePath && s.Env == other.Env } diff --git a/vendor/github.com/containerd/containerd/v2/core/images/handlers.go b/vendor/github.com/containerd/containerd/v2/core/images/handlers.go index 245b9ac43c58..7d1ec35cb478 100644 --- a/vendor/github.com/containerd/containerd/v2/core/images/handlers.go +++ b/vendor/github.com/containerd/containerd/v2/core/images/handlers.go @@ -23,6 +23,7 @@ import ( "slices" "sort" "strings" + "sync" "github.com/containerd/containerd/v2/core/content" "github.com/containerd/errdefs" @@ -32,6 +33,16 @@ import ( "golang.org/x/sync/semaphore" ) +const ( + // defaultMaxConcurrency bounds the number of concurrent handler + // goroutines when Dispatch is called with a nil limiter. + defaultMaxConcurrency = 32 + + // maxReferences caps the references in a single Dispatch or Walk. + // Duplicate references count toward this limit. + maxReferences = 10_000 +) + var ( // ErrSkipDesc is used to skip processing of a descriptor and // its descendants. @@ -88,9 +99,26 @@ func Handlers(handlers ...Handler) HandlerFunc { // // This differs from dispatch in that each sibling resource is considered // synchronously. +// +// Each call is limited to 10,000 references, including duplicates. +// Exceeding this limit returns an error wrapping +// [errdefs.ErrResourceExhausted]. func Walk(ctx context.Context, handler Handler, descs ...ocispec.Descriptor) error { - for _, desc := range descs { + w := &walker{} + return w.walk(ctx, handler, descs...) +} + +type walker struct { + referenceCount int +} + +func (w *walker) walk(ctx context.Context, handler Handler, descs ...ocispec.Descriptor) error { + if w.referenceCount+len(descs) > maxReferences { + return fmt.Errorf("too many descriptors (limit %d): %w", maxReferences, errdefs.ErrResourceExhausted) + } + w.referenceCount += len(descs) + for _, desc := range descs { children, err := handler.Handle(ctx, desc) if err != nil { if errors.Is(err, ErrSkipDesc) { @@ -100,7 +128,7 @@ func Walk(ctx context.Context, handler Handler, descs ...ocispec.Descriptor) err } if len(children) > 0 { - if err := Walk(ctx, handler, children...); err != nil { + if err := w.walk(ctx, handler, children...); err != nil { return err } } @@ -146,45 +174,81 @@ func WalkNotEmpty(ctx context.Context, handler Handler, descs ...ocispec.Descrip // handler may return `ErrSkipDesc` to signal to the dispatcher to not traverse // any children. // -// A concurrency limiter can be passed in to limit the number of concurrent -// handlers running. When limiter is nil, there is no limit. +// The limiter bounds concurrent handlers. When limiter is nil, +// the limit is 32. // // Typically, this function will be used with `FetchHandler`, often composed // with other handlers. // // If any handler returns an error, the dispatch session will be canceled. +// +// Each call is limited to 10,000 references, including duplicates. +// Exceeding this limit returns an error wrapping +// [errdefs.ErrResourceExhausted]. func Dispatch(ctx context.Context, handler Handler, limiter *semaphore.Weighted, descs ...ocispec.Descriptor) error { - eg, ctx2 := errgroup.WithContext(ctx) - for _, desc := range descs { - if limiter != nil { - if err := limiter.Acquire(ctx, 1); err != nil { - return err - } + if len(descs) == 0 { + return nil + } + if err := ctx.Err(); err != nil { + return err + } + + if limiter == nil { + limiter = semaphore.NewWeighted(defaultMaxConcurrency) + } + + var ( + mu sync.Mutex + next []ocispec.Descriptor + referenceCount int + ) + + admit := func(descs []ocispec.Descriptor) error { + mu.Lock() + defer mu.Unlock() + if referenceCount+len(descs) > maxReferences { + return fmt.Errorf("too many descriptors (limit %d): %w", maxReferences, errdefs.ErrResourceExhausted) } + referenceCount += len(descs) + next = append(next, descs...) + return nil + } - eg.Go(func() error { - desc := desc + if err := admit(descs); err != nil { + return err + } - children, err := handler.Handle(ctx2, desc) - if limiter != nil { - limiter.Release(1) - } - if err != nil { - if errors.Is(err, ErrSkipDesc) { - return nil // don't traverse the children. - } - return err - } + for len(next) > 0 { + level := next + next = nil - if len(children) > 0 { - return Dispatch(ctx2, handler, limiter, children...) + eg, egCtx := errgroup.WithContext(ctx) + for _, desc := range level { + // Acquire here to bound the number of handler goroutines. + if err := limiter.Acquire(egCtx, 1); err != nil { + break } - - return nil - }) + eg.Go(func() error { + defer limiter.Release(1) + children, err := handler.Handle(egCtx, desc) + if err != nil { + if errors.Is(err, ErrSkipDesc) { + return nil // don't traverse the children. + } + return err + } + return admit(children) + }) + } + if err := eg.Wait(); err != nil { + return err + } + // Acquire can fail on cancellation even if every handler returns nil. + if err := ctx.Err(); err != nil { + return err + } } - - return eg.Wait() + return nil } // ChildrenHandler decodes well-known manifest types and returns their children. diff --git a/vendor/github.com/containerd/containerd/v2/core/remotes/docker/fetcher.go b/vendor/github.com/containerd/containerd/v2/core/remotes/docker/fetcher.go index b5c29b1070c2..8b9961470076 100644 --- a/vendor/github.com/containerd/containerd/v2/core/remotes/docker/fetcher.go +++ b/vendor/github.com/containerd/containerd/v2/core/remotes/docker/fetcher.go @@ -218,6 +218,40 @@ type dockerFetcher struct { *dockerBase } +func stripSensitiveHeadersForExternalURLs(h http.Header) { + h.Del("Authorization") + h.Del("Proxy-Authorization") + h.Del("Cookie") + h.Del("Cookie2") +} + +func effectivePort(u *url.URL) string { + if port := u.Port(); port != "" { + return port + } + switch strings.ToLower(u.Scheme) { + case "http": + return "80" + case "https": + return "443" + default: + return "" + } +} + +func isRegistryOrigin(u *url.URL, hosts []RegistryHost) bool { + for _, host := range hosts { + if !strings.EqualFold(u.Scheme, host.Scheme) { + continue + } + hostURL := &url.URL{Scheme: host.Scheme, Host: host.Host} + if strings.EqualFold(u.Hostname(), hostURL.Hostname()) && effectivePort(u) == effectivePort(hostURL) { + return true + } + } + return false +} + func (r dockerFetcher) Fetch(ctx context.Context, desc ocispec.Descriptor) (io.ReadCloser, error) { ctx = log.WithLogger(ctx, log.G(ctx).WithField("digest", desc.Digest)) @@ -255,7 +289,9 @@ func (r dockerFetcher) Fetch(ctx context.Context, desc ocispec.Descriptor) (io.R Capabilities: HostCapabilityPull, } req := r.request(host, http.MethodGet) - // Strip namespace from base + if !isRegistryOrigin(u, hosts) { + stripSensitiveHeadersForExternalURLs(req.header) + } req.path = u.Path if u.RawQuery != "" { req.path = req.path + "?" + u.RawQuery diff --git a/vendor/github.com/containerd/containerd/v2/pkg/tracing/helpers.go b/vendor/github.com/containerd/containerd/v2/pkg/tracing/helpers.go index ab1278ef1fcf..4354e6c14b12 100644 --- a/vendor/github.com/containerd/containerd/v2/pkg/tracing/helpers.go +++ b/vendor/github.com/containerd/containerd/v2/pkg/tracing/helpers.go @@ -73,13 +73,15 @@ func keyValue(k string, v any) attribute.KeyValue { return attribute.String(k, typed) case []string: return attribute.StringSlice(k, typed) + case error: + return attribute.String(k, fmt.Sprint(typed)) } if stringer, ok := v.(fmt.Stringer); ok { - return attribute.String(k, stringer.String()) + return attribute.String(k, fmt.Sprint(stringer)) } if b, err := json.Marshal(v); b != nil && err == nil { return attribute.String(k, string(b)) } - return attribute.String(k, fmt.Sprintf("%v", v)) + return attribute.String(k, fmt.Sprint(v)) } diff --git a/vendor/github.com/containerd/containerd/v2/version/version.go b/vendor/github.com/containerd/containerd/v2/version/version.go index c256226281b0..e5776233e7b1 100644 --- a/vendor/github.com/containerd/containerd/v2/version/version.go +++ b/vendor/github.com/containerd/containerd/v2/version/version.go @@ -24,7 +24,7 @@ var ( Package = "github.com/containerd/containerd/v2" // Version holds the complete version number. Filled in at linking time. - Version = "2.3.4+unknown" + Version = "2.3.6+unknown" // Revision is filled with the VCS (e.g. git) revision being used to build // the program at linking time. diff --git a/vendor/github.com/moby/buildkit/client/llb/definition.go b/vendor/github.com/moby/buildkit/client/llb/definition.go index 33e996bea8ec..48ce487268d7 100644 --- a/vendor/github.com/moby/buildkit/client/llb/definition.go +++ b/vendor/github.com/moby/buildkit/client/llb/definition.go @@ -75,7 +75,13 @@ func NewDefinitionOp(def *pb.Definition) (*DefinitionOp, error) { } for dgst, locs := range def.Source.Locations { + if locs == nil { + return nil, errors.Errorf("invalid nil source locations for vertex %s", dgst) + } for _, loc := range locs.Locations { + if loc == nil { + return nil, errors.Errorf("invalid nil source location for vertex %s", dgst) + } if loc.SourceIndex < 0 || int(loc.SourceIndex) >= len(sourceMaps) { return nil, errors.Errorf("failed to find source map with index %d", loc.SourceIndex) } @@ -90,6 +96,9 @@ func NewDefinitionOp(def *pb.Definition) (*DefinitionOp, error) { var index pb.OutputIndex if dgst != "" { + if len(ops[dgst].Inputs) == 0 { + return nil, errors.New("invalid definition with no inputs on last vertex") + } index = pb.OutputIndex(ops[dgst].Inputs[0].Index) dgst = digest.Digest(ops[dgst].Inputs[0].Digest) } diff --git a/vendor/github.com/moby/buildkit/client/solve.go b/vendor/github.com/moby/buildkit/client/solve.go index 12d68b0c452c..863d87651b78 100644 --- a/vendor/github.com/moby/buildkit/client/solve.go +++ b/vendor/github.com/moby/buildkit/client/solve.go @@ -108,6 +108,8 @@ func (c *Client) solve(ctx context.Context, def *llb.Definition, runGateway runG if opt.Ref != "" { ref = opt.Ref } + + callerCtx := ctx eg, ctx := errgroup.WithContext(ctx) statusContext, cancelStatus := context.WithCancelCause(context.Background()) @@ -439,8 +441,8 @@ func (c *Client) solve(ctx context.Context, def *llb.Definition, runGateway runG } // Reset cache stores that have reset=true — delete unreferenced blobs for _, ref := range cacheOpt.storesToReset { - if err := resetCacheStore(ctx, ref.store, ref.path); err != nil { - bklog.G(ctx).WithError(err).Warn("failed to reset cache store") + if err := resetCacheStore(callerCtx, ref.store, ref.path); err != nil { + bklog.G(callerCtx).WithError(err).Warn("failed to reset cache store") } } return res, nil diff --git a/vendor/github.com/moby/buildkit/frontend/dockerui/config.go b/vendor/github.com/moby/buildkit/frontend/dockerui/config.go index fed608f4574e..09f8e51a4b4a 100644 --- a/vendor/github.com/moby/buildkit/frontend/dockerui/config.go +++ b/vendor/github.com/moby/buildkit/frontend/dockerui/config.go @@ -394,15 +394,11 @@ func (bc *Client) ReadEntrypoint(ctx context.Context, lang string, opts ...llb.L return nil, err } - dt, err := ref.ReadFile(ctx, client.ReadRequest{ - Filename: bctx.filename, - }) + dt, err := ReadFile(ctx, ref, bctx.filename) if err != nil { - if path.Base(bctx.filename) == DefaultDockerfileName { + if path.Base(bctx.filename) == DefaultDockerfileName && !isFileTooLarge(err) { var err1 error - dt, err1 = ref.ReadFile(ctx, client.ReadRequest{ - Filename: path.Join(path.Dir(bctx.filename), strings.ToLower(DefaultDockerfileName)), - }) + dt, err1 = ReadFile(ctx, ref, path.Join(path.Dir(bctx.filename), strings.ToLower(DefaultDockerfileName))) if err1 == nil { err = nil } @@ -414,12 +410,14 @@ func (bc *Client) ReadEntrypoint(ctx context.Context, lang string, opts ...llb.L smap := llb.NewSourceMap(src, bctx.filename, lang, dt) smap.Definition = def - dt, err = ref.ReadFile(ctx, client.ReadRequest{ - Filename: bctx.filename + ".dockerignore", - }) + // a missing ignore file is not an error, but an oversized one must not + // be silently skipped + dt, err = ReadFile(ctx, ref, bctx.filename+".dockerignore") if err == nil { bc.dockerignore = dt bc.dockerignoreName = bctx.filename + ".dockerignore" + } else if isFileTooLarge(err) { + return nil, err } return &Source{ @@ -563,9 +561,12 @@ func (bc *Client) dockerIgnorePatterns(ctx context.Context, bctx *buildContext) if err != nil { return nil, err } - dt, _ := ref.ReadFile(ctx, client.ReadRequest{ // ignore error - Filename: DefaultDockerignoreName, - }) + // a missing ignore file is not an error, but an oversized one must + // not be silently skipped + dt, err := ReadFile(ctx, ref, DefaultDockerignoreName) + if isFileTooLarge(err) { + return nil, err + } if dt == nil { dt = []byte{} } diff --git a/vendor/github.com/moby/buildkit/frontend/dockerui/context.go b/vendor/github.com/moby/buildkit/frontend/dockerui/context.go index 4848c04d41eb..9c556463072f 100644 --- a/vendor/github.com/moby/buildkit/frontend/dockerui/context.go +++ b/vendor/github.com/moby/buildkit/frontend/dockerui/context.go @@ -220,9 +220,7 @@ func archiveMaxTimeFromHTTPArchive(ctx context.Context, bctx *buildContext) (*ti if bctx.contextRef == nil || bctx.httpContextFilename == "" { return nil, nil } - dt, err := bctx.contextRef.ReadFile(ctx, client.ReadRequest{ - Filename: bctx.httpContextFilename, - }) + dt, err := ReadFile(ctx, bctx.contextRef, bctx.httpContextFilename) if err != nil { return nil, err } diff --git a/vendor/github.com/moby/buildkit/frontend/dockerui/namedcontext.go b/vendor/github.com/moby/buildkit/frontend/dockerui/namedcontext.go index 1d27ddcf488e..b125761880a4 100644 --- a/vendor/github.com/moby/buildkit/frontend/dockerui/namedcontext.go +++ b/vendor/github.com/moby/buildkit/frontend/dockerui/namedcontext.go @@ -252,10 +252,12 @@ func (nc *NamedContext) load(ctx context.Context, count int) (*llb.State, *docke } var excludes []string if !opt.NoDockerignore { - dt, _ := ref.ReadFile(ctx, client.ReadRequest{ - Filename: DefaultDockerignoreName, - }) // error ignored - + // a missing ignore file is not an error, but an oversized one + // must not be silently skipped + dt, err := ReadFile(ctx, ref, DefaultDockerignoreName) + if isFileTooLarge(err) { + return nil, nil, err + } if len(dt) != 0 { excludes, err = ignorefile.ReadAll(bytes.NewBuffer(dt)) if err != nil { diff --git a/vendor/github.com/moby/buildkit/frontend/dockerui/readfile.go b/vendor/github.com/moby/buildkit/frontend/dockerui/readfile.go new file mode 100644 index 000000000000..a22e8da73cfb --- /dev/null +++ b/vendor/github.com/moby/buildkit/frontend/dockerui/readfile.go @@ -0,0 +1,52 @@ +package dockerui + +import ( + "context" + "fmt" + + "github.com/containerd/containerd/v2/defaults" + "github.com/moby/buildkit/frontend/gateway/client" + "github.com/pkg/errors" +) + +// maxFileSize bounds files that the builtin frontend loads into daemon memory. +// A frontend running over the gateway cannot read more than one gRPC message in +// a single request anyway, so the builtin frontend is held to the same size. +const maxFileSize = defaults.DefaultMaxRecvMsgSize + +type fileTooLargeError struct { + filename string +} + +func (e *fileTooLargeError) Error() string { + return fmt.Sprintf("%s exceeds maximum allowed size of %d bytes", e.filename, maxFileSize) +} + +func isFileTooLarge(err error) bool { + var e *fileTooLargeError + return errors.As(err, &e) +} + +// ReadFile reads filename from ref, refusing files larger than maxFileSize. +// Oversized files are rejected on their reported size so that nothing is read +// from them, and the read itself carries a range so that no more than +// maxFileSize+1 bytes are ever loaded even when the size could not be checked. +func ReadFile(ctx context.Context, ref client.Reference, filename string) ([]byte, error) { + // stat failures are left to the read, which reports them properly + if st, err := ref.StatFile(ctx, client.StatRequest{Path: filename}); err == nil && st.Size > maxFileSize { + return nil, errors.WithStack(&fileTooLargeError{filename: filename}) + } + dt, err := ref.ReadFile(ctx, client.ReadRequest{ + Filename: filename, + Range: &client.FileRange{ + Length: maxFileSize + 1, + }, + }) + if err != nil { + return nil, err + } + if len(dt) > maxFileSize { + return nil, errors.WithStack(&fileTooLargeError{filename: filename}) + } + return dt, nil +} diff --git a/vendor/golang.org/x/crypto/ssh/certs.go b/vendor/golang.org/x/crypto/ssh/certs.go index fa848f51a5f9..a3b802e4b8bf 100644 --- a/vendor/golang.org/x/crypto/ssh/certs.go +++ b/vendor/golang.org/x/crypto/ssh/certs.go @@ -10,6 +10,7 @@ import ( "fmt" "io" "net" + "slices" "sort" "time" ) @@ -305,8 +306,11 @@ const sourceAddressCriticalOption = "source-address" // minimally, the IsAuthority callback should be set. type CertChecker struct { // SupportedCriticalOptions lists the CriticalOptions that the - // server application layer understands. These are only used - // for user certificates. + // application layer understands. A certificate carrying a critical + // option that is not listed here is rejected. + // CertChecker.Authenticate additionally accepts the source-address + // option, which the server enforces on the Permissions that + // Authenticate returns. SupportedCriticalOptions []string // IsUserAuthority should return true if the key is recognized as an @@ -369,8 +373,9 @@ func (c *CertChecker) CheckHostKey(addr string, remote net.Addr, key PublicKey) return c.CheckCert(hostname, cert) } -// Authenticate checks a user certificate. Authenticate can be used as -// a value for ServerConfig.PublicKeyCallback. +// Authenticate checks a user certificate. Authenticate can be used as a value +// for ServerConfig.PublicKeyCallback. The source-address critical option is +// allowed, as it will be enforced by the server. func (c *CertChecker) Authenticate(conn ConnMetadata, pubKey PublicKey) (*Permissions, error) { cert, ok := pubKey.(*Certificate) if !ok { @@ -389,8 +394,11 @@ func (c *CertChecker) Authenticate(conn ConnMetadata, pubKey PublicKey) (*Permis if !c.IsUserAuthority(cert.SignatureKey) { return nil, fmt.Errorf("ssh: certificate signed by unrecognized authority") } - - if err := c.CheckCert(conn.User(), cert); err != nil { + // The source-address critical option is enforced by serverAuthenticate, + // so it is supported regardless of SupportedCriticalOptions + cc := *c + cc.SupportedCriticalOptions = append(slices.Clip(cc.SupportedCriticalOptions), sourceAddressCriticalOption) + if err := cc.CheckCert(conn.User(), cert); err != nil { return nil, err } @@ -398,27 +406,15 @@ func (c *CertChecker) Authenticate(conn ConnMetadata, pubKey PublicKey) (*Permis } // CheckCert checks CriticalOptions, ValidPrincipals, revocation, timestamp and -// the signature of the certificate. +// the signature of the certificate. Critical options that are not listed in +// SupportedCriticalOptions are rejected. func (c *CertChecker) CheckCert(principal string, cert *Certificate) error { if c.IsRevoked != nil && c.IsRevoked(cert) { return fmt.Errorf("ssh: certificate serial %d revoked", cert.Serial) } for opt := range cert.CriticalOptions { - // sourceAddressCriticalOption will be enforced by - // serverAuthenticate - if opt == sourceAddressCriticalOption { - continue - } - - found := false - for _, supp := range c.SupportedCriticalOptions { - if supp == opt { - found = true - break - } - } - if !found { + if !slices.Contains(c.SupportedCriticalOptions, opt) { return fmt.Errorf("ssh: unsupported critical option %q in certificate", opt) } } diff --git a/vendor/golang.org/x/crypto/ssh/channel.go b/vendor/golang.org/x/crypto/ssh/channel.go index ba3279e91d68..d6010fd77b99 100644 --- a/vendor/golang.org/x/crypto/ssh/channel.go +++ b/vendor/golang.org/x/crypto/ssh/channel.go @@ -173,6 +173,12 @@ type channel struct { // (for outbound channels) or received (for inbound channels). decided bool + // established is set to true once the channel is open and may carry normal + // channel traffic: for an outbound channel when the peer's open + // confirmation is received, for an inbound channel when the local side + // accepts it. It is set and read from different goroutines. + established atomic.Bool + // direction contains either channelOutbound, for channels created // locally, or channelInbound, for channels created by the peer. direction channelDirection @@ -434,10 +440,20 @@ func (ch *channel) responseMessageReceived() error { return errors.New("ssh: duplicate response received for channel") } ch.decided = true + ch.established.Store(true) return nil } func (ch *channel) handlePacket(packet []byte) error { + // Only the open response is expected before the channel is established. + if !ch.established.Load() { + switch packet[0] { + case msgChannelOpenConfirm, msgChannelOpenFailure: + default: + return nil + } + } + switch packet[0] { case msgChannelData, msgChannelExtendedData: return ch.handleData(packet) @@ -503,7 +519,8 @@ func (ch *channel) handlePacket(packet []byte) error { default: } default: - ch.msg <- msg + // No other message type is expected on an established channel. + return fmt.Errorf("ssh: unexpected message type %d on channel %d", packet[0], ch.localId) } return nil } @@ -554,6 +571,7 @@ func (ch *channel) Accept() (Channel, <-chan *Request, error) { MaxPacketSize: ch.maxIncomingPayload, } ch.decided = true + ch.established.Store(true) if err := ch.sendMessage(confirm); err != nil { return nil, nil, err } diff --git a/vendor/golang.org/x/crypto/ssh/transport.go b/vendor/golang.org/x/crypto/ssh/transport.go index fa3dd6a4299b..540865dfc823 100644 --- a/vendor/golang.org/x/crypto/ssh/transport.go +++ b/vendor/golang.org/x/crypto/ssh/transport.go @@ -331,13 +331,19 @@ func exchangeVersions(rw io.ReadWriter, versionLine []byte) (them []byte, err er // chars const maxVersionStringBytes = 255 +// maxPreVersionLines is the maximum number of lines sent by the peer +// before the version string. Each of these lines is limited to a maximum +// of maxVersionStringBytes chars. Lines sent before the version string +// are silently ignored. +const maxPreVersionLines = 1024 + // Read version string as specified by RFC 4253, section 4.2. func readVersion(r io.Reader) ([]byte, error) { versionString := make([]byte, 0, 64) var ok bool var buf [1]byte - for length := 0; length < maxVersionStringBytes; length++ { + for lines := 0; len(versionString) < maxVersionStringBytes && lines < maxPreVersionLines; { _, err := io.ReadFull(r, buf[:]) if err != nil { return nil, err @@ -347,9 +353,9 @@ func readVersion(r io.Reader) ([]byte, error) { if buf[0] == '\n' { if !bytes.HasPrefix(versionString, []byte("SSH-")) { // RFC 4253 says we need to ignore all version string lines - // except the one containing the SSH version (provided that - // all the lines do not exceed 255 bytes in total). + // except the one containing the SSH version. versionString = versionString[:0] + lines++ continue } ok = true diff --git a/vendor/modules.txt b/vendor/modules.txt index b17bfbeef39b..73fbcb63c52a 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -212,8 +212,8 @@ github.com/containerd/console # github.com/containerd/containerd/api v1.11.1 ## explicit; go 1.24.0 github.com/containerd/containerd/api/services/content/v1 -# github.com/containerd/containerd/v2 v2.3.4 -## explicit; go 1.26.3 +# github.com/containerd/containerd/v2 v2.3.6 +## explicit; go 1.26.8 github.com/containerd/containerd/v2/core/content github.com/containerd/containerd/v2/core/content/proxy github.com/containerd/containerd/v2/core/images @@ -644,8 +644,8 @@ github.com/mitchellh/go-wordwrap # github.com/mitchellh/hashstructure/v2 v2.0.2 ## explicit; go 1.14 github.com/mitchellh/hashstructure/v2 -# github.com/moby/buildkit v0.33.0 -## explicit; go 1.26.3 +# github.com/moby/buildkit v0.33.1 +## explicit; go 1.26.8 github.com/moby/buildkit/api/services/control github.com/moby/buildkit/api/types github.com/moby/buildkit/cache/remotecache/gha/ghatypes @@ -1273,8 +1273,8 @@ go.yaml.in/yaml/v3 ## explicit; go 1.18 go.yaml.in/yaml/v4 go.yaml.in/yaml/v4/internal/libyaml -# golang.org/x/crypto v0.55.0 -## explicit; go 1.25.0 +# golang.org/x/crypto v0.56.0 +## explicit; go 1.26.0 golang.org/x/crypto/argon2 golang.org/x/crypto/bcrypt golang.org/x/crypto/blake2b