Merge pull request #53098 from thaJeztah/bump_go_archive

vendor: github.com/moby/go-archive main / v0.3.0-dev
This commit is contained in:
Paweł Gronowski
2026-07-27 20:56:57 +02:00
committed by GitHub
17 changed files with 603 additions and 168 deletions

2
go.mod
View File

@@ -64,7 +64,7 @@ require (
github.com/mitchellh/copystructure v1.2.0
github.com/moby/buildkit v0.32.0-rc2
github.com/moby/docker-image-spec v1.3.1
github.com/moby/go-archive v0.2.1
github.com/moby/go-archive v0.2.2-0.20260724112411-2ff9bfb8b2ee
github.com/moby/ipvs v1.1.0
github.com/moby/locker v1.0.1
github.com/moby/moby/api v1.55.0

4
go.sum
View File

@@ -547,8 +547,8 @@ github.com/moby/buildkit v0.32.0-rc2 h1:2XiJmnPSZyJ0akoKG2ShWAFbWRT5DKvyAI2+Johq
github.com/moby/buildkit v0.32.0-rc2/go.mod h1:Y10FBWvqxl/Wmhdzjee1Y2wQfjifTiwxENIUdaVNdME=
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.1 h1:fAa0wUS/ikZKyx7o/1fhUYmhZ7RgpthdeoDhJvunTLc=
github.com/moby/go-archive v0.2.1/go.mod h1:Npdv43fFqlhZW7Xo8fbm3ZMYFvAGNviUPqX21VERbcE=
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/ipvs v1.1.0 h1:ONN4pGaZQgAx+1Scz5RvWV4Q7Gb+mvfRh3NsPS+1XQQ=
github.com/moby/ipvs v1.1.0/go.mod h1:4VJMWuf098bsUMmZEiD4Tjk/O7mOn3l1PTD3s4OoYAs=
github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg=

View File

@@ -8,9 +8,11 @@ import (
"fmt"
"io"
"os"
"path"
"path/filepath"
"runtime"
"strings"
"sync"
"syscall"
"time"
@@ -95,6 +97,23 @@ 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
}
// breakoutErr marks errors caused by archive breakout attempts.
// Unit tests use it to distinguish expected breakout failures from other
// errors.
@@ -268,7 +287,7 @@ func ReadSecurityXattrToTarHeader(filePath 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 {
@@ -357,7 +376,7 @@ func (ta *tarAppender) addTarFile(srcPath, archivePath 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
@@ -418,7 +437,10 @@ func (ta *tarAppender) addTarFile(srcPath, archivePath string) error {
return nil
}
func createTarFile(dstPath, 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
@@ -440,18 +462,24 @@ func createTarFile(dstPath, extractDir string, hdr *tar.Header, reader io.Reader
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(dstPath); err != nil || !fi.IsDir() {
if err := os.Mkdir(dstPath, 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(dstPath, 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
}
@@ -466,14 +494,12 @@ func createTarFile(dstPath, extractDir string, hdr *tar.Header, reader io.Reader
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, dstPath); 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, dstPath); 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": dstPath, "type": hdr.Typeflag}).Debug("creating fifo node in a userns")
@@ -483,27 +509,29 @@ func createTarFile(dstPath, extractDir string, hdr *tar.Header, reader io.Reader
}
case tar.TypeLink:
// #nosec G305 -- The target path is checked for path traversal.
linkTarget := filepath.Join(extractDir, hdr.Linkname)
// check for hardlink breakout
if !strings.HasPrefix(linkTarget, extractDir) {
return breakoutError(fmt.Errorf("invalid hardlink %q -> %q", linkTarget, 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(linkTarget, dstPath); 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(dstPath), hdr.Linkname) // #nosec G305 -- The target path is checked for path traversal.
// Symlink targets are archive data, not filesystem paths. Preserve the
// target verbatim rather than cleaning or converting it (filepath.FromSlash).
linkTarget := hdr.Linkname
// 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", dstPath, hdr.Linkname))
}
if err := os.Symlink(hdr.Linkname, dstPath); err != nil {
// 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
}
@@ -520,7 +548,7 @@ func createTarFile(dstPath, extractDir string, hdr *tar.Header, reader io.Reader
if chownOpts == nil {
chownOpts = &ChownOpts{UID: hdr.Uid, GID: hdr.Gid}
}
if err := os.Lchown(dstPath, 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)"
@@ -530,12 +558,21 @@ func createTarFile(dstPath, extractDir string, hdr *tar.Header, reader io.Reader
}
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(dstPath, 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).
@@ -554,7 +591,7 @@ func createTarFile(dstPath, extractDir string, hdr *tar.Header, reader io.Reader
// 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, dstPath, hdrInfo); err != nil {
if err := handleLChmod(root, dstPath, hdr, hdrInfo); err != nil {
return err
}
@@ -564,20 +601,20 @@ func createTarFile(dstPath, extractDir string, hdr *tar.Header, reader io.Reader
switch hdr.Typeflag {
case tar.TypeSymlink:
// Apply timestamps to the symlink itself (AT_SYMLINK_NOFOLLOW).
if err := lchtimes(dstPath, aTime, mTime); err != nil {
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 := os.Lstat(hdr.Linkname)
fi, err := root.Lstat(filepath.FromSlash(path.Clean(hdr.Linkname)))
if err == nil && fi.Mode()&os.ModeSymlink == 0 {
if err := chtimes(dstPath, aTime, mTime); err != nil {
if err := root.Chtimes(dstPath, aTime, mTime); err != nil {
return err
}
}
default:
// All other file types follow symlinks.
if err := chtimes(dstPath, aTime, mTime); err != nil {
if err := root.Chtimes(dstPath, aTime, mTime); err != nil {
return err
}
}
@@ -823,14 +860,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.
@@ -851,48 +902,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
// 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
}
// #nosec G305 -- The joined path is checked for path traversal.
dstPath := filepath.Join(dest, hdr.Name)
rel, err := filepath.Rel(dest, dstPath)
if err != nil {
return err
}
if strings.HasPrefix(rel, ".."+string(os.PathSeparator)) {
return breakoutError(fmt.Errorf("%q is outside of %q", hdr.Name, dest))
}
// 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(dstPath); 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", dstPath, 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", dstPath, dest)
return fmt.Errorf("cannot overwrite non-directory %q with directory %q", hdr.Name, dest)
}
if fi.IsDir() && hdr.Name == "." {
@@ -900,7 +953,7 @@ loop:
}
if !fi.IsDir() || hdr.Typeflag != tar.TypeDir {
if err := os.RemoveAll(dstPath); err != nil {
if err := root.RemoveAll(dstPath); err != nil {
return err
}
}
@@ -910,8 +963,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, dstPath)
writeFile, err := whiteoutConverter.ConvertRead(root, hdr, dstPath)
if err != nil {
return err
}
@@ -920,49 +981,122 @@ loop:
}
}
if err := createTarFile(dstPath, 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.
dstPath := filepath.Join(dest, hdr.Name)
if err := chtimes(dstPath, 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 := root.Chtimes(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.
func createImpliedDirectories(dest string, hdr *tar.Header, options *TarOptions) error {
//
// 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.Dir(hdr.Name)
parentPath := filepath.Join(dest, parent)
if _, err := os.Lstat(parentPath); err != nil && os.IsNotExist(err) {
if options.NoLchown {
return os.MkdirAll(parentPath, ImpliedDirectoryMode)
}
// 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()
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()
err = user.MkdirAllAndChown(parentPath, ImpliedDirectoryMode, uid, gid, user.WithOnlyNew)
// 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
}
}
}

View File

@@ -76,7 +76,7 @@ func (c overlayWhiteoutConverter) ConvertWrite(hdr *tar.Header, filePath string,
}, nil
}
func (c overlayWhiteoutConverter) ConvertRead(hdr *tar.Header, filePath string) (bool, error) {
func (c overlayWhiteoutConverter) ConvertRead(root *os.Root, hdr *tar.Header, filePath string) (bool, error) {
base := filepath.Base(filePath)
dir := filepath.Dir(filePath)
@@ -85,9 +85,15 @@ func (c overlayWhiteoutConverter) ConvertRead(hdr *tar.Header, filePath string)
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()
// If a directory is marked as opaque by the AUFS special file, we need to translate that to overlay.
if err := unix.Setxattr(dir, c.opaqueXattr, []byte{'y'}, 0); err != nil {
return false, fmt.Errorf("setxattr('%s', %s=y): %w", dir, c.opaqueXattr, err)
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
@@ -98,9 +104,16 @@ func (c overlayWhiteoutConverter) ConvertRead(hdr *tar.Header, filePath string)
// Regular file.
return true, nil
}
parent, err := root.Open(dir)
if err != nil {
return false, err
}
defer parent.Close()
// 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)
}
@@ -117,9 +130,9 @@ func (c overlayWhiteoutConverter) ConvertRead(hdr *tar.Header, filePath string)
// 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 Lchown can be removed to avoid the remaining TOCTOU window.
if err := os.Lchown(originalPath, hdr.Uid, hdr.Gid); err != nil {
return false, err
// 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}
}
}

View File

@@ -8,6 +8,7 @@ import (
"fmt"
"math"
"os"
"path"
"path/filepath"
"strings"
"syscall"
@@ -59,7 +60,7 @@ func getFileUIDGID(stat any) (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, dstPath string) error {
func handleTarTypeBlockCharFifo(root *os.Root, hdr *tar.Header, dstPath string) error {
mode := uint32(hdr.Mode & 0o7777)
switch hdr.Typeflag {
case tar.TypeBlock:
@@ -80,20 +81,80 @@ func handleTarTypeBlockCharFifo(hdr *tar.Header, dstPath string) error {
return fmt.Errorf("device number %d:%d for %q out of range: %w", hdr.Devmajor, hdr.Devminor, hdr.Name, errInvalidArchive)
}
return mknod(dstPath, mode, unix.Mkdev(uint32(hdr.Devmajor), uint32(hdr.Devminor)))
// 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, dstPath 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(dstPath, hdrInfo.Mode()); err != nil {
return err
}
}
} else if hdr.Typeflag != tar.TypeSymlink {
if err := os.Chmod(dstPath, 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
}

View File

@@ -48,11 +48,12 @@ func getInodeFromStat(stat any) (uint64, 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, path string, hdr *tar.Header, hdrInfo os.FileInfo) error {
return nil
}

21
vendor/github.com/moby/go-archive/dev_darwin.go generated vendored Normal file
View File

@@ -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.
}

View File

@@ -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)
}

View File

@@ -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)) // #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.
}

View File

@@ -7,8 +7,8 @@ import (
"fmt"
"io"
"os"
"path"
"path/filepath"
"runtime"
"strings"
"github.com/containerd/log"
@@ -20,9 +20,17 @@ 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 {
@@ -45,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)
// 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
// 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
}
// 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
}
@@ -81,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 {
@@ -89,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.
dstPath := filepath.Join(dest, hdr.Name)
rel, err := filepath.Rel(dest, dstPath)
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))
}
// 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(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) // #nosec G122 -- FIXME: consider root-scoped APIs (e.g. os.Root) to prevent symlink TOCTOU traversal
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
})
@@ -139,7 +157,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
}
}
@@ -148,9 +166,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(dstPath); err == nil {
if fi, err := root.Lstat(dstPath); err == nil {
if !fi.IsDir() || hdr.Typeflag != tar.TypeDir {
if err := os.RemoveAll(dstPath); err != nil {
if err := root.RemoveAll(dstPath); err != nil {
return 0, err
}
}
@@ -161,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")
@@ -179,23 +197,23 @@ func UnpackLayer(dest string, layer io.Reader, options *TarOptions) (size int64,
return 0, err
}
if err := createTarFile(dstPath, 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[dstPath] = 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.
dstPath := filepath.Join(dest, hdr.Name)
if err := chtimes(dstPath, hdr.AccessTime, hdr.ModTime); err != nil {
for _, d := range dirs {
if err := root.Chtimes(d.name, boundTime(latestTime(d.hdr.AccessTime, d.hdr.ModTime)), boundTime(d.hdr.ModTime)); err != nil {
return 0, err
}
}

112
vendor/github.com/moby/go-archive/rootpath.go generated vendored Normal file
View File

@@ -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
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -3,7 +3,12 @@
package archive
import (
"errors"
"os"
"path"
"path/filepath"
"strings"
"syscall"
"time"
"golang.org/x/sys/unix"
@@ -28,14 +33,33 @@ func timeToTimespec(time time.Time) unix.Timespec {
return unix.NsecToTimespec(time.UnixNano())
}
func lchtimes(name string, atime time.Time, mtime time.Time) error {
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),
}
err := unix.UtimesNanoAt(unix.AT_FDCWD, name, utimes[0:], unix.AT_SYMLINK_NOFOLLOW)
if err != nil && err != unix.ENOSYS {
return err
// #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 err
return nil
}

View File

@@ -27,6 +27,6 @@ func chtimes(name string, atime time.Time, mtime time.Time) error {
return windows.SetFileTime(h, &c, nil, nil)
}
func lchtimes(name string, atime time.Time, mtime time.Time) error {
func lchtimes(root *os.Root, name string, atime time.Time, mtime time.Time) error {
return nil
}

2
vendor/modules.txt vendored
View File

@@ -1266,7 +1266,7 @@ github.com/moby/buildkit/worker/label
# 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.1
# github.com/moby/go-archive v0.2.2-0.20260724112411-2ff9bfb8b2ee
## explicit; go 1.25
github.com/moby/go-archive
github.com/moby/go-archive/chrootarchive