tar: support --one-top-dir=/abs/olute

It appears that people were using the --one-top-dir=DIR option
with an absolute file name, even though this behavior was
undocumented and erratic and recent tar versions rejected it.
Document and implement behavior that I hope people expect.
* src/extract.c (MAKEDIR_FULL_ESCAPE): New constant.
(make_directories): Let full directory names escape if
MAKEDIR_FULL_ESCAPE is passed.
(create_dir): Let DIR escape if it is the one-top-level dir
or an ancestor.
* src/misc.c (fdbase_opendir): Replace old CHILD_OFLAGS arg with
two new args CHILD and OFLAG, so that the functionalities can be
distinguished.  All callers changed.
(fdbase_escape): New function.
(fdbase): Use it, for simplicity.
* tests/onetop06.at: New test file.
* tests/Makefile.am (TESTSUITE_AT): Add it.
* tests/testsuite.at: Include it.
This commit is contained in:
Paul Eggert
2026-08-20 15:46:21 -07:00
parent 0713d3502a
commit 961e48a903
8 changed files with 164 additions and 46 deletions
+6 -1
View File
@@ -1,4 +1,4 @@
GNU tar NEWS - User visible changes. 2026-07-22
GNU tar NEWS - User visible changes. 2026-08-20
Please send GNU tar bug reports to <bug-tar@gnu.org>
version 1.35.90 (git)
@@ -38,6 +38,11 @@ option.
** The --one-top-level option now requires either -x or -d mode.
Previously the behavior was unspecified in other operation modes.
** If DIR is absolute, the --one-top-level=DIR option now extracts all
files relative to DIR, without stripping leading DIR from file names.
Any -C options are ignored. Formerly, the behavior was
undocumented and differed from one tar version to the next.
* Bug fixes
** When extracting, tar no longer follows symbolic links to targets
+53 -24
View File
@@ -3287,23 +3287,13 @@ directory.
@opsummary{one-top-level}
@item --one-top-level[=@var{dir}]
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 @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
@option{--strip-components}) will be prefixed with it. Recognized
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.
Extract files relative to @var{dir},
ignoring any @var{dir} prefixes in the archive.
An absent @var{dir} defaults to
the base name of the archive minus any recognized archive suffix.
If @var{dir} is relative, it is relative to the working directory;
if absolute, any @option{--directory} (@option{-C}) options are irrelevant.
@xref{directory}.
@opsummary{overwrite}
@item --overwrite
@@ -9440,23 +9430,59 @@ mentioned by name on the standard error.
@node directory
@subsection Changing the Working Directory
@cindex Changing directory mid-stream
@cindex Directory, changing mid-stream
@cindex Changing working directory
@cindex Directory, changing
@cindex Working directory, specifying
To change the working directory in the middle of a list of file names,
either on the command line or in a file specified using
@option{--files-from} (@option{-T}), use @option{--directory} (@option{-C}).
This will change the working directory to the specified directory
after that point in the list.
You can also use the @code{--one-top-level} option to alter
the working directory location, though this option applies to all file names.
@table @option
@opindex directory
@item --directory=@var{directory}
@itemx -C @var{directory}
Changes the working directory in the middle of a command line.
Change the working directory for later files in the command line.
@item --one-top-level
@item --one-top-level[=@var{dir}]
Change the effective working directory to @var{dir} for all files.
If @var{dir} does not already exist, create it and any ancestors as
necessary before extracting any files into it.
If @var{dir} is relative, extract all files under that directory;
if @option{-C} is also given,
@var{dir} is relative to the @option{-C} directory.
Prefix any member names that do not begin with @var{dir}
(after transformations from @option{--transform} and
@option{--strip-components}) with @var{dir}.
An absent @var{dir} defaults to the base name of the archive
minus any recognized archive suffix.
Recognized file name suffixes are @samp{.tar},
and any compression suffixes recognizable by @xref{--auto-compress}.
If @var{dir} is absolute, extract all files under that directory;
any @option{-C} options are irrelevant,
and @var{dir} itself is exempt from the usual safety rules
prohibiting extraction outside the working directory.
The two main differences between @samp{--one-top-level=/abs/olute}
and @samp{-C /abs/olute} are that the former affects all files
whereas the latter affects only later files,
and in the former @file{/abs/olute} and its ancestors are created
if they do not already exist
whereas in the latter @file{/abs/olute} must already exist.
The @option{--one-top-level} option can be used only when extracting,
comparing, or reading from the archive.
It is incompatible with the @option{--absolute-names} (@option{-P}) option.
@end table
All filename arguments appearing after this option will be taken
All filename arguments appearing after a
@option{--directory=@var{directory}} (@option{-C @var{directory}}) option
are taken
relative to @var{directory}. For example,
@smallexample
@@ -9511,7 +9537,7 @@ the original working directory of @command{tar}, due to a previous
@option{--directory} option.
There is an important exception, though: the @option{-C} option does
not affect filename arguments to another options that follow it, in
not affect filename arguments to other options that follow it, in
particular @option{-f}. This means that the example above can be
written as
@@ -9538,6 +9564,7 @@ The command-line options not affected by @option{-C} are:
@item @option{-T}, @option{--files-from}
@item @option{-X}, @option{--exclude-from}
@item @option{--mtime}
@item @option{--one-top-level=@var{dir}} when @var{dir} is absolute
@item @option{--owner-map}
@item @option{-f}, @option{--file}
@item @option{-F}, @option{--info-script}, @option{--new-volume-script}
@@ -13171,8 +13198,10 @@ by an untrusted user, that user therefore can write into any file
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. You can use the
@option{--directory} (@option{-C}) option to specify the working
directory (@pxref{directory}).
@option{--directory} (@option{-C}) and @option{--one-top-level}
options to specify the working directory; the latter option starts by
creating the working directory for you if it does not already exist.
@xref{directory}.
When extracting from an archive, @command{tar} by default rejects attempts to
modify files outside the working directory.
+1
View File
@@ -787,6 +787,7 @@ void chdir_do (idx_t dir, bool create);
struct chdir_id { int err; dev_t st_dev; ino_t st_ino; } chdir_id (void);
struct fdbase fdbase (char const *);
struct fdbase fdbase1 (char const *);
struct fdbase fdbase_escape (char const *, bool);
int open_searchdir (char const *);
int fdbase_close (int);
void fdbase_clear (void);
+29 -8
View File
@@ -771,11 +771,10 @@ fixup_delayed_set_stat (char const *src, char const *dst)
}
/* Ensure that a directory exists by creating it and ancestors as needed.
If MAKEDIR == MAKEDIR_FULL the directory is FILE_NAME;
otherwise it is FILE_NAME's parent directory.
Follow symlinks. Do not overwrite existing files.
Allow races with other processes that are also trying to create
the requested directory.
Allow races with other processes that are doing likewise.
Do not overwrite existing files.
Follow symlinks, but do not escape the destination directory
unless MAKEDIR == MAKEDIR_FULL_ESCAPE.
Possibly temporarily modify FILE_NAME if it contains slashes,
but restore it before returning.
Return:
@@ -783,7 +782,23 @@ fixup_delayed_set_stat (char const *src, char const *dst)
0 if the requested directory likely exists
(and if MAKEDIR == MAKEDIR_PARENT_CHECK, check that it does exist),
-1 (issuing a diagnostic) otherwise. */
enum makedir { MAKEDIR_PARENT_CHECK = -1, MAKEDIR_PARENT, MAKEDIR_FULL };
enum makedir
{
/* Make the file name's parent directory,
and also check that it was made. */
MAKEDIR_PARENT_CHECK = -1,
/* Make the file name's parent directory,
but for efficiency do not check that it was actually made. */
MAKEDIR_PARENT,
/* Treat the entire file name as the name of the directory to be made. */
MAKEDIR_FULL,
/* Likewise, but ignore the usual fence around the destination
directory. */
MAKEDIR_FULL_ESCAPE
};
static int
make_directories (char *file_name, enum makedir makedir)
{
@@ -841,7 +856,8 @@ make_directories (char *file_name, enum makedir makedir)
*cursor = '\0'; /* truncate the name there */
desired_mode = MODE_RWX & ~ newdir_umask;
mode = desired_mode | (we_are_root ? 0 : MODE_WXUSR);
struct fdbase f = fdbase (file_name);
struct fdbase f = fdbase_escape (file_name,
makedir == MAKEDIR_FULL_ESCAPE);
if (f.fd != BADFD && mkdirat (f.fd, f.base, mode) == 0)
{
@@ -918,7 +934,12 @@ make_directories (char *file_name, enum makedir makedir)
bool
create_dir (char *dir)
{
return 0 <= make_directories (dir, MAKEDIR_FULL);
/* If --one-top-level=TOPDIR is used and TOPDIR is absolute,
DIR must be TOPDIR or an ancestor, so let DIR escape. */
return 0 <= make_directories (dir,
((one_top_level_dir
&& IS_ABSOLUTE_FILE_NAME (one_top_level_dir))
? MAKEDIR_FULL_ESCAPE : MAKEDIR_FULL));
}
/* Return true if FILE_NAME (with status *STP, if STP) is not a
+34 -13
View File
@@ -32,7 +32,7 @@
static void namebuf_add_dir (namebuf_t, char const *);
static char *namebuf_finish (namebuf_t);
static const char *tar_getcdpath (idx_t);
static struct fdbase fdbase_opendir (char const *, bool, int);
static struct fdbase fdbase_opendir (char const *, bool, bool, int);
char const *
quote_n_colon (int n, char const *arg)
@@ -1109,7 +1109,7 @@ chdir_do (idx_t i, bool create)
chdir_do ((i - 1) & ~+one_top_level, false);
int oflags = open_searchdir_how.flags & ~O_NOFOLLOW;
fd = fdbase_opendir (curr->name, false, oflags).fd;
fd = fdbase_opendir (curr->name, false, true, oflags).fd;
if (fd < 0)
{
if (errno == ENOENT)
@@ -1119,7 +1119,7 @@ chdir_do (idx_t i, bool create)
if (!create_dir (curr->name))
fatal_exit ();
/* Directory likely exists now; retry. */
fd = fdbase_opendir (curr->name, false, oflags).fd;
fd = fdbase_opendir (curr->name, false, true, oflags).fd;
}
else if (i & one_top_level)
{
@@ -1282,12 +1282,13 @@ open_subdir (int fd, char const *subdir, int oflags)
only the alternate cache; otherwise, use and update only the main cache;
this means a call with ALTERNATE cannot invalidate a call without.
If FILE_NAME is relative, it is relative to chdir_fd.
If CHILD_OFLAGS, open FILE_NAME itself, posssibly letting it escape from
chdir_fd; otherwise, open FILE_NAME's parent but do not let it escape.
If CHILD, open FILE_NAME; otherwise open FILE_NAME's parent directory.
If OFLAGS, open the directory with those flags, possibly letting it
escape from chdir_fd; otherwise, do not let it escape.
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, int child_oflags)
fdbase_opendir (char const *file_name, bool alternate, bool child, int oflags)
{
char const *name = file_name;
int dfd = IS_ABSOLUTE_FILE_NAME (file_name) ? AT_FDCWD : chdir_fd;
@@ -1311,10 +1312,10 @@ fdbase_opendir (char const *file_name, bool alternate, int child_oflags)
continue;
}
/* For files immediately under CHDIR_FD, and for root directories,
just use CHDIR_FD and NAME. Empty NAME is invalid, though. */
/* For file names immediately under DFD, and for names of root directories,
just use DFD and NAME. Empty NAME is invalid, though. */
char const *base = last_component (name);
idx_t newdirlen = base + (child_oflags ? strlen (base) : 0) - name;
idx_t newdirlen = base + (child ? strlen (base) : 0) - name;
if (!newdirlen | !*base)
{
if (*name)
@@ -1380,7 +1381,7 @@ fdbase_opendir (char const *file_name, bool alternate, int child_oflags)
open descendant to FD rather than to CHDIR_FD. */
bool descendant = old_prefixes_new & chdirmatch;
int newfd = open_subdir (descendant ? fd : chdir_fd,
&newdir[descendant ? subdirlen : 0], child_oflags);
&newdir[descendant ? subdirlen : 0], oflags);
if (newfd < 0)
return (struct fdbase) { .fd = BADFD, .base = base };
@@ -1395,22 +1396,42 @@ fdbase_opendir (char const *file_name, bool alternate, int child_oflags)
return (struct fdbase) { .fd = newfd, .base = base };
}
/* Return an fd open to NAME's parent directory
along with the corresponding base name.
Do not escape from chdir_fd unless ESCAPE or unless -P is used. */
struct fdbase
fdbase_escape (char const *name, bool escape)
{
return fdbase_opendir (name, false, false,
escape ? open_searchdir_how.flags : 0);
}
/* Return an fd open to NAME's parent directory
along with the corresponding base name.
Do not escape from chdir_fd unless -P is used. */
struct fdbase
fdbase (char const *name)
{
return fdbase_opendir (name, false, 0);
return fdbase_escape (name, false);
}
/* Return an fd open to NAME's parent directory
along with the corresponding base name.
Do not escape from chdir_fd unless -P is used.
Use the alternate cache instead of the main one;
this is for syscalls like 'linkat' that need two fds. */
struct fdbase
fdbase1 (char const *name)
{
return fdbase_opendir (name, true, 0);
return fdbase_opendir (name, true, false, 0);
}
/* Return an fd open to NAME.
Do not escape from chdir_fd unless -P is used. */
int
open_searchdir (char const *name)
{
return fdbase_opendir (name, false, open_searchdir_how.flags).fd;
return fdbase_opendir (name, false, true, open_searchdir_how.flags).fd;
}
+1
View File
@@ -204,6 +204,7 @@ TESTSUITE_AT = \
onetop03.at\
onetop04.at\
onetop05.at\
onetop06.at\
opcomp01.at\
opcomp02.at\
opcomp03.at\
+39
View File
@@ -0,0 +1,39 @@
# Process this file with autom4te to create testsuite. -*- Autotest -*-
#
# Test suite for GNU tar.
# Copyright 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/>.
#
AT_SETUP([tar --one-top-level into nonexistent absolute dir])
AT_KEYWORDS([extract onetop onetop06])
AT_TAR_CHECK([
mkdir d f && genfile --file d/file || framework_failure_
tar -cf test.tar d || exit
tar -xf test.tar --one-top-level=$PWD/e/c || exit
diff d e/c/d || exit
tar -xf test.tar --one-top-level=$PWD/e/d || exit
diff d e/d/d || exit
tar -cf test1.tar e/c || exit
tar -xf test1.tar -C f --one-top-level=e/c || exit
diff e/c/d f/e/c/d || exit
tar -xf test1.tar -C f --one-top-level=$PWD/g || exit
diff e/c/d g/e/c/d || exit
])
AT_CLEANUP
+1
View File
@@ -543,6 +543,7 @@ m4_include([onetop02.at])
m4_include([onetop03.at])
m4_include([onetop04.at])
m4_include([onetop05.at])
m4_include([onetop06.at])
AT_BANNER([Star tests])
m4_include([star/gtarfail.at])