e4010a
From 14d8fc718f0c872274b90991ee634b0cd8e1a6f0 Mon Sep 17 00:00:00 2001
e4010a
From: Sergey Poznyakoff <gray@gnu.org>
e4010a
Date: Sat, 8 Feb 2020 13:01:47 +0200
e4010a
Subject: [PATCH] Fix the --no-overwrite-dir option
e4010a
e4010a
Given this option, tar failed to preserve permissions of empty directories
e4010a
and to create files under directories owned by the current user that did
e4010a
not have the S_IWUSR bit set.
e4010a
e4010a
* src/extract.c (fd_chmod): Rename to fd_i_chmod.
e4010a
(fd_chmod): New function.
e4010a
(safe_dir_mode): New function.
e4010a
(extract_dir): Special handling for existing directories in
e4010a
--no-overwrite-dir mode.
e4010a
* tests/extrac23.at: New file.
e4010a
* tests/Makefile.am: Add new test case.
e4010a
* tests/testsuite.at: Likewise.
e4010a
---
e4010a
 src/extract.c      | 128 ++++++++++++++++++++++++++++++---------------
e4010a
 tests/Makefile.am  |   1 +
e4010a
 tests/extrac23.at  |  58 ++++++++++++++++++++
e4010a
 tests/testsuite.at |   1 +
e4010a
 4 files changed, 146 insertions(+), 42 deletions(-)
e4010a
 create mode 100644 tests/extrac23.at
e4010a
e4010a
diff --git a/src/extract.c b/src/extract.c
e4010a
index a4a35a57..5a38ba70 100644
e4010a
--- a/src/extract.c
e4010a
+++ b/src/extract.c
e4010a
@@ -194,7 +194,7 @@ extr_init (void)
e4010a
 
e4010a
 /* Use fchmod if possible, fchmodat otherwise.  */
e4010a
 static int
