]> Dogcows Code - chaz/tar/blobdiff - src/create.c
tar: live within system-supplied limits on file descriptors
[chaz/tar] / src / create.c
index e137325484e83efd8dd819722e64bda87e1e04fc..5e2171b561917a74c2bde424a9eb7e0648c970a0 100644 (file)
 #include "common.h"
 #include <hash.h>
 
+/* Error number to use when an impostor is discovered.
+   Pretend the impostor isn't there.  */
+enum { IMPOSTOR_ERRNO = ENOENT };
+
 struct link
   {
     dev_t dev;
@@ -72,13 +76,13 @@ exclusion_tag_warning (const char *dirname, const char *tagname,
 }
 
 enum exclusion_tag_type
-check_exclusion_tags (int fd, char const **tag_file_name)
+check_exclusion_tags (struct tar_stat_info const *st, char const **tag_file_name)
 {
   struct exclusion_tag *tag;
 
   for (tag = exclusion_tags; tag; tag = tag->next)
     {
-      int tagfd = openat (fd, tag->name, open_read_flags);
+      int tagfd = subfile_open (st, tag->name, open_read_flags);
       if (0 <= tagfd)
        {
          bool satisfied = !tag->predicate || tag->predicate (tagfd);
@@ -1038,7 +1042,7 @@ dump_regular_file (int fd, struct tar_stat_info *st)
            memset (blk->buffer + size_left, 0, BLOCKSIZE - count);
        }
 
-      count = (fd < 0) ? bufsize : safe_read (fd, blk->buffer, bufsize);
+      count = (fd <= 0) ? bufsize : safe_read (fd, blk->buffer, bufsize);
       if (count == SAFE_READ_ERROR)
        {
          read_diag_details (st->orig_file_name,
@@ -1159,7 +1163,7 @@ dump_dir0 (struct tar_stat_info *st, char const *directory)
       char *name_buf;
       size_t name_size;
 
-      switch (check_exclusion_tags (st->fd, &tag_file_name))
+      switch (check_exclusion_tags (st, &tag_file_name))
        {
        case exclusion_tag_all:
          /* Handled in dump_file0 */
@@ -1224,21 +1228,93 @@ ensure_slash (char **pstr)
   (*pstr)[len] = '\0';
 }
 
+/* If we just ran out of file descriptors, release a file descriptor
+   in the directory chain somewhere leading from DIR->parent->parent
+   up through the root.  Return true if successful, false (preserving
+   errno == EMFILE) otherwise.
+
+   Do not release DIR's file descriptor, or DIR's parent, as other
+   code assumes that they work.  On some operating systems, another
+   process can claim file descriptor resources as we release them, and
+   some calls or their emulations require multiple file descriptors,
+   so callers should not give up if a single release doesn't work.  */
+
 static bool
-dump_dir (struct tar_stat_info *st)
+open_failure_recover (struct tar_stat_info const *dir)
+{
+  if (errno == EMFILE && dir && dir->parent)
+    {
+      struct tar_stat_info *p;
+      for (p = dir->parent->parent; p; p = p->parent)
+       if (0 < p->fd && (! p->parent || p->parent->fd <= 0))
+         {
+           tar_stat_close (p);
+           return true;
+         }
+      errno = EMFILE;
+    }
+
+  return false;
+}
+
+/* Return the directory entries of ST, in a dynamically allocated buffer,
+   each entry followed by '\0' and the last followed by an extra '\0'.
+   Return null on failure, setting errno.  */
+char *
+get_directory_entries (struct tar_stat_info *st)
 {
-  char *directory = 0;
-  int dupfd = dup (st->fd);
-  if (0 <= dupfd)
+  DIR *dirstream;
+  while (! (dirstream = fdopendir (st->fd)) && open_failure_recover (st))
+    continue;
+
+  if (! dirstream)
+    return 0;
+  else
     {
-      directory = fdsavedir (dupfd);
-      if (! directory)
+      char *entries = streamsavedir (dirstream);
+      int streamsavedir_errno = errno;
+
+      int fd = dirfd (dirstream);
+      if (fd < 0)
        {
-         int e = errno;
-         close (dupfd);
-         errno = e;
+         /* The dirent.h implementation doesn't use file descriptors
+            for directory streams, so open the directory again.  */
+         char const *name = st->orig_file_name;
+         if (closedir (dirstream) != 0)
+           close_diag (name);
+         dirstream = 0;
+         fd = subfile_open (st->parent,
+                            st->parent ? last_component (name) : name,
+                            open_searchdir_flags);
+         if (fd < 0)
+           fd = - errno;
+         else
+           {
+             struct stat dirst;
+             if (! (fstat (fd, &dirst) == 0
+                    && st->stat.st_ino == dirst.st_ino
+                    && st->stat.st_dev == dirst.st_dev))
+               {
+                 close (fd);
+                 fd = - IMPOSTOR_ERRNO;
+               }
+           }
        }
+
+      st->fd = fd;
+      st->dirstream = dirstream;
+      errno = streamsavedir_errno;
+      return entries;
     }
+}
+
+/* Dump the directory ST.  Return true if successful, false (emitting
+   diagnostics) otherwise.  Get ST's entries, recurse through its
+   subdirectories, and clean up file descriptors afterwards.  */
+static bool
+dump_dir (struct tar_stat_info *st)
+{
+  char *directory = get_directory_entries (st);
   if (! directory)
     {
       savedir_diag (st->orig_file_name);
@@ -1247,6 +1323,7 @@ dump_dir (struct tar_stat_info *st)
 
   dump_dir0 (st, directory);
 
+  restore_parent_fd (st);
   free (directory);
   return true;
 }
@@ -1307,21 +1384,19 @@ create_archive (void)
                    {
                      if (! st.orig_file_name)
                        {
-                         st.orig_file_name = xstrdup (p->name);
-                         st.fd = open (st.orig_file_name,
-                                       ((open_read_flags - O_RDONLY
-                                         + O_SEARCH)
-                                        | O_DIRECTORY));
-                         if (st.fd < 0)
+                         int fd = open (p->name, open_searchdir_flags);
+                         if (fd < 0)
                            {
                              open_diag (p->name);
                              break;
                            }
-                         if (fstat (st.fd, &st.stat) != 0)
+                         st.fd = fd;
+                         if (fstat (fd, &st.stat) != 0)
                            {
                              stat_diag (p->name);
                              break;
                            }
+                         st.orig_file_name = xstrdup (p->name);
                        }
                      if (buffer_size < plen + qlen)
                        {
@@ -1492,6 +1567,74 @@ check_links (void)
     }
 }
 
+/* Assuming DIR is the working directory, open FILE, using FLAGS to
+   control the open.  A null DIR means to use ".".  If we are low on
+   file descriptors, try to release one or more from DIR's parents to
+   reuse it.  */
+int
+subfile_open (struct tar_stat_info const *dir, char const *file, int flags)
+{
+  int fd;
+
+  static bool initialized;
+  if (! initialized)
+    {
+      /* Initialize any tables that might be needed when file
+        descriptors are exhausted, and whose initialization might
+        require a file descriptor.  This includes the system message
+        catalog and tar's message catalog.  */
+      initialized = true;
+      strerror (ENOENT);
+      gettext ("");
+    }
+
+  while ((fd = openat (dir ? dir->fd : AT_FDCWD, file, flags)) < 0
+        && open_failure_recover (dir))
+    continue;
+  return fd;
+}
+
+/* Restore the file descriptor for ST->parent, if it was temporarily
+   closed to conserve file descriptors.  On failure, set the file
+   descriptor to the negative of the corresponding errno value.  Call
+   this every time a subdirectory is ascended from.  */
+void
+restore_parent_fd (struct tar_stat_info const *st)
+{
+  struct tar_stat_info *parent = st->parent;
+  if (parent && ! parent->fd)
+    {
+      int parentfd = openat (st->fd, "..", open_searchdir_flags);
+      struct stat parentstat;
+
+      if (parentfd < 0)
+       parentfd = - errno;
+      else if (! (fstat (parentfd, &parentstat) == 0
+                 && parent->stat.st_ino == parentstat.st_ino
+                 && parent->stat.st_dev == parentstat.st_dev))
+       {
+         close (parentfd);
+         parentfd = IMPOSTOR_ERRNO;
+       }
+
+      if (parentfd < 0)
+       {
+         int origfd = open (parent->orig_file_name, open_searchdir_flags);
+         if (0 <= origfd)
+           {
+             if (fstat (parentfd, &parentstat) == 0
+                 && parent->stat.st_ino == parentstat.st_ino
+                 && parent->stat.st_dev == parentstat.st_dev)
+               parentfd = origfd;
+             else
+               close (origfd);
+           }
+       }
+
+      parent->fd = parentfd;
+    }
+}
+
 /* Dump a single file, recursing on directories.  ST is the file's
    status info, NAME its name relative to the parent directory, and P
    its full name (which may be relative to the working directory).  */
@@ -1508,10 +1651,11 @@ dump_file0 (struct tar_stat_info *st, char const *name, char const *p)
   struct timespec original_ctime;
   struct timespec restore_times[2];
   off_t block_ordinal = -1;
-  int fd = -1;
+  int fd = 0;
   bool is_dir;
-  bool top_level = ! st->parent;
-  int parentfd = top_level ? AT_FDCWD : st->parent->fd;
+  struct tar_stat_info const *parent = st->parent;
+  bool top_level = ! parent;
+  int parentfd = top_level ? AT_FDCWD : parent->fd;
   void (*diag) (char const *) = 0;
 
   if (interactive_option && !confirm ("add", p))
@@ -1523,15 +1667,24 @@ dump_file0 (struct tar_stat_info *st, char const *name, char const *p)
 
   transform_name (&st->file_name, XFORM_REGFILE);
 
-  if (fstatat (parentfd, name, &st->stat, fstatat_flags) != 0)
+  if (parentfd < 0 && ! top_level)
+    {
+      errno = - parentfd;
+      diag = open_diag;
+    }
+  else if (fstatat (parentfd, name, &st->stat, fstatat_flags) != 0)
     diag = stat_diag;
   else if (file_dumpable_p (&st->stat))
     {
-      fd = st->fd = openat (parentfd, name, open_read_flags);
+      fd = subfile_open (parent, name, open_read_flags);
       if (fd < 0)
        diag = open_diag;
-      else if (fstat (fd, &st->stat) != 0)
-       diag = stat_diag;
+      else
+       {
+         st->fd = fd;
+         if (fstat (fd, &st->stat) != 0)
+           diag = stat_diag;
+       }
     }
   if (diag)
     {
@@ -1600,7 +1753,7 @@ dump_file0 (struct tar_stat_info *st, char const *name, char const *p)
          ensure_slash (&st->orig_file_name);
          ensure_slash (&st->file_name);
 
-         if (check_exclusion_tags (fd, &tag_file_name) == exclusion_tag_all)
+         if (check_exclusion_tags (st, &tag_file_name) == exclusion_tag_all)
            {
              exclusion_tag_warning (st->orig_file_name, tag_file_name,
                                     _("directory not dumped"));
@@ -1608,12 +1761,15 @@ dump_file0 (struct tar_stat_info *st, char const *name, char const *p)
            }
 
          ok = dump_dir (st);
+
+         fd = st->fd;
+         parentfd = top_level ? AT_FDCWD : parent->fd;
        }
       else
        {
          enum dump_status status;
 
-         if (fd != -1 && sparse_option && ST_IS_SPARSE (st->stat))
+         if (fd && sparse_option && ST_IS_SPARSE (st->stat))
            {
              status = sparse_dump_file (fd, st);
              if (status == dump_status_not_implemented)
@@ -1641,14 +1797,26 @@ dump_file0 (struct tar_stat_info *st, char const *name, char const *p)
 
       if (ok)
        {
-         if ((fd < 0
-              ? fstatat (parentfd, name, &final_stat, fstatat_flags)
-              : fstat (fd, &final_stat))
-             != 0)
+         if (fd < 0)
            {
-             file_removed_diag (p, top_level, stat_diag);
+             errno = - fd;
              ok = false;
            }
+         else if (fd == 0)
+           {
+             if (parentfd < 0 && ! top_level)
+               {
+                 errno = - parentfd;
+                 ok = false;
+               }
+             else
+               ok = fstatat (parentfd, name, &final_stat, fstatat_flags) == 0;
+           }
+         else
+           ok = fstat (fd, &final_stat) == 0;
+
+         if (! ok)
+           file_removed_diag (p, top_level, stat_diag);
        }
 
       if (ok)
@@ -1669,16 +1837,7 @@ dump_file0 (struct tar_stat_info *st, char const *name, char const *p)
            utime_error (p);
        }
 
-      if (0 < fd)
-       {
-         if (close (fd) != 0)
-           {
-             close_diag (p);
-             ok = false;
-           }
-         st->fd = 0;
-       }
-
+      ok &= tar_stat_close (st);
       if (ok && remove_files_option)
        queue_deferred_unlink (p, is_dir);
 
This page took 0.029421 seconds and 4 git commands to generate.