Hi,

GNU tar current master (9371d36c) can silently extract a member into
the wrong sibling directory when a failed parent-directory open leaves
a stale file descriptor in fdbase_cache.

A minimal archive containing only these regular members, with no
directory entries:

  a/one  ("first")
  b/two  ("second")

is enough to reproduce it. From an empty extraction directory, current
master prints both advertised names and exits 0, but creates:

  a/one  ("first")
  a/two  ("second")
  b/      (empty)

The first lookup caches directory "a" after tar creates it. The lookup
for missing directory "b" then copies "b" into the cache before
open_subdir fails, but retains the descriptor for "a" and its nonzero
length. After tar creates "b", the same-length retry treats the cache
as an exact match and returns the old descriptor.

This causes silent write redirection within the extraction root and
can overwrite an unintended file under the preceding sibling. I have
not observed or claimed an escape outside the extraction root.

The regression was introduced by e54e505a ("tar: open directories more
efficiently"). The attached patch closes an unowned old descriptor,
invalidates the cache entry while preserving errno, and adds an
Autotest regression covering all configured archive formats.

Validation:

* The new test fails on unmodified 9371d36c and passes after the patch.
* The same archive extracts correctly at pre-regression parent 941f62b2.
* An optimized full suite ran 221 tests: 220 passed; the sole multiv04
failure also occurs on unmodified 941f62b2 on this macOS host.
* The focused test and extraction suite pass under Clang ASan/UBSan.

Thanks,
Darren
From 586878a0893b0ba9971fc9ec3129b97efbc210d0 Mon Sep 17 00:00:00 2001
From: Darren Carreras <[email protected]>
Date: Wed, 5 Aug 2026 13:53:03 -0400
Subject: [PATCH] tar: invalidate directory cache after failed open

fdbase_opendir copies a requested directory name into the cache before
opening it.  If open_subdir fails, this leaves the new name associated
with the old file descriptor.  A retry after tar creates the missing
parent can then extract into the old directory.

Close an unowned old descriptor and invalidate the cache entry on
failure while preserving errno.  Add a regression test using
equal-length sibling directory names.

* src/misc.c (fdbase_opendir): Invalidate the cache after a failed
replacement open.
* tests/extrac35.at: New test.
* tests/Makefile.am (TESTSUITE_AT): Add it.
* tests/testsuite.at: Include it.
---
 src/misc.c         | 12 ++++++++++--
 tests/Makefile.am  |  1 +
 tests/extrac35.at  | 37 +++++++++++++++++++++++++++++++++++++
 tests/testsuite.at |  1 +
 4 files changed, 49 insertions(+), 2 deletions(-)
 create mode 100644 tests/extrac35.at

diff --git a/src/misc.c b/src/misc.c
index ad9d8335..6a4fc4d8 100644
--- a/src/misc.c
+++ b/src/misc.c
@@ -1377,12 +1377,20 @@ fdbase_opendir (char const *file_name, bool alternate, int child_oflags)
 		}
 	    }
 
+	  /* Remove any old directory info, and add new info if the new
+	     directory can be opened.  */
 	  int newfd = open_subdir (chdir_fd, c->subdir, child_oflags);
 	  if (newfd < 0)
-	    fd = BADFD == -1 ? newfd : BADFD;
+	    {
+	      int e = errno;
+	      if (0 < c->subdirlen && !chdirable (fd))
+		close (fd);
+	      c->subdirlen = 0;
+	      errno = e;
+	      fd = BADFD == -1 ? newfd : BADFD;
+	    }
 	  else
 	    {
-	      /* Remove any old directory info, and add new info.  */
 	      if (0 < c->subdirlen && !chdirable (fd))
 		close (fd);
 	      c->chdir_current = chdir_current;
diff --git a/tests/Makefile.am b/tests/Makefile.am
index 6583ba1b..b68fd83c 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -144,6 +144,7 @@ TESTSUITE_AT = \
  extrac32.at\
  extrac33.at\
  extrac34.at\
+ extrac35.at\
  filerem01.at\
  filerem02.at\
  filerem03.at\
diff --git a/tests/extrac35.at b/tests/extrac35.at
new file mode 100644
index 00000000..2a536afe
--- /dev/null
+++ b/tests/extrac35.at
@@ -0,0 +1,37 @@
+# Check failed parent lookup does not corrupt the directory cache. -*- 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/>.
+
+AT_SETUP([failed parent lookup and directory cache])
+AT_KEYWORDS([extract extrac35 chdir])
+
+AT_TAR_CHECK([
+mkdir -p src/a src/b dest
+printf 'one\n' > src/a/one
+printf 'two\n' > src/b/two
+
+tar -cf archive.tar -C src --no-recursion a/one b/two &&
+tar -xf archive.tar -C dest &&
+test -f dest/a/one &&
+test -f dest/b/two &&
+test ! -e dest/a/two &&
+cmp src/a/one dest/a/one &&
+cmp src/b/two dest/b/two
+])
+
+AT_CLEANUP
diff --git a/tests/testsuite.at b/tests/testsuite.at
index 9eb0d11c..29982cd1 100644
--- a/tests/testsuite.at
+++ b/tests/testsuite.at
@@ -361,6 +361,7 @@ m4_include([extrac31.at])
 m4_include([extrac32.at])
 m4_include([extrac33.at])
 m4_include([extrac34.at])
+m4_include([extrac35.at])
 
 m4_include([backup01.at])
 
-- 
2.50.1 (Apple Git-155)

Reply via email to