bug-gzip
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

FYI: race condition patch for gzip


From: Paul Eggert
Subject: FYI: race condition patch for gzip
Date: Wed, 06 Dec 2006 22:59:56 -0800
User-agent: Gnus/5.1008 (Gnus v5.10.8) Emacs/21.4 (gnu/linux)

I inspected gzip for race condition problems and installed
the following patch.

2006-12-06  Paul Eggert  <address@hidden>

        * NEWS: Document the fixes below.
        * configure.ac (AC_CHECK_FUNCS_ONCE): Add siginterrupt.
        * gzip.c (lstat) [!defined(HAVE_LSTAT) && !defined(lstat)]: Remove.
        (SA_NOCLDSTOP, sigprocmask, sigset_t) [!defined SA_NOCLDSTOP]:
        New macros.
        (siginterrupt) [!defined SA_NOCLDSTOP && ! HAVE_SIGINTERRUPT]:
        New macro.
        (HAVE_WORKING_O_NOFOLLOW): Define to 0 if not defined.
        (caught_signals, exiting_signal, remove_ofname_fd): New vars.
        (remove_ofname): Remove; all uses changed to remove_ofname_fd.
        (same_file, name_too_long): Remove.
        (install_signal_handlers): New function.  It prefers sigaction,
        which is more reliable in the presence of race conditions.
        Catch SIGXCPU and SIGXFSZ, too.
        (main): Use it instead of installing them by hand.
        (treat_file): Check for read error when closing ifd.
        (treat_file, create_outfile, remove_output_file):
        Avoid race conditions with signals.
        (create_outfile, check_ofname): Remove most of the gorp about
        working around ENAMETOOLONG deficiencies.  It's obsolete now, and
        anyway it had race conditions.  Just rely on O_EXCL and
        ENAMETOOLONG.  The worst that can happen is that a file name will
        be silently truncated on an obsolete machine, but no data will be lost.
        (open_and_stat): Use HAVE_WORKING_O_NOFOLLOW rather than O_NOFOLLOW
        to work around some O_NOFOLLOW bugs.  Otherwise, fall back on
        lstat only if lstat is available, as symlinks don't exist otherwise.
        (open_input_file): Use O_NONBLOCK and O_NOCTTY too, to avoid
        hanging gzip on special files.
        (abort_gzip_signal): If sigaction is not available, ignore
        signals.  If we get the exiting signal, exit with ERROR status
        rather than raising a signal.
        * tailor.h (NO_ST_INO): Remove; no longer used now that same_file
        is gone.

