Compare commits

..
14 Commits
Author SHA1 Message Date
Paweł GronowskiandGitHub a7dcaa6fdb Merge pull request #7151 from vvoland/update-archive
vendor: github.com/moby/go-archive v0.3.3
2026-08-05 19:34:15 +02:00
Paweł Gronowski 400b45f682 vendor: github.com/moby/go-archive v0.3.3
full diff: https://github.com/moby/go-archive/compare/v0.3.2...v0.3.3

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2026-08-05 19:11:05 +02:00
Sebastiaan van StijnandGitHub 38887ec4ed Merge pull request #7145 from winklemad/fix-service-duplicate-removal-panic
cli/command/service: fix panic when removing duplicate values
2026-08-05 19:10:29 +02:00
Sebastiaan van StijnandGitHub 904aef7f69 Merge pull request #7147 from docker/dependabot/github_actions/docker-actions-269857e486
build(deps): bump docker/docker-agent-action/.github/workflows/review-pr.yml from 2.0.2 to 2.0.3 in the docker-actions group across 1 directory
2026-08-04 13:11:45 +02:00
dependabot[bot]andGitHub f08e60eb09 build(deps): bump docker/docker-agent-action/.github/workflows/review-pr.yml
Bumps the docker-actions group with 1 update in the / directory: [docker/docker-agent-action/.github/workflows/review-pr.yml](https://github.com/docker/docker-agent-action).


Updates `docker/docker-agent-action/.github/workflows/review-pr.yml` from 2.0.2 to 2.0.3
- [Release notes](https://github.com/docker/docker-agent-action/releases)
- [Commits](https://github.com/docker/docker-agent-action/compare/774b6e0e60d6c648b0f2dc43bd5221377a0a7420...baf90543d81f5de59751dfd10e6cf45e21a5a982)

---
updated-dependencies:
- dependency-name: docker/docker-agent-action/.github/workflows/review-pr.yml
  dependency-version: 2.0.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: docker-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-04 08:42:59 +00:00
Paweł GronowskiandGitHub abfd89157d Merge pull request #7149 from vvoland/update-docker
VERSION: 29.7.2
2026-08-03 18:24:10 +02:00
Paweł Gronowski 519eb45d03 VERSION: 29.7.2
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2026-08-03 18:18:56 +02:00
Madan Kumar 8717af7168 cli/command/service: fix panic when removing duplicate values
Both makeEnv and updateHosts removed elements from a slice while ranging
over that same slice. The range expression is evaluated once, so after a
removal the loop keeps using the original length: it reads stale elements
that shifted down, skips a live element, and can slice past the end of the
shrunken slice, which panics.

In makeEnv, the "no update required" continue also applied to the inner
loop instead of skipping the re-append, so an env-var passed twice with
the same value was stored twice. A third occurrence with a different value
then tried to remove both entries and panicked:

    docker service create --env A=1 --env A=1 --env A=2 --name repro nginx
    panic: runtime error: slice bounds out of range [2:1]

The same happens with an env-file that lists a variable twice, and the
panic occurs before any API call, so no daemon is needed to hit it.

updateHosts has the same problem when a hostname is listed more than once
in a single entry: --host-rm either leaves a copy behind or panics with
"slice bounds out of range". That needs a spec written through the API or
swarmkit directly, as the CLI does not produce such entries itself, so it
is less likely to be hit in practice.

Use slices.DeleteFunc for both, which removes every match in one pass, and
add tests for makeEnv, which had no coverage.

Signed-off-by: Madan Kumar <winklemad@outlook.com>
2026-08-02 07:13:16 +05:30
Paweł GronowskiandGitHub e9452d6e78 Merge pull request #7142 from thaJeztah/bump_go_archive_0.3.2
vendor: github.com/moby/go-archive v0.3.2
2026-07-31 19:03:37 +02:00
Sebastiaan van Stijn a6014a702b vendor: github.com/moby/go-archive v0.3.2
full diff: https://github.com/moby/go-archive/compare/v0.3.1...v0.3.2

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-07-31 18:53:47 +02:00
Paweł GronowskiandGitHub 2465fca604 Merge pull request #7140 from thaJeztah/bump_go_archive_0.3.1
vendor: github.com/moby/go-archive v0.3.1
2026-07-31 18:33:04 +02:00
Sebastiaan van StijnandGitHub 52b15cc571 Merge pull request #7141 from thaJeztah/bump_version2
update version to 29.7.1
2026-07-31 18:20:09 +02:00
Sebastiaan van Stijn 13590921a4 update version to 29.7.1
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-07-31 18:11:45 +02:00
Sebastiaan van Stijn 0b50545471 vendor: github.com/moby/go-archive v0.3.1
full diff: https://github.com/moby/go-archive/compare/v0.3.0...v0.3.1

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-07-31 18:10:04 +02:00
19 changed files with 447 additions and 150 deletions
+1 -1
View File
@@ -11,7 +11,7 @@ permissions:
jobs:
review:
uses: docker/docker-agent-action/.github/workflows/review-pr.yml@774b6e0e60d6c648b0f2dc43bd5221377a0a7420 # v2.0.2
uses: docker/docker-agent-action/.github/workflows/review-pr.yml@baf90543d81f5de59751dfd10e6cf45e21a5a982 # v2.0.3
permissions:
contents: read # Read repository files and PR diffs
pull-requests: write # Post review comments
+1 -1
View File
@@ -1 +1 @@
29.7.0
29.7.2
+7 -8
View File
@@ -8,6 +8,7 @@ import (
"errors"
"fmt"
"net/netip"
"slices"
"sort"
"strconv"
"strings"
@@ -687,15 +688,13 @@ func (options *serviceOptions) makeEnv() ([]string, error) {
}
currentEnv := make([]string, 0, len(envVariables))
for _, env := range envVariables { // need to process each var, in order
k, _, _ := strings.Cut(env, "=")
for i, current := range currentEnv { // remove duplicates
if current == env {
continue // no update required, may hide this behind flag to preserve order of envVariables
}
if strings.HasPrefix(current, k+"=") {
currentEnv = append(currentEnv[:i], currentEnv[i+1:]...)
}
if slices.Contains(currentEnv, env) {
continue // no update required, may hide this behind flag to preserve order of envVariables
}
k, _, _ := strings.Cut(env, "=")
currentEnv = slices.DeleteFunc(currentEnv, func(current string) bool { // remove duplicates
return strings.HasPrefix(current, k+"=")
})
currentEnv = append(currentEnv, env)
}
+46
View File
@@ -373,3 +373,49 @@ func TestToServiceSysCtls(t *testing.T) {
assert.NilError(t, err)
assert.Check(t, is.DeepEqual(service.TaskTemplate.ContainerSpec.Sysctls, expected))
}
func TestMakeEnv(t *testing.T) {
tests := []struct {
doc string
env []string
expected []string
}{
{
doc: "no duplicates",
env: []string{"one=1", "two=2"},
expected: []string{"one=1", "two=2"},
},
{
doc: "same variable repeated",
env: []string{"one=1", "one=1"},
expected: []string{"one=1"},
},
{
doc: "same variable repeated, then overridden",
env: []string{"one=1", "one=1", "one=2"},
expected: []string{"one=2"},
},
{
doc: "repeated variable last",
env: []string{"one=1", "two=2", "two=2"},
expected: []string{"one=1", "two=2"},
},
{
doc: "last value wins",
env: []string{"one=1", "two=2", "one=3"},
expected: []string{"two=2", "one=3"},
},
}
for _, tc := range tests {
t.Run(tc.doc, func(t *testing.T) {
o := newServiceOptions()
for _, env := range tc.env {
assert.NilError(t, o.env.Set(env))
}
actual, err := o.makeEnv()
assert.NilError(t, err)
assert.Check(t, is.DeepEqual(tc.expected, actual))
})
}
}
+3 -5
View File
@@ -1213,11 +1213,9 @@ func updateHosts(flags *pflag.FlagSet, hosts *[]string) error {
if rm.IPAddr != "" && rm.IPAddr != ip {
continue
}
for i, h := range hostNames {
if h == rm.Host {
hostNames = append(hostNames[:i], hostNames[i+1:]...)
}
}
hostNames = slices.DeleteFunc(hostNames, func(h string) bool {
return h == rm.Host
})
}
if len(hostNames) > 0 {
newHosts = append(newHosts, fmt.Sprintf("%s %s", ip, strings.Join(hostNames, " ")))
+15
View File
@@ -1727,3 +1727,18 @@ func TestUpdateUlimits(t *testing.T) {
})
}
}
func TestUpdateHostsRemoveRepeatedHost(t *testing.T) {
flags := newUpdateCommand(nil).Flags()
flags.Set("host-rm", "host1")
//nolint:dupword // ignore "Duplicate words (host1) found"
hosts := []string{"127.0.0.1 host1 host1 host2", "127.0.0.2 host2 host1 host1"}
err := updateHosts(flags, &hosts)
assert.NilError(t, err)
// All occurrences of `host1` should be removed, also if the same host
// is listed multiple times in the same entry.
assert.Check(t, is.DeepEqual([]string{"127.0.0.1 host2", "127.0.0.2 host2"}, hosts))
}
+1 -1
View File
@@ -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.3.0
github.com/moby/go-archive v0.3.3
github.com/moby/moby/api v1.55.0
github.com/moby/moby/client v0.5.1
github.com/moby/patternmatcher v0.6.1
+6 -2
View File
@@ -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.3.0 h1:nos4BtzzUIqB406BgQnWGMI4qib9BZ8XUHU+ucv/n1c=
github.com/moby/go-archive v0.3.0/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/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=
@@ -121,6 +121,10 @@ github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
github.com/moby/sys/capability v0.4.0 h1:4D4mI6KlNtWMCM1Z/K0i7RV1FkX+DBDHKVJpCndZoHk=
github.com/moby/sys/capability v0.4.0/go.mod h1:4g9IK291rVkms3LKCDOoYlnV8xKwoDTpIrNEE35Wq0I=
github.com/moby/sys/mount v0.3.5 h1:eS3fsZTjHaBihwjp4/+5Z3jxqLXYsbwxqpVSfFv3M00=
github.com/moby/sys/mount v0.3.5/go.mod h1:WUQDO+/uCiCIkIztx8SrwIDVn2dtMFRBebRhpDFT71M=
github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg=
github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4=
github.com/moby/sys/sequential v0.7.0 h1:ASQNGNROJSuOO6LL6bPHbKvuZu6NU8P4ldPWk31zj/8=
github.com/moby/sys/sequential v0.7.0/go.mod h1:NfSTAp6V3fw4tmkD62PEcOKeZKquXT8VKCkf7aVR79o=
github.com/moby/sys/signal v0.7.1 h1:PrQxdvxcGijdo6UXXo/lU/TvHUWyPhj7UOpSo8tuvk0=
+183 -78
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.
@@ -439,6 +453,90 @@ func (ta *tarAppender) addTarFile(srcPath, archivePath string) error {
return nil
}
// 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.
//
// This is a compatibility workaround rather than the preferred long-term
// implementation. 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.
//
// This helper should eventually be replaced by handle-relative resolution and
// operations with resolve-in-root semantics, avoiding the resolution/use race
// and repeated path traversal.
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 := 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))
}
return resolveArchivePath(root, filepath.FromSlash(cleaned))
}
// 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.
@@ -447,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.
@@ -455,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,
@@ -462,6 +562,15 @@ func createTarFile(root *os.Root, dstPath string, hdr *tar.Header, reader io.Rea
// 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
}
}
switch hdr.Typeflag {
case tar.TypeDir:
// Create directory unless it already exists as one; merge in that case.
@@ -511,13 +620,7 @@ func createTarFile(root *os.Root, dstPath string, hdr *tar.Header, reader io.Rea
}
case tar.TypeLink:
// 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 := root.Link(filepath.FromSlash(linkname), dstPath); err != nil {
if err := root.Link(hardlinkTarget, dstPath); err != nil {
return err
}
@@ -593,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, hdr, hdrInfo); err != nil {
if err := handleLChmod(root, dstPath, hardlinkTarget, hdr, hdrInfo, internalOpts); err != nil {
return err
}
@@ -608,7 +711,7 @@ func createTarFile(root *os.Root, dstPath string, hdr *tar.Header, reader io.Rea
}
case tar.TypeLink:
// Follow the hardlink only when its target is not itself a symlink.
fi, err := root.Lstat(filepath.FromSlash(path.Clean(hdr.Linkname)))
fi, err := root.Lstat(hardlinkTarget)
if err == nil && fi.Mode()&os.ModeSymlink == 0 {
if err := chtimes(root, dstPath, aTime, mTime); err != nil {
return err
@@ -698,13 +801,13 @@ func (t *Tarballer) Do() {
defer func() {
// Make sure to check the error on Close.
if err := ta.TarWriter.Close(); err != nil {
if err := ta.TarWriter.Close(); err != nil && !errors.Is(err, io.ErrClosedPipe) {
log.G(context.TODO()).Errorf("Can't close tar writer: %s", err)
}
if err := t.compressWriter.Close(); err != nil {
if err := t.compressWriter.Close(); err != nil && !errors.Is(err, io.ErrClosedPipe) {
log.G(context.TODO()).Errorf("Can't close compress writer: %s", err)
}
if err := t.pipeWriter.Close(); err != nil {
if err := t.pipeWriter.Close(); err != nil && !errors.Is(err, io.ErrClosedPipe) {
log.G(context.TODO()).Errorf("Can't close pipe writer: %s", err)
}
}()
@@ -931,7 +1034,10 @@ loop:
// 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)
dstPath, err := resolveArchivePath(root, filepath.FromSlash(hdr.Name))
if err != nil {
return err
}
// 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
@@ -969,7 +1075,7 @@ loop:
//
// 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 {
if err := createImpliedDirectories(root, dstPath, options); err != nil {
return err
}
@@ -1024,81 +1130,80 @@ func unrepresentableOnWindows(hdr *tar.Header) error {
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 necessary because
// the tar format permits implicit directories whose existence is defined only
// by file paths, without corresponding directory headers from which metadata
// could be restored.
//
// The caller must have normalized hdr.Name (no leading ".." components).
// All directory creation is performed via root so it is bounded within the
// destination at the OS level (openat(2) semantics), preventing escape via
// symlinks in the destination tree.
func createImpliedDirectories(root *os.Root, hdr *tar.Header, options *TarOptions) error {
// For non-directory entries, ensure that the parent directory exists.
if hdr.Typeflag != tar.TypeDir {
parent := filepath.FromSlash(path.Dir(strings.TrimSuffix(hdr.Name, "/")))
// Skip when the parent is the root itself; nothing to create.
if parent == "." || parent == "" {
return nil
}
if _, err := root.Lstat(parent); err == nil {
return nil
} else if !os.IsNotExist(err) {
return err
}
// RootPair() is confined inside this loop as most cases will not require a call, so we can spend some
// unneeded function calls in the uncommon case to encapsulate logic -- implied directories are a niche
// usage that reduces the portability of an image.
uid, gid := options.IDMap.RootPair()
// The caller must pass a normalized, root-relative local path. Any archive-path
// conversion and resolve-in-root handling must already have been applied.
// 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 {
parent := filepath.Dir(dstPath)
// 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
}
// 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()
fi, err := root.Stat(cur)
if err != nil {
return err
}
if fi.IsDir() {
continue
}
return &os.PathError{Op: "mkdir", Path: cur, Err: syscall.ENOTDIR}
// 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
}
if options.NoLchown {
continue
}
// Only the successful Mkdir case is newly-created.
dir, err := root.Open(cur)
fi, err := root.Stat(cur)
if err != nil {
return err
}
if uid != 0 || gid != 0 {
if err := dir.Chown(uid, gid); err != nil {
_ = dir.Close()
return err
}
if fi.IsDir() {
continue
}
// 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 {
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
}
if err := dir.Close(); err != nil {
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
}
}
+18
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()
+8
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
}
+7 -19
View File
@@ -8,11 +8,11 @@ import (
"fmt"
"math"
"os"
"path"
"path/filepath"
"strings"
"syscall"
"github.com/moby/go-archive/internal/archiveoptions"
"golang.org/x/sys/unix"
)
@@ -88,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, 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
@@ -96,21 +96,21 @@ func handleLChmod(root *os.Root, dstPath string, hdr *tar.Header, hdrInfo os.Fil
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)))
fi, err := root.Lstat(hardlinkTarget)
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
@@ -127,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
+1 -1
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, path 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
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
}
+27
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
}
+18 -18
View File
@@ -29,8 +29,9 @@ func UnpackLayer(dest string, layer io.Reader, options *TarOptions) (size int64,
tr := tar.NewReader(layer)
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 tracks resolved, native-separator, 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 {
@@ -71,12 +72,6 @@ func UnpackLayer(dest string, layer io.Reader, options *TarOptions) (size int64,
continue
}
// Ensure that the parent directory exists.
err = createImpliedDirectories(root, hdr, options)
if err != nil {
return 0, err
}
// Skip AUFS metadata dirs
if strings.HasPrefix(hdr.Name, WhiteoutMetaPrefix) {
// Regular files inside /.wh..wh.plnk can be used as hardlink targets
@@ -109,10 +104,15 @@ func UnpackLayer(dest string, layer io.Reader, options *TarOptions) (size int64,
// 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) {
dstPath, err := resolveArchivePath(root, filepath.FromSlash(hdr.Name))
if err != nil {
return 0, err
}
// Ensure that the parent directory exists.
if err := createImpliedDirectories(root, dstPath, options); err != nil {
return 0, err
}
if base := filepath.Base(dstPath); strings.HasPrefix(base, WhiteoutPrefix) {
dir := filepath.Dir(dstPath)
if base == WhiteoutOpaqueDir {
_, err := root.Lstat(dir)
@@ -144,9 +144,9 @@ func UnpackLayer(dest string, layer io.Reader, options *TarOptions) (size int64,
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 {
// 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
@@ -206,9 +206,9 @@ func UnpackLayer(dest string, layer io.Reader, options *TarOptions) (size int64,
if hdr.Typeflag == tar.TypeDir {
dirs = append(dirs, unpackedDir{hdr: hdr, name: dstPath})
}
// 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{}{}
// Record the resolved, native-separator, root-relative path so it
// matches the paths produced by the opaque-whiteout walk.
unpackedPaths[dstPath] = struct{}{}
}
}
+12
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
}
+45 -15
View File
@@ -24,31 +24,47 @@ import (
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 root, nil
return result, nil
}
var linksWalked int // to protect against cycles
for {
i := linksWalked
newpath, err := walkLinks(root, path, &linksWalked)
newpath, err := walkLinks(root, path, &linksWalked, &result)
if err != nil {
return "", err
return fsRootPathResult{}, err
}
path = newpath
if i == linksWalked {
newpath = filepath.Join(string(os.PathSeparator), newpath)
if path == newpath {
return filepath.Join(root, newpath), nil
result.path = filepath.Join(root, newpath)
return result, nil
}
path = newpath
}
}
}
func walkLink(root, path string, linksWalked *int) (newpath string, islink bool, err error) {
func walkLink(root, path string, linksWalked *int, result *fsRootPathResult) (newpath string, islink bool, err error) {
if *linksWalked > 255 {
return "", false, errTooManyLinks
}
@@ -74,37 +90,51 @@ func walkLink(root, path string, linksWalked *int) (newpath string, islink bool,
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) (string, error) {
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)
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)
return walkLinks(root, dir[:len(dir)-1], linksWalked, result)
}
newpath, _, err := walkLink(root, dir, linksWalked)
newpath, _, err := walkLink(root, dir, linksWalked, result)
return newpath, err
default:
newdir, err := walkLinks(root, dir, linksWalked)
newdir, err := walkLinks(root, dir, linksWalked, result)
if err != nil {
return "", err
}
newpath, islink, err := walkLink(root, filepath.Join(newdir, file), linksWalked)
newpath, islink, err := walkLink(root, filepath.Join(newdir, file), linksWalked, result)
if err != nil {
return "", err
}
if !islink {
return newpath, nil
}
if filepath.IsAbs(newpath) {
if !islink || filepath.IsAbs(newpath) {
return newpath, nil
}
return filepath.Join(newdir, newpath), nil
+2 -1
View File
@@ -162,10 +162,11 @@ 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.3.0
# github.com/moby/go-archive v0.3.3
## explicit; go 1.25
github.com/moby/go-archive
github.com/moby/go-archive/compression
github.com/moby/go-archive/internal/archiveoptions
github.com/moby/go-archive/tarheader
# github.com/moby/moby/api v1.55.0
## explicit; go 1.24