diff --git a/tar-1.35-Avoid-acl_-prefix-for-functions.patch b/tar-1.35-Avoid-acl_-prefix-for-functions.patch new file mode 100644 index 0000000..98f6208 --- /dev/null +++ b/tar-1.35-Avoid-acl_-prefix-for-functions.patch @@ -0,0 +1,99 @@ +From f1ecf60ea59c1503e63e887e455756b68db04806 Mon Sep 17 00:00:00 2001 +From: Pavel Cahyna +Date: Wed, 24 Jun 2026 18:45:30 +0200 +Subject: [PATCH] Avoid acl_ prefix for functions + +The acl.h header from libacl uses acl_ prefix for its functions. Avoid +defining functions with the same name in order to protect its namespace. +--- + src/xattrs.c | 30 +++++++++++++++--------------- + 1 file changed, 15 insertions(+), 15 deletions(-) + +diff --git a/src/xattrs.c b/src/xattrs.c +index 4f5872c1..f98e8e0f 100644 +--- a/src/xattrs.c ++++ b/src/xattrs.c +@@ -133,13 +133,13 @@ static struct + #ifdef HAVE_POSIX_ACLS + + /* acl-at wrappers, TODO: move to gnulib in future? */ +-static acl_t acl_get_file_at (int, const char *, acl_type_t); +-static int acl_set_file_at (int, const char *, acl_type_t, acl_t); ++static acl_t tar_acl_get_file_at (int, const char *, acl_type_t); ++static int tar_acl_set_file_at (int, const char *, acl_type_t, acl_t); + static int file_has_acl_at (int, char const *, struct stat const *); +-static int acl_delete_def_file_at (int, char const *); ++static int tar_acl_delete_def_file_at (int, char const *); + +-/* acl_get_file_at */ +-#define AT_FUNC_NAME acl_get_file_at ++/* tar_acl_get_file_at */ ++#define AT_FUNC_NAME tar_acl_get_file_at + #define AT_FUNC_RESULT acl_t + #define AT_FUNC_FAIL (acl_t)NULL + #define AT_FUNC_F1 acl_get_file +@@ -153,8 +153,8 @@ static int acl_delete_def_file_at (int, char const *); + #undef AT_FUNC_POST_FILE_PARAM_DECLS + #undef AT_FUNC_POST_FILE_ARGS + +-/* acl_set_file_at */ +-#define AT_FUNC_NAME acl_set_file_at ++/* tar_acl_set_file_at */ ++#define AT_FUNC_NAME tar_acl_set_file_at + #define AT_FUNC_F1 acl_set_file + #define AT_FUNC_POST_FILE_PARAM_DECLS , acl_type_t type, acl_t acl + #define AT_FUNC_POST_FILE_ARGS , type, acl +@@ -164,8 +164,8 @@ static int acl_delete_def_file_at (int, char const *); + #undef AT_FUNC_POST_FILE_PARAM_DECLS + #undef AT_FUNC_POST_FILE_ARGS + +-/* acl_delete_def_file_at */ +-#define AT_FUNC_NAME acl_delete_def_file_at ++/* tar_acl_delete_def_file_at */ ++#define AT_FUNC_NAME tar_acl_delete_def_file_at + #define AT_FUNC_F1 acl_delete_def_file + #define AT_FUNC_POST_FILE_PARAM_DECLS + #define AT_FUNC_POST_FILE_ARGS +@@ -294,10 +294,10 @@ xattrs__acls_set (struct tar_stat_info const *st, + FILE_NAME may already have inherited default acls from parent + directory; clean them up. */ + struct fdbase f1 = fdbase (file_name); +- if (f1.fd == BADFD || acl_delete_def_file_at (f1.fd, f1.base)) ++ if (f1.fd == BADFD || tar_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 " ++ _("tar_acl_delete_def_file_at: Cannot drop default POSIX ACLs " + "for file '%s'"), + file_name)); + return; +@@ -311,11 +311,11 @@ xattrs__acls_set (struct tar_stat_info const *st, + } + + struct fdbase f = fdbase (file_name); +- if (f.fd == BADFD || acl_set_file_at (f.fd, f.base, type, acl) == -1) ++ if (f.fd == BADFD || tar_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, +- _ ("acl_set_file_at: Cannot set POSIX ACLs for file '%s'"), ++ _ ("tar_acl_set_file_at: Cannot set POSIX ACLs for file '%s'"), + file_name)); + + acl_free (acl); +@@ -351,10 +351,10 @@ acls_get_text (int parentfd, const char *file_name, acl_type_t type, + char *val = NULL; + acl_t acl; + +- if (!(acl = acl_get_file_at (parentfd, file_name, type))) ++ if (!(acl = tar_acl_get_file_at (parentfd, file_name, type))) + { + if (errno != ENOTSUP) +- call_arg_warn ("acl_get_file_at", file_name); ++ call_arg_warn ("tar_acl_get_file_at", file_name); + return; + } + +-- +2.47.3 + diff --git a/tar-1.35-CVE-2026-18477.patch b/tar-1.35-CVE-2026-18477.patch new file mode 100644 index 0000000..32d2ea8 --- /dev/null +++ b/tar-1.35-CVE-2026-18477.patch @@ -0,0 +1,445 @@ +diff --git a/NEWS b/NEWS +index 45fedbf5..f4621d7f 100644 +--- a/NEWS ++++ b/NEWS +@@ -13,6 +13,10 @@ version 1.35.90 (git) + ** When extracting, tar no longer follows symbolic links to targets + outside the working directory. + ++** When extracting from an incremental dump, tar now strips leading '/' ++ from names of temporary directories specified by 'X' entries, ++ unless --absolute-names (-P) is used. ++ + ** tar no longer fails merely if an extraction directory is unreadable + on Linux kernels. + +diff --git a/THANKS b/THANKS +index b9e4ce54..f12d98c7 100644 +--- a/THANKS ++++ b/THANKS +@@ -330,6 +330,7 @@ Manuel Munier Manuel.Munier@loria.fr + Marc Boucher marc@cam.org + Marc Ewing marc@redhat.com + Marcin Matuszewski marcin@frodo.nask.org.pl ++Marcin Wyczechowski mwyczechowski@afine.com + Marcus Daniels marcus@sysc.pdx.edu + Mark Bynum bynum@cennas.nhmfl.gov + Mark Clements mpc@mbsmm.com +@@ -367,6 +368,7 @@ Michael Schmidt michael@muc.de + Michael Schwingen m.schwingen@stochastik.rwth-aachen.de + Michael Smolsky fnsiguc@astro.weizmann.ac.il + Michal Žejdl zejdl@suas.cz ++Michał Majchrowicz mmajchrowicz@afine.com + Mike Muuss mike@brl.mil + Mike Nolan nolan@lpl.arizona.edu + Mike Rogers mike@demon.net +diff --git a/lib/Makefile.am b/lib/Makefile.am +index 38645b2c..45152c49 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 00000000..e35b9bba +--- /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 . ++ ++ Written by Paul Eggert. */ ++ ++#include ++ ++#include "mkdtempat.h" ++ ++#include ++ ++#include ++#include ++ ++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 00000000..03a18dc1 +--- /dev/null ++++ b/lib/mkdtempat.h +@@ -0,0 +1 @@ ++char *mkdtempat (int, char *); +diff --git a/src/incremen.c b/src/incremen.c +index 194d5cb1..a808e1c4 100644 +--- a/src/incremen.c ++++ b/src/incremen.c +@@ -19,6 +19,7 @@ + + #include + #include ++#include + #include + #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 b9ed8270..6031d7a1 100644 +--- a/tests/Makefile.am ++++ b/tests/Makefile.am +@@ -209,6 +209,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 00000000..5452fb4f +--- /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 . ++ ++# 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 00000000..8346ff0a +--- /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 . ++ ++# 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 0cc7adda..e4edaba5 100644 +--- a/tests/testsuite.at ++++ b/tests/testsuite.at +@@ -401,6 +401,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-1.35-CVE-2026-5704.patch b/tar-1.35-CVE-2026-5704.patch new file mode 100644 index 0000000..e59820a --- /dev/null +++ b/tar-1.35-CVE-2026-5704.patch @@ -0,0 +1,497 @@ +diff --git a/NEWS b/NEWS +index 5eadfe9d..ffb4f54f 100644 +--- a/NEWS ++++ b/NEWS +@@ -20,6 +20,10 @@ version 1.35.90 (git) + not even temporarily. This matches the documentation better and avoids + some permissions glitches. + ++** tar no longer behaves erratically when reading nonzero size fields ++ in archive headers representing special files, fifos, and symlinks. ++ Although these size fields are typically zero, POSIX allows some to ++ be nonzero, and in practice they do not count data blocks. + + version 1.35 - Sergey Poznyakoff, 2023-07-18 + +diff --git a/THANKS b/THANKS +index aee0a924..b9e4ce54 100644 +--- a/THANKS ++++ b/THANKS +@@ -38,6 +38,7 @@ Andrey A. Chernov ache@astral.msk.su + Andy Gay andy@rdl.co.uk + Antonio Jose Coutinho ajc@di.uminho.pt + Anthony G. Basile blueness@gentoo.org ++Antonio Teixeira antonio.teixeira@suse.com + Ariel Faigon ariel@engr.sgi.com + Arne Wichmann aw@math.uni-sb.de + Arnold Robbins arnold@gnu.org +diff --git a/src/extract.c b/src/extract.c +index cfaf5daf..4685f83c 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); + +@@ -1266,10 +1264,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 + { +@@ -1289,7 +1284,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); +@@ -1337,6 +1331,7 @@ extract_file (char *file_name, int typeflag) + } + + skim_file (size, false); ++ current_stat_info.skipped = true; + + mv_end (); + +@@ -1869,15 +1864,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 7bcfdb93..194d5cb1 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 e9a68159..7d7cde5a 100644 +--- a/src/list.c ++++ b/src/list.c +@@ -437,20 +437,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 +@@ -513,11 +508,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; +@@ -531,6 +530,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; +@@ -547,6 +547,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) +@@ -1440,6 +1456,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; ++ } ++} ++ + /* Skip the current member in the archive. + If MUST_COPY, always copy instead of skipping. */ + void +@@ -1447,17 +1481,18 @@ skim_member (bool must_copy) + { + if (!current_stat_info.skipped) + { +- char save_typeflag = current_header->header.typeflag; + set_next_block_after (current_header); + + mv_begin_read (¤t_stat_info); + + if (current_stat_info.is_sparse) + sparse_skim_file (¤t_stat_info, must_copy); +- else if (save_typeflag != DIRTYPE) ++ else if (!member_is_dir (¤t_stat_info, ++ current_header->header.typeflag)) + skim_file (current_stat_info.stat.st_size, must_copy); + + mv_end (); ++ current_stat_info.skipped = true; + } + } + +diff --git a/tests/Makefile.am b/tests/Makefile.am +index 311a4315..b9ed8270 100644 +--- a/tests/Makefile.am ++++ b/tests/Makefile.am +@@ -128,6 +128,8 @@ TESTSUITE_AT = \ + extrac27.at\ + extrac30.at\ + extrac31.at\ ++ extrac32.at\ ++ extrac34.at\ + filerem01.at\ + filerem02.at\ + dirrem01.at\ +@@ -235,6 +237,7 @@ TESTSUITE_AT = \ + shortupd.at\ + shortrec.at\ + sigpipe.at\ ++ skipdir.at\ + sparse01.at\ + sparse02.at\ + sparse03.at\ +diff --git a/tests/extrac32.at b/tests/extrac32.at +new file mode 100644 +index 00000000..af40916d +--- /dev/null ++++ b/tests/extrac32.at +@@ -0,0 +1,48 @@ ++# Check for file injection bug with symlinks. -*- Autotest -*- ++ ++# 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 . ++ ++# Thanks to Guillermo de Angel for the bug report and test cases; see: ++# https://lists.gnu.org/r/bug-tar/2026-03/msg00007.html ++ ++AT_SETUP([skip file injection]) ++AT_KEYWORDS([injection]) ++AT_DATA([archive.in], ++[/Td6WFoAAATm1rRGBMDbAYAcIQEcAAAAAAAAACYr+9LgDf8A010AMZhKvfVdtHe4Rxjj7M03ek97 ++UgeKfJ0ORqYg0XDFntWxdTH4PYrTOo9CoqBrnTM2NcwFBrRVr7aFwdd56vddyAw2QGDjxgNexDU3 ++ImTi/+z8ZOLMi/+AybdEpd5aA/M9Maa+8tQ84bySzSAwrmxMWJJ6W9IKvsqfiRa3TrD51v44PZU/ ++KLVKpocS56n/O3g+b+hiZwaysR0eLO+tiU8FB/e3PEq3vTtDFVi/YfZMieBWSzomSX9eF13K1yPY ++UuWgp7VokXqduL0YGNVV40MTPG9oAAAApD6mpajengIAAfcBgBwAAOM4xw6xxGf7AgAAAAAEWVo= ++]) ++AT_CHECK([base64 --help >/dev/null 2>&1 || AT_SKIP_TEST ++xz --help >/dev/null 2>&1 || AT_SKIP_TEST ++base64 -d < archive.in | xz -c -d > archive.tar ++]) ++AT_CHECK([tar tf archive.tar], ++[0], ++[carrier_entry ++injected.txt ++marker.txt ++]) ++AT_CHECK([tar xvf archive.tar], ++[0], ++[carrier_entry ++injected.txt ++marker.txt ++]) ++AT_CLEANUP +diff --git a/tests/extrac34.at b/tests/extrac34.at +new file mode 100644 +index 00000000..1cedb463 +--- /dev/null ++++ b/tests/extrac34.at +@@ -0,0 +1,40 @@ ++# Check hard link with nonzero size field in tarball. -*- Autotest -*- ++ ++# 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 . ++ ++# Check extracting from a nonconforming tarball that has ++# a hard link with nonzero size. ++ ++AT_SETUP([hard link with a nonzero size field]) ++AT_KEYWORDS([extract extrac34 hard link]) ++AT_DATA([archive.in], ++[/Td6WFoAAATm1rRGBMBwgFAhARwAAAAAAAAAAAbKrCjgJ/8AaF0AMIAzUBhoiawFdYeRen4lxlj0 ++QWRLpUM+28ArRsXVY5bGv4H5kijsqiJ4Z9YIVhZd01+IppF+AkltS60aB8fuUW35Tp/3XzUx9Mq2 ++4ypJFzvcgHXsSIvc9L+mmTDuHzhvJQ/oe7ya8QAASbOfD7CgZcIAAYwBgFAAAEWxohaxxGf7AgAA ++AAAEWVo= ++]) ++AT_CHECK([base64 --help >/dev/null 2>&1 || AT_SKIP_TEST ++xz --help >/dev/null 2>&1 || AT_SKIP_TEST ++base64 -d < archive.in | xz -c -d > archive.tar ++]) ++AT_CHECK([mkdir dir ++]) ++AT_CHECK([tar -C dir -xf archive.tar || exit 1 ++cmp dir/a dir/b ++]) ++AT_CLEANUP +diff --git a/tests/skipdir.at b/tests/skipdir.at +new file mode 100644 +index 00000000..0bc38e4f +--- /dev/null ++++ b/tests/skipdir.at +@@ -0,0 +1,56 @@ ++# Process this file with autom4te to create testsuite. -*- Autotest -*- ++ ++# Test suite for GNU tar. ++# Copyright 2025-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 . ++ ++# Description: determining member type when listing and extracting ++# should follow the same principles. ++# ++# Until version 1.35 the same archive member could have been processed ++# as a directory when extracting and as a regular file when being ++# skipped during listing. ++# ++# References: https://savannah.gnu.org/patch/index.php?10100 ++ ++AT_SETUP([skip directory members]) ++AT_KEYWORDS([skipdir]) ++AT_DATA([archive.in], ++[/Td6WFoAAATm1rRGAgAhARwAAAAQz1jM4Cf/AG1dADedyh4ubnxHHIi7Cen6orusgKqY3paKeQwp ++3//HS9EIT7Hm+MsndXfRntXVt8mu8oDpLOfC+AB9VldyCtp2jqOfTwa455qfGAcONPn6WWDgsaAh ++O2Y6ptXuaF/vdaNkub7SkOBME8jHYITT5QAAAAAAHtdcflb5Zw8AAYkBgFAAAPYgb0axxGf7AgAA ++AAAEWVo= ++]) ++AT_CHECK([base64 --help >/dev/null 2>&1 || AT_SKIP_TEST ++xz --help >/dev/null 2>&1 || AT_SKIP_TEST ++base64 -d < archive.in | xz -c -d > archive.tar ++]) ++AT_CHECK([tar tf archive.tar], ++[0], ++[owo1/ ++owo2/ ++]) ++AT_CHECK([tar vxf archive.tar], ++[0], ++[owo1/ ++owo2/ ++]) ++AT_CHECK([tar -xvf archive.tar --exclude owo1], ++[0], ++[owo2/ ++]) ++AT_CLEANUP +diff --git a/tests/testsuite.at b/tests/testsuite.at +index fd8b8b59..0cc7adda 100644 +--- a/tests/testsuite.at ++++ b/tests/testsuite.at +@@ -352,6 +352,8 @@ m4_include([extrac25.at]) + m4_include([extrac27.at]) + m4_include([extrac30.at]) + m4_include([extrac31.at]) ++m4_include([extrac32.at]) ++m4_include([extrac34.at]) + + m4_include([backup01.at]) + +@@ -475,6 +477,7 @@ m4_include([shortupd.at]) + m4_include([grow.at]) + m4_include([sigpipe.at]) + m4_include([comperr.at]) ++m4_include([skipdir.at]) + + AT_BANNER([Removing files after archiving]) + m4_include([remfiles01.at]) diff --git a/tar-1.35-fix-absolute-one-top-level.patch b/tar-1.35-fix-absolute-one-top-level.patch new file mode 100644 index 0000000..da21a92 --- /dev/null +++ b/tar-1.35-fix-absolute-one-top-level.patch @@ -0,0 +1,793 @@ +diff --git a/NEWS b/NEWS +index ffb4f54f..45fedbf5 100644 +--- a/NEWS ++++ b/NEWS +@@ -1,12 +1,12 @@ +-GNU tar NEWS - User visible changes. 2026-01-30 ++GNU tar NEWS - User visible changes. 2026-07-22 + Please send GNU tar bug reports to + + version 1.35.90 (git) + + * Changes to behavior + +-** --one-top-level=DIR now requires DIR to be relative. +- Previously this restriction was alluded to in the manual but not enforced. ++** The --one-top-level option now requires either -x or -d mode. ++ Previously the behavior was unspecified in other operation modes. + + * Bug fixes + +diff --git a/doc/tar.texi b/doc/tar.texi +index 9b04b94b..91302f05 100644 +--- a/doc/tar.texi ++++ b/doc/tar.texi +@@ -3271,14 +3271,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 +@@ -3286,6 +3286,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 ac0e7eef..fb732f0b 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 51c3f213..582ca4c6 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 4685f83c..a31f560b 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) + { +@@ -1844,7 +1844,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. */ +@@ -1864,8 +1864,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 (); +@@ -1878,7 +1905,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 7d7cde5a..068ca5fb 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. */ +@@ -1159,9 +1178,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) + { +@@ -1354,6 +1373,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 575d13d9..f6c9a819 100644 +--- a/src/misc.c ++++ b/src/misc.c +@@ -23,6 +23,7 @@ + #include + #include + #include ++#include + + #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 403192be..56cee63c 100644 +--- a/src/names.c ++++ b/src/names.c +@@ -867,6 +867,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 (); + } + +@@ -1119,7 +1120,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; + } +@@ -1182,7 +1184,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) + { +@@ -1211,7 +1213,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); +@@ -1326,7 +1328,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; +@@ -1348,7 +1350,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); + } +@@ -1778,7 +1780,7 @@ collect_and_sort_names (void) + if (name->found_count || name->directory) + continue; + +- chdir_do (name->change_dir); ++ chdir_do (name->change_dir, false); + + if (name->name[0] == 0) + continue; +@@ -1924,7 +1926,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 9a5fbd0b..56c9ed2a 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 33d2ccd1..e199bab3 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 2bc38207..24167778 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.spec b/tar.spec index 8bb2870..0d67e28 100644 --- a/tar.spec +++ b/tar.spec @@ -10,7 +10,7 @@ Summary: GNU file archiving program Name: tar Epoch: 2 Version: 1.35 -Release: 11%{?dist} +Release: 13%{?dist} License: GPL-3.0-or-later URL: https://www.gnu.org/software/tar/ @@ -59,6 +59,32 @@ Patch22: tar-1.35-no-overwrite-dir-no-overwrite-even-temporarily.patch # 45b6e6898d1f931bfca41d961289bd6ac33238e5 Patch23: tar-1.35-CVE-2025-45582.patch Patch24: tar-1.35-tar-one-top-level-DIR-must-be-relative.patch +# Source: https://cgit.git.savannah.gnu.org/cgit/tar.git/diff/?id=08c3fc2e9337094aff01a511170fd35fdb8f1ee3 +Patch25: tar-1.35-Avoid-acl_-prefix-for-functions.patch +#Upstream commits +# b009124ffde415515081db844d7a104e1d1c6c58 +# b8d8a61b25588caca4efaf9bdd2e3f1a49da77e3 +# 67981bbb1587803bb1e029393d2228492cef8c4f +# 19a3a73e8c48bd3c59cbea9b5ed6780fc6836c6d +Patch26: tar-1.35-CVE-2026-5704.patch +#Upstream commits +# bfc33463942060316f70f275471ed202b0076972 +# b4fc9ca13617411c5db57286a3ff534bd40acdc1 +# 67c8dff6968aae58fcdfb05268eb1b4c07308f1a +# 55e8233438b3c13294109df502a9b220a9a3f4f5 +# 0470c109c08f466d8332ba3326070554b4d81aa1 +# 1b91f5f66f8e6c490eef0fdee50f652cfe155844 +# 325b899214ac13519153318e66c889a31c15342d +# 1980e032afe60c5fe0e5df573457885ece4e69d5 +# e335e2c8b102f83b63ffd373f0b88401da3ebe6c +# part of 941f62b2 +# Also "by the way" fixes CVE-2026-18508. +Patch27: tar-1.35-fix-absolute-one-top-level.patch +#Upstream commits +# 0714d2f082104005a1c70ee6ec4175194943ea88 +# d479b2cc9160d9c2fb61afbc9ee70c2faadf80db +# b17665b2c0548c77b6cd8d2d5b61e4c4fcc4f770 +Patch28: tar-1.35-CVE-2026-18477.patch BuildRequires: autoconf BuildRequires: automake @@ -157,6 +183,20 @@ make check || ( %changelog +* Mon Aug 24 2026 Pavel Cahyna - 2:1.35-13 +- Backport upstream patches for CVE-2026-18477, fixes a bug + where incremental restore with cyclic renames between backups + may create a temporary directory at an archive-controlled path + outside the extraction tree. + The fix for CVE-2025-45582 already prevents exploiting + this problem, so it is more a correctness and hardening change. + +* Fri Aug 21 2026 Pavel Cahyna - 2:1.35-12 +- Backport upstream fix for CVE-2026-5704 (file injection hidden from -t) +- Fix --one-top-level with absolute path (broken by the CVE-2025-45582 fix) + Also fixes CVE-2026-18508 (escape from --one-top-level via hardlinks). +- Upstream fix for build with libacl 2.4.0 + * Mon Feb 23 2026 Pavel Cahyna - 2:1.35-11 - Add upstream patch to improve error message for --one-top-level with absolute path (unsupported as a result of CVE-2025-45582 fix)