diff --git a/SPECS/docker-cli/CVE-2026-17106.patch b/SPECS/docker-cli/CVE-2026-17106.patch new file mode 100644 index 00000000000..9b6b10ed6e7 --- /dev/null +++ b/SPECS/docker-cli/CVE-2026-17106.patch @@ -0,0 +1,1372 @@ +From df55fdf3cd62b85c6f22c7b1002c3acb3481f78c Mon Sep 17 00:00:00 2001 +From: Cesar Talledo +Date: Wed, 15 Jul 2026 16:15:36 -0700 +Subject: [PATCH] archive: harden tar extraction against path traversal + +Backport the moby/go-archive fix for CVE-2026-17106 (CopyEscape) into the +vendored github.com/docker/docker/pkg/archive package. + +Upstream advisory: GHSA-hfg8-hc9c-6c3h / CVE-2026-17106. The archive +extraction routines (Unpack/Untar and UnpackLayer) confined filesystem +operations to the destination using lexical string checks only, so a crafted +archive could follow symlinks introduced by the archive itself (including in +intermediate path components) or use hardlink targets to create or overwrite +files outside the destination directory (e.g. via `docker cp`). + +Upstream fixed this by rewriting extraction to use os.Root (Go 1.24), which +enforces containment at the OS level (openat(2) semantics), plus resolve-in-root +handling of intermediate symlinks (resolveArchivePath) and validation/resolution +of hardlink targets (resolveHardlinkTarget). This backport carries that model +onto the older package while keeping its existing dependencies (idtools, +pkg/system). + +This squashes the following upstream commits: + + df55fdf3cd62 archive: harden tar extraction against path traversal + 9e6d2c7c969f archive: resolve absolute symlinks within extraction root + 4f6cd58fdfec archive: resolve hardlinks through absolute symlinks + +The unrelated RebaseArchiveEntries change (moby/go-archive 8829a251 / +1c23372e) is not part of this security fix and is intentionally not +backported. + +The Azure Linux golang toolchain (>= 1.24) provides os.Root, so no toolchain +change is required. + +Co-authored-by: Cesar Talledo +Co-authored-by: Paweł Gronowski +Co-authored-by: Sebastiaan van Stijn +Signed-off-by: Cesar Talledo +Signed-off-by: Paweł Gronowski +Signed-off-by: Sebastiaan van Stijn + +Upstream References: + - https://github.com/advisories/GHSA-hfg8-hc9c-6c3h + - https://github.com/moby/go-archive/commit/df55fdf3cd62b85c6f22c7b1002c3acb3481f78c + - https://github.com/moby/go-archive/commit/9e6d2c7c969f4871fe6ded98ae0e28963fde311f + - https://github.com/moby/go-archive/pull/45 +--- + .../docker/docker/pkg/archive/archive.go | 404 ++++++++++++++---- + .../docker/pkg/archive/archive_linux.go | 21 +- + .../docker/docker/pkg/archive/archive_unix.go | 66 ++- + .../docker/pkg/archive/archive_windows.go | 5 +- + .../docker/docker/pkg/archive/dev_darwin.go | 17 + + .../docker/docker/pkg/archive/dev_freebsd.go | 20 + + .../docker/docker/pkg/archive/dev_unix.go | 20 + + .../docker/docker/pkg/archive/diff.go | 155 ++++--- + .../docker/docker/pkg/archive/rootpath.go | 140 ++++++ + .../docker/pkg/archive/sequential_other.go | 6 + + .../pkg/archive/sequential_windows_go126.go | 9 + + .../pkg/archive/sequential_windows_pre126.go | 6 + + 12 files changed, 708 insertions(+), 161 deletions(-) + create mode 100644 vendor/github.com/docker/docker/pkg/archive/dev_darwin.go + create mode 100644 vendor/github.com/docker/docker/pkg/archive/dev_freebsd.go + create mode 100644 vendor/github.com/docker/docker/pkg/archive/dev_unix.go + create mode 100644 vendor/github.com/docker/docker/pkg/archive/rootpath.go + create mode 100644 vendor/github.com/docker/docker/pkg/archive/sequential_other.go + create mode 100644 vendor/github.com/docker/docker/pkg/archive/sequential_windows_go126.go + create mode 100644 vendor/github.com/docker/docker/pkg/archive/sequential_windows_pre126.go + +diff --git a/vendor/github.com/docker/docker/pkg/archive/archive.go b/vendor/github.com/docker/docker/pkg/archive/archive.go +index 43133a0..1876c96 100644 +--- a/vendor/github.com/docker/docker/pkg/archive/archive.go ++++ b/vendor/github.com/docker/docker/pkg/archive/archive.go +@@ -13,10 +13,11 @@ import ( + "io" + "os" + "os/exec" ++ pathpkg "path" + "path/filepath" + "runtime" + "strconv" + "strings" + "syscall" + "time" + +@@ -92,6 +94,94 @@ func NewDefaultArchiver() *Archiver { + return &Archiver{Untar: Untar} + } + ++// 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 ++} ++ ++// resolveArchivePath resolves intermediate symlinks in name using chroot-like ++// semantics when os.Root cannot traverse them. The final path component is ++// intentionally preserved because archive extraction may create or replace it. ++// ++// It resolves the path separately before the actual operation, so a concurrent ++// filesystem change may cause the operation to affect a different path within ++// root. The subsequent os.Root operation still confines the operation to root ++// and prevents such a change from escaping it. ++// ++// Paths with missing components are supported. Existing symlinks are resolved, ++// and any remaining nonexistent components are retained for later creation. ++func resolveArchivePath(root *os.Root, name string) (string, error) { ++ parent, base := filepath.Split(name) ++ if parent == "" { ++ return name, nil ++ } ++ ++ parent = filepath.Clean(parent) ++ ++ // Follow the final parent component: it is an intermediate component of name, ++ // and an absolute symlink there must trigger the resolve-in-root fallback. ++ _, statErr := root.Stat(parent) ++ switch { ++ case statErr == nil: ++ return name, nil ++ case !os.IsNotExist(statErr) && !isPathEscapes(statErr): ++ return "", statErr ++ } ++ ++ // Resolve the parent both to handle ENOENT from missing components or dangling ++ // symlinks, and to determine whether an os.Root breakout was caused by an ++ // absolute symlink. Relative symlink escapes preserve the original Stat error. ++ resolved, err := resolveFSRootPath(root.Name(), parent) ++ if err != nil { ++ return "", err ++ } ++ ++ if isPathEscapes(statErr) && (!resolved.followedAbsoluteLink || resolved.relativeEscapeBeforeAbsolute) { ++ return "", statErr ++ } ++ ++ relParent, err := filepath.Rel(root.Name(), resolved.path) ++ if err != nil { ++ return "", breakoutError(fmt.Errorf( ++ "could not make resolved parent %q relative to root %q: %w", ++ resolved.path, ++ root.Name(), ++ err, ++ )) ++ } ++ if relParent != "." && !filepath.IsLocal(relParent) { ++ return "", breakoutError(fmt.Errorf( ++ "resolved parent %q escapes root %q", ++ resolved.path, ++ root.Name(), ++ )) ++ } ++ ++ return filepath.Join(relParent, base), nil ++} ++ ++// resolveHardlinkTarget validates a POSIX hardlink target and resolves it to ++// the native, root-relative filesystem path used for extraction. ++func resolveHardlinkTarget(root *os.Root, linkname string) (string, error) { ++ cleaned := pathpkg.Clean(linkname) ++ if cleaned == "." || !filepath.IsLocal(cleaned) { ++ return "", breakoutError(fmt.Errorf("invalid hardlink target %q", linkname)) ++ } ++ return resolveArchivePath(root, filepath.FromSlash(cleaned)) ++} ++ + // 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. +@@ -511,7 +601,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 { +@@ -608,7 +698,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(pathpkg.Base(hdr.Name), WhiteoutPrefix) && !ta.IdentityMapping.Empty() { + fileIDPair, err := getFileUIDGID(fi.Sys()) + if err != nil { + return err +@@ -675,7 +765,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 +@@ -693,20 +786,59 @@ func createTarFile(path, extractDir string, hdr *tar.Header, reader io.Reader, o + // so use hdrInfo.Mode() (they differ for e.g. setuid bits) + hdrInfo := hdr.FileInfo() + ++ var hardlinkTarget string ++ if hdr.Typeflag == tar.TypeLink { ++ var err error ++ hardlinkTarget, err = resolveHardlinkTarget(root, hdr.Linkname) ++ if err != nil { ++ return err ++ } ++ } ++ ++ // absPath is computed lazily and memoized. It is only required for xattrs ++ // and symlink timestamps. Symlinks intentionally use root.Symlink directly ++ // to preserve absolute targets; os.Root.Symlink rejects absolute targets ++ // like /usr/lib. A plain closure is used (rather than sync.OnceValues) to ++ // avoid generics, which the go1.16 manpages build (man/go.mod) cannot ++ // instantiate; createTarFile runs on a single goroutine, so the memoized ++ // value needs no locking. ++ var ( ++ absPathVal string ++ absPathErr error ++ absPathDone bool ++ ) ++ absPath := func() (string, error) { ++ if !absPathDone { ++ absPathDone = true ++ var parent string ++ parent, absPathErr = fsRootPath(root.Name(), filepath.Dir(dstPath)) ++ if absPathErr == nil { ++ absPathVal = filepath.Join(parent, filepath.Base(dstPath)) ++ } ++ } ++ return absPathVal, absPathErr ++ } ++ + 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 + } +@@ -720,39 +839,35 @@ func createTarFile(path, extractDir string, hdr *tar.Header, reader io.Reader, o + if inUserns { // cannot create devices 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 { + 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)) +- } +- if err := os.Link(targetPath, path); err != nil { ++ // root.Link confines both the (already validated and resolved) target ++ // and the new link to root, preventing hardlink breakout. ++ if err := root.Link(hardlinkTarget, 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 + } + +@@ -769,12 +884,12 @@ func createTarFile(path, extractDir string, hdr *tar.Header, reader io.Reader, o + if chownOpts == nil { + chownOpts = &idtools.Identity{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 { + msg := "failed to Lchown %q for UID %d, GID %d" + if errors.Is(err, syscall.EINVAL) && userns.RunningInUserNS() { + msg += " (try increasing the number of subordinate IDs in /etc/subuid and /etc/subgid)" + } +- return errors.Wrapf(err, msg, path, hdr.Uid, hdr.Gid) ++ return errors.Wrapf(err, msg, dstPath, hdr.Uid, hdr.Gid) + } + } + +@@ -784,7 +899,13 @@ func createTarFile(path, extractDir string, hdr *tar.Header, reader io.Reader, o + if !ok { + continue + } +- if err := system.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 := system.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). +@@ -803,7 +924,7 @@ 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, hardlinkTarget, hdr, hdrInfo); err != nil { + return err + } + +@@ -815,17 +936,21 @@ func createTarFile(path, extractDir string, hdr *tar.Header, reader io.Reader, o + + // system.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 := system.Chtimes(path, aTime, hdr.ModTime); err != nil { ++ if fi, err := root.Lstat(hardlinkTarget); err == nil && (fi.Mode()&os.ModeSymlink == 0) { ++ if err := root.Chtimes(dstPath, aTime, hdr.ModTime); err != nil { + return err + } + } + } else if hdr.Typeflag != tar.TypeSymlink { +- if err := system.Chtimes(path, aTime, hdr.ModTime); err != nil { ++ if err := root.Chtimes(dstPath, aTime, hdr.ModTime); err != nil { + return err + } + } else { + ts := []syscall.Timespec{timeToTimespec(aTime), timeToTimespec(hdr.ModTime)} ++ path, err := absPath() ++ if err != nil { ++ return err ++ } + if err := system.LUtimesNano(path, ts); err != nil && err != system.ErrNotSupportedPlatform { + return err + } +@@ -1079,13 +1204,27 @@ 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 { ++ root, err := os.OpenRoot(dest) ++ if err != nil { ++ return err ++ } ++ defer func() { _ = root.Close() }() ++ + tr := tar.NewReader(decompressedArchive) + trBuf := pools.BufioReader32KPool.Get(nil) + defer pools.BufioReader32KPool.Put(trBuf) + +- var dirs []*tar.Header ++ var dirs []unpackedDir + whiteoutConverter, err := getWhiteoutConverter(options.WhiteoutFormat, options.InUserNS) + if err != nil { + return err +@@ -1109,48 +1248,53 @@ 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 := pathpkg.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 ++ if err := unrepresentableOnWindows(hdr); err != nil { ++ log.G(context.TODO()).Warnf("Windows: ignoring entry: %v", err) ++ continue + } + +- // #nosec G305 -- The joined path is checked for path traversal. +- path := filepath.Join(dest, hdr.Name) +- rel, err := filepath.Rel(dest, path) ++ // path 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. ++ path := filepath.FromSlash(hdr.Name) ++ dstPath, err := resolveArchivePath(root, 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)) +- } + +- // If path exits we almost always just want to remove and replace it ++ // If dstPath exits 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", dstPath, 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", dstPath, dest) + } + + if fi.IsDir() && hdr.Name == "." { +@@ -1158,7 +1302,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 + } + } +@@ -1169,8 +1313,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, dstPath, 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 + } +@@ -1179,51 +1331,133 @@ loop: + } + } + +- if err := createTarFile(path, dest, hdr, trBuf, options); err != nil { ++ if err := createTarFile(root, dstPath, hdr, trBuf, 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 := system.Chtimes(path, hdr.AccessTime, hdr.ModTime); err != nil { ++ for _, d := range dirs { ++ fi, err := root.Lstat(d.name) ++ if err != nil { ++ if os.IsNotExist(err) { ++ continue ++ } + return err + } ++ if !fi.IsDir() { ++ continue ++ } ++ aTime := d.hdr.AccessTime ++ if aTime.Before(d.hdr.ModTime) { ++ aTime = d.hdr.ModTime ++ } ++ path, err := fsRootPath(root.Name(), d.name) ++ if err != nil { ++ return err ++ } ++ if err := system.Chtimes(path, aTime, d.hdr.ModTime); err != nil { ++ return err ++ } ++ } ++ return nil ++} ++ ++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) ++ } ++ 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. ++// createImpliedDirectories creates all parent directories of dstPath 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 must 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 { ++// dstPath must be a normalized, root-relative native path whose parent ++// components have already been resolved within root by resolveArchivePath. ++// Directory creation is performed through root, so it remains confined to the ++// extraction destination even if the destination tree changes concurrently. ++func createImpliedDirectories(root *os.Root, dstPath string, 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. +- rootIDs := options.IDMap.RootPair() +- +- err = idtools.MkdirAllAndChownNew(parentPath, ImpliedDirectoryMode, rootIDs) ++ if !strings.HasSuffix(dstPath, string(os.PathSeparator)) { ++ parent := filepath.Dir(dstPath) ++ // 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. ++ rootIDs := 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.Split(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 rootIDs.UID != 0 || rootIDs.GID != 0 { ++ if err := dir.Chown(rootIDs.UID, rootIDs.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 ++ } + } + } + +diff --git a/vendor/github.com/docker/docker/pkg/archive/archive_linux.go b/vendor/github.com/docker/docker/pkg/archive/archive_linux.go +index 2c3786c..0ff48a5 100644 +--- a/vendor/github.com/docker/docker/pkg/archive/archive_linux.go ++++ b/vendor/github.com/docker/docker/pkg/archive/archive_linux.go +@@ -63,13 +63,19 @@ func (overlayWhiteoutConverter) ConvertWrite(hdr *tar.Header, path string, fi os + return + } + +-func (c overlayWhiteoutConverter) ConvertRead(hdr *tar.Header, path string) (bool, error) { ++func (c overlayWhiteoutConverter) ConvertRead(root *os.Root, hdr *tar.Header, path string) (bool, error) { + base := filepath.Base(path) + dir := filepath.Dir(path) + + // if a directory is marked as opaque by the AUFS special file, we need to translate that to overlay + if base == WhiteoutOpaqueDir { +- err := unix.Setxattr(dir, "trusted.overlay.opaque", []byte{'y'}, 0) ++ parent, err := root.Open(dir) ++ if err != nil { ++ return false, err ++ } ++ defer parent.Close() ++ ++ err = unix.Fsetxattr(int(parent.Fd()), "trusted.overlay.opaque", []byte{'y'}, 0) + if err != nil { + return false, errors.Wrapf(err, "setxattr(%q, trusted.overlay.opaque=y)", dir) + } +@@ -81,12 +87,17 @@ func (c overlayWhiteoutConverter) ConvertRead(hdr *tar.Header, path string) (boo + if strings.HasPrefix(base, WhiteoutPrefix) { + originalBase := base[len(WhiteoutPrefix):] + originalPath := filepath.Join(dir, originalBase) ++ parent, err := root.Open(dir) ++ if err != nil { ++ return false, err ++ } ++ defer parent.Close() + +- 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, errors.Wrapf(err, "failed to mknod(%q, S_IFCHR, 0)", originalPath) + } +- if err := os.Chown(originalPath, hdr.Uid, hdr.Gid); err != nil { +- return false, err ++ 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 +diff --git a/vendor/github.com/docker/docker/pkg/archive/archive_unix.go b/vendor/github.com/docker/docker/pkg/archive/archive_unix.go +index ff59d01..2184b59 100644 +--- a/vendor/github.com/docker/docker/pkg/archive/archive_unix.go ++++ b/vendor/github.com/docker/docker/pkg/archive/archive_unix.go +@@ -96,7 +96,7 @@ func getFileUIDGID(stat interface{}) (idtools.Identity, error) { + + // 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 { + mode := uint32(hdr.Mode & 0o7777) + switch hdr.Typeflag { + case tar.TypeBlock: +@@ -107,7 +107,7 @@ func handleTarTypeBlockCharFifo(hdr *tar.Header, path string) error { + mode |= unix.S_IFIFO + } + +- err := system.Mknod(path, mode, int(system.Mkdev(hdr.Devmajor, hdr.Devminor))) ++ err := mknodInRoot(root, path, mode, uint64(system.Mkdev(hdr.Devmajor, hdr.Devminor))) + if errors.Is(err, syscall.EPERM) && userns.RunningInUserNS() { + // In most cases, cannot create a device if running in user namespace + err = nil +@@ -115,17 +115,59 @@ func handleTarTypeBlockCharFifo(hdr *tar.Header, path string) error { + return err + } + +-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, hardlinkTarget string, hdr *tar.Header, hdrInfo os.FileInfo) error { ++ switch hdr.Typeflag { ++ case tar.TypeSymlink: ++ return nil ++ case tar.TypeLink: ++ if fi, err := root.Lstat(hardlinkTarget); err != nil || fi.Mode()&os.ModeSymlink != 0 { ++ return nil + } ++ return chmodNoSymlink(root, dstPath, hdrInfo.Mode()) ++ default: ++ return chmodNoSymlink(root, dstPath, hdrInfo.Mode()) ++ } ++} ++ ++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) ++ if err := unix.Fchmodat(int(parent.Fd()), base, perm, unix.AT_SYMLINK_NOFOLLOW); err == nil { ++ return nil ++ } else if !errors.Is(err, syscall.EOPNOTSUPP) && !errors.Is(err, syscall.ENOTSUP) { ++ return &os.PathError{Op: "fchmodat", Path: name, Err: err} ++ } ++ ++ 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 + } ++ ++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/docker/docker/pkg/archive/archive_windows.go b/vendor/github.com/docker/docker/pkg/archive/archive_windows.go +index 09a2583..d0986c8 100644 +--- a/vendor/github.com/docker/docker/pkg/archive/archive_windows.go ++++ b/vendor/github.com/docker/docker/pkg/archive/archive_windows.go +@@ -43,11 +43,12 @@ func getInodeFromStat(stat interface{}) (inode uint64, err error) { + + // 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, dstPath string, hardlinkTarget string, hdr *tar.Header, hdrInfo os.FileInfo) error { + return nil + } + +diff --git a/vendor/github.com/docker/docker/pkg/archive/dev_darwin.go b/vendor/github.com/docker/docker/pkg/archive/dev_darwin.go +new file mode 100644 +index 0000000..4620099 +--- /dev/null ++++ b/vendor/github.com/docker/docker/pkg/archive/dev_darwin.go +@@ -0,0 +1,17 @@ ++//go:build darwin ++ ++package archive ++ ++import ( ++ "os" ++ ++ "golang.org/x/sys/unix" ++) ++ ++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)) ++} +diff --git a/vendor/github.com/docker/docker/pkg/archive/dev_freebsd.go b/vendor/github.com/docker/docker/pkg/archive/dev_freebsd.go +new file mode 100644 +index 0000000..a151c69 +--- /dev/null ++++ b/vendor/github.com/docker/docker/pkg/archive/dev_freebsd.go +@@ -0,0 +1,20 @@ ++//go:build freebsd ++ ++package archive ++ ++import ( ++ "os" ++ "path/filepath" ++ ++ "golang.org/x/sys/unix" ++) ++ ++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/docker/docker/pkg/archive/dev_unix.go b/vendor/github.com/docker/docker/pkg/archive/dev_unix.go +new file mode 100644 +index 0000000..8c1c2fe +--- /dev/null ++++ b/vendor/github.com/docker/docker/pkg/archive/dev_unix.go +@@ -0,0 +1,20 @@ ++//go:build !darwin && !freebsd && !windows ++ ++package archive ++ ++import ( ++ "os" ++ "path/filepath" ++ ++ "golang.org/x/sys/unix" ++) ++ ++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)) ++} +diff --git a/vendor/github.com/docker/docker/pkg/archive/diff.go b/vendor/github.com/docker/docker/pkg/archive/diff.go +index 318f594..65b9af9 100644 +--- a/vendor/github.com/docker/docker/pkg/archive/diff.go ++++ b/vendor/github.com/docker/docker/pkg/archive/diff.go +@@ -6,8 +6,8 @@ import ( + "fmt" + "io" + "os" ++ pathpkg "path" + "path/filepath" +- "runtime" + "strings" + + "github.com/containerd/log" +@@ -19,11 +19,19 @@ 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) + trBuf := pools.BufioReader32KPool.Get(tr) + defer pools.BufioReader32KPool.Put(trBuf) + +- 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 { +@@ -49,34 +57,22 @@ 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 := pathpkg.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 + +- // Ensure that the parent directory exists. +- err = createImpliedDirectories(dest, hdr, options) +- if err != nil { +- return 0, err ++ // 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 + } + + // Skip AUFS metadata dirs +@@ -85,7 +81,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 := pathpkg.Base(hdr.Name) + aufsHardlinks[basename] = hdr + if aufsTempdir == "" { + if aufsTempdir, err = os.MkdirTemp(dest, "dockerplnk"); err != nil { +@@ -93,47 +89,74 @@ 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) ++ // path 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. ++ path := filepath.FromSlash(hdr.Name) ++ dstPath, err := resolveArchivePath(root, 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)) ++ // Ensure that the resolved entry's parent directory exists. ++ err = createImpliedDirectories(root, dstPath, options) ++ if err != nil { ++ return 0, fmt.Errorf("failed to create implied directories for %q: %w", hdr.Name, err) + } +- base := filepath.Base(path) ++ ++ 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 ++ } ++ // 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(dir, func(path string, info os.DirEntry, err error) error { ++ 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 resolved, native-separator, ++ // root-relative paths, matching filepath.WalkDir's paths. ++ if _, exists := unpackedPaths[rel]; !exists { ++ return root.RemoveAll(rel) + } + return nil + }) +@@ -143,7 +166,7 @@ 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 + } + } +@@ -152,9 +175,9 @@ func UnpackLayer(dest string, layer io.Reader, options *TarOptions) (size int64, + // 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 + } + } +@@ -166,8 +189,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(pathpkg.Clean(hdr.Linkname), WhiteoutLinkDir) { ++ linkBasename := pathpkg.Base(hdr.Linkname) + srcHdr = aufsHardlinks[linkBasename] + if srcHdr == nil { + return 0, fmt.Errorf("Invalid aufs hardlink") +@@ -184,23 +207,41 @@ func UnpackLayer(dest string, layer io.Reader, options *TarOptions) (size int64, + return 0, err + } + +- if err := createTarFile(path, dest, srcHdr, srcData, options); err != nil { +- return 0, err ++ if err := createTarFile(root, dstPath, srcHdr, srcData, options); err != nil { ++ return 0, fmt.Errorf("failed to create %q: %w", hdr.Name, 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{}{} ++ // Record the resolved, native-separator, root-relative path so it ++ // matches the paths produced by the opaque-whiteout walk. ++ unpackedPaths[dstPath] = 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 := system.Chtimes(path, hdr.AccessTime, hdr.ModTime); err != nil { ++ for _, d := range dirs { ++ fi, err := root.Lstat(d.name) ++ if err != nil { ++ if os.IsNotExist(err) { ++ continue ++ } ++ return 0, err ++ } ++ if !fi.IsDir() { ++ continue ++ } ++ aTime := d.hdr.AccessTime ++ if aTime.Before(d.hdr.ModTime) { ++ aTime = d.hdr.ModTime ++ } ++ path, err := fsRootPath(root.Name(), d.name) ++ if err != nil { ++ return 0, err ++ } ++ if err := system.Chtimes(path, aTime, d.hdr.ModTime); err != nil { + return 0, err + } + } +diff --git a/vendor/github.com/docker/docker/pkg/archive/rootpath.go b/vendor/github.com/docker/docker/pkg/archive/rootpath.go +new file mode 100644 +index 0000000..1a66a39 +--- /dev/null ++++ b/vendor/github.com/docker/docker/pkg/archive/rootpath.go +@@ -0,0 +1,140 @@ ++/* ++ 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") ++ ++type fsRootPathResult struct { ++ path string ++ followedAbsoluteLink bool ++ relativeEscapeBeforeAbsolute bool ++} ++ ++// fsRootPath joins a path with a root, evaluating and bounding any ++// symlink to the root directory. ++func fsRootPath(root, path string) (string, error) { ++ result, err := resolveFSRootPath(root, path) ++ if err != nil { ++ return "", err ++ } ++ return result.path, nil ++} ++ ++func resolveFSRootPath(root, path string) (fsRootPathResult, error) { ++ result := fsRootPathResult{path: root} ++ if path == "" { ++ return result, nil ++ } ++ var linksWalked int // to protect against cycles ++ for { ++ i := linksWalked ++ newpath, err := walkLinks(root, path, &linksWalked, &result) ++ if err != nil { ++ return fsRootPathResult{}, err ++ } ++ path = newpath ++ if i == linksWalked { ++ newpath = filepath.Join(string(os.PathSeparator), newpath) ++ if path == newpath { ++ result.path = filepath.Join(root, newpath) ++ return result, nil ++ } ++ path = newpath ++ } ++ } ++} ++ ++func walkLink(root, path string, linksWalked *int, result *fsRootPathResult) (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 ++ } ++ if filepath.IsAbs(newpath) { ++ result.followedAbsoluteLink = true ++ } else if !result.followedAbsoluteLink { ++ // Record an escape before a later absolute link can make the original ++ // os.Root error appear eligible for resolve-in-root fallback. ++ relativeDir, err := filepath.Rel(string(os.PathSeparator), filepath.Dir(path)) ++ if err != nil { ++ return "", false, err ++ } ++ resolved := filepath.Join(relativeDir, newpath) ++ if resolved != "." && !filepath.IsLocal(resolved) { ++ result.relativeEscapeBeforeAbsolute = true ++ } ++ } ++ ++ *linksWalked++ ++ return newpath, true, nil ++} ++ ++func walkLinks(root, path string, linksWalked *int, result *fsRootPathResult) (string, error) { ++ switch dir, file := filepath.Split(path); { ++ case dir == "": ++ newpath, _, err := walkLink(root, file, linksWalked, result) ++ 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, result) ++ } ++ newpath, _, err := walkLink(root, dir, linksWalked, result) ++ return newpath, err ++ default: ++ newdir, err := walkLinks(root, dir, linksWalked, result) ++ if err != nil { ++ return "", err ++ } ++ newpath, islink, err := walkLink(root, filepath.Join(newdir, file), linksWalked, result) ++ if err != nil { ++ return "", err ++ } ++ if !islink || filepath.IsAbs(newpath) { ++ return newpath, nil ++ } ++ return filepath.Join(newdir, newpath), nil ++ } ++} +diff --git a/vendor/github.com/docker/docker/pkg/archive/sequential_other.go b/vendor/github.com/docker/docker/pkg/archive/sequential_other.go +new file mode 100644 +index 0000000..90edb13 +--- /dev/null ++++ b/vendor/github.com/docker/docker/pkg/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/docker/docker/pkg/archive/sequential_windows_go126.go b/vendor/github.com/docker/docker/pkg/archive/sequential_windows_go126.go +new file mode 100644 +index 0000000..1e80d11 +--- /dev/null ++++ b/vendor/github.com/docker/docker/pkg/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/docker/docker/pkg/archive/sequential_windows_pre126.go b/vendor/github.com/docker/docker/pkg/archive/sequential_windows_pre126.go +new file mode 100644 +index 0000000..2b28174 +--- /dev/null ++++ b/vendor/github.com/docker/docker/pkg/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 +-- +2.45.4 + diff --git a/SPECS/docker-cli/docker-cli.spec b/SPECS/docker-cli/docker-cli.spec index f5a307e42c8..ea5290a1a47 100644 --- a/SPECS/docker-cli/docker-cli.spec +++ b/SPECS/docker-cli/docker-cli.spec @@ -3,7 +3,7 @@ Summary: The open-source application container engine client. Name: docker-cli Version: 25.0.7 -Release: 4%{?dist} +Release: 5%{?dist} License: ASL 2.0 Vendor: Microsoft Corporation Distribution: Azure Linux @@ -16,6 +16,7 @@ Patch1: CVE-2024-24786.patch Patch2: CVE-2025-11065.patch Patch3: CVE-2026-39821.patch Patch4: CVE-2026-56852.patch +Patch5: CVE-2026-17106.patch BuildRequires: git BuildRequires: go-md2man BuildRequires: golang @@ -83,6 +84,9 @@ install -p -m 644 contrib/completion/fish/docker.fish %{buildroot}%{_datadir}/fi %{_datadir}/fish/vendor_completions.d/docker.fish %changelog +* Mon Aug 31 2026 Swapnil Sahu - 25.0.7-5 +- Patch for CVE-2026-17106 + * Mon Jul 27 2026 Azure Linux Security Servicing Account - 25.0.7-4 - Patch for CVE-2026-56852