remove.c: allow compilation on cygwin
[platform/upstream/coreutils.git] / src / remove.c
index 5997467..9bcd6f7 100644 (file)
@@ -1,10 +1,10 @@
 /* remove.c -- core functions for removing files and directories
-   Copyright (C) 88, 90, 91, 1994-2005 Free Software Foundation, Inc.
+   Copyright (C) 88, 90, 91, 1994-2008 Free Software Foundation, Inc.
 
-   This program is free software; you can redistribute it and/or modify
+   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 2, or (at your option)
-   any later version.
+   the Free Software Foundation, either version 3 of the License, 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
@@ -12,8 +12,7 @@
    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, write to the Free Software Foundation,
-   Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.  */
+   along with this program.  If not, see <http://www.gnu.org/licenses/>.  */
 
 /* Extracted from rm.c and librarified, then rewritten by Jim Meyering.  */
 
@@ -26,7 +25,6 @@
 #include "system.h"
 #include "cycle-check.h"
 #include "dirfd.h"
-#include "dirname.h"
 #include "error.h"
 #include "euidaccess.h"
 #include "euidaccess-stat.h"
 #include "hash-pjw.h"
 #include "lstat.h"
 #include "obstack.h"
-#include "openat.h"
 #include "quote.h"
 #include "remove.h"
 #include "root-dev-ino.h"
 #include "unlinkdir.h"
