From 376d663340a9dc91c91a5849e5713f07571c1628 Mon Sep 17 00:00:00 2001
From: Sergey Poznyakoff <gray@gnu.org>
Date: Thu, 27 Apr 2023 15:14:23 +0300
Subject: [PATCH] Fix 45b0ee2b407913c533f7ded8d6f8cbeec16ff6ca.

The commit in question brought in more problems than solutions.  To
properly fix the issue, use symlink placeholders, modelled after
delayed symlinks in tar.

* src/copyin.c (symlink_placeholder)
(replace_symlink_placeholders): New functions.
(copyin_link): Create symlink placeholder if --no-absolute-filenames
was given.
(process_copy_in): Replace placeholders after extraction.
* tests/CVE-2015-1197.at: Update. Don't use /tmp.
---
 src/copyin.c           | 172 +++++++++++++++++++++++++++++++++++------
 tests/CVE-2015-1197.at |  40 +++++++++++++++++++++++++++++++++++
 2 files changed, 192 insertions(+), 20 deletions(-)

diff --git a/src/copyin.c b/src/copyin.c
index 60cef9d..5ed2db6 100644
--- a/src/copyin.c
+++ b/src/copyin.c
@@ -30,6 +30,7 @@
 #ifndef	FNM_PATHNAME
 # include <fnmatch.h>
 #endif
+#include <hash.h>
 
 #ifndef HAVE_LCHOWN
 # define lchown(f,u,g) 0
@@ -620,6 +621,136 @@ copyin_device (struct cpio_file_stat* file_hdr)
 		    file_hdr->c_mtime);
 }
 
+struct delayed_link
+  {
+    /* The device and inode number of the placeholder. */
+    dev_t dev;
+    ino_t ino;
+
+    /* The desired link metadata. */
+    mode_t mode;
+    uid_t uid;
+    gid_t gid;
+    time_t mtime;
+
+    /* Link source and target names. */
+    char *source;
+    char target[1];
+  };
+
+static Hash_table *delayed_link_table;
+
+static size_t
+dl_hash (void const *entry, size_t table_size)
+{
+  struct delayed_link const *dl = entry;
+  uintmax_t n = dl->dev;
+  int nshift = (sizeof (n) - sizeof (dl->dev)) * CHAR_BIT;
+  if (0 < nshift)
+    n <<= nshift;
+  n ^= dl->ino;
+  return n % table_size;
+}
+
+static bool
+dl_compare (void const *a, void const *b)
+{
+  struct delayed_link const *da = a, *db = b;
+  return (da->dev == db->dev) & (da->ino == db->ino);
+}
+
+static int
+symlink_placeholder (char *oldpath, char *newpath, struct cpio_file_stat *file_stat)
+{
+  int fd = open (newpath, O_WRONLY | O_CREAT | O_EXCL, 0);
+  struct stat st;
+  struct delayed_link *p;
+  size_t newlen = strlen (newpath);
+  
+  if (fd < 0)
+    {
+      open_error (newpath);
+      return -1;
+    }
+  
+  if (fstat (fd, &st) != 0)
+    {
+      stat_error (newpath);
+      close (fd);
+      return -1;
+    }
+
+  close (fd);
+
+  p = xmalloc (sizeof (*p) + strlen (oldpath) + newlen + 1);
+  p->dev = st.st_dev;
+  p->ino = st.st_ino;
+
+  p->mode = file_stat->c_mode;
+  p->uid = file_stat->c_uid;
+  p->gid = file_stat->c_gid;
+  p->mtime = file_stat->c_mtime;
+
+  strcpy (p->target, newpath);
+  p->source = p->target + newlen + 1;
+  strcpy (p->source, oldpath);
+
+  if (!((delayed_link_table
+	 || (delayed_link_table = hash_initialize (0, 0, dl_hash,
+						   dl_compare, free)))
+	&& hash_insert (delayed_link_table, p)))
+    xalloc_die ();
+
+  return 0;
+}
+
+static void
+replace_symlink_placeholders (void)
+{
+  struct delayed_link *dl;
+
+  if (!delayed_link_table)
+    return;
+  for (dl = hash_get_first (delayed_link_table);
+       dl;
+       dl = hash_get_next (delayed_link_table, dl))
+    {
+      struct stat st;
+      
+      /* Make sure the placeholder file is still there.  If not,
+	 don't create a link, as the placeholder was probably
+	 removed by a later extraction.  */
+      if (lstat (dl->target, &st) == 0
+	  && st.st_dev == dl->dev
+	  && st.st_ino == dl->ino)
+	{
+	  if (unlink (dl->target))
+	    unlink_error (dl->target);
+	  else
+	    {
+	      int res = UMASKED_SYMLINK (dl->source, dl->target, dl->mode);
+	      if (res < 0 && create_dir_flag)
+		{
+		  create_all_directories (dl->target);
+		  res = UMASKED_SYMLINK (dl->source, dl->target, dl->mode);
+		}
+	      if (res < 0)
+		symlink_error (dl->source, dl->target);
+	      else if (!no_chown_flag)
+		{
+		  uid_t uid = set_owner_flag ? set_owner : dl->uid;
+		  gid_t gid = set_group_flag ? set_group : dl->gid;
+		  if (lchown (dl->target, uid, gid) < 0 && errno != EPERM)
+		    chown_error_details (dl->target, uid, gid);
+		}
+	    }
+	}
+    }
+  
+  hash_free (delayed_link_table);
+  delayed_link_table = NULL;
+}
+
 static void
 copyin_link (struct cpio_file_stat *file_hdr, int in_file_des)
 {
@@ -645,28 +776,26 @@ copyin_link (struct cpio_file_stat *file_hdr, int in_file_des)
       link_name = xstrdup (file_hdr->c_tar_linkname);
     }
 