e4010a
-fd_chmod (int fd, char const *file, mode_t mode, int atflag)
e4010a
+fd_i_chmod (int fd, char const *file, mode_t mode, int atflag)
e4010a
 {
e4010a
   if (0 <= fd)
e4010a
     {
e4010a
@@ -205,6 +205,42 @@ fd_chmod (int fd, char const *file, mode_t mode, int atflag)
e4010a
   return fchmodat (chdir_fd, file, mode, atflag);
e4010a
 }
e4010a
 
e4010a
+/* A version of fd_i_chmod which gracefully handles several common error
e4010a
+   conditions.  Additional argument TYPEFLAG is the type of file in tar
e4010a
+   notation.
e4010a
+ */
e4010a
+static int
e4010a
+fd_chmod(int fd, char const *file_name, int mode, int atflag, int typeflag)
e4010a
+{
e4010a
+  int chmod_errno = fd_i_chmod (fd, file_name, mode, atflag) == 0 ? 0 : errno;
e4010a
+
e4010a
+  /* On Solaris, chmod may fail if we don't have PRIV_ALL, because
e4010a
+     setuid-root files would otherwise be a backdoor.  See
e4010a
+     http://opensolaris.org/jive/thread.jspa?threadID=95826
e4010a
+     (2009-09-03).  */
e4010a
+  if (chmod_errno == EPERM && (mode & S_ISUID)
e4010a
+      && priv_set_restore_linkdir () == 0)
e4010a
+    {
e4010a
+      chmod_errno = fd_i_chmod (fd, file_name, mode, atflag) == 0 ? 0 : errno;
e4010a
+      priv_set_remove_linkdir ();
e4010a
+    }
e4010a
+
e4010a
+  /* Linux fchmodat does not support AT_SYMLINK_NOFOLLOW, and
e4010a
+     returns ENOTSUP even when operating on non-symlinks, try
e4010a
+     again with the flag disabled if it does not appear to be
e4010a
+     supported and if the file is not a symlink.  This
e4010a
+     introduces a race, alas.  */
e4010a
+  if (atflag && typeflag != SYMTYPE && ! implemented (chmod_errno))
e4010a
+    chmod_errno = fd_i_chmod (fd, file_name, mode, 0) == 0 ? 0 : errno;
e4010a
+
e4010a
+  if (chmod_errno && (typeflag != SYMTYPE || implemented (chmod_errno)))
e4010a
+    {
e4010a
+      errno = chmod_errno;
e4010a
+      return -1;
e4010a
+    }
e4010a
+  return 0;
e4010a
+}
e4010a
+
e4010a
 /* Use fchown if possible, fchownat otherwise.  */
e4010a
 static int
e4010a
 fd_chown (int fd, char const *file, uid_t uid, gid_t gid, int atflag)
e4010a
@@ -259,35 +295,8 @@ set_mode (char const *file_name,
e4010a
 
e4010a
       if (current_mode != mode)
e4010a
 	{
e4010a
-	  int chmod_errno =
e4010a
-	    fd_chmod (fd, file_name, mode, atflag) == 0 ? 0 : errno;
e4010a
-
e4010a
-	  /* On Solaris, chmod may fail if we don't have PRIV_ALL, because
e4010a
-	     setuid-root files would otherwise be a backdoor.  See
e4010a
-	     http://opensolaris.org/jive/thread.jspa?threadID=95826
e4010a
-	     (2009-09-03).  */
e4010a
-	  if (chmod_errno == EPERM && (mode & S_ISUID)
e4010a
-	      && priv_set_restore_linkdir () == 0)
e4010a
-	    {
e4010a
-	      chmod_errno =
e4010a
-		fd_chmod (fd, file_name, mode, atflag) == 0 ? 0 : errno;
e4010a
-	      priv_set_remove_linkdir ();
e4010a
-	    }
e4010a
-
e4010a
-	  /* Linux fchmodat does not support AT_SYMLINK_NOFOLLOW, and
e4010a
-	     returns ENOTSUP even when operating on non-symlinks, try
e4010a
-	     again with the flag disabled if it does not appear to be
e4010a
-	     supported and if the file is not a symlink.  This
e4010a
-	     introduces a race, alas.  */
e4010a
-	  if (atflag && typeflag != SYMTYPE && ! implemented (chmod_errno))
e4010a
-	    chmod_errno = fd_chmod (fd, file_name, mode, 0) == 0 ? 0 : errno;
e4010a
-
e4010a
-	  if (chmod_errno
e4010a
-	      && (typeflag != SYMTYPE || implemented (chmod_errno)))
e4010a
-	    {
e4010a
-	      errno = chmod_errno;
e4010a
-	      chmod_error_details (file_name, mode);
e4010a
-	    }
e4010a
+	  if (fd_chmod (fd, file_name, mode, atflag, typeflag))
e4010a
+	    chmod_error_details (file_name, mode);
e4010a
 	}
e4010a
     }
e4010a
 }
e4010a
@@ -975,6 +984,26 @@ is_directory_link (const char *file_name)
e4010a
   return res;
e4010a
 }
e4010a
 
e4010a
+/* Given struct stat of a directory (or directory member) whose ownership
e4010a
+   or permissions of will be restored later, return the temporary permissions
e4010a
+   for that directory, sufficiently restrictive so that in the meantime
e4010a
+   processes owned by other users do not inadvertently create files under this
e4010a
+   directory that inherit the wrong owner, group, or permissions from the
e4010a
+   directory.
e4010a
+
e4010a
+   If not root, though, make the directory writeable and searchable at first,
e4010a
+   so that files can be created under it.
e4010a
+*/
e4010a
+static inline int
e4010a
+safe_dir_mode (struct stat const *st)
e4010a
+{
e4010a
+  return ((st->st_mode
e4010a
+	   & (0 < same_owner_option || 0 < same_permissions_option
e4010a
+	      ? S_IRWXU
e4010a
+	      : MODE_RWX))
e4010a
+	  | (we_are_root ? 0 : MODE_WXUSR));
e4010a
+}
e4010a
+
e4010a
 /* Extractor functions for various member types */