+#include "write-any-file.h"
 #include "yesno.h"
 
 /* Avoid shadowing warnings because these are functions declared
 #define dir_name rm_dir_name
 #define dir_len rm_dir_len
 
-#define obstack_chunk_alloc malloc
-#define obstack_chunk_free free
-
-/* Define to the options that make open (or openat) fail to open
-   a symlink.  Define to 0 if there are no such options.
-   This is useful because it permits us to skip the `fstat'
-   and dev/ino comparison in AD_push.  */
-#if defined O_NOFOLLOW
-# define OPEN_NO_FOLLOW_SYMLINK O_NOFOLLOW
-#else
-# define OPEN_NO_FOLLOW_SYMLINK 0
-#endif
-
 /* This is the maximum number of consecutive readdir/unlink calls that
-   can be made (with no intervening rewinddir or closedir/opendir)
-   before triggering a bug that makes readdir return NULL even though
-   some directory entries have not been processed.  The bug afflicts
-   SunOS's readdir when applied to ufs file systems and Darwin 6.5's
-   (and OSX v.10.3.8's) HFS+.  This maximum is conservative in that
-   demonstrating the problem seems to require a directory containing
-   at least 254 deletable entries (which doesn't count . and ..), so
-   we could conceivably increase the maximum value to 254.  */
+   can be made (with no intervening rewinddir or closedir/opendir) before
+   triggering a bug that makes readdir return NULL even though some
+   directory entries have not been processed.  The bug afflicts SunOS's
+   readdir when applied to ufs file systems and Darwin 6.5's (and OSX
+   v.10.3.8's) HFS+.  This maximum is conservative in that demonstrating
+   the problem requires a directory containing at least 16 deletable
+   entries (which doesn't count . and ..).
+   This problem also affects Darwin 7.9.0 (aka MacOS X 10.3.9) on HFS+
+   and NFS-mounted file systems, but not vfat ones.  */
 enum
   {
-    CONSECUTIVE_READDIR_UNLINK_THRESHOLD = 200
+    CONSECUTIVE_READDIR_UNLINK_THRESHOLD = 10
   };
 
+/* If the heuristics in preprocess_dir suggest that there
+   are fewer than this many entries in a directory, then it
+   skips the preprocessing altogether.  */
 enum
-  {
-    OPENAT_CWD_RESTORE__REQUIRE = false,
-    OPENAT_CWD_RESTORE__ALLOW_FAILURE = true
-  };
+{
+  INODE_SORT_DIR_ENTRIES_THRESHOLD = 10000
+};
+
+/* FIXME: in 2009, or whenever Darwin 7.9.0 (aka MacOS X 10.3.9) is no
+   longer relevant, remove this work-around code.  Then, there will be
+   no need to perform the extra rewinddir call, ever.  */
+#define NEED_REWIND(readdir_unlink_count) \
+  (CONSECUTIVE_READDIR_UNLINK_THRESHOLD <= (readdir_unlink_count))
 
 enum Ternary
   {
@@ -122,7 +116,22 @@ struct AD_ent
   struct dev_ino dev_ino;
 };
 
-extern char *program_name;
+/* D_TYPE(D) is the type of directory entry D if known, DT_UNKNOWN
+   otherwise.  */
+#if HAVE_STRUCT_DIRENT_D_TYPE
+# define D_TYPE(d) ((d)->d_type)
+#else
+# define D_TYPE(d) DT_UNKNOWN
+
+/* Any int values will do here, so long as they're distinct.
+   Undef any existing macros out of the way.  */
+# undef DT_UNKNOWN
+# undef DT_DIR
+# undef DT_LNK
+# define DT_UNKNOWN 0
+# define DT_DIR 1
+# define DT_LNK 2
+#endif
 
 struct dirstack_state
 {
@@ -158,6 +167,52 @@ struct dirstack_state
 };
 typedef struct dirstack_state Dirstack_state;
 
+/* A static buffer and its allocated size, these variables are used by
+   xfull_filename and full_filename to form full, relative file names.  */
+static char *g_buf;
+static size_t g_n_allocated;
+
+/* Like fstatat, but cache the result.  If ST->st_size is -1, the
+   status has not been gotten yet.  If less than -1, fstatat failed
+   with errno == ST->st_ino.  Otherwise, the status has already
+   been gotten, so return 0.  */
+static int
+cache_fstatat (int fd, char const *file, struct stat *st, int flag)
+{
+  if (st->st_size == -1 && fstatat (fd, file, st, flag) != 0)
+    {
+      st->st_size = -2;
+      st->st_ino = errno;
+    }
+  if (0 <= st->st_size)
+    return 0;
+  errno = (int) st->st_ino;
+  return -1;
+}
+
+/* Initialize a fstatat cache *ST.  Return ST for convenience.  */
+static inline struct stat *
+cache_stat_init (struct stat *st)
+{
+  st->st_size = -1;
+  return st;
+}
+
+/* Return true if *ST has been statted.  */
+static inline bool
+cache_statted (struct stat *st)
+{
+  return (st->st_size != -1);
+}
+
+/* Return true if *ST has been statted successfully.  */
+static inline bool
+cache_stat_ok (struct stat *st)
+{
+  return (0 <= st->st_size);
+}
+
+
 static void
 hash_freer (void *x)
 {
@@ -170,11 +225,33 @@ hash_compare_strings (void const *x, void const *y)
   return STREQ (x, y) ? true : false;
 }
 
+/* Obstack allocator: longjump on failure.  */
+static void *
+rm_malloc (void *v_jumpbuf, long size)
+{
+  jmp_buf *jumpbuf = v_jumpbuf;
+  void *p = malloc (size);
+  if (p)
+    return p;
+  longjmp (*jumpbuf, 1);
+}
+
+/* With the 2-arg allocator, we must also provide a two-argument freer.  */
+static void
+rm_free (void *v_jumpbuf ATTRIBUTE_UNUSED, void *ptr)
+{
+  free (ptr);
+}
+
 static inline void
 push_dir (Dirstack_state *ds, const char *dir_name)
 {
   size_t len = strlen (dir_name);
 
+  /* Don't copy trailing slashes.  */
+  while (1 < len && dir_name[len - 1] == '/')
+    --len;
+
   /* Append the string onto the stack.  */
   obstack_grow (&ds->dir_stack, dir_name, len);
 
@@ -191,13 +268,15 @@ push_dir (Dirstack_state *ds, const char *dir_name)
 /* Return the entry name of the directory on the top of the stack
    in malloc'd storage.  */
 static inline char *
-top_dir (Dirstack_state const *ds)
+top_dir (Dirstack_state *ds)
 {
   size_t n_lengths = obstack_object_size (&ds->len_stack) / sizeof (size_t);
   size_t *length = obstack_base (&ds->len_stack);
   size_t top_len = length[n_lengths - 1];
   char const *p = obstack_next_free (&ds->dir_stack) - top_len;
-  char *q = xmalloc (top_len);
+  char *q = malloc (top_len);
+  if (q == NULL)
+    longjmp (ds->current_arg_jumpbuf, 1);
   memcpy (q, p, top_len - 1);
   q[top_len - 1] = 0;
   return q;
@@ -253,86 +332,112 @@ right_justify (char *dst, size_t dst_len, const char *src, size_t src_len,
   return dst_len - src_len;
 }
 
-/* Using the global directory name obstack, create the full name FILENAME.
+/* Using the global directory name obstack, create the full name of FILENAME.
    Return it in sometimes-realloc'd space that should not be freed by the
-   caller.  Realloc as necessary.  If realloc fails, use a static buffer
-   and put as long a suffix in that buffer as possible.  */
+   caller.  Realloc as necessary.  If realloc fails, return NULL.  */
 
-#define full_filename(Filename) full_filename_ (ds, Filename)
 static char *
-full_filename_ (Dirstack_state const *ds, const char *filename)
+full_filename0 (Dirstack_state const *ds, const char *filename)
 {
-  static char *buf = NULL;
-  static size_t n_allocated = 0;
-
   size_t dir_len = obstack_object_size (&ds->dir_stack);
   char *dir_name = obstack_base (&ds->dir_stack);
-  size_t n_bytes_needed;
-  size_t filename_len;
-
-  filename_len = strlen (filename);
-  n_bytes_needed = dir_len + filename_len + 1;
+  size_t filename_len = strlen (filename);
+  size_t n_bytes_needed = dir_len + filename_len + 1;
 
-  if (n_allocated < n_bytes_needed)
+  if (g_n_allocated < n_bytes_needed)
     {
-      /* This code requires that realloc accept NULL as the first arg.
-         This function must not use xrealloc.  Otherwise, an out-of-memory
-        error involving a file name to be expanded here wouldn't ever
-        be issued.  Use realloc and fall back on using a static buffer
-        if memory allocation fails.  */
-      char *new_buf = realloc (buf, n_bytes_needed);
-      n_allocated = n_bytes_needed;
-
+      char *new_buf = realloc (g_buf, n_bytes_needed);
       if (new_buf == NULL)
-       {
-#define SBUF_SIZE 512
-#define ELLIPSES_PREFIX "[...]"
-         static char static_buf[SBUF_SIZE];
-         bool truncated;
-         size_t len;
-         char *p;
-
-         free (buf);
-         len = right_justify (static_buf, SBUF_SIZE, filename,
-                              filename_len + 1, &p, &truncated);
-         right_justify (static_buf, len, dir_name, dir_len, &p, &truncated);
-         if (truncated)
-           {
-             memcpy (static_buf, ELLIPSES_PREFIX,
-                     sizeof (ELLIPSES_PREFIX) - 1);
-           }
-         return p;
-       }
+       return NULL;
 
-      buf = new_buf;
+      g_buf = new_buf;
+      g_n_allocated = n_bytes_needed;
     }
 
-  if (filename_len == 1 && *filename == '.' && dir_len)
+  if (STREQ (filename, ".") && dir_len)
     {
       /* FILENAME is just `.' and dir_len is nonzero.
         Copy the directory part, omitting the trailing slash,
         and append a trailing zero byte.  */
-      char *p = mempcpy (buf, dir_name, dir_len - 1);
+      char *p = mempcpy (g_buf, dir_name, dir_len - 1);
       *p = 0;
     }
   else
     {
       /* Copy the directory part, including trailing slash, and then
         append the filename part, including a trailing zero byte.  */
-      memcpy (mempcpy (buf, dir_name, dir_len), filename, filename_len + 1);
-      assert (strlen (buf) + 1 == n_bytes_needed);
+      memcpy (mempcpy (g_buf, dir_name, dir_len), filename, filename_len + 1);
+      assert (strlen (g_buf) + 1 == n_bytes_needed);
     }
 
+  return g_buf;
+}
+
+/* Using the global directory name obstack, create the full name of FILENAME.
+   Return it in sometimes-realloc'd space that should not be freed by the
+   caller.  Realloc as necessary.  If realloc fails, die.  */
+
+static char *
+xfull_filename (Dirstack_state const *ds, const char *filename)
+{
+  char *buf = full_filename0 (ds, filename);
+  if (buf == NULL)
+    xalloc_die ();
   return buf;
 }
 
-static size_t
+/* Using the global directory name obstack, create the full name FILENAME.
+   Return it in sometimes-realloc'd space that should not be freed by the
+   caller.  Realloc as necessary.  If realloc fails, use a static buffer
+   and put as long a suffix in that buffer as possible.  Be careful not
+   to change errno.  */
+
+#define full_filename(Filename) full_filename_ (ds, Filename)
+static char *
+full_filename_ (Dirstack_state const *ds, const char *filename)
+{
+  int saved_errno = errno;
+  char *full_name = full_filename0 (ds, filename);
+  if (full_name)
+    {
+      errno = saved_errno;
+      return full_name;
+    }
+
+  {
+#define SBUF_SIZE 512
+#define ELLIPSES_PREFIX "[...]"
+    static char static_buf[SBUF_SIZE];
+    bool file_truncated;
+    bool dir_truncated;
+    size_t n_bytes_remaining;
+    char *p;
+    char *dir_name = obstack_base (&ds->dir_stack);
+    size_t dir_len = obstack_object_size (&ds->dir_stack);
+
+    free (g_buf);
+    n_bytes_remaining = right_justify (static_buf, SBUF_SIZE, filename,
+                                      strlen (filename) + 1, &p,
+                                      &file_truncated);
+    right_justify (static_buf, n_bytes_remaining, dir_name, dir_len,
+                  &p, &dir_truncated);
+    if (file_truncated || dir_truncated)
+      {
+       memcpy (static_buf, ELLIPSES_PREFIX,
+               sizeof (ELLIPSES_PREFIX) - 1);
+      }
+    errno = saved_errno;
+    return p;
+  }
+}
+
+static inline size_t
 AD_stack_height (Dirstack_state const *ds)
 {
   return obstack_object_size (&ds->Active_dir) / sizeof (struct AD_ent);
 }
 
-static struct AD_ent *
+static inline struct AD_ent *
 AD_stack_top (Dirstack_state const *ds)
 {
   return (struct AD_ent *)
@@ -351,14 +456,41 @@ AD_stack_pop (Dirstack_state *ds)
   obstack_blank (&ds->Active_dir, -(int) sizeof (struct AD_ent));
 }
 
-static Dirstack_state *
-ds_init (void)
+static void
+AD_stack_clear (Dirstack_state *ds)
+{
+  while (0 < AD_stack_height (ds))
+    {
+      AD_stack_pop (ds);
+    }
+}
+
+/* Initialize obstack O just enough so that it may be freed
+   with obstack_free.  */
+static void
+obstack_init_minimal (struct obstack *o)
 {
-  Dirstack_state *ds = xmalloc (sizeof *ds);
-  obstack_init (&ds->dir_stack);
-  obstack_init (&ds->len_stack);
-  obstack_init (&ds->Active_dir);
-  return ds;
+  o->chunk = NULL;
+}
+
+static void
+ds_init (Dirstack_state *ds)
+{
+  unsigned int i;
+  struct obstack *o[3];
+  o[0] = &ds->dir_stack;
+  o[1] = &ds->len_stack;
+  o[2] = &ds->Active_dir;
+
+  /* Ensure each of these is NULL, in case init/allocation
+     fails and we end up calling ds_free on all three while only
+     one or two has been initialized.  */
+  for (i = 0; i < 3; i++)
+    obstack_init_minimal (o[i]);
+
+  for (i = 0; i < 3; i++)
+    obstack_specify_allocation_with_arg
+      (o[i], 0, 0, rm_malloc, rm_free, &ds->current_arg_jumpbuf);
 }
 
 static void
@@ -377,37 +509,58 @@ ds_free (Dirstack_state *ds)
   obstack_free (&ds->dir_stack, NULL);
   obstack_free (&ds->len_stack, NULL);
   obstack_free (&ds->Active_dir, NULL);
-  free (ds);
 }
 
-/* Pop the active directory (AD) stack and move *DIRP `up' one level,
+/* Pop the active directory (AD) stack and prepare to move `up' one level,
    safely.  Moving `up' usually means opening `..', but when we've just
    finished recursively processing a command-line directory argument,
-   there's nothing left on the stack, so set *DIRP to NULL in that case.
-   The idea is to return with *DIRP opened on the parent directory,
+   there's nothing left on the stack, so set *FDP to AT_FDCWD in that case.
+   The idea is to return with *FDP opened on the parent directory,
    assuming there are entries in that directory that we need to remove.
 
+   Note that we must not call opendir (or fdopendir) just yet, since
+   the caller must first remove the directory we're coming from.
+   That is because some file system implementations cache readdir
+   results at opendir time; so calling opendir, rmdir, readdir would
+   return an entry for the just-removed directory.
+
    Whenever using chdir '..' (virtually, now, via openat), verify
    that the post-chdir dev/ino numbers for `.' match the saved ones.
-   If any system call fails or if dev/ino don't match then give a
+   If any system call fails or if dev/ino don't match, then give a
    diagnostic and longjump out.
-   Set *PREV_DIR to the name (in malloc'd storage) of the
+   Return the name (in malloc'd storage) of the
    directory (usually now empty) from which we're coming, and which
-   corresponds to the input value of *DIRP.  */
-static void
-AD_pop_and_chdir (DIR **dirp, Dirstack_state *ds, char **prev_dir)
+   corresponds to the input value of DIRP.
+
+   Finally, note that while this function's name is no longer as
+   accurate as it once was (it no longer calls chdir), it does open
+   the destination directory.  */
+static char *
+AD_pop_and_chdir (DIR *dirp, int *fdp, Dirstack_state *ds)
 {
-  enum RM_status old_status = AD_stack_top(ds)->status;
+  struct AD_ent *leaf_dir_ent = AD_stack_top(ds);
+  struct dev_ino leaf_dev_ino = leaf_dir_ent->dev_ino;
+  enum RM_status old_status = leaf_dir_ent->status;
   struct AD_ent *top;
 
   /* Get the name of the current (but soon to be `previous') directory
      from the top of the stack.  */
-  *prev_dir = top_dir (ds);
+  char *prev_dir = top_dir (ds);
 
   AD_stack_pop (ds);
   pop_dir (ds);
   top = AD_stack_top (ds);
 
+  /* If the directory we're about to leave (and try to rmdir)
+     is the one whose dev_ino is being used to detect a cycle,
+     reset cycle_check_state.dev_ino to that of the parent.
+     Otherwise, once that directory is removed, its dev_ino
+     could be reused in the creation (by some other process)
+     of a directory that this rm process would encounter,
+     which would result in a false-positive cycle indication.  */
+  CYCLE_CHECK_REFLECT_CHDIR_UP (&ds->cycle_check_state,
+                               top->dev_ino, leaf_dev_ino);
+
   /* Propagate any failure to parent.  */
   UPDATE_STATUS (top->status, old_status);
 
@@ -416,25 +569,25 @@ AD_pop_and_chdir (DIR **dirp, Dirstack_state *ds, char **prev_dir)
   if (1 < AD_stack_height (ds))
     {
       struct stat sb;
-      int fd = openat (dirfd (*dirp), "..", O_RDONLY);
-      if (CLOSEDIR (*dirp) != 0)
+      int fd = openat (dirfd (dirp), "..", O_RDONLY);
+      if (closedir (dirp) != 0)
        {
          error (0, errno, _("FATAL: failed to close directory %s"),
-                quote (full_filename (*prev_dir)));
-         longjmp (ds->current_arg_jumpbuf, 1);
+                quote (full_filename (prev_dir)));
+         goto next_cmdline_arg;
        }
 
-      /* The above fails with EACCES when *DIRP is readable but not
+      /* The above fails with EACCES when DIRP is readable but not
         searchable, when using Solaris' openat.  Without this openat
         call, tests/rm2 would fail to remove directories a/2 and a/3.  */
       if (fd < 0)
-       fd = openat (AT_FDCWD, full_filename ("."), O_RDONLY);
+       fd = openat (AT_FDCWD, xfull_filename (ds, "."), O_RDONLY);
 
       if (fd < 0)
        {
          error (0, errno, _("FATAL: cannot open .. from %s"),
-                quote (full_filename (*prev_dir)));
-         longjmp (ds->current_arg_jumpbuf, 1);
+                quote (full_filename (prev_dir)));
+         goto next_cmdline_arg;
        }
 
       if (fstat (fd, &sb))
@@ -442,8 +595,7 @@ AD_pop_and_chdir (DIR **dirp, Dirstack_state *ds, char **prev_dir)
          error (0, errno,
                 _("FATAL: cannot ensure %s (returned to via ..) is safe"),
                 quote (full_filename (".")));
-         close (fd);
-         longjmp (ds->current_arg_jumpbuf, 1);
+         goto close_and_next;
        }
 
       /*  Ensure that post-chdir dev/ino match the stored ones.  */
@@ -451,35 +603,32 @@ AD_pop_and_chdir (DIR **dirp, Dirstack_state *ds, char **prev_dir)
        {
          error (0, 0, _("FATAL: directory %s changed dev/ino"),
                 quote (full_filename (".")));
+       close_and_next:;
          close (fd);
-         longjmp (ds->current_arg_jumpbuf, 1);
-       }
 
-      *dirp = fdopendir (fd);
-      if (*dirp == NULL)
-       {
-         error (0, errno, _("FATAL: cannot return to .. from %s"),
-                quote (full_filename (".")));
-         close (fd);
+       next_cmdline_arg:;
+         free (prev_dir);
          longjmp (ds->current_arg_jumpbuf, 1);
        }
+      *fdp = fd;
     }
   else
     {
-      if (CLOSEDIR (*dirp) != 0)
+      if (closedir (dirp) != 0)
        {
          error (0, errno, _("FATAL: failed to close directory %s"),
-                quote (full_filename (*prev_dir)));
-         longjmp (ds->current_arg_jumpbuf, 1);
+                quote (full_filename (prev_dir)));
+         goto next_cmdline_arg;
        }
