diff --git a/go.mod b/go.mod index ef330538..1a5db563 100644 --- a/go.mod +++ b/go.mod @@ -17,7 +17,7 @@ require ( github.com/sirupsen/logrus v1.9.4 github.com/spf13/cobra v1.10.2 github.com/stretchr/testify v1.11.1 - oras.land/oras-go/v2 v2.6.0 + oras.land/oras-go/v2 v2.6.1 ) require ( @@ -35,7 +35,7 @@ require ( github.com/spf13/pflag v1.0.9 // indirect github.com/stretchr/objx v0.5.2 // indirect golang.org/x/crypto v0.45.0 // indirect - golang.org/x/sync v0.14.0 // indirect + golang.org/x/sync v0.20.0 // indirect golang.org/x/sys v0.38.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 01eaee0e..be31add5 100644 --- a/go.sum +++ b/go.sum @@ -109,8 +109,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= -golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -158,5 +158,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.2.0 h1:I0DwBVMGAx26dttAj1BtJLAkVGncrkkUXfJLC4Flt/I= gotest.tools/v3 v3.2.0/go.mod h1:Mcr9QNxkg0uMvy/YElmo4SpXgJKWgQvYrT7Kw5RzJ1A= -oras.land/oras-go/v2 v2.6.0 h1:X4ELRsiGkrbeox69+9tzTu492FMUu7zJQW6eJU+I2oc= -oras.land/oras-go/v2 v2.6.0/go.mod h1:magiQDfG6H1O9APp+rOsvCPcW1GD2MM7vgnKY0Y+u1o= +oras.land/oras-go/v2 v2.6.1 h1:bonOEkjLfp8tt6qXWRRWP6p1F+9octchOf2EqnWB4Zs= +oras.land/oras-go/v2 v2.6.1/go.mod h1:dhtFrFOuZuDtAVeZ9FUnaa5zfzplG3ZnFX9/uH1J/Yk= diff --git a/vendor/golang.org/x/sync/errgroup/errgroup.go b/vendor/golang.org/x/sync/errgroup/errgroup.go index cfafed5b..f69fd754 100644 --- a/vendor/golang.org/x/sync/errgroup/errgroup.go +++ b/vendor/golang.org/x/sync/errgroup/errgroup.go @@ -3,7 +3,7 @@ // license that can be found in the LICENSE file. // Package errgroup provides synchronization, error propagation, and Context -// cancelation for groups of goroutines working on subtasks of a common task. +// cancellation for groups of goroutines working on subtasks of a common task. // // [errgroup.Group] is related to [sync.WaitGroup] but adds handling of tasks // returning errors. @@ -12,8 +12,6 @@ package errgroup import ( "context" "fmt" - "runtime" - "runtime/debug" "sync" ) @@ -33,10 +31,6 @@ type Group struct { errOnce sync.Once err error - - mu sync.Mutex - panicValue any // = PanicError | PanicValue; non-nil if some Group.Go goroutine panicked. - abnormal bool // some Group.Go goroutine terminated abnormally (panic or goexit). } func (g *Group) done() { @@ -56,80 +50,47 @@ func WithContext(ctx context.Context) (*Group, context.Context) { return &Group{cancel: cancel}, ctx } -// Wait blocks until all function calls from the Go method have returned -// normally, then returns the first non-nil error (if any) from them. -// -// If any of the calls panics, Wait panics with a [PanicValue]; -// and if any of them calls [runtime.Goexit], Wait calls runtime.Goexit. +// Wait blocks until all function calls from the Go method have returned, then +// returns the first non-nil error (if any) from them. func (g *Group) Wait() error { g.wg.Wait() if g.cancel != nil { g.cancel(g.err) } - if g.panicValue != nil { - panic(g.panicValue) - } - if g.abnormal { - runtime.Goexit() - } return g.err } // Go calls the given function in a new goroutine. -// The first call to Go must happen before a Wait. -// It blocks until the new goroutine can be added without the number of -// active goroutines in the group exceeding the configured limit. // +// The first call to Go must happen before a Wait. // It blocks until the new goroutine can be added without the number of // goroutines in the group exceeding the configured limit. // -// The first goroutine in the group that returns a non-nil error, panics, or -// invokes [runtime.Goexit] will cancel the associated Context, if any. +// The first goroutine in the group that returns a non-nil error will +// cancel the associated Context, if any. The error will be returned +// by Wait. func (g *Group) Go(f func() error) { if g.sem != nil { g.sem <- token{} } - g.add(f) -} - -func (g *Group) add(f func() error) { g.wg.Add(1) go func() { defer g.done() - normalReturn := false - defer func() { - if normalReturn { - return - } - v := recover() - g.mu.Lock() - defer g.mu.Unlock() - if !g.abnormal { - if g.cancel != nil { - g.cancel(g.err) - } - g.abnormal = true - } - if v != nil && g.panicValue == nil { - switch v := v.(type) { - case error: - g.panicValue = PanicError{ - Recovered: v, - Stack: debug.Stack(), - } - default: - g.panicValue = PanicValue{ - Recovered: v, - Stack: debug.Stack(), - } - } - } - }() - err := f() - normalReturn = true - if err != nil { + // It is tempting to propagate panics from f() + // up to the goroutine that calls Wait, but + // it creates more problems than it solves: + // - it delays panics arbitrarily, + // making bugs harder to detect; + // - it turns f's panic stack into a mere value, + // hiding it from crash-monitoring tools; + // - it risks deadlocks that hide the panic entirely, + // if f's panic leaves the program in a state + // that prevents the Wait call from being reached. + // See #53757, #74275, #74304, #74306. + + if err := f(); err != nil { g.errOnce.Do(func() { g.err = err if g.cancel != nil { @@ -154,7 +115,19 @@ func (g *Group) TryGo(f func() error) bool { } } - g.add(f) + g.wg.Add(1) + go func() { + defer g.done() + + if err := f(); err != nil { + g.errOnce.Do(func() { + g.err = err + if g.cancel != nil { + g.cancel(g.err) + } + }) + } + }() return true } @@ -171,38 +144,8 @@ func (g *Group) SetLimit(n int) { g.sem = nil return } - if len(g.sem) != 0 { - panic(fmt.Errorf("errgroup: modify limit while %v goroutines in the group are still active", len(g.sem))) + if active := len(g.sem); active != 0 { + panic(fmt.Errorf("errgroup: modify limit while %v goroutines in the group are still active", active)) } g.sem = make(chan token, n) } - -// PanicError wraps an error recovered from an unhandled panic -// when calling a function passed to Go or TryGo. -type PanicError struct { - Recovered error - Stack []byte // result of call to [debug.Stack] -} - -func (p PanicError) Error() string { - // A Go Error method conventionally does not include a stack dump, so omit it - // here. (Callers who care can extract it from the Stack field.) - return fmt.Sprintf("recovered from errgroup.Group: %v", p.Recovered) -} - -func (p PanicError) Unwrap() error { return p.Recovered } - -// PanicValue wraps a value that does not implement the error interface, -// recovered from an unhandled panic when calling a function passed to Go or -// TryGo. -type PanicValue struct { - Recovered any - Stack []byte // result of call to [debug.Stack] -} - -func (p PanicValue) String() string { - if len(p.Stack) > 0 { - return fmt.Sprintf("recovered from errgroup.Group: %v\n%s", p.Recovered, p.Stack) - } - return fmt.Sprintf("recovered from errgroup.Group: %v", p.Recovered) -} diff --git a/vendor/modules.txt b/vendor/modules.txt index 229a8fb6..bd9db885 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -95,8 +95,8 @@ github.com/stretchr/testify/mock ## explicit; go 1.24.0 golang.org/x/crypto/pkcs12 golang.org/x/crypto/pkcs12/internal/rc2 -# golang.org/x/sync v0.14.0 -## explicit; go 1.23.0 +# golang.org/x/sync v0.20.0 +## explicit; go 1.25.0 golang.org/x/sync/errgroup golang.org/x/sync/semaphore # golang.org/x/sys v0.38.0 @@ -111,8 +111,8 @@ golang.org/x/sys/windows gopkg.in/yaml.v3 # gotest.tools/v3 v3.2.0 ## explicit; go 1.13 -# oras.land/oras-go/v2 v2.6.0 -## explicit; go 1.23.0 +# oras.land/oras-go/v2 v2.6.1 +## explicit; go 1.25.0 oras.land/oras-go/v2 oras.land/oras-go/v2/content oras.land/oras-go/v2/content/file diff --git a/vendor/oras.land/oras-go/v2/.goreleaser.yaml b/vendor/oras.land/oras-go/v2/.goreleaser.yaml new file mode 100644 index 00000000..b4ad1873 --- /dev/null +++ b/vendor/oras.land/oras-go/v2/.goreleaser.yaml @@ -0,0 +1,26 @@ +# Copyright The ORAS 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. + +version: 2 + +# oras-go is a library — no binary builds or archives needed. +builds: + - skip: true + +checksum: + disable: true + +release: + # Tags containing -alpha, -beta, or -rc are automatically marked pre-release. + prerelease: auto + draft: false diff --git a/vendor/oras.land/oras-go/v2/CODEOWNERS b/vendor/oras.land/oras-go/v2/CODEOWNERS index 45a68a31..f3858e76 100644 --- a/vendor/oras.land/oras-go/v2/CODEOWNERS +++ b/vendor/oras.land/oras-go/v2/CODEOWNERS @@ -1,2 +1,2 @@ # Derived from OWNERS.md -* @sajayantony @shizhMSFT @stevelasker @Wwwsylvia +* @sabre1041 @shizhMSFT @TerryHowe @Wwwsylvia diff --git a/vendor/oras.land/oras-go/v2/Makefile b/vendor/oras.land/oras-go/v2/Makefile index bc671e44..d0af0a73 100644 --- a/vendor/oras.land/oras-go/v2/Makefile +++ b/vendor/oras.land/oras-go/v2/Makefile @@ -26,12 +26,10 @@ clean: .PHONY: check-encoding check-encoding: ! find . -not -path "./vendor/*" -name "*.go" -type f -exec file "{}" ";" | grep CRLF - ! find scripts -name "*.sh" -type f -exec file "{}" ";" | grep CRLF .PHONY: fix-encoding fix-encoding: find . -not -path "./vendor/*" -name "*.go" -type f -exec sed -i -e "s/\r//g" {} + - find scripts -name "*.sh" -type f -exec sed -i -e "s/\r//g" {} + .PHONY: vendor vendor: diff --git a/vendor/oras.land/oras-go/v2/OWNERS.md b/vendor/oras.land/oras-go/v2/OWNERS.md index 402c4a97..4d29e5eb 100644 --- a/vendor/oras.land/oras-go/v2/OWNERS.md +++ b/vendor/oras.land/oras-go/v2/OWNERS.md @@ -1,11 +1,13 @@ # Owners Owners: - - Sajay Antony (@sajayantony) + - Andrew Block (@sabre1041) - Shiwei Zhang (@shizhMSFT) - - Steve Lasker (@stevelasker) - Sylvia Lei (@Wwwsylvia) + - Terry Howe (@TerryHowe) Emeritus: - Avi Deitcher (@deitch) - Josh Dolitsky (@jdolitsky) + - Sajay Antony (@sajayantony) + - Steve Lasker (@stevelasker) diff --git a/vendor/oras.land/oras-go/v2/README.md b/vendor/oras.land/oras-go/v2/README.md index 9224754f..31bdb47c 100644 --- a/vendor/oras.land/oras-go/v2/README.md +++ b/vendor/oras.land/oras-go/v2/README.md @@ -12,7 +12,7 @@ `oras-go` is a Go library for managing OCI artifacts, compliant with the [OCI Image Format Specification](https://github.com/opencontainers/image-spec) and the [OCI Distribution Specification](https://github.com/opencontainers/distribution-spec). It provides unified APIs for pushing, pulling, and managing artifacts across OCI-compliant registries, local file systems, and in-memory stores. > [!Note] -> The `main` branch follows [Go's Security Policy](https://github.com/golang/go/security/policy) and supports the two latest versions of Go (currently `1.23` and `1.24`). +> The `main` branch follows [Go's Security Policy](https://github.com/golang/go/security/policy) and supports the two latest versions of Go (currently `1.24` and `1.25`). ## Getting Started diff --git a/vendor/oras.land/oras-go/v2/RELEASES.md b/vendor/oras.land/oras-go/v2/RELEASES.md new file mode 100644 index 00000000..c802c22d --- /dev/null +++ b/vendor/oras.land/oras-go/v2/RELEASES.md @@ -0,0 +1,108 @@ +# Releasing oras-go + +Releases are created via a GitOps workflow. Merging a `release/vX.Y.Z` branch +into `v2` automatically tags the commit and publishes the GitHub Release. + +## Steps + +### 1. Create a release branch + +The release branch needs at least one commit so GitHub will allow a PR to be +opened. Use an empty commit as a lightweight marker: + +```bash +git fetch upstream +git checkout -b release/v2.7.0 upstream/v2 +git commit --allow-empty -s -m "chore: prepare release v2.7.0" +git push origin release/v2.7.0 +``` + +The release does not need to contain the changes being released — those are +already on `v2`. The PR is a trigger: when it merges, the workflow tags the +PR's `merge_commit_sha` (the exact commit that landed on `v2`), which includes +all prior work on the branch. + +### 2. Open a pull request + +Open a PR from `release/v2.7.0` targeting the `v2` branch. Write the release +notes directly in the PR description using the format from prior releases: + +```markdown +## New Features +... + +## Bug Fixes +... + +## Documentation +... + +## Other Changes +... +``` + +The PR description becomes the GitHub Release body verbatim, so write it in +its final form. + +### 3. Get approvals + +Branch protection on `v2` requires approval from at least 3 of the 4 owners +listed in [OWNERS.md](OWNERS.md). Reviewers should verify: + +- The target commit is correct +- The release notes are accurate and complete +- All CI checks pass + +### 4. Merge + +Merge the PR. The [release workflow](.github/workflows/release.yml) +automatically: + +1. Extracts the version from the branch name (`release/v2.7.0` → `v2.7.0`) +2. Creates and pushes the git tag +3. Publishes the GitHub Release with the PR body as release notes + +## Pre-releases + +Tags containing `-alpha`, `-beta`, or `-rc` (e.g., `v2.7.0-rc.1`) are +automatically marked as pre-release on GitHub. Use the same branch naming +convention: `release/v2.7.0-rc.1`. + +## Testing the workflow locally + +Three levels of local validation are available without triggering a real release: + +**1. Validate the goreleaser config:** +```bash +goreleaser check +``` + +**2. Validate workflow structure and job matching (dry run):** +```bash +act pull_request \ + -e .github/act/release-event.json \ + -W .github/workflows/release.yml \ + -n +``` + +**3. Run the workflow end-to-end with a fake token (Colima + cached actions required):** +```bash +act pull_request \ + -e .github/act/release-event.json \ + -W .github/workflows/release.yml \ + -s GITHUB_TOKEN=fake \ + --pull=false \ + --action-offline-mode \ + --container-daemon-socket - +``` + +This runs all steps up to and including version extraction (`version=vX.Y.Z` will +appear in the output). The `git push` step then fails with a permission error — +that is expected and confirms no tag was pushed. The mock event payload is at +`.github/act/release-event.json`. + +## Updating the documentation site + +After a release, update [oras-www](https://github.com/oras-project/oras-www) +to reflect the new version. See the `CLAUDE.md` in that repository for the +exact steps. diff --git a/vendor/oras.land/oras-go/v2/content/file/file.go b/vendor/oras.land/oras-go/v2/content/file/file.go index 5caaedd0..6cdba2e4 100644 --- a/vendor/oras.land/oras-go/v2/content/file/file.go +++ b/vendor/oras.land/oras-go/v2/content/file/file.go @@ -39,7 +39,7 @@ import ( // bufPool is a pool of byte buffers that can be reused for copying content // between files. var bufPool = sync.Pool{ - New: func() interface{} { + New: func() any { // the buffer size should be larger than or equal to 128 KiB // for performance considerations. // we choose 1 MiB here so there will be less disk I/O. @@ -174,7 +174,7 @@ func (s *Store) Close() error { s.setClosed() var errs []string - s.tmpFiles.Range(func(name, _ interface{}) bool { + s.tmpFiles.Range(func(name, _ any) bool { if err := os.Remove(name.(string)); err != nil { errs = append(errs, err.Error()) } @@ -625,6 +625,13 @@ func (s *Store) resolveWritePath(name string) (string, error) { if strings.HasPrefix(rel, "../") || rel == ".." { return "", ErrPathTraversalDisallowed } + // The lexical check above prevents "../" escapes but does not resolve + // symlinks. A symlink component under workingDir (e.g. "out" -> "/outside") + // passes the lexical check yet directs writes outside workingDir. + // Re-check after resolving symlinks in the parent path to close that gap. + if err := checkSymlinkEscape(base, target); err != nil { + return "", err + } } if s.DisableOverwrite { if _, err := os.Stat(path); err == nil { @@ -686,3 +693,52 @@ func (s *Store) setClosed() { func ensureDir(path string) error { return os.MkdirAll(path, 0777) } + +// checkSymlinkEscape returns ErrPathTraversalDisallowed if resolving symlinks +// in target's ancestor directories causes it to escape base. target may not +// yet exist, so symlinks are resolved on its deepest existing ancestor. +func checkSymlinkEscape(base, target string) error { + realBase, err := filepath.EvalSymlinks(base) + if err != nil { + if os.IsNotExist(err) { + return nil // base doesn't exist yet; no symlinks to follow + } + return err + } + realTarget, err := realPathForWrite(target) + if err != nil { + return err + } + rel, err := filepath.Rel(realBase, realTarget) + if err != nil { + return ErrPathTraversalDisallowed + } + rel = filepath.ToSlash(rel) + if strings.HasPrefix(rel, "../") || rel == ".." { + return ErrPathTraversalDisallowed + } + return nil +} + +// realPathForWrite resolves symlinks in the deepest existing ancestor of path +// and returns the resulting absolute path. Non-existent path components are +// appended verbatim, matching the semantics of a file about to be created. +func realPathForWrite(path string) (string, error) { + dir := filepath.Dir(path) + suffix := filepath.Base(path) + for { + real, err := filepath.EvalSymlinks(dir) + if err == nil { + return filepath.Join(real, suffix), nil + } + if !os.IsNotExist(err) { + return "", err + } + parent := filepath.Dir(dir) + if parent == dir { + return path, nil // reached filesystem root + } + suffix = filepath.Join(filepath.Base(dir), suffix) + dir = parent + } +} diff --git a/vendor/oras.land/oras-go/v2/content/reader.go b/vendor/oras.land/oras-go/v2/content/reader.go index 37bab5e1..def9ebde 100644 --- a/vendor/oras.land/oras-go/v2/content/reader.go +++ b/vendor/oras.land/oras-go/v2/content/reader.go @@ -24,6 +24,12 @@ import ( ocispec "github.com/opencontainers/image-spec/specs-go/v1" ) +// maxDescriptorSize is the upper-bound for descriptor sizes accepted by +// ReadAll. Descriptors sourced from attacker-supplied OCI layouts can carry +// arbitrarily large Size values; without this cap, make([]byte, desc.Size) +// triggers a runtime panic before any allocation occurs. +const maxDescriptorSize = 32 * 1024 * 1024 // 32 MiB + var ( // ErrInvalidDescriptorSize is returned by ReadAll() when // the descriptor has an invalid size. @@ -119,7 +125,7 @@ func NewVerifyReader(r io.Reader, desc ocispec.Descriptor) *VerifyReader { // The read content is verified against the size and the digest // using a VerifyReader. func ReadAll(r io.Reader, desc ocispec.Descriptor) ([]byte, error) { - if desc.Size < 0 { + if desc.Size < 0 || desc.Size > maxDescriptorSize { return nil, ErrInvalidDescriptorSize } buf := make([]byte, desc.Size) diff --git a/vendor/oras.land/oras-go/v2/internal/cas/memory.go b/vendor/oras.land/oras-go/v2/internal/cas/memory.go index 7e358e13..2d97e2a6 100644 --- a/vendor/oras.land/oras-go/v2/internal/cas/memory.go +++ b/vendor/oras.land/oras-go/v2/internal/cas/memory.go @@ -80,7 +80,7 @@ func (m *Memory) Exists(_ context.Context, target ocispec.Descriptor) (bool, err // necessarily correspond to any consistent snapshot of the storage contents. func (m *Memory) Map() map[descriptor.Descriptor][]byte { res := make(map[descriptor.Descriptor][]byte) - m.content.Range(func(key, value interface{}) bool { + m.content.Range(func(key, value any) bool { res[key.(descriptor.Descriptor)] = value.([]byte) return true }) diff --git a/vendor/oras.land/oras-go/v2/internal/graph/memory.go b/vendor/oras.land/oras-go/v2/internal/graph/memory.go index 016e5f96..ffd06545 100644 --- a/vendor/oras.land/oras-go/v2/internal/graph/memory.go +++ b/vendor/oras.land/oras-go/v2/internal/graph/memory.go @@ -25,7 +25,6 @@ import ( "oras.land/oras-go/v2/content" "oras.land/oras-go/v2/errdef" "oras.land/oras-go/v2/internal/container/set" - "oras.land/oras-go/v2/internal/descriptor" "oras.land/oras-go/v2/internal/status" "oras.land/oras-go/v2/internal/syncutil" ) @@ -34,9 +33,9 @@ import ( type Memory struct { // nodes has the following properties and behaviors: // 1. a node exists in Memory.nodes if and only if it exists in the memory - // 2. Memory.nodes saves the ocispec.Descriptor map keys, which are used by + // 2. Memory.nodes saves the ocispec.Descriptor indexed by digest, which are used by // the other fields. - nodes map[descriptor.Descriptor]ocispec.Descriptor + nodes map[digest.Digest]ocispec.Descriptor // predecessors has the following properties and behaviors: // 1. a node exists in Memory.predecessors if it has at least one predecessor @@ -44,14 +43,14 @@ type Memory struct { // the memory. // 2. a node does not exist in Memory.predecessors, if it doesn't have any predecessors // in the memory. - predecessors map[descriptor.Descriptor]set.Set[descriptor.Descriptor] + predecessors map[digest.Digest]set.Set[digest.Digest] // successors has the following properties and behaviors: // 1. a node exists in Memory.successors if and only if it exists in the memory. // 2. a node's entry in Memory.successors is always consistent with the actual // content of the node, regardless of whether or not each successor exists // in the memory. - successors map[descriptor.Descriptor]set.Set[descriptor.Descriptor] + successors map[digest.Digest]set.Set[digest.Digest] lock sync.RWMutex } @@ -59,9 +58,9 @@ type Memory struct { // NewMemory creates a new memory PredecessorFinder. func NewMemory() *Memory { return &Memory{ - nodes: make(map[descriptor.Descriptor]ocispec.Descriptor), - predecessors: make(map[descriptor.Descriptor]set.Set[descriptor.Descriptor]), - successors: make(map[descriptor.Descriptor]set.Set[descriptor.Descriptor]), + nodes: make(map[digest.Digest]ocispec.Descriptor), + predecessors: make(map[digest.Digest]set.Set[digest.Digest]), + successors: make(map[digest.Digest]set.Set[digest.Digest]), } } @@ -108,14 +107,13 @@ func (m *Memory) Predecessors(_ context.Context, node ocispec.Descriptor) ([]oci m.lock.RLock() defer m.lock.RUnlock() - key := descriptor.FromOCI(node) - set, exists := m.predecessors[key] + set, exists := m.predecessors[node.Digest] if !exists { return nil, nil } var res []ocispec.Descriptor - for k := range set { - res = append(res, m.nodes[k]) + for digest := range set { + res = append(res, m.nodes[digest]) } return res, nil } @@ -126,25 +124,24 @@ func (m *Memory) Remove(node ocispec.Descriptor) []ocispec.Descriptor { m.lock.Lock() defer m.lock.Unlock() - nodeKey := descriptor.FromOCI(node) var danglings []ocispec.Descriptor // remove the node from its successors' predecessor list - for successorKey := range m.successors[nodeKey] { - predecessorEntry := m.predecessors[successorKey] - predecessorEntry.Delete(nodeKey) + for successorDigest := range m.successors[node.Digest] { + predecessorEntry := m.predecessors[successorDigest] + predecessorEntry.Delete(node.Digest) // if none of the predecessors of the node still exists, we remove the // predecessors entry and return it as a dangling node. Otherwise, we do // not remove the entry. if len(predecessorEntry) == 0 { - delete(m.predecessors, successorKey) - if _, exists := m.nodes[successorKey]; exists { - danglings = append(danglings, m.nodes[successorKey]) + delete(m.predecessors, successorDigest) + if _, exists := m.nodes[successorDigest]; exists { + danglings = append(danglings, m.nodes[successorDigest]) } } } - delete(m.successors, nodeKey) - delete(m.nodes, nodeKey) + delete(m.successors, node.Digest) + delete(m.nodes, node.Digest) return danglings } @@ -154,8 +151,8 @@ func (m *Memory) DigestSet() set.Set[digest.Digest] { defer m.lock.RUnlock() s := set.New[digest.Digest]() - for desc := range m.nodes { - s.Add(desc.Digest) + for digest := range m.nodes { + s.Add(digest) } return s } @@ -170,22 +167,20 @@ func (m *Memory) index(ctx context.Context, fetcher content.Fetcher, node ocispe defer m.lock.Unlock() // index the node - nodeKey := descriptor.FromOCI(node) - m.nodes[nodeKey] = node + m.nodes[node.Digest] = node // for each successor, put it into the node's successors list, and // put node into the succeesor's predecessors list - successorSet := set.New[descriptor.Descriptor]() - m.successors[nodeKey] = successorSet + successorSet := set.New[digest.Digest]() + m.successors[node.Digest] = successorSet for _, successor := range successors { - successorKey := descriptor.FromOCI(successor) - successorSet.Add(successorKey) - predecessorSet, exists := m.predecessors[successorKey] + successorSet.Add(successor.Digest) + predecessorSet, exists := m.predecessors[successor.Digest] if !exists { - predecessorSet = set.New[descriptor.Descriptor]() - m.predecessors[successorKey] = predecessorSet + predecessorSet = set.New[digest.Digest]() + m.predecessors[successor.Digest] = predecessorSet } - predecessorSet.Add(nodeKey) + predecessorSet.Add(node.Digest) } return successors, nil } @@ -195,7 +190,6 @@ func (m *Memory) Exists(node ocispec.Descriptor) bool { m.lock.RLock() defer m.lock.RUnlock() - nodeKey := descriptor.FromOCI(node) - _, exists := m.nodes[nodeKey] + _, exists := m.nodes[node.Digest] return exists } diff --git a/vendor/oras.land/oras-go/v2/internal/syncutil/once.go b/vendor/oras.land/oras-go/v2/internal/syncutil/once.go index e4497053..d46ebd75 100644 --- a/vendor/oras.land/oras-go/v2/internal/syncutil/once.go +++ b/vendor/oras.land/oras-go/v2/internal/syncutil/once.go @@ -24,7 +24,7 @@ import ( // Once is an object that will perform exactly one action. // Unlike sync.Once, this Once allows the action to have return values. type Once struct { - result interface{} + result any err error status chan bool } @@ -46,7 +46,7 @@ func NewOnce() *Once { // Besides the return value of the function f, including the error, Do returns // true if the function f passed is called first and is not cancelled, deadline // exceeded, or panicking. Otherwise, returns false. -func (o *Once) Do(ctx context.Context, f func() (interface{}, error)) (bool, interface{}, error) { +func (o *Once) Do(ctx context.Context, f func() (any, error)) (bool, any, error) { defer func() { if r := recover(); r != nil { o.status <- true diff --git a/vendor/oras.land/oras-go/v2/registry/remote/auth/cache.go b/vendor/oras.land/oras-go/v2/registry/remote/auth/cache.go index d11c092b..80091ddf 100644 --- a/vendor/oras.land/oras-go/v2/registry/remote/auth/cache.go +++ b/vendor/oras.land/oras-go/v2/registry/remote/auth/cache.go @@ -109,7 +109,7 @@ func (cc *concurrentCache) Set(ctx context.Context, registry string, scheme Sche }, " ") statusValue, _ := cc.status.LoadOrStore(statusKey, syncutil.NewOnce()) fetchOnce := statusValue.(*syncutil.Once) - fetchedFirst, result, err := fetchOnce.Do(ctx, func() (interface{}, error) { + fetchedFirst, result, err := fetchOnce.Do(ctx, func() (any, error) { return fetch(ctx) }) if fetchedFirst { diff --git a/vendor/oras.land/oras-go/v2/registry/remote/auth/client.go b/vendor/oras.land/oras-go/v2/registry/remote/auth/client.go index 5c5330e7..ce9297af 100644 --- a/vendor/oras.land/oras-go/v2/registry/remote/auth/client.go +++ b/vendor/oras.land/oras-go/v2/registry/remote/auth/client.go @@ -23,6 +23,7 @@ import ( "errors" "fmt" "io" + "net" "net/http" "net/url" "strings" @@ -136,7 +137,50 @@ func (c *Client) send(req *http.Request) (*http.Response, error) { for key, values := range c.Header { req.Header[key] = append(req.Header[key], values...) } - return c.client().Do(req) + // Drop the Authorization header when a redirect crosses an HTTP origin + // (scheme, host, or port). The standard library only strips sensitive + // headers when the hostname changes, so a redirect to a different port on + // the same host would otherwise forward credentials to an unintended + // endpoint. Any caller-provided CheckRedirect is preserved. + // Reference: https://github.com/oras-project/oras-go/security/advisories/GHSA-vh4v-2xq2-g5cg + client := c.client() + clientCopy := *client + checkRedirect := client.CheckRedirect + clientCopy.CheckRedirect = func(req *http.Request, via []*http.Request) error { + if len(via) > 0 && !sameHTTPOrigin(via[len(via)-1].URL, req.URL) { + req.Header.Del("Authorization") + } + if checkRedirect != nil { + return checkRedirect(req, via) + } + return nil + } + return clientCopy.Do(req) +} + +// sameHTTPOrigin reports whether a and b share the same HTTP origin, i.e. the +// same scheme and host. Default ports are normalized so that, for example, +// "example.com" and "example.com:443" compare equal over https. +func sameHTTPOrigin(a, b *url.URL) bool { + if !strings.EqualFold(a.Scheme, b.Scheme) { + return false + } + return canonicalHost(a) == canonicalHost(b) +} + +// canonicalHost returns the lower-cased host of u with the default port for its +// scheme applied when no explicit port is present. +func canonicalHost(u *url.URL) string { + port := u.Port() + if port == "" { + switch strings.ToLower(u.Scheme) { + case "https": + port = "443" + case "http": + port = "80" + } + } + return strings.ToLower(u.Hostname()) + ":" + port } // credential resolves the credential for the given registry. @@ -156,6 +200,49 @@ func (c *Client) cache() Cache { return c.Cache } +// validateRealm rejects bearer token realm URLs that would have the client +// forward credentials to obviously unsafe destinations: +// +// - schemes other than http or https, +// - http realms when the registry was contacted over https (TLS downgrade), +// - hosts that are IP literals in loopback, link-local, private, or +// unspecified ranges (e.g. cloud instance metadata services such as +// 169.254.169.254). +// +// Cross-host realms with a public hostname are permitted, because the +// distribution spec allows a separate token endpoint (e.g. Docker Hub's +// auth.docker.io). When the registry itself is reached at the same hostname +// as the realm, the IP-literal check is skipped so loopback and in-cluster +// deployments continue to work. +func validateRealm(realm string, registryURL *url.URL) error { + if realm == "" { + return nil + } + realmURL, err := url.Parse(realm) + if err != nil { + return fmt.Errorf("failed to parse bearer realm %q: %w", realm, err) + } + switch realmURL.Scheme { + case "https": + // always allowed + case "http": + if registryURL != nil && registryURL.Scheme == "https" { + return fmt.Errorf("bearer realm %q uses http but registry was contacted over https", realm) + } + default: + return fmt.Errorf("bearer realm %q uses unsupported scheme %q", realm, realmURL.Scheme) + } + if ip := net.ParseIP(realmURL.Hostname()); ip != nil { + if ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() || + ip.IsPrivate() || ip.IsUnspecified() { + if registryURL == nil || realmURL.Hostname() != registryURL.Hostname() { + return fmt.Errorf("bearer realm host %q is a loopback, link-local, private, or unspecified address", realmURL.Hostname()) + } + } + } + return nil +} + // SetUserAgent sets the user agent for all out-going requests. func (c *Client) SetUserAgent(userAgent string) { if c.Header == nil { @@ -182,6 +269,9 @@ func (c *Client) Do(originalReq *http.Request) (*http.Response, error) { var attemptedKey string cache := c.cache() host := originalReq.Host + if host == "" { + host = originalReq.URL.Host + } scheme, err := cache.GetScheme(ctx, host) if err == nil { switch scheme { @@ -207,6 +297,13 @@ func (c *Client) Do(originalReq *http.Request) (*http.Response, error) { if resp.StatusCode != http.StatusUnauthorized { return resp, nil } + // If the challenge came from a different origin than originally requested + // (e.g. the request was redirected to another host or port), do not resolve + // or send the registry credentials to that origin. + // Reference: https://github.com/oras-project/oras-go/security/advisories/GHSA-vh4v-2xq2-g5cg + if resp.Request != nil && !sameHTTPOrigin(originalReq.URL, resp.Request.URL) { + return resp, nil + } // attempt again with credentials for recognized schemes challenge := resp.Header.Get("Www-Authenticate") @@ -257,6 +354,9 @@ func (c *Client) Do(originalReq *http.Request) (*http.Response, error) { // attempt with credentials realm := params["realm"] + if err := validateRealm(realm, originalReq.URL); err != nil { + return nil, fmt.Errorf("%s %q: %w", resp.Request.Method, resp.Request.URL, err) + } service := params["service"] token, err := cache.Set(ctx, host, SchemeBearer, key, func(ctx context.Context) (string, error) { return c.fetchBearerToken(ctx, host, realm, service, scopes) diff --git a/vendor/oras.land/oras-go/v2/registry/remote/auth/scope.go b/vendor/oras.land/oras-go/v2/registry/remote/auth/scope.go index bdd6e5c4..4e515a81 100644 --- a/vendor/oras.land/oras-go/v2/registry/remote/auth/scope.go +++ b/vendor/oras.land/oras-go/v2/registry/remote/auth/scope.go @@ -254,7 +254,7 @@ func CleanScopes(scopes []string) []string { actionSet = make(map[string]struct{}) namedActions[resourceName] = actionSet } - for _, action := range strings.Split(actions, ",") { + for action := range strings.SplitSeq(actions, ",") { if action != "" { actionSet[action] = struct{}{} } diff --git a/vendor/oras.land/oras-go/v2/registry/remote/manifest.go b/vendor/oras.land/oras-go/v2/registry/remote/manifest.go index 0e10297c..845bf6c5 100644 --- a/vendor/oras.land/oras-go/v2/registry/remote/manifest.go +++ b/vendor/oras.land/oras-go/v2/registry/remote/manifest.go @@ -16,6 +16,7 @@ limitations under the License. package remote import ( + "slices" "strings" ocispec "github.com/opencontainers/image-spec/specs-go/v1" @@ -41,12 +42,7 @@ func isManifest(manifestMediaTypes []string, desc ocispec.Descriptor) bool { if len(manifestMediaTypes) == 0 { manifestMediaTypes = defaultManifestMediaTypes } - for _, mediaType := range manifestMediaTypes { - if desc.MediaType == mediaType { - return true - } - } - return false + return slices.Contains(manifestMediaTypes, desc.MediaType) } // manifestAcceptHeader generates the set in the `Accept` header for resolving diff --git a/vendor/oras.land/oras-go/v2/registry/remote/referrers.go b/vendor/oras.land/oras-go/v2/registry/remote/referrers.go index 720430d3..f2ad0b2d 100644 --- a/vendor/oras.land/oras-go/v2/registry/remote/referrers.go +++ b/vendor/oras.land/oras-go/v2/registry/remote/referrers.go @@ -118,8 +118,8 @@ func isReferrersFilterApplied(applied, requested string) bool { if applied == "" || requested == "" { return false } - filters := strings.Split(applied, ",") - for _, f := range filters { + filters := strings.SplitSeq(applied, ",") + for f := range filters { if f == requested { return true } diff --git a/vendor/oras.land/oras-go/v2/registry/remote/repository.go b/vendor/oras.land/oras-go/v2/registry/remote/repository.go index fed993df..bc649ec2 100644 --- a/vendor/oras.land/oras-go/v2/registry/remote/repository.go +++ b/vendor/oras.land/oras-go/v2/registry/remote/repository.go @@ -24,6 +24,7 @@ import ( "io" "mime" "net/http" + "net/url" "slices" "strconv" "strings" @@ -872,6 +873,25 @@ func (s *blobStore) Push(ctx context.Context, expected ocispec.Descriptor, conte return s.completePushAfterInitialPost(ctx, req, resp, expected, content) } +// sameUploadHost reports whether location and reqURL refer to the same host, +// normalizing implicit default ports (80 for http, 443 for https) so that +// e.g. "example.com" and "example.com:443" compare equal over HTTPS. +func sameUploadHost(location, reqURL *url.URL) bool { + if location.Hostname() != reqURL.Hostname() { + return false + } + canonicalPort := func(u *url.URL) string { + if p := u.Port(); p != "" { + return p + } + if u.Scheme == "https" { + return "443" + } + return "80" + } + return canonicalPort(location) == canonicalPort(reqURL) +} + // completePushAfterInitialPost implements step 2 of the push protocol. This can be invoked either by // Push or by Mount when the receiving repository does not implement the // mount endpoint. @@ -894,6 +914,15 @@ func (s *blobStore) completePushAfterInitialPost(ctx context.Context, req *http. if reqPort == "443" && locationHostname == reqHostname && locationPort == "" { location.Host = locationHostname + ":" + reqPort } + // Validate the Location stays on the same host to prevent credentials from + // being forwarded to an attacker-controlled endpoint. + // Reference: https://github.com/oras-project/oras-go/security/advisories/GHSA-jxpm-75mh-9fp7 + if !sameUploadHost(location, req.URL) { + return fmt.Errorf("blob upload Location %q is on a different host than the registry %q", location.Host, req.URL.Host) + } + if req.URL.Scheme == "https" && location.Scheme != "https" { + return fmt.Errorf("blob upload Location %q downgrades scheme from https", location.Host) + } url := location.String() req, err = http.NewRequestWithContext(ctx, http.MethodPut, url, content) if err != nil {