e4010a
 
e4010a
 static int
e4010a
@@ -1004,18 +1033,7 @@ extract_dir (char *file_name, int typeflag)
e4010a
   else if (typeflag == GNUTYPE_DUMPDIR)
e4010a
     skip_member ();
e4010a
 
e4010a
-  /* If ownership or permissions will be restored later, create the
e4010a
-     directory with restrictive permissions at first, so that in the
e4010a
-     meantime processes owned by other users do not inadvertently
e4010a
-     create files under this directory that inherit the wrong owner,
e4010a
-     group, or permissions from the directory.  If not root, though,
e4010a
-     make the directory writeable and searchable at first, so that
e4010a
-     files can be created under it.  */
e4010a
-  mode = ((current_stat_info.stat.st_mode
e4010a
-	   & (0 < same_owner_option || 0 < same_permissions_option
e4010a
-	      ? S_IRWXU
e4010a
-	      : MODE_RWX))
e4010a
-	  | (we_are_root ? 0 : MODE_WXUSR));
e4010a
+  mode = safe_dir_mode (&current_stat_info.stat);
e4010a
 
e4010a
   for (;;)
e4010a
     {
e4010a
@@ -1031,6 +1049,7 @@ extract_dir (char *file_name, int typeflag)
e4010a
       if (errno == EEXIST
e4010a
 	  && (interdir_made
e4010a
 	      || keep_directory_symlink_option
e4010a
+	      || old_files_option == NO_OVERWRITE_DIR_OLD_FILES
e4010a
 	      || old_files_option == DEFAULT_OLD_FILES
e4010a
 	      || old_files_option == OVERWRITE_OLD_FILES))
e4010a
 	{
e4010a
@@ -1051,6 +1070,31 @@ extract_dir (char *file_name, int typeflag)
e4010a
 		      repair_delayed_set_stat (file_name, &st);
e4010a
 		      return 0;
e4010a
 		    }
e4010a
+		  else if (old_files_option == NO_OVERWRITE_DIR_OLD_FILES)
e4010a
+		    {
e4010a
+		      /* Temporarily change the directory mode to a safe
e4010a
+			 value, to be able to create files in it, should
e4010a
+			 the need be.
e4010a
+		      */
e4010a
+		      mode = safe_dir_mode (&st);
e4010a
+		      status = fd_chmod(-1, file_name, mode,
e4010a
+					AT_SYMLINK_NOFOLLOW, DIRTYPE);
e4010a
+		      if (status == 0)
e4010a
+			{
e4010a
+			  /* Store the actual directory mode, to be restored
e4010a
+			     later.
e4010a
+			  */
e4010a
+			  current_stat_info.stat = st;
e4010a
+			  current_mode = mode & ~ current_umask;
e4010a
+			  current_mode_mask = MODE_RWX;
e4010a
+			  atflag = AT_SYMLINK_NOFOLLOW;
e4010a
+			  break;
e4010a
+			}
e4010a
+		      else
e4010a
+			{
e4010a
+			  chmod_error_details (file_name, mode);
e4010a
+			}
e4010a
+		    }
e4010a
 		  break;
e4010a
 		}
e4010a
 	    }
e4010a
diff --git a/tests/Makefile.am b/tests/Makefile.am
e4010a
index 0369a950..31ae3460 100644
e4010a
--- a/tests/Makefile.am
e4010a
+++ b/tests/Makefile.am
e4010a
@@ -121,6 +121,7 @@ TESTSUITE_AT = \
e4010a
  extrac19.at\
e4010a
  extrac20.at\
e4010a
  extrac21.at\