-      *dirp = NULL;
+      *fdp = AT_FDCWD;
     }
+
+  return prev_dir;
 }
 
-/* Initialize *HT if it is NULL.
-   Insert FILENAME into HT.  */
-static void
-AD_mark_helper (Hash_table **ht, char const *filename)
+/* Initialize *HT if it is NULL.  Return *HT.  */
+static Hash_table *
+AD_ensure_initialized (Hash_table **ht)
 {
   if (*ht == NULL)
     {
@@ -488,8 +637,23 @@ AD_mark_helper (Hash_table **ht, char const *filename)
       if (*ht == NULL)
        xalloc_die ();
     }
-  if (! hash_insert (*ht, filename))
+
+  return *ht;
+}
+
+/* Initialize *HT if it is NULL.
+   Insert FILENAME into HT.  */
+static void
+AD_mark_helper (Hash_table **ht, char *filename)
+{
+  void *ent = hash_insert (AD_ensure_initialized (ht), filename);
+  if (ent == NULL)
     xalloc_die ();
+  else
+    {
+      if (ent != filename)
+       free (filename);
+    }
 }
 
 /* Mark FILENAME (in current directory) as unremovable.  */
@@ -507,7 +671,7 @@ static void
 AD_mark_current_as_unremovable (Dirstack_state *ds)
 {
   struct AD_ent *top = AD_stack_top (ds);
-  char const *curr = top_dir (ds);
+  char *curr = top_dir (ds);
 
   assert (1 < AD_stack_height (ds));
 
@@ -553,7 +717,7 @@ AD_push (int fd_cwd, Dirstack_state *ds, char const *dir,
 
   /* If our uses of openat are guaranteed not to
      follow a symlink, then we can skip this check.  */
-  if ( ! OPEN_NO_FOLLOW_SYMLINK)
+  if (! HAVE_WORKING_O_NOFOLLOW)
     {
       struct stat sb;
       if (fstat (fd_cwd, &sb) != 0)
@@ -572,13 +736,23 @@ AD_push (int fd_cwd, Dirstack_state *ds, char const *dir,
        }
     }
 
+  if (cycle_check (&ds->cycle_check_state, dir_sb_from_parent))
+    {
+      error (0, 0, _("\
+WARNING: Circular directory structure.\n\
+This almost certainly means that you have a corrupted file system.\n\
+NOTIFY YOUR SYSTEM MANAGER.\n\
+The following directory is part of the cycle:\n  %s\n"),
+            quote (full_filename (".")));
+      longjmp (ds->current_arg_jumpbuf, 1);
+    }
+
   /* Extend the stack.  */
   obstack_blank (&ds->Active_dir, sizeof (struct AD_ent));
 
-  {
-    size_t n_lengths = obstack_object_size (&ds->len_stack) / sizeof (size_t);
-    assert (AD_stack_height (ds) == n_lengths + 1);
-  }
+  /* The active directory stack must be one larger than the length stack.  */
+  assert (AD_stack_height (ds) ==
+         1 + obstack_object_size (&ds->len_stack) / sizeof (size_t));
 
   /* Fill in the new values.  */
   top = AD_stack_top (ds);
@@ -587,58 +761,30 @@ AD_push (int fd_cwd, Dirstack_state *ds, char const *dir,
   top->unremovable = NULL;
 }
 
-static bool
+static inline bool
 AD_is_removable (Dirstack_state const *ds, char const *file)
 {
   struct AD_ent *top = AD_stack_top (ds);
   return ! (top->unremovable && hash_lookup (top->unremovable, file));
 }
 
-/* Return true if DIR is determined to be an empty directory
-   or if fdopendir or readdir fails.  */
-static bool
-is_empty_dir (int fd_cwd, char const *dir)
-{
-  DIR *dirp;
-  struct dirent const *dp;
-  int saved_errno;
-  int fd = openat (fd_cwd, dir, O_RDONLY);
-
-  if (fd < 0)
-    return false;
-
-  dirp = fdopendir (fd);
-  if (dirp == NULL)
-    {
-      close (fd);
-      return false;
-    }
-
-  errno = 0;
-  dp = readdir_ignoring_dot_and_dotdot (dirp);
-  saved_errno = errno;
-  closedir (dirp);
-  if (dp != NULL)
-    return false;
-  return saved_errno == 0 ? true : false;
-}
-
-/* Return true if FILE is determined to be an unwritable non-symlink.
-   Otherwise, return false (including when lstat'ing it fails).
-   If lstat (aka fstatat) succeeds, set *BUF_P to BUF.
+/* Return 1 if FILE is an unwritable non-symlink,
+   0 if it is writable or some other type of file,
+   -1 and set errno if there is some problem in determining the answer.
+   Set *BUF to the file status.
    This is to avoid calling euidaccess when FILE is a symlink.  */
-static bool
+static int
 write_protected_non_symlink (int fd_cwd,
                             char const *file,
                             Dirstack_state const *ds,
-                            struct stat **buf_p,
                             struct stat *buf)
 {
-  if (fstatat (fd_cwd, file, buf, AT_SYMLINK_NOFOLLOW) != 0)
-    return false;
-  *buf_p = buf;
+  if (can_write_any_file ())
+    return 0;
+  if (cache_fstatat (fd_cwd, file, buf, AT_SYMLINK_NOFOLLOW) != 0)
+    return -1;
   if (S_ISLNK (buf->st_mode))
-    return false;
+    return 0;
   /* Here, we know FILE is not a symbolic link.  */
 
   /* In order to be reentrant -- i.e., to avoid changing the working
@@ -658,13 +804,13 @@ write_protected_non_symlink (int fd_cwd,
        Disadvantage: changes working directory (not reentrant) and can't
        work if save_cwd fails.
 
-     3) if (euidaccess (full_filename (file), W_OK) == 0)
-       Disadvantage: doesn't work if full_filename is too long.
+     3) if (euidaccess (xfull_filename (file), W_OK) == 0)
+       Disadvantage: doesn't work if xfull_filename is too long.
        Inefficient for very deep trees (O(Depth^2)).
 
      4) If the full pathname is sufficiently short (say, less than
        PATH_MAX or 8192 bytes, whichever is shorter):
-       use method (3) (i.e., euidaccess (full_filename (file), W_OK));
+       use method (3) (i.e., euidaccess (xfull_filename (file), W_OK));
        Otherwise: vfork, fchdir in the child, run euidaccess in the
        child, then the child exits with a status that tells the parent
        whether euidaccess succeeded.
@@ -678,7 +824,7 @@ write_protected_non_symlink (int fd_cwd,
 
      5) If the full file name is sufficiently short (say, less than
        PATH_MAX or 8192 bytes, whichever is shorter):
-       use method (3) (i.e., euidaccess (full_filename (file), W_OK));
+       use method (3) (i.e., euidaccess (xfull_filename (file), W_OK));
        Otherwise: look just at the file bits.  Perhaps issue a warning
        the first time this occurs.
 
@@ -692,9 +838,19 @@ write_protected_non_symlink (int fd_cwd,
     size_t file_name_len
       = obstack_object_size (&ds->dir_stack) + strlen (file);
 
-    return (file_name_len < MIN (PATH_MAX, 8192)
-           ? euidaccess (full_filename (file), W_OK) != 0 && errno == EACCES
-           : euidaccess_stat (buf, W_OK) != 0);
+    if (MIN (PATH_MAX, 8192) <= file_name_len)
+      return ! euidaccess_stat (buf, W_OK);
+    if (euidaccess (xfull_filename (ds, file), W_OK) == 0)
+      return 0;
+    if (errno == EACCES)
+      {
+       errno = 0;
+       return 1;
+      }
+
+    /* Perhaps some other process has removed the file, or perhaps this
+       is a buggy NFS client.  */
+    return -1;
   }
 }
 
@@ -704,121 +860,152 @@ write_protected_non_symlink (int fd_cwd,
    return RM_USER_DECLINED.  If not ignoring missing files and we
    cannot lstat FILENAME, then return RM_ERROR.
 
+   *PDIRENT_TYPE is the type of the directory entry; update it to DT_DIR
+   or DT_LNK as needed.  *SBUF is the file's status.
+
    Depending on MODE, ask whether to `descend into' or to `remove' the
    directory FILENAME.  MODE is ignored when FILENAME is not a directory.
    Set *IS_EMPTY to T_YES if FILENAME is an empty directory, and it is
    appropriate to try to remove it with rmdir (e.g. recursive mode).
-   Don't even try to set *IS_EMPTY when MODE == PA_REMOVE_DIR.
-   Set *IS_DIR to T_YES or T_NO if we happen to determine whether
-   FILENAME is a directory.  */
+   Don't even try to set *IS_EMPTY when MODE == PA_REMOVE_DIR.  */
 static enum RM_status
 prompt (int fd_cwd, Dirstack_state const *ds, char const *filename,
+       int *pdirent_type, struct stat *sbuf,
        struct rm_options const *x, enum Prompt_action mode,
-       Ternary *is_dir, Ternary *is_empty)
+       Ternary *is_empty)
 {
-  bool write_protected = false;
-  struct stat *sbuf = NULL;
-  struct stat buf;
+  int write_protected = 0;
+  int dirent_type = *pdirent_type;
 
   *is_empty = T_UNKNOWN;
-  *is_dir = T_UNKNOWN;
 
-  if (((!x->ignore_missing_files & (x->interactive | x->stdin_tty))
-       && (write_protected = write_protected_non_symlink (fd_cwd, filename,
-                                                         ds, &sbuf, &buf)))
-      || x->interactive)
+  if (x->interactive == RMI_NEVER)
+    return RM_OK;
+
+  int wp_errno = 0;
+
+  if (!x->ignore_missing_files
+      && ((x->interactive == RMI_ALWAYS) || x->stdin_tty)
+      && dirent_type != DT_LNK)
+    {
+      write_protected = write_protected_non_symlink (fd_cwd, filename, ds, sbuf);
+      wp_errno = errno;
+    }
+
+  if (write_protected || x->interactive == RMI_ALWAYS)
     {
-      if (sbuf == NULL)
+      if (0 <= write_protected && dirent_type == DT_UNKNOWN)
        {
-         sbuf = &buf;
-         if (fstatat (fd_cwd, filename, sbuf, AT_SYMLINK_NOFOLLOW))
+         if (cache_fstatat (fd_cwd, filename, sbuf, AT_SYMLINK_NOFOLLOW) == 0)
            {
-             /* lstat failed.  This happens e.g., with `rm '''.  */
-             error (0, errno, _("cannot remove %s"),
-                    quote (full_filename (filename)));
-             return RM_ERROR;
+             if (S_ISLNK (sbuf->st_mode))
+               dirent_type = DT_LNK;
+             else if (S_ISDIR (sbuf->st_mode))
+               dirent_type = DT_DIR;
+             /* Otherwise it doesn't matter, so leave it DT_UNKNOWN.  */
+             *pdirent_type = dirent_type;
+           }
+         else
+           {
+             /* This happens, e.g., with `rm '''.  */
+             write_protected = -1;
+             wp_errno = errno;
            }
        }
 
-      if (S_ISDIR (sbuf->st_mode) && !x->recursive)
+      if (0 <= write_protected)
+       switch (dirent_type)
+         {
+         case DT_LNK:
+           /* Using permissions doesn't make sense for symlinks.  */
+           if (x->interactive != RMI_ALWAYS)
+             return RM_OK;
+           break;
+
+         case DT_DIR:
+           if (!x->recursive)
+             {
+               write_protected = -1;
+               wp_errno = EISDIR;
+             }
+           break;
+         }
+
+      char const *quoted_name = quote (full_filename (filename));
+
+      if (write_protected < 0)
        {
-         error (0, EISDIR, _("cannot remove directory %s"),
-                quote (full_filename (filename)));
+         error (0, wp_errno, _("cannot remove %s"), quoted_name);
          return RM_ERROR;
        }
 
-      /* Using permissions doesn't make sense for symlinks.  */
-      if (S_ISLNK (sbuf->st_mode))
+      /* Issue the prompt.  */
+      /* FIXME: use a variant of error (instead of fprintf) that doesn't
+        append a newline.  Then we won't have to declare program_name in
+        this file.  */
+      if (dirent_type == DT_DIR
+         && mode == PA_DESCEND_INTO_DIR
+         && ((*is_empty = (is_empty_dir (fd_cwd, filename) ? T_YES : T_NO))
+             == T_NO))
+       fprintf (stderr,
+                (write_protected
+                 ? _("%s: descend into write-protected directory %s? ")
+                 : _("%s: descend into directory %s? ")),
+                program_name, quoted_name);
+      else
        {
-         if ( ! x->interactive)
-           return RM_OK;
-         write_protected = false;
-       }
+         if (cache_fstatat (fd_cwd, filename, sbuf, AT_SYMLINK_NOFOLLOW) != 0)
+           {
+             error (0, errno, _("cannot remove %s"), quoted_name);
+             return RM_ERROR;
+           }
 
-      /* Issue the prompt.  */
-      {
-       char const *quoted_name = quote (full_filename (filename));
-
-       *is_dir = (S_ISDIR (sbuf->st_mode) ? T_YES : T_NO);
-
-       /* FIXME: use a variant of error (instead of fprintf) that doesn't
-          append a newline.  Then we won't have to declare program_name in
-          this file.  */
-       if (S_ISDIR (sbuf->st_mode)
-           && x->recursive
-           && mode == PA_DESCEND_INTO_DIR
-           && ((*is_empty = (is_empty_dir (fd_cwd, filename) ? T_YES : T_NO))
-               == T_NO))
          fprintf (stderr,
                   (write_protected
-                   ? _("%s: descend into write-protected directory %s? ")
-                   : _("%s: descend into directory %s? ")),
-                  program_name, quoted_name);
-       else
-         {
-           /* TRANSLATORS: You may find it more convenient to translate
-              the equivalent of _("%s: remove %s (write-protected) %s? ").
-              It should avoid grammatical problems with the output
-              of file_type.  */
-           fprintf (stderr,
-                    (write_protected
-                     ? _("%s: remove write-protected %s %s? ")
-                     : _("%s: remove %s %s? ")),
-                    program_name, file_type (sbuf), quoted_name);
-         }
+                   /* TRANSLATORS: You may find it more convenient to
+                      translate "%s: remove %s (write-protected) %s? "
+                      instead.  It should avoid grammatical problems
+                      with the output of file_type.  */
+                   ? _("%s: remove write-protected %s %s? ")
+                   : _("%s: remove %s %s? ")),
+                  program_name, file_type (sbuf), quoted_name);
+       }
 
-       if (!yesno ())
-         return RM_USER_DECLINED;
-      }
+      if (!yesno ())
+       return RM_USER_DECLINED;
     }
   return RM_OK;
 }
 
 /* Return true if FILENAME is a directory (and not a symlink to a directory).
    Otherwise, including the case in which lstat fails, return false.
+   *ST is FILENAME's tstatus.
    Do not modify errno.  */
 static inline bool
