Thanks for reporting the bug. I installed the attached patch. Please
give it a try.
I am happy to coordinate on a disclosure timeline.
Thanks for that too, but it's already disclosed here:
https://lists.gnu.org/r/bug-tar/2026-03/msg00007.html
Please feel free to report the bug elsewhere. There should not be much
trouble for people who follow the advice in the tar manual, which says
"When extracting from an untrusted archive, it is therefore good
practice to create an empty directory and run tar in that directory."[1]
Of course not everybody follows advice in software manuals.
[1]: https://www.gnu.org/software/tar/manual/html_node/Integrity.html
From b8d8a61b25588caca4efaf9bdd2e3f1a49da77e3 Mon Sep 17 00:00:00 2001
From: Paul Eggert <[email protected]>
Date: Sun, 22 Mar 2026 12:19:40 -0700
Subject: [PATCH] Fix more -t/-x discrepancies
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Problem reported by Guillermo de Angel in:
https://lists.gnu.org/r/bug-tar/2026-03/msg00007.html
* THANKS: Add him, and sort.
* src/extract.c (extract_dir, extract_file):
* src/incremen.c (purge_directory):
Do not call skip_member, as the caller now does that, and does it
more reliably.
* src/extract.c (extract_file):
Mark file as skipped when we’ve read it.
(extract_archive): Always call skip_member after extracting,
as it suppresses the skip as needed.
* src/incremen.c (try_purge_directory): Remove; no longer
needed. Move internals to purge_directory.
* src/list.c (read_header): Do not treat LNKTYPE header as having
size zero, as it can be nonzero (e.g., ‘pax -o linkdata’).
Set info->skipped field according to how the header was read.
(member_is_dir): Remove; no longer needed.
(skim_member): Skip directory data too, unless it’s already been
skipped (i.e., read).
* tests/extrac32.at: New file.
* tests/Makefile.am (TESTSUITE_AT):
* tests/testsuite.at:
Add it.
* tests/skipdir.at (skip directory members):
Fix test to match the correct behavior.
This fixes a bug introduced in commit
b009124ffde415515081db844d7a104e1d1c6c58
dated 2025-05-12 17:17:21 +0300.
---
THANKS | 30 +++++++++++++++--------------
src/extract.c | 23 ++++++----------------
src/incremen.c | 22 +++++++--------------
src/list.c | 48 +++++++++++++++-------------------------------
tests/Makefile.am | 1 +
tests/extrac32.at | 47 +++++++++++++++++++++++++++++++++++++++++++++
tests/skipdir.at | 6 +-----
tests/testsuite.at | 1 +
8 files changed, 94 insertions(+), 84 deletions(-)
create mode 100644 tests/extrac32.at
diff --git a/THANKS b/THANKS
index 20afca38..f908c1b0 100644
--- a/THANKS
+++ b/THANKS
@@ -6,6 +6,7 @@ Many people further contributed to GNU tar by reporting problems,
suggesting various improvements or submitting actual code. Here is a
list of these people. Help me keep it complete and exempt of errors.
See various ChangeLogs for a detailed description of contributions.
+This listed is sorted via "LC_ALL=C sort".
Aage Robeck [email protected]
Adam Borowski [email protected]
@@ -36,8 +37,8 @@ Andrew J. Schorr [email protected]
Andrew Torda [email protected]
Andrey A. Chernov [email protected]
Andy Gay [email protected]
-Antonio Jose Coutinho [email protected]
Anthony G. Basile [email protected]
+Antonio Jose Coutinho [email protected]
Ariel Faigon [email protected]
Arne Wichmann [email protected]
Arnold Robbins [email protected]
@@ -79,9 +80,9 @@ Cesar Romani [email protected]
Chad Hurwitz [email protected]
Chance Reschke [email protected]
Charles Fu [email protected]
-Charles McGarvey [email protected]
Charles Lopes [email protected]
Charles M. Hannum [email protected]
+Charles McGarvey [email protected]
Chip Salzenberg tct!chip
Chris Arthur [email protected]
Chris F.M. Verberne [email protected]
@@ -93,9 +94,9 @@ Christian Callsen [email protected]
Christian Kirsch [email protected]
Christian Laubscher [email protected]
Christian T. Dum [email protected]
-Christian von Roques [email protected]
-Christian Wetzel [email protected]
Christian Weisgerber [email protected]
+Christian Wetzel [email protected]
+Christian von Roques [email protected]
Christoph Litauer [email protected]
Christophe Colle [email protected]
Christophe Kalt [email protected]
@@ -117,8 +118,8 @@ Dan Bloch [email protected]
Dan Drake [email protected]
Dan Reish [email protected]
Daniel Hagerty [email protected]
-Daniel Quinlan [email protected]
Daniel Kahn Gillmor [email protected]
+Daniel Quinlan [email protected]
Daniel R. Guilderson [email protected]
Daniel S. Barclay [email protected]
Daniel Trinkle [email protected]
@@ -198,6 +199,7 @@ Greg Hudson [email protected]
Greg Maples [email protected]
Greg McGary [email protected]
Greg Schafer [email protected]
+Guillermo de Angel [email protected]
Göran Uddeborg [email protected]
Gürkan Karaman [email protected]
Hans Guerth [email protected]
@@ -222,6 +224,7 @@ Indra Singhal [email protected]
J. Dean Brock [email protected]
J.J. Bailey [email protected]
J.T. Conklin [email protected]
+James Antill [email protected]
James Crawford Ralston [email protected]
James E. Carpenter [email protected]
James H Caldwell Jr [email protected]
@@ -233,15 +236,15 @@ Jan Carlson [email protected]
Jan Djarv [email protected]
Janice Burton [email protected]
Janne Snabb [email protected]
-Jason R. Mastaler [email protected]
Jason Armistead [email protected]
+Jason R. Mastaler [email protected]
Jay Fenlason [email protected]
Jean-Louis Martineau [email protected]
-Jean-Michel Soenen [email protected]
Jean-Loup Gailly [email protected]
-Jeff Moskow [email protected]
+Jean-Michel Soenen [email protected]
Jean-Ph. Martin-Flatin [email protected]
Jean-Pierre Demailly [email protected]
+Jeff Moskow [email protected]
Jeff Prothero [email protected]
Jeff Siegel [email protected]
Jeff Sorensen [email protected]
@@ -249,7 +252,6 @@ Jeffrey Goldberg [email protected]
Jeffrey Mark Siskind [email protected]
Jeffrey W. Parker [email protected]
Jens Henrik Jensen [email protected]
-Jérémy Bobbio [email protected]
Jim Blandy [email protected]
Jim Clausing [email protected]
Jim Farrell [email protected]
@@ -283,13 +285,14 @@ Joutsiniemi Tommi Il [email protected]
Joy Kendall [email protected]
Judy Ricker [email protected]
Juha Sarlin [email protected]
-Jürgen Botz [email protected]
Jyh-Shyang Wang [email protected]
+Jérémy Bobbio [email protected]
Jörg Schilling [email protected]
-Jörg Weule [email protected]
Jörg Weilbier [email protected]
+Jörg Weule [email protected]
Jörgen Hågg [email protected]
Jörgen Weigert [email protected]
+Jürgen Botz [email protected]
Jürgen Lüters [email protected]
Jürgen Reiss [email protected]
Kai Petzke [email protected]
@@ -312,7 +315,6 @@ Kirill Furman [email protected]
Koji Kishi [email protected]
Konno Hiroharu [email protected]
Kurt Jaeger [email protected]
-James Antill [email protected]
Larry Creech [email protected]
Larry Schwimmer [email protected]
Lasse Collin [email protected]
@@ -396,7 +398,6 @@ Oswald P. Backus IV [email protected]
Pascal Meheut [email protected]
Patrick Fulconis [email protected]
Patrick Timmons [email protected]
-Pavel Raiskup [email protected]
Paul Eggert [email protected]
Paul Kanz [email protected]
Paul Mitchell [email protected]
@@ -404,6 +405,7 @@ Paul Nevai [email protected]
Paul Nordstrom [email protected]
Paul O'Connor [email protected]
Paul Siddall [email protected]
+Pavel Raiskup [email protected]
Peder Chr. Norgaard [email protected]
Pekka Janhunen [email protected]
Per Bojsen [email protected]
@@ -422,9 +424,9 @@ Piotr Rotter [email protected]
R. Kent Dybvig [email protected]
R. Scott Butler [email protected]
Rainer Orth [email protected]
-Ralf Wildenhues [email protected]
Ralf S. Engelschall [email protected]
Ralf Suckow [email protected]
+Ralf Wildenhues [email protected]
Ralph Corderoy [email protected]
Ralph Schleicher [email protected]
Randy Bias [email protected]
diff --git a/src/extract.c b/src/extract.c
index ab83a650..f9623a45 100644
--- a/src/extract.c
+++ b/src/extract.c
@@ -1132,7 +1132,7 @@ safe_dir_mode (struct stat const *st)
/* Extractor functions for various member types */
static bool
-extract_dir (char *file_name, char typeflag)
+extract_dir (char *file_name, char UNNAMED (typeflag))
{
int status;
mode_t mode;
@@ -1157,8 +1157,6 @@ extract_dir (char *file_name, char 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);
@@ -1338,10 +1336,7 @@ extract_file (char *file_name, char typeflag)
{
fd = sys_exec_command (file_name, 'f', ¤t_stat_info);
if (fd < 0)
- {
- skip_member ();
- return true;
- }
+ return true;
}
else
{
@@ -1362,7 +1357,6 @@ extract_file (char *file_name, char typeflag)
= maybe_recoverable (file_name, true, &interdir_made);
if (recover != RECOVER_OK)
{
- skip_member ();
if (recover == RECOVER_SKIP)
return true;
open_error (file_name);
@@ -1409,6 +1403,7 @@ extract_file (char *file_name, char typeflag)
}
skim_file (size, false);
+ current_stat_info.skipped = true;
mv_end ();
@@ -1921,15 +1916,9 @@ extract_archive (void)
tar_extractor_t fun = prepare_to_extract (current_stat_info.file_name,
typeflag);
- if (fun)
- {
- if (fun (current_stat_info.file_name, typeflag))
- return;
- }
- else
- skip_member ();
-
- if (backup_option)
+ bool ok = fun && fun (current_stat_info.file_name, typeflag);
+ skip_member ();
+ if (!ok && backup_option)
undo_last_backup ();
}
diff --git a/src/incremen.c b/src/incremen.c
index b7b0d1cc..6be07193 100644
--- a/src/incremen.c
+++ b/src/incremen.c
@@ -1621,8 +1621,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;
@@ -1630,18 +1630,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, false);
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)
@@ -1661,7 +1661,7 @@ try_purge_directory (char const *directory_name)
quote (temp_stub));
free (temp_stub);
free (current_dir);
- return false;
+ return;
}
}
else if (*arc == 'R')
@@ -1695,7 +1695,7 @@ try_purge_directory (char const *directory_name)
free (current_dir);
/* FIXME: Make sure purge_directory(dst) will return
immediately */
- return false;
+ return;
}
}
}
@@ -1749,14 +1749,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 d541cf26..8e9caf5c 100644
--- a/src/list.c
+++ b/src/list.c
@@ -423,20 +423,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
@@ -493,11 +488,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;
@@ -511,6 +510,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;
@@ -1412,23 +1412,6 @@ 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
@@ -1436,18 +1419,17 @@ skim_member (bool must_copy)
{
if (!current_stat_info.skipped)
{
- bool is_dir = member_is_dir (¤t_stat_info,
- 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 (!is_dir)
+ else
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 ae8bd3ff..0625b021 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -141,6 +141,7 @@ TESTSUITE_AT = \
extrac29.at\
extrac30.at\
extrac31.at\
+ extrac32.at\
filerem01.at\
filerem02.at\
filerem03.at\
diff --git a/tests/extrac32.at b/tests/extrac32.at
new file mode 100644
index 00000000..3829a483
--- /dev/null
+++ b/tests/extrac32.at
@@ -0,0 +1,47 @@
+# 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 <http://www.gnu.org/licenses/>.
+
+# 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
+])
+cp archive.tar /tmp
+AT_CHECK([tar tf archive.tar],
+[0],
+[carrier_entry
+marker.txt
+])
+AT_CHECK([tar xvf archive.tar],
+[0],
+[carrier_entry
+marker.txt
+])
+AT_CLEANUP
diff --git a/tests/skipdir.at b/tests/skipdir.at
index 0bc38e4f..79119ddf 100644
--- a/tests/skipdir.at
+++ b/tests/skipdir.at
@@ -42,15 +42,11 @@ 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/
-])
+[0])
AT_CLEANUP
diff --git a/tests/testsuite.at b/tests/testsuite.at
index 63458eae..5979512a 100644
--- a/tests/testsuite.at
+++ b/tests/testsuite.at
@@ -358,6 +358,7 @@ m4_include([extrac28.at])
m4_include([extrac29.at])
m4_include([extrac30.at])
m4_include([extrac31.at])
+m4_include([extrac32.at])
m4_include([backup01.at])
--
2.51.0