e4010a
+ extrac23.at\
e4010a
  filerem01.at\
e4010a
  filerem02.at\
e4010a
  dirrem01.at\
e4010a
diff --git a/tests/extrac23.at b/tests/extrac23.at
e4010a
new file mode 100644
e4010a
index 00000000..669d18b6
e4010a
--- /dev/null
e4010a
+++ b/tests/extrac23.at
e4010a
@@ -0,0 +1,58 @@
e4010a
+# Test suite for GNU tar.                             -*- Autotest -*-
e4010a
+# Copyright 2020 Free Software Foundation, Inc.
e4010a
+#
e4010a
+# This file is part of GNU tar.
e4010a
+#
e4010a
+# GNU tar is free software; you can redistribute it and/or modify
e4010a
+# it under the terms of the GNU General Public License as published by
e4010a
+# the Free Software Foundation; either version 3 of the License, or
e4010a
+# (at your option) any later version.
e4010a
+#
e4010a
+# GNU tar is distributed in the hope that it will be useful,
e4010a
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
e4010a
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
e4010a
+# GNU General Public License for more details.
e4010a
+#
e4010a
+# You should have received a copy of the GNU General Public License
e4010a
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
e4010a
+AT_SETUP([--no-overwrite-dir])
e4010a
+AT_KEYWORDS([extract extrac23 no-overwrite-dir])
e4010a
+
e4010a
+# Description: Implementation of the --no-overwrite-dir option was flawed in
e4010a
+# tar versions up to 1.32.90.  This option is intended to preserve metadata
e4010a
+# of existing directories.  In fact it worked only for non-empty directories.
e4010a
+# Moreover, if the actual directory was owned by the user tar runs as and the
e4010a
+# S_IWUSR bit was not set in its actual permissions, tar failed to create files
e4010a
+# in it.
e4010a
+#
e4010a
+# Reported by: Michael Kaufmann <mail@michael-kaufmann.ch>
e4010a
+# References: <20200207112934.Horde.anXzYhAj2CHiwUrw5CuT0G-@webmail.michael-kaufmann.ch>,
e4010a
+#   https://lists.gnu.org/archive/html/bug-tar/2020-02/msg00003.html
e4010a
+
e4010a
+AT_TAR_CHECK([
e4010a
+# Test if the directory permissions are restored properly.
e4010a
+mkdir dir
e4010a
+chmod 755 dir
e4010a
+tar cf a.tar dir
e4010a
+chmod 777 dir
e4010a
+tar -xf a.tar --no-overwrite-dir
e4010a
+genfile --stat=mode.777 dir
e4010a
+
e4010a
+# Test if temprorary permissions are set correctly to allow the owner
e4010a
+# to write to the directory.
e4010a
+genfile --file dir/file
e4010a
+tar cf a.tar dir
e4010a
+rm dir/file
e4010a
+chmod 400 dir
e4010a
+tar -xf a.tar --no-overwrite-dir
e4010a
+genfile --stat=mode.777 dir
e4010a
+chmod 700 dir
e4010a
+find dir
e4010a
+],
e4010a
+[0],
e4010a
+[777
e4010a
+400
e4010a
+dir
e4010a
+dir/file
e4010a
+])
e4010a
+AT_CLEANUP
e4010a
diff --git a/tests/testsuite.at b/tests/testsuite.at
e4010a
index 2cc43a19..0620a3c7 100644
e4010a
--- a/tests/testsuite.at
e4010a
+++ b/tests/testsuite.at
e4010a
@@ -343,6 +343,7 @@ m4_include([extrac19.at])
e4010a
 m4_include([extrac19.at])
e4010a
 m4_include([extrac20.at])
e4010a
 m4_include([extrac21.at])
e4010a
+m4_include([extrac23.at])
e4010a
 
e4010a
 m4_include([backup01.at])
e4010a
 
e4010a
-- 
e4010a
2.37.3
e4010a