-  res = UMASKED_SYMLINK (link_name, file_hdr->c_name,
-			 file_hdr->c_mode);
-  if (res < 0 && create_dir_flag)
+  if (no_abs_paths_flag)
+    symlink_placeholder (link_name, file_hdr->c_name, file_hdr);
+  else
     {
-      create_all_directories (file_hdr->c_name);
       res = UMASKED_SYMLINK (link_name, file_hdr->c_name,
-			     file_hdr->c_mode);
-    }
-  if (res < 0)
-    {
-      error (0, errno, _("%s: Cannot symlink to %s"),
-	     quotearg_colon (link_name), quote_n (1, file_hdr->c_name));
-      free (link_name);
-      return;
-    }
-  if (!no_chown_flag)
-    {
-      uid_t uid = set_owner_flag ? set_owner : file_hdr->c_uid;
-      gid_t gid = set_group_flag ? set_group : file_hdr->c_gid;
-      if ((lchown (file_hdr->c_name, uid, gid) < 0)
-  	  && errno != EPERM)
-	chown_error_details (file_hdr->c_name, uid, gid);
+			    file_hdr->c_mode);
+      if (res < 0 && create_dir_flag)
+	{
+	  create_all_directories (file_hdr->c_name);
+	  res = UMASKED_SYMLINK (link_name, file_hdr->c_name, file_hdr->c_mode);
+	}
+      if (res < 0)
+	symlink_error (link_name, file_hdr->c_name);
+      else if (!no_chown_flag)
+	{
+	  uid_t uid = set_owner_flag ? set_owner : file_hdr->c_uid;
+	  gid_t gid = set_group_flag ? set_group : file_hdr->c_gid;
+	  if (lchown (file_hdr->c_name, uid, gid) < 0 && errno != EPERM)
+	    chown_error_details (file_hdr->c_name, uid, gid);
+	}
     }
   free (link_name);
 }
@@ -1425,6 +1553,7 @@ process_copy_in (void)
   if (dot_flag)
     fputc ('\n', stderr);
 
+  replace_symlink_placeholders ();
   apply_delayed_set_stat ();
 
   cpio_file_stat_free (&file_hdr);
diff --git a/tests/CVE-2015-1197.at b/tests/CVE-2015-1197.at
new file mode 100644
index 0000000..6079af7
--- /dev/null
+++ b/tests/CVE-2015-1197.at
@@ -0,0 +1,40 @@
+# Process this file with autom4te to create testsuite.  -*- Autotest-*-
+# Copyright (C) 2009-2019 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, 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 <http://www.gnu.org/licenses/>.
+
+AT_SETUP([CVE-2015-1197 (--no-absolute-filenames for symlinks)])
+AT_CHECK([
+tempdir=$(pwd)/tmp
+mkdir $tempdir
+touch $tempdir/file
+ln -s $tempdir dir
+AT_DATA([filelist],
+[dir
+dir/file
+])
+cpio -o < filelist > test.cpio
+rm -rf dir $tempdir
+cpio --no-absolute-filenames -iv < test.cpio
+],
+[2],
+[],
+[1 block
+dir
+cpio: dir/file: Cannot open: Not a directory
+dir/file
+1 block
+])
+AT_CLEANUP
+
-- 
2.27.0