Index: NEWS
===================================================================
RCS file: /cvsroot/gzip/gzip/NEWS,v
retrieving revision 1.6
diff -p -u -r1.6 NEWS
--- NEWS        5 Dec 2006 07:45:00 -0000       1.6
+++ NEWS        7 Dec 2006 06:56:55 -0000
@@ -2,7 +2,14 @@ Major changes in Gzip 1.3.7 (not yet rel
 
 * Fix some gzip problems:
   - Refuse to compress setuid or setgid files, or files with the sticky bit.
-  - Fix more race conditions in setting file permissions and owner.
+  - Fix more race conditions in setting file permissions and owner,
+    removing output files, following symbolic links, and dealing with
+    special files.
+  - Remove most of the code working around ENAMETOOLONG deficiencies.
+    Systems with those deficiencies are long-dead, and the workarounds
+    had race conditions on modern hosts.
+  - Catch CPU time and file size limit signals, too.
+  - Check for read errors when closing files.
   - Fix a core dump caused by a stray abort mistakenly introduced in 1.3.6.
 * Fix some gzexe problems:
   - Improve resistance to denial-of-service attacks.
Index: configure.ac
===================================================================
RCS file: /cvsroot/gzip/gzip/configure.ac,v
retrieving revision 1.3
diff -p -u -r1.3 configure.ac
--- configure.ac        6 Dec 2006 04:04:22 -0000       1.3
+++ configure.ac        7 Dec 2006 06:56:55 -0000
@@ -79,7 +79,7 @@ AC_ISC_POSIX
 AC_C_CONST
 AC_HEADER_STDC
 AC_CHECK_HEADERS_ONCE(fcntl.h limits.h memory.h time.h)
-AC_CHECK_FUNCS_ONCE([fchmod fchown fdopendir lstat])
+AC_CHECK_FUNCS_ONCE([fchmod fchown fdopendir lstat siginterrupt])
 AC_HEADER_DIRENT
 AC_TYPE_SIGNAL
 AC_TYPE_SIZE_T
Index: gzip.c
===================================================================
RCS file: /cvsroot/gzip/gzip/gzip.c,v
retrieving revision 1.7
diff -p -u -r1.7 gzip.c
--- gzip.c      6 Dec 2006 04:04:23 -0000       1.7
+++ gzip.c      7 Dec 2006 06:56:55 -0000
@@ -115,10 +115,6 @@ static char rcsid[] = "$Id: gzip.c,v 1.7
 # define CLOSEDIR(d) closedir(d)
 #endif
 
-#if !defined(HAVE_LSTAT) && !defined(lstat)
-# define lstat(name, buf) stat(name, buf)
-#endif
-
 #ifndef NO_UTIME
 #  include <utimens.h>
 #endif
@@ -149,6 +145,21 @@ static char rcsid[] = "$Id: gzip.c,v 1.7
 #define OFF_T_MAX (~ (off_t) 0 - OFF_T_MIN)
 #endif
 
+/* Use SA_NOCLDSTOP as a proxy for whether the sigaction machinery is
+   present.  */
+#ifndef SA_NOCLDSTOP
+# define SA_NOCLDSTOP 0
+# define sigprocmask(how, set, oset) /* empty */
+# define sigset_t int
+# if ! HAVE_SIGINTERRUPT
+#  define siginterrupt(sig, flag) /* empty */
+# endif
+#endif
+
+#ifndef HAVE_WORKING_O_NOFOLLOW
+# define HAVE_WORKING_O_NOFOLLOW 0
+#endif
+
 #ifndef ELOOP
 # define ELOOP EINVAL
 #endif
@@ -203,13 +214,23 @@ char **args = NULL;   /* argv pointer if
 char *z_suffix;       /* default suffix (can be set with --suffix) */
 size_t z_len;         /* strlen(z_suffix) */
 
+/* The set of signals that are caught.  */
+static sigset_t caught_signals;
+
+/* If nonzero then exit with status 1, rather than with the usual
+   signal status, on receipt of a signal with this value.  This
+   suppresses a "Broken Pipe" message with some shells.  */
+static int volatile exiting_signal;
+
+/* If nonnegative, close this file descriptor and unlink ofname on error.  */
+static int volatile remove_ofname_fd = -1;
+
 off_t bytes_in;             /* number of input bytes */
 off_t bytes_out;            /* number of output bytes */
 off_t total_in;                    /* input bytes for all files */
 off_t total_out;           /* output bytes for all files */
 char ifname[MAX_PATH_LEN]; /* input file name */
 char ofname[MAX_PATH_LEN]; /* output file name */
-int volatile remove_ofname = 0;  /* remove output file on error */
 struct stat istat;         /* status for input file */
 int  ifd;                  /* input file descriptor */
 int  ofd;                  /* output file descriptor */
@@ -261,13 +282,12 @@ local int create_outfile OF((void));
 local char *get_suffix  OF((char *name));
 local int  open_input_file OF((char *iname, struct stat *sbuf));
 local int  make_ofname  OF((void));
-local int  same_file    OF((struct stat *stat1, struct stat *stat2));
-local int name_too_long OF((char *name, struct stat *statb));
 local void shorten_name  OF((char *name));
 local int  get_method   OF((int in));
 local void do_list      OF((int ifd, int method));
 local int  check_ofname OF((void));
 local void copy_stat    OF((struct stat *ifstat));
+local void install_signal_handlers OF((void));
 local void remove_output_file OF((void));
 local RETSIGTYPE abort_gzip_signal OF((int));
 local void do_exit      OF((int exitcode)) ATTRIBUTE_NORETURN;
@@ -387,21 +407,6 @@ int main (argc, argv)
     env = add_envopt(&argc, &argv, OPTIONS_VAR);
     if (env != NULL) args = argv;
 
-    foreground = signal(SIGINT, SIG_IGN) != SIG_IGN;
-    if (foreground) {
-       signal (SIGINT, abort_gzip_signal);
-    }
-#ifdef SIGTERM
-    if (signal(SIGTERM, SIG_IGN) != SIG_IGN) {
-       signal (SIGTERM, abort_gzip_signal);
-    }
-#endif
-#ifdef SIGHUP
-    if (signal(SIGHUP, SIG_IGN) != SIG_IGN) {
-       signal (SIGHUP, abort_gzip_signal);
-    }
-#endif
-
 #ifndef GNU_STANDARD
     /* For compatibility with old compress, use program name as an option.
      * If you compile with -DGNU_STANDARD, this program will behave as
@@ -500,12 +505,6 @@ int main (argc, argv)
        }
     } /* loop on all arguments */
 
-#ifdef SIGPIPE
-    /* Ignore "Broken Pipe" message with --quiet */
-    if (quiet && signal (SIGPIPE, SIG_IGN) != SIG_IGN)
-      signal (SIGPIPE, abort_gzip_signal);
-#endif
-
     /* By default, save name and timestamp on compression but do not
      * restore them on decompression.
      */
@@ -540,6 +539,8 @@ int main (argc, argv)
     ALLOC(ush, tab_prefix1, 1L<<(BITS-1));
 #endif
 
+    install_signal_handlers ();
+
     /* And get to work */
     if (file_count != 0) {
        if (to_stdout && !test && !list && (!decompress || !ascii)) {
@@ -774,7 +775,8 @@ local void treat_file(iname)
     }
     if (list) {
         do_list(ifd, method);
-        close(ifd);
+        if (close (ifd) != 0)
+         read_error ();
         return;
     }
 
@@ -784,7 +786,7 @@ local void treat_file(iname)
      */
     if (to_stdout) {
        ofd = fileno(stdout);
-       /* keep remove_ofname as zero */
+       /* Keep remove_ofname_fd negative.  */
     } else {
        if (create_outfile() != OK) return;
 
@@ -816,32 +818,40 @@ local void treat_file(iname)
        bytes_out = 0;            /* required for length check */
     }
 
-    close(ifd);
+    if (close (ifd) != 0)
+      read_error ();
 
     if (!to_stdout)
       {
+       sigset_t oldset;
+       int unlink_errno;
+
        copy_stat (&istat);
        if (close (ofd) != 0)
          write_error ();
-       remove_ofname = 0;
 
-       /* It's now safe to remove the input file.  */
-       if (xunlink (ifname) != 0)
+       sigprocmask (SIG_BLOCK, &caught_signals, &oldset);
+       remove_ofname_fd = -1;
+       unlink_errno = xunlink (ifname) == 0 ? 0 : errno;
+       sigprocmask (SIG_SETMASK, &oldset, NULL);
+
+       if (unlink_errno)
          {
-           int e = errno;
            WARN ((stderr, "%s: ", program_name));
            if (!quiet)
              {
-               errno = e;
+               errno = unlink_errno;
                perror (ifname);
              }
          }
       }
 
     if (method == -1) {
-       if (!to_stdout) xunlink (ofname);
+       if (!to_stdout)
+         remove_output_file ();
        return;
     }
+
     /* Display statistics */
     if(verbose) {
        if (test) {
@@ -869,55 +879,55 @@ local void treat_file(iname)
  */
 local int create_outfile()
 {
-    struct stat        ostat; /* stat for ofname */
-    int flags = O_WRONLY | O_CREAT | O_EXCL | O_BINARY;
+  int name_shortened = 0;
+  int flags = (O_WRONLY | O_CREAT | O_EXCL
+              | (ascii && decompress ? 0 : O_BINARY));
 
-    if (ascii && decompress) {
-       flags &= ~O_BINARY; /* force ascii text mode */
-    }
-    for (;;) {
-       /* Make sure that ofname is not an existing file */
-       if (check_ofname() != OK) {
-           close(ifd);
-           return ERROR;
-       }
-       /* Create the output file */
-       remove_ofname = 1;
-       ofd = OPEN(ofname, flags, RW_USER);
-       if (ofd == -1) {
-           progerror(ofname);
-           close(ifd);
-           return ERROR;
-       }
+  for (;;)
+    {
+      int open_errno;
+      sigset_t oldset;
 
-       /* Check for name truncation on new file (1234567890123.gz) */
-#ifdef NO_FSTAT
-       if (stat(ofname, &ostat) != 0) {
-#else
-       if (fstat(ofd, &ostat) != 0) {
+      sigprocmask (SIG_BLOCK, &caught_signals, &oldset);
+      remove_ofname_fd = ofd = OPEN (ofname, flags, RW_USER);
+      open_errno = errno;
+      sigprocmask (SIG_SETMASK, &oldset, NULL);
+
+      if (0 <= ofd)
+       break;
+
+      switch (open_errno)
+       {
+#ifdef ENAMETOOLONG
+       case ENAMETOOLONG:
+         shorten_name (ofname);
+         name_shortened = 1;
+         break;
 #endif
-           progerror(ofname);
-           close(ifd); close(ofd);
-           xunlink (ofname);
-           return ERROR;
-       }
-       if (!name_too_long(ofname, &ostat)) return OK;
 
-       if (decompress) {
-           /* name might be too long if an original name was saved */
-           WARN((stderr, "%s: %s: warning, name truncated\n",
-                 program_name, ofname));
-           return OK;
+       case EEXIST:
+         if (check_ofname () != OK)
+           {
+             close (ifd);
+             return ERROR;
+           }
+         break;
+
+       default:
+         progerror (ofname);
+         close (ifd);
+         return ERROR;
        }
-       close(ofd);
-       xunlink (ofname);
-#ifdef NO_MULTIPLE_DOTS
-       /* Should never happen, see check_ofname() */
-       fprintf (stderr, "%s: %s: name too long\n", program_name, ofname);
-       do_exit(ERROR);
-#endif
-       shorten_name(ofname);
     }
+
+  if (name_shortened && decompress)
+    {
+      /* name might be too long if an original name was saved */
+      WARN ((stderr, "%s: %s: warning, name truncated\n",
+            program_name, ofname));
+    }
+
+  return OK;
 }
 
 /* ========================================================================
@@ -986,12 +996,19 @@ open_and_stat (char *name, int flags, mo
   /* Refuse to follow symbolic links unless -c or -f.  */
   if (!to_stdout && !force)
     {
-      if (O_NOFOLLOW)
+      if (HAVE_WORKING_O_NOFOLLOW)
        flags |= O_NOFOLLOW;
-      else if (lstat (name, st) == 0 && S_ISLNK (st->st_mode))
+      else
        {
-         errno = ELOOP;
-         return -1;
+#if HAVE_LSTAT || defined lstat
+         if (lstat (name, st) != 0)
+           return -1;
+         else if (S_ISLNK (st->st_mode))
+           {
+             errno = ELOOP;
+             return -1;
+           }
+#endif
        }
     }
 
@@ -1028,7 +1045,8 @@ open_input_file (iname, sbuf)
     char *dot; /* pointer to ifname extension, or NULL */
 #endif
     int fd;
-    int open_flags = ascii && !decompress ? O_RDONLY : O_RDONLY | O_BINARY;
+    int open_flags = (O_RDONLY | O_NONBLOCK | O_NOCTTY
+                     | (ascii && !decompress ? 0 : O_BINARY));
 
     *suf = z_suffix;
 
@@ -1170,8 +1188,8 @@ local int make_ofname()
                strcat(ofname, "gz"); /* enough room */
                return OK;
            }
-        /* On the Atari and some versions of MSDOS, name_too_long()
-         * does not work correctly because of a bug in stat(). So we
+        /* On the Atari and some versions of MSDOS,
+         * ENAMETOOLONG does not work correctly.  So we
          * must truncate here.
          */
         } else if (strlen(suff)-1 + z_len > MAX_SUFFIX) {
@@ -1501,49 +1519,6 @@ local void do_list(ifd, method)
 }
 
 /* ========================================================================
- * Return true if the two stat structures correspond to the same file.
- */
-local int same_file(stat1, stat2)
-    struct stat *stat1;
-    struct stat *stat2;
-{
-    return stat1->st_ino   == stat2->st_ino
-       && stat1->st_dev   == stat2->st_dev
-#ifdef NO_ST_INO
-        /* Can't rely on st_ino and st_dev, use other fields: */
-       && stat1->st_mode  == stat2->st_mode
-       && stat1->st_uid   == stat2->st_uid
-       && stat1->st_gid   == stat2->st_gid
-       && stat1->st_size  == stat2->st_size
-       && stat1->st_atime == stat2->st_atime
-       && stat1->st_mtime == stat2->st_mtime
-       && stat1->st_ctime == stat2->st_ctime
-#endif
-           ;
-}
-
-/* ========================================================================
- * Return true if a file name is ambiguous because the operating system
- * truncates file names.
- */
-local int name_too_long(name, statb)
-    char *name;           /* file name to check */
-    struct stat *statb;   /* stat buf for this file name */
-{
-    int s = strlen(name);
-    char c = name[s-1];
-    struct stat        tstat; /* stat for truncated name */
-    int res;
-
-    tstat = *statb;      /* Just in case OS does not fill all fields */
-    name[s-1] = '\0';
-    res = lstat(name, &tstat) == 0 && same_file(statb, &tstat);
-    name[s-1] = c;
-    Trace((stderr, " too_long(%s) => %d\n", name, res));
-    return res;
-}
-
-/* ========================================================================
  * Shorten the given name by one character, or replace a .tar extension
  * with .tgz. Truncate the last part of the name which is longer than
  * MIN_PART characters: 1234.678.012.gz -> 123.678.012.gz. If the name
@@ -1608,58 +1583,11 @@ local void shorten_name(name)
 }
 
 /* ========================================================================
- * If compressing to a file, check if ofname is not ambiguous
- * because the operating system truncates names. Otherwise, generate
- * a new ofname and save the original name in the compressed file.
- * If the compressed file already exists, ask for confirmation.
- *    The check for name truncation is made dynamically, because different
- * file systems on the same OS might use different truncation rules (on SVR4
- * s5 truncates to 14 chars and ufs does not truncate).
- *    This function returns -1 if the file must be skipped, and
- * updates save_orig_name if necessary.
- * IN assertions: save_orig_name is already set if ofname has been
- * already truncated because of NO_MULTIPLE_DOTS. The input file has
- * already been open and istat is set.
+ * The compressed file already exists, so ask for confirmation.
+ * Return ERROR if the file must be skipped.
  */
 local int check_ofname()
 {
-    struct stat        ostat; /* stat for ofname */
-
-#ifdef ENAMETOOLONG
-    /* Check for strictly conforming Posix systems (which return ENAMETOOLONG
-     * instead of silently truncating filenames).
-     */
-    errno = 0;
-    while (lstat(ofname, &ostat) != 0) {
-        if (errno != ENAMETOOLONG) return 0; /* ofname does not exist */
-       shorten_name(ofname);
-    }
-#else
-    if (lstat(ofname, &ostat) != 0) return 0;
-#endif
-    /* Check for name truncation on existing file. Do this even on systems
-     * defining ENAMETOOLONG, because on most systems the strict Posix
-     * behavior is disabled by default (silent name truncation allowed).
-     */
-    if (!decompress && name_too_long(ofname, &ostat)) {
-       shorten_name(ofname);
-       if (lstat(ofname, &ostat) != 0) return 0;
-    }
-
-    /* Check that the input and output files are different (could be
-     * the same by name truncation or links).
-     */
-    if (same_file(&istat, &ostat)) {
-       if (strequ(ifname, ofname)) {
-           fprintf(stderr, "%s: %s: cannot %scompress onto itself\n",
-                   program_name, ifname, decompress ? "de" : "");
-       } else {
-           fprintf(stderr, "%s: %s and %s are the same file\n",
-                   program_name, ifname, ofname);
-       }
-       exit_code = ERROR;
-       return ERROR;
-    }
     /* Ask permission to overwrite the existing file */
     if (!force) {
        int ok = 0;
@@ -1821,6 +1749,73 @@ local void treat_dir (fd, dir)
 }
 #endif /* ! NO_DIR */
 
+/* Make sure signals get handled properly.  */
+
+static void
+install_signal_handlers ()
+{
+  static int sig[] =
+    {
+      /* SIGINT must be first, as 'foreground' depends on it.  */
+      SIGINT
+
+#ifdef SIGHUP
+      , SIGHUP
+#endif
+#ifdef SIGPIPE
+      , SIGPIPE
+#else
+# define SIGPIPE 0
+#endif
+#ifdef SIGTERM
+      , SIGTERM
+#endif
+#ifdef SIGXCPU
+      , SIGXCPU
+#endif
+#ifdef SIGXFSZ
+      , SIGXFSZ
+#endif
+    };
+  int nsigs = sizeof sig / sizeof sig[0];
+  int i;
+
+  exiting_signal = quiet ? SIGPIPE : 0;
+
+#if SA_NOCLDSTOP
+  struct sigaction act;
+
+  sigemptyset (&caught_signals);
+  for (i = 0; i < nsigs; i++)
+    {
+      sigaction (sig[i], NULL, &act);
+      if (act.sa_handler != SIG_IGN)
+       sigaddset (&caught_signals, sig[i]);
+    }
+
+  act.sa_handler = abort_gzip_signal;
+  act.sa_mask = caught_signals;
+  act.sa_flags = 0;
+
+  for (i = 0; i < nsigs; i++)
+    if (sigismember (&caught_signals, sig[i]))
+      {
+       if (i == 0)
+         foreground = 1;
+       sigaction (sig[i], &act, NULL);
+      }
+#else
+  for (i = 0; i < nsigs; i++)
+    if (signal (sig[i], SIG_IGN) != SIG_IGN)
+      {
+       if (i == 0)
+         foreground = 1;
+       signal (sig[i], abort_gzip_signal);
+       siginterrupt (sig[i], 1);
+      }
+#endif
+}
+
 /* ========================================================================
  * Free all dynamically allocated variables and exit with the given code.
  */
@@ -1852,10 +1847,18 @@ local void do_exit(exitcode)
 static void
 remove_output_file ()
 {
-   if (remove_ofname) {
-       close(ofd);
-       xunlink (ofname);
-   }
+  int fd;
+  sigset_t oldset;
+
+  sigprocmask (SIG_BLOCK, &caught_signals, &oldset);
+  fd = remove_ofname_fd;
+  if (0 <= fd)
+    {
+      remove_ofname_fd = -1;
+      close (fd);
+      xunlink (ofname);
+    }
+  sigprocmask (SIG_SETMASK, &oldset, NULL);
 }
 
 /* ========================================================================
@@ -1875,7 +1878,11 @@ static RETSIGTYPE
 abort_gzip_signal (sig)
      int sig;
 {
+  if (! SA_NOCLDSTOP)
+    signal (sig, SIG_IGN);
    remove_output_file ();
+   if (sig == exiting_signal)
+     _exit (ERROR);
    signal (sig, SIG_DFL);
    raise (sig);
 }
Index: tailor.h
===================================================================
RCS file: /cvsroot/gzip/gzip/tailor.h,v
retrieving revision 1.4
diff -p -u -r1.4 tailor.h
--- tailor.h    20 Nov 2006 08:40:33 -0000      1.4
+++ tailor.h    7 Dec 2006 06:56:55 -0000
@@ -1,6 +1,6 @@
 /* tailor.h -- target dependent definitions
 
-   Copyright (C) 1997, 1998, 1999, 2002 Free Software Foundation, Inc.
+   Copyright (C) 1997, 1998, 1999, 2002, 2006 Free Software Foundation, Inc.
    Copyright (C) 1992-1993 Jean-loup Gailly
 
    This program is free software; you can redistribute it and/or modify
@@ -256,10 +256,6 @@
 #  define OS_CODE  0x0a
 #endif
 
-#ifndef unix
-#  define NO_ST_INO /* don't rely on inode numbers */
-#endif
-
 
        /* Common defaults */
 




reply via email to

[Prev in Thread] Current Thread [Next in Thread]