From b97bbb10103a911d418015b7ff41384af0419952 Mon Sep 17 00:00:00 2001
From: Paul Eggert <eggert@cs.ucla.edu>
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.

CVE: CVE-2026-5704
Upstream-Status: Backport [https://cgit.git.savannah.gnu.org/cgit/tar.git/commit/?id=b8d8a61b25588caca4efaf9bdd2e3f1a49da77e3]

Backport Changes:
- Replaced UNNAMED(typeflag) with MAYBE_UNUSED char typeflag in extract_dir()
  under src/extract.c, as the UNNAMED macro is not available in tar 1.35
  (introduced later in upstream commit 9280aa3807c1 "Prefer UNNAMED to
  MAYBE_UNUSED").
- THANKS file context difference: tar 1.35 has "Jurgen Botz" (ASCII),
  while upstream has "Jürgen Botz" (UTF-8 umlaut), changed in upstream
  commit 390950282d98 ("maint: fix some encodings and email addresses");
  the removed line in the diff reflects the tar 1.35 spelling.

(cherry picked from commit b8d8a61b25588caca4efaf9bdd2e3f1a49da77e3)
Signed-off-by: Himanshu Jadon <hjadon@cisco.com>
---
 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 aee0a924..03fa49d5 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		aagero@ifi.uio.no
 Adam Borowski		kilobyte@angband.pl
@@ -36,8 +37,8 @@ Andrew J. Schorr	schorr@ead.dsa.com
 Andrew Torda		torda@igc.chem.ethz.ch
 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 Jose Coutinho	ajc@di.uminho.pt
 Ariel Faigon		ariel@engr.sgi.com
 Arne Wichmann		aw@math.uni-sb.de
 Arnold Robbins		arnold@gnu.org
@@ -79,9 +80,9 @@ Cesar Romani		romani@ifm.uni-hamburg.de
 Chad Hurwitz		churritz@cts.com
 Chance Reschke		creschke@usra.edu
 Charles Fu		ccwf@klab.caltech.edu
-Charles McGarvey        chazmcgarvey@brokenzipper.com
 Charles Lopes		Charles.Lopes@infm.ulst.ac.uk
 Charles M. Hannum	mycroft@gnu.org
+Charles McGarvey        chazmcgarvey@brokenzipper.com
 Chip Salzenberg		tct!chip
 Chris Arthur		csa@gnu.org
 Chris F.M. Verberne	verberne@prl.philips.nl
@@ -93,9 +94,9 @@ Christian Callsen	Christian.Callsen@eng.sun.com
 Christian Kirsch	ck@held.mind.de
 Christian Laubscher	christian.laubscher@tiscalinet.ch
 Christian T. Dum	ctd@mpe-garching.mpg.de
-Christian von Roques	roques@pond.sub.org
-Christian Wetzel	wetzel@phoenix-pacs.de
 Christian Weisgerber	naddy@mips.inka.de
+Christian Wetzel	wetzel@phoenix-pacs.de
+Christian von Roques	roques@pond.sub.org
 Christoph Litauer	litauer@mailhost.uni-koblenz.de
 Christophe Colle	colle@krtkg1.rug.ac.be
 Christophe Kalt		Christophe.Kalt@kbcfp.com
@@ -117,8 +118,8 @@ Dan Bloch		dan@transarc.com
 Dan Drake               dan@dandrake.org
 Dan Reish		dreish@izzy.net
 Daniel Hagerty		hag@gnu.org
-Daniel Quinlan		quinlan@pathname.com
 Daniel Kahn Gillmor	dkg@fifthhorseman.net
+Daniel Quinlan		quinlan@pathname.com
 Daniel R. Guilderson	d.guilderson@ma30.bull.com
 Daniel S. Barclay	daniel@compass-da.com
 Daniel Trinkle		trinkle@cs.purdue.edu
@@ -198,6 +199,7 @@ Greg Hudson		ghudson@mit.edu
 Greg Maples		greg@clari.net
 Greg McGary		gkm@cstone.net
 Greg Schafer		gschafer@zip.com.au
+Guillermo de Angel	gdeangelg@gmail.com
 Göran Uddeborg		gvran@uddeborg.pp.se
 Gürkan Karaman		karaman@dssgmbh.de
 Hans Guerth		100664.3101@compuserve.com
@@ -222,6 +224,7 @@ Indra Singhal		indra@synoptics.com
 J. Dean Brock		brock@cs.unca.edu
 J.J. Bailey		jjb@jagware.bcc.com
 J.T. Conklin		jtc@cygnus.com
+James Antill            jantill@redhat.com
 James Crawford Ralston	qralston+@pitt.edu
 James E. Carpenter	jimc@zach1.tiac.net
 James H Caldwell Jr	caldwell@cs.fsu.edu
@@ -233,15 +236,15 @@ Jan Carlson		janc@sni.ca
 Jan Djarv		jan.djarv@mbox200.swipnet.se
 Janice Burton		r06a165@bcc25.kodak.com
 Janne Snabb		snabb@niksula.hut.fi
-Jason R. Mastaler	jason@webmaster.net
 Jason Armistead		Jason.Armistead@otis.com
+Jason R. Mastaler	jason@webmaster.net
 Jay Fenlason		hack@gnu.org
 Jean-Louis Martineau    martineau@zmanda.com
-Jean-Michel Soenen	soenen@lectra.fr
 Jean-Loup Gailly	jloup@chorus.fr
-Jeff Moskow		jeff@rtr.com
+Jean-Michel Soenen	soenen@lectra.fr
 Jean-Ph. Martin-Flatin	syj@ecmwf.int
 Jean-Pierre Demailly	Jean-Pierre.Demailly@ujf-grenoble.fr
+Jeff Moskow		jeff@rtr.com
 Jeff Prothero		jsp@betz.biostr.washington.edu
 Jeff Siegel		js@hornet.att.com
 Jeff Sorensen		sorenj@alumni.rpi.edu
@@ -249,7 +252,6 @@ Jeffrey Goldberg	J.Goldberg@cranfield.ac.uk
 Jeffrey Mark Siskind	Qobi@emba.uvm.edu
 Jeffrey W. Parker	jwpkr@mcs.com
 Jens Henrik Jensen	recjhl@mediator.uni-c.dk
-Jérémy Bobbio		lunar@debian.org
 Jim Blandy		jimb@totoro.cs.oberlin.edu
 Jim Clausing		jac@postbox.acs.ohio-state.edu
 Jim Farrell		jwf@platinum.com
@@ -283,13 +285,14 @@ Joutsiniemi Tommi Il	tj75064@cs.tut.fi
 Joy Kendall		jak8@world.std.com
 Judy Ricker		jricker@gdstech.grumman.com
 Juha Sarlin		juha@tds.kth.se
-Jurgen Botz		jbotz@orixa.mtholyoke.edu
 Jyh-Shyang Wang		erik@vsp.ee.nctu.edu.tw
+Jérémy Bobbio		lunar@debian.org
 Jörg Schilling		schilling@fokus.fraunhofer.de
-Jörg Weule		weule@cs.uni-duesseldorf.de
 Jörg Weilbier 		gnu@weilbier.net
+Jörg Weule		weule@cs.uni-duesseldorf.de
 Jörgen Hågg		Jorgen.Hagg@axis.se
 Jörgen Weigert		jw@suse.de
+Jürgen Botz		jbotz@orixa.mtholyoke.edu
 Jürgen Lüters		jlueters@t-online.de
 Jürgen Reiss		reiss@psychologie.uni-wuerzburg.de
 Kai Petzke		wpp@marie.physik.tu-berlin.de
@@ -311,7 +314,6 @@ Kimmy Posey		kimmyd@bnr.ca
 Koji Kishi		kis@rqa.sony.co.jp
 Konno Hiroharu		konno@pac.co.jp
 Kurt Jaeger		pi@lf.net
-James Antill            jantill@redhat.com
 Larry Creech		lcreech@lonestar.rcclub.org
 Larry Schwimmer		rosebud@cyclone.stanford.edu
 Lasse Collin 		lasse.collin@tukaani.org
@@ -394,7 +396,6 @@ Oswald P. Backus IV	backus@lks.csi.com
 Pascal Meheut		pascal@cnam.cnam.fr
 Patrick Fulconis	fulco@sig.uvsq.fr
 Patrick Timmons		timmons@electech.polymtl.ca
-Pavel Raiskup           praiskup@redhat.com
 Paul Eggert		eggert@twinsun.com
 Paul Kanz		paul@icx.com
 Paul Mitchell		P.Mitchell@surrey.ac.uk
@@ -402,6 +403,7 @@ Paul Nevai		pali+@osu.edu
 Paul Nordstrom		100067.3532@compuserve.com
 Paul O'Connor		oconnorp@ul.ie
 Paul Siddall		pauls@postman.essex.ac.uk
+Pavel Raiskup           praiskup@redhat.com
 Peder Chr. Norgaard	pcn@tbit.dk
 Pekka Janhunen		Pekka.Janhunen@fmi.fi
 Per Bojsen		pb@delta.dk
@@ -420,9 +422,9 @@ Piotr Rotter 		piotr.rotter@active24.pl
 R. Kent Dybvig		dyb@cadence.bloomington.in.us
 R. Scott Butler		butler@prism.es.dupont.com
 Rainer Orth		ro@TechFak.Uni-Bielefeld.DE
-Ralf Wildenhues 	Ralf.Wildenhues@gmx.de
 Ralf S. Engelschall	rse@engelschall.com
 Ralf Suckow		suckow@contrib.de
+Ralf Wildenhues 	Ralf.Wildenhues@gmx.de
 Ralph Corderoy		ralph@inputplus.co.uk
 Ralph Schleicher	rs@purple.ul.bawue.de
 Randy Bias		randyb@edge.edge.net
diff --git a/src/extract.c b/src/extract.c
index b384fed3..3bf0d77e 100644
--- a/src/extract.c
+++ b/src/extract.c
@@ -1037,7 +1037,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, MAYBE_UNUSED char typeflag)
 {
   int status;
   mode_t mode;
@@ -1060,8 +1060,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 (&current_stat_info.stat);
 
@@ -1266,10 +1264,7 @@ extract_file (char *file_name, char typeflag)
     {
       fd = sys_exec_command (file_name, 'f', &current_stat_info);
       if (fd < 0)
-	{
-	  skip_member ();
-	  return true;
-	}
+	return true;
     }
   else
     {
@@ -1289,7 +1284,6 @@ extract_file (char *file_name, char typeflag)
 	  int recover = maybe_recoverable (file_name, true, &interdir_made);
 	  if (recover != RECOVER_OK)
 	    {
-	      skip_member ();
 	      if (recover == RECOVER_SKIP)
 		return true;
 	      open_error (file_name);
@@ -1337,6 +1331,7 @@ extract_file (char *file_name, char typeflag)
       }
 
   skim_file (size, false);
+  current_stat_info.skipped = true;
 
   mv_end ();
 
@@ -1841,15 +1836,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 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 (&current_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 928779e1..c9623eb4 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;
@@ -1440,23 +1440,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
@@ -1464,18 +1447,17 @@ skim_member (bool must_copy)
 {
   if (!current_stat_info.skipped)
     {
-      bool is_dir = member_is_dir (&current_stat_info,
-				   current_header->header.typeflag);
       set_next_block_after (current_header);
 
       mv_begin_read (&current_stat_info);
 
       if (current_stat_info.is_sparse)
 	sparse_skim_file (&current_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 6cf726c0..baeb55bb 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -133,6 +133,7 @@ TESTSUITE_AT = \
  extrac23.at\
  extrac24.at\
  extrac25.at\
+ extrac32.at\
  filerem01.at\
  filerem02.at\
  grow.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 7106ee74..a58a20fd 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 f2229be1..58757005 100644
--- a/tests/testsuite.at
+++ b/tests/testsuite.at
@@ -349,6 +349,7 @@ m4_include([extrac22.at])
 m4_include([extrac23.at])
 m4_include([extrac24.at])
 m4_include([extrac25.at])
+m4_include([extrac32.at])
 
 m4_include([backup01.at])
 
-- 
2.44.1

