From f6d6bede46bc8f358c927d9872c59af7e6306f13 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Thu, 30 Jul 2026 19:17:27 +0200 Subject: [PATCH] vendor: github.com/moby/go-archive v0.3.0 full diff: https://github.com/moby/go-archive/compare/v0.2.1...v0.3.0 v0.3.0 This release fixes CVE-2026-17106 / GHSA-hfg8-hc9c-6c3h, where a crafted tar archive could use links to cause extraction operations to create or overwrite files outside the intended destination directory. The issue affected Unpack, UnpackLayer, Untar, UntarUncompressed, and the ApplyLayer helpers. Users should upgrade and avoid extracting untrusted archives with earlier versions. What's Changed * archive: harden tar extraction against path traversal * archive: do not follow reparse points in chtimes * archive: fix creation time updates on Windows * archive: minor cleanups and godoc touch-up * archive: RebaseArchiveEntries: fix archive path rebasing Signed-off-by: Sebastiaan van Stijn --- vendor.mod | 2 +- vendor.sum | 4 +- vendor/github.com/moby/go-archive/archive.go | 8 +- vendor/github.com/moby/go-archive/copy.go | 45 ++++++-- vendor/github.com/moby/go-archive/diff.go | 2 +- vendor/github.com/moby/go-archive/time.go | 4 + .../moby/go-archive/time_nonwindows.go | 35 +++--- .../moby/go-archive/time_windows.go | 105 +++++++++++++++--- vendor/modules.txt | 2 +- 9 files changed, 157 insertions(+), 50 deletions(-) diff --git a/vendor.mod b/vendor.mod index ae1f6a2e01f9..db9e9ac843c3 100644 --- a/vendor.mod +++ b/vendor.mod @@ -31,7 +31,7 @@ require ( github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/google/uuid v1.6.0 github.com/mattn/go-runewidth v0.0.24 - github.com/moby/go-archive v0.2.2-0.20260724112411-2ff9bfb8b2ee // main / v0.3.0-dev + github.com/moby/go-archive v0.3.0 github.com/moby/moby/api v1.55.0 github.com/moby/moby/client v0.5.1 github.com/moby/patternmatcher v0.6.1 diff --git a/vendor.sum b/vendor.sum index ccf406d9a037..86c70a452ba0 100644 --- a/vendor.sum +++ b/vendor.sum @@ -107,8 +107,8 @@ github.com/mattn/go-runewidth v0.0.24/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhg github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 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.2-0.20260724112411-2ff9bfb8b2ee h1:VUrUP/hu1E43KunXVZlHsNstFGeZOpm/CQoLx5OSuMw= -github.com/moby/go-archive v0.2.2-0.20260724112411-2ff9bfb8b2ee/go.mod h1:Npdv43fFqlhZW7Xo8fbm3ZMYFvAGNviUPqX21VERbcE= +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.55.0 h1:2/sexvQyqIWS8pRSCFddBfpW2qE7vR7FCL+vN8pxwMc= github.com/moby/moby/api v1.55.0/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs= github.com/moby/moby/client v0.5.1 h1:tYNaJno4c0HXz12y5BiqEDy0rVTYkWzI26lGvnTMiJw= diff --git a/vendor/github.com/moby/go-archive/archive.go b/vendor/github.com/moby/go-archive/archive.go index 1654a0733330..4d9e87d20490 100644 --- a/vendor/github.com/moby/go-archive/archive.go +++ b/vendor/github.com/moby/go-archive/archive.go @@ -123,6 +123,8 @@ 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 OverlayWhiteoutFormat WhiteoutFormat = 1 // OverlayWhiteoutFormat formats whiteout according to the overlay standard. @@ -608,13 +610,13 @@ func createTarFile(root *os.Root, dstPath string, hdr *tar.Header, reader io.Rea // 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 := root.Chtimes(dstPath, aTime, mTime); err != nil { + if err := chtimes(root, dstPath, aTime, mTime); err != nil { return err } } default: // All other file types follow symlinks. - if err := root.Chtimes(dstPath, aTime, mTime); err != nil { + if err := chtimes(root, dstPath, aTime, mTime); err != nil { return err } } @@ -994,7 +996,7 @@ loop: for _, d := range dirs { aTime := boundTime(latestTime(d.hdr.AccessTime, d.hdr.ModTime)) - if err := root.Chtimes(d.name, aTime, boundTime(d.hdr.ModTime)); err != nil { + if err := chtimes(root, d.name, aTime, boundTime(d.hdr.ModTime)); err != nil { return err } } diff --git a/vendor/github.com/moby/go-archive/copy.go b/vendor/github.com/moby/go-archive/copy.go index b4ee74ef47c9..7447e8bd106b 100644 --- a/vendor/github.com/moby/go-archive/copy.go +++ b/vendor/github.com/moby/go-archive/copy.go @@ -316,19 +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 { - oldBase = filepath.ToSlash(oldBase) - newBase = filepath.ToSlash(newBase) - - if oldBase == "/" { - // 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() { @@ -356,9 +377,9 @@ 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 { diff --git a/vendor/github.com/moby/go-archive/diff.go b/vendor/github.com/moby/go-archive/diff.go index 75b84f97ca44..055f3c11ee6b 100644 --- a/vendor/github.com/moby/go-archive/diff.go +++ b/vendor/github.com/moby/go-archive/diff.go @@ -213,7 +213,7 @@ func UnpackLayer(dest string, layer io.Reader, options *TarOptions) (size int64, } for _, d := range dirs { - if err := root.Chtimes(d.name, boundTime(latestTime(d.hdr.AccessTime, d.hdr.ModTime)), boundTime(d.hdr.ModTime)); err != nil { + 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/time.go b/vendor/github.com/moby/go-archive/time.go index 4e9ae9508432..fb8c0299b9d3 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 f959b9009aae..418bc887a9b8 100644 --- a/vendor/github.com/moby/go-archive/time_nonwindows.go +++ b/vendor/github.com/moby/go-archive/time_nonwindows.go @@ -14,23 +14,13 @@ import ( "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) -} - -func timeToTimespec(time time.Time) unix.Timespec { - if time.IsZero() { - // Return UTIME_OMIT special value - return unix.Timespec{ - Sec: 0, - Nsec: (1 << 30) - 2, - } - } - return unix.NsecToTimespec(time.UnixNano()) +// 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 { @@ -63,3 +53,14 @@ func lchtimes(root *os.Root, name string, atime, mtime time.Time) error { } return nil } + +func timeToTimespec(time time.Time) unix.Timespec { + if time.IsZero() { + // Return UTIME_OMIT special value + return unix.Timespec{ + Sec: 0, + Nsec: (1 << 30) - 2, + } + } + return unix.NsecToTimespec(time.UnixNano()) +} diff --git a/vendor/github.com/moby/go-archive/time_windows.go b/vendor/github.com/moby/go-archive/time_windows.go index c4a007fb7a56..66173c58bbe2 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) +} + +func lchtimes(root *os.Root, name string, atime time.Time, mtime time.Time) error { + return nil +} - pathp, err := windows.UTF16PtrFromString(name) +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(root *os.Root, 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/modules.txt b/vendor/modules.txt index 01bd5f6d2b18..b5f6d9db5581 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -162,7 +162,7 @@ github.com/mattn/go-runewidth # 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.2-0.20260724112411-2ff9bfb8b2ee +# github.com/moby/go-archive v0.3.0 ## explicit; go 1.25 github.com/moby/go-archive github.com/moby/go-archive/compression