[packages/tar] Rel 2. Backport post-1.35 extraction and archive-parsing fixes.
arekm
arekm at pld-linux.org
Tue Oct 20 22:50:32 CEST 2026
commit adaef9d884f75901a38e4d96e12c2f47b9b16714
Author: Arkadiusz Miśkiewicz <arekm at maven.pl>
Date: Tue Oct 20 22:49:16 2026 +0200
Rel 2. Backport post-1.35 extraction and archive-parsing fixes.
CVE: CVE-2025-45582, CVE-2026-5704, CVE-2026-18508, CVE-2026-18477
tar-CVE-2025-45582.patch | 2416 ++++++++++++++++++++++++++++++
tar-CVE-2026-18477.patch | 412 +++++
tar-CVE-2026-18508.patch | 775 ++++++++++
tar-CVE-2026-5704.patch | 472 ++++++
tar-acl-namespace.patch | 87 ++
tar-deref-extract-over-symlink.patch | 98 ++
tar-dumpdir-missing-terminator.patch | 86 ++
tar-fix-extract-dot-diagnostic.patch | 137 ++
tar-fix-restore-parent-fd.patch | 15 +
tar-missing-exclude-tests.patch | 136 ++
tar-one-top-level-must-be-relative.patch | 49 +
tar.spec | 39 +-
12 files changed, 4719 insertions(+), 3 deletions(-)
---
diff --git a/tar.spec b/tar.spec
index d050ffc..11efbd6 100644
--- a/tar.spec
+++ b/tar.spec
@@ -1,6 +1,6 @@
#
# Conditional build:
-%bcond_with tests # perform make check
+%bcond_without tests # perform make check
Summary: A GNU file archiving program
Summary(de.UTF-8): GNU-Magnetband-Archivierprogramm (tar)
@@ -11,7 +11,7 @@ Summary(pt_BR.UTF-8): GNU Tape Archiver (tar)
Summary(tr.UTF-8): Yaygın kullanılan yedekleyici
Name: tar
Version: 1.35
-Release: 1
+Release: 2
Epoch: 1
License: GPL v3+
Group: Applications/Archiving
@@ -21,14 +21,32 @@ Source1: http://www.mif.pg.gda.pl/homepages/ankry/man-PLD/%{name}-non-english-ma
# Source1-md5: 4e4b1655fe42c27a4eb5d7bcd82e74ac
Patch0: %{name}-info.patch
Patch1: %{name}-zero-block.patch
+Patch2: %{name}-fix-extract-dot-diagnostic.patch
+Patch3: %{name}-CVE-2025-45582.patch
+Patch4: %{name}-one-top-level-must-be-relative.patch
+Patch5: %{name}-acl-namespace.patch
+Patch6: %{name}-CVE-2026-5704.patch
+Patch7: %{name}-CVE-2026-18508.patch
+Patch8: %{name}-CVE-2026-18477.patch
+Patch9: %{name}-missing-exclude-tests.patch
+Patch10: %{name}-dumpdir-missing-terminator.patch
+Patch11: %{name}-deref-extract-over-symlink.patch
+Patch12: %{name}-fix-restore-parent-fd.patch
URL: http://www.gnu.org/software/tar/tar.html
+BuildRequires: acl-devel
BuildRequires: autoconf >= 2.71
BuildRequires: automake >= 1:1.11
BuildRequires: bison
BuildRequires: gettext-tools >= 0.21
BuildRequires: help2man
+BuildRequires: libselinux-devel
BuildRequires: sed >= 4.0
BuildRequires: texinfo
+%if %{with tests}
+BuildRequires: acl
+BuildRequires: attr
+BuildRequires: bash
+%endif
Conflicts: amanda-client < 2.5.2
BuildRoot: %{tmpdir}/%{name}-%{version}-root-%(id -u -n)
@@ -140,6 +158,17 @@ z pakietu dump.
%setup -q
%patch -P0 -p1
%patch -P1 -p1
+%patch -P2 -p1
+%patch -P3 -p1
+%patch -P4 -p1
+%patch -P5 -p1
+%patch -P6 -p1
+%patch -P7 -p1
+%patch -P8 -p1
+%patch -P9 -p1
+%patch -P10 -p1
+%patch -P11 -p1
+%patch -P12 -p1
%{__rm} po/stamp-po
@@ -154,7 +183,10 @@ z pakietu dump.
%{__make}
-%{?with_tests:%{__make} check}
+%if %{with tests}
+# upstream test scripts use bash-only redirections
+%{__make} check SHELL=/bin/bash
+%endif
%install
rm -rf $RPM_BUILD_ROOT
@@ -162,6 +194,7 @@ install -d $RPM_BUILD_ROOT{/usr/bin,%{_mandir}/man1}
%{__make} install \
DESTDIR=$RPM_BUILD_ROOT
+%{__rm} $RPM_BUILD_ROOT%{_infodir}/dir
ln -sf %{_bindir}/tar $RPM_BUILD_ROOT/usr/bin/gtar
diff --git a/tar-CVE-2025-45582.patch b/tar-CVE-2025-45582.patch
new file mode 100644
index 0000000..29be200
--- /dev/null
+++ b/tar-CVE-2025-45582.patch
@@ -0,0 +1,2416 @@
+# upstream extraction jail (75b03fdf and the fdbase/chdir series):
+# keep extracted paths inside the extraction directory (CVE-2025-45582)
+diff --git a/doc/tar.texi b/doc/tar.texi
+index 2057e1b..282cd0a 100644
+--- a/doc/tar.texi
++++ b/doc/tar.texi
+@@ -12947,26 +12947,31 @@ when you later extract from the archive you will get incorrect data.
+ When @command{tar} extracts from an archive, by default it writes into
+ files relative to the working directory. If the archive was generated
+ by an untrusted user, that user therefore can write into any file
+-under the working directory. If the working directory contains a
+-symbolic link to another directory, the untrusted user can also write
+-into any file under the referenced directory. When extracting from an
++under the working directory. When extracting from an
+ untrusted archive, it is therefore good practice to create an empty
+-directory and run @command{tar} in that directory.
+-
+-When extracting from two or more untrusted archives, each one should
+-be extracted independently, into different empty directories.
+-Otherwise, the first archive could create a symbolic link into an area
+-outside the working directory, and the second one could follow the
+-link and overwrite data that is not under the working directory. For
+-example, when restoring from a series of incremental dumps, the
+-archives should have been created by a trusted process, as otherwise
+-the incremental restores might alter data outside the working
+-directory.
++directory and run @command{tar} in that directory. You can use the
++ at option{--directory} (@option{-C}) option to specify the working
++directory (@pxref{directory}).
++
++When extracting from an archive, @command{tar} rejects attempts to
++modify files outside the working directory.
++For example, if a symbolic link points outside the working directory,
++ at command{tar} refuses to follow the link, regardless of whether the
++symbolic link existed before @command{tar} was run.
++Therefore, when extracting from two or more untrusted archives,
++each one can be extracted in turn, into the same initially-empty directory.
++Even if an earlier archive creates a symbolic link that
++points outside the working directory,
++ at command{tar} will reject any later attempts to follow that symbolic link.
++However, this safety mechanism applies only to @command{tar} itself:
++it does not apply to other programs you may run later, which will
++ordinarily follow symbolic links even if they escape the working directory.
+
+ If you use the @option{--absolute-names} (@option{-P}) option when
+ extracting, @command{tar} respects any file names in the archive, even
+-file names that begin with @file{/} or contain @file{..}. As this
+-lets the archive overwrite any file in your system that you can write,
++file names that begin with @file{/}, contain @file{..}, or that follow
++a symbolic link to escape the extraction directory. As this lets the
++archive overwrite any file in your system that you can write,
+ the @option{--absolute-names} (@option{-P}) option should be used only
+ for trusted archives.
+
+@@ -13036,7 +13041,7 @@ Protect archives at least as much as you protect any of the files
+ being archived.
+
+ @item
+-Extract from an untrusted archive only into an otherwise-empty
++Extract from untrusted archives only into an otherwise-empty
+ directory. This directory and its parent should be accessible only to
+ trusted users. For example:
+
+@@ -13049,8 +13054,6 @@ $ @kbd{tar -xvf /archives/got-it-off-the-net.tar.gz}
+ @end group
+ @end example
+
+-As a corollary, do not do an incremental restore from an untrusted archive.
+-
+ @item
+ Do not let untrusted users access files extracted from untrusted
+ archives without checking first for problems such as setuid programs.
+diff --git a/gnu/Makefile.am b/gnu/Makefile.am
+index 0e2fc49..69d0cae 100644
+--- a/gnu/Makefile.am
++++ b/gnu/Makefile.am
+@@ -86,6 +86,7 @@
+ # modechange \
+ # obstack \
+ # openat \
++# openat2 \
+ # parse-datetime \
+ # priv-set \
+ # progname \
+@@ -935,10 +936,12 @@ fcntl.h: fcntl.in.h $(top_builddir)/config.status $(CXXDEFS_H) $(ARG_NONNULL_H)
+ -e 's/@''GNULIB_NONBLOCKING''@/$(GL_GNULIB_NONBLOCKING)/g' \
+ -e 's/@''GNULIB_OPEN''@/$(GL_GNULIB_OPEN)/g' \
+ -e 's/@''GNULIB_OPENAT''@/$(GL_GNULIB_OPENAT)/g' \
++ -e 's/@''GNULIB_OPENAT2''@/$(GL_GNULIB_OPENAT2)/g' \
+ -e 's/@''GNULIB_MDA_CREAT''@/$(GL_GNULIB_MDA_CREAT)/g' \
+ -e 's/@''GNULIB_MDA_OPEN''@/$(GL_GNULIB_MDA_OPEN)/g' \
+ -e 's|@''HAVE_FCNTL''@|$(HAVE_FCNTL)|g' \
+ -e 's|@''HAVE_OPENAT''@|$(HAVE_OPENAT)|g' \
++ -e 's|@''HAVE_OPENAT2''@|$(HAVE_OPENAT2)|g' \
+ -e 's|@''REPLACE_CREAT''@|$(REPLACE_CREAT)|g' \
+ -e 's|@''REPLACE_FCNTL''@|$(REPLACE_FCNTL)|g' \
+ -e 's|@''REPLACE_OPEN''@|$(REPLACE_OPEN)|g' \
+@@ -2060,6 +2063,14 @@ EXTRA_DIST += fcntl--.h fcntl-safer.h
+
+ ## end gnulib module openat-safer
+
++## begin gnulib module openat2
++
++if GL_COND_OBJ_OPENAT2
++libgnu_a_SOURCES += openat2.c
++endif
++
++## end gnulib module openat2
++
+ ## begin gnulib module opendir
+
+ if GL_COND_OBJ_OPENDIR
+diff --git a/gnu/fcntl.in.h b/gnu/fcntl.in.h
+index d0bb85f..e08d2f7 100644
+--- a/gnu/fcntl.in.h
++++ b/gnu/fcntl.in.h
+@@ -238,6 +238,46 @@ _GL_WARN_ON_USE (openat, "openat is not portable - "
+ # endif
+ #endif
+
++#if @GNULIB_OPENAT2@
++# if !defined RESOLVE_NO_XDEV && defined __has_include
++# if __has_include (<linux/openat2.h>)
++# include <linux/openat2.h>
++# endif
++# endif
++# ifndef RESOLVE_NO_XDEV
++struct open_how
++{
++# ifdef __UINT64_TYPE__
++ __UINT64_TYPE__ flags, mode, resolve;
++# else
++ unsigned long long int flags, mode, resolve;
++# endif
++};
++# define RESOLVE_NO_XDEV 0x01
++# define RESOLVE_NO_MAGICLINKS 0x02
++# define RESOLVE_NO_SYMLINKS 0x04
++# define RESOLVE_BENEATH 0x08
++# define RESOLVE_IN_ROOT 0x10
++# define RESOLVE_CACHED 0x20
++# endif
++
++# if !@HAVE_OPENAT2@
++_GL_FUNCDECL_SYS (openat2, int,
++ (int fd, char const *file, struct open_how const *how,
++ size_t size)
++ _GL_ARG_NONNULL ((2, 3)));
++# endif
++_GL_CXXALIAS_SYS (openat2, int,
++ (int fd, char const *file, struct open_how const *how,
++ size_t size));
++_GL_CXXALIASWARN (openat2);
++#elif defined GNULIB_POSIXCHECK
++# undef openat2
++# if HAVE_RAW_DECL_OPENAT2
++_GL_WARN_ON_USE (openat2, "openat2 is not portable - "
++ "use gnulib module openat2 for portability");
++# endif
++#endif
+
+ /* Fix up the FD_* macros, only known to be missing on mingw. */
+
+@@ -445,6 +485,15 @@ _GL_WARN_ON_USE (openat, "openat is not portable - "
+ # define AT_NO_AUTOMOUNT 0
+ #endif
+
++/* errno when openat+O_NOFOLLOW fails because the file is a symlink. */
++#if defined __FreeBSD__ || defined __FreeBSD_kernel__ || defined __DragonFly__
++# define _GL_OPENAT_ESYMLINK EMLINK
++#elif defined __NetBSD__
++# define _GL_OPENAT_ESYMLINK EFTYPE
++#else
++# define _GL_OPENAT_ESYMLINK ELOOP
++#endif
++
+ #endif /* _ at GUARD_PREFIX@_FCNTL_H */
+ #endif /* _ at GUARD_PREFIX@_FCNTL_H */
+ #endif
+diff --git a/gnu/openat2.c b/gnu/openat2.c
+new file mode 100644
+index 0000000..bfa2a8e
+--- /dev/null
++++ b/gnu/openat2.c
+@@ -0,0 +1,596 @@
++/* Open a file, with more flags than openat
++ Copyright 2025 Free Software Foundation, Inc.
++
++ This file is free software: you can redistribute it and/or modify
++ it under the terms of the GNU Lesser General Public License as
++ published by the Free Software Foundation; either version 2.1 of the
++ License, or (at your option) any later version.
++
++ This file is distributed in the hope that it will be useful,
++ but WITHOUT ANY WARRANTY; without even the implied warranty of
++ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
++ GNU Lesser General Public License for more details.
++
++ You should have received a copy of the GNU Lesser General Public License
++ along with this program. If not, see <https://www.gnu.org/licenses/>. */
++
++/* written by Paul Eggert */
++
++#include <config.h>
++
++#include <fcntl.h>
++
++#include "eloop-threshold.h"
++#include "filename.h"
++#include "ialloc.h"
++#include "idx.h"
++#include "verify.h"
++
++#include <errno.h>
++#include <limits.h>
++#include <stdckdint.h>
++#include <string.h>
++#include <sys/stat.h>
++#include <unistd.h>
++
++#if defined __linux__ || defined __ANDROID__
++# include <sys/syscall.h>
++# include <linux/version.h>
++# if HAVE_SYS_VFS_H && HAVE_FSTATFS && HAVE_STRUCT_STATFS_F_TYPE
++# include <sys/vfs.h>
++/* Linux-specific constant from coreutils src/fs.h. */
++# define S_MAGIC_PROC 0x9FA0
++# endif
++#endif
++
++/* FSTAT_O_PATH_BUG is true if fstat fails on O_PATH file descriptors.
++ Although it can be dicey to use static checks for Linux kernel versions,
++ due to the dubious practice of building on newer kernels for older ones,
++ do it here anyway as the buggy kernels are rare (all EOLed by 2016)
++ and builders for them are unlikely to use the dubious practice.
++ Circa 2030 we should remove the old-kernel workarounds entirely. */
++#ifdef LINUX_VERSION_CODE
++# define FSTAT_O_PATH_BUG (KERNEL_VERSION (2, 6, 39) <= LINUX_VERSION_CODE \
++ && LINUX_VERSION_CODE < KERNEL_VERSION (3, 6, 0))
++#else
++# define FSTAT_O_PATH_BUG false
++#endif
++
++#ifndef E2BIG
++# define E2BIG EINVAL
++#endif
++
++#ifndef ENOTCAPABLE /* A FreeBSD error number. */
++# define ENOTCAPABLE 0
++#endif
++
++#ifndef PATH_MAX
++# define PATH_MAX IDX_MAX
++#endif
++
++#ifndef O_ASYNC
++# define O_ASYNC 0
++#endif
++#ifndef O_CLOFORK
++# define O_CLOFORK 0
++#endif
++#ifndef O_LARGEFILE
++# define O_LARGEFILE 0
++#endif
++#ifndef O_NOCLOBBER
++# define O_NOCLOBBER 0
++#endif
++#ifndef O_PATH
++# define O_PATH 0
++#endif
++#ifndef O_RESOLVE_BENEATH /* A FreeBSD flag. */
++# define O_RESOLVE_BENEATH 0
++#endif
++#ifndef O_TMPFILE
++# define O_TMPFILE 0
++#endif
++#if O_PATH
++enum { O_PATHSEARCH = O_PATH };
++#else
++enum { O_PATHSEARCH = O_SEARCH };
++#endif
++
++/* Return true if the memory region at S of size N contains only zeros. */
++static bool
++memiszero (void const *s, idx_t n)
++{
++ /* Keep it simple, as N is typically zero. */
++ char const *p = s;
++ for (idx_t i = 0; i < n; i++)
++ if (p[i])
++ return false;
++ return true;
++}
++
++/* Return the negative of errno, helping the compiler about its sign. */
++static int
++negative_errno (void)
++{
++ int err = -errno;
++ assume (err < 0);
++ return err;
++}
++
++/* Make *BUF, which is of size BUFSIZE and which is heap-allocated
++ if not equal to STACKBUF, large enough to hold an object of NGROW + NTAIL.
++ Keep the last NTAIL bytes of *BUF; the rest of *BUF becomes uninitialized.
++ NTAIL must not exceed BUFSIZE.
++ Return the resulting buffer size, or a negative errno value
++ if the buffer could not be grown. */
++static ptrdiff_t
++maybe_grow (char **buf, idx_t bufsize, char *stackbuf,
++ idx_t ngrow, idx_t ntail)
++{
++ if (ngrow <= bufsize - ntail)
++ return bufsize;
++
++ idx_t needed;
++ if (ckd_add (&needed, ngrow, ntail))
++ return -ENOMEM;
++ idx_t s = ckd_add (&s, needed, needed >> 1) ? needed : s;
++ char *newbuf = imalloc (s);
++ if (!newbuf)
++ return negative_errno ();
++ char *oldbuf = *buf;
++ memcpy (newbuf + s - ntail, oldbuf + bufsize - ntail, ntail);
++ if (oldbuf != stackbuf)
++ free (oldbuf);
++ *buf = newbuf;
++ return s;
++}
++
++/* Store DIRFD's file status into *ST.
++ DIRFD is either AT_FDCWD or a nonnegative file descriptor.
++ Return 0 on success, -1 (setting errno) on failure. */
++static int
++dirstat (int dirfd, struct stat *st)
++{
++ /* Use fstatat only if fstat is buggy. fstatat is a bit slower,
++ and using it only on buggy hosts means openat2 need not depend on
++ Gnulib's fstatat module, as all systems with the fstat bug have
++ an fstatat that works well enough. */
++#if FSTAT_O_PATH_BUG
++ return fstatat (dirfd, ".", st);
++#else
++ return dirfd < 0 ? stat (".", st) : fstat (dirfd, st);
++#endif
++}
++
++/* Like openat2 (*FD, FILENAME, h, sizeof h) where h is
++ (struct open_how) { .flags = FLAGS, .resolve = RESOLVE, .mode = MODE },
++ except trust h's contents, advance *FD as we go,
++ use and update *BUF (originally pointing to a buffer of size BUFSIZE,
++ though it may be changed to point to a freshly allocated heap buffer),
++ and return the negative of the errno value on failure.
++ *FD and *BUF can be updated even on failure.
++ BUFSIZE must be at least 2. */
++static int
++do_openat2 (int *fd, char const *filename,
++ int flags, char resolve, mode_t mode,
++ char **buf, idx_t bufsize)
++{
++ int dfd = *fd;
++
++ /* RESOLVED_CACHED cannot be implemented properly in user space,
++ so pretend nothing is cached. */
++ if (resolve & RESOLVE_CACHED)
++ return -EAGAIN;
++
++ /* Put the file name being processed (including trailing NUL) at buffer end,
++ to simplify symlink resolution. */
++ idx_t filenamelen = strlen (filename);
++ if (!filenamelen)
++ return -ENOENT;
++ idx_t filenamesize = filenamelen + 1;
++ if (PATH_MAX < filenamesize)
++ return -ENAMETOOLONG;
++ char *stackbuf = *buf;
++ bufsize = maybe_grow (buf, bufsize, stackbuf, filenamesize, 0);
++ if (bufsize < 0)
++ return bufsize;
++
++ /* Pointer to buffer end. A common access is E[-I] where I is a
++ negative index relative to buffer end. */
++ char *e = *buf + bufsize;
++ memcpy (&e[-filenamesize], filename, filenamesize);
++
++ /* Directory depth below DFD. This is -1 if ".." ascended above
++ DFD at any point in the past, which can happen only if
++ neither RESOLVE_BENEATH nor RESOLVE_IN_ROOT is in effect. */
++ ptrdiff_t depth = 0;
++
++ /* DFD's device. UNKNOWN_DDEV if not acquired yet. If the actual
++ device number equals UNKNOWN_DDEV the code still works,
++ albeit more slowly. */
++ dev_t const UNKNOWN_DEV = -1;
++ dev_t ddev = UNKNOWN_DEV;
++
++ long int maxlinks = resolve & RESOLVE_NO_SYMLINKS ? 0 : __eloop_threshold ();
++
++ /* Iterates through file name components, possibly expanded by
++ symlink contents. */
++ while (true)
++ {
++ /* Make progress in interpreting &E[-FILENAMESIZE] as a file name.
++ If relative, it is relative to *FD.
++ FILENAMESIZE is positive.
++
++ Start by computing sizes of strings at the file name's end.
++ Use negations of sizes to index into E.
++ Here is an example file name and sizes of the trailing strings:
++
++ ///usr//bin/.////cat
++ F G H 1
++
++ As the '1' indicates, all sizes are positive
++ and include the trailing NUL at E[-1].
++
++ If there are file name components (the typical case),
++ -F <= -G < -H <= -1 and the first component
++ starts at E[-G] and ends just before E[-H].
++ Otherwise if the file name is nonempty,
++ -F < -G = -H = -1 and &E[-F] is a file system root.
++ Otherwise it is Solaris and we resolved an empty final symlink, and
++ -F = -G = -H = -1 and the empty file name is equivalent to ".".
++
++ F = G means the file name is relative to *FD;
++ otherwise the file name is not relative.
++
++ F (i.e., FILENAMESIZE) is the size of the file name. &E[-F] is what
++ is typically passed next to openat (with E[-H] set to NUL).
++
++ G is the size of the file name's suffix that starts with the name's
++ first component; &E[-G] addresses the first component.
++
++ H is the size of the suffix after the first component, i.e.,
++ E[-H] is the slash or NUL after the first component.
++
++ If there is no component, G and H are both 1. */
++ idx_t f = filenamesize;
++ idx_t g = f - FILE_SYSTEM_PREFIX_LEN (&e[-f]);
++ while (ISSLASH (e[-g]))
++ g--;
++ if (f != g)
++ {
++ /* The file name is not relative. */
++ if (resolve & RESOLVE_BENEATH)
++ return -EXDEV;
++ if (resolve & RESOLVE_IN_ROOT)
++ f = g;
++ if (*fd != dfd)
++ {
++ /* A non-relative symlink had been resolved at positive depth.
++ Forget its parent directory. */
++ close (*fd);
++ *fd = dfd;
++ }
++ }
++ idx_t h = g;
++ while (h > 1)
++ {
++ h--;
++ if (ISSLASH (e[-h]))
++ break;
++ }
++
++ /* Properties of the file name through the first component's end,
++ or to file name end if there is no component. */
++ bool leading_dot = e[-f] == '.';
++ bool dot_or_empty = f - h == leading_dot;
++ bool dotdot = leading_dot & (f - h == 2) & (e[-h - 1] == '.');
++ bool dotdot_as_dot = dotdot & !depth & !!(resolve & RESOLVE_IN_ROOT);
++ bool dotlike = dot_or_empty | dotdot_as_dot;
++
++ if (dotdot & !depth & !!(resolve & RESOLVE_BENEATH))
++ return -EXDEV;
++
++ /* NEXTF is the value of FILENAMESIZE next time through the loop,
++ unless a symlink intervenes. */
++ idx_t nextf = h;
++ while (ISSLASH (e[-nextf]))
++ nextf--;
++
++ /* FINAL means this is the last time through the loop,
++ unless a symlink intervenes. */
++ bool final = nextf == 1;
++
++ if (!final & dotlike)
++ {
++ /* A non-final component that acts like "."; skip it. */
++ filenamesize = nextf;
++ }
++ else if (!final & dotdot & (depth == 1))
++ {
++ /* A non-final ".." in a name like "x/../y/z" when "x" is an
++ existing non-symlink directory. As an optimization,
++ resolve it like "y/z". */
++ close (*fd);
++ *fd = dfd;
++ depth = 0;
++ filenamesize = nextf;
++ }
++ else
++ {
++ if (dotlike)
++ {
++ /* This is empty or the last component, and acts like ".".
++ Use "." regardless of whether it was "" or "." or "..". */
++ f = sizeof ".";
++ e[-f] = '.';
++ }
++
++ /* Open the current component, as either an internal directory or
++ the final open. Do not follow symlinks. */
++ char eh = e[-h];
++ int subflags = ((!final
++ ? O_PATHSEARCH | O_CLOEXEC | O_CLOFORK
++ : flags)
++ | O_NOFOLLOW | (eh ? O_DIRECTORY : 0));
++ e[-h] = '\0';
++ int subfd = openat (*fd, &e[-f], subflags, mode);
++
++ if (subfd < 0)
++ {
++ int openerr = negative_errno ();
++ if (! ((openerr == -_GL_OPENAT_ESYMLINK)
++ | (!!(subflags & O_DIRECTORY) & (openerr == -ENOTDIR))))
++ return openerr;
++
++ /* Likely a symlink.
++ Try to read symlink contents into buffer start.
++ But if the root prefix might be needed,
++ leave room for it at buffer start. */
++ idx_t rootlen = f - g;
++ char *slink;
++ ssize_t slinklen;
++ for (idx_t more = rootlen + 1; ; more = bufsize - f + 1)
++ {
++ bufsize = maybe_grow (buf, bufsize, stackbuf, more, f);
++ if (bufsize < 0)
++ return bufsize;
++ e = *buf + bufsize;
++ slink = *buf + rootlen;
++ idx_t slinksize = bufsize - f - rootlen;
++ slinklen = readlinkat (*fd, &e[-f], slink, slinksize);
++ if (slinklen < 0)
++ {
++ int readlinkerr = negative_errno ();
++ return readlinkerr == -EINVAL ? -ENOTDIR : readlinkerr;
++ }
++ if (slinklen < slinksize)
++ break;
++ }
++
++ if (maxlinks <= 0)
++ return -_GL_OPENAT_ESYMLINK;
++ maxlinks--;
++
++ /* A symlink and the symlink loop count is not exhausted.
++ Fail now if magic and if RESOLVE_NO_MAGIC_LINKS. */
++#ifdef S_MAGIC_PROC
++ if (resolve & RESOLVE_NO_MAGICLINKS)
++ {
++ bool relative = IS_RELATIVE_FILE_NAME (&e[-f]);
++ struct statfs st;
++ int r;
++ if (relative)
++ r = *fd < 0 ? statfs (".", &st) : fstatfs (*fd, &st);
++ else
++ {
++ char eg = e[-g];
++ e[-g] = '\0';
++ r = statfs (&e[-f], &st);
++ e[-g] = eg;
++ }
++ if (r < 0)
++ return negative_errno ();
++ if (st.f_type == S_MAGIC_PROC)
++ return -_GL_OPENAT_ESYMLINK;
++ }
++#endif
++
++ /* Compute KEPT, the number of trailing bytes in the file
++ name that will be appended to the symlink contents. */
++ idx_t kept;
++ if (slinklen == 0)
++ {
++ /* On Solaris empty symlinks act like ".".
++ On other platforms that allow them,
++ they fail with ENOENT. */
++#ifdef __sun
++ slink[slinklen] = '\0'; /* For IS_RELATIVE_FILE_NAME. */
++ kept = nextf;
++#else
++ return -ENOENT;
++#endif
++ }
++ else if (ISSLASH (slink[slinklen - 1]))
++ {
++ /* Skip any leading slashes in the kept bytes.
++ This can matter if the symlink contains only slashes,
++ because "//" and "/" can be distinct directories. */
++ kept = nextf;
++ }
++ else
++ {
++ e[-h] = eh;
++ kept = h;
++ }
++
++ if (ISSLASH ('\\'))
++ slink[slinklen] = '\0'; /* For IS_RELATIVE_FILE_NAME. */
++
++ /* Compute the new file name by concatenating:
++ - Any old root prefix if the symlink contents are relative.
++ - The symlink contents.
++ - The last KEPT bytes of the old file name.
++ The KEPT part is already in place. */
++ char const *prefix; /* [old root prefix +] symlink contents */
++ idx_t prefixlen;
++ if (IS_RELATIVE_FILE_NAME (slink))
++ {
++ prefix = memmove (*buf, &e[-f], rootlen);
++ prefixlen = rootlen + slinklen;
++ }
++ else
++ {
++ if (*fd != dfd)
++ {
++ close (*fd);
++ *fd = dfd;
++ }
++ prefix = slink;
++ prefixlen = slinklen;
++ }
++ filenamesize = prefixlen + kept;
++ if (PATH_MAX < filenamesize)
++ return -ENAMETOOLONG;
++ memmove (&e[-filenamesize], prefix, prefixlen);
++ }
++ else
++ {
++ if (*fd != dfd)
++ close (*fd);
++ *fd = subfd;
++
++ /* SUBFD is open to the file named by the current component.
++ If requested, require it to be in the same file system. */
++ if (resolve & RESOLVE_NO_XDEV)
++ {
++ struct stat st;
++ if (ddev == UNKNOWN_DEV)
++ {
++ if (dirstat (dfd, &st) < 0)
++ return negative_errno ();
++ ddev = st.st_dev;
++ }
++ if (dirstat (subfd, &st) < 0)
++ return negative_errno ();
++ if (st.st_dev != ddev)
++ return -EXDEV;
++ }
++
++ if (final)
++ {
++ *fd = dfd;
++ return subfd;
++ }
++
++ /* The component cannot be dotlike here, so if the depth is
++ nonnegative adjust it by +1 or -1. */
++ if (0 <= depth)
++ depth += dotdot ? -1 : 1;
++
++ filenamesize = nextf;
++ }
++ }
++ }
++}
++
++/* Like openat (DFD, FILENAME, HOW->flags, HOW->mode),
++ but with extra flags in *HOW, which is of size HOWSIZE. */
++int
++openat2 (int dfd, char const *filename,
++ struct open_how const *how, size_t howsize)
++{
++ int r;
++
++#ifdef SYS_openat2
++ r = syscall (SYS_openat2, dfd, filename, how, howsize);
++ if (! (r < 0 && errno == ENOSYS))
++ return r;
++
++ /* Keep going, to support the dubious practice of compiling for an
++ older kernel. The openat2 syscall was introduced in Linux 5.6.
++ Linux 5.4 LTS is EOLed at the end of 2025, so perhaps after that
++ we can simply return the syscall result instead of continuing. */
++#endif
++
++ /* Check for invalid arguments. Once the size test has succeeded,
++ *HOW's members are safe to access, so use & and | as there is
++ little point to using && or || when invalid arguments are rare.
++ (Other parts of this file also use & and | for similar reasons.)
++ These checks mimic those of the Linux kernel: when the Linux
++ kernel is overly generous, these checks are too. */
++ if (howsize < sizeof *how)
++ r = -EINVAL;
++ else if (!memiszero (how + 1, howsize - sizeof *how))
++ r = -E2BIG;
++ else if ((how->flags
++ & ~ (O_CLOFORK | O_CLOEXEC | O_DIRECTORY
++ | O_NOFOLLOW | O_PATH
++ | (how->flags & O_PATH
++ ? 0
++ : (O_ACCMODE | O_APPEND | O_ASYNC | O_BINARY
++ | O_CREAT | O_DIRECT | O_DSYNC | O_EXCL
++ | O_IGNORE_CTTY | O_LARGEFILE | O_NDELAY
++ | O_NOATIME | O_NOCLOBBER | O_NOCTTY
++ | O_NOLINK | O_NOLINKS | O_NONBLOCK | O_NOTRANS
++ | O_RSYNC | O_SYNC
++ | O_TEXT | O_TMPFILE | O_TRUNC | O_TTY_INIT))))
++ | (!!(how->flags & O_CREAT)
++ & !!(how->flags & (O_DIRECTORY | O_TMPFILE)))
++ | (!!(how->flags & O_TMPFILE & ~O_DIRECTORY)
++ & ((how->flags & (O_ACCMODE | O_PATH)) != O_WRONLY)
++ & ((how->flags & (O_ACCMODE | O_PATH)) != O_RDWR))
++ | (how->mode
++ & ~ (how->flags & (O_CREAT | (O_TMPFILE & ~O_DIRECTORY))
++ ? (S_ISUID | S_ISGID | S_ISVTX
++ | S_IRWXU | S_IRWXG | S_IRWXO)
++ : 0))
++ | (how->resolve
++ & ~ (RESOLVE_BENEATH | RESOLVE_CACHED | RESOLVE_IN_ROOT
++ | RESOLVE_NO_MAGICLINKS | RESOLVE_NO_SYMLINKS
++ | RESOLVE_NO_XDEV))
++ | ((how->resolve & (RESOLVE_BENEATH | RESOLVE_IN_ROOT))
++ == (RESOLVE_BENEATH | RESOLVE_IN_ROOT)))
++ r = -EINVAL;
++ else
++ {
++ /* Args are valid so it is safe to use narrower types. */
++ int flags = how->flags;
++ char resolve = how->resolve;
++ mode_t mode = how->mode;
++
++ int fd;
++
++ /* For speed use a single openat if it suffices. */
++ if (O_RESOLVE_BENEATH ? !(resolve & ~RESOLVE_BENEATH) : !resolve)
++ {
++ fd = openat (dfd, filename,
++ flags | (resolve ? O_RESOLVE_BENEATH : 0), mode);
++
++ /* Return FD now unless openat failed with an errno that might
++ be an O_RESOLVE_BENEATH failure. On platforms with
++ ENOTCAPABLE that is the errno; otherwise, this is macOS 15
++ where EACCES is the errno. */
++ if (! (fd < 0 && resolve
++ && errno == (ENOTCAPABLE ? ENOTCAPABLE : EACCES)))
++ return fd;
++ }
++
++ fd = dfd;
++ char stackbuf[256];
++ char *buf = stackbuf;
++
++ r = do_openat2 (&fd, filename, flags, resolve, mode,
++ &buf, sizeof stackbuf);
++
++ if (fd != dfd)
++ close (fd);
++ if (buf != stackbuf)
++ free (buf);
++ }
++
++ if (r < 0)
++ {
++ errno = -r;
++ return -1;
++ }
++ return r;
++}
+diff --git a/m4/fcntl_h.m4 b/m4/fcntl_h.m4
+index 68f4e64..30317a6 100644
+--- a/m4/fcntl_h.m4
++++ b/m4/fcntl_h.m4
+@@ -1,6 +1,6 @@
+-# serial 20
+-# Configure fcntl.h.
+-dnl Copyright (C) 2006-2007, 2009-2023 Free Software Foundation, Inc.
++# fcntl_h.m4
++# serial 21
++dnl Copyright (C) 2006-2007, 2009-2025 Free Software Foundation, Inc.
+ dnl This file is free software; the Free Software Foundation
+ dnl gives unlimited permission to copy and/or distribute it,
+ dnl with or without modifications, as long as this notice is preserved.
+@@ -23,7 +23,7 @@ AC_DEFUN_ONCE([gl_FCNTL_H],
+ dnl corresponding gnulib module is not in use, if it is not common
+ dnl enough to be declared everywhere.
+ gl_WARN_ON_USE_PREPARE([[#include <fcntl.h>
+- ]], [fcntl openat])
++ ]], [fcntl openat openat2])
+ ])
+
+ # gl_FCNTL_MODULE_INDICATOR([modulename])
+@@ -50,6 +50,7 @@ AC_DEFUN([gl_FCNTL_H_REQUIRE_DEFAULTS],
+ gl_MODULE_INDICATOR_INIT_VARIABLE([GNULIB_NONBLOCKING])
+ gl_MODULE_INDICATOR_INIT_VARIABLE([GNULIB_OPEN])
+ gl_MODULE_INDICATOR_INIT_VARIABLE([GNULIB_OPENAT])
++ gl_MODULE_INDICATOR_INIT_VARIABLE([GNULIB_OPENAT2])
+ dnl Support Microsoft deprecated alias function names by default.
+ gl_MODULE_INDICATOR_INIT_VARIABLE([GNULIB_MDA_CREAT], [1])
+ gl_MODULE_INDICATOR_INIT_VARIABLE([GNULIB_MDA_OPEN], [1])
+@@ -63,6 +64,7 @@ AC_DEFUN([gl_FCNTL_H_DEFAULTS],
+ dnl Assume proper GNU behavior unless another module says otherwise.
+ HAVE_FCNTL=1; AC_SUBST([HAVE_FCNTL])
+ HAVE_OPENAT=1; AC_SUBST([HAVE_OPENAT])
++ HAVE_OPENAT2=0; AC_SUBST([HAVE_OPENAT2])
+ REPLACE_CREAT=0; AC_SUBST([REPLACE_CREAT])
+ REPLACE_FCNTL=0; AC_SUBST([REPLACE_FCNTL])
+ REPLACE_OPEN=0; AC_SUBST([REPLACE_OPEN])
+diff --git a/m4/gnulib-comp.m4 b/m4/gnulib-comp.m4
+index 33c295d..0a4f371 100644
+--- a/m4/gnulib-comp.m4
++++ b/m4/gnulib-comp.m4
+@@ -237,6 +237,7 @@ AC_DEFUN([gl_EARLY],
+ # Code from module openat-die:
+ # Code from module openat-h:
+ # Code from module openat-safer:
++ # Code from module openat2:
+ # Code from module opendir:
+ # Code from module opendirat:
+ # Code from module parse-datetime:
+@@ -1064,6 +1065,13 @@ AC_DEFUN([gl_INIT],
+ gl_FCNTL_MODULE_INDICATOR([openat])
+ gl_OPENAT_SAFER
+ gl_MODULE_INDICATOR([openat-safer])
++ gl_FUNC_OPENAT2
++ gl_CONDITIONAL([GL_COND_OBJ_OPENAT2],
++ [test $HAVE_OPENAT2 = 0])
++ AM_COND_IF([GL_COND_OBJ_OPENAT], [
++ gl_PREREQ_OPENAT2
++ ])
++ gl_FCNTL_MODULE_INDICATOR([openat2])
+ gl_FUNC_OPENDIR
+ gl_CONDITIONAL([GL_COND_OBJ_OPENDIR],
+ [test $HAVE_OPENDIR = 0 || test $REPLACE_OPENDIR = 1])
+@@ -2020,6 +2028,7 @@ AC_DEFUN([gl_FILE_LIST], [
+ lib/openat-safer.c
+ lib/openat.c
+ lib/openat.h
++ lib/openat2.c
+ lib/opendir-safer.c
+ lib/opendir.c
+ lib/opendirat.c
+@@ -2386,6 +2395,7 @@ AC_DEFUN([gl_FILE_LIST], [
+ m4/open-slash.m4
+ m4/open.m4
+ m4/openat.m4
++ m4/openat2.m4
+ m4/opendir.m4
+ m4/parse-datetime.m4
+ m4/pathmax.m4
+diff --git a/m4/openat2.m4 b/m4/openat2.m4
+new file mode 100644
+index 0000000..891654f
+--- /dev/null
++++ b/m4/openat2.m4
+@@ -0,0 +1,33 @@
++# openat2.m4
++# serial 1
++
++dnl Copyright 2025 Free Software Foundation, Inc.
++dnl This file is free software; the Free Software Foundation
++dnl gives unlimited permission to copy and/or distribute it,
++dnl with or without modifications, as long as this notice is preserved.
++dnl This file is offered as-is, without any warranty.
++
++# Written by Paul Eggert.
++
++AC_DEFUN([gl_FUNC_OPENAT2],
++[
++ AC_REQUIRE([gl_FCNTL_H_DEFAULTS])
++ AC_REQUIRE([gl_USE_SYSTEM_EXTENSIONS])
++ AC_REQUIRE([gl_FCNTL_O_FLAGS])
++ AC_CHECK_FUNCS_ONCE([openat2])
++ AS_CASE([$ac_cv_func_openat2],
++ [yes], [HAVE_OPENAT2=1])
++])
++
++# Prerequisites of lib/openat2.c.
++AC_DEFUN([gl_PREREQ_OPENAT2],
++[
++ AC_CHECK_FUNCS_ONCE([fstatfs])
++ AC_CHECK_HEADERS_ONCE([sys/vfs.h])
++ AS_CASE([$ac_cv_func_fstatfs,$ac_cv_header_sys_vfs_h],
++ [yes,yes],
++ [AC_CHECK_MEMBERS([struct statfs.f_type], [], [],
++ [[$ac_includes_default
++ #include <sys/vfs.h>
++ ]])])
++])
+diff --git a/src/common.h b/src/common.h
+index 9451e13..b4a5821 100644
+--- a/src/common.h
++++ b/src/common.h
+@@ -401,7 +401,7 @@ GLOBAL struct stat archive_stat;
+
+ /* Flags for reading, searching, and fstatatting files. */
+ GLOBAL int open_read_flags;
+-GLOBAL int open_searchdir_flags;
++GLOBAL struct open_how open_searchdir_how;
+ GLOBAL int fstatat_flags;
+
+ GLOBAL int seek_option;
+@@ -644,6 +644,7 @@ void assign_string_n (char **string, const char *value, size_t n);
+ #define ASSIGN_STRING_N(s,v) assign_string_n (s, v, sizeof (v))
+ int unquote_string (char *str);
+ char *zap_slashes (char *name);
++idx_t dotslashlen (char const *);
+ char *normalize_filename (int cdidx, const char *name);
+ void normalize_filename_x (char *name);
+ void replace_prefix (char **pname, const char *samp, size_t slen,
+@@ -722,10 +723,17 @@ int deref_stat (char const *name, struct stat *buf);
+ size_t blocking_read (int fd, void *buf, size_t count);
+ size_t blocking_write (int fd, void const *buf, size_t count);
+
++/* Not valid as the first argument to openat.
++ It is a negative integer not equal to AT_FDCWD. */
++enum { BADFD = AT_FDCWD == -1 ? -2 : -1 };
++
+ extern int chdir_current;
+-extern int chdir_fd;
+ int chdir_arg (char const *dir);
+ void chdir_do (int dir);
++struct chdir_id { int err; dev_t st_dev; ino_t st_ino; } chdir_id (void);
++struct fdbase { int fd; char const *base; } fdbase (char const *);
++struct fdbase fdbase1 (char const *);
++void fdbase_clear (void);
+ int chdir_count (void);
+
+ void close_diag (char const *name);
+diff --git a/src/compare.c b/src/compare.c
+index 4f9824c..a8e1260 100644
+--- a/src/compare.c
++++ b/src/compare.c
+@@ -218,7 +218,9 @@ diff_file (void)
+ }
+ else
+ {
+- diff_handle = openat (chdir_fd, file_name, open_read_flags);
++ struct fdbase f = fdbase (file_name);
++ diff_handle = (f.fd == BADFD ? -1
++ : openat (f.fd, f.base, open_read_flags));
+
+ if (diff_handle < 0)
+ {
+@@ -239,7 +241,7 @@ diff_file (void)
+ && stat_data.st_size != 0)
+ {
+ struct timespec atime = get_stat_atime (&stat_data);
+- if (set_file_atime (diff_handle, chdir_fd, file_name, atime)
++ if (set_file_atime (diff_handle, f.fd, f.base, atime)
+ != 0)
+ utime_error (file_name);
+ }
+@@ -274,9 +276,9 @@ diff_symlink (void)
+ char buf[1024];
+ size_t len = strlen (current_stat_info.link_name);
+ char *linkbuf = len < sizeof buf ? buf : xmalloc (len + 1);
+-
+- ssize_t status = readlinkat (chdir_fd, current_stat_info.file_name,
+- linkbuf, len + 1);
++ struct fdbase f = fdbase (current_stat_info.file_name);
++ ssize_t status = (f.fd == BADFD ? -1
++ : readlinkat (f.fd, f.base, linkbuf, len + 1));
+
+ if (status < 0)
+ {
+@@ -403,7 +405,7 @@ static void
+ diff_multivol (void)
+ {
+ struct stat stat_data;
+- int fd, status;
++ int status;
+ off_t offset;
+
+ if (current_stat_info.had_trailing_slash)
+@@ -433,7 +435,8 @@ diff_multivol (void)
+ }
+
+
+- fd = openat (chdir_fd, current_stat_info.file_name, open_read_flags);
++ struct fdbase f = fdbase (current_stat_info.file_name);
++ int fd = f.fd == BADFD ? -1 : openat (f.fd, f.base, open_read_flags);
+
+ if (fd < 0)
+ {
+diff --git a/src/create.c b/src/create.c
+index d20178c..845b194 100644
+--- a/src/create.c
++++ b/src/create.c
+@@ -1376,8 +1376,10 @@ create_archive (void)
+ {
+ if (! st.orig_file_name)
+ {
+- int fd = openat (chdir_fd, p->name,
+- open_searchdir_flags);
++ struct fdbase f = fdbase (p->name);
++ int fd = (f.fd == BADFD ? -1
++ : openat (f.fd, f.base,
++ open_searchdir_how.flags));
+ if (fd < 0)
+ {
+ file_removed_diag (p->name, !p->parent,
+@@ -1579,9 +1581,18 @@ subfile_open (struct tar_stat_info const *dir, char const *file, int flags)
+ gettext ("");
+ }
+
+- while ((fd = openat (dir ? dir->fd : chdir_fd, file, flags)) < 0
+- && open_failure_recover (dir))
+- continue;
++ do
++ {
++ if (dir)
++ fd = openat (dir->fd, file, flags);
++ else
++ {
++ struct fdbase f = fdbase (file);
++ fd = f.fd == BADFD ? -1 : openat (f.fd, f.base, flags);
++ }
++ }
++ while (fd < 0 && open_failure_recover (dir));
++
+ return fd;
+ }
+
+@@ -1595,7 +1606,7 @@ restore_parent_fd (struct tar_stat_info const *st)
+ struct tar_stat_info *parent = st->parent;
+ if (parent && ! parent->fd)
+ {
+- int parentfd = openat (st->fd, "..", open_searchdir_flags);
++ int parentfd = openat (st->fd, "..", open_searchdir_how.flags);
+ struct stat parentstat;
+
+ if (parentfd < 0)
+@@ -1610,8 +1621,9 @@ restore_parent_fd (struct tar_stat_info const *st)
+
+ if (parentfd < 0)
+ {
+- int origfd = openat (chdir_fd, parent->orig_file_name,
+- open_searchdir_flags);
++ struct fdbase f = fdbase (parent->orig_file_name);
++ int origfd = (f.fd == BADFD ? -1
++ : openat (f.fd, f.base, open_searchdir_how.flags));
+ if (0 <= origfd)
+ {
+ if (fstat (parentfd, &parentstat) == 0
+@@ -1644,7 +1656,6 @@ dump_file0 (struct tar_stat_info *st, char const *name, char const *p)
+ bool is_dir;
+ struct tar_stat_info const *parent = st->parent;
+ bool top_level = ! parent;
+- int parentfd = top_level ? chdir_fd : parent->fd;
+ void (*diag) (char const *) = 0;
+
+ if (interactive_option && !confirm ("add", p))
+@@ -1656,12 +1667,24 @@ dump_file0 (struct tar_stat_info *st, char const *name, char const *p)
+
+ transform_name (&st->file_name, XFORM_REGFILE);
+
+- if (parentfd < 0 && ! top_level)
++ struct fdbase f;
++ if (top_level)
++ f = fdbase (name);
++ else
++ {
++ /* Avoid a compound literal here, to work around a bug
++ in Oracle Developer Studio 12.6 (sparc64). */
++ f.fd = parent->fd;
++ f.base = name;
++ }
++
++ if (!top_level && parent->fd < 0)
+ {
+- errno = - parentfd;
++ errno = - parent->fd;
+ diag = open_diag;
+ }
+- else if (fstatat (parentfd, name, &st->stat, fstatat_flags) != 0)
++ else if (f.fd == BADFD
++ || fstatat (f.fd, f.base, &st->stat, fstatat_flags) != 0)
+ diag = stat_diag;
+ else if (file_dumpable_p (&st->stat))
+ {
+@@ -1737,9 +1760,9 @@ dump_file0 (struct tar_stat_info *st, char const *name, char const *p)
+ bool ok;
+ struct stat st2;
+
+- xattrs_acls_get (parentfd, name, st, !is_dir);
+- xattrs_selinux_get (parentfd, name, st, fd);
+- xattrs_xattrs_get (parentfd, name, st, fd);
++ xattrs_acls_get (f.fd, f.base, st, !is_dir);
++ xattrs_selinux_get (f.fd, f.base, st, fd);
++ xattrs_xattrs_get (f.fd, f.base, st, fd);
+
+ if (is_dir)
+ {
+@@ -1757,7 +1780,15 @@ dump_file0 (struct tar_stat_info *st, char const *name, char const *p)
+ ok = dump_dir (st);
+
+ fd = st->fd;
+- parentfd = top_level ? chdir_fd : parent->fd;
++ if (top_level)
++ f = fdbase (name);
++ else
++ {
++ /* Avoid a compound literal here, to work around a bug
++ in Oracle Developer Studio 12.6 (sparc64). */
++ f.fd = parent->fd;
++ f.base = name;
++ }
+ }
+ else
+ {
+@@ -1798,9 +1829,9 @@ dump_file0 (struct tar_stat_info *st, char const *name, char const *p)
+ }
+ else if (fd == 0)
+ {
+- if (parentfd < 0 && ! top_level)
++ if (!top_level && parent->fd < 0)
+ {
+- errno = - parentfd;
++ errno = - parent->fd;
+ ok = false;
+ }
+ }
+@@ -1851,7 +1882,7 @@ dump_file0 (struct tar_stat_info *st, char const *name, char const *p)
+ }
+ else if (atime_preserve_option == replace_atime_preserve
+ && timespec_cmp (st->atime, get_stat_atime (&st2)) != 0
+- && set_file_atime (fd, parentfd, name, st->atime) != 0)
++ && set_file_atime (fd, f.fd, f.base, st->atime) != 0)
+ utime_error (p);
+ }
+
+@@ -1864,7 +1895,7 @@ dump_file0 (struct tar_stat_info *st, char const *name, char const *p)
+ #ifdef HAVE_READLINK
+ else if (S_ISLNK (st->stat.st_mode))
+ {
+- st->link_name = areadlinkat_with_size (parentfd, name, st->stat.st_size);
++ st->link_name = areadlinkat_with_size (f.fd, f.base, st->stat.st_size);
+ if (!st->link_name)
+ {
+ if (errno == ENOMEM)
+@@ -1877,8 +1908,8 @@ dump_file0 (struct tar_stat_info *st, char const *name, char const *p)
+ < strlen (st->link_name))
+ write_long_link (st);
+
+- xattrs_selinux_get (parentfd, name, st, 0);
+- xattrs_xattrs_get (parentfd, name, st, 0);
++ xattrs_selinux_get (f.fd, f.base, st, 0);
++ xattrs_xattrs_get (f.fd, f.base, st, 0);
+
+ block_ordinal = current_block_ordinal ();
+ st->stat.st_size = 0; /* force 0 size on symlink */
+@@ -1900,23 +1931,23 @@ dump_file0 (struct tar_stat_info *st, char const *name, char const *p)
+ else if (S_ISCHR (st->stat.st_mode))
+ {
+ type = CHRTYPE;
+- xattrs_acls_get (parentfd, name, st, true);
+- xattrs_selinux_get (parentfd, name, st, 0);
+- xattrs_xattrs_get (parentfd, name, st, 0);
++ xattrs_acls_get (f.fd, f.base, st, true);
++ xattrs_selinux_get (f.fd, f.base, st, 0);
++ xattrs_xattrs_get (f.fd, f.base, st, 0);
+ }
+ else if (S_ISBLK (st->stat.st_mode))
+ {
+ type = BLKTYPE;
+- xattrs_acls_get (parentfd, name, st, true);
+- xattrs_selinux_get (parentfd, name, st, 0);
+- xattrs_xattrs_get (parentfd, name, st, 0);
++ xattrs_acls_get (f.fd, f.base, st, true);
++ xattrs_selinux_get (f.fd, f.base, st, 0);
++ xattrs_xattrs_get (f.fd, f.base, st, 0);
+ }
+ else if (S_ISFIFO (st->stat.st_mode))
+ {
+ type = FIFOTYPE;
+- xattrs_acls_get (parentfd, name, st, true);
+- xattrs_selinux_get (parentfd, name, st, 0);
+- xattrs_xattrs_get (parentfd, name, st, 0);
++ xattrs_acls_get (f.fd, f.base, st, true);
++ xattrs_selinux_get (f.fd, f.base, st, 0);
++ xattrs_xattrs_get (f.fd, f.base, st, 0);
+ }
+ else if (S_ISSOCK (st->stat.st_mode))
+ {
+diff --git a/src/exclist.c b/src/exclist.c
+index 92b814f..8886272 100644
+--- a/src/exclist.c
++++ b/src/exclist.c
+@@ -79,7 +79,7 @@ info_attach_exclist (struct tar_stat_info *dir)
+ return;
+ for (file = excfile_head; file; file = file->next)
+ {
+- if (faccessat (dir ? dir->fd : chdir_fd, file->name, F_OK, 0) == 0)
++ if (faccessat (dir->fd, file->name, F_OK, 0) == 0)
+ {
+ FILE *fp;
+ struct exclude *ex = NULL;
+@@ -178,12 +178,7 @@ excluded_name (char const *name, struct tar_stat_info *st)
+ break;
+
+ if (!rname)
+- {
+- rname = name;
+- /* Skip leading ./ */
+- while (*rname == '.' && ISSLASH (rname[1]))
+- rname += 2;
+- }
++ rname = name + dotslashlen (name);
+ if ((result = excluded_file_name (ep->excluded, rname)))
+ break;
+
+diff --git a/src/extract.c b/src/extract.c
+index 3e3932b..ae03190 100644
+--- a/src/extract.c
++++ b/src/extract.c
+@@ -246,7 +246,8 @@ fd_i_chmod (int fd, char const *file, mode_t mode, int atflag)
+ if (result == 0 || implemented (errno))
+ return result;
+ }
+- return fchmodat (chdir_fd, file, mode, atflag);
++ struct fdbase f = fdbase (file);
++ return f.fd == BADFD ? -1 : fchmodat (f.fd, f.base, mode, atflag);
+ }
+
+ /* A version of fd_i_chmod which gracefully handles several common error
+@@ -295,16 +296,18 @@ fd_chown (int fd, char const *file, uid_t uid, gid_t gid, int atflag)
+ if (result == 0 || implemented (errno))
+ return result;
+ }
+- return fchownat (chdir_fd, file, uid, gid, atflag);
++ struct fdbase f = fdbase (file);
++ return f.fd == BADFD ? -1 : fchownat (f.fd, f.base, uid, gid, atflag);
+ }
+
+ /* Use fstat if possible, fstatat otherwise. */
+ static int
+ fd_stat (int fd, char const *file, struct stat *st, int atflag)
+ {
+- return (0 <= fd
+- ? fstat (fd, st)
+- : fstatat (chdir_fd, file, st, atflag));
++ if (0 <= fd)
++ return fstat (fd, st);
++ struct fdbase f = fdbase (file);
++ return f.fd == BADFD ? -1 : fstatat (f.fd, f.base, st, atflag);
+ }
+
+ /* Set the mode for FILE_NAME to MODE.
+@@ -402,7 +405,15 @@ set_stat (char const *file_name,
+ ts[0].tv_nsec = UTIME_OMIT;
+ ts[1] = st->mtime;
+
+- if (fdutimensat (fd, chdir_fd, file_name, ts, atflag) == 0)
++ int r;
++ if (0 <= fd)
++ r = futimens (fd, ts);
++ if (fd < 0 || (r < 0 && errno == ENOSYS))
++ {
++ struct fdbase f = fdbase (file_name);
++ r = f.fd == BADFD ? -1 : utimensat (f.fd, f.base, ts, atflag);
++ }
++ if (r == 0)
+ {
+ if (incremental_option)
+ check_time (file_name, ts[0]);
+@@ -522,8 +533,9 @@ delay_set_stat (char const *file_name, struct tar_stat_info const *st,
+ if (data->interdir)
+ {
+ struct stat real_st;
+- if (fstatat (chdir_fd, data->file_name,
+- &real_st, data->atflag) != 0)
++ struct fdbase f = fdbase (data->file_name);
++ if (f.fd == BADFD
++ || fstatat (f.fd, f.base, &real_st, data->atflag) != 0)
+ {
+ stat_error (data->file_name);
+ }
+@@ -604,7 +616,8 @@ repair_delayed_set_stat (char const *dir,
+ for (data = delayed_set_stat_head; data; data = data->next)
+ {
+ struct stat st;
+- if (fstatat (chdir_fd, data->file_name, &st, data->atflag) != 0)
++ struct fdbase f = fdbase (data->file_name);
++ if (f.fd == BADFD || fstatat (f.fd, f.base, &st, data->atflag) != 0)
+ {
+ stat_error (data->file_name);
+ return;
+@@ -718,7 +731,8 @@ make_directories (char *file_name, bool *interdir_made)
+ *cursor = '\0'; /* truncate the name there */
+ desired_mode = MODE_RWX & ~ newdir_umask;
+ mode = desired_mode | (we_are_root ? 0 : MODE_WXUSR);
+- status = mkdirat (chdir_fd, file_name, mode);
++ struct fdbase f = fdbase (file_name);
++ status = f.fd == BADFD ? -1 : mkdirat (f.fd, f.base, mode);
+
+ if (status == 0)
+ {
+@@ -761,8 +775,9 @@ make_directories (char *file_name, bool *interdir_made)
+ process may have created it, so check whether it exists now. */
+ *parent_end = '\0';
+ struct stat st;
+- int stat_status = fstatat (chdir_fd, file_name, &st, 0);
+- if (!stat_status && !S_ISDIR (st.st_mode))
++ struct fdbase f = fdbase (file_name);
++ int stat_status = f.fd == BADFD ? -1 : fstatat (f.fd, f.base, &st, 0);
++ if (! (stat_status < 0 || S_ISDIR (st.st_mode)))
+ stat_status = -1;
+ if (stat_status)
+ {
+@@ -918,7 +933,8 @@ set_xattr (char const *file_name, struct tar_stat_info const *st,
+ #ifdef HAVE_XATTRS
+ if ((xattrs_option > 0) && st->xattr_map.xm_size)
+ {
+- int r = mknodat (chdir_fd, file_name, mode, 0);
++ struct fdbase f = fdbase (file_name);
++ int r = f.fd == BADFD ? -1 : mknodat (f.fd, f.base, mode, 0);
+ if (r < 0)
+ return r;
+ xattrs_xattrs_set (st, file_name, typeflag, 0);
+@@ -962,7 +978,8 @@ apply_nonancestor_delayed_set_stat (char const *file_name, bool after_links)
+
+ if (check_for_renamed_directories)
+ {
+- if (fstatat (chdir_fd, data->file_name, &st, data->atflag) != 0)
++ struct fdbase f = fdbase (data->file_name);
++ if (f.fd == BADFD || fstatat (f.fd, f.base, &st, data->atflag) != 0)
+ {
+ stat_error (data->file_name);
+ skip_this_one = 1;
+@@ -1010,8 +1027,9 @@ static bool
+ is_directory_link (char const *file_name, struct stat *st)
+ {
+ char buf[1];
+- return (0 <= readlinkat (chdir_fd, file_name, buf, sizeof buf)
+- && fstatat (chdir_fd, file_name, st, 0) == 0
++ struct fdbase f = fdbase (file_name);
++ return (f.fd != BADFD && 0 <= readlinkat (f.fd, f.base, buf, sizeof buf)
++ && fstatat (f.fd, f.base, st, 0) == 0
+ && S_ISDIR (st->st_mode));
+ }
+
+@@ -1050,12 +1068,14 @@ extract_dir (char *file_name, int typeflag)
+ /* Save 'root device' to avoid purging mount points. */
+ if (one_file_system_option && root_device == 0)
+ {
+- struct stat st;
+-
+- if (fstatat (chdir_fd, ".", &st, 0) != 0)
+- stat_diag (".");
++ struct chdir_id id = chdir_id ();
++ if (id.err)
++ {
++ errno = id.err;
++ stat_diag (".");
++ }
+ else
+- root_device = st.st_dev;
++ root_device = id.st_dev;
+ }
+
+ if (incremental_option)
+@@ -1068,7 +1088,8 @@ extract_dir (char *file_name, int typeflag)
+
+ for (;;)
+ {
+- status = mkdirat (chdir_fd, file_name, mode);
++ struct fdbase f = fdbase (file_name);
++ status = f.fd == BADFD ? -1 : mkdirat (f.fd, f.base, mode);
+ if (status == 0)
+ {
+ current_mode = mode & ~ current_umask;
+@@ -1200,21 +1221,24 @@ open_output_file (char const *file_name, int typeflag, mode_t mode,
+ }
+ }
+
++ struct fdbase f = fdbase (file_name);
++
+ /* If O_NOFOLLOW is needed but does not work, check for a symlink
+ separately. There's a race condition, but that cannot be avoided
+ on hosts lacking O_NOFOLLOW. */
+ if (! HAVE_WORKING_O_NOFOLLOW
+- && overwriting_old_files && ! dereference_option)
++ && overwriting_old_files && ! dereference_option
++ && f.fd != BADFD)
+ {
+ char buf[1];
+- if (0 <= readlinkat (chdir_fd, file_name, buf, sizeof buf))
++ if (0 <= readlinkat (f.fd, f.base, buf, sizeof buf))
+ {
+ errno = ELOOP;
+ return -1;
+ }
+ }
+
+- fd = openat (chdir_fd, file_name, openflag, mode);
++ fd = f.fd == BADFD ? -1 : openat (f.fd, f.base, openflag, mode);
+ if (0 <= fd)
+ {
+ if (openflag & O_EXCL)
+@@ -1380,7 +1404,8 @@ find_delayed_link_source (char const *name)
+ if (!delayed_link_table)
+ return false;
+
+- if (fstatat (chdir_fd, name, &st, AT_SYMLINK_NOFOLLOW))
++ struct fdbase f = fdbase (name);
++ if (f.fd == BADFD || fstatat (f.fd, f.base, &st, AT_SYMLINK_NOFOLLOW))
+ {
+ if (errno != ENOENT)
+ stat_error (name);
+@@ -1406,8 +1431,16 @@ create_placeholder_file (char *file_name, bool is_symlink, bool *interdir_made)
+ int fd;
+ struct stat st;
+
+- while ((fd = openat (chdir_fd, file_name, O_WRONLY | O_CREAT | O_EXCL, 0)) < 0)
++ for (;;)
+ {
++ struct fdbase f = fdbase (file_name);
++ if (f.fd != BADFD)
++ {
++ fd = openat (f.fd, f.base, O_WRONLY | O_CREAT | O_EXCL, 0);
++ if (0 <= fd)
++ break;
++ }
++
+ if (errno == EEXIST && find_delayed_link_source (file_name))
+ {
+ /* The placeholder file has already been created. This means
+@@ -1429,7 +1462,7 @@ create_placeholder_file (char *file_name, bool is_symlink, bool *interdir_made)
+ open_error (file_name);
+ return -1;
+ }
+- }
++ }
+
+ if (fstat (fd, &st) != 0)
+ {
+@@ -1506,15 +1539,23 @@ extract_link (char *file_name, MAYBE_UNUSED int typeflag)
+
+ do
+ {
+- struct stat st1, st2;
+- int e;
+- int status = linkat (chdir_fd, link_name, chdir_fd, file_name, 0);
+- e = errno;
++ struct stat st, st1;
++ int status;
++
++ struct fdbase f = fdbase (file_name), f1;
++ if (f.fd == BADFD)
++ status = -1;
++ else
++ {
++ f1 = fdbase1 (link_name);
++ status = (f1.fd == BADFD ? -1
++ : linkat (f1.fd, f1.base, f.fd, f.base, 0));
++ }
+
+ if (status == 0)
+ {
+ if (delayed_link_table
+- && fstatat (chdir_fd, link_name, &st1, AT_SYMLINK_NOFOLLOW) == 0)
++ && fstatat (f1.fd, f1.base, &st1, AT_SYMLINK_NOFOLLOW) == 0)
+ {
+ struct delayed_link dl1;
+ dl1.ino = st1.st_ino;
+@@ -1534,15 +1575,15 @@ extract_link (char *file_name, MAYBE_UNUSED int typeflag)
+
+ return 0;
+ }
+- else if ((e == EEXIST && strcmp (link_name, file_name) == 0)
+- || ((fstatat (chdir_fd, link_name, &st1, AT_SYMLINK_NOFOLLOW)
+- == 0)
+- && (fstatat (chdir_fd, file_name, &st2, AT_SYMLINK_NOFOLLOW)
+- == 0)
+- && st1.st_dev == st2.st_dev
+- && st1.st_ino == st2.st_ino))
+- return 0;
+
++ int e = errno;
++ if ((e == EEXIST && strcmp (link_name, file_name) == 0)
++ || (f.fd != BADFD && f1.fd != BADFD
++ && fstatat (f1.fd, f1.base, &st1, AT_SYMLINK_NOFOLLOW) == 0
++ && fstatat (f.fd, f.base, &st, AT_SYMLINK_NOFOLLOW) == 0
++ && st1.st_dev == st.st_dev
++ && st1.st_ino == st.st_ino))
++ return 0;
+ errno = e;
+ }
+ while ((rc = maybe_recoverable (file_name, false, &interdir_made))
+@@ -1569,7 +1610,10 @@ extract_symlink (char *file_name, MAYBE_UNUSED int typeflag)
+ || contains_dot_dot (current_stat_info.link_name)))
+ return create_placeholder_file (file_name, true, &interdir_made);
+
+- while (symlinkat (current_stat_info.link_name, chdir_fd, file_name) != 0)
++ for (struct fdbase f;
++ ((f = fdbase (file_name)).fd == BADFD
++ || symlinkat (current_stat_info.link_name, f.fd, f.base) != 0);
++ )
+ switch (maybe_recoverable (file_name, false, &interdir_made))
+ {
+ case RECOVER_OK:
+@@ -1609,8 +1653,10 @@ extract_node (char *file_name, int typeflag)
+ mode_t mode = (current_stat_info.stat.st_mode & (MODE_RWX | S_IFBLK | S_IFCHR)
+ & ~ (0 < same_owner_option ? S_IRWXG | S_IRWXO : 0));
+
+- while (mknodat (chdir_fd, file_name, mode, current_stat_info.stat.st_rdev)
+- != 0)
++ for (struct fdbase f;
++ ((f = fdbase (file_name)).fd == BADFD
++ || mknodat (f.fd, f.base, mode, current_stat_info.stat.st_rdev) != 0);
++ )
+ switch (maybe_recoverable (file_name, false, &interdir_made))
+ {
+ case RECOVER_OK:
+@@ -1639,7 +1685,10 @@ extract_fifo (char *file_name, int typeflag)
+ mode_t mode = (current_stat_info.stat.st_mode & MODE_RWX
+ & ~ (0 < same_owner_option ? S_IRWXG | S_IRWXO : 0));
+
+- while (mkfifoat (chdir_fd, file_name, mode) != 0)
++ for (struct fdbase f;
++ ((f = fdbase (file_name)).fd == BADFD
++ || mkfifoat (f.fd, f.base, mode) != 0);
++ )
+ switch (maybe_recoverable (file_name, false, &interdir_made))
+ {
+ case RECOVER_OK:
+@@ -1862,7 +1911,7 @@ static void
+ apply_delayed_link (struct delayed_link *ds)
+ {
+ struct string_list *sources = ds->sources;
+- char const *valid_source = 0;
++ char const *valid_source = NULL;
+
+ chdir_do (ds->change_dir);
+
+@@ -1874,25 +1923,30 @@ apply_delayed_link (struct delayed_link *ds)
+ /* Make sure the placeholder file is still there. If not,
+ don't create a link, as the placeholder was probably
+ removed by a later extraction. */
+- if (fstatat (chdir_fd, source, &st, AT_SYMLINK_NOFOLLOW) == 0
++ struct fdbase f = fdbase (source);
++ if (f.fd != BADFD && fstatat (f.fd, f.base, &st, AT_SYMLINK_NOFOLLOW) == 0
+ && st.st_dev == ds->dev
+ && st.st_ino == ds->ino
+ && BIRTHTIME_EQ (get_stat_birthtime (&st), ds->birthtime))
+ {
+ /* Unlink the placeholder, then create a hard link if possible,
+ a symbolic link otherwise. */
+- if (unlinkat (chdir_fd, source, 0) != 0)
++ struct fdbase f1;
++ if (unlinkat (f.fd, f.base, 0) != 0)
+ unlink_error (source);
+ else if (valid_source
+- && (linkat (chdir_fd, valid_source, chdir_fd, source, 0)
+- == 0))
++ && ((f1 = f.fd == BADFD ? f : fdbase1 (valid_source)).fd
++ != BADFD)
++ && linkat (f1.fd, f1.base, f.fd, f.base, 0) == 0)
+ ;
+ else if (!ds->is_symlink)
+ {
+- if (linkat (chdir_fd, ds->target, chdir_fd, source, 0) != 0)
++ f1 = f.fd == BADFD ? f : fdbase1 (ds->target);
++ if (f1.fd == BADFD
++ || linkat (f1.fd, f1.base, f.fd, f.base, 0) != 0)
+ link_error (ds->target, source);
+ }
+- else if (symlinkat (ds->target, chdir_fd, source) != 0)
++ else if (symlinkat (ds->target, f.fd, f.base) != 0)
+ symlink_error (ds->target, source);
+ else
+ {
+@@ -1968,9 +2022,14 @@ extract_finish (void)
+ bool
+ rename_directory (char *src, char *dst)
+ {
+- if (renameat (chdir_fd, src, chdir_fd, dst) == 0)
+- fixup_delayed_set_stat (src, dst);
+- else
++ struct fdbase f1 = fdbase1 (src);
++ struct fdbase f = f1.fd == BADFD ? f1 : fdbase (dst);
++ if (f.fd != BADFD && renameat (f1.fd, f1.base, f.fd, f.base) == 0)
++ {
++ fdbase_clear ();
++ fixup_delayed_set_stat (src, dst);
++ }
++ else if (f1.fd != BADFD)
+ {
+ int e = errno;
+
+@@ -1979,8 +2038,13 @@ rename_directory (char *src, char *dst)
+ case ENOENT:
+ if (make_directories (dst, NULL) == 0)
+ {
+- if (renameat (chdir_fd, src, chdir_fd, dst) == 0)
+- return true;
++ f = fdbase (dst);
++ if (f.fd != BADFD
++ && renameat (f1.fd, f1.base, f.fd, f.base) == 0)
++ {
++ fdbase_clear ();
++ return true;
++ }
+ e = errno;
+ }
+ break;
+diff --git a/src/misc.c b/src/misc.c
+index 73c0464..0e874eb 100644
+--- a/src/misc.c
++++ b/src/misc.c
+@@ -269,6 +269,30 @@ zap_slashes (char *name)
+ return name;
+ }
+
++/* The number of file name slashes at the start of the string F. */
++static idx_t
++slashlen (char const *f)
++{
++ idx_t i = 0;
++ while (ISSLASH (f[i]))
++ i++;
++ return i;
++}
++
++/* The length of the longest initial prefix of F that consists
++ entirely of a sequence of '.'s each followed by one or more slashes.
++ This prefix acts like the working directory, i.e., the file name F
++ acts like "." if 0 < dotslashlen (F) == strlen (F), and acts like
++ &F[dotslashlen (F)] otherwise. */
++idx_t
++dotslashlen (char const *f)
++{
++ idx_t i = 0;
++ while (f[i] == '.' && ISSLASH (f[i + 1]))
++ i += 2 + slashlen (&f[i + 2]);
++ return i;
++}
++
+ /* Normalize FILE_NAME by removing redundant slashes and "."
+ components, including redundant trailing slashes.
+ Leave ".." alone, as it may be significant in the presence
+@@ -279,8 +303,6 @@ void
+ normalize_filename_x (char *file_name)
+ {
+ char *name = file_name + FILE_SYSTEM_PREFIX_LEN (file_name);
+- char *p;
+- char const *q;
+ char c;
+
+ /* Don't squeeze leading "//" to "/", on hosts where they're distinct. */
+@@ -288,16 +310,20 @@ normalize_filename_x (char *file_name)
+ && ISSLASH (*name) && ISSLASH (name[1]) && ! ISSLASH (name[2]));
+
+ /* Omit redundant leading "." components. */
+- for (q = p = name; (*p = *q) == '.' && ISSLASH (q[1]); p += !*q)
+- for (q += 2; ISSLASH (*q); q++)
+- continue;
++ char *p = name;
++ char const *q = name + dotslashlen (name);
++
++ if (p < q && !*q)
++ q = "."; /* NAME is nonempty and equivalent to ".". */
+
+ /* Copy components from Q to P, omitting redundant slashes and
+ internal "." components. */
+ while ((*p++ = c = *q++) != '\0')
+ if (ISSLASH (c))
+- while (ISSLASH (q[*q == '.']))
+- q += (*q == '.') + 1;
++ {
++ q += slashlen (q);
++ q += dotslashlen (q);
++ }
+
+ /* Omit redundant trailing "." component and slash. */
+ if (2 < p - name)
+@@ -581,7 +607,8 @@ decode_timespec (char const *arg, char **arg_lim, bool parse_fraction)
+ static char *before_backup_name;
+ static char *after_backup_name;
+
+-/* Return 1 if FILE_NAME is obviously "." or "/". */
++/* Return 1 if FILE_NAME must identify a working or root directory.
++ FILE_NAME should not be empty. */
+ bool
+ must_be_dot_or_slash (char const *file_name)
+ {
+@@ -589,6 +616,7 @@ must_be_dot_or_slash (char const *file_name)
+
+ if (ISSLASH (file_name[0]))
+ {
++ /* It must be a root directory if all components are "." or "..". */
+ for (;;)
+ if (ISSLASH (file_name[1]))
+ file_name++;
+@@ -600,31 +628,32 @@ must_be_dot_or_slash (char const *file_name)
+ }
+ else
+ {
+- while (file_name[0] == '.' && ISSLASH (file_name[1]))
+- {
+- file_name += 2;
+- while (ISSLASH (*file_name))
+- file_name++;
+- }
+-
+- return ! file_name[0] || (file_name[0] == '.' && ! file_name[1]);
++ /* It must be a working directory if it is "." or "",
++ after skipping ^(\./+)* ERE. */
++ file_name += dotslashlen (file_name);
++ return ! file_name[file_name[0] == '.'];
+ }
+ }
+
+-/* Some implementations of rmdir let you remove '.' or '/'.
+- Report an error with errno set to zero for obvious cases of this;
+- otherwise call rmdir. */
++/* Act like rmdir (FILENAME) relative to chdir_fd, i.e., like rmdir (F).
++ However, reject attempts to remove a root directory
++ even on systems that allow such a thing.
++ Also, do not try to change the removed directory's status later. */
+ static int
+-safer_rmdir (const char *file_name)
++safer_rmdir (const char *file_name, struct fdbase f)
+ {
+- if (must_be_dot_or_slash (file_name))
++ if (f.fd == BADFD)
++ return -1; /* Preserve errno. */
++
++ if (IS_ABSOLUTE_FILE_NAME (f.base))
+ {
+- errno = 0;
++ errno = EBUSY;
+ return -1;
+ }
+
+- if (unlinkat (chdir_fd, file_name, AT_REMOVEDIR) == 0)
++ if (f.fd != BADFD && unlinkat (f.fd, f.base, AT_REMOVEDIR) == 0)
+ {
++ fdbase_clear ();
+ remove_delayed_set_stat (file_name);
+ return 0;
+ }
+@@ -646,10 +675,15 @@ remove_any_file (const char *file_name, enum remove_option option)
+ non-directory. */
+ bool try_unlink_first = cannot_unlink_dir ();
+
++ struct fdbase f = fdbase (file_name);
++
+ if (try_unlink_first)
+ {
+- if (unlinkat (chdir_fd, file_name, 0) == 0)
+- return 1;
++ if (f.fd != BADFD && unlinkat (f.fd, f.base, 0) == 0)
++ {
++ fdbase_clear ();
++ return 1;
++ }
+
+ /* POSIX 1003.1-2001 requires EPERM when attempting to unlink a
+ directory without appropriate privileges, but many Linux
+@@ -658,13 +692,16 @@ remove_any_file (const char *file_name, enum remove_option option)
+ return 0;
+ }
+
+- if (safer_rmdir (file_name) == 0)
++ if (safer_rmdir (file_name, f) == 0)
+ return 1;
+
+ switch (errno)
+ {
+ case ENOTDIR:
+- return !try_unlink_first && unlinkat (chdir_fd, file_name, 0) == 0;
++ if (try_unlink_first || f.fd == BADFD || unlinkat (f.fd, f.base, 0) < 0)
++ return 0;
++ fdbase_clear ();
++ return 1;
+
+ case 0:
+ case EEXIST:
+@@ -705,7 +742,7 @@ remove_any_file (const char *file_name, enum remove_option option)
+ }
+
+ free (directory);
+- return safer_rmdir (file_name) == 0;
++ return safer_rmdir (file_name, fdbase (file_name)) == 0;
+ }
+ }
+ break;
+@@ -755,13 +792,27 @@ maybe_backup_file (const char *file_name, bool this_is_the_archive)
+ && (S_ISBLK (file_stat.st_mode) || S_ISCHR (file_stat.st_mode)))
+ return true;
+
+- after_backup_name = find_backup_file_name (chdir_fd, file_name, backup_type);
++ struct fdbase f = fdbase (file_name);
++ if (f.fd == BADFD)
++ {
++ open_error (file_name);
++ return false;
++ }
++ idx_t subdirlen = f.base - file_name;
++ after_backup_name = find_backup_file_name (f.fd, f.base, backup_type);
+ if (! after_backup_name)
+ xalloc_die ();
+-
+- if (renameat (chdir_fd, before_backup_name, chdir_fd, after_backup_name)
+- == 0)
++ idx_t after_backup_namelen = strlen (after_backup_name);
++ after_backup_name = xrealloc (after_backup_name,
++ subdirlen + after_backup_namelen + 1);
++ memmove (after_backup_name + subdirlen, after_backup_name,
++ after_backup_namelen + 1);
++ memcpy (after_backup_name, file_name, subdirlen);
++
++ if (renameat (f.fd, f.base, f.fd, &after_backup_name[subdirlen]) == 0)
+ {
++ if (S_ISLNK (file_stat.st_mode))
++ fdbase_clear ();
+ if (verbose_option)
+ fprintf (stdlis, _("Renaming %s to %s\n"),
+ quote_n (0, before_backup_name),
+@@ -787,8 +838,11 @@ undo_last_backup (void)
+ {
+ if (after_backup_name)
+ {
+- if (renameat (chdir_fd, after_backup_name, chdir_fd, before_backup_name)
+- != 0)
++ struct fdbase f = fdbase (before_backup_name);
++ if (f.fd == BADFD
++ || (renameat (f.fd, &after_backup_name[f.base - before_backup_name],
++ f.fd, f.base)
++ != 0))
+ {
+ int e = errno;
+ ERROR ((0, e, _("%s: Cannot rename to %s"),
+@@ -809,7 +863,8 @@ undo_last_backup (void)
+ int
+ deref_stat (char const *name, struct stat *buf)
+ {
+- return fstatat (chdir_fd, name, buf, fstatat_flags);
++ struct fdbase f = fdbase (name);
++ return f.fd == BADFD ? -1 : fstatat (f.fd, f.base, buf, fstatat_flags);
+ }
+
+ /* Read from FD into the buffer BUF with COUNT bytes. Attempt to fill
+@@ -890,6 +945,11 @@ struct wd
+ the working directory. If zero, the directory needs to be opened
+ to be used. */
+ int fd;
++
++ /* If ID.err is zero, the directory's identity;
++ if positive, a failure indication with errno = ID.err;
++ if negative, no attempt has been made yet to get the identity. */
++ struct chdir_id id;
+ };
+
+ /* A vector of chdir targets. wd[0] is the initial working directory. */
+@@ -922,33 +982,37 @@ chdir_count (void)
+ return wd_count - 1;
+ }
+
++/* Grow the WD table by at least one entry. */
++static void
++grow_wd (void)
++{
++ if (wd_alloc == 0)
++ wd_alloc = 2;
++ wd = x2nrealloc (wd, &wd_alloc, sizeof *wd);
++
++ if (! wd_count)
++ {
++ wd[wd_count].name = ".";
++ wd[wd_count].abspath = NULL;
++ wd[wd_count].fd = AT_FDCWD;
++ wd[wd_count].id.err = -1;
++ wd_count++;
++ }
++}
++
+ /* DIR is the operand of a -C option; add it to vector of chdir targets,
+ and return the index of its location. */
+ int
+ chdir_arg (char const *dir)
+ {
+ if (wd_count == wd_alloc)
+- {
+- if (wd_alloc == 0)
+- wd_alloc = 2;
+- wd = x2nrealloc (wd, &wd_alloc, sizeof *wd);
+-
+- if (! wd_count)
+- {
+- wd[wd_count].name = ".";
+- wd[wd_count].abspath = NULL;
+- wd[wd_count].fd = AT_FDCWD;
+- wd_count++;
+- }
+- }
++ grow_wd ();
+
+ /* Optimize the common special case of the working directory,
+ or the working directory as a prefix. */
+ if (dir[0])
+ {
+- while (dir[0] == '.' && ISSLASH (dir[1]))
+- for (dir += 2; ISSLASH (*dir); dir++)
+- continue;
++ dir += dotslashlen (dir);
+ if (! dir[dir[0] == '.'])
+ return wd_count - 1;
+ }
+@@ -956,6 +1020,7 @@ chdir_arg (char const *dir)
+ wd[wd_count].name = dir;
+ wd[wd_count].abspath = NULL;
+ wd[wd_count].fd = 0;
++ wd[wd_count].id.err = -1;
+ return wd_count++;
+ }
+
+@@ -966,7 +1031,7 @@ int chdir_current;
+ similar locations for fstatat, etc. This is an open file
+ descriptor, or AT_FDCWD if the working directory is current. It is
+ valid until the next invocation of chdir_do. */
+-int chdir_fd = AT_FDCWD;
++static int chdir_fd = AT_FDCWD;
+
+ /* Change to directory I, in a virtual way. This does not actually
+ invoke chdir; it merely sets chdir_fd to an int suitable as the
+@@ -986,7 +1051,7 @@ chdir_do (int i)
+ if (! IS_ABSOLUTE_FILE_NAME (curr->name))
+ chdir_do (i - 1);
+ fd = openat (chdir_fd, curr->name,
+- open_searchdir_flags & ~ O_NOFOLLOW);
++ open_searchdir_how.flags & ~O_NOFOLLOW);
+ if (fd < 0)
+ open_fatal (curr->name);
+
+@@ -1027,6 +1092,186 @@ chdir_do (int i)
+ chdir_fd = fd;
+ }
+ }
++
++/* Return the identity of the current directory. */
++struct chdir_id
++chdir_id (void)
++{
++ if (!wd)
++ grow_wd ();
++
++ struct wd *curr = &wd[chdir_current];
++ if (curr->id.err < 0)
++ {
++ struct stat st;
++ curr->id = ((chdir_fd < 0 ? stat (".", &st) : fstat (chdir_fd, &st)) < 0
++ ? (struct chdir_id) { .err = errno }
++ : (struct chdir_id) { .st_dev = st.st_dev,
++ .st_ino = st.st_ino });
++ }
++ return curr->id;
++}
++
++/* Caches of recent calls to fdbase and fdbase1. */
++static struct fdbase_cache
++{
++ /* Length of subdirectory name, which need not be null-terminated.
++ If the length is zero, no subdir is cached here:
++ SUBDIR (if nonnull) is merely a buffer available for use later,
++ and CHDIR_CURRENT and FD are irrelevant. */
++ idx_t subdirlen;
++
++ /* Index of ancestor of this subdirectory. */
++ idx_t chdir_current;
++
++ /* Buffer containing name of subdirectory relative to the ancestor. */
++ char *subdir;
++
++ /* Number of bytes allocated for SUBDIR. */
++ idx_t subdiralloc;
++
++ /* FD of subdirectory. */
++ int fd;
++} fdbase_cache[2];
++
++/* Clear the fdbase cache. Call this after any action that might
++ invalidate the cache. Such actions include removing or renaming
++ directories or symlinks to directories. Call this if in doubt,
++ e.g., if it is not known whether a removed directory entry is a
++ symlink to a directory. */
++void
++fdbase_clear (void)
++{
++ for (int i = 0; i < 2; i++)
++ {
++ struct fdbase_cache *c = &fdbase_cache[i];
++ if (c->subdirlen)
++ {
++ if (0 <= c->fd)
++ close (c->fd);
++ c->subdirlen = 0;
++ }
++ }
++}
++
++/* Starting from the directory FD, open a subdirectory SUBDIR for search.
++ If extracting or diffing and --absolute-names (-P) is not in effect,
++ do not let the subdirectory escape FD, i.e., the subdirectory must
++ be at or under FD in the directory hierarchy. */
++static int
++open_subdir (int fd, char const *subdir)
++{
++ return openat2 (fd, subdir, &open_searchdir_how, sizeof open_searchdir_how);
++}
++
++/* Return an fd open to FILE_NAME's parent directory,
++ along with the base name of FILE_NAME.
++ Use the alternate cache if ALTERNATE, the main cache otherwise.
++ If FILE_NAME is relative, it is relative to chdir_fd.
++ Return AT_FDCWD if FILE_NAME is relative to the working directory.
++ Return BADFD (setting errno) on failure. */
++static struct fdbase
++fdbase_opendir (char const *file_name, bool alternate)
++{
++ char const *name = file_name;
++
++ /* Skip past leading "./"s,
++ but not past the last "./" if that ends the name. */
++ idx_t dslen = dotslashlen (name);
++ if (dslen)
++ {
++ name += dslen;
++ if (!*name)
++ for (name--; *--name != '.'; )
++ continue;
++ }
++
++ /* For files immediately under CHDIR_FD, and for root directories,
++ just use CHDIR_FD and NAME. */
++ char const *base = last_component (name);
++ idx_t subdirlen = base - name;
++ if (!subdirlen | !*base)
++ return (struct fdbase) { .fd = chdir_fd, .base = name };
++
++ struct fdbase_cache *c = &fdbase_cache[alternate];
++ int fd = c->fd;
++ bool submatch = (0 < c->subdirlen && c->subdirlen <= subdirlen
++ && c->chdir_current == chdir_current
++ && !ISSLASH (name[c->subdirlen])
++ && memcmp (c->subdir, name, c->subdirlen) == 0);
++
++ if (! (submatch && c->subdirlen == subdirlen))
++ {
++ /* Copy the new directory's name into the cache. */
++ char *subdir = c->subdir;
++ if (c->subdiralloc <= subdirlen)
++ c->subdir = subdir = xpalloc (subdir, &c->subdiralloc,
++ subdirlen - c->subdiralloc + 1, -1, 1);
++ char *p = mempcpy (subdir, name, subdirlen);
++ *p = '\0';
++
++ if (submatch && c->subdirlen < subdirlen
++ && !ISSLASH (subdir[c->subdirlen]))
++ {
++ /* The new directory is a subdirectory of the old,
++ so open relative to FD rather than to chdir_fd. */
++ int subfd = open_subdir (fd, &subdir[c->subdirlen]);
++ if (subfd < 0)
++ {
++ /* Keep the old directory cached and report open failure,
++ unless EMFILE means it's possible that falling
++ through to close the old directory would mean we
++ could successfully retry from the chdir_fd level.
++ When reporting failure, there is no need to
++ null-terminate the old directory, since the code does
++ not assume null termination. */
++ if (errno != EMFILE)
++ return (struct fdbase) { .fd = BADFD, .base = base };
++ }
++ else
++ {
++ /* Replace the old directory with the new one. */
++ close (fd);
++ c->fd = subfd;
++ c->subdirlen = subdirlen;
++ return (struct fdbase) { .fd = subfd, .base = base };
++ }
++ }
++
++ /* Remove any old directory info,
++ and add new info if the new directory can be opened. */
++ if (0 < c->subdirlen)
++ close (fd);
++ fd = open_subdir (chdir_fd, c->subdir);
++ if (fd < 0)
++ {
++ if (BADFD != -1 && fd < 0)
++ fd = BADFD;
++ c->subdirlen = 0;
++ }
++ else
++ {
++ c->chdir_current = chdir_current;
++ c->fd = fd;
++ c->subdirlen = subdirlen;
++ }
++ }
++
++ return (struct fdbase) { .fd = fd, .base = base };
++}
++
++struct fdbase
++fdbase (char const *name)
++{
++ return fdbase_opendir (name, false);
++}
++
++struct fdbase
++fdbase1 (char const *name)
++{
++ return fdbase_opendir (name, true);
++}
++
+
+ const char *
+ tar_dirname (void)
+@@ -1310,7 +1555,9 @@ tar_savedir (const char *name, int must_exist)
+ {
+ char *ret = NULL;
+ DIR *dir = NULL;
+- int fd = openat (chdir_fd, name, open_read_flags | O_DIRECTORY);
++ struct fdbase f = fdbase (name);
++ int fd = (f.fd == BADFD ? -1
++ : openat (f.fd, f.base, open_read_flags | O_DIRECTORY));
+ if (fd < 0)
+ {
+ if (!must_exist && errno == ENOENT)
+diff --git a/src/names.c b/src/names.c
+index d302737..54fd696 100644
+--- a/src/names.c
++++ b/src/names.c
+@@ -1797,8 +1797,9 @@ collect_and_sort_names (void)
+ }
+ if (S_ISDIR (st.stat.st_mode))
+ {
+- int dir_fd = openat (chdir_fd, name->name,
+- open_read_flags | O_DIRECTORY);
++ struct fdbase f = fdbase (name->name);
++ int dir_fd = (f.fd == BADFD ? -1
++ : openat (f.fd, f.base, open_read_flags | O_DIRECTORY));
+ if (dir_fd < 0)
+ open_diag (name->name);
+ else
+diff --git a/src/tar.c b/src/tar.c
+index 7efb003..272c63b 100644
+--- a/src/tar.c
++++ b/src/tar.c
+@@ -2614,16 +2614,29 @@ decode_options (int argc, char **argv)
+ if (recursive_unlink_option)
+ old_files_option = UNLINK_FIRST_OLD_FILES;
+
+- /* Flags for accessing files to be read from or copied into. POSIX says
+- O_NONBLOCK has unspecified effect on most types of files, but in
+- practice it never harms and sometimes helps. */
++ /* Flags for accessing files to be read from, searched, or statted. */
+ {
+- int base_open_flags =
+- (O_BINARY | O_CLOEXEC | O_NOCTTY | O_NONBLOCK
+- | (dereference_option ? 0 : O_NOFOLLOW)
+- | (atime_preserve_option == system_atime_preserve ? O_NOATIME : 0));
+- open_read_flags = O_RDONLY | base_open_flags;
+- open_searchdir_flags = O_SEARCH | O_DIRECTORY | base_open_flags;
++ int noatime_flag = (atime_preserve_option == system_atime_preserve
++ ? O_NOATIME : 0);
++ int nofollow_flag = dereference_option ? 0 : O_NOFOLLOW;
++
++ /* POSIX says O_NONBLOCK has unspecified effect on most types of
++ files, but in practice it harms only with O_PATH and sometimes
++ helps otherwise. */
++ open_read_flags = (O_RDONLY | O_BINARY | O_CLOEXEC | O_NOCTTY | O_NONBLOCK
++ | noatime_flag | nofollow_flag);
++
++#if defined O_PATH && O_SEARCH == O_RDONLY
++ int search_flags = O_PATH; /* openat2 rejects O_PATH | O_NOATIME. */
++#else
++ int search_flags = O_SEARCH | noatime_flag;
++#endif
++ open_searchdir_how.flags = (search_flags | nofollow_flag
++ | O_BINARY | O_CLOEXEC | O_DIRECTORY);
++ if (!absolute_names_option
++ && (subcommand_option == EXTRACT_SUBCOMMAND
++ || subcommand_option == DIFF_SUBCOMMAND))
++ open_searchdir_how.resolve = RESOLVE_BENEATH;
+ }
+ fstatat_flags = dereference_option ? 0 : AT_SYMLINK_NOFOLLOW;
+
+diff --git a/src/unlink.c b/src/unlink.c
+index 704e42b..33d2ccd 100644
+--- a/src/unlink.c
++++ b/src/unlink.c
+@@ -117,7 +117,10 @@ flush_deferred_unlinks (bool force)
+ else
+ fname = p->file_name;
+
+- if (unlinkat (chdir_fd, fname, AT_REMOVEDIR) != 0)
++ struct fdbase f = fdbase (fname);
++ if (f.fd != BADFD && unlinkat (f.fd, f.base, AT_REMOVEDIR) == 0)
++ fdbase_clear ();
++ else
+ {
+ switch (errno)
+ {
+@@ -143,7 +146,10 @@ flush_deferred_unlinks (bool force)
+ }
+ else
+ {
+- if (unlinkat (chdir_fd, p->file_name, 0) != 0 && errno != ENOENT)
++ struct fdbase f = fdbase (p->file_name);
++ if (f.fd != BADFD && unlinkat (f.fd, f.base, 0) == 0)
++ fdbase_clear ();
++ else if (errno != ENOENT)
+ unlink_error (p->file_name);
+ }
+ dunlink_reclaim (p);
+@@ -178,11 +184,12 @@ flush_deferred_unlinks (bool force)
+ else
+ fname = p->file_name;
+
+- if (unlinkat (chdir_fd, fname, AT_REMOVEDIR) != 0)
+- {
+- if (errno != ENOENT)
+- rmdir_error (fname);
+- }
++ struct fdbase f = fdbase (fname);
++ if (f.fd != BADFD && unlinkat (f.fd, f.base, AT_REMOVEDIR) == 0)
++ fdbase_clear ();
++ else if (errno != ENOENT)
++ rmdir_error (fname);
++
+ dunlink_reclaim (p);
+ dunlink_count--;
+ p = next;
+diff --git a/src/update.c b/src/update.c
+index dc5e5ac..2bc3820 100644
+--- a/src/update.c
++++ b/src/update.c
+@@ -49,7 +49,8 @@ static bool acting_as_filter;
+ static void
+ append_file (char *file_name)
+ {
+- int handle = openat (chdir_fd, file_name, O_RDONLY | O_BINARY);
++ struct fdbase f = fdbase (file_name);
++ int handle = f.fd == BADFD ? -1 : openat (f.fd, f.base, O_RDONLY | O_BINARY);
+
+ if (handle < 0)
+ {
+diff --git a/src/xattrs.c b/src/xattrs.c
+index 86a7e59..92698ea 100644
+--- a/src/xattrs.c
++++ b/src/xattrs.c
+@@ -299,7 +299,8 @@ xattrs__acls_set (struct tar_stat_info const *st,
+ /* No "default" IEEE 1003.1e ACL set for directory. At this moment,
+ FILE_NAME may already have inherited default acls from parent
+ directory; clean them up. */
+- if (acl_delete_def_file_at (chdir_fd, file_name))
++ struct fdbase f1 = fdbase (file_name);
++ if (f1.fd == BADFD || acl_delete_def_file_at (f1.fd, f1.base))
+ WARNOPT (WARN_XATTR_WRITE,
+ (0, errno,
+ _("acl_delete_def_file_at: Cannot drop default POSIX ACLs "
+@@ -316,7 +317,8 @@ xattrs__acls_set (struct tar_stat_info const *st,
+ return;
+ }
+
+- if (acl_set_file_at (chdir_fd, file_name, type, acl) == -1)
++ struct fdbase f = fdbase (file_name);
++ if (f.fd == BADFD || acl_set_file_at (f.fd, f.base, type, acl) == -1)
+ /* warn even if filesystem does not support acls */
+ WARNOPT (WARN_XATTR_WRITE,
+ (0, errno,
+@@ -619,13 +621,16 @@ xattrs__fd_set (char const *file_name, char typeflag,
+ {
+ const char *sysname = "setxattrat";
+ int ret = -1;
++ struct fdbase f = fdbase (file_name);
+
+- if (typeflag != SYMTYPE)
+- ret = setxattrat (chdir_fd, file_name, attr, ptr, len, 0);
++ if (f.fd == BADFD)
++ ret = -1;
++ else if (typeflag != SYMTYPE)
++ ret = setxattrat (f.fd, f.base, attr, ptr, len, 0);
+ else
+ {
+ sysname = "lsetxattr";
+- ret = lsetxattrat (chdir_fd, file_name, attr, ptr, len, 0);
++ ret = lsetxattrat (f.fd, f.base, attr, ptr, len, 0);
+ }
+
+ if (ret == -1)
+@@ -679,14 +684,17 @@ xattrs_selinux_set (struct tar_stat_info const *st,
+ if (!st->cntx_name)
+ return;
+
+- if (typeflag != SYMTYPE)
++ struct fdbase f = fdbase (file_name);
++ if (f.fd == BADFD)
++ ret = -1;
++ else if (typeflag != SYMTYPE)
+ {
+- ret = setfileconat (chdir_fd, file_name, st->cntx_name);
++ ret = setfileconat (f.fd, f.base, st->cntx_name);
+ sysname = "setfileconat";
+ }
+ else
+ {
+- ret = lsetfileconat (chdir_fd, file_name, st->cntx_name);
++ ret = lsetfileconat (f.fd, f.base, st->cntx_name);
+ sysname = "lsetfileconat";
+ }
+
+diff --git a/tests/Makefile.am b/tests/Makefile.am
+index ea5b66e..2c4aa3f 100644
+--- a/tests/Makefile.am
++++ b/tests/Makefile.am
+@@ -126,6 +126,7 @@ TESTSUITE_AT = \
+ extrac24.at\
+ extrac25.at\
+ extrac27.at\
++ extrac31.at\
+ filerem01.at\
+ filerem02.at\
+ dirrem01.at\
+diff --git a/tests/extrac31.at b/tests/extrac31.at
+new file mode 100644
+index 0000000..5cc68a4
+--- /dev/null
++++ b/tests/extrac31.at
+@@ -0,0 +1,55 @@
++# Test suite for GNU tar. -*- Autotest -*-
++# Copyright 2025 Free Software Foundation, Inc.
++#
++# This file is part of GNU tar.
++#
++# GNU tar is free software; you can redistribute it and/or modify
++# it under the terms of the GNU General Public License as published by
++# the Free Software Foundation; either version 3 of the License, or
++# (at your option) any later version.
++#
++# GNU tar is distributed in the hope that it will be useful,
++# but WITHOUT ANY WARRANTY; without even the implied warranty of
++# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
++# GNU General Public License for more details.
++#
++# You should have received a copy of the GNU General Public License
++# along with this program. If not, see <http://www.gnu.org/licenses/>.
++
++AT_SETUP([extracting untrusted incremental])
++AT_KEYWORDS([extract extrac31 --absolute-names])
++
++
++AT_TAR_CHECK([
++
++# Extraction should not escape the extraction directory
++# even when extracting multiple times to the same directory.
++(umask 022 && mkdir -p dira/sub dirb/sym dirb/sub/sym ext victimdir victimexp)
++ln -s .. dira/sub/dotdot
++ln -s ../sub dira/sub/dot
++ln -s dotdot/sub dira/sub/anotherdot
++ln -s ../victimdir dira/sym
++ln -s dotdot/../victimdir dira/sub/sym
++echo b1 >dirb/sym/file1
++echo b2 >dirb/sub/sym/file2
++echo v >victimdir/expected
++echo v >victimdir/file1
++echo v >victimdir/file2
++cp victimdir/* victimexp
++tar -cf a.tar -C dira sub sym
++tar -cf b.tar -C dirb sym/file1 sub/sym/file2
++tar -xf a.tar -C ext
++echo status1=$?
++tar -xf b.tar -C ext
++echo status2=$?
++diff victimdir victimexp
++],
++[],
++[status1=0
++status2=2
++],
++[tar: sym/file1: Cannot open: Invalid cross-device link
++tar: sub/sym/file2: Cannot open: Invalid cross-device link
++tar: Exiting with failure status due to previous errors
++])
++AT_CLEANUP
+diff --git a/tests/testsuite.at b/tests/testsuite.at
+index 5cb87c7..963836c 100644
+--- a/tests/testsuite.at
++++ b/tests/testsuite.at
+@@ -350,6 +350,7 @@ m4_include([extrac23.at])
+ m4_include([extrac24.at])
+ m4_include([extrac25.at])
+ m4_include([extrac27.at])
++m4_include([extrac31.at])
+
+ m4_include([backup01.at])
+
diff --git a/tar-CVE-2026-18477.patch b/tar-CVE-2026-18477.patch
new file mode 100644
index 0000000..812813e
--- /dev/null
+++ b/tar-CVE-2026-18477.patch
@@ -0,0 +1,412 @@
+# upstream 0714d2f0, d479b2cc, b17665b2: strip '/' from incremental
+# dumpdir 'X' rename templates unless -P (CVE-2026-18477)
+diff --git a/lib/Makefile.am b/lib/Makefile.am
+index 38645b2..45152c4 100644
+--- a/lib/Makefile.am
++++ b/lib/Makefile.am
+@@ -30,6 +30,7 @@ AM_CPPFLAGS = -I$(top_srcdir)/gnu -I../ -I../gnu
+ AM_CFLAGS = $(GNULIB_WARN_CFLAGS) $(WERROR_CFLAGS)
+
+ noinst_HEADERS = \
++ mkdtempat.h\
+ paxlib.h\
+ rmt.h\
+ system.h\
+@@ -38,6 +39,7 @@ noinst_HEADERS = \
+ xattr-at.h
+
+ libtar_a_SOURCES = \
++ mkdtempat.c\
+ paxerror.c paxexit-status.c paxlib.h paxnames.c \
+ rtapelib.c \
+ rmt.h \
+diff --git a/lib/mkdtempat.c b/lib/mkdtempat.c
+new file mode 100644
+index 0000000..e35b9bb
+--- /dev/null
++++ b/lib/mkdtempat.c
+@@ -0,0 +1,45 @@
++/* Copyright 2026 Free Software Foundation, Inc.
++
++ This program is free software; you can redistribute it and/or modify it
++ under the terms of the GNU General Public License as published by the
++ Free Software Foundation; either version 3 of the License, or (at your
++ option) any later version.
++
++ This program is distributed in the hope that it will be useful,
++ but WITHOUT ANY WARRANTY; without even the implied warranty of
++ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
++ GNU General Public License for more details.
++
++ You should have received a copy of the GNU General Public License along
++ with this program. If not, see <http://www.gnu.org/licenses/>.
++
++ Written by Paul Eggert. */
++
++#include <config.h>
++
++#include "mkdtempat.h"
++
++#include <tempname.h>
++
++#include <stddef.h>
++#include <sys/stat.h>
++
++static int
++try_dir (char *tmpl, void *flags)
++{
++ int *pdirfd = flags;
++ return mkdirat (*pdirfd, tmpl, S_IRUSR | S_IWUSR | S_IXUSR);
++}
++
++/* Relative to the directory DIRFD if XTEMPLATE is relative,
++ generate a unique temporary directory from XTEMPLATE.
++ The last six characters of XTEMPLATE must be "XXXXXX";
++ replace them with a string that makes the generated directory unique.
++ Create the directory mode 700, and return its name.
++ On failure, return NULL and set errno. */
++char *
++mkdtempat (int dirfd, char *xtemplate)
++{
++ return (try_tempname_len (xtemplate, 0, &dirfd, try_dir, 6) < 0
++ ? NULL : xtemplate);
++}
+diff --git a/lib/mkdtempat.h b/lib/mkdtempat.h
+new file mode 100644
+index 0000000..03a18dc
+--- /dev/null
++++ b/lib/mkdtempat.h
+@@ -0,0 +1 @@
++char *mkdtempat (int, char *);
+diff --git a/src/incremen.c b/src/incremen.c
+index 194d5cb..a808e1c 100644
+--- a/src/incremen.c
++++ b/src/incremen.c
+@@ -19,6 +19,7 @@
+
+ #include <system.h>
+ #include <hash.h>
++#include <mkdtempat.h>
+ #include <quotearg.h>
+ #include "common.h"
+
+@@ -1653,13 +1654,16 @@ purge_directory (char const *directory_name)
+ if (*arc == 'X')
+ {
+ #define TEMP_DIR_TEMPLATE "tar.XXXXXX"
+- size_t len = strlen (arc + 1);
++ char *d = safer_name_suffix (arc + 1, false, absolute_names_option);
++ size_t len = strlen (d);
+ temp_stub = xrealloc (temp_stub, len + 1 + sizeof TEMP_DIR_TEMPLATE);
+- memcpy (temp_stub, arc + 1, len);
+- temp_stub[len] = '/';
+- memcpy (temp_stub + len + 1, TEMP_DIR_TEMPLATE,
++ char *copy_end = mempcpy (temp_stub, d, len);
++ *copy_end = '/';
++ memcpy (copy_end + !ISSLASH (copy_end[-1]), TEMP_DIR_TEMPLATE,
+ sizeof TEMP_DIR_TEMPLATE);
+- if (!mkdtemp (temp_stub))
++ struct fdbase f = fdbase (temp_stub);
++ if (f.fd == BADFD
++ || !mkdtempat (f.fd, temp_stub + (f.base - temp_stub)))
+ {
+ ERROR ((0, errno,
+ _("Cannot create temporary directory using template %s"),
+diff --git a/tests/Makefile.am b/tests/Makefile.am
+index 3298ac6..d28bf25 100644
+--- a/tests/Makefile.am
++++ b/tests/Makefile.am
+@@ -208,6 +208,8 @@ TESTSUITE_AT = \
+ rename04.at\
+ rename05.at\
+ rename06.at\
++ rename08.at\
++ rename09.at\
+ remfiles01.at\
+ remfiles02.at\
+ remfiles03.at\
+diff --git a/tests/rename08.at b/tests/rename08.at
+new file mode 100644
+index 0000000..5452fb4
+--- /dev/null
++++ b/tests/rename08.at
+@@ -0,0 +1,131 @@
++# Process this file with autom4te to create testsuite. -*- Autotest -*-
++
++# Test suite for GNU tar.
++# Copyright 2006-2026 Free Software Foundation, Inc.
++
++# This file is part of GNU tar.
++
++# GNU tar is free software; you can redistribute it and/or modify
++# it under the terms of the GNU General Public License as published by
++# the Free Software Foundation; either version 3 of the License, or
++# (at your option) any later version.
++
++# GNU tar is distributed in the hope that it will be useful,
++# but WITHOUT ANY WARRANTY; without even the implied warranty of
++# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
++# GNU General Public License for more details.
++
++# You should have received a copy of the GNU General Public License
++# along with this program. If not, see <http://www.gnu.org/licenses/>.
++
++# Description: Handling of cyclic renames in incremental archives.
++
++AT_SETUP([cyclic renames])
++AT_KEYWORDS([incremental rename rename08 cyclic-rename])
++
++AT_TAR_CHECK([
++AT_SORT_PREREQ
++
++mkdir foo
++genfile --file foo/file1
++genfile --file foo/file2
++
++mkdir foo/a
++genfile --file foo/a/filea
++
++mkdir foo/b
++genfile --file foo/b/fileb
++
++mkdir foo/c
++genfile --file foo/c/filec
++
++sleep 1
++
++echo "First dump"
++echo "First dump">&2
++tar -g incr -cf arch.1 -v foo 2>tmperr
++sort tmperr >&2
++
++# Shuffle directories:
++(cd foo
++mv a $$
++mv c a
++mv b c
++mv $$ b)
++
++echo "Second dump"
++echo "Second dump" >&2
++tar -g incr -cf arch.2 -v foo 2>tmperr
++sort tmperr >&2
++
++mkdir -p restoreparent/restore
++cd restoreparent
++tar xfg ../arch.1 /dev/null --warning=no-timestamp -C restore
++
++echo "Begin directory listing 1"
++( cd restore; find foo ) | sort
++echo "End directory listing 1"
++
++tar xfgv ../arch.2 /dev/null --warning=no-timestamp -C restore
++echo Begin directory listing 2
++( cd restore ; find foo ) | sort
++echo End directory listing 2
++],
++[0],
++[First dump
++foo/
++foo/a/
++foo/b/
++foo/c/
++foo/file1
++foo/file2
++foo/a/filea
++foo/b/fileb
++foo/c/filec
++Second dump
++foo/
++foo/a/
++foo/b/
++foo/c/
++Begin directory listing 1
++foo
++foo/a
++foo/a/filea
++foo/b
++foo/b/fileb
++foo/c
++foo/c/filec
++foo/file1
++foo/file2
++End directory listing 1
++foo/
++foo/a/
++foo/b/
++foo/c/
++Begin directory listing 2
++foo
++foo/a
++foo/a/filec
++foo/b
++foo/b/filea
++foo/c
++foo/c/fileb
++foo/file1
++foo/file2
++End directory listing 2
++],
++[First dump
++tar: foo/a: Directory is new
++tar: foo/b: Directory is new
++tar: foo/c: Directory is new
++tar: foo: Directory is new
++Second dump
++tar: foo/a: Directory has been renamed from 'foo/c'
++tar: foo/b: Directory has been renamed from 'foo/a'
++tar: foo/c: Directory has been renamed from 'foo/b'
++],
++[],[],[gnu, oldgnu, posix])
++
++AT_CLEANUP
++
++# End of rename03.at
+diff --git a/tests/rename09.at b/tests/rename09.at
+new file mode 100644
+index 0000000..8346ff0
+--- /dev/null
++++ b/tests/rename09.at
+@@ -0,0 +1,129 @@
++# Process this file with autom4te to create testsuite. -*- Autotest -*-
++
++# Test suite for GNU tar.
++# Copyright 2006-2026 Free Software Foundation, Inc.
++
++# This file is part of GNU tar.
++
++# GNU tar is free software; you can redistribute it and/or modify
++# it under the terms of the GNU General Public License as published by
++# the Free Software Foundation; either version 3 of the License, or
++# (at your option) any later version.
++
++# GNU tar is distributed in the hope that it will be useful,
++# but WITHOUT ANY WARRANTY; without even the implied warranty of
++# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
++# GNU General Public License for more details.
++
++# You should have received a copy of the GNU General Public License
++# along with this program. If not, see <http://www.gnu.org/licenses/>.
++
++# Description: Handling of cyclic renames in incremental archives.
++
++AT_SETUP([cyclic renames])
++AT_KEYWORDS([incremental rename rename09 cyclic-rename])
++
++AT_TAR_CHECK([
++AT_SORT_PREREQ
++
++mkdir foo
++genfile --file foo/file1
++genfile --file foo/file2
++
++mkdir foo/a
++genfile --file foo/a/filea
++
++mkdir foo/b
++genfile --file foo/b/fileb
++
++mkdir foo/c
++genfile --file foo/c/filec
++
++sleep 1
++
++echo "First dump"
++echo "First dump">&2
++tar -g incr -cf arch.1 -v foo 2>tmperr
++sort tmperr >&2
++
++# Shuffle directories:
++(cd foo
++mv a $$
++mv c a
++mv b c
++mv $$ b)
++
++echo "Second dump"
++echo "Second dump" >&2
++tar -g incr -cf arch.2 -v foo 2>tmperr
++sort tmperr >&2
++
++tar xfg arch.1 /dev/null --warning=no-timestamp --one-top-level=restore
++
++echo "Begin directory listing 1"
++( cd restore; find foo ) | sort
++echo "End directory listing 1"
++
++tar xfgv arch.2 /dev/null --warning=no-timestamp --one-top-level=restore
++echo Begin directory listing 2
++( cd restore ; find foo ) | sort
++echo End directory listing 2
++],
++[0],
++[First dump
++foo/
++foo/a/
++foo/b/
++foo/c/
++foo/file1
++foo/file2
++foo/a/filea
++foo/b/fileb
++foo/c/filec
++Second dump
++foo/
++foo/a/
++foo/b/
++foo/c/
++Begin directory listing 1
++foo
++foo/a
++foo/a/filea
++foo/b
++foo/b/fileb
++foo/c
++foo/c/filec
++foo/file1
++foo/file2
++End directory listing 1
++foo/
++foo/a/
++foo/b/
++foo/c/
++Begin directory listing 2
++foo
++foo/a
++foo/a/filec
++foo/b
++foo/b/filea
++foo/c
++foo/c/fileb
++foo/file1
++foo/file2
++End directory listing 2
++],
++[First dump
++tar: foo/a: Directory is new
++tar: foo/b: Directory is new
++tar: foo/c: Directory is new
++tar: foo: Directory is new
++Second dump
++tar: foo/a: Directory has been renamed from 'foo/c'
++tar: foo/b: Directory has been renamed from 'foo/a'
++tar: foo/c: Directory has been renamed from 'foo/b'
++],
++[],[],[gnu, oldgnu, posix])
++
++AT_CLEANUP
++
++# End of rename03.at
+diff --git a/tests/testsuite.at b/tests/testsuite.at
+index 6baabf7..19cb484 100644
+--- a/tests/testsuite.at
++++ b/tests/testsuite.at
+@@ -400,6 +400,8 @@ m4_include([rename03.at])
+ m4_include([rename04.at])
+ m4_include([rename05.at])
+ m4_include([rename06.at])
++m4_include([rename08.at])
++m4_include([rename09.at])
+ m4_include([chtype.at])
+
+ AT_BANNER([Ignore failing reads])
diff --git a/tar-CVE-2026-18508.patch b/tar-CVE-2026-18508.patch
new file mode 100644
index 0000000..7ee5d90
--- /dev/null
+++ b/tar-CVE-2026-18508.patch
@@ -0,0 +1,775 @@
+# upstream bfc33463..941f62b2: confine --one-top-level hard link
+# targets to the top-level directory (CVE-2026-18508)
+diff --git a/doc/tar.texi b/doc/tar.texi
+index e924be6..cfac644 100644
+--- a/doc/tar.texi
++++ b/doc/tar.texi
+@@ -3266,14 +3266,14 @@ directory.
+
+ @opsummary{one-top-level}
+ @item --one-top-level[=@var{dir}]
+-Tells @command{tar} to create a new directory beneath the extraction directory
+-(or the one passed to @option{-C}) and use it to prevent @command{tar}
+-from modifying files outside that directory.
+-If @var{dir} is present, it must be a relative file name.
+-If it is absent, the name of the new directory
++Tells @command{tar} to use a directory beneath the extraction directory
++(or the one passed to @option{-C}) to prevent @command{tar}
++from accessing files outside that directory.
++If @var{dir} is absent, the name of the directory
+ is the base name of the archive minus any recognized archive suffix.
+-If multiple @option{-C} options are present,
+-each has its own subdirectory with the same name.
++If @var{dir} is an absolute file name, that is the only such directory;
++otherwise, the working directory and the directories specified by
++any @option{-C} options each have the named subdirectory.
+ Any member names that do not begin
+ with that directory name (after
+ transformations from @option{--transform} and
+@@ -3281,6 +3281,9 @@ transformations from @option{--transform} and
+ file name suffixes are @samp{.tar}, and any compression suffixes
+ recognizable by @xref{--auto-compress}.
+
++This option can be used only when extracting, comparing, or reading
++from the archive.
++
+ @opsummary{overwrite}
+ @item --overwrite
+
+diff --git a/src/common.h b/src/common.h
+index b4a5821..cc8dbe5 100644
+--- a/src/common.h
++++ b/src/common.h
+@@ -246,7 +246,6 @@ GLOBAL bool numeric_owner_option;
+ GLOBAL bool one_file_system_option;
+
+ /* Create a top-level directory for extracting based on the archive name. */
+-GLOBAL bool one_top_level_option;
+ GLOBAL char *one_top_level_dir;
+
+ /* Specified value to be put into tar file in place of stat () results, or
+@@ -545,6 +544,7 @@ void verify_volume (void);
+ /* Module extract.c. */
+
+ void extr_init (void);
++int make_directories (char *file_name, bool *interdir_made);
+ void extract_archive (void);
+ void extract_finish (void);
+ bool rename_directory (char *src, char *dst);
+@@ -635,6 +635,20 @@ void skim_member (bool must_copy);
+ #define min(a, b) ((a) < (b) ? (a) : (b))
+ #define max(a, b) ((a) < (b) ? (b) : (a))
+
++/* A directory FD, and a file name BASE that matches the regexp "[^/]*[/]*".
++ If BASE is absolute (i.e., matches "/+"), FD is valid but irrelevant.
++ Otherwise if FD == AT_FDCWD, BASE is relative to the current directory.
++ Otherwise if FD == BADFD, the parent directory could not be opened
++ and BASE is merely the original file name's basename.
++ Otherwise, FD is open to a parent directory,
++ and BASE is relative to that directory.
++ BASE points to storage managed elsewhere; do not free it directly. */
++struct fdbase
++ {
++ int fd;
++ char const *base;
++ };
++
+ char const *quote_n_colon (int n, char const *arg);
+ void assign_string_or_null (char **dest, const char *src)
+ ATTRIBUTE_NONNULL ((1));
+@@ -657,6 +671,8 @@ void namebuf_free (namebuf_t buf);
+ char *namebuf_name (namebuf_t buf, const char *name);
+
+ const char *tar_dirname (void);
++char *transform_top_level (const char *name)
++ _GL_ATTRIBUTE_MALLOC _GL_ATTRIBUTE_DEALLOC_FREE;
+
+ /* Represent N using a signed integer I such that (uintmax_t) I == N.
+ With a good optimizing compiler, this is equivalent to (intmax_t) i
+@@ -728,10 +744,10 @@ size_t blocking_write (int fd, void const *buf, size_t count);
+ enum { BADFD = AT_FDCWD == -1 ? -2 : -1 };
+
+ extern int chdir_current;
+-int chdir_arg (char const *dir);
+-void chdir_do (int dir);
++int chdir_arg (char const *dir, bool one_top_level);
++void chdir_do (int dir, bool create);
+ struct chdir_id { int err; dev_t st_dev; ino_t st_ino; } chdir_id (void);
+-struct fdbase { int fd; char const *base; } fdbase (char const *);
++struct fdbase fdbase (char const *);
+ struct fdbase fdbase1 (char const *);
+ void fdbase_clear (void);
+ int chdir_count (void);
+diff --git a/src/create.c b/src/create.c
+index 845b194..08f77f1 100644
+--- a/src/create.c
++++ b/src/create.c
+@@ -1266,7 +1266,7 @@ ensure_slash (char **pstr)
+ /* If we just ran out of file descriptors, release a file descriptor
+ in the directory chain somewhere leading from DIR->parent->parent
+ up through the root. Return true if successful, false (preserving
+- errno == EMFILE) otherwise.
++ errno) otherwise.
+
+ Do not release DIR's file descriptor, or DIR's parent, as other
+ code assumes that they work. On some operating systems, another
+@@ -1277,7 +1277,8 @@ ensure_slash (char **pstr)
+ static bool
+ open_failure_recover (struct tar_stat_info const *dir)
+ {
+- if (errno == EMFILE && dir && dir->parent)
++ int err = errno;
++ if ((err == EMFILE || err == ENFILE) && dir && dir->parent)
+ {
+ struct tar_stat_info *p;
+ for (p = dir->parent->parent; p; p = p->parent)
+@@ -1286,7 +1287,7 @@ open_failure_recover (struct tar_stat_info const *dir)
+ tar_stat_close (p);
+ return true;
+ }
+- errno = EMFILE;
++ errno = err;
+ }
+
+ return false;
+diff --git a/src/extract.c b/src/extract.c
+index d1c86a0..49e4dd4 100644
+--- a/src/extract.c
++++ b/src/extract.c
+@@ -698,7 +698,7 @@ fixup_delayed_set_stat (char const *src, char const *dst)
+ create all required directories. Return zero if all the required
+ directories were created, nonzero (issuing a diagnostic) otherwise.
+ Set *INTERDIR_MADE (unless NULL) if at least one directory was created. */
+-static int
++int
+ make_directories (char *file_name, bool *interdir_made)
+ {
+ char *cursor0 = file_name + FILE_SYSTEM_PREFIX_LEN (file_name);
+@@ -741,7 +741,7 @@ make_directories (char *file_name, bool *interdir_made)
+ repair_delayed_set_stat may need to update the struct. */
+ delay_set_stat (file_name,
+ 0, mode & ~ current_umask, MODE_RWX,
+- desired_mode, AT_SYMLINK_NOFOLLOW);
++ desired_mode, AT_SYMLINK_NOFOLLOW);
+ if (interdir_made)
+ *interdir_made = true;
+ print_for_mkdir (file_name, desired_mode);
+@@ -974,7 +974,7 @@ apply_nonancestor_delayed_set_stat (char const *file_name, bool after_links)
+ && memcmp (file_name, data->file_name, data->file_name_len) == 0))
+ break;
+
+- chdir_do (data->change_dir);
++ chdir_do (data->change_dir, false);
+
+ if (check_for_renamed_directories)
+ {
+@@ -1869,7 +1869,7 @@ extract_archive (void)
+ {
+ int dir = chdir_current;
+ apply_nonancestor_delayed_set_stat (current_stat_info.file_name, 0);
+- chdir_do (dir);
++ chdir_do (dir, false);
+ }
+
+ /* Take a safety backup of a previously existing file. */
+@@ -1889,8 +1889,35 @@ extract_archive (void)
+ typeflag = sparse_member_p (¤t_stat_info) ?
+ GNUTYPE_SPARSE : current_header->header.typeflag;
+
+- bool ok = prepare_to_extract (current_stat_info.file_name, typeflag, &fun)
+- && fun (current_stat_info.file_name, typeflag) == 0;
++ bool ok = false;
++ if (prepare_to_extract (current_stat_info.file_name, typeflag, &fun))
++ {
++ if (one_top_level_dir)
++ {
++ /* Create one_top_level dir if it does not exist. */
++ chdir_do (chdir_current, true);
++ /* Flush delayed stat to mirror the code above that does it
++ before extracting a new entry. Creating the one_top_level
++ dir may have created new delayed_set_stat interdir
++ entries, so repeat the operation. Ideally this should not
++ be needed, but the newly-created interdir entries have
++ st_dev/st_ino uninitialized, which would be a problem if
++ there is a "." entry afterwards:
++ apply_nonancestor_delayed_set_stat would use the
++ uninitialized values. Ideally, st_dev/st_ino would be
++ initialized by mark_metadata_set, but this one does not
++ take chdir into account, so it stats a wrong file. */
++ if (!delay_directory_restore_option)
++ {
++ int dir = chdir_current;
++ apply_nonancestor_delayed_set_stat (current_stat_info.file_name,
++ false);
++ chdir_do (dir, false);
++ }
++ }
++ if (fun (current_stat_info.file_name, typeflag) == 0)
++ ok = true;
++ }
+ skip_member ();
+ if (!ok && backup_option)
+ undo_last_backup ();
+@@ -1903,7 +1930,7 @@ apply_delayed_link (struct delayed_link *ds)
+ struct string_list *sources = ds->sources;
+ char const *valid_source = NULL;
+
+- chdir_do (ds->change_dir);
++ chdir_do (ds->change_dir, false);
+
+ for (sources = ds->sources; sources; sources = sources->next)
+ {
+diff --git a/src/list.c b/src/list.c
+index b560569..dab6c3c 100644
+--- a/src/list.c
++++ b/src/list.c
+@@ -128,16 +128,28 @@ enforce_one_top_level (char **pfile_name)
+ int pos = strlen (one_top_level_dir);
+ if (strncmp (p, one_top_level_dir, pos) == 0)
+ {
+- if (ISSLASH (p[pos]) || p[pos] == 0)
+- return;
++ /* Remove the one_top_level_dir prefix if it ends at
++ component boundary. */
++ if (ISSLASH (p[pos]))
++ {
++ *pfile_name = xstrdup (p[pos+1] ? &p[pos+1] : ".");
++ free (file_name);
++ return;
++ }
++ else if (p[pos] == 0)
++ {
++ *pfile_name = xstrdup (".");
++ free (file_name);
++ return;
++ }
+ }
+-
+- *pfile_name = make_file_name (one_top_level_dir, file_name);
+- normalize_filename_x (*pfile_name);
++ /* If the prefix does not match, do nothing. */
+ }
+ else
+- *pfile_name = xstrdup (one_top_level_dir);
+- free (file_name);
++ {
++ *pfile_name = xstrdup (".");
++ free (file_name);
++ }
+ }
+
+ void
+@@ -158,8 +170,15 @@ transform_stat_info (int typeflag, struct tar_stat_info *stat_info)
+ transform_member_name (&stat_info->link_name, XFORM_LINK);
+ }
+
+- if (one_top_level_option)
+- enforce_one_top_level (¤t_stat_info.file_name);
++ if (one_top_level_dir)
++ {
++ enforce_one_top_level (&stat_info->file_name);
++ /* Hard links are interpreted relative to cwd, and --one-top-level
++ works by means of a hidden change of cwd to the requested directory.
++ Adjust hard link targets as well. */
++ if (typeflag == LNKTYPE)
++ enforce_one_top_level (&stat_info->link_name);
++ }
+ }
+
+ /* Main loop for reading an archive. */
+@@ -1168,9 +1187,9 @@ simple_print_header (struct tar_stat_info *st, union block *blk,
+ int sizelen;
+
+ if (show_transformed_names_option)
+- temp_name = st->file_name ? st->file_name : st->orig_file_name;
++ temp_name = transform_top_level (st->file_name ? st->file_name : st->orig_file_name);
+ else
+- temp_name = st->orig_file_name ? st->orig_file_name : st->file_name;
++ temp_name = xstrdup (st->orig_file_name ? st->orig_file_name : st->file_name);
+
+ if (block_number_option)
+ {
+@@ -1363,6 +1382,7 @@ simple_print_header (struct tar_stat_info *st, union block *blk,
+ }
+ fflush (stdlis);
+ xattrs_print (st);
++ free (temp_name);
+ }
+
+
+diff --git a/src/misc.c b/src/misc.c
+index 0e874eb..e14df2b 100644
+--- a/src/misc.c
++++ b/src/misc.c
+@@ -23,6 +23,7 @@
+ #include <xgetcwd.h>
+ #include <unlinkdir.h>
+ #include <utimens.h>
++#include <assert.h>
+
+ #ifndef DOUBLE_SLASH_IS_DISTINCT_ROOT
+ # define DOUBLE_SLASH_IS_DISTINCT_ROOT 0
+@@ -946,6 +947,7 @@ struct wd
+ to be used. */
+ int fd;
+
++ bool one_top_level;
+ /* If ID.err is zero, the directory's identity;
+ if positive, a failure indication with errno = ID.err;
+ if negative, no attempt has been made yet to get the identity. */
+@@ -977,9 +979,17 @@ static size_t wdcache_count;
+ int
+ chdir_count (void)
+ {
+- if (wd_count == 0)
+- return wd_count;
+- return wd_count - 1;
++ int count = 0;
++ if (wd_count)
++ {
++ /* Do not count the initial CWD entry -> start at 1. */
++ for (int i = 1; i < wd_count; i++)
++ {
++ if (! wd[i].one_top_level)
++ count++;
++ }
++ }
++ return count;
+ }
+
+ /* Grow the WD table by at least one entry. */
+@@ -996,15 +1006,27 @@ grow_wd (void)
+ wd[wd_count].abspath = NULL;
+ wd[wd_count].fd = AT_FDCWD;
+ wd[wd_count].id.err = -1;
++ wd[wd_count].one_top_level = false;
+ wd_count++;
++ if (one_top_level_dir)
++ {
++ wd[wd_count].name = one_top_level_dir;
++ wd[wd_count].abspath = NULL;
++ wd[wd_count].fd = 0;
++ wd[wd_count].id.err = -1;
++ wd[wd_count].one_top_level = true;
++ wd_count++;
++ }
+ }
+ }
+
+ /* DIR is the operand of a -C option; add it to vector of chdir targets,
+ and return the index of its location. */
+ int
+-chdir_arg (char const *dir)
++chdir_arg (char const *dir, bool one_top_level)
+ {
++ if (one_top_level)
++ chdir_arg (dir, false);
+ if (wd_count == wd_alloc)
+ grow_wd ();
+
+@@ -1014,13 +1036,22 @@ chdir_arg (char const *dir)
+ {
+ dir += dotslashlen (dir);
+ if (! dir[dir[0] == '.'])
+- return wd_count - 1;
++ {
++ if (wd[wd_count - 1].one_top_level == one_top_level)
++ return wd_count - 1;
++ else
++ return wd_count - 2;
++ }
+ }
+
++ if (one_top_level)
++ dir = one_top_level_dir;
++
+ wd[wd_count].name = dir;
+ wd[wd_count].abspath = NULL;
+ wd[wd_count].fd = 0;
+ wd[wd_count].id.err = -1;
++ wd[wd_count].one_top_level = one_top_level;
+ return wd_count++;
+ }
+
+@@ -1039,21 +1070,74 @@ static int chdir_fd = AT_FDCWD;
+ working directory; otherwise, I must be a value returned by
+ chdir_arg. */
+ void
+-chdir_do (int i)
++chdir_do (int i, bool create)
+ {
+- if (chdir_current != i)
+- {
+- struct wd *curr = &wd[i];
+- int fd = curr->fd;
++ struct wd *curr = &wd[i];
++ int fd = curr->fd;
+
+- if (! fd)
++ /* Nothing to create unless we are at the one_top_level dir that has
++ not been created yet. */
++ create = create && curr->one_top_level && (fd == BADFD || fd == 0);
++
++ if (chdir_current != i || create)
++ {
++ if (! fd || create)
+ {
+ if (! IS_ABSOLUTE_FILE_NAME (curr->name))
+- chdir_do (i - 1);
++ {
++ int j = i - 1;
++ if (wd[j].one_top_level)
++ {
++ j--;
++ assert (! wd[j].one_top_level);
++ }
++ chdir_do (j, false);
++ }
+ fd = openat (chdir_fd, curr->name,
+ open_searchdir_how.flags & ~O_NOFOLLOW);
+ if (fd < 0)
+- open_fatal (curr->name);
++ {
++ if (create)
++ {
++ char *dir_with_dot;
++ struct open_how saved_open_searchdir_how = open_searchdir_how;
++ /* Don't use O_BENEATH during creation of the
++ directory. The one-top-level directory is
++ allowed to be given as an absolute path. */
++ open_searchdir_how.resolve = 0;
++ /* Append a dot. make_directories creates
++ directories up to and excluding the last
++ component of the path. So, in order to create
++ "a/b", we need to pass "a/b/." to it. */
++ {
++ namebuf_t nbuf = namebuf_create (curr->name);
++ namebuf_add_dir (nbuf, ".");
++ dir_with_dot = namebuf_finish (nbuf);
++ }
++ if (make_directories (dir_with_dot, NULL) == 0)
++ /* Directory created, retry */
++ fd = openat (chdir_fd, curr->name,
++ open_searchdir_how.flags & ~O_NOFOLLOW);
++ open_searchdir_how = saved_open_searchdir_how;
++ free (dir_with_dot);
++ /* Either the creation or open failed */
++ if (fd < 0)
++ open_fatal (curr->name);
++ }
++ else if (errno == ENOENT && curr->one_top_level)
++ {
++ /* We are requested to not create the directory now. Mark it
++ as to be created later when called with create == true. */
++ chdir_fd = curr->fd = BADFD;
++ chdir_current = i;
++ /* Do not add it to the cache */
++ return;
++ }
++ else
++ {
++ open_fatal (curr->name);
++ }
++ }
+
+ curr->fd = fd;
+
+@@ -1071,7 +1155,7 @@ chdir_do (int i)
+ }
+ }
+
+- if (0 < fd)
++ if (0 < fd && /* no assumption about sign of BADFD */ fd != BADFD)
+ {
+ /* Move the i value to the front of the cache. This is
+ O(CHDIR_CACHE_SIZE), but the cache is small. */
+@@ -1175,6 +1259,14 @@ fdbase_opendir (char const *file_name, bool alternate)
+ {
+ char const *name = file_name;
+
++ if (chdir_fd == BADFD && ! IS_ABSOLUTE_FILE_NAME (file_name))
++ {
++ /* BADFD is a sentinel value meaning that the chdir directory
++ needs to be created lazily, therefore if we encounter it, the
++ directory does not exist yet. */
++ errno = ENOENT;
++ return (struct fdbase) { .fd = chdir_fd, .base = name };
++ }
+ /* Skip past leading "./"s,
+ but not past the last "./" if that ends the name. */
+ idx_t dslen = dotslashlen (name);
+@@ -1219,13 +1311,13 @@ fdbase_opendir (char const *file_name, bool alternate)
+ if (subfd < 0)
+ {
+ /* Keep the old directory cached and report open failure,
+- unless EMFILE means it's possible that falling
++ unless EMFILE/ENFILE means it's possible that falling
+ through to close the old directory would mean we
+ could successfully retry from the chdir_fd level.
+ When reporting failure, there is no need to
+ null-terminate the old directory, since the code does
+ not assume null termination. */
+- if (errno != EMFILE)
++ if (errno != EMFILE && errno != ENFILE)
+ return (struct fdbase) { .fd = BADFD, .base = base };
+ }
+ else
+@@ -1279,6 +1371,29 @@ tar_dirname (void)
+ return wd[chdir_current].name;
+ }
+
++/* Return a newly allocated string that shows NAME from the user's
++ viewpoint, given that --one-top-level may be in effect. */
++char *
++transform_top_level (const char *name)
++{
++ if (wd[chdir_current].one_top_level)
++ {
++ if (strcmp (name, ".") == 0)
++ {
++ /* nothing to append - .../. is the same as ... */
++ return xstrdup (wd[chdir_current].name);
++ }
++ else
++ {
++ namebuf_t nbuf = namebuf_create (wd[chdir_current].name);
++ namebuf_add_dir (nbuf, name);
++ return namebuf_finish (nbuf);
++ }
++ }
++ else
++ return xstrdup (name);
++}
++
+ /* Return the absolute path that represents the working
+ directory referenced by IDX.
+
+@@ -1307,12 +1422,13 @@ tar_getcdpath (int idx)
+ int save_cwdi = chdir_current;
+
+ for (i = idx; i >= 0; i--)
+- if (wd[i].abspath)
++ if (wd[i].abspath && !wd[i].one_top_level)
+ break;
+
+ while (++i <= idx)
+ {
+- chdir_do (i);
++ if (!wd[i].one_top_level)
++ chdir_do (i, false);
+ if (i == 0)
+ {
+ if ((wd[i].abspath = xgetcwd ()) == NULL)
+@@ -1325,13 +1441,18 @@ tar_getcdpath (int idx)
+ wd[i].abspath = xstrdup (wd[i].name);
+ else
+ {
+- namebuf_t nbuf = namebuf_create (wd[i - 1].abspath);
++ int j = i - 1;
++ if (wd[j].one_top_level)
++ {
++ j--;
++ assert (! wd[j].one_top_level);
++ }
++ namebuf_t nbuf = namebuf_create (wd[j].abspath);
+ namebuf_add_dir (nbuf, wd[i].name);
+ wd[i].abspath = namebuf_finish (nbuf);
+ }
+ }
+-
+- chdir_do (save_cwdi);
++ chdir_do (save_cwdi, false);
+ }
+
+ return wd[idx].abspath;
+diff --git a/src/names.c b/src/names.c
+index 54fd696..eb063fe 100644
+--- a/src/names.c
++++ b/src/names.c
+@@ -868,6 +868,7 @@ name_init (void)
+ {
+ name_buffer = xmalloc (NAME_FIELD_SIZE + 2);
+ name_buffer_length = NAME_FIELD_SIZE;
++ chdir_do (chdir_arg (".", !!one_top_level_dir), false);
+ name_list_adjust ();
+ }
+
+@@ -1120,7 +1121,8 @@ name_next_elt (int change_dirs)
+ case NELT_CHDIR:
+ if (change_dirs)
+ {
+- chdir_do (chdir_arg (xstrdup (ep->v.name)));
++ chdir_do (chdir_arg (xstrdup (ep->v.name), !!one_top_level_dir),
++ false);
+ name_list_advance ();
+ break;
+ }
+@@ -1183,7 +1185,7 @@ name_gather (void)
+ static int change_dir;
+
+ while ((ep = name_next_elt (0)) && ep->type == NELT_CHDIR)
+- change_dir = chdir_arg (xstrdup (ep->v.name));
++ change_dir = chdir_arg (xstrdup (ep->v.name), !!one_top_level_dir);
+
+ if (ep)
+ {
+@@ -1212,7 +1214,7 @@ name_gather (void)
+ {
+ int change_dir0 = change_dir;
+ while ((ep = name_next_elt (0)) && ep->type == NELT_CHDIR)
+- change_dir = chdir_arg (xstrdup (ep->v.name));
++ change_dir = chdir_arg (xstrdup (ep->v.name), !!one_top_level_dir);
+
+ if (ep)
+ addname (ep->v.name, change_dir, true, NULL);
+@@ -1327,7 +1329,7 @@ name_match (const char *file_name)
+
+ if (cursor->name[0] == 0)
+ {
+- chdir_do (cursor->change_dir);
++ chdir_do (cursor->change_dir, false);
+ namelist = NULL;
+ nametail = NULL;
+ return true;
+@@ -1349,7 +1351,7 @@ name_match (const char *file_name)
+ if (!(ISSLASH (file_name[cursor->length]) && recursion_option)
+ || cursor->found_count == 0)
+ cursor->found_count++; /* remember it matched */
+- chdir_do (cursor->change_dir);
++ chdir_do (cursor->change_dir, false);
+ /* We got a match. */
+ return ISFOUND (cursor);
+ }
+@@ -1783,7 +1785,7 @@ collect_and_sort_names (void)
+ /* NOTE: EXCLUDE_ANCHORED is not relevant here */
+ /* FIXME: just skip regexps for now */
+ continue;
+- chdir_do (name->change_dir);
++ chdir_do (name->change_dir, false);
+
+ if (name->name[0] == 0)
+ continue;
+@@ -1929,7 +1931,7 @@ name_from_list (void)
+ {
+ if (!gnu_list_name->is_wildcard)
+ gnu_list_name->found_count++;
+- chdir_do (gnu_list_name->change_dir);
++ chdir_do (gnu_list_name->change_dir, false);
+ return gnu_list_name;
+ }
+ return NULL;
+diff --git a/src/tar.c b/src/tar.c
+index 9a5fbd0..56c9ed2 100644
+--- a/src/tar.c
++++ b/src/tar.c
+@@ -1572,7 +1572,6 @@ parse_opt (int key, char *arg, struct argp_state *state)
+
+ case ONE_TOP_LEVEL_OPTION:
+ optloc_save (OC_ONE_TOP_LEVEL, args->loc);
+- one_top_level_option = true;
+ one_top_level_dir = arg;
+ break;
+
+@@ -2576,9 +2575,11 @@ decode_options (int argc, char **argv)
+ same_order_option = false;
+ }
+
+- if (one_top_level_option)
++ if (optloc_lookup (OC_ONE_TOP_LEVEL))
+ {
+- char *base;
++ if (!IS_SUBCOMMAND_CLASS (SUBCL_READ))
++ option_conflict_error ("--one-top-level",
++ subcommand_string (subcommand_option));
+
+ if (absolute_names_option)
+ {
+@@ -2589,17 +2590,18 @@ decode_options (int argc, char **argv)
+
+ if (optloc_eq (one_top_level_loc, absolute_names_loc))
+ option_conflict_error ("--one-top-level", "--absolute-names");
+- else if (one_top_level_loc->source == OPTS_COMMAND_LINE)
++ if (one_top_level_loc->source == OPTS_COMMAND_LINE)
+ absolute_names_option = false;
+ else
+- one_top_level_option = false;
++ one_top_level_dir = NULL;
+ }
+
+- if (!one_top_level_dir && one_top_level_option)
++ if (!absolute_names_option && !one_top_level_dir)
+ {
+- /* If the user wants to guarantee that everything is under one
+- directory, determine its name now and let it be created later. */
+- base = base_name (archive_name_array[0]);
++ /* Determine name now; the directory (or directories, if -C
++ means there are multiple top-level directories) are
++ created later if needed. */
++ char *base = base_name (archive_name_array[0]);
+ one_top_level_dir = strip_compression_suffix (base);
+ free (base);
+
+@@ -2609,9 +2611,7 @@ decode_options (int argc, char **argv)
+ "please set it explicitly with --one-top-level=DIR")));
+ }
+
+- if (one_top_level_dir && !IS_RELATIVE_FILE_NAME (one_top_level_dir))
+- USAGE_ERROR ((0, 0,
+- _("--one-top-level=DIR must use a relative file name")));
++ normalize_filename_x (one_top_level_dir);
+ }
+
+ /* If ready to unlink hierarchies, so we are for simpler files. */
+diff --git a/src/unlink.c b/src/unlink.c
+index 33d2ccd..e199bab 100644
+--- a/src/unlink.c
++++ b/src/unlink.c
+@@ -103,7 +103,7 @@ flush_deferred_unlinks (bool force)
+ if (force
+ || records_written > p->records_written + deferred_unlink_delay)
+ {
+- chdir_do (p->dir_idx);
++ chdir_do (p->dir_idx, false);
+ if (p->is_dir)
+ {
+ const char *fname;
+@@ -175,11 +175,11 @@ flush_deferred_unlinks (bool force)
+ struct deferred_unlink *next = p->next;
+ const char *fname;
+
+- chdir_do (p->dir_idx);
++ chdir_do (p->dir_idx, false);
+ if (p->dir_idx && IS_CWD (p))
+ {
+ fname = tar_dirname ();
+- chdir_do (p->dir_idx - 1);
++ chdir_do (p->dir_idx - 1, false);
+ }
+ else
+ fname = p->file_name;
+@@ -195,9 +195,9 @@ flush_deferred_unlinks (bool force)
+ p = next;
+ }
+ dunlink_head = dunlink_tail = NULL;
+- }
+-
+- chdir_do (saved_chdir);
++ }
++
++ chdir_do (saved_chdir, false);
+ }
+
+ void
+diff --git a/src/update.c b/src/update.c
+index 2bc3820..2416777 100644
+--- a/src/update.c
++++ b/src/update.c
+@@ -141,7 +141,7 @@ update_archive (void)
+ {
+ struct stat s;
+
+- chdir_do (name->change_dir);
++ chdir_do (name->change_dir, false);
+ if (deref_stat (current_stat_info.file_name, &s) == 0)
+ {
+ if (S_ISDIR (s.st_mode))
diff --git a/tar-CVE-2026-5704.patch b/tar-CVE-2026-5704.patch
new file mode 100644
index 0000000..93287a6
--- /dev/null
+++ b/tar-CVE-2026-5704.patch
@@ -0,0 +1,472 @@
+# upstream b8d8a61b: make -t and -x agree on nonzero size fields of
+# symlinks, devices and fifos (CVE-2026-5704)
+diff --git a/src/extract.c b/src/extract.c
+index ae03190..d1c86a0 100644
+--- a/src/extract.c
++++ b/src/extract.c
+@@ -1056,7 +1056,7 @@ safe_dir_mode (struct stat const *st)
+ /* Extractor functions for various member types */
+
+ static int
+-extract_dir (char *file_name, int typeflag)
++extract_dir (char *file_name, MAYBE_UNUSED int typeflag)
+ {
+ int status;
+ mode_t mode;
+@@ -1081,8 +1081,6 @@ extract_dir (char *file_name, int typeflag)
+ if (incremental_option)
+ /* Read the entry and delete files that aren't listed in the archive. */
+ purge_directory (file_name);
+- else if (typeflag == GNUTYPE_DUMPDIR)
+- skip_member ();
+
+ mode = safe_dir_mode (¤t_stat_info.stat);
+
+@@ -1291,10 +1289,7 @@ extract_file (char *file_name, int typeflag)
+ {
+ fd = sys_exec_command (file_name, 'f', ¤t_stat_info);
+ if (fd < 0)
+- {
+- skip_member ();
+- return 0;
+- }
++ return 0;
+ }
+ else
+ {
+@@ -1314,7 +1309,6 @@ extract_file (char *file_name, int typeflag)
+ int recover = maybe_recoverable (file_name, true, &interdir_made);
+ if (recover != RECOVER_OK)
+ {
+- skip_member ();
+ if (recover == RECOVER_SKIP)
+ return 0;
+ open_error (file_name);
+@@ -1362,6 +1356,7 @@ extract_file (char *file_name, int typeflag)
+ }
+
+ skim_file (size, false);
++ current_stat_info.skipped = true;
+
+ mv_end ();
+
+@@ -1894,15 +1889,10 @@ extract_archive (void)
+ typeflag = sparse_member_p (¤t_stat_info) ?
+ GNUTYPE_SPARSE : current_header->header.typeflag;
+
+- if (prepare_to_extract (current_stat_info.file_name, typeflag, &fun))
+- {
+- if (fun (current_stat_info.file_name, typeflag) == 0)
+- return;
+- }
+- else
+- skip_member ();
+-
+- if (backup_option)
++ bool ok = prepare_to_extract (current_stat_info.file_name, typeflag, &fun)
++ && fun (current_stat_info.file_name, typeflag) == 0;
++ skip_member ();
++ if (!ok && backup_option)
+ undo_last_backup ();
+ }
+
+diff --git a/src/incremen.c b/src/incremen.c
+index 7bcfdb9..194d5cb 100644
+--- a/src/incremen.c
++++ b/src/incremen.c
+@@ -1625,8 +1625,8 @@ dumpdir_ok (char *dumpdir)
+
+ /* Examine the directories under directory_name and delete any
+ files that were not there at the time of the back-up. */
+-static bool
+-try_purge_directory (char const *directory_name)
++void
++purge_directory (char const *directory_name)
+ {
+ char *current_dir;
+ char *cur, *arc, *p;
+@@ -1634,18 +1634,18 @@ try_purge_directory (char const *directory_name)
+ struct dumpdir *dump;
+
+ if (!is_dumpdir (¤t_stat_info))
+- return false;
++ return;
+
+ current_dir = tar_savedir (directory_name, 0);
+
+ if (!current_dir)
+ /* The directory doesn't exist now. It'll be created. In any
+ case, we don't have to delete any files out of it. */
+- return false;
++ return;
+
+ /* Verify if dump directory is sane */
+ if (!dumpdir_ok (current_stat_info.dumpdir))
+- return false;
++ return;
+
+ /* Process renames */
+ for (arc = current_stat_info.dumpdir; *arc; arc += strlen (arc) + 1)
+@@ -1666,7 +1666,7 @@ try_purge_directory (char const *directory_name)
+ quote (temp_stub)));
+ free (temp_stub);
+ free (current_dir);
+- return false;
++ return;
+ }
+ }
+ else if (*arc == 'R')
+@@ -1700,7 +1700,7 @@ try_purge_directory (char const *directory_name)
+ free (current_dir);
+ /* FIXME: Make sure purge_directory(dst) will return
+ immediately */
+- return false;
++ return;
+ }
+ }
+ }
+@@ -1758,14 +1758,6 @@ try_purge_directory (char const *directory_name)
+ dumpdir_free (dump);
+
+ free (current_dir);
+- return true;
+-}
+-
+-void
+-purge_directory (char const *directory_name)
+-{
+- if (!try_purge_directory (directory_name))
+- skip_member ();
+ }
+
+ void
+diff --git a/src/list.c b/src/list.c
+index f963d73..b560569 100644
+--- a/src/list.c
++++ b/src/list.c
+@@ -446,20 +446,15 @@ read_header (union block **return_block, struct tar_stat_info *info,
+ if ((status = tar_checksum (header, false)) != HEADER_SUCCESS)
+ break;
+
+- /* Good block. Decode file size and return. */
+-
+- if (header->header.typeflag == LNKTYPE)
+- info->stat.st_size = 0; /* links 0 size on tape */
+- else
++ info->stat.st_size = OFF_FROM_HEADER (header->header.size);
++ if (info->stat.st_size < 0)
+ {
+- info->stat.st_size = OFF_FROM_HEADER (header->header.size);
+- if (info->stat.st_size < 0)
+- {
+- status = HEADER_FAILURE;
+- break;
+- }
++ status = HEADER_FAILURE;
++ break;
+ }
+
++ info->skipped = false;
++
+ if (header->header.typeflag == GNUTYPE_LONGNAME
+ || header->header.typeflag == GNUTYPE_LONGLINK
+ || header->header.typeflag == XHDTYPE
+@@ -522,11 +517,15 @@ read_header (union block **return_block, struct tar_stat_info *info,
+ }
+
+ *bp = '\0';
++ info->skipped = true;
+ }
+ else if (header->header.typeflag == XHDTYPE
+ || header->header.typeflag == SOLARIS_XHDTYPE)
+- xheader_read (&info->xhdr, header,
+- OFF_FROM_HEADER (header->header.size));
++ {
++ xheader_read (&info->xhdr, header,
++ OFF_FROM_HEADER (header->header.size));
++ info->skipped = true;
++ }
+ else if (header->header.typeflag == XGLTYPE)
+ {
+ struct xheader xhdr;
+@@ -540,6 +539,7 @@ read_header (union block **return_block, struct tar_stat_info *info,
+ OFF_FROM_HEADER (header->header.size));
+ xheader_decode_global (&xhdr);
+ xheader_destroy (&xhdr);
++ info->skipped = true;
+ if (mode == read_header_x_global)
+ {
+ status = HEADER_SUCCESS_EXTENDED;
+@@ -556,6 +556,22 @@ read_header (union block **return_block, struct tar_stat_info *info,
+ struct posix_header const *h = &header->header;
+ char namebuf[sizeof h->prefix + 1 + NAME_FIELD_SIZE + 1];
+
++ switch (h->typeflag)
++ {
++ /* For these file types, although POSIX does not specify the
++ meaning of the size, it does say there should be no data,
++ so treat the size as zero. */
++ case BLKTYPE: case CHRTYPE: case FIFOTYPE:
++
++ /* For these file types, POSIX requires that the size be zero.
++ Be generous and accept any size as zero, as some
++ nonconforming programs generate nonzero size fields along
++ with no data. */
++ case LNKTYPE: case SYMTYPE:
++
++ info->stat.st_size = 0;
++ }
++
+ free (recent_long_name);
+
+ if (next_long_name)
+@@ -1449,6 +1465,24 @@ skip_member (void)
+ skim_member (false);
+ }
+
++static bool
++member_is_dir (struct tar_stat_info *info, char typeflag)
++{
++ switch (typeflag)
++ {
++ case AREGTYPE:
++ case REGTYPE:
++ case CONTTYPE:
++ return info->had_trailing_slash;
++
++ case DIRTYPE:
++ return true;
++
++ default:
++ return false;
++ }
++}
<Skipped 878 lines>
================================================================
---- gitweb:
http://git.pld-linux.org/gitweb.cgi/packages/tar.git/commitdiff/adaef9d884f75901a38e4d96e12c2f47b9b16714
More information about the pld-cvs-commit
mailing list