mirror of
https://git.savannah.gnu.org/git/tar.git
synced 2026-08-22 01:06:03 +00:00
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:
@@ -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
@@ -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.
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -204,6 +204,7 @@ TESTSUITE_AT = \
|
||||
onetop03.at\
|
||||
onetop04.at\
|
||||
onetop05.at\
|
||||
onetop06.at\
|
||||
opcomp01.at\
|
||||
opcomp02.at\
|
||||
opcomp03.at\
|
||||
|
||||
@@ -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
|
||||
@@ -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])
|
||||
|
||||
Reference in New Issue
Block a user