From f1e57f4da887b326e4478d46e7de84fe35b28270 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 25 Aug 2026 01:53:14 +0000 Subject: [PATCH] Bump github.com/moby/go-archive Bumps the go_modules group with 1 update in the / directory: [github.com/moby/go-archive](https://github.com/moby/go-archive). Updates `github.com/moby/go-archive` from 0.2.0 to 0.3.0 - [Release notes](https://github.com/moby/go-archive/releases) - [Changelog](https://github.com/moby/go-archive/blob/main/changes_test.go) - [Commits](https://github.com/moby/go-archive/compare/v0.2.0...v0.3.0) --- updated-dependencies: - dependency-name: github.com/moby/go-archive dependency-version: 0.3.0 dependency-type: indirect dependency-group: go_modules ... Signed-off-by: dependabot[bot] --- go.mod | 8 +- go.sum | 16 +- .../github.com/moby/go-archive/.golangci.yml | 12 + vendor/github.com/moby/go-archive/archive.go | 444 ++++++++++++------ .../moby/go-archive/archive_linux.go | 109 +++-- .../moby/go-archive/archive_unix.go | 110 ++++- .../moby/go-archive/archive_windows.go | 15 +- vendor/github.com/moby/go-archive/changes.go | 18 +- .../moby/go-archive/changes_linux.go | 2 +- .../moby/go-archive/changes_other.go | 2 +- .../go-archive/compression/compression.go | 4 +- vendor/github.com/moby/go-archive/copy.go | 64 ++- .../github.com/moby/go-archive/dev_darwin.go | 21 + .../github.com/moby/go-archive/dev_freebsd.go | 17 +- vendor/github.com/moby/go-archive/dev_unix.go | 21 +- vendor/github.com/moby/go-archive/diff.go | 139 +++--- vendor/github.com/moby/go-archive/rootpath.go | 112 +++++ .../moby/go-archive/sequential_other.go | 6 + .../go-archive/sequential_windows_go126.go | 9 + .../go-archive/sequential_windows_pre126.go | 6 + .../moby/go-archive/tarheader/tarheader.go | 2 +- .../go-archive/tarheader/tarheader_unix.go | 9 +- vendor/github.com/moby/go-archive/time.go | 4 + .../moby/go-archive/time_nonwindows.go | 61 ++- .../moby/go-archive/time_windows.go | 105 ++++- .../moby/go-archive/xattr_supported.go | 20 +- .../moby/sys/sequential/sequential_unix.go | 1 - .../moby/sys/sequential/sequential_windows.go | 103 +--- .../sequential/sequential_windows_go126.go | 13 + .../sequential/sequential_windows_pre126.go | 76 +++ .../github.com/moby/sys/user/lookup_unix.go | 1 - vendor/github.com/moby/sys/user/user.go | 229 ++++----- .../github.com/moby/sys/user/user_fuzzer.go | 1 - vendor/github.com/moby/sys/user/user_utils.go | 64 +++ vendor/modules.txt | 14 +- 35 files changed, 1263 insertions(+), 575 deletions(-) create mode 100644 vendor/github.com/moby/go-archive/dev_darwin.go create mode 100644 vendor/github.com/moby/go-archive/rootpath.go create mode 100644 vendor/github.com/moby/go-archive/sequential_other.go create mode 100644 vendor/github.com/moby/go-archive/sequential_windows_go126.go create mode 100644 vendor/github.com/moby/go-archive/sequential_windows_pre126.go create mode 100644 vendor/github.com/moby/sys/sequential/sequential_windows_go126.go create mode 100644 vendor/github.com/moby/sys/sequential/sequential_windows_pre126.go create mode 100644 vendor/github.com/moby/sys/user/user_utils.go diff --git a/go.mod b/go.mod index af30097c3..aa44ca39f 100644 --- a/go.mod +++ b/go.mod @@ -41,16 +41,16 @@ require ( github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.2.6 // indirect github.com/goccy/go-json v0.10.6 // indirect - github.com/klauspost/compress v1.18.6 // indirect + github.com/klauspost/compress v1.18.7 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/magiconair/properties v1.8.10 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect - github.com/moby/go-archive v0.2.0 // indirect + github.com/moby/go-archive v0.3.0 // indirect github.com/moby/moby/api v1.54.2 // indirect github.com/moby/moby/client v0.4.0 // indirect github.com/moby/patternmatcher v0.6.1 // indirect - github.com/moby/sys/sequential v0.6.0 // indirect - github.com/moby/sys/user v0.4.0 // indirect + github.com/moby/sys/sequential v0.7.0 // indirect + github.com/moby/sys/user v0.4.1 // indirect github.com/moby/sys/userns v0.1.0 // indirect github.com/moby/term v0.5.2 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect diff --git a/go.sum b/go.sum index 709d499e2..72c83c85c 100644 --- a/go.sum +++ b/go.sum @@ -65,8 +65,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao= -github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +github.com/klauspost/compress v1.18.7 h1:aUyZsS4kH3QTKurYhAOwAHxllVPnOthb3vPfnF1Ehjw= +github.com/klauspost/compress v1.18.7/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -77,18 +77,18 @@ github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8S github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= 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= -github.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU= +github.com/moby/go-archive v0.3.0 h1:nos4BtzzUIqB406BgQnWGMI4qib9BZ8XUHU+ucv/n1c= +github.com/moby/go-archive v0.3.0/go.mod h1:Npdv43fFqlhZW7Xo8fbm3ZMYFvAGNviUPqX21VERbcE= github.com/moby/moby/api v1.54.2 h1:wiat9QAhnDQjA7wk1kh/TqHz2I1uUA7M7t9SAl/JNXg= github.com/moby/moby/api v1.54.2/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs= github.com/moby/moby/client v0.4.0 h1:S+2XegzHQrrvTCvF6s5HFzcrywWQmuVnhOXe2kiWjIw= github.com/moby/moby/client v0.4.0/go.mod h1:QWPbvWchQbxBNdaLSpoKpCdf5E+WxFAgNHogCWDoa7g= github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U= github.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= -github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= -github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= -github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= -github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= +github.com/moby/sys/sequential v0.7.0 h1:ASQNGNROJSuOO6LL6bPHbKvuZu6NU8P4ldPWk31zj/8= +github.com/moby/sys/sequential v0.7.0/go.mod h1:NfSTAp6V3fw4tmkD62PEcOKeZKquXT8VKCkf7aVR79o= +github.com/moby/sys/user v0.4.1 h1:RgjRlaDKi/Xmyrz4t8lyzXT6v2ooFeO/7xtchmhVWE0= +github.com/moby/sys/user v0.4.1/go.mod h1:E9QsW5WRe1kUAf7kW8hXKwu1uhsZEAdPLYHYSDudF4Y= github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= diff --git a/vendor/github.com/moby/go-archive/.golangci.yml b/vendor/github.com/moby/go-archive/.golangci.yml index 21439e5c6..761e36d9d 100644 --- a/vendor/github.com/moby/go-archive/.golangci.yml +++ b/vendor/github.com/moby/go-archive/.golangci.yml @@ -9,6 +9,7 @@ issues: linters: enable: - errorlint + - gosec - unconvert - unparam exclusions: @@ -16,7 +17,18 @@ linters: presets: - comments - std-error-handling + rules: + # Ignore "G204: Subprocess launched with a potential tainted input or cmd arguments" + - path: '(.+)_test\.go' + linters: + - gosec + text: 'G204: Subprocess launched' settings: + gosec: + excludes: + - G301 # Expect directory permissions to be 0750 or less + - G304 # Potential file inclusion via variable + - G306 # Expect WriteFile permissions to be 0600 or less staticcheck: # Enable all options, with some exceptions. # For defaults, see https://golangci-lint.run/usage/linters/#staticcheck diff --git a/vendor/github.com/moby/go-archive/archive.go b/vendor/github.com/moby/go-archive/archive.go index 8dce2e6e2..4d9e87d20 100644 --- a/vendor/github.com/moby/go-archive/archive.go +++ b/vendor/github.com/moby/go-archive/archive.go @@ -8,9 +8,11 @@ import ( "fmt" "io" "os" + "path" "path/filepath" "runtime" "strings" + "sync" "syscall" "time" @@ -46,9 +48,18 @@ type ( // TarOptions wraps the tar options. TarOptions struct { - IncludeFiles []string - ExcludePatterns []string - Compression compression.Compression + // IncludeFiles lists archive-relative paths to include. + // Paths use POSIX ('/') separators. + IncludeFiles []string + + // ExcludePatterns lists archive-relative exclude patterns. + // Patterns use POSIX ('/') separators, matching patternmatcher semantics. + ExcludePatterns []string + Compression compression.Compression + // NoLchown disables applying ownership from the archive to extracted files + // and directories. Despite its historical name, it applies to all ownership + // changes, leaving extracted filesystem objects owned by the user performing + // the extraction. NoLchown bool IDMap user.IdentityMapping ChownOpts *ChownOpts @@ -86,10 +97,33 @@ func NewDefaultArchiver() *Archiver { return &Archiver{Untar: Untar} } -// breakoutError is used to differentiate errors related to breaking out -// When testing archive breakout in the unit tests, this error is expected -// in order for the test to pass. -type breakoutError error +// isPathEscapes reports whether err is os.Root's path-containment error. +// +// os.Root currently returns an unexported errPathEscapes sentinel, so callers +// cannot detect it with errors.Is. Keep the string comparison isolated here +// until Go exports the error; see https://go.dev/issue/74640. +func isPathEscapes(err error) bool { + // https://github.com/golang/go/blob/go1.26.5/src/os/file.go#L421 + const errPathEscapes = "path escapes from parent" + for err != nil { + if errors.Unwrap(err) == nil { + return err.Error() == errPathEscapes + } + err = errors.Unwrap(err) + } + return false +} + +// breakoutErr marks errors caused by archive breakout attempts. +// Unit tests use it to distinguish expected breakout failures from other +// errors. +type breakoutErr struct{ error } + +func breakoutError(err error) error { + return &breakoutErr{error: err} +} + +func (e *breakoutErr) Unwrap() error { return e.error } const ( AUFSWhiteoutFormat WhiteoutFormat = 0 // AUFSWhiteoutFormat is the default format for whiteouts @@ -98,17 +132,17 @@ const ( // IsArchivePath checks if the (possibly compressed) file at the given path // starts with a tar file header. -func IsArchivePath(path string) bool { - file, err := os.Open(path) +func IsArchivePath(filePath string) bool { + file, err := os.Open(filePath) if err != nil { return false } - defer file.Close() + defer func() { _ = file.Close() }() rdr, err := compression.DecompressStream(file) if err != nil { return false } - defer rdr.Close() + defer func() { _ = rdr.Close() }() r := tar.NewReader(rdr) _, err = r.Next() return err == nil @@ -129,8 +163,10 @@ func ReplaceFileTarWrapper(inputTarStream io.ReadCloser, mods map[string]TarModi go func() { tarReader := tar.NewReader(inputTarStream) tarWriter := tar.NewWriter(pipeWriter) - defer inputTarStream.Close() - defer tarWriter.Close() + defer func() { + _ = tarWriter.Close() + _ = inputTarStream.Close() + }() modify := func(name string, original *tar.Header, modifier TarModifierFunc, tarReader io.Reader) error { header, data, err := modifier(name, original, tarReader) @@ -164,7 +200,7 @@ func ReplaceFileTarWrapper(inputTarStream io.ReadCloser, mods map[string]TarModi break } if err != nil { - pipeWriter.CloseWithError(err) + _ = pipeWriter.CloseWithError(err) return } @@ -172,11 +208,11 @@ func ReplaceFileTarWrapper(inputTarStream io.ReadCloser, mods map[string]TarModi if !ok { // No modifiers for this file, copy the header and data if err := tarWriter.WriteHeader(originalHeader); err != nil { - pipeWriter.CloseWithError(err) + _ = pipeWriter.CloseWithError(err) return } if err := copyWithBuffer(tarWriter, tarReader); err != nil { - pipeWriter.CloseWithError(err) + _ = pipeWriter.CloseWithError(err) return } continue @@ -184,7 +220,7 @@ func ReplaceFileTarWrapper(inputTarStream io.ReadCloser, mods map[string]TarModi delete(mods, originalHeader.Name) if err := modify(originalHeader.Name, originalHeader, modifier, tarReader); err != nil { - pipeWriter.CloseWithError(err) + _ = pipeWriter.CloseWithError(err) return } } @@ -192,12 +228,12 @@ func ReplaceFileTarWrapper(inputTarStream io.ReadCloser, mods map[string]TarModi // Apply the modifiers that haven't matched any files in the archive for name, modifier := range mods { if err := modify(name, nil, modifier, nil); err != nil { - pipeWriter.CloseWithError(err) + _ = pipeWriter.CloseWithError(err) return } } - pipeWriter.Close() + _ = pipeWriter.Close() }() return pipeReader } @@ -218,7 +254,7 @@ func FileInfoHeader(name string, fi os.FileInfo, link string) (*tar.Header, erro hdr.ModTime = hdr.ModTime.Truncate(time.Second) hdr.AccessTime = time.Time{} hdr.ChangeTime = time.Time{} - hdr.Mode = int64(chmodTarEntry(os.FileMode(hdr.Mode))) + hdr.Mode = chmodTarEntry(hdr.Mode) hdr.Name = canonicalTarName(name, fi.IsDir()) return hdr, nil } @@ -227,7 +263,7 @@ const paxSchilyXattr = "SCHILY.xattr." // ReadSecurityXattrToTarHeader reads security.capability xattr from filesystem // to a tar header -func ReadSecurityXattrToTarHeader(path string, hdr *tar.Header) error { +func ReadSecurityXattrToTarHeader(filePath string, hdr *tar.Header) error { const ( // Values based on linux/include/uapi/linux/capability.h xattrCapsSz2 = 20 @@ -235,7 +271,7 @@ func ReadSecurityXattrToTarHeader(path string, hdr *tar.Header) error { vfsCapRevision2 = 2 vfsCapRevision3 = 3 ) - capability, _ := lgetxattr(path, "security.capability") + capability, _ := lgetxattr(filePath, "security.capability") if capability != nil { if capability[versionOffset] == vfsCapRevision3 { // Convert VFS_CAP_REVISION_3 to VFS_CAP_REVISION_2 as root UID makes no @@ -253,7 +289,7 @@ func ReadSecurityXattrToTarHeader(path string, hdr *tar.Header) error { type tarWhiteoutConverter interface { ConvertWrite(*tar.Header, string, os.FileInfo) (*tar.Header, error) - ConvertRead(*tar.Header, string) (bool, error) + ConvertRead(*os.Root, *tar.Header, string) (bool, error) } type tarAppender struct { @@ -292,9 +328,10 @@ func canonicalTarName(name string, isDir bool) string { return name } -// addTarFile adds to the tar archive a file from `path` as `name` -func (ta *tarAppender) addTarFile(path, name string) error { - fi, err := os.Lstat(path) +// addTarFile adds to the tar archive a file from `srcPath` as `name` +func (ta *tarAppender) addTarFile(srcPath, archivePath string) error { + archivePath = filepath.ToSlash(archivePath) + fi, err := os.Lstat(srcPath) if err != nil { return err } @@ -302,17 +339,17 @@ func (ta *tarAppender) addTarFile(path, name string) error { var link string if fi.Mode()&os.ModeSymlink != 0 { var err error - link, err = os.Readlink(path) + link, err = os.Readlink(srcPath) if err != nil { return err } } - hdr, err := FileInfoHeader(name, fi, link) + hdr, err := FileInfoHeader(archivePath, fi, link) if err != nil { return err } - if err := ReadSecurityXattrToTarHeader(path, hdr); err != nil { + if err := ReadSecurityXattrToTarHeader(srcPath, hdr); err != nil { return err } @@ -321,7 +358,7 @@ func (ta *tarAppender) addTarFile(path, name string) error { if !fi.IsDir() && hasHardlinks(fi) { inode, err := getInodeFromStat(fi.Sys()) if err != nil { - return err + return fmt.Errorf("unexpected file info for %q: %w", srcPath, err) } // a link should have a name that it links too // and that linked name should be first in the tar archive @@ -330,7 +367,7 @@ func (ta *tarAppender) addTarFile(path, name string) error { hdr.Linkname = oldpath hdr.Size = 0 // This Must be here for the writer math to add up! } else { - ta.SeenFiles[inode] = name + ta.SeenFiles[inode] = hdr.Name } } @@ -341,7 +378,7 @@ func (ta *tarAppender) addTarFile(path, name string) error { // handle re-mapping container ID mappings back to host ID mappings before // writing tar headers/files. We skip whiteout files because they were written // by the kernel and already have proper ownership relative to the host - if !isOverlayWhiteout && !strings.HasPrefix(filepath.Base(hdr.Name), WhiteoutPrefix) && !ta.IdentityMapping.Empty() { + if !isOverlayWhiteout && !strings.HasPrefix(path.Base(hdr.Name), WhiteoutPrefix) && !ta.IdentityMapping.Empty() { uid, gid, err := getFileUIDGID(fi.Sys()) if err != nil { return err @@ -359,7 +396,7 @@ func (ta *tarAppender) addTarFile(path, name string) error { } if ta.WhiteoutConverter != nil { - wo, err := ta.WhiteoutConverter.ConvertWrite(hdr, path, fi) + wo, err := ta.WhiteoutConverter.ConvertWrite(hdr, srcPath, fi) if err != nil { return err } @@ -370,12 +407,12 @@ func (ta *tarAppender) addTarFile(path, name string) error { // hdr may have been updated to be a whiteout with returning // a whiteout header if wo != nil { + if hdr.Typeflag == tar.TypeReg && hdr.Size > 0 { + return fmt.Errorf("tar: cannot use whiteout for non-empty file %q", hdr.Name) + } if err := ta.TarWriter.WriteHeader(hdr); err != nil { return err } - if hdr.Typeflag == tar.TypeReg && hdr.Size > 0 { - return fmt.Errorf("tar: cannot use whiteout for non-empty file") - } hdr = wo } } @@ -387,13 +424,13 @@ func (ta *tarAppender) addTarFile(path, name string) error { if hdr.Typeflag == tar.TypeReg && hdr.Size > 0 { // We use sequential file access to avoid depleting the standby list on // Windows. On Linux, this equates to a regular os.Open. - file, err := sequential.Open(path) + file, err := sequential.Open(srcPath) if err != nil { return err } err = copyWithBuffer(ta.TarWriter, file) - file.Close() + _ = file.Close() if err != nil { return err } @@ -402,7 +439,10 @@ func (ta *tarAppender) addTarFile(path, name string) error { return nil } -func createTarFile(path, extractDir string, hdr *tar.Header, reader io.Reader, opts *TarOptions) error { +// createTarFile extracts a single tar entry into the given root. dstPath is the +// root-relative path of the entry being extracted, in native (host-separator) +// form so it can be passed directly to os.Root methods and fsRootPath. +func createTarFile(root *os.Root, dstPath string, hdr *tar.Header, reader io.Reader, opts *TarOptions) error { var ( Lchown = true inUserns, bestEffortXattrs bool @@ -424,18 +464,24 @@ func createTarFile(path, extractDir string, hdr *tar.Header, reader io.Reader, o switch hdr.Typeflag { case tar.TypeDir: - // Create directory unless it exists as a directory already. - // In that case we just want to merge the two - if fi, err := os.Lstat(path); err != nil || !fi.IsDir() { - if err := os.Mkdir(path, hdrInfo.Mode()); err != nil { + // Create directory unless it already exists as one; merge in that case. + // os.Root.Mkdir only accepts the nine least-significant permission + // bits; special bits (setuid, setgid, sticky) are applied afterward + // by handleLChmod via root.Chmod. + if fi, err := root.Lstat(dstPath); err != nil || !fi.IsDir() { + if err := root.Mkdir(dstPath, hdrInfo.Mode()&0o777); err != nil { return err } } case tar.TypeReg: - // Source is regular file. We use sequential file access to avoid depleting - // the standby list on Windows. On Linux, this equates to a regular os.OpenFile. - file, err := sequential.OpenFile(path, os.O_CREATE|os.O_WRONLY, hdrInfo.Mode()) + // Source is a regular file. Use os.Root.OpenFile so that all + // path resolution is bounded within root using openat(2) semantics. + // os.Root.OpenFile only accepts the nine least-significant permission + // bits; special bits are applied afterward by handleLChmod. + // We use sequential file access to avoid depleting the standby list + // on Windows (go1.26). On Linux, this equates to a regular os.OpenFile. + file, err := root.OpenFile(dstPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC|windows_O_FILE_FLAG_SEQUENTIAL_SCAN, hdrInfo.Mode()&0o777) if err != nil { return err } @@ -447,47 +493,47 @@ func createTarFile(path, extractDir string, hdr *tar.Header, reader io.Reader, o case tar.TypeBlock, tar.TypeChar: if inUserns { // cannot create devices in a userns - log.G(context.TODO()).WithFields(log.Fields{"path": path, "type": hdr.Typeflag}).Debug("skipping device nodes in a userns") + log.G(context.TODO()).WithFields(log.Fields{"path": dstPath, "type": hdr.Typeflag}).Debug("skipping device nodes in a userns") return nil } - // Handle this is an OS-specific way - if err := handleTarTypeBlockCharFifo(hdr, path); err != nil { + if err := handleTarTypeBlockCharFifo(root, hdr, dstPath); err != nil { return err } case tar.TypeFifo: - // Handle this is an OS-specific way - if err := handleTarTypeBlockCharFifo(hdr, path); err != nil { + if err := handleTarTypeBlockCharFifo(root, hdr, dstPath); err != nil { if inUserns && errors.Is(err, syscall.EPERM) { // In most cases, cannot create a fifo if running in user namespace - log.G(context.TODO()).WithFields(log.Fields{"error": err, "path": path, "type": hdr.Typeflag}).Debug("creating fifo node in a userns") + log.G(context.TODO()).WithFields(log.Fields{"error": err, "path": dstPath, "type": hdr.Typeflag}).Debug("creating fifo node in a userns") return nil } return err } case tar.TypeLink: - // #nosec G305 -- The target path is checked for path traversal. - targetPath := filepath.Join(extractDir, hdr.Linkname) - // check for hardlink breakout - if !strings.HasPrefix(targetPath, extractDir) { - return breakoutError(fmt.Errorf("invalid hardlink %q -> %q", targetPath, hdr.Linkname)) + // Defence in depth: root.Link's containment is limited when + // dest is a volume root. + linkname := path.Clean(hdr.Linkname) + if linkname == "." || !filepath.IsLocal(linkname) { + return breakoutError(fmt.Errorf("invalid hardlink target %q", hdr.Linkname)) } - if err := os.Link(targetPath, path); err != nil { + if err := root.Link(filepath.FromSlash(linkname), dstPath); err != nil { return err } case tar.TypeSymlink: - // path -> hdr.Linkname = targetPath - // e.g. /extractDir/path/to/symlink -> ../2/file = /extractDir/path/2/file - targetPath := filepath.Join(filepath.Dir(path), hdr.Linkname) // #nosec G305 -- The target path is checked for path traversal. - - // the reason we don't need to check symlinks in the path (with FollowSymlinkInScope) is because - // that symlink would first have to be created, which would be caught earlier, at this very check: - if !strings.HasPrefix(targetPath, extractDir) { - return breakoutError(fmt.Errorf("invalid symlink %q -> %q", path, hdr.Linkname)) - } - if err := os.Symlink(hdr.Linkname, path); err != nil { + // Symlink targets are archive data, not filesystem paths. Preserve the + // target verbatim rather than cleaning or converting it (filepath.FromSlash). + linkTarget := hdr.Linkname + + // os.Root.Symlink contains the symlink's location (newname) within + // root but stores the target (oldname) verbatim, so absolute targets + // such as /usr/lib -- common and legitimate in container images -- are + // preserved rather than rejected. The symlink node is therefore always + // created within root via openat(2) semantics, without resolving to an + // absolute path; containment applies when the symlink is followed, not + // at creation. + if err := root.Symlink(linkTarget, dstPath); err != nil { return err } @@ -504,22 +550,31 @@ func createTarFile(path, extractDir string, hdr *tar.Header, reader io.Reader, o if chownOpts == nil { chownOpts = &ChownOpts{UID: hdr.Uid, GID: hdr.Gid} } - if err := os.Lchown(path, chownOpts.UID, chownOpts.GID); err != nil { + if err := root.Lchown(dstPath, chownOpts.UID, chownOpts.GID); err != nil { var msg string if inUserns && errors.Is(err, syscall.EINVAL) { msg = " (try increasing the number of subordinate IDs in /etc/subuid and /etc/subgid)" } - return fmt.Errorf("failed to Lchown %q for UID %d, GID %d%s: %w", path, hdr.Uid, hdr.Gid, msg, err) + return fmt.Errorf("failed to Lchown %q for UID %d, GID %d%s: %w", dstPath, hdr.Uid, hdr.Gid, msg, err) } } var xattrErrs []string + absPath := sync.OnceValues(func() (string, error) { + return fsRootPath(root.Name(), dstPath) + }) for key, value := range hdr.PAXRecords { xattr, ok := strings.CutPrefix(key, paxSchilyXattr) if !ok { continue } - if err := lsetxattr(path, xattr, []byte(value), 0); err != nil { + // os.Root has no xattr support; use the absolute path derived from + // the root so the path remains bounded. + ap, err := absPath() + if err != nil { + return err + } + if err := lsetxattr(ap, xattr, []byte(value), 0); err != nil { if bestEffortXattrs && errors.Is(err, syscall.ENOTSUP) || errors.Is(err, syscall.EPERM) { // EPERM occurs if modifying xattrs is not allowed. This can // happen when running in userns with restrictions (ChromeOS). @@ -538,39 +593,43 @@ func createTarFile(path, extractDir string, hdr *tar.Header, reader io.Reader, o // There is no LChmod, so ignore mode for symlink. Also, this // must happen after chown, as that can modify the file mode - if err := handleLChmod(hdr, path, hdrInfo); err != nil { + if err := handleLChmod(root, dstPath, hdr, hdrInfo); err != nil { return err } aTime := boundTime(latestTime(hdr.AccessTime, hdr.ModTime)) mTime := boundTime(hdr.ModTime) - // chtimes doesn't support a NOFOLLOW flag atm - if hdr.Typeflag == tar.TypeLink { - if fi, err := os.Lstat(hdr.Linkname); err == nil && (fi.Mode()&os.ModeSymlink == 0) { - if err := chtimes(path, aTime, mTime); err != nil { + switch hdr.Typeflag { + case tar.TypeSymlink: + // Apply timestamps to the symlink itself (AT_SYMLINK_NOFOLLOW). + if err := lchtimes(root, dstPath, aTime, mTime); err != nil { + return err + } + case tar.TypeLink: + // Follow the hardlink only when its target is not itself a symlink. + fi, err := root.Lstat(filepath.FromSlash(path.Clean(hdr.Linkname))) + if err == nil && fi.Mode()&os.ModeSymlink == 0 { + if err := chtimes(root, dstPath, aTime, mTime); err != nil { return err } } - } else if hdr.Typeflag != tar.TypeSymlink { - if err := chtimes(path, aTime, mTime); err != nil { - return err - } - } else { - if err := lchtimes(path, aTime, mTime); err != nil { + default: + // All other file types follow symlinks. + if err := chtimes(root, dstPath, aTime, mTime); err != nil { return err } } return nil } -// Tar creates an archive from the directory at `path`, and returns it as a +// Tar creates an archive from the directory at `srcPath`, and returns it as a // stream of bytes. -func Tar(path string, comp compression.Compression) (io.ReadCloser, error) { - return TarWithOptions(path, &TarOptions{Compression: comp}) +func Tar(srcPath string, comp compression.Compression) (io.ReadCloser, error) { + return TarWithOptions(srcPath, &TarOptions{Compression: comp}) } -// TarWithOptions creates an archive from the directory at `path`, only including files whose relative +// TarWithOptions creates an archive from the directory at `srcPath`, only including files whose relative // paths are included in `options.IncludeFiles` (if non-nil) or not in `options.ExcludePatterns`. func TarWithOptions(srcPath string, options *TarOptions) (io.ReadCloser, error) { tb, err := NewTarballer(srcPath, options) @@ -803,11 +862,28 @@ func (t *Tarballer) Do() { } } +// unpackedDir records a directory whose mtime must be restored after all +// entries are extracted, along with the root-relative entry name used during +// extraction. +type unpackedDir struct { + hdr *tar.Header + name string // root-relative entry name +} + // Unpack unpacks the decompressedArchive to dest with options. func Unpack(decompressedArchive io.Reader, dest string, options *TarOptions) error { + if options == nil { + options = &TarOptions{} + } + root, err := os.OpenRoot(dest) + if err != nil { + return err + } + defer func() { _ = root.Close() }() + tr := tar.NewReader(decompressedArchive) - var dirs []*tar.Header + var dirs []unpackedDir whiteoutConverter := getWhiteoutConverter(options.WhiteoutFormat) // Iterate through the files in the archive. @@ -828,48 +904,50 @@ loop: continue } - // Normalize name, for safety and for a simple is-root check - // This keeps "../" as-is, but normalizes "/../" to "/". Or Windows: - // This keeps "..\" as-is, but normalizes "\..\" to "\". - hdr.Name = filepath.Clean(hdr.Name) - + // Strip a leading "/" so absolute entries stay root-relative, and + // normalize the POSIX tar path. Skip entries referring to the extraction + // root and reject paths that escape it. + name := path.Clean(strings.TrimLeft(hdr.Name, "/")) + if name == "." { + continue + } + if !filepath.IsLocal(name) { + return breakoutError(fmt.Errorf("invalid entry name %q", hdr.Name)) + } for _, exclude := range options.ExcludePatterns { - if strings.HasPrefix(hdr.Name, exclude) { + if strings.HasPrefix(name, exclude) { continue loop } } - // Ensure that the parent directory exists. - err = createImpliedDirectories(dest, hdr, options) - if err != nil { - return err - } + hdr.Name = name - // #nosec G305 -- The joined path is checked for path traversal. - path := filepath.Join(dest, hdr.Name) - rel, err := filepath.Rel(dest, path) - if err != nil { - return err - } - if strings.HasPrefix(rel, ".."+string(os.PathSeparator)) { - return breakoutError(fmt.Errorf("%q is outside of %q", hdr.Name, dest)) + // Skip entries whose name (or hardlink target) Windows cannot represent. + if err := unrepresentableOnWindows(hdr); err != nil { + log.G(context.TODO()).Warnf("Windows: ignoring entry: %v", err) + continue loop } - // If path exits we almost always just want to remove and replace it + // dstPath is the native (host-separator) form of the entry name, + // used at all filesystem boundaries (os.Root methods, fsRootPath). + // hdr.Name stays POSIX (forward-slash) for logical string checks. + dstPath := filepath.FromSlash(hdr.Name) + + // If dstPath exists we almost always just want to remove and replace it. // The only exception is when it is a directory *and* the file from // the layer is also a directory. Then we want to merge them (i.e. // just apply the metadata from the layer). - if fi, err := os.Lstat(path); err == nil { + if fi, err := root.Lstat(dstPath); err == nil { if options.NoOverwriteDirNonDir && fi.IsDir() && hdr.Typeflag != tar.TypeDir { // If NoOverwriteDirNonDir is true then we cannot replace // an existing directory with a non-directory from the archive. - return fmt.Errorf("cannot overwrite directory %q with non-directory %q", path, dest) + return fmt.Errorf("cannot overwrite directory %q with non-directory %q", hdr.Name, dest) } if options.NoOverwriteDirNonDir && !fi.IsDir() && hdr.Typeflag == tar.TypeDir { // If NoOverwriteDirNonDir is true then we cannot replace // an existing non-directory with a directory from the archive. - return fmt.Errorf("cannot overwrite non-directory %q with directory %q", path, dest) + return fmt.Errorf("cannot overwrite non-directory %q with directory %q", hdr.Name, dest) } if fi.IsDir() && hdr.Name == "." { @@ -877,7 +955,7 @@ loop: } if !fi.IsDir() || hdr.Typeflag != tar.TypeDir { - if err := os.RemoveAll(path); err != nil { + if err := root.RemoveAll(dstPath); err != nil { return err } } @@ -887,8 +965,16 @@ loop: return err } + // Ensure that the parent directory exists. + // + // This must be done before whiteoutConverter.ConvertRead, which + // may set xattrs on the directory or create whiteout files. + if err := createImpliedDirectories(root, hdr, options); err != nil { + return err + } + if whiteoutConverter != nil { - writeFile, err := whiteoutConverter.ConvertRead(hdr, path) + writeFile, err := whiteoutConverter.ConvertRead(root, hdr, dstPath) if err != nil { return err } @@ -897,51 +983,122 @@ loop: } } - if err := createTarFile(path, dest, hdr, tr, options); err != nil { + if err := createTarFile(root, dstPath, hdr, tr, options); err != nil { return err } // Directory mtimes must be handled at the end to avoid further // file creation in them to modify the directory mtime if hdr.Typeflag == tar.TypeDir { - dirs = append(dirs, hdr) + dirs = append(dirs, unpackedDir{hdr: hdr, name: dstPath}) } } - for _, hdr := range dirs { - // #nosec G305 -- The header was checked for path traversal before it was appended to the dirs slice. - path := filepath.Join(dest, hdr.Name) - - if err := chtimes(path, boundTime(latestTime(hdr.AccessTime, hdr.ModTime)), boundTime(hdr.ModTime)); err != nil { + for _, d := range dirs { + aTime := boundTime(latestTime(d.hdr.AccessTime, d.hdr.ModTime)) + if err := chtimes(root, d.name, aTime, boundTime(d.hdr.ModTime)); err != nil { return err } } return nil } +// unrepresentableOnWindows returns an error describing why a tar entry cannot +// be faithfully created on Windows, or nil if it can (always on non-Windows). +// On Windows ":" is illegal in a filename and "\" is a path separator, so a tar +// name or hardlink target containing them (they use POSIX semantics) would be +// misinterpreted by os.Root (e.g. "a\b" resolved as two components). Symlink +// targets are stored verbatim (not resolved at creation), so they are exempt. +func unrepresentableOnWindows(hdr *tar.Header) error { + if runtime.GOOS != "windows" { + return nil + } + if strings.ContainsAny(hdr.Name, `:\`) { + return fmt.Errorf("entry name %q contains a character Windows cannot represent in a path", hdr.Name) + } + // A hardlink target is resolved within the root by os.Root.Link; a symlink + // target is stored verbatim, so only hardlinks need the target checked. + if hdr.Typeflag == tar.TypeLink && strings.ContainsAny(hdr.Linkname, `:\`) { + return fmt.Errorf("hardlink target %q contains a character Windows cannot represent in a path", hdr.Linkname) + } + return nil +} + // createImpliedDirectories will create all parent directories of the current path with default permissions, if they do // not already exist. This is possible as the tar format supports 'implicit' directories, where their existence is // defined by the paths of files in the tar, but there are no header entries for the directories themselves, and thus // we most both create them and choose metadata like permissions. // -// The caller should have performed filepath.Clean(hdr.Name), so hdr.Name will now be in the filepath format for the OS -// on which the daemon is running. This precondition is required because this function assumes a OS-specific path -// separator when checking that a path is not the root. -func createImpliedDirectories(dest string, hdr *tar.Header, options *TarOptions) error { - // Not the root directory, ensure that the parent directory exists - if !strings.HasSuffix(hdr.Name, string(os.PathSeparator)) { - parent := filepath.Dir(hdr.Name) - parentPath := filepath.Join(dest, parent) - if _, err := os.Lstat(parentPath); err != nil && os.IsNotExist(err) { - // RootPair() is confined inside this loop as most cases will not require a call, so we can spend some - // unneeded function calls in the uncommon case to encapsulate logic -- implied directories are a niche - // usage that reduces the portability of an image. - uid, gid := options.IDMap.RootPair() - - err = user.MkdirAllAndChown(parentPath, ImpliedDirectoryMode, uid, gid, user.WithOnlyNew) +// The caller must have normalized hdr.Name (no leading ".." components). +// All directory creation is performed via root so it is bounded within the +// destination at the OS level (openat(2) semantics), preventing escape via +// symlinks in the destination tree. +func createImpliedDirectories(root *os.Root, hdr *tar.Header, options *TarOptions) error { + // For non-directory entries, ensure that the parent directory exists. + if hdr.Typeflag != tar.TypeDir { + parent := filepath.FromSlash(path.Dir(strings.TrimSuffix(hdr.Name, "/"))) + // Skip when the parent is the root itself; nothing to create. + if parent == "." || parent == "" { + return nil + } + if _, err := root.Lstat(parent); err == nil { + return nil + } else if !os.IsNotExist(err) { + return err + } + // RootPair() is confined inside this loop as most cases will not require a call, so we can spend some + // unneeded function calls in the uncommon case to encapsulate logic -- implied directories are a niche + // usage that reduces the portability of an image. + uid, gid := options.IDMap.RootPair() + + // Similar to [user.MkdirAllAndChown] + // + // [user.MkdirAllAndChown]: https://pkg.go.dev/github.com/moby/sys/user#MkdirAllAndChown + var cur string + for c := range strings.SplitSeq(parent, string(os.PathSeparator)) { + if c == "" { + continue + } + cur = filepath.Join(cur, c) + if err := root.Mkdir(cur, ImpliedDirectoryMode); err != nil { + if !errors.Is(err, os.ErrExist) { + return err + } + + fi, err := root.Stat(cur) + if err != nil { + return err + } + if fi.IsDir() { + continue + } + return &os.PathError{Op: "mkdir", Path: cur, Err: syscall.ENOTDIR} + } + if options.NoLchown { + continue + } + // Only the successful Mkdir case is newly-created. + dir, err := root.Open(cur) if err != nil { return err } + if uid != 0 || gid != 0 { + if err := dir.Chown(uid, gid); err != nil { + _ = dir.Close() + return err + } + } + // root.Mkdir applies the mode subject to the process umask, so + // re-apply it with Chmod to guarantee ImpliedDirectoryMode + // independent of umask, matching the previous MkdirAllAndChown + // behavior. + if err := dir.Chmod(ImpliedDirectoryMode); err != nil { + _ = dir.Close() + return err + } + if err := dir.Close(); err != nil { + return err + } } } @@ -974,9 +1131,6 @@ func untarHandler(tarArchive io.Reader, dest string, options *TarOptions, decomp if options == nil { options = &TarOptions{} } - if options.ExcludePatterns == nil { - options.ExcludePatterns = []string{} - } r := tarArchive if decompress { @@ -984,7 +1138,7 @@ func untarHandler(tarArchive io.Reader, dest string, options *TarOptions, decomp if err != nil { return err } - defer decompressedArchive.Close() + defer func() { _ = decompressedArchive.Close() }() r = decompressedArchive } @@ -998,7 +1152,7 @@ func (archiver *Archiver) TarUntar(src, dst string) error { if err != nil { return err } - defer archive.Close() + defer func() { _ = archive.Close() }() return archiver.Untar(archive, dst, &TarOptions{ IDMap: archiver.IDMapping, }) @@ -1010,7 +1164,7 @@ func (archiver *Archiver) UntarPath(src, dst string) error { if err != nil { return err } - defer archive.Close() + defer func() { _ = archive.Close() }() return archiver.Untar(archive, dst, &TarOptions{ IDMap: archiver.IDMapping, }) @@ -1070,13 +1224,13 @@ func (archiver *Archiver) CopyFileWithTar(src, dst string) (err error) { defer close(errC) errC <- func() error { - defer w.Close() + defer func() { _ = w.Close() }() srcF, err := os.Open(src) if err != nil { return err } - defer srcF.Close() + defer func() { _ = srcF.Close() }() hdr, err := tarheader.FileInfoHeaderNoLookups(srcSt, "") if err != nil { @@ -1087,14 +1241,14 @@ func (archiver *Archiver) CopyFileWithTar(src, dst string) (err error) { hdr.AccessTime = time.Time{} hdr.ChangeTime = time.Time{} hdr.Name = filepath.Base(dst) - hdr.Mode = int64(chmodTarEntry(os.FileMode(hdr.Mode))) + hdr.Mode = chmodTarEntry(hdr.Mode) if err := remapIDs(archiver.IDMapping, hdr); err != nil { return err } tw := tar.NewWriter(w) - defer tw.Close() + defer func() { _ = tw.Close() }() if err := tw.WriteHeader(hdr); err != nil { return err } @@ -1112,7 +1266,7 @@ func (archiver *Archiver) CopyFileWithTar(src, dst string) (err error) { err = archiver.Untar(r, filepath.Dir(dst), nil) if err != nil { - r.CloseWithError(err) + _ = r.CloseWithError(err) } return err } diff --git a/vendor/github.com/moby/go-archive/archive_linux.go b/vendor/github.com/moby/go-archive/archive_linux.go index 7b6c3e02b..9341bc599 100644 --- a/vendor/github.com/moby/go-archive/archive_linux.go +++ b/vendor/github.com/moby/go-archive/archive_linux.go @@ -4,6 +4,7 @@ import ( "archive/tar" "fmt" "os" + "path" "path/filepath" "strings" @@ -13,36 +14,43 @@ import ( func getWhiteoutConverter(format WhiteoutFormat) tarWhiteoutConverter { if format == OverlayWhiteoutFormat { - return overlayWhiteoutConverter{} + return newOverlayWhiteoutConverter() } return nil } -type overlayWhiteoutConverter struct{} +type overlayWhiteoutConverter struct { + opaqueXattr string +} + +func newOverlayWhiteoutConverter() overlayWhiteoutConverter { + opaqueXattr := "trusted.overlay.opaque" + if userns.RunningInUserNS() { + opaqueXattr = "user.overlay.opaque" + } + return overlayWhiteoutConverter{ + opaqueXattr: opaqueXattr, + } +} -func (overlayWhiteoutConverter) ConvertWrite(hdr *tar.Header, path string, fi os.FileInfo) (wo *tar.Header, _ error) { +func (c overlayWhiteoutConverter) ConvertWrite(hdr *tar.Header, filePath string, fi os.FileInfo) (wo *tar.Header, _ error) { // convert whiteouts to AUFS format if fi.Mode()&os.ModeCharDevice != 0 && hdr.Devmajor == 0 && hdr.Devminor == 0 { // we just rename the file and make it normal - dir, filename := filepath.Split(hdr.Name) - hdr.Name = filepath.Join(dir, WhiteoutPrefix+filename) + dir, filename := path.Split(hdr.Name) + hdr.Name = path.Join(dir, WhiteoutPrefix+filename) hdr.Mode = 0o600 hdr.Typeflag = tar.TypeReg hdr.Size = 0 } - if fi.Mode()&os.ModeDir == 0 { + if !fi.IsDir() { // FIXME(thaJeztah): return a sentinel error instead of nil, nil return nil, nil } - opaqueXattrName := "trusted.overlay.opaque" - if userns.RunningInUserNS() { - opaqueXattrName = "user.overlay.opaque" - } - // convert opaque dirs to AUFS format by writing an empty file with the prefix - opaque, err := lgetxattr(path, opaqueXattrName) + opaque, err := lgetxattr(filePath, c.opaqueXattr) if err != nil { return nil, err } @@ -50,14 +58,14 @@ func (overlayWhiteoutConverter) ConvertWrite(hdr *tar.Header, path string, fi os // FIXME(thaJeztah): return a sentinel error instead of nil, nil return nil, nil } - delete(hdr.PAXRecords, paxSchilyXattr+opaqueXattrName) + delete(hdr.PAXRecords, paxSchilyXattr+c.opaqueXattr) // create a header for the whiteout file // it should inherit some properties from the parent, but be a regular file return &tar.Header{ Typeflag: tar.TypeReg, Mode: hdr.Mode & int64(os.ModePerm), - Name: filepath.Join(hdr.Name, WhiteoutOpaqueDir), // #nosec G305 -- An archive is being created, not extracted. + Name: path.Join(hdr.Name, WhiteoutOpaqueDir), // #nosec G305 -- An archive is being created, not extracted. Size: 0, Uid: hdr.Uid, Uname: hdr.Uname, @@ -68,40 +76,67 @@ func (overlayWhiteoutConverter) ConvertWrite(hdr *tar.Header, path string, fi os }, nil } -func (c overlayWhiteoutConverter) ConvertRead(hdr *tar.Header, path string) (bool, error) { - base := filepath.Base(path) - dir := filepath.Dir(path) +func (c overlayWhiteoutConverter) ConvertRead(root *os.Root, hdr *tar.Header, filePath string) (bool, error) { + base := filepath.Base(filePath) + dir := filepath.Dir(filePath) - // if a directory is marked as opaque by the AUFS special file, we need to translate that to overlay - if base == WhiteoutOpaqueDir { - opaqueXattrName := "trusted.overlay.opaque" - if userns.RunningInUserNS() { - opaqueXattrName = "user.overlay.opaque" + switch base { + case WhiteoutPrefix, WhiteoutPrefix + ".", WhiteoutPrefix + "..": + return false, fmt.Errorf("invalid whiteout entry %q", hdr.Name) + + case WhiteoutOpaqueDir: + parent, err := root.Open(dir) + if err != nil { + return false, err } + defer parent.Close() - err := unix.Setxattr(dir, opaqueXattrName, []byte{'y'}, 0) + // If a directory is marked as opaque by the AUFS special file, we need to translate that to overlay. + if err := unix.Fsetxattr(int(parent.Fd()), c.opaqueXattr, []byte{'y'}, 0); err != nil { + return false, fmt.Errorf("fsetxattr('%s', %s=y): %w", dir, c.opaqueXattr, err) + } + // Don't write the whiteout file itself. + return false, nil + + default: + originalBase, ok := strings.CutPrefix(base, WhiteoutPrefix) + if !ok { + // Regular file. + return true, nil + } + + parent, err := root.Open(dir) if err != nil { - return false, fmt.Errorf("setxattr('%s', %s=y): %w", dir, opaqueXattrName, err) + return false, err } - // don't write the file itself - return false, err - } + defer parent.Close() - // if a file was deleted and we are using overlay, we need to create a character device - if strings.HasPrefix(base, WhiteoutPrefix) { - originalBase := base[len(WhiteoutPrefix):] + // If a file was deleted, and we are using overlay, we need to create a character device. originalPath := filepath.Join(dir, originalBase) - - if err := unix.Mknod(originalPath, unix.S_IFCHR, 0); err != nil { + if err := unix.Mknodat(int(parent.Fd()), originalBase, unix.S_IFCHR, 0); err != nil { return false, fmt.Errorf("failed to mknod('%s', S_IFCHR, 0): %w", originalPath, err) } - if err := os.Chown(originalPath, hdr.Uid, hdr.Gid); err != nil { - return false, err + + // Header IDs have already been remapped. Optimize the common non-remapped + // root-owned (0:0) case by assuming the created whiteout has the expected + // ownership, rather than comparing against the effective UID/GID or stat'ing + // the created node to verify it. + if hdr.Uid != 0 || hdr.Gid != 0 { + // TODO(thaJeztah): Revisit whether whiteout ownership needs to be preserved. + // + // This was added in the original overlay whiteout implementation: + // https://github.com/moby/moby/pull/18560 / https://github.com/moby/moby/pull/22126 + // + // OverlayFS documents whiteouts in terms of a character device with device + // number 0:0, not ownership: https://docs.kernel.org/filesystems/overlayfs.html#whiteouts-and-opaque-directories + // + // If ownership is not required, this Fchownat can be removed to avoid the remaining TOCTOU window. + if err := unix.Fchownat(int(parent.Fd()), originalBase, hdr.Uid, hdr.Gid, unix.AT_SYMLINK_NOFOLLOW); err != nil { + return false, &os.PathError{Op: "lchown", Path: originalPath, Err: err} + } } - // don't write the file itself + // Don't write the whiteout file itself. return false, nil } - - return true, nil } diff --git a/vendor/github.com/moby/go-archive/archive_unix.go b/vendor/github.com/moby/go-archive/archive_unix.go index 3a9f5b0b5..e2dcc4290 100644 --- a/vendor/github.com/moby/go-archive/archive_unix.go +++ b/vendor/github.com/moby/go-archive/archive_unix.go @@ -5,7 +5,10 @@ package archive import ( "archive/tar" "errors" + "fmt" + "math" "os" + "path" "path/filepath" "strings" "syscall" @@ -13,6 +16,8 @@ import ( "golang.org/x/sys/unix" ) +var errInvalidArchive = errors.New("invalid archive") + // addLongPathPrefix adds the Windows long path prefix to the path provided if // it does not already have it. It is a no-op on platforms other than Windows. func addLongPathPrefix(srcPath string) string { @@ -29,20 +34,19 @@ func getWalkRoot(srcPath string, include string) string { // chmodTarEntry is used to adjust the file permissions used in tar header based // on the platform the archival is done. -func chmodTarEntry(perm os.FileMode) os.FileMode { - return perm // noop for unix as golang APIs provide perm bits correctly +func chmodTarEntry(mode int64) int64 { + return mode // noop for unix as golang APIs provide perm bits correctly } -func getInodeFromStat(stat interface{}) (uint64, error) { +func getInodeFromStat(stat any) (uint64, error) { s, ok := stat.(*syscall.Stat_t) if !ok { - // FIXME(thaJeztah): this should likely return an error; see https://github.com/moby/moby/pull/49493#discussion_r1979152897 - return 0, nil + return 0, fmt.Errorf("unexpected stat type %T", stat) } return s.Ino, nil } -func getFileUIDGID(stat interface{}) (int, int, error) { +func getFileUIDGID(stat any) (int, int, error) { s, ok := stat.(*syscall.Stat_t) if !ok { @@ -56,7 +60,7 @@ func getFileUIDGID(stat interface{}) (int, int, error) { // // Creating device nodes is not supported when running in a user namespace, // produces a [syscall.EPERM] in most cases. -func handleTarTypeBlockCharFifo(hdr *tar.Header, path string) error { +func handleTarTypeBlockCharFifo(root *os.Root, hdr *tar.Header, dstPath string) error { mode := uint32(hdr.Mode & 0o7777) switch hdr.Typeflag { case tar.TypeBlock: @@ -67,20 +71,90 @@ func handleTarTypeBlockCharFifo(hdr *tar.Header, path string) error { mode |= unix.S_IFIFO } - return mknod(path, mode, unix.Mkdev(uint32(hdr.Devmajor), uint32(hdr.Devminor))) + // Devmajor and Devminor come straight from the (untrusted) tar header as + // int64, but Mkdev only takes uint32. Casting a value that does not fit + // silently truncates it, so the node created on disk would carry a + // different major/minor than the header declares. Reject those instead of + // creating a mismatched device. + if hdr.Devmajor < 0 || hdr.Devmajor > math.MaxUint32 || + hdr.Devminor < 0 || hdr.Devminor > math.MaxUint32 { + return fmt.Errorf("device number %d:%d for %q out of range: %w", hdr.Devmajor, hdr.Devminor, hdr.Name, errInvalidArchive) + } + + // Prefer mknodat; fall back to a bounded path where unavailable. + return mknodInRoot(root, dstPath, mode, unix.Mkdev(uint32(hdr.Devmajor), uint32(hdr.Devminor))) } -func handleLChmod(hdr *tar.Header, path string, hdrInfo os.FileInfo) error { - if hdr.Typeflag == tar.TypeLink { - if fi, err := os.Lstat(hdr.Linkname); err == nil && (fi.Mode()&os.ModeSymlink == 0) { - if err := os.Chmod(path, hdrInfo.Mode()); err != nil { - return err - } - } - } else if hdr.Typeflag != tar.TypeSymlink { - if err := os.Chmod(path, hdrInfo.Mode()); err != nil { - return err +// handleLChmod applies the mode from hdrInfo to dstPath within root, skipping +// symlinks (there is no lchmod). For hardlinks, the mode is applied only when +// the link target is itself not a symlink. +func handleLChmod(root *os.Root, dstPath string, hdr *tar.Header, hdrInfo os.FileInfo) error { + switch hdr.Typeflag { + case tar.TypeSymlink: + return nil + + case tar.TypeLink: + // If the target is a symlink, there is no way to chmod the hardlink + // without following it. + fi, err := root.Lstat(filepath.FromSlash(path.Clean(hdr.Linkname))) + if err != nil || fi.Mode()&os.ModeSymlink != 0 { + return nil } + return chmodNoSymlink(root, dstPath, hdrInfo.Mode()) + + default: + return chmodNoSymlink(root, dstPath, hdrInfo.Mode()) + } +} + +// chmodNoSymlink applies mode to a non-symlink entry. +// +// Callers must have already excluded symlink entries. +func chmodNoSymlink(root *os.Root, name string, mode os.FileMode) error { + parent, err := root.OpenFile(filepath.Dir(name), os.O_RDONLY, 0) + if err != nil { + return err + } + defer parent.Close() + + base := filepath.Base(name) + perm := fileModeToPerm(mode) + // #nosec G115 -- ignore integer overflow conversion for parent.Fd + if err := unix.Fchmodat(int(parent.Fd()), base, perm, unix.AT_SYMLINK_NOFOLLOW); err == nil { + return nil + } else if !errors.Is(err, syscall.EOPNOTSUPP) { + return &os.PathError{Op: "fchmodat2", Path: name, Err: err} + } + + // Fallback for systems that cannot perform fchmodat with AT_SYMLINK_NOFOLLOW. + // Open the entry without following symlinks and apply the mode through the + // resulting file descriptor. + // #nosec G115 -- ignore integer overflow conversion for parent.Fd + fd, err := unix.Openat(int(parent.Fd()), base, unix.O_RDONLY|unix.O_NOFOLLOW|unix.O_NONBLOCK, 0) + if err != nil { + return &os.PathError{Op: "openat", Path: name, Err: err} + } + defer unix.Close(fd) + + if err := unix.Fchmod(fd, perm); err != nil { + return &os.PathError{Op: "fchmod", Path: name, Err: err} } return nil } + +// fileModeToPerm returns the subset of an os.FileMode that can be applied +// by chmod. +func fileModeToPerm(mode os.FileMode) uint32 { + perm := uint32(mode.Perm()) + + if mode&os.ModeSetuid != 0 { + perm |= unix.S_ISUID + } + if mode&os.ModeSetgid != 0 { + perm |= unix.S_ISGID + } + if mode&os.ModeSticky != 0 { + perm |= unix.S_ISVTX + } + return perm +} diff --git a/vendor/github.com/moby/go-archive/archive_windows.go b/vendor/github.com/moby/go-archive/archive_windows.go index 0e3e316af..ffe00a44d 100644 --- a/vendor/github.com/moby/go-archive/archive_windows.go +++ b/vendor/github.com/moby/go-archive/archive_windows.go @@ -33,30 +33,31 @@ func getWalkRoot(srcPath string, include string) string { // chmodTarEntry is used to adjust the file permissions used in tar header based // on the platform the archival is done. -func chmodTarEntry(perm os.FileMode) os.FileMode { +func chmodTarEntry(mode int64) int64 { // Remove group- and world-writable bits. - perm &= 0o755 + mode &= 0o755 // Add the x bit: make everything +x on Windows - return perm | 0o111 + return mode | 0o111 } -func getInodeFromStat(stat interface{}) (uint64, error) { +func getInodeFromStat(stat any) (uint64, error) { // do nothing. no notion of Inode in stat on Windows return 0, nil } // handleTarTypeBlockCharFifo is an OS-specific helper function used by // createTarFile to handle the following types of header: Block; Char; Fifo -func handleTarTypeBlockCharFifo(hdr *tar.Header, path string) error { +func handleTarTypeBlockCharFifo(root *os.Root, hdr *tar.Header, path string) error { return nil } -func handleLChmod(hdr *tar.Header, path string, hdrInfo os.FileInfo) error { +// handleLChmod is a no-op on Windows because chmod is not supported. +func handleLChmod(root *os.Root, path string, hdr *tar.Header, hdrInfo os.FileInfo) error { return nil } -func getFileUIDGID(stat interface{}) (int, int, error) { +func getFileUIDGID(stat any) (int, int, error) { // no notion of file ownership mapping yet on Windows return 0, 0, nil } diff --git a/vendor/github.com/moby/go-archive/changes.go b/vendor/github.com/moby/go-archive/changes.go index 02a0372c6..c5d647c03 100644 --- a/vendor/github.com/moby/go-archive/changes.go +++ b/vendor/github.com/moby/go-archive/changes.go @@ -7,6 +7,7 @@ import ( "fmt" "io" "io/fs" + "maps" "os" "path/filepath" "sort" @@ -217,8 +218,8 @@ func (info *FileInfo) LookUp(path string) *FileInfo { return info } - pathElements := strings.Split(path, string(os.PathSeparator)) - for _, elem := range pathElements { + pathElements := strings.SplitSeq(path, string(os.PathSeparator)) + for elem := range pathElements { if elem != "" { child := parent.children[elem] if child == nil { @@ -256,9 +257,7 @@ func (info *FileInfo) addChanges(oldInfo *FileInfo, changes *[]Change) { // otherwise any previous delete/change is considered recursive oldChildren := make(map[string]*FileInfo) if oldInfo != nil && info.isDir() { - for k, v := range oldInfo.children { - oldChildren[k] = v - } + maps.Copy(oldChildren, oldInfo.children) } for name, newChild := range info.children { @@ -401,7 +400,7 @@ func ExportChanges(dir string, changes []Change, idMap user.IdentityMapping) (io whiteOut := filepath.Join(whiteOutDir, WhiteoutPrefix+whiteOutBase) timestamp := time.Now() hdr := &tar.Header{ - Name: whiteOut[1:], + Name: strings.TrimPrefix(filepath.ToSlash(whiteOut), "/"), Size: 0, ModTime: timestamp, AccessTime: timestamp, @@ -411,9 +410,10 @@ func ExportChanges(dir string, changes []Change, idMap user.IdentityMapping) (io log.G(context.TODO()).Debugf("Can't write whiteout header: %s", err) } } else { - path := filepath.Join(dir, change.Path) - if err := ta.addTarFile(path, change.Path[1:]); err != nil { - log.G(context.TODO()).Debugf("Can't add file %s to tar: %s", path, err) + srcPath := filepath.Join(dir, change.Path) + archivePath := strings.TrimPrefix(filepath.ToSlash(change.Path), "/") + if err := ta.addTarFile(srcPath, archivePath); err != nil { + log.G(context.TODO()).Debugf("Can't add file %s to tar: %s", srcPath, err) } } } diff --git a/vendor/github.com/moby/go-archive/changes_linux.go b/vendor/github.com/moby/go-archive/changes_linux.go index 8289fe17d..39ec7475f 100644 --- a/vendor/github.com/moby/go-archive/changes_linux.go +++ b/vendor/github.com/moby/go-archive/changes_linux.go @@ -265,7 +265,7 @@ func parseDirent(buf []byte, names []nameIno) (consumed int, newnames []nameIno) } func clen(n []byte) int { - for i := 0; i < len(n); i++ { + for i := range n { if n[i] == 0 { return i } diff --git a/vendor/github.com/moby/go-archive/changes_other.go b/vendor/github.com/moby/go-archive/changes_other.go index a8a3a5a6f..7eb195d50 100644 --- a/vendor/github.com/moby/go-archive/changes_other.go +++ b/vendor/github.com/moby/go-archive/changes_other.go @@ -26,7 +26,7 @@ func collectFileInfoForChanges(oldDir, newDir string) (*FileInfo, *FileInfo, err }() // block until both routines have returned - for i := 0; i < 2; i++ { + for range 2 { if err := <-errs; err != nil { return nil, nil, err } diff --git a/vendor/github.com/moby/go-archive/compression/compression.go b/vendor/github.com/moby/go-archive/compression/compression.go index e298cefb3..824267924 100644 --- a/vendor/github.com/moby/go-archive/compression/compression.go +++ b/vendor/github.com/moby/go-archive/compression/compression.go @@ -66,7 +66,7 @@ type nopWriteCloser struct { func (nopWriteCloser) Close() error { return nil } var bufioReader32KPool = &sync.Pool{ - New: func() interface{} { return bufio.NewReaderSize(nil, 32*1024) }, + New: func() any { return bufio.NewReaderSize(nil, 32*1024) }, } type bufferedReader struct { @@ -217,7 +217,7 @@ func gzipDecompress(ctx context.Context, buf io.Reader) (io.ReadCloser, error) { log.G(ctx).Debugf("Using %s to decompress", unpigzPath) - return cmdStream(exec.CommandContext(ctx, unpigzPath, "-d", "-c"), buf) + return cmdStream(exec.CommandContext(ctx, unpigzPath, "-d", "-c"), buf) // #nosec G204 -- Subprocess launched with variable } // cmdStream executes a command, and returns its stdout as a stream. diff --git a/vendor/github.com/moby/go-archive/copy.go b/vendor/github.com/moby/go-archive/copy.go index 77d038c42..7447e8bd1 100644 --- a/vendor/github.com/moby/go-archive/copy.go +++ b/vendor/github.com/moby/go-archive/copy.go @@ -22,7 +22,7 @@ var ( ) var copyPool = sync.Pool{ - New: func() interface{} { s := make([]byte, 32*1024); return &s }, + New: func() any { s := make([]byte, 32*1024); return &s }, } func copyWithBuffer(dst io.Writer, src io.Reader) error { @@ -316,16 +316,40 @@ func PrepareArchiveCopy(srcContent io.Reader, srcInfo, dstInfo CopyInfo) (dstDir } } +// newNameRebaser returns a function that replaces oldBase with newBase at the +// beginning of POSIX-style archive entry names. It converts oldBase and newBase +// to forward-slash form and trims trailing slashes. +// +// When rebasing from the archive root, the returned function removes all +// leading slashes from names. It otherwise preserves the remainder verbatim +// and does not clean or canonicalize paths. +func newNameRebaser(oldBase, newBase string) func(string) string { + oldBase = strings.TrimRight(filepath.ToSlash(oldBase), "/") + newBase = strings.TrimRight(filepath.ToSlash(newBase), "/") + + if oldBase == "" { + return func(name string) string { + name = strings.TrimLeft(name, "/") + if newBase == "" { + return name + } + return newBase + "/" + name + } + } + + return func(name string) string { + suffix, ok := strings.CutPrefix(name, oldBase) + if !ok || suffix != "" && !strings.HasPrefix(suffix, "/") { + return name + } + return newBase + suffix + } +} + // RebaseArchiveEntries rewrites the given srcContent archive replacing // an occurrence of oldBase with newBase at the beginning of entry names. func RebaseArchiveEntries(srcContent io.Reader, oldBase, newBase string) io.ReadCloser { - if oldBase == string(os.PathSeparator) { - // If oldBase specifies the root directory, use an empty string as - // oldBase instead so that newBase doesn't replace the path separator - // that all paths will start with. - oldBase = "" - } - + rebase := newNameRebaser(oldBase, newBase) rebased, w := io.Pipe() go func() { @@ -336,12 +360,12 @@ func RebaseArchiveEntries(srcContent io.Reader, oldBase, newBase string) io.Read hdr, err := srcTar.Next() if errors.Is(err, io.EOF) { // Signals end of archive. - rebasedTar.Close() - w.Close() + _ = rebasedTar.Close() + _ = w.Close() return } if err != nil { - w.CloseWithError(err) + _ = w.CloseWithError(err) return } @@ -353,13 +377,13 @@ func RebaseArchiveEntries(srcContent io.Reader, oldBase, newBase string) io.Read // // To fix, set the format to PAX here. See docker/for-linux issue #484. hdr.Format = tar.FormatPAX - hdr.Name = strings.Replace(hdr.Name, oldBase, newBase, 1) + hdr.Name = rebase(hdr.Name) if hdr.Typeflag == tar.TypeLink { - hdr.Linkname = strings.Replace(hdr.Linkname, oldBase, newBase, 1) + hdr.Linkname = rebase(hdr.Linkname) } if err = rebasedTar.WriteHeader(hdr); err != nil { - w.CloseWithError(err) + _ = w.CloseWithError(err) return } @@ -374,7 +398,7 @@ func RebaseArchiveEntries(srcContent io.Reader, oldBase, newBase string) io.Read // not be vulnerable to this code consuming memory. //nolint:gosec // G110: Potential DoS vulnerability via decompression bomb (gosec) if _, err = io.Copy(rebasedTar, srcTar); err != nil { - w.CloseWithError(err) + _ = w.CloseWithError(err) return } } @@ -408,7 +432,7 @@ func CopyResource(srcPath, dstPath string, followLink bool) error { if err != nil { return err } - defer content.Close() + defer func() { _ = content.Close() }() return CopyTo(content, srcInfo, dstPath) } @@ -427,14 +451,12 @@ func CopyTo(content io.Reader, srcInfo CopyInfo, dstPath string) error { if err != nil { return err } - defer copyArchive.Close() + defer func() { _ = copyArchive.Close() }() - options := &TarOptions{ + return Untar(copyArchive, dstDir, &TarOptions{ NoLchown: true, NoOverwriteDirNonDir: true, - } - - return Untar(copyArchive, dstDir, options) + }) } // ResolveHostSourcePath decides real path need to be copied with parameters such as diff --git a/vendor/github.com/moby/go-archive/dev_darwin.go b/vendor/github.com/moby/go-archive/dev_darwin.go new file mode 100644 index 000000000..fcfe730ce --- /dev/null +++ b/vendor/github.com/moby/go-archive/dev_darwin.go @@ -0,0 +1,21 @@ +//go:build darwin + +package archive + +import ( + "os" + + "golang.org/x/sys/unix" +) + +func mknod(path string, mode uint32, dev uint64) error { + return unix.Mknod(path, mode, int(dev)) // #nosec G115 -- Required conversion for the platform-specific Mknod API. +} + +func mknodInRoot(root *os.Root, path string, mode uint32, dev uint64) error { + abs, err := fsRootPath(root.Name(), path) + if err != nil { + return err + } + return unix.Mknod(abs, mode, int(dev)) // #nosec G115 -- Required conversion for the platform-specific Mknod API. +} diff --git a/vendor/github.com/moby/go-archive/dev_freebsd.go b/vendor/github.com/moby/go-archive/dev_freebsd.go index b3068fce9..d18e829b0 100644 --- a/vendor/github.com/moby/go-archive/dev_freebsd.go +++ b/vendor/github.com/moby/go-archive/dev_freebsd.go @@ -2,8 +2,23 @@ package archive -import "golang.org/x/sys/unix" +import ( + "os" + "path/filepath" + + "golang.org/x/sys/unix" +) func mknod(path string, mode uint32, dev uint64) error { return unix.Mknod(path, mode, dev) } + +func mknodInRoot(root *os.Root, path string, mode uint32, dev uint64) error { + parent, err := root.OpenFile(filepath.Dir(path), os.O_RDONLY|unix.O_DIRECTORY, 0) + if err != nil { + return err + } + defer parent.Close() + + return unix.Mknodat(int(parent.Fd()), filepath.Base(path), mode, dev) +} diff --git a/vendor/github.com/moby/go-archive/dev_unix.go b/vendor/github.com/moby/go-archive/dev_unix.go index dffc596f9..0eb8b6da5 100644 --- a/vendor/github.com/moby/go-archive/dev_unix.go +++ b/vendor/github.com/moby/go-archive/dev_unix.go @@ -1,9 +1,24 @@ -//go:build !windows && !freebsd +//go:build !darwin && !freebsd && !windows package archive -import "golang.org/x/sys/unix" +import ( + "os" + "path/filepath" + + "golang.org/x/sys/unix" +) func mknod(path string, mode uint32, dev uint64) error { - return unix.Mknod(path, mode, int(dev)) + return unix.Mknod(path, mode, int(dev)) // #nosec G115 -- Required conversion for the platform-specific Mknod API. +} + +func mknodInRoot(root *os.Root, path string, mode uint32, dev uint64) error { + parent, err := root.OpenFile(filepath.Dir(path), os.O_RDONLY|unix.O_DIRECTORY, 0) + if err != nil { + return err + } + defer parent.Close() + + return unix.Mknodat(int(parent.Fd()), filepath.Base(path), mode, int(dev)) // #nosec G115 -- Required conversion for the platform-specific Mknod API. } diff --git a/vendor/github.com/moby/go-archive/diff.go b/vendor/github.com/moby/go-archive/diff.go index 96db972d1..055f3c11e 100644 --- a/vendor/github.com/moby/go-archive/diff.go +++ b/vendor/github.com/moby/go-archive/diff.go @@ -7,8 +7,8 @@ import ( "fmt" "io" "os" + "path" "path/filepath" - "runtime" "strings" "github.com/containerd/log" @@ -20,17 +20,22 @@ import ( // compressed or uncompressed. // Returns the size in bytes of the contents of the layer. func UnpackLayer(dest string, layer io.Reader, options *TarOptions) (size int64, err error) { + root, err := os.OpenRoot(dest) + if err != nil { + return 0, err + } + defer root.Close() + tr := tar.NewReader(layer) - var dirs []*tar.Header + var dirs []unpackedDir + // unpackedPaths tracks root-relative paths already written in this layer + // so that the AUFS opaque-whiteout walk knows which paths to preserve. unpackedPaths := make(map[string]struct{}) if options == nil { options = &TarOptions{} } - if options.ExcludePatterns == nil { - options.ExcludePatterns = []string{} - } aufsTempdir := "" aufsHardlinks := make(map[string]*tar.Header) @@ -48,32 +53,26 @@ func UnpackLayer(dest string, layer io.Reader, options *TarOptions) (size int64, size += hdr.Size - // Normalize name, for safety and for a simple is-root check - hdr.Name = filepath.Clean(hdr.Name) - - // Windows does not support filenames with colons in them. Ignore - // these files. This is not a problem though (although it might - // appear that it is). Let's suppose a client is running docker pull. - // The daemon it points to is Windows. Would it make sense for the - // client to be doing a docker pull Ubuntu for example (which has files - // with colons in the name under /usr/share/man/man3)? No, absolutely - // not as it would really only make sense that they were pulling a - // Windows image. However, for development, it is necessary to be able - // to pull Linux images which are in the repository. - // - // TODO Windows. Once the registry is aware of what images are Windows- - // specific or Linux-specific, this warning should be changed to an error - // to cater for the situation where someone does manage to upload a Linux - // image but have it tagged as Windows inadvertently. - if runtime.GOOS == "windows" { - if strings.Contains(hdr.Name, ":") { - log.G(context.TODO()).Warnf("Windows: Ignoring %s (is this a Linux image?)", hdr.Name) - continue - } + // Strip a leading "/" so absolute entries stay root-relative, and + // normalize the POSIX tar path. Skip entries referring to the extraction + // root and reject paths that escape it. + name := path.Clean(strings.TrimLeft(hdr.Name, "/")) + if name == "." { + continue + } + if !filepath.IsLocal(name) { + return 0, breakoutError(fmt.Errorf("invalid entry name %q", hdr.Name)) + } + hdr.Name = name + + // Skip entries whose name (or hardlink target) Windows cannot represent. + if err := unrepresentableOnWindows(hdr); err != nil { + log.G(context.TODO()).Warnf("Windows: ignoring entry: %v", err) + continue } // Ensure that the parent directory exists. - err = createImpliedDirectories(dest, hdr, options) + err = createImpliedDirectories(root, hdr, options) if err != nil { return 0, err } @@ -84,7 +83,7 @@ func UnpackLayer(dest string, layer io.Reader, options *TarOptions) (size int64, // We don't want this directory, but we need the files in them so that // such hardlinks can be resolved. if strings.HasPrefix(hdr.Name, WhiteoutLinkDir) && hdr.Typeflag == tar.TypeReg { - basename := filepath.Base(hdr.Name) + basename := path.Base(hdr.Name) aufsHardlinks[basename] = hdr if aufsTempdir == "" { if aufsTempdir, err = os.MkdirTemp(dest, "dockerplnk"); err != nil { @@ -92,47 +91,63 @@ func UnpackLayer(dest string, layer io.Reader, options *TarOptions) (size int64, } defer os.RemoveAll(aufsTempdir) } - if err := createTarFile(filepath.Join(aufsTempdir, basename), dest, hdr, tr, options); err != nil { + aufsRoot, err := os.OpenRoot(aufsTempdir) + if err != nil { return 0, err } + cerr := createTarFile(aufsRoot, basename, hdr, tr, options) + _ = aufsRoot.Close() + if cerr != nil { + return 0, cerr + } } if hdr.Name != WhiteoutOpaqueDir { continue } } - // #nosec G305 -- The joined path is guarded against path traversal. - path := filepath.Join(dest, hdr.Name) - rel, err := filepath.Rel(dest, path) - if err != nil { - return 0, err - } - - // Note as these operations are platform specific, so must the slash be. - if strings.HasPrefix(rel, ".."+string(os.PathSeparator)) { - return 0, breakoutError(fmt.Errorf("%q is outside of %q", hdr.Name, dest)) - } - base := filepath.Base(path) + // dstPath is the native (host-separator) form of the entry name, + // used at all filesystem boundaries (os.Root methods, fsRootPath). + // The tar-header name (hdr.Name) is POSIX, so convert it here. + dstPath := filepath.FromSlash(hdr.Name) + base := filepath.Base(dstPath) if strings.HasPrefix(base, WhiteoutPrefix) { - dir := filepath.Dir(path) + dir := filepath.Dir(dstPath) if base == WhiteoutOpaqueDir { - _, err := os.Lstat(dir) + _, err := root.Lstat(dir) if err != nil { return 0, err } - err = filepath.WalkDir(dir, func(path string, info os.DirEntry, err error) error { + // Walk the absolute directory so we can call os.RemoveAll on + // paths outside the walk callback's reach, then convert each + // walked path back to a root-relative name for the + // unpackedPaths check. + // fsRootPath walks each path component and bounds any symlinks + // within the root to prevent TOCTOU symlink attacks. + absDir, err := fsRootPath(root.Name(), dir) + if err != nil { + return 0, err + } + err = filepath.WalkDir(absDir, func(p string, info os.DirEntry, err error) error { if err != nil { if os.IsNotExist(err) { - err = nil // parent was deleted + return nil // parent was deleted } return err } - if path == dir { + if p == absDir { return nil } - if _, exists := unpackedPaths[path]; !exists { - return os.RemoveAll(path) + rel, err := filepath.Rel(root.Name(), p) + if err != nil { + return err + } + + // unpackedPaths is keyed by root-relative slash paths; convert + // filepath.WalkDir's native path before looking it up. + if _, exists := unpackedPaths[filepath.ToSlash(rel)]; !exists { + return root.RemoveAll(rel) } return nil }) @@ -142,18 +157,18 @@ func UnpackLayer(dest string, layer io.Reader, options *TarOptions) (size int64, } else { originalBase := base[len(WhiteoutPrefix):] originalPath := filepath.Join(dir, originalBase) - if err := os.RemoveAll(originalPath); err != nil { + if err := root.RemoveAll(originalPath); err != nil { return 0, err } } } else { - // If path exits we almost always just want to remove and replace it. + // If dstPath exists we almost always just want to remove and replace it. // The only exception is when it is a directory *and* the file from // the layer is also a directory. Then we want to merge them (i.e. // just apply the metadata from the layer). - if fi, err := os.Lstat(path); err == nil { + if fi, err := root.Lstat(dstPath); err == nil { if !fi.IsDir() || hdr.Typeflag != tar.TypeDir { - if err := os.RemoveAll(path); err != nil { + if err := root.RemoveAll(dstPath); err != nil { return 0, err } } @@ -164,8 +179,8 @@ func UnpackLayer(dest string, layer io.Reader, options *TarOptions) (size int64, // Hard links into /.wh..wh.plnk don't work, as we don't extract that directory, so // we manually retarget these into the temporary files we extracted them into - if hdr.Typeflag == tar.TypeLink && strings.HasPrefix(filepath.Clean(hdr.Linkname), WhiteoutLinkDir) { - linkBasename := filepath.Base(hdr.Linkname) + if hdr.Typeflag == tar.TypeLink && strings.HasPrefix(path.Clean(hdr.Linkname), WhiteoutLinkDir) { + linkBasename := path.Base(hdr.Linkname) srcHdr = aufsHardlinks[linkBasename] if srcHdr == nil { return 0, errors.New("invalid aufs hardlink") @@ -182,23 +197,23 @@ func UnpackLayer(dest string, layer io.Reader, options *TarOptions) (size int64, return 0, err } - if err := createTarFile(path, dest, srcHdr, srcData, options); err != nil { + if err := createTarFile(root, dstPath, srcHdr, srcData, options); err != nil { return 0, err } // Directory mtimes must be handled at the end to avoid further // file creation in them to modify the directory mtime if hdr.Typeflag == tar.TypeDir { - dirs = append(dirs, hdr) + dirs = append(dirs, unpackedDir{hdr: hdr, name: dstPath}) } - unpackedPaths[path] = struct{}{} + // unpackedPaths is keyed by the POSIX (forward-slash) name so it + // matches the ToSlash'd lookup in the opaque-whiteout walk above. + unpackedPaths[hdr.Name] = struct{}{} } } - for _, hdr := range dirs { - // #nosec G305 -- The header was checked for path traversal before it was appended to the dirs slice. - path := filepath.Join(dest, hdr.Name) - if err := chtimes(path, hdr.AccessTime, hdr.ModTime); err != nil { + for _, d := range dirs { + if err := chtimes(root, d.name, boundTime(latestTime(d.hdr.AccessTime, d.hdr.ModTime)), boundTime(d.hdr.ModTime)); err != nil { return 0, err } } diff --git a/vendor/github.com/moby/go-archive/rootpath.go b/vendor/github.com/moby/go-archive/rootpath.go new file mode 100644 index 000000000..3834af2c4 --- /dev/null +++ b/vendor/github.com/moby/go-archive/rootpath.go @@ -0,0 +1,112 @@ +/* + 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 archive + +import ( + "errors" + "os" + "path/filepath" +) + +var errTooManyLinks = errors.New("too many links") + +// fsRootPath joins a path with a root, evaluating and bounding any +// symlink to the root directory. +func fsRootPath(root, path string) (string, error) { + if path == "" { + return root, nil + } + var linksWalked int // to protect against cycles + for { + i := linksWalked + newpath, err := walkLinks(root, path, &linksWalked) + if err != nil { + return "", err + } + path = newpath + if i == linksWalked { + newpath = filepath.Join(string(os.PathSeparator), newpath) + if path == newpath { + return filepath.Join(root, newpath), nil + } + path = newpath + } + } +} + +func walkLink(root, path string, linksWalked *int) (newpath string, islink bool, err error) { + if *linksWalked > 255 { + return "", false, errTooManyLinks + } + + path = filepath.Join(string(os.PathSeparator), path) + if path == string(os.PathSeparator) { + return path, false, nil + } + realPath := filepath.Join(root, path) + + fi, err := os.Lstat(realPath) + if err != nil { + // If path does not yet exist, treat as non-symlink + if os.IsNotExist(err) { + return path, false, nil + } + return "", false, err + } + if fi.Mode()&os.ModeSymlink == 0 { + return path, false, nil + } + newpath, err = os.Readlink(realPath) + if err != nil { + return "", false, err + } + *linksWalked++ + return newpath, true, nil +} + +func walkLinks(root, path string, linksWalked *int) (string, error) { + switch dir, file := filepath.Split(path); { + case dir == "": + newpath, _, err := walkLink(root, file, linksWalked) + return newpath, err + case file == "": + if os.IsPathSeparator(dir[len(dir)-1]) { + if dir == string(os.PathSeparator) { + return dir, nil + } + return walkLinks(root, dir[:len(dir)-1], linksWalked) + } + newpath, _, err := walkLink(root, dir, linksWalked) + return newpath, err + default: + newdir, err := walkLinks(root, dir, linksWalked) + if err != nil { + return "", err + } + newpath, islink, err := walkLink(root, filepath.Join(newdir, file), linksWalked) + if err != nil { + return "", err + } + if !islink { + return newpath, nil + } + if filepath.IsAbs(newpath) { + return newpath, nil + } + return filepath.Join(newdir, newpath), nil + } +} diff --git a/vendor/github.com/moby/go-archive/sequential_other.go b/vendor/github.com/moby/go-archive/sequential_other.go new file mode 100644 index 000000000..90edb1393 --- /dev/null +++ b/vendor/github.com/moby/go-archive/sequential_other.go @@ -0,0 +1,6 @@ +//go:build !windows + +package archive + +// windows_O_FILE_FLAG_SEQUENTIAL_SCAN is not supported on go < 1.26. +const windows_O_FILE_FLAG_SEQUENTIAL_SCAN = 0 diff --git a/vendor/github.com/moby/go-archive/sequential_windows_go126.go b/vendor/github.com/moby/go-archive/sequential_windows_go126.go new file mode 100644 index 000000000..1e80d11b7 --- /dev/null +++ b/vendor/github.com/moby/go-archive/sequential_windows_go126.go @@ -0,0 +1,9 @@ +//go:build windows && go1.26 + +package archive + +// windows_O_FILE_FLAG_SEQUENTIAL_SCAN matches [golang.org/x/sys/windows.O_FILE_FLAG_SEQUENTIAL_SCAN]. +// Starting in Go 1.26, os.OpenFile supports passing this flag through. +// +// TODO(thaJeztah): use windows.O_FILE_FLAG_SEQUENTIAL_SCAN once we drop Go <1.26. +const windows_O_FILE_FLAG_SEQUENTIAL_SCAN = 0x08000000 diff --git a/vendor/github.com/moby/go-archive/sequential_windows_pre126.go b/vendor/github.com/moby/go-archive/sequential_windows_pre126.go new file mode 100644 index 000000000..2b281741a --- /dev/null +++ b/vendor/github.com/moby/go-archive/sequential_windows_pre126.go @@ -0,0 +1,6 @@ +//go:build windows && !go1.26 + +package archive + +// windows_O_FILE_FLAG_SEQUENTIAL_SCAN is not supported on go < 1.26. +const windows_O_FILE_FLAG_SEQUENTIAL_SCAN = 0 diff --git a/vendor/github.com/moby/go-archive/tarheader/tarheader.go b/vendor/github.com/moby/go-archive/tarheader/tarheader.go index 03732a4f8..78ab55cd4 100644 --- a/vendor/github.com/moby/go-archive/tarheader/tarheader.go +++ b/vendor/github.com/moby/go-archive/tarheader/tarheader.go @@ -32,7 +32,7 @@ func (fi nosysFileInfo) Gname() (string, error) { return "", nil } -func (fi nosysFileInfo) Sys() interface{} { +func (fi nosysFileInfo) Sys() any { // A Sys value of type *tar.Header is safe as it is system-independent. // The tar.FileInfoHeader function copies the fields into the returned // header without performing any OS lookups. diff --git a/vendor/github.com/moby/go-archive/tarheader/tarheader_unix.go b/vendor/github.com/moby/go-archive/tarheader/tarheader_unix.go index 9c3311c63..bd12d46a8 100644 --- a/vendor/github.com/moby/go-archive/tarheader/tarheader_unix.go +++ b/vendor/github.com/moby/go-archive/tarheader/tarheader_unix.go @@ -36,10 +36,11 @@ func sysStat(fi os.FileInfo, hdr *tar.Header) error { hdr.Uid = int(s.Uid) hdr.Gid = int(s.Gid) - if s.Mode&unix.S_IFBLK != 0 || - s.Mode&unix.S_IFCHR != 0 { - hdr.Devmajor = int64(unix.Major(uint64(s.Rdev))) //nolint: unconvert - hdr.Devminor = int64(unix.Minor(uint64(s.Rdev))) //nolint: unconvert + if s.Mode&unix.S_IFBLK != 0 || s.Mode&unix.S_IFCHR != 0 { + // #nosec G115 -- Rdev type varies by platform. + rdev := uint64(s.Rdev) //nolint:unconvert // Rdev type varies by platform. + hdr.Devmajor = int64(unix.Major(rdev)) + hdr.Devminor = int64(unix.Minor(rdev)) } return nil diff --git a/vendor/github.com/moby/go-archive/time.go b/vendor/github.com/moby/go-archive/time.go index 4e9ae9508..fb8c0299b 100644 --- a/vendor/github.com/moby/go-archive/time.go +++ b/vendor/github.com/moby/go-archive/time.go @@ -22,6 +22,10 @@ func init() { } } +// boundTime returns t if it falls within the range supported by os.Chtimes. +// Times before the Unix epoch (minTime) or after the end of Unix time +// (maxTime) are replaced with minTime, as os.Chtimes has undefined behavior +// outside that range. func boundTime(t time.Time) time.Time { if t.Before(minTime) || t.After(maxTime) { return minTime diff --git a/vendor/github.com/moby/go-archive/time_nonwindows.go b/vendor/github.com/moby/go-archive/time_nonwindows.go index 5bfdfa2f1..418bc887a 100644 --- a/vendor/github.com/moby/go-archive/time_nonwindows.go +++ b/vendor/github.com/moby/go-archive/time_nonwindows.go @@ -3,18 +3,55 @@ package archive import ( + "errors" "os" + "path" + "path/filepath" + "strings" + "syscall" "time" "golang.org/x/sys/unix" ) -// chtimes changes the access time and modified time of a file at the given path. -// If the modified time is prior to the Unix Epoch (unixMinTime), or after the -// end of Unix Time (unixEpochTime), os.Chtimes has undefined behavior. In this -// case, Chtimes defaults to Unix Epoch, just in case. -func chtimes(name string, atime time.Time, mtime time.Time) error { - return os.Chtimes(name, atime, mtime) +// chtimes changes the access and modification time of a file at the given +// path relative to root. +// +// Callers must use boundTime to ensure timestamps are within the range +// supported by os.Chtimes. +func chtimes(root *os.Root, name string, atime, mtime time.Time) error { + return root.Chtimes(name, atime, mtime) +} + +func lchtimes(root *os.Root, name string, atime, mtime time.Time) error { + dir, base := path.Split(filepath.ToSlash(name)) + if base == "" { + return &os.PathError{Op: "lchtimes", Path: name, Err: syscall.EINVAL} + } + + dir = strings.TrimSuffix(dir, "/") + if dir == "" { + dir = "." + } + + parent, err := root.Open(dir) + if err != nil { + return err + } + defer parent.Close() + + utimes := [2]unix.Timespec{ + timeToTimespec(atime), + timeToTimespec(mtime), + } + // #nosec G115 -- ignore integer overflow conversion for parent.Fd + if err := unix.UtimesNanoAt(int(parent.Fd()), base, utimes[:], unix.AT_SYMLINK_NOFOLLOW); err != nil { + if errors.Is(err, unix.ENOSYS) { + return nil + } + return &os.PathError{Op: "lchtimes", Path: name, Err: err} + } + return nil } func timeToTimespec(time time.Time) unix.Timespec { @@ -27,15 +64,3 @@ func timeToTimespec(time time.Time) unix.Timespec { } return unix.NsecToTimespec(time.UnixNano()) } - -func lchtimes(name string, atime time.Time, mtime time.Time) error { - utimes := [2]unix.Timespec{ - timeToTimespec(atime), - timeToTimespec(mtime), - } - err := unix.UtimesNanoAt(unix.AT_FDCWD, name, utimes[0:], unix.AT_SYMLINK_NOFOLLOW) - if err != nil && err != unix.ENOSYS { - return err - } - return err -} diff --git a/vendor/github.com/moby/go-archive/time_windows.go b/vendor/github.com/moby/go-archive/time_windows.go index af1f7c8f3..66173c58b 100644 --- a/vendor/github.com/moby/go-archive/time_windows.go +++ b/vendor/github.com/moby/go-archive/time_windows.go @@ -1,32 +1,111 @@ package archive import ( + "errors" "os" + "path/filepath" "time" + "unsafe" "golang.org/x/sys/windows" ) -func chtimes(name string, atime time.Time, mtime time.Time) error { - if err := os.Chtimes(name, atime, mtime); err != nil { +// chtimes changes the access and modification time of a file at the given +// path relative to root. +// +// Symlink entries are handled separately through lchtimes. The final path +// component is expected not to be a reparse point; if one is encountered, +// chtimes returns an error. +// +// Callers must use boundTime to ensure timestamps are within the range +// supported by os.Chtimes. +func chtimes(root *os.Root, name string, atime, mtime time.Time) error { + parent, err := root.OpenFile(filepath.Dir(name), os.O_RDONLY, 0) + if err != nil { return err } + defer parent.Close() + + // Symlink entries are handled by lchtimes. The destination for all + // chtimes callers is therefore expected not to be a reparse point. + // + // Do not follow the final component: if it was concurrently replaced + // with a reparse point, fail instead of updating its target. + return chtimesAt(parent, filepath.Base(name), atime, mtime, true) +} - pathp, err := windows.UTF16PtrFromString(name) +func lchtimes(root *os.Root, name string, atime time.Time, mtime time.Time) error { + return nil +} + +func chtimesAt(parent *os.File, name string, atime, mtime time.Time, noFollow bool) error { + h, err := openForWriteAttributesAt(windows.Handle(parent.Fd()), name, noFollow) if err != nil { + if noFollow && errors.Is(err, windows.STATUS_REPARSE_POINT_ENCOUNTERED) { + // Encountering a reparse point when noFollow is requested is unexpected. + // Treat it as a potential breakout to fail extraction safely. + return breakoutError(err) + } return err } - h, err := windows.CreateFile(pathp, - windows.FILE_WRITE_ATTRIBUTES, windows.FILE_SHARE_WRITE, nil, - windows.OPEN_EXISTING, windows.FILE_FLAG_BACKUP_SEMANTICS, 0) + defer func() { _ = windows.Close(h) }() + + var ( + creationTime = windows.NsecToFiletime(mtime.UnixNano()) + accessTime = windows.NsecToFiletime(atime.UnixNano()) + modificationTime = windows.NsecToFiletime(mtime.UnixNano()) + ) + return windows.SetFileTime(h, &creationTime, &accessTime, &modificationTime) +} + +// openForWriteAttributesAt opens name relative to parent with permission to +// modify its file attributes. If noFollow is true, it does not follow reparse +// points. +// +// This implementation is based on Go's internal Windows Openat support: +// +// https://github.com/golang/go/blob/go1.26.0/src/internal/syscall/windows/at_windows.go +// +// It is used by os.Root's Windows implementation for root-relative filesystem +// operations: +// +// https://github.com/golang/go/blob/go1.26.0/src/os/root_windows.go +// +// Keep this implementation aligned with the upstream code until an equivalent +// operation is available from golang.org/x/sys/windows. +func openForWriteAttributesAt(parent windows.Handle, name string, noFollow bool) (windows.Handle, error) { + name16, err := windows.UTF16FromString(name) if err != nil { - return err + return windows.InvalidHandle, err } - defer windows.Close(h) - c := windows.NsecToFiletime(mtime.UnixNano()) - return windows.SetFileTime(h, &c, nil, nil) -} -func lchtimes(name string, atime time.Time, mtime time.Time) error { - return nil + attrs := uint32(windows.OBJ_CASE_INSENSITIVE) + if noFollow { + attrs |= windows.OBJ_DONT_REPARSE + } + + var handle windows.Handle + err = windows.NtCreateFile( + &handle, + windows.SYNCHRONIZE|windows.FILE_WRITE_ATTRIBUTES, + &windows.OBJECT_ATTRIBUTES{ + Length: uint32(unsafe.Sizeof(windows.OBJECT_ATTRIBUTES{})), + RootDirectory: parent, + ObjectName: &windows.NTUnicodeString{ + Length: uint16((len(name16) - 1) * 2), // #nosec G115 -- Length is USHORT by definition. A Windows path component cannot exceed uint16 bytes. + MaximumLength: uint16(len(name16) * 2), // #nosec G115 -- MaximumLength is USHORT by definition. A Windows path component cannot exceed uint16 bytes. + Buffer: &name16[0], + }, + Attributes: attrs, + }, + &windows.IO_STATUS_BLOCK{}, + nil, + windows.FILE_ATTRIBUTE_NORMAL, + windows.FILE_SHARE_READ|windows.FILE_SHARE_WRITE|windows.FILE_SHARE_DELETE, + windows.FILE_OPEN, + windows.FILE_OPEN_FOR_BACKUP_INTENT|windows.FILE_SYNCHRONOUS_IO_NONALERT, + 0, // EA buffer + 0, // EA length + ) + return handle, err } diff --git a/vendor/github.com/moby/go-archive/xattr_supported.go b/vendor/github.com/moby/go-archive/xattr_supported.go index 652a1f0f3..8478fe144 100644 --- a/vendor/github.com/moby/go-archive/xattr_supported.go +++ b/vendor/github.com/moby/go-archive/xattr_supported.go @@ -13,26 +13,26 @@ import ( // lgetxattr retrieves the value of the extended attribute identified by attr // and associated with the given path in the file system. // It returns a nil slice and nil error if the xattr is not set. -func lgetxattr(path string, attr string) ([]byte, error) { +func lgetxattr(filePath string, attr string) ([]byte, error) { // Start with a 128 length byte array dest := make([]byte, 128) - sz, err := unix.Lgetxattr(path, attr, dest) + sz, err := unix.Lgetxattr(filePath, attr, dest) for errors.Is(err, unix.ERANGE) { // Buffer too small, use zero-sized buffer to get the actual size - sz, err = unix.Lgetxattr(path, attr, []byte{}) + sz, err = unix.Lgetxattr(filePath, attr, []byte{}) if err != nil { - return nil, wrapPathError("lgetxattr", path, attr, err) + return nil, wrapPathError("lgetxattr", filePath, attr, err) } dest = make([]byte, sz) - sz, err = unix.Lgetxattr(path, attr, dest) + sz, err = unix.Lgetxattr(filePath, attr, dest) } if err != nil { if errors.Is(err, noattr) { return nil, nil } - return nil, wrapPathError("lgetxattr", path, attr, err) + return nil, wrapPathError("lgetxattr", filePath, attr, err) } return dest[:sz], nil @@ -40,13 +40,13 @@ func lgetxattr(path string, attr string) ([]byte, error) { // lsetxattr sets the value of the extended attribute identified by attr // and associated with the given path in the file system. -func lsetxattr(path string, attr string, data []byte, flags int) error { - return wrapPathError("lsetxattr", path, attr, unix.Lsetxattr(path, attr, data, flags)) +func lsetxattr(filePath string, attr string, data []byte, flags int) error { + return wrapPathError("lsetxattr", filePath, attr, unix.Lsetxattr(filePath, attr, data, flags)) } -func wrapPathError(op, path, attr string, err error) error { +func wrapPathError(op, filePath, attr string, err error) error { if err == nil { return nil } - return &fs.PathError{Op: op, Path: path, Err: fmt.Errorf("xattr %q: %w", attr, err)} + return &fs.PathError{Op: op, Path: filePath, Err: fmt.Errorf("xattr %q: %w", attr, err)} } diff --git a/vendor/github.com/moby/sys/sequential/sequential_unix.go b/vendor/github.com/moby/sys/sequential/sequential_unix.go index 278cdfb07..24c7c4be4 100644 --- a/vendor/github.com/moby/sys/sequential/sequential_unix.go +++ b/vendor/github.com/moby/sys/sequential/sequential_unix.go @@ -1,5 +1,4 @@ //go:build !windows -// +build !windows package sequential diff --git a/vendor/github.com/moby/sys/sequential/sequential_windows.go b/vendor/github.com/moby/sys/sequential/sequential_windows.go index 3500ecc68..a8bfc3265 100644 --- a/vendor/github.com/moby/sys/sequential/sequential_windows.go +++ b/vendor/github.com/moby/sys/sequential/sequential_windows.go @@ -1,3 +1,5 @@ +//go:build windows + package sequential import ( @@ -6,108 +8,36 @@ import ( "strconv" "sync" "time" - "unsafe" - - "golang.org/x/sys/windows" ) // Create is a copy of [os.Create], modified to use sequential file access. // -// It uses [windows.FILE_FLAG_SEQUENTIAL_SCAN] rather than [windows.FILE_ATTRIBUTE_NORMAL] -// as implemented in golang. Refer to the [Win32 API documentation] for details -// on sequential file access. +// It uses the Windows sequential scan file flag. Refer to the [Win32 API +// documentation] for details on sequential file access. // // [Win32 API documentation]: https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilea#FILE_FLAG_SEQUENTIAL_SCAN func Create(name string) (*os.File, error) { - return openFileSequential(name, windows.O_RDWR|windows.O_CREAT|windows.O_TRUNC) + return openFileSequential(name, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o666) } // Open is a copy of [os.Open], modified to use sequential file access. // -// It uses [windows.FILE_FLAG_SEQUENTIAL_SCAN] rather than [windows.FILE_ATTRIBUTE_NORMAL] -// as implemented in golang. Refer to the [Win32 API documentation] for details -// on sequential file access. +// It uses the Windows sequential scan file flag. Refer to the [Win32 API +// documentation] for details on sequential file access. // // [Win32 API documentation]: https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilea#FILE_FLAG_SEQUENTIAL_SCAN func Open(name string) (*os.File, error) { - return openFileSequential(name, windows.O_RDONLY) + return openFileSequential(name, os.O_RDONLY, 0) } // OpenFile is a copy of [os.OpenFile], modified to use sequential file access. // -// It uses [windows.FILE_FLAG_SEQUENTIAL_SCAN] rather than [windows.FILE_ATTRIBUTE_NORMAL] -// as implemented in golang. Refer to the [Win32 API documentation] for details -// on sequential file access. +// It uses the Windows sequential scan file flag. Refer to the [Win32 API +// documentation] for details on sequential file access. // // [Win32 API documentation]: https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilea#FILE_FLAG_SEQUENTIAL_SCAN -func OpenFile(name string, flag int, _ os.FileMode) (*os.File, error) { - return openFileSequential(name, flag) -} - -func openFileSequential(name string, flag int) (file *os.File, err error) { - if name == "" { - return nil, &os.PathError{Op: "open", Path: name, Err: windows.ERROR_FILE_NOT_FOUND} - } - r, e := openSequential(name, flag|windows.O_CLOEXEC) - if e != nil { - return nil, &os.PathError{Op: "open", Path: name, Err: e} - } - return os.NewFile(uintptr(r), name), nil -} - -func makeInheritSa() *windows.SecurityAttributes { - var sa windows.SecurityAttributes - sa.Length = uint32(unsafe.Sizeof(sa)) - sa.InheritHandle = 1 - return &sa -} - -func openSequential(path string, mode int) (fd windows.Handle, err error) { - if len(path) == 0 { - return windows.InvalidHandle, windows.ERROR_FILE_NOT_FOUND - } - pathp, err := windows.UTF16PtrFromString(path) - if err != nil { - return windows.InvalidHandle, err - } - var access uint32 - switch mode & (windows.O_RDONLY | windows.O_WRONLY | windows.O_RDWR) { - case windows.O_RDONLY: - access = windows.GENERIC_READ - case windows.O_WRONLY: - access = windows.GENERIC_WRITE - case windows.O_RDWR: - access = windows.GENERIC_READ | windows.GENERIC_WRITE - } - if mode&windows.O_CREAT != 0 { - access |= windows.GENERIC_WRITE - } - if mode&windows.O_APPEND != 0 { - access &^= windows.GENERIC_WRITE - access |= windows.FILE_APPEND_DATA - } - sharemode := uint32(windows.FILE_SHARE_READ | windows.FILE_SHARE_WRITE) - var sa *windows.SecurityAttributes - if mode&windows.O_CLOEXEC == 0 { - sa = makeInheritSa() - } - var createmode uint32 - switch { - case mode&(windows.O_CREAT|windows.O_EXCL) == (windows.O_CREAT | windows.O_EXCL): - createmode = windows.CREATE_NEW - case mode&(windows.O_CREAT|windows.O_TRUNC) == (windows.O_CREAT | windows.O_TRUNC): - createmode = windows.CREATE_ALWAYS - case mode&windows.O_CREAT == windows.O_CREAT: - createmode = windows.OPEN_ALWAYS - case mode&windows.O_TRUNC == windows.O_TRUNC: - createmode = windows.TRUNCATE_EXISTING - default: - createmode = windows.OPEN_EXISTING - } - // Use FILE_FLAG_SEQUENTIAL_SCAN rather than FILE_ATTRIBUTE_NORMAL as implemented in golang. - // https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilea#FILE_FLAG_SEQUENTIAL_SCAN - h, e := windows.CreateFile(pathp, access, sharemode, sa, createmode, windows.FILE_FLAG_SEQUENTIAL_SCAN, 0) - return h, e +func OpenFile(name string, flag int, perm os.FileMode) (*os.File, error) { + return openFileSequential(name, flag, perm) } // Helpers for CreateTemp @@ -134,9 +64,8 @@ func nextSuffix() string { // CreateTemp is a copy of [os.CreateTemp], modified to use sequential file access. // -// It uses [windows.FILE_FLAG_SEQUENTIAL_SCAN] rather than [windows.FILE_ATTRIBUTE_NORMAL] -// as implemented in golang. Refer to the [Win32 API documentation] for details -// on sequential file access. +// It uses the Windows sequential scan file flag. Refer to the [Win32 API +// documentation] for details on sequential file access. // // [Win32 API documentation]: https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilea#FILE_FLAG_SEQUENTIAL_SCAN func CreateTemp(dir, prefix string) (f *os.File, err error) { @@ -145,9 +74,9 @@ func CreateTemp(dir, prefix string) (f *os.File, err error) { } nconflict := 0 - for i := 0; i < 10000; i++ { + for range 10000 { name := filepath.Join(dir, prefix+nextSuffix()) - f, err = openFileSequential(name, windows.O_RDWR|windows.O_CREAT|windows.O_EXCL) + f, err = openFileSequential(name, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0o600) if os.IsExist(err) { if nconflict++; nconflict > 10 { randmu.Lock() diff --git a/vendor/github.com/moby/sys/sequential/sequential_windows_go126.go b/vendor/github.com/moby/sys/sequential/sequential_windows_go126.go new file mode 100644 index 000000000..926c4201d --- /dev/null +++ b/vendor/github.com/moby/sys/sequential/sequential_windows_go126.go @@ -0,0 +1,13 @@ +//go:build windows && go1.26 + +package sequential + +import ( + "os" + + "golang.org/x/sys/windows" +) + +func openFileSequential(name string, flag int, perm os.FileMode) (*os.File, error) { + return os.OpenFile(name, flag|windows.O_FILE_FLAG_SEQUENTIAL_SCAN, perm) +} diff --git a/vendor/github.com/moby/sys/sequential/sequential_windows_pre126.go b/vendor/github.com/moby/sys/sequential/sequential_windows_pre126.go new file mode 100644 index 000000000..fa8129cf2 --- /dev/null +++ b/vendor/github.com/moby/sys/sequential/sequential_windows_pre126.go @@ -0,0 +1,76 @@ +//go:build windows && !go1.26 + +package sequential + +import ( + "os" + "unsafe" + + "golang.org/x/sys/windows" +) + +func openFileSequential(name string, flag int, _ os.FileMode) (file *os.File, err error) { + if name == "" { + return nil, &os.PathError{Op: "open", Path: name, Err: windows.ERROR_FILE_NOT_FOUND} + } + r, e := openSequential(name, flag|windows.O_CLOEXEC) + if e != nil { + return nil, &os.PathError{Op: "open", Path: name, Err: e} + } + return os.NewFile(uintptr(r), name), nil +} + +func makeInheritSa() *windows.SecurityAttributes { + var sa windows.SecurityAttributes + sa.Length = uint32(unsafe.Sizeof(sa)) + sa.InheritHandle = 1 + return &sa +} + +func openSequential(path string, mode int) (fd windows.Handle, err error) { + if len(path) == 0 { + return windows.InvalidHandle, windows.ERROR_FILE_NOT_FOUND + } + pathp, err := windows.UTF16PtrFromString(path) + if err != nil { + return windows.InvalidHandle, err + } + var access uint32 + switch mode & (windows.O_RDONLY | windows.O_WRONLY | windows.O_RDWR) { + case windows.O_RDONLY: + access = windows.GENERIC_READ + case windows.O_WRONLY: + access = windows.GENERIC_WRITE + case windows.O_RDWR: + access = windows.GENERIC_READ | windows.GENERIC_WRITE + } + if mode&windows.O_CREAT != 0 { + access |= windows.GENERIC_WRITE + } + if mode&windows.O_APPEND != 0 { + access &^= windows.GENERIC_WRITE + access |= windows.FILE_APPEND_DATA + } + sharemode := uint32(windows.FILE_SHARE_READ | windows.FILE_SHARE_WRITE) + var sa *windows.SecurityAttributes + if mode&windows.O_CLOEXEC == 0 { + sa = makeInheritSa() + } + var createmode uint32 + switch { + case mode&(windows.O_CREAT|windows.O_EXCL) == (windows.O_CREAT | windows.O_EXCL): + createmode = windows.CREATE_NEW + case mode&(windows.O_CREAT|windows.O_TRUNC) == (windows.O_CREAT | windows.O_TRUNC): + createmode = windows.CREATE_ALWAYS + case mode&windows.O_CREAT == windows.O_CREAT: + createmode = windows.OPEN_ALWAYS + case mode&windows.O_TRUNC == windows.O_TRUNC: + createmode = windows.TRUNCATE_EXISTING + default: + createmode = windows.OPEN_EXISTING + } + // Use FILE_FLAG_SEQUENTIAL_SCAN rather than FILE_ATTRIBUTE_NORMAL as implemented in golang. + // https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilea#FILE_FLAG_SEQUENTIAL_SCAN + h, e := windows.CreateFile(pathp, access, sharemode, sa, createmode, windows.FILE_FLAG_SEQUENTIAL_SCAN, 0) + return h, e +} diff --git a/vendor/github.com/moby/sys/user/lookup_unix.go b/vendor/github.com/moby/sys/user/lookup_unix.go index f95c1409f..4540747d2 100644 --- a/vendor/github.com/moby/sys/user/lookup_unix.go +++ b/vendor/github.com/moby/sys/user/lookup_unix.go @@ -1,5 +1,4 @@ //go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris -// +build darwin dragonfly freebsd linux netbsd openbsd solaris package user diff --git a/vendor/github.com/moby/sys/user/user.go b/vendor/github.com/moby/sys/user/user.go index 198c49367..857711ebf 100644 --- a/vendor/github.com/moby/sys/user/user.go +++ b/vendor/github.com/moby/sys/user/user.go @@ -56,11 +56,11 @@ type IDMap struct { Count int64 } -func parseLine(line []byte, v ...interface{}) { +func parseLine(line []byte, v ...any) { parseParts(bytes.Split(line, []byte(":")), v...) } -func parseParts(parts [][]byte, v ...interface{}) { +func parseParts(parts [][]byte, v ...any) { if len(parts) == 0 { return } @@ -97,12 +97,7 @@ func parseParts(parts [][]byte, v ...interface{}) { } func ParsePasswdFile(path string) ([]User, error) { - passwd, err := os.Open(path) - if err != nil { - return nil, err - } - defer passwd.Close() - return ParsePasswd(passwd) + return ParsePasswdFileFilter(path, nil) } func ParsePasswd(passwd io.Reader) ([]User, error) { @@ -154,13 +149,7 @@ func ParsePasswdFilter(r io.Reader, filter func(User) bool) ([]User, error) { } func ParseGroupFile(path string) ([]Group, error) { - group, err := os.Open(path) - if err != nil { - return nil, err - } - - defer group.Close() - return ParseGroup(group) + return ParseGroupFileFilter(path, nil) } func ParseGroup(group io.Reader) ([]Group, error) { @@ -168,7 +157,7 @@ func ParseGroup(group io.Reader) ([]Group, error) { } func ParseGroupFileFilter(path string, filter func(Group) bool) ([]Group, error) { - group, err := os.Open(path) + group, err := openUserFile(path) if err != nil { return nil, err } @@ -180,52 +169,22 @@ func ParseGroupFilter(r io.Reader, filter func(Group) bool) ([]Group, error) { if r == nil { return nil, errors.New("nil source for group-formatted data") } - rd := bufio.NewReader(r) - out := []Group{} - - // Read the file line-by-line. - for { - var ( - isPrefix bool - wholeLine []byte - err error - ) - - // Read the next line. We do so in chunks (as much as reader's - // buffer is able to keep), check if we read enough columns - // already on each step and store final result in wholeLine. - for { - var line []byte - line, isPrefix, err = rd.ReadLine() - if err != nil { - // We should return no error if EOF is reached - // without a match. - if err == io.EOF { - err = nil - } - return out, err - } - - // Simple common case: line is short enough to fit in a - // single reader's buffer. - if !isPrefix && len(wholeLine) == 0 { - wholeLine = line - break - } - wholeLine = append(wholeLine, line...) + var ( + s = bufio.NewScanner(r) + out = []Group{} + ) - // Check if we read the whole line already. - if !isPrefix { - break - } - } + // A group's user_list may be arbitrarily long, so allow lines that are + // much larger than bufio.Scanner's default maximum token size (64 KiB). + s.Buffer(nil, 1024*1024) + for s.Scan() { // There's no spec for /etc/passwd or /etc/group, but we try to follow // the same rules as the glibc parser, which allows comments and blank // space at the beginning of a line. - wholeLine = bytes.TrimSpace(wholeLine) - if len(wholeLine) == 0 || wholeLine[0] == '#' { + line := bytes.TrimSpace(s.Bytes()) + if len(line) == 0 || line[0] == '#' { continue } @@ -235,12 +194,17 @@ func ParseGroupFilter(r io.Reader, filter func(Group) bool) ([]Group, error) { // root:x:0:root // adm:x:4:root,adm,daemon p := Group{} - parseLine(wholeLine, &p.Name, &p.Pass, &p.Gid, &p.List) + parseLine(line, &p.Name, &p.Pass, &p.Gid, &p.List) if filter == nil || filter(p) { out = append(out, p) } } + if err := s.Err(); err != nil { + return nil, err + } + + return out, nil } type ExecUser struct { @@ -257,12 +221,12 @@ type ExecUser struct { func GetExecUserPath(userSpec string, defaults *ExecUser, passwdPath, groupPath string) (*ExecUser, error) { var passwd, group io.Reader - if passwdFile, err := os.Open(passwdPath); err == nil { + if passwdFile, err := openUserFile(passwdPath); err == nil { passwd = passwdFile defer passwdFile.Close() } - if groupFile, err := os.Open(groupPath); err == nil { + if groupFile, err := openUserFile(groupPath); err == nil { group = groupFile defer groupFile.Close() } @@ -270,6 +234,38 @@ func GetExecUserPath(userSpec string, defaults *ExecUser, passwdPath, groupPath return GetExecUser(userSpec, defaults, passwd, group) } +// parseNumeric parses the given UID or GID value to an integer and within +// the minID - maxID range. +// +// While the Linux kernel allows the max UID to be MaxUint32 - 2, +// and the OCI Runtime Spec has no definition about the max UID, we require +// the UID to be <= MaxInt32. +// +// See https://github.com/containerd/containerd/commit/de1341c201ffb0effebbf51d00376181968c8779 +func parseNumeric(val string) (int, bool, error) { + if val == "" { + return 0, false, nil + } + id, err := strconv.Atoi(val) + if err != nil { + if errors.Is(err, strconv.ErrSyntax) { + // Discard the error, because non-numeric values are expected + // when passing a username or group-name. + return 0, false, nil + } + if errors.Is(err, strconv.ErrRange) { + return 0, true, ErrRange + } + // Other errors ("invalid base", "invalid bit size"); should never + // happen with strconv.Atoi. + return 0, false, err + } + if id < minID || id > maxID { + return 0, true, ErrRange + } + return id, true, nil +} + // GetExecUser parses a user specification string (using the passwd and group // readers as sources for /etc/passwd and /etc/group data, respectively). In // the case of blank fields or missing data from the sources, the values in @@ -315,8 +311,14 @@ func GetExecUser(userSpec string, defaults *ExecUser, passwd, group io.Reader) ( // Convert userArg and groupArg to be numeric, so we don't have to execute // Atoi *twice* for each iteration over lines. - uidArg, uidErr := strconv.Atoi(userArg) - gidArg, gidErr := strconv.Atoi(groupArg) + uidArg, isUID, err := parseNumeric(userArg) + if err != nil { + return nil, err + } + gidArg, isGID, err := parseNumeric(groupArg) + if err != nil { + return nil, err + } // Find the matching user. users, err := ParsePasswdFilter(passwd, func(u User) bool { @@ -325,8 +327,8 @@ func GetExecUser(userSpec string, defaults *ExecUser, passwd, group io.Reader) ( return u.Uid == user.Uid } - if uidErr == nil { - // If the userArg is numeric, always treat it as a UID. + if isUID { + // If the userArg is a valid numeric value, always treat it as a UID. return uidArg == u.Uid } @@ -352,18 +354,11 @@ func GetExecUser(userSpec string, defaults *ExecUser, passwd, group io.Reader) ( // If we can't find a user with the given username, the only other valid // option is if it's a numeric username with no associated entry in passwd. - if uidErr != nil { + if !isUID { // Not numeric. return nil, fmt.Errorf("unable to find user %s: %w", userArg, ErrNoPasswdEntries) } user.Uid = uidArg - - // Must be inside valid uid range. - if user.Uid < minID || user.Uid > maxID { - return nil, ErrRange - } - - // Okay, so it's numeric. We can just roll with this. } // On to the groups. If we matched a username, we need to do this because of @@ -381,7 +376,7 @@ func GetExecUser(userSpec string, defaults *ExecUser, passwd, group io.Reader) ( return false } - if gidErr == nil { + if isGID { // If the groupArg is numeric, always treat it as a GID. return gidArg == g.Gid } @@ -401,18 +396,11 @@ func GetExecUser(userSpec string, defaults *ExecUser, passwd, group io.Reader) ( // If we can't find a group with the given name, the only other valid // option is if it's a numeric group name with no associated entry in group. - if gidErr != nil { + if !isGID { // Not numeric. return nil, fmt.Errorf("unable to find group %s: %w", groupArg, ErrNoGroupEntries) } user.Gid = gidArg - - // Must be inside valid gid range. - if user.Gid < minID || user.Gid > maxID { - return nil, ErrRange - } - - // Okay, so it's numeric. We can just roll with this. } } else if len(groups) > 0 { // Supplementary group ids only make sense if in the implicit form. @@ -426,55 +414,80 @@ func GetExecUser(userSpec string, defaults *ExecUser, passwd, group io.Reader) ( return user, nil } +// groupArg is a parsed group argument for [GetAdditionalGroups]. +type groupArg struct { + name string + gid int + isNumeric bool +} + +// matches reports whether group g satisfies the argument. Numeric arguments +// are matched by GID only, others by name. +func (ag groupArg) matches(g Group) bool { + if ag.isNumeric { + return g.Gid == ag.gid + } + return g.Name == ag.name +} + // GetAdditionalGroups looks up a list of groups by name or group id // against the given /etc/group formatted data. If a group name cannot // be found, an error will be returned. If a group id cannot be found, // or the given group data is nil, the id will be returned as-is // provided it is in the legal range. func GetAdditionalGroups(additionalGroups []string, group io.Reader) ([]int, error) { + addtlGroups := make([]groupArg, len(additionalGroups)) + for i, ag := range additionalGroups { + gid, ok, err := parseNumeric(ag) + if err != nil { + return nil, err + } + addtlGroups[i] = groupArg{ + name: ag, + gid: gid, + isNumeric: ok, + } + } + groups := []Group{} if group != nil { var err error groups, err = ParseGroupFilter(group, func(g Group) bool { - for _, ag := range additionalGroups { - if g.Name == ag || strconv.Itoa(g.Gid) == ag { + for _, ag := range addtlGroups { + if ag.matches(g) { return true } } return false }) if err != nil { - return nil, fmt.Errorf("Unable to find additional groups %v: %w", additionalGroups, err) + return nil, fmt.Errorf("unable to find additional groups %v: %w", additionalGroups, err) } } gidMap := make(map[int]struct{}) - for _, ag := range additionalGroups { + for _, ag := range addtlGroups { var found bool for _, g := range groups { - // if we found a matched group either by name or gid, take the - // first matched as correct - if g.Name == ag || strconv.Itoa(g.Gid) == ag { - if _, ok := gidMap[g.Gid]; !ok { - gidMap[g.Gid] = struct{}{} - found = true - break + if ag.matches(g) { + // take the first matched group as correct + if g.Gid < minID || g.Gid > maxID { + return nil, ErrRange } + gidMap[g.Gid] = struct{}{} + found = true + break } } - // we asked for a group but didn't find it. let's check to see - // if we wanted a numeric group + // We asked for a group but didn't find it. Numeric group IDs may be + // used as-is even when they are not present in /etc/group; non-numeric + // group names must be found. if !found { - gid, err := strconv.ParseInt(ag, 10, 64) - if err != nil { + if !ag.isNumeric { // Not a numeric ID either. - return nil, fmt.Errorf("Unable to find group %s: %w", ag, ErrNoGroupEntries) + return nil, fmt.Errorf("unable to find group %s: %w", ag.name, ErrNoGroupEntries) } - // Ensure gid is inside gid range. - if gid < minID || gid > maxID { - return nil, ErrRange - } - gidMap[int(gid)] = struct{}{} + gidMap[ag.gid] = struct{}{} } } gids := []int{} @@ -498,12 +511,7 @@ func GetAdditionalGroupsPath(additionalGroups []string, groupPath string) ([]int } func ParseSubIDFile(path string) ([]SubID, error) { - subid, err := os.Open(path) - if err != nil { - return nil, err - } - defer subid.Close() - return ParseSubID(subid) + return ParseSubIDFileFilter(path, nil) } func ParseSubID(subid io.Reader) ([]SubID, error) { @@ -551,12 +559,7 @@ func ParseSubIDFilter(r io.Reader, filter func(SubID) bool) ([]SubID, error) { } func ParseIDMapFile(path string) ([]IDMap, error) { - r, err := os.Open(path) - if err != nil { - return nil, err - } - defer r.Close() - return ParseIDMap(r) + return ParseIDMapFileFilter(path, nil) } func ParseIDMap(r io.Reader) ([]IDMap, error) { diff --git a/vendor/github.com/moby/sys/user/user_fuzzer.go b/vendor/github.com/moby/sys/user/user_fuzzer.go index e018eae61..5f56cc00a 100644 --- a/vendor/github.com/moby/sys/user/user_fuzzer.go +++ b/vendor/github.com/moby/sys/user/user_fuzzer.go @@ -1,5 +1,4 @@ //go:build gofuzz -// +build gofuzz package user diff --git a/vendor/github.com/moby/sys/user/user_utils.go b/vendor/github.com/moby/sys/user/user_utils.go new file mode 100644 index 000000000..978aa927e --- /dev/null +++ b/vendor/github.com/moby/sys/user/user_utils.go @@ -0,0 +1,64 @@ +package user + +import ( + "errors" + "fmt" + "io" + "os" +) + +// maxUserFileBytes caps how much data is read from any user-database file. +// User database files are expected to be relatively small. 10 MiB provides +// generous headroom while bounding memory usage. +const maxUserFileBytes = 10 << 20 + +// openUserFile attempts to open a user-database file with a limitedFile +// capped at maxUserFileBytes. It produces an error if the given path is +// a non-regular file. +func openUserFile(path string) (*limitedFile, error) { + f, err := os.Open(path) + if err != nil { + return nil, err + } + + info, err := f.Stat() + if err != nil { + _ = f.Close() + return nil, err + } + if !info.Mode().IsRegular() { + _ = f.Close() + return nil, &os.PathError{ + Op: "open", + Path: path, + Err: errors.New("not a regular file"), + } + } + + return &limitedFile{ + File: f, + // Allow one byte past the cap so an overflow surfaces as an + // error rather than a silent EOF that the parser would treat as + // a clean end-of-file (and miss any entries past the cap). + LimitedReader: &io.LimitedReader{R: f, N: maxUserFileBytes + 1}, + name: path, + }, nil +} + +type limitedFile struct { + *os.File + *io.LimitedReader + name string +} + +func (l *limitedFile) Read(p []byte) (int, error) { + n, err := l.LimitedReader.Read(p) + if l.LimitedReader.N == 0 { + return n, &os.PathError{ + Op: "read", + Path: l.name, + Err: fmt.Errorf("file exceeds %d bytes", maxUserFileBytes), + } + } + return n, err +} diff --git a/vendor/modules.txt b/vendor/modules.txt index b91f4fb0c..d99c8f910 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -111,7 +111,7 @@ github.com/google/uuid # github.com/hashicorp/go-version v1.7.0 ## explicit github.com/hashicorp/go-version -# github.com/klauspost/compress v1.18.6 +# github.com/klauspost/compress v1.18.7 ## explicit; go 1.24 github.com/klauspost/compress github.com/klauspost/compress/flate @@ -132,8 +132,8 @@ github.com/magiconair/properties # github.com/moby/docker-image-spec v1.3.1 ## explicit; go 1.18 github.com/moby/docker-image-spec/specs-go/v1 -# github.com/moby/go-archive v0.2.0 -## explicit; go 1.23.0 +# github.com/moby/go-archive v0.3.0 +## explicit; go 1.25 github.com/moby/go-archive github.com/moby/go-archive/compression github.com/moby/go-archive/tarheader @@ -170,11 +170,11 @@ github.com/moby/moby/client/pkg/versions ## explicit; go 1.19 github.com/moby/patternmatcher github.com/moby/patternmatcher/ignorefile -# github.com/moby/sys/sequential v0.6.0 -## explicit; go 1.17 +# github.com/moby/sys/sequential v0.7.0 +## explicit; go 1.24.0 github.com/moby/sys/sequential -# github.com/moby/sys/user v0.4.0 -## explicit; go 1.17 +# github.com/moby/sys/user v0.4.1 +## explicit; go 1.18 github.com/moby/sys/user # github.com/moby/sys/userns v0.1.0 ## explicit; go 1.21