From a29119bc8fc0392c0183ed28eb6bb644d3f52d07 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Wed, 30 Sep 2026 21:32:01 +0900 Subject: [PATCH] cp: pass the directory to tar via -C instead of the working directory GNU tar 1.30-13.el8_10 (AlmaLinux 8, released 2026-09-18) aborts with "Cannot getcwd: No such file or directory" when its working directory is under /proc//root of a container, as the path is unreachable from the host mount namespace (the kernel returns "(unreachable)/...", and glibc turns it into ENOENT). The regression comes from the combination of two upstream tar commits as backported to RHEL/AlmaLinux 8's tar 1.30: - 56fb4a96 ("chdir_id refactoring"), backported in 1.30-12 as part of the CVE-2025-45582 fix, introduced grow_wd(). Upstream initializes wd[0].abspath lazily (NULL), but the 1.30 backport keeps calling xgetcwd() eagerly and fails fatally on error. - 1b91f5f6 ("Draft patch for openat2 changes vs --one-top-level"), backported in 1.30-13, adds an unconditional `chdir_do (chdir_arg (".", ...), false)` to name_init(), so grow_wd() (and thus getcwd) is now reached on every invocation, not only when -C is specified. AlmaLinux 8 went from 1.30-11 directly to 1.30-13. Upstream tar is not affected (getcwd is lazy there), and neither commit is in an upstream release as of v1.35. Using `-C ` keeps the tar process's working directory on the host while tar opens the directory by itself. As the tar process no longer chdirs into the extraction directory, an inaccessible destination is now checked with access(2) beforehand, so that it is still reported as ErrTargetIsReadOnly. Fixes #5237 (the `nerdctl cp` part) Assisted-by: Claude Opus 5.5 (1M context) Signed-off-by: Akihiro Suda --- pkg/containerutil/cp_linux.go | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/pkg/containerutil/cp_linux.go b/pkg/containerutil/cp_linux.go index 6c2646e3c51..2332e1583dd 100644 --- a/pkg/containerutil/cp_linux.go +++ b/pkg/containerutil/cp_linux.go @@ -28,6 +28,8 @@ import ( "strconv" "strings" + "golang.org/x/sys/unix" + containerd "github.com/containerd/containerd/v2/client" "github.com/containerd/containerd/v2/core/containers" "github.com/containerd/containerd/v2/core/mount" @@ -262,7 +264,11 @@ func CopyFiles(ctx context.Context, client *containerd.Client, container contain if options.FollowSymLink { tarC = append(tarC, "-h") } - tarC = append(tarC, "-c", "-f", "-", tarCArg) + // Use -C rather than setting the working directory of the tar process, as GNU tar + // 1.30-13.el8_10 (AlmaLinux 8) fails with "Cannot getcwd" when its working directory is + // under /proc//root of another mount namespace. + // https://github.com/containerd/nerdctl/issues/5237 + tarC = append(tarC, "-C", tarCDir, "-c", "-f", "-", tarCArg) } tarXDir := destinationSpec.resolvedPath @@ -277,7 +283,13 @@ func CopyFiles(ctx context.Context, client *containerd.Client, container contain if options.Container2Host && isGNUTar { tarX = append(tarX, "--no-same-owner") } - tarX = append(tarX, "-f", "-") + tarX = append(tarX, "-C", tarXDir, "-f", "-") + + // tar opens the -C directory by itself and fails with an unhelpful error when the directory + // is not accessible, so detect this beforehand. + if accessErr := unix.Access(tarXDir, unix.X_OK); errors.Is(accessErr, unix.EACCES) { + return ErrTargetIsReadOnly + } } if rootlessutil.IsRootless() { @@ -293,12 +305,10 @@ func CopyFiles(ctx context.Context, client *containerd.Client, container contain // WARNING: some of our testing on stderr might not be portable across different versions of tar // In these cases (readonly target), we will just get the straight tar output instead tarCCmd := exec.CommandContext(ctx, tarC[0], tarC[1:]...) - tarCCmd.Dir = tarCDir tarCCmd.Stdin = nil tarCCmd.Stderr = os.Stderr tarXCmd := exec.CommandContext(ctx, tarX[0], tarX[1:]...) - tarXCmd.Dir = tarXDir if sourceSpec.fromStdin { // Reading from tar should pipe stdin into dst tarXCmd.Stdin = bufio.NewReader(os.Stdin) @@ -319,12 +329,12 @@ func CopyFiles(ctx context.Context, client *containerd.Client, container contain var tarErr bytes.Buffer tarXCmd.Stderr = &tarErr - log.G(ctx).Debugf("executing %v in %q", tarCCmd.Args, tarCCmd.Dir) + log.G(ctx).Debugf("executing %v", tarCCmd.Args) if err := tarCCmd.Start(); err != nil { return errors.Join(fmt.Errorf("failed to execute %v", tarCCmd.Args), err) } - log.G(ctx).Debugf("executing %v in %q", tarXCmd.Args, tarXCmd.Dir) + log.G(ctx).Debugf("executing %v", tarXCmd.Args) if err := tarXCmd.Start(); err != nil { if strings.Contains(err.Error(), "permission denied") { return ErrTargetIsReadOnly