vendor: github.com/moby/go-archive v0.3.3

- Fix a regression introduced in v0.3.0 that caused archive extraction
  to reject hardlinks with absolute targets, as produced by some image
  builders. Absolute hardlink targets are now resolved relative to the
  extraction root, while paths that escape the root remain rejected.
- Fix a regression introduced in v0.3.0 that caused archive extraction
  to fail when applying permissions to device nodes, including nodes on
  `nodev` filesystems and `dev/ptmx`. Device nodes are now referenced
  without opening the underlying device before applying their mode.
- Set close-on-exec on file descriptors used by the Linux permission
  fallback to prevent them from leaking into child processes.

full diff: https://github.com/moby/go-archive/compare/v0.3.2...v0.3.3

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
This commit is contained in:
Sebastiaan van Stijn
2026-08-04 21:07:17 +02:00
committed by Paweł Gronowski
parent ee7c2c635d
commit 2a3ab8fcc0
13 changed files with 169 additions and 27 deletions

View File

@@ -103,6 +103,11 @@ func (daemon *Daemon) containerExtractToDir(container *container.Container, path
defer container.Unlock()
options := daemon.defaultTarCopyOptions(allowOverwriteDirWithFile)
options, cleanup, err := archive.WithProcSelfFD(options)
if err != nil {
return err
}
defer cleanup()
// Decompress the archive before switching into the container's
// filesystem to avoid executing decompression binaries (xz, unpigz)

2
go.mod
View File

@@ -64,7 +64,7 @@ require (
github.com/mitchellh/copystructure v1.2.0
github.com/moby/buildkit v0.32.2
github.com/moby/docker-image-spec v1.3.1
github.com/moby/go-archive v0.3.2
github.com/moby/go-archive v0.3.3
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.2 h1:Sfy7+u6dUv/2yuBc9KCoK70Re8atuV8aPZ5UOC068Vc=
github.com/moby/buildkit v0.32.2/go.mod h1:0GB/EJ1d+4VIVqIAgy3asaoGkVXy7IrDfVy7mPhOvg8=
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.3.2 h1:x893kC3zRygv2C+k4Y9kMxYRPLCj4XEJB0srbAP06Hw=
github.com/moby/go-archive v0.3.2/go.mod h1:Npdv43fFqlhZW7Xo8fbm3ZMYFvAGNviUPqX21VERbcE=
github.com/moby/go-archive v0.3.3 h1:OxxR9paxsluYi+zDUEXTTaIxtkK3viymW+Ka7vRhhME=
github.com/moby/go-archive v0.3.3/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

@@ -17,6 +17,7 @@ import (
"time"
"github.com/containerd/log"
"github.com/moby/go-archive/internal/archiveoptions"
"github.com/moby/patternmatcher"
"github.com/moby/sys/sequential"
"github.com/moby/sys/user"
@@ -81,9 +82,22 @@ type (
// were probably in the archive for a reason, so set this option at
// your own peril.
BestEffortXattrs bool
// internalOptions contains options for use by packages within this module.
internalOptions *archiveoptions.Options
}
)
// WithProcSelfFD returns a copy of opts prepared for extraction in a
// filesystem context where /proc/self/fd may not be accessible by path.
//
// The caller must invoke the returned cleanup function after extraction
// completes. On platforms that do not use /proc/self/fd for extraction,
// the returned cleanup function is a no-op.
func WithProcSelfFD(opts *TarOptions) (*TarOptions, func(), error) {
return withProcSelfFD(opts)
}
// Archiver implements the Archiver interface and allows the reuse of most utility functions of
// this package with a pluggable Untar function. Also, to facilitate the passing of specific id
// mappings for untar, an Archiver can be created with maps which will then be passed to Untar operations.
@@ -509,6 +523,14 @@ func resolveArchivePath(root *os.Root, name string) (string, error) {
// the native, root-relative filesystem path used for extraction.
func resolveHardlinkTarget(root *os.Root, linkname string) (string, error) {
cleaned := path.Clean(linkname)
if strings.HasPrefix(cleaned, "/") {
// Some image builders (e.g. kaniko) write hardlink targets as absolute
// paths. Resolve those relative to the extraction root, with chroot-like
// semantics matching absolute symlink targets. Strip the root from the
// original linkname rather than the cleaned one so that ".." components
// are not collapsed against "/" but instead rejected below.
cleaned = path.Clean(strings.TrimLeft(linkname, "/"))
}
if cleaned == "." || !filepath.IsLocal(cleaned) {
return "", breakoutError(fmt.Errorf("invalid hardlink target %q", linkname))
}
@@ -523,6 +545,7 @@ func createTarFile(root *os.Root, dstPath string, hdr *tar.Header, reader io.Rea
Lchown = true
inUserns, bestEffortXattrs bool
chownOpts *ChownOpts
internalOpts *archiveoptions.Options
)
// TODO(thaJeztah): make opts a required argument.
@@ -531,6 +554,7 @@ func createTarFile(root *os.Root, dstPath string, hdr *tar.Header, reader io.Rea
inUserns = opts.InUserNS // TODO(thaJeztah): consider deprecating opts.InUserNS and detect locally.
chownOpts = opts.ChownOpts
bestEffortXattrs = opts.BestEffortXattrs
internalOpts = opts.internalOptions
}
// hdr.Mode is in linux format, which we can use for sycalls,
@@ -672,7 +696,7 @@ func createTarFile(root *os.Root, dstPath string, hdr *tar.Header, reader io.Rea
// 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(root, dstPath, hardlinkTarget, hdr, hdrInfo); err != nil {
if err := handleLChmod(root, dstPath, hardlinkTarget, hdr, hdrInfo, internalOpts); err != nil {
return err
}

View File

@@ -8,10 +8,28 @@ import (
"path/filepath"
"strings"
"github.com/moby/go-archive/internal/archiveoptions"
"github.com/moby/sys/userns"
"golang.org/x/sys/unix"
)
func withProcSelfFD(opts *TarOptions) (*TarOptions, func(), error) {
procSelfFD, err := os.Open("/proc/self/fd")
if err != nil {
return nil, nil, err
}
var prepared TarOptions
if opts != nil {
prepared = *opts
}
prepared.internalOptions = &archiveoptions.Options{
ProcSelfFD: procSelfFD,
}
return &prepared, func() { _ = procSelfFD.Close() }, nil
}
func getWhiteoutConverter(format WhiteoutFormat) tarWhiteoutConverter {
if format == OverlayWhiteoutFormat {
return newOverlayWhiteoutConverter()

View File

@@ -2,6 +2,14 @@
package archive
func withProcSelfFD(opts *TarOptions) (*TarOptions, func(), error) {
var prepared TarOptions
if opts != nil {
prepared = *opts
}
return &prepared, func() {}, nil
}
func getWhiteoutConverter(format WhiteoutFormat) tarWhiteoutConverter {
return nil
}

View File

@@ -12,6 +12,7 @@ import (
"strings"
"syscall"
"github.com/moby/go-archive/internal/archiveoptions"
"golang.org/x/sys/unix"
)
@@ -87,7 +88,7 @@ func handleTarTypeBlockCharFifo(root *os.Root, hdr *tar.Header, dstPath string)
// 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 {
func handleLChmod(root *os.Root, dstPath string, hardlinkTarget string, hdr *tar.Header, hdrInfo os.FileInfo, opts *archiveoptions.Options) error {
switch hdr.Typeflag {
case tar.TypeSymlink:
return nil
@@ -99,17 +100,17 @@ func handleLChmod(root *os.Root, dstPath string, hardlinkTarget string, hdr *tar
if err != nil || fi.Mode()&os.ModeSymlink != 0 {
return nil
}
return chmodNoSymlink(root, dstPath, hdrInfo.Mode())
return chmodNoSymlink(root, dstPath, hdrInfo.Mode(), opts)
default:
return chmodNoSymlink(root, dstPath, hdrInfo.Mode())
return chmodNoSymlink(root, dstPath, hdrInfo.Mode(), opts)
}
}
// 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 {
func chmodNoSymlink(root *os.Root, name string, mode os.FileMode, opts *archiveoptions.Options) error {
parent, err := root.OpenFile(filepath.Dir(name), os.O_RDONLY, 0)
if err != nil {
return err
@@ -126,19 +127,7 @@ func chmodNoSymlink(root *os.Root, name string, mode os.FileMode) error {
}
// 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
return chmodNoSymlinkFallback(int(parent.Fd()), base, name, perm, opts) // #nosec G115 -- ignore integer overflow conversion for parent.Fd
}
// fileModeToPerm returns the subset of an os.FileMode that can be applied

View File

@@ -53,7 +53,7 @@ func handleTarTypeBlockCharFifo(root *os.Root, hdr *tar.Header, path string) err
}
// 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 {
func handleLChmod(root *os.Root, dstPath string, hardlinkTarget string, hdr *tar.Header, hdrInfo os.FileInfo, opts any) error {
return nil
}

46
vendor/github.com/moby/go-archive/chmod_linux.go generated vendored Normal file
View File

@@ -0,0 +1,46 @@
package archive
import (
"fmt"
"os"
"runtime"
"strconv"
"github.com/moby/go-archive/internal/archiveoptions"
"golang.org/x/sys/unix"
)
// chmodNoSymlinkFallback applies mode without following the final path
// component on systems without fchmodat2 support.
//
// Callers must have already excluded symlink entries.
func chmodNoSymlinkFallback(parentFD int, base, name string, perm uint32, opts *archiveoptions.Options) error {
fd, err := unix.Openat(parentFD, base, unix.O_PATH|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0)
if err != nil {
return &os.PathError{Op: "openat", Path: name, Err: err}
}
defer unix.Close(fd)
if opts != nil && opts.ProcSelfFD != nil {
err := unix.Fchmodat(int(opts.ProcSelfFD.Fd()), strconv.Itoa(fd), perm, 0)
// Keep the os.File alive until fchmodat has finished using its descriptor.
runtime.KeepAlive(opts.ProcSelfFD)
if err != nil {
return &os.PathError{
Op: "fchmodat",
Path: name,
Err: fmt.Errorf("via pre-opened /proc/self/fd/%d: %w", fd, err),
}
}
} else {
procPath := "/proc/self/fd/" + strconv.Itoa(fd)
if err := unix.Chmod(procPath, perm); err != nil {
return &os.PathError{
Op: "chmod",
Path: name,
Err: fmt.Errorf("via %s: %w", procPath, err),
}
}
}
return nil
}

View File

@@ -0,0 +1,27 @@
//go:build !linux && !windows
package archive
import (
"os"
"github.com/moby/go-archive/internal/archiveoptions"
"golang.org/x/sys/unix"
)
// chmodNoSymlinkFallback applies mode without following the final path
// component on systems without fchmodat2 support.
//
// Callers must have already excluded symlink entries.
func chmodNoSymlinkFallback(parentFD int, base, name string, perm uint32, _ *archiveoptions.Options) error {
fd, err := unix.Openat(parentFD, base, unix.O_RDONLY|unix.O_NOFOLLOW|unix.O_NONBLOCK|unix.O_CLOEXEC, 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
}

View File

@@ -9,9 +9,15 @@ import (
"github.com/moby/go-archive"
)
func doUnpack(decompressedArchive io.Reader, relDest, root string, options *archive.TarOptions) error {
func doUnpack(decompressedArchive io.Reader, relDest, root string, opts *archive.TarOptions) error {
options, closeOptions, err := archive.WithProcSelfFD(opts)
if err != nil {
return err
}
defer closeOptions()
done := make(chan error)
err := goInChroot(root, func() { done <- archive.Unpack(decompressedArchive, relDest, options) })
err = goInChroot(root, func() { done <- archive.Unpack(decompressedArchive, relDest, options) })
if err != nil {
return err
}
@@ -30,14 +36,20 @@ func doPack(relSrc, root string, options *archive.TarOptions) (io.ReadCloser, er
return tb.Reader(), nil
}
func doUnpackLayer(root string, layer io.Reader, options *archive.TarOptions) (int64, error) {
func doUnpackLayer(root string, layer io.Reader, opts *archive.TarOptions) (int64, error) {
options, closeOptions, err := archive.WithProcSelfFD(opts)
if err != nil {
return 0, err
}
defer closeOptions()
type result struct {
layerSize int64
err error
}
done := make(chan result)
err := goInChroot(root, func() {
err = goInChroot(root, func() {
// We need to be able to set any perms
_ = unix.Umask(0)

View File

@@ -0,0 +1,12 @@
// Package archiveoptions defines internal options shared between archive and
// chrootarchive.
package archiveoptions
import "os"
// Options contains extraction resources supplied by internal callers.
type Options struct {
// ProcSelfFD references /proc/self/fd as opened before entering a chroot.
// The caller retains ownership of the file.
ProcSelfFD *os.File
}

3
vendor/modules.txt vendored
View File

@@ -1266,11 +1266,12 @@ 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.3.2
# github.com/moby/go-archive v0.3.3
## explicit; go 1.25
github.com/moby/go-archive
github.com/moby/go-archive/chrootarchive
github.com/moby/go-archive/compression
github.com/moby/go-archive/internal/archiveoptions
github.com/moby/go-archive/internal/mounttree
github.com/moby/go-archive/internal/unshare
github.com/moby/go-archive/tarheader