-is_dir_lstat (char const *filename)
+is_dir_lstat (int fd_cwd, char const *filename, struct stat *st)
 {
-  struct stat sbuf;
   int saved_errno = errno;
-  bool is_dir = lstat (filename, &sbuf) == 0 && S_ISDIR (sbuf.st_mode);
+  bool is_dir =
+    (cache_fstatat (fd_cwd, filename, st, AT_SYMLINK_NOFOLLOW) == 0
+     && S_ISDIR (st->st_mode));
   errno = saved_errno;
   return is_dir;
 }
 
-#if HAVE_STRUCT_DIRENT_D_TYPE
-
-/* True if the type of the directory entry D is known.  */
-# define DT_IS_KNOWN(d) ((d)->d_type != DT_UNKNOWN)
-
-/* True if the type of the directory entry D must be T.  */
-# define DT_MUST_BE(d, t) ((d)->d_type == (t))
-
-#else
-# define DT_IS_KNOWN(d) false
-# define DT_MUST_BE(d, t) false
-#endif
+/* Return true if FILENAME is a non-directory.
+   Otherwise, including the case in which lstat fails, return false.
+   *ST is FILENAME's tstatus.
+   Do not modify errno.  */
+static inline bool
+is_nondir_lstat (int fd_cwd, char const *filename, struct stat *st)
+{
+  int saved_errno = errno;
+  bool is_non_dir =
+    (cache_fstatat (fd_cwd, filename, st, AT_SYMLINK_NOFOLLOW) == 0
+     && !S_ISDIR (st->st_mode));
+  errno = saved_errno;
+  return is_non_dir;
+}
 
 #define DO_UNLINK(Fd_cwd, Filename, X)                                 \
   do                                                                   \
@@ -830,7 +1017,7 @@ is_dir_lstat (char const *filename)
          return RM_OK;                                                 \
        }                                                               \
                                                                        \
-      if (errno == ENOENT && (X)->ignore_missing_files)                        \
+      if (ignorable_missing (X, errno))                                        \
        return RM_OK;                                                   \
     }                                                                  \
   while (0)
@@ -846,7 +1033,7 @@ is_dir_lstat (char const *filename)
          return RM_OK;                                 \
        }                                               \
                                                        \
-      if (errno == ENOENT && (X)->ignore_missing_files)        \
+      if (ignorable_missing (X, errno))                        \
        return RM_OK;                                   \
                                                        \
       if (errno == ENOTEMPTY || errno == EEXIST)       \
@@ -854,19 +1041,49 @@ is_dir_lstat (char const *filename)
     }                                                  \
   while (0)
 
+/* When a function like unlink, rmdir, or fstatat fails with an errno
+   value of ERRNUM, return true if the specified file system object
+   is guaranteed not to exist;  otherwise, return false.  */
+static inline bool
+nonexistent_file_errno (int errnum)
+{
+  /* Do not include ELOOP here, since the specified file may indeed
+     exist, but be (in)accessible only via too long a symlink chain.
+     Likewise for ENAMETOOLONG, since rm -f ./././.../foo may fail
+     if the "..." part expands to a long enough sequence of "./"s,
+     even though ./foo does indeed exist.  */
+
+  switch (errnum)
+    {
+    case ENOENT:
+    case ENOTDIR:
+      return true;
+    default:
+      return false;
+    }
+}
+
+/* Encapsulate the test for whether the errno value, ERRNUM, is ignorable.  */
+static inline bool
+ignorable_missing (struct rm_options const *x, int errnum)
+{
+  return x->ignore_missing_files && nonexistent_file_errno (errnum);
+}
+
 /* Remove the file or directory specified by FILENAME.
    Return RM_OK if it is removed, and RM_ERROR or RM_USER_DECLINED if not.
    But if FILENAME specifies a non-empty directory, return RM_NONEMPTY_DIR. */
 
 static enum RM_status
 remove_entry (int fd_cwd, Dirstack_state const *ds, char const *filename,
-             struct rm_options const *x, struct dirent const *dp)
+             int dirent_type_arg, struct stat *st,
+             struct rm_options const *x)
 {
-  Ternary is_dir;
   Ternary is_empty_directory;
-  enum RM_status s = prompt (fd_cwd, ds, filename, x, PA_DESCEND_INTO_DIR,
-                            &is_dir, &is_empty_directory);
-
+  enum RM_status s = prompt (fd_cwd, ds, filename, &dirent_type_arg, st, x,
+                            PA_DESCEND_INTO_DIR,
+                            &is_empty_directory);
+  int dirent_type = dirent_type_arg;
   if (s != RM_OK)
     return s;
 
@@ -886,9 +1103,9 @@ remove_entry (int fd_cwd, Dirstack_state const *ds, char const *filename,
 
   if (cannot_unlink_dir ())
     {
-      if (is_dir == T_YES && ! x->recursive)
+      if (dirent_type == DT_DIR && ! x->recursive)
        {
-         error (0, EISDIR, _("cannot remove directory %s"),
+         error (0, EISDIR, _("cannot remove %s"),
                 quote (full_filename (filename)));
          return RM_ERROR;
        }
@@ -902,10 +1119,11 @@ remove_entry (int fd_cwd, Dirstack_state const *ds, char const *filename,
 
       /* If we happen to know that FILENAME is a directory, return now
         and let the caller remove it -- this saves the overhead of a failed
-        unlink call.  If FILENAME is a command-line argument, then dp is NULL,
-        so we'll first try to unlink it.  Using unlink here is ok, because it
-        cannot remove a directory.  */
-      if ((dp && DT_MUST_BE (dp, DT_DIR)) || is_dir == T_YES)
+        unlink call.  If FILENAME is a command-line argument, then
+        DIRENT_TYPE is DT_UNKNOWN so we'll first try to unlink it.
+        Using unlink here is ok, because it cannot remove a
+        directory.  */
+      if (dirent_type == DT_DIR)
        return RM_NONEMPTY_DIR;
 
       DO_UNLINK (fd_cwd, filename, x);
@@ -913,46 +1131,50 @@ remove_entry (int fd_cwd, Dirstack_state const *ds, char const *filename,
       /* Upon a failed attempt to unlink a directory, most non-Linux systems
         set errno to the POSIX-required value EPERM.  In that case, change
         errno to EISDIR so that we emit a better diagnostic.  */
-      if (! x->recursive && errno == EPERM && is_dir_lstat (filename))
+      if (! x->recursive && errno == EPERM && is_dir_lstat (fd_cwd,
+                                                           filename, st))
        errno = EISDIR;
 
       if (! x->recursive
-         || errno == ENOENT || errno == ENOTDIR
-         || errno == ELOOP || errno == ENAMETOOLONG)
+         || (cache_stat_ok (st) && !S_ISDIR (st->st_mode))
+         || ((errno == EACCES || errno == EPERM)
+             && is_nondir_lstat (fd_cwd, filename, st))
+         )
        {
+         if (ignorable_missing (x, errno))
+           return RM_OK;
+
          /* Either --recursive is not in effect, or the file cannot be a
             directory.  Report the unlink problem and fail.  */
          error (0, errno, _("cannot remove %s"),
                 quote (full_filename (filename)));
          return RM_ERROR;
        }
+      assert (!cache_stat_ok (st) || S_ISDIR (st->st_mode));
     }
   else
     {
-      /* If we don't already know whether FILENAME is a directory, find out now.
-        Then, if it's a non-directory, we can use unlink on it.  */
-      if (is_dir == T_UNKNOWN)
+      /* If we don't already know whether FILENAME is a directory,
+        find out now.  Then, if it's a non-directory, we can use
+        unlink on it.  */
+
+      if (dirent_type == DT_UNKNOWN)
        {
-         if (dp && DT_IS_KNOWN (dp))
-           is_dir = DT_MUST_BE (dp, DT_DIR) ? T_YES : T_NO;
-         else
+         if (fstatat (fd_cwd, filename, st, AT_SYMLINK_NOFOLLOW))
            {
-             struct stat sbuf;
-             if (fstatat (fd_cwd, filename, &sbuf, AT_SYMLINK_NOFOLLOW))
-               {
-                 if (errno == ENOENT && x->ignore_missing_files)
-                   return RM_OK;
-
-                 error (0, errno, _("cannot remove %s"),
-                        quote (full_filename (filename)));
-                 return RM_ERROR;
-               }
-
-             is_dir = S_ISDIR (sbuf.st_mode) ? T_YES : T_NO;
+             if (ignorable_missing (x, errno))
+               return RM_OK;
+
+             error (0, errno, _("cannot remove %s"),
+                    quote (full_filename (filename)));
+             return RM_ERROR;
            }
+
+         if (S_ISDIR (st->st_mode))
+           dirent_type = DT_DIR;
        }
 
-      if (is_dir == T_NO)
+      if (dirent_type != DT_DIR)
        {
          /* At this point, barring race conditions, FILENAME is known
             to be a non-directory, so it's ok to try to unlink it.  */
@@ -966,7 +1188,7 @@ remove_entry (int fd_cwd, Dirstack_state const *ds, char const *filename,
 
       if (! x->recursive)
        {
-         error (0, EISDIR, _("cannot remove directory %s"),
+         error (0, EISDIR, _("cannot remove %s"),
                 quote (full_filename (filename)));
          return RM_ERROR;
        }
@@ -991,58 +1213,223 @@ remove_entry (int fd_cwd, Dirstack_state const *ds, char const *filename,
    unlink- or rmdir-like system call -- use that value instead of ENOTDIR
    if an opened file turns out not to be a directory.  This is important
    when the preceding non-dir-unlink failed due to e.g., EPERM or EACCES.
-   The caller must set OPENAT_CWD_RESTORE_ALLOW_FAILURE to true the first
+   The caller must use a nonnnull CWD_ERRNO the first
    time this function is called for each command-line-specified directory.
-   Set *CWD_RESTORE_FAILED if OPENAT_CWD_RESTORE_ALLOW_FAILURE is true
-   and openat_ro fails to restore the initial working directory.
-   CWD_RESTORE_FAILED may be NULL.  */
+   If CWD_ERRNO is not null, set *CWD_ERRNO to the appropriate error number
+   if this function fails to restore the initial working directory.
+   If it is null, report an error and exit if the working directory
+   isn't restored.  */
 static DIR *
 fd_to_subdirp (int fd_cwd, char const *f,
-              struct rm_options const *x, int prev_errno,
-              struct stat *subdir_sb, Dirstack_state *ds,
-              bool openat_cwd_restore_allow_failure,
-              bool *cwd_restore_failed)
+              int prev_errno,
+              struct stat *subdir_sb,
+              int *cwd_errno ATTRIBUTE_UNUSED)
 {
-  int fd_sub;
-  bool dummy;
+  int open_flags = O_RDONLY | O_NOCTTY | O_NOFOLLOW | O_NONBLOCK;
+  int fd_sub = openat_permissive (fd_cwd, f, open_flags, 0, cwd_errno);
+  int saved_errno;
 
   /* Record dev/ino of F.  We may compare them against saved values
      to thwart any attempt to subvert the traversal.  They are also used
      to detect directory cycles.  */
-  if ((fd_sub = (openat_cwd_restore_allow_failure
-                ? openat_ro (fd_cwd, f, O_RDONLY | OPEN_NO_FOLLOW_SYMLINK,
-                             (cwd_restore_failed
-                              ? cwd_restore_failed : &dummy))
-                : openat (fd_cwd, f, O_RDONLY | OPEN_NO_FOLLOW_SYMLINK))) < 0
-      || fstat (fd_sub, subdir_sb) != 0)
+  if (fd_sub < 0)
+    return NULL;
+  else if (fstat (fd_sub, subdir_sb) != 0)
+    saved_errno = errno;
+  else if (S_ISDIR (subdir_sb->st_mode))
     {
-      if (errno != ENOENT || !x->ignore_missing_files)
-       error (0, errno,
-              _("cannot remove %s"), quote (full_filename (f)));
-      if (0 <= fd_sub)
-       close (fd_sub);
-      return NULL;
+      DIR *subdir_dirp = fdopendir (fd_sub);
+      if (subdir_dirp)
+       return subdir_dirp;
+      saved_errno = errno;
     }
+  else
+    saved_errno = (prev_errno ? prev_errno : ENOTDIR);
+
+  close (fd_sub);
+  errno = saved_errno;
+  return NULL;
+}
+
+struct readdir_data
+{
+  ino_t ino;
+  char name[FLEXIBLE_ARRAY_MEMBER];
+};
+
+#if HAVE_STRUCT_DIRENT_D_TYPE
+/* A comparison function to sort on increasing inode number.  */
+static int
+compare_ino (void const *av, void const *bv)
+{
+  struct readdir_data const *const *a = av;
+  struct readdir_data const *const *b = bv;
+  return (a[0]->ino < b[0]->ino ? -1
+         : b[0]->ino < a[0]->ino ? 1 : 0);
+}
+
+/* Return an approximation of the maximum number of dirent entries
+   in a directory with stat data *ST.  */
+static size_t
+dirent_count (struct stat const *st)
+{
+  return st->st_size / 16;
+}
 
-  if (! S_ISDIR (subdir_sb->st_mode))
+# if HAVE_SYS_VFS_H && HAVE_FSTATFS && HAVE_STRUCT_STATFS_F_TYPE
+#  include <sys/vfs.h>
+#  include "fs.h"
+
+/* Return false if it is easy to determine the file system type of
+   the directory on which DIR_FD is open, and sorting dirents on
+   inode numbers is known not to improve traversal performance with
+   that type of file system.  Otherwise, return true.  */
+static bool
+dirent_inode_sort_may_be_useful (int dir_fd)
+{
+  /* Skip the sort only if we can determine efficiently
+     that skipping it is the right thing to do.
+     The cost of performing an unnecessary sort is negligible,
+     while the cost of *not* performing it can be O(N^2) with
+     a very large constant.  */
+  struct statfs fs_buf;
+
+  /* If fstatfs fails, assume sorting would be useful.  */
+  if (fstatfs (dir_fd, &fs_buf) != 0)
+    return true;
+
+  /* FIXME: what about when f_type is not an integral type?
+     deal with that if/when it's encountered.  */
+  switch (fs_buf.f_type)
     {
-      error (0, prev_errno ? prev_errno : ENOTDIR, _("cannot remove %s"),
-            quote (full_filename (f)));
-      close (fd_sub);
-      return NULL;
+    case S_MAGIC_TMPFS:
+    case S_MAGIC_NFS:
+      /* On a file system of any of these types, sorting
+        is unnecessary, and hence wasteful.  */
+      return false;
+
+    default:
+      return true;
     }
+}
+# else /* !HAVE_STRUCT_STATFS_F_TYPE */
+static bool dirent_inode_sort_may_be_useful (int dir_fd) { return true; }
+# endif /* !HAVE_STRUCT_STATFS_F_TYPE */
+#endif /* HAVE_STRUCT_DIRENT_D_TYPE */
+
+/* When a directory contains very many entries, operating on N entries in
+   readdir order can be very seek-intensive (be it to unlink or even to
+   merely stat each entry), to the point that it results in O(N^2) work.
+   This is file-system-specific: ext3 and ext4 (as of 2008) are susceptible,
+   but tmpfs is not.  The general solution is to process entries in inode
+   order.  That means reading all entries, and sorting them before operating
+   on any.  As such, it is useful only on systems with useful dirent.d_ino.
+   Since 'rm -r's removal process must traverse into directories and since
+   this preprocessing phase can allocate O(N) storage, here we store and
+   sort only non-directory entries, and then remove all of those, so that we
+   can free all allocated storage before traversing into any subdirectory.
+   Perform this optimization only when not interactive and not in verbose
+   mode, to keep the implementation simple and to minimize duplication.
+   Upon failure, simply free any resources and return.  */
+static void
+preprocess_dir (DIR **dirp, struct rm_options const *x)
+{
+#if HAVE_STRUCT_DIRENT_D_TYPE
+
+  struct stat st;
+  /* There are many reasons to return right away, skipping this
+     optimization.  The penalty for being wrong is that we will
+     perform a small amount of extra work.
 
-  DIR *subdir_dirp = fdopendir (fd_sub);
-  if (subdir_dirp == NULL)
+     Skip this optimization if... */
+
+  int dir_fd = dirfd (*dirp);
+  if (/* - there is a chance of interactivity */
+      x->interactive != RMI_NEVER
+
+      /* - we're in verbose mode */
+      || x->verbose
+
+      /* - privileged users can unlink nonempty directories.
+        Otherwise, there'd be a race condition between the readdir
+        call (in which we learn dirent.d_type) and the unlink, by
+        which time the non-directory may be replaced with a directory. */
+      || ! cannot_unlink_dir ()
+
+      /* - we can't fstat the file descriptor */
+      || fstat (dir_fd, &st) != 0
+
+      /* - the directory is smaller than some threshold.
+        Estimate the number of inodes with a heuristic.
+         There's no significant benefit to sorting if there are
+        too few inodes.  */
+      || dirent_count (&st) < INODE_SORT_DIR_ENTRIES_THRESHOLD
+
+      /* Sort only if it might help.  */
+      || ! dirent_inode_sort_may_be_useful (dir_fd))
+    return;
+
+  /* FIXME: maybe test file system type, too; skip if it's tmpfs: see fts.c */
+
+  struct obstack o_readdir_data;  /* readdir data: inode,name pairs  */
+  struct obstack o_p;  /* an array of pointers to each inode,name pair */
+
+  /* Arrange to longjmp upon obstack allocation failure.  */
+  jmp_buf readdir_jumpbuf;
+  if (setjmp (readdir_jumpbuf))
+    goto cleanup;
+
+  obstack_init_minimal (&o_readdir_data);
+  obstack_init_minimal (&o_p);
+
+  obstack_specify_allocation_with_arg (&o_readdir_data, 0, 0,
+                                      rm_malloc, rm_free, &readdir_jumpbuf);
+  obstack_specify_allocation_with_arg (&o_p, 0, 0,
+                                      rm_malloc, rm_free, &readdir_jumpbuf);
+
+  /* Read all entries, storing <d_ino, d_name> for each non-dir one.
+     Maintain a parallel list of pointers into the primary buffer.  */
+  while (1)
+    {
+      struct dirent const *dp;
+      dp = readdir_ignoring_dot_and_dotdot (*dirp);
+      /* no need to distinguish EOF from failure */
+      if (dp == NULL)
+       break;
+
+      /* Skip known-directory and type-unknown entries.  */
+      if (D_TYPE (dp) == DT_UNKNOWN || D_TYPE (dp) == DT_DIR)
+       break;
+
+      size_t name_len = strlen (dp->d_name);
+      size_t ent_len = offsetof (struct readdir_data, name) + name_len + 1;
+      struct readdir_data *v = obstack_alloc (&o_readdir_data, ent_len);
+      v->ino = D_INO (dp);
+      memcpy (v->name, dp->d_name, name_len + 1);
+
+      /* Append V to the list of pointers.  */
+      obstack_ptr_grow (&o_p, v);
+    }
+
+  /* Compute size and finalize VV.  */
+  size_t n = obstack_object_size (&o_p) / sizeof (void *);
+  struct readdir_data **vv = obstack_finish (&o_p);
+
+  /* Sort on inode number.  */
+  qsort(vv, n, sizeof *vv, compare_ino);
+
+  /* Iterate through those pointers, unlinking each name.  */
+  for (size_t i = 0; i < n; i++)
     {
-      if (errno != ENOENT || !x->ignore_missing_files)
-       error (0, errno, _("cannot open directory %s"),
-              quote (full_filename (f)));
-      close (fd_sub);
-      return NULL;
+      /* ignore failure */
+      (void) unlinkat (dir_fd, vv[i]->name, 0);
     }
 
-  return subdir_dirp;
+ cleanup:
+  obstack_free (&o_readdir_data, NULL);
+  obstack_free (&o_p, NULL);
+  rewinddir (*dirp);
+#endif
 }
 
 /* Remove entries in the directory open on DIRP
@@ -1067,6 +1454,10 @@ remove_cwd_entries (DIR **dirp,
   assert (VALID_STATUS (status));
   *subdir = NULL;
 
+  /* This is just an optimization.
+     It's not a fatal problem if it fails.  */
+  preprocess_dir (dirp, x);
+
   while (1)
     {
       struct dirent const *dp;
@@ -1083,8 +1474,7 @@ remove_cwd_entries (DIR **dirp,
            {
              /* fall through */
            }
-         else if (CONSECUTIVE_READDIR_UNLINK_THRESHOLD
-                  < n_unlinked_since_opendir_or_last_rewind)
+         else if (NEED_REWIND (n_unlinked_since_opendir_or_last_rewind))
            {
              /* Call rewinddir if we've called unlink or rmdir so many times
                 (since the opendir or the previous rewinddir) that this
@@ -1106,7 +1496,9 @@ remove_cwd_entries (DIR **dirp,
         case can decide whether to use unlink or chdir.
         Systems without the d_type member will have to endure
         the performance hit of first calling lstat F. */
-      tmp_status = remove_entry (dirfd (*dirp), ds, f, x, dp);
+      cache_stat_init (subdir_sb);
+      tmp_status = remove_entry (dirfd (*dirp), ds, f,
+                                D_TYPE (dp), subdir_sb, x);
       switch (tmp_status)
        {
        case RM_OK:
@@ -1126,29 +1518,36 @@ remove_cwd_entries (DIR **dirp,
        case RM_NONEMPTY_DIR:
          {
            DIR *subdir_dirp = fd_to_subdirp (dirfd (*dirp), f,
-                                             x, errno, subdir_sb, ds,
-                                             OPENAT_CWD_RESTORE__REQUIRE,
-                                             NULL);
+                                             errno, subdir_sb, NULL);
            if (subdir_dirp == NULL)
              {
-               AD_mark_as_unremovable (ds, f);
                status = RM_ERROR;
-               break;
-             }
 
-           if (cycle_check (&ds->cycle_check_state, subdir_sb))
-             {
-               error (0, 0, _("\
-WARNING: Circular directory structure.\n\
-This almost certainly means that you have a corrupted file system.\n\
-NOTIFY YOUR SYSTEM MANAGER.\n\
-The following directory is part of the cycle:\n  %s\n"),
-                      quote (full_filename (".")));
-               longjmp (ds->current_arg_jumpbuf, 1);
+               /* CAUTION: this test and diagnostic are identical to
+                  those following the other use of fd_to_subdirp.  */
+               if (ignorable_missing (x, errno))
+                 {
+                   /* With -f, don't report "file not found".  */
+                 }
+               else
+                 {
+                   /* Upon fd_to_subdirp failure, try to remove F directly,
+                      in case it's just an empty directory.  */
+                   int saved_errno = errno;
+                   if (unlinkat (dirfd (*dirp), f, AT_REMOVEDIR) == 0)
+                     status = RM_OK;
+                   else
+                     error (0, saved_errno,
+                            _("cannot remove %s"), quote (full_filename (f)));
+                 }
+
+               if (status == RM_ERROR)
+                 AD_mark_as_unremovable (ds, f);
+               break;
              }
 
            *subdir = xstrdup (f);
-           if (CLOSEDIR (*dirp) != 0)
+           if (closedir (*dirp) != 0)
              {
                error (0, 0, _("failed to close directory %s"),
                       quote (full_filename (".")));
@@ -1193,10 +1592,11 @@ The following directory is part of the cycle:\n  %s\n"),
 
 static enum RM_status
 remove_dir (int fd_cwd, Dirstack_state *ds, char const *dir,
-           struct rm_options const *x, bool *cwd_restore_failed)
+           struct stat *dir_st,
+           struct rm_options const *x, int *cwd_errno)
 {
   enum RM_status status;
-  struct stat dir_sb;
+  dev_t current_dev = dir_st->st_dev;
 
   /* There is a race condition in that an attacker could replace the nonempty
      directory, DIR, with a symlink between the preceding call to rmdir
@@ -1206,21 +1606,39 @@ remove_dir (int fd_cwd, Dirstack_state *ds, char const *dir,
      fd_to_subdirp's fstat, along with the `fstat' and the dev/ino
      comparison in AD_push ensure that we detect it and fail.  */
 
-  DIR *dirp = fd_to_subdirp (fd_cwd, dir, x, 0, &dir_sb, ds,
-                            OPENAT_CWD_RESTORE__ALLOW_FAILURE,
-                            cwd_restore_failed);
+  DIR *dirp = fd_to_subdirp (fd_cwd, dir, 0, dir_st, cwd_errno);
 
   if (dirp == NULL)
-    return RM_ERROR;
+    {
+      /* CAUTION: this test and diagnostic are identical to
+        those following the other use of fd_to_subdirp.  */
+      if (ignorable_missing (x, errno))
+       {
+         /* With -f, don't report "file not found".  */
+       }
+      else
+       {
+         /* Upon fd_to_subdirp failure, try to remove DIR directly,
+            in case it's just an empty directory.  */
+         int saved_errno = errno;
+         if (unlinkat (fd_cwd, dir, AT_REMOVEDIR) == 0)
+           return RM_OK;
+
+         error (0, saved_errno,
+                _("cannot remove %s"), quote (full_filename (dir)));
+       }
 
-  if (ROOT_DEV_INO_CHECK (x->root_dev_ino, &dir_sb))
+      return RM_ERROR;
+    }
+
+  if (ROOT_DEV_INO_CHECK (x->root_dev_ino, dir_st))
     {
       ROOT_DEV_INO_WARN (full_filename (dir));
       status = RM_ERROR;
       goto closedir_and_return;
     }
 
-  AD_push (dirfd (dirp), ds, dir, &dir_sb);
+  AD_push (dirfd (dirp), ds, dir, dir_st);
   AD_INIT_OTHER_MEMBERS ();
 
   status = RM_OK;
@@ -1240,40 +1658,55 @@ remove_dir (int fd_cwd, Dirstack_state *ds, char const *dir,
        }
       if (subdir)
        {
-         AD_push (dirfd (dirp), ds, subdir, &subdir_sb);
-         AD_INIT_OTHER_MEMBERS ();
+         if ( ! x->one_file_system
+              || subdir_sb.st_dev == current_dev)
+           {
+             AD_push (dirfd (dirp), ds, subdir, &subdir_sb);
+             AD_INIT_OTHER_MEMBERS ();
+             free (subdir);
+             continue;
+           }
 
+         /* Here, --one-file-system is in effect, and with remove_cwd_entries'
+            traversal into the current directory, (known as SUBDIR, from ..),
+            DIRP's device number is different from CURRENT_DEV.  Arrange not
+            to do anything more with this hierarchy.  */
+         error (0, 0, _("skipping %s, since it's on a different device"),
+                quote (full_filename (subdir)));
          free (subdir);
-         continue;
+         AD_mark_current_as_unremovable (ds);
+         tmp_status = RM_ERROR;
+         UPDATE_STATUS (status, tmp_status);
        }
 
       /* Execution reaches this point when we've removed the last
-        removable entry from the current directory.  */
+        removable entry from the current directory -- or, with
+        --one-file-system, when the current directory is on a
+        different file system.  */
       {
+       int fd;
        /* The name of the directory that we have just processed,
           nominally removing all of its contents.  */
-       char *empty_dir;
-
-       AD_pop_and_chdir (&dirp, ds, &empty_dir);
-       int fd = (dirp != NULL ? dirfd (dirp) : AT_FDCWD);
-       assert (dirp != NULL || AD_stack_height (ds) == 1);
+       char *empty_dir = AD_pop_and_chdir (dirp, &fd, ds);
+       dirp = NULL;
+       assert (fd != AT_FDCWD || AD_stack_height (ds) == 1);
 
        /* Try to remove EMPTY_DIR only if remove_cwd_entries succeeded.  */
        if (tmp_status == RM_OK)
          {
-           /* This does a little more work than necessary when it actually
-              prompts the user.  E.g., we already know that D is a directory
-              and that it's almost certainly empty, yet we lstat it.
-              But that's no big deal since we're interactive.  */
-           Ternary is_dir;
+           struct stat empty_st;
            Ternary is_empty;
-           enum RM_status s = prompt (fd, ds, empty_dir, x,
-                                      PA_REMOVE_DIR, &is_dir, &is_empty);
+           int dirent_type = DT_DIR;
+           enum RM_status s = prompt (fd, ds, empty_dir, &dirent_type,
+                                      cache_stat_init (&empty_st), x,
+                                      PA_REMOVE_DIR, &is_empty);
 
            if (s != RM_OK)
              {
                free (empty_dir);
                status = s;
+               if (fd != AT_FDCWD)
+                 close (fd);
                goto closedir_and_return;
              }
 
@@ -1295,8 +1728,17 @@ remove_dir (int fd_cwd, Dirstack_state *ds, char const *dir,
 
        free (empty_dir);
 
-       if (AD_stack_height (ds) == 1)
+       if (fd == AT_FDCWD)
          break;
+
+       dirp = fdopendir (fd);
+       if (dirp == NULL)
+         {
+           error (0, errno, _("FATAL: cannot return to .. from %s"),
+                  quote (full_filename (".")));
+           close (fd);
+           longjmp (ds->current_arg_jumpbuf, 1);
+         }
       }
     }
 
@@ -1305,7 +1747,7 @@ remove_dir (int fd_cwd, Dirstack_state *ds, char const *dir,
   AD_stack_pop (ds);
 
  closedir_and_return:;
-  if (dirp != NULL && CLOSEDIR (dirp) != 0)
+  if (dirp != NULL && closedir (dirp) != 0)
     {
       error (0, 0, _("failed to close directory %s"),
             quote (full_filename (".")));
@@ -1320,23 +1762,42 @@ remove_dir (int fd_cwd, Dirstack_state *ds, char const *dir,
 
 static enum RM_status
 rm_1 (Dirstack_state *ds, char const *filename,
-      struct rm_options const *x, bool *cwd_restore_failed)
+      struct rm_options const *x, int *cwd_errno)
 {
-  char const *base = base_name (filename);
-  if (DOT_OR_DOTDOT (base))
+  char const *base = last_component (filename);
+  if (dot_or_dotdot (base))
     {
-      error (0, 0, _("cannot remove `.' or `..'"));
+      error (0, 0, _(base == filename
+                    ? N_("cannot remove directory %s")
+                    : N_("cannot remove %s directory %s")),
+            quote_n (0, base), quote_n (1, filename));
       return RM_ERROR;
     }
 
+  struct stat st;
+  cache_stat_init (&st);
+  cycle_check_init (&ds->cycle_check_state);
+  if (x->root_dev_ino)
+    {
+      if (cache_fstatat (AT_FDCWD, filename, &st, AT_SYMLINK_NOFOLLOW) != 0)
+       {
+         if (ignorable_missing (x, errno))
+           return RM_OK;
+         error (0, errno, _("cannot remove %s"), quote (filename));
+         return RM_ERROR;
+       }
+      if (SAME_INODE (st, *(x->root_dev_ino)))
+       {
+         error (0, 0, _("cannot remove root directory %s"), quote (filename));
+         return RM_ERROR;
+       }
+    }
+
   AD_push_initial (ds);
   AD_INIT_OTHER_MEMBERS ();
 
-  /* Put `status' in static storage, so it can't be clobbered
-     by the potential longjmp into this function.  */
-  static enum RM_status status;
-  int fd_cwd = AT_FDCWD;
-  status = remove_entry (fd_cwd, ds, filename, x, NULL);
+  enum RM_status status = remove_entry (AT_FDCWD, ds, filename,
+                                       DT_UNKNOWN, &st, x);
   if (status == RM_NONEMPTY_DIR)
     {
       /* In the event that remove_dir->remove_cwd_entries detects
@@ -1345,15 +1806,12 @@ rm_1 (Dirstack_state *ds, char const *filename,
       if (setjmp (ds->current_arg_jumpbuf))
        status = RM_ERROR;
       else
-       {
-         bool t_cwd_restore_failed = false;
-         status = remove_dir (fd_cwd, ds, filename, x, &t_cwd_restore_failed);
-         *cwd_restore_failed |= t_cwd_restore_failed;
-       }
+       status = remove_dir (AT_FDCWD, ds, filename, &st, x, cwd_errno);
+
+      AD_stack_clear (ds);
     }
 
   ds_clear (ds);
-
   return status;
 }
 
@@ -1363,33 +1821,43 @@ extern enum RM_status
 rm (size_t n_files, char const *const *file, struct rm_options const *x)
 {
   enum RM_status status = RM_OK;
-  Dirstack_state *ds = ds_init ();
-  bool cwd_restore_failed = false;
+  Dirstack_state ds;
+  int cwd_errno = 0;
   size_t i;
 
+  /* Arrange for obstack allocation failure to longjmp.  */
+  if (setjmp (ds.current_arg_jumpbuf))
+    {
+      status = RM_ERROR;
+      goto cleanup;
+    }
+
+  ds_init (&ds);
+
   for (i = 0; i < n_files; i++)
     {
-      if (cwd_restore_failed && IS_RELATIVE_FILE_NAME (file[i]))
+      if (cwd_errno && IS_RELATIVE_FILE_NAME (file[i]))
        {
          error (0, 0, _("cannot remove relative-named %s"), quote (file[i]));
          status = RM_ERROR;
-         continue;
        }
-
-      cycle_check_init (&ds->cycle_check_state);
-      enum RM_status s = rm_1 (ds, file[i], x, &cwd_restore_failed);
-      assert (VALID_STATUS (s));
-      UPDATE_STATUS (status, s);
+      else
+       {
+         enum RM_status s = rm_1 (&ds, file[i], x, &cwd_errno);
+         assert (VALID_STATUS (s));
+         UPDATE_STATUS (status, s);
+       }
     }
 
-  if (x->require_restore_cwd && cwd_restore_failed)
+  if (x->require_restore_cwd && cwd_errno)
     {
-      error (0, 0,
+      error (0, cwd_errno,
             _("cannot restore current working directory"));
       status = RM_ERROR;
     }
 
-  ds_free (ds);
+ cleanup:;
+  ds_free (&ds);
 
   return status;
 }