]> git.ipfire.org Git - thirdparty/coreutils.git/commitdiff
chcon,chgrp,chmod,chown,du,ls: ignore missing files while traversing
authorPádraig Brady <P@draigBrady.com>
Wed, 22 Jul 2026 18:23:26 +0000 (19:23 +0100)
committerPádraig Brady <P@draigBrady.com>
Fri, 24 Jul 2026 12:19:23 +0000 (13:19 +0100)
* src/fts-missing.h: Add helper routines to determine
if an FTS entry is skippable.  I.e., that is traversed
and not specified on the command line.
* src/chcon.c (process_file): Setup to ignore skippable files.
(change_file_context): Ignore missing files if appropriate.
* src/chmod.c (process_file): Setup to ignore skippable files.
Ignore missing files if appropriate.
(mode_changed): Ignore missing files if appropriate.
* src/chown-core.c (change_file_owner): Setup to ignore skippable files.
Ignore missing files if appropriate.
* src/du.c (process_file): Likewise.
* src/ls.c (print_dir): Ignore missing files if appropriate.
(get_link_name): Likewise.
(gobble_file): Likewise.  Also handle the dangling symlink case.
* src/system.h (ignorable_traversal_errno): Match ENOENT for missing
files.
* tests/chcon/traversal-missing.sh: A new test.
* tests/misc/traversal-missing.sh: Likewise.
* tests/local.mk: Reference the new tests.
* NEWS: Mention the bug fix.
Fixes https://bugs.gnu.org/81444

12 files changed:
NEWS
src/chcon.c
src/chmod.c
src/chown-core.c
src/du.c
src/fts-missing.h [new file with mode: 0644]
src/local.mk
src/ls.c
src/system.h
tests/chcon/traversal-missing.sh [new file with mode: 0755]
tests/local.mk
tests/misc/traversal-missing.sh [new file with mode: 0755]

diff --git a/NEWS b/NEWS
index 73c7af9d1b77b51d77eb5eae1711756285e2b01f..76354e9e9cc64f3baa31da771fb330052d5d5ef5 100644 (file)
--- a/NEWS
+++ b/NEWS
@@ -4,6 +4,10 @@ GNU coreutils NEWS                                    -*- outline -*-
 
 ** Bug fixes
 
+  'chcon', 'chgrp', 'chmod', 'chown', 'du', 'ls' which traverse hierarchies with
+  -R,  no longer fail merely because files may be being removed in parallel.
+  [This bug was present in "the beginning".]
+
   'comm - -' no longer closes standard input twice.  Previously it would
   mistakenly exit with a nonzero status.
   [This bug was present in "the beginning".]
index 91b78148d045438e4632be0a328c8a76a7b83dc8..4ab2d75701e3ab7d71a8121bc2ba9325605b2011 100644 (file)
@@ -22,6 +22,7 @@
 
 #include "system.h"
 #include "dev-ino.h"
+#include "fts-missing.h"
 #include "ignore-value.h"
 #include "quote.h"
 #include "root-dev-ino.h"
@@ -137,7 +138,7 @@ compute_context_from_mask (char const *context, context_t *ret)
    Return 0 if successful, 1 if errors occurred. */
 
 static int
-change_file_context (int fd, char const *file)
+change_file_context (int fd, char const *file, bool ignore_missing)
 {
   char *file_context = NULL;
   context_t context IF_LINT (= NULL);
@@ -152,6 +153,8 @@ change_file_context (int fd, char const *file)
 
       if (status < 0 && errno != ENODATA)
         {
+          if (ignore_missing && ignorable_traversal_errno (errno))
+            return 0;
           error (0, errno, _("failed to get security context of %s"),
                  quoteaf (file));
           return 1;
@@ -188,9 +191,12 @@ change_file_context (int fd, char const *file)
 
       if (fail)
         {
-          errors = 1;
-          error (0, errno, _("failed to change context of %s to %s"),
-                 quoteaf_n (0, file), quote_n (1, context_string));
+          if (! (ignore_missing && ignorable_traversal_errno (errno)))
+            {
+              errors = 1;
+              error (0, errno, _("failed to change context of %s to %s"),
+                     quoteaf_n (0, file), quote_n (1, context_string));
+            }
         }
     }
 
@@ -214,6 +220,10 @@ process_file (FTS *fts, FTSENT *ent)
   char const *file = ent->fts_accpath;
   const struct stat *file_stats = ent->fts_statp;
   bool ok = true;
+  bool ignore_missing = ignore_missing_fts_entry (ent);
+
+  if (ignore_missing && ignorable_fts_error (ent))
+    return true;
 
   switch (ent->fts_info)
     {
@@ -295,7 +305,7 @@ process_file (FTS *fts, FTSENT *ent)
         printf (_("changing security context of %s\n"),
                 quoteaf (file_full_name));
 
-      if (change_file_context (fts->fts_cwd_fd, file) != 0)
+      if (change_file_context (fts->fts_cwd_fd, file, ignore_missing) != 0)
         ok = false;
     }
 
index 8b57172707f9164f33cdd3bde7b956c90af135e5..68f37e18107bd7217dab1f7cbbfbb763efaf504b 100644 (file)
@@ -25,6 +25,7 @@
 #include "assure.h"
 #include "dev-ino.h"
 #include "filemode.h"
+#include "fts-missing.h"
 #include "ignore-value.h"
 #include "modechange.h"
 #include "quote.h"
@@ -125,8 +126,10 @@ static struct option const long_options[] =
 
 static bool
 mode_changed (int dir_fd, char const *file, char const *file_full_name,
-              mode_t old_mode, mode_t new_mode)
+              mode_t old_mode, mode_t new_mode, bool ignore_missing,
+              bool *file_missing)
 {
+  *file_missing = false;
   if (new_mode & (S_ISUID | S_ISGID | S_ISVTX))
     {
       /* The new mode contains unusual bits that the call to chmod may
@@ -136,7 +139,9 @@ mode_changed (int dir_fd, char const *file, char const *file_full_name,
 
       if (fstatat (dir_fd, file, &new_stats, 0) != 0)
         {
-          if (! force_silent)
+          *file_missing = (ignore_missing
+                           && ignorable_traversal_errno (errno));
+          if (! *file_missing && ! force_silent)
             error (0, errno, _("getting new attributes of %s"),
                    quoteaf (file_full_name));
           return false;
@@ -215,6 +220,10 @@ process_file (FTS *fts, FTSENT *ent)
   struct change_status ch = {0};
   ch.status = CH_NO_STAT;
   struct stat stat_buf;
+  bool ignore_missing = ignore_missing_fts_entry (ent);
+
+  if (ignore_missing && ignorable_fts_error (ent))
+    return true;
 
   switch (ent->fts_info)
     {
@@ -267,6 +276,9 @@ process_file (FTS *fts, FTSENT *ent)
         {
           if (fstatat (fts->fts_cwd_fd, file, &stat_buf, 0) != 0)
             {
+              if (ignore_missing
+                  && ignorable_traversal_errno (errno))
+                return true;
               if (! force_silent)
                 error (0, errno, _("cannot dereference %s"),
                        quoteaf (file_full_name));
@@ -314,6 +326,9 @@ process_file (FTS *fts, FTSENT *ent)
         ch.status = CH_SUCCEEDED;
       else
         {
+          if (ignore_missing
+              && ignorable_traversal_errno (errno))
+            return true;
           if (! is_ENOTSUP (errno))
             {
               if (! force_silent)
@@ -328,10 +343,16 @@ process_file (FTS *fts, FTSENT *ent)
 
   if (verbosity != V_off)
     {
+      bool file_missing;
       if (ch.status == CH_SUCCEEDED
           && !mode_changed (fts->fts_cwd_fd, file, file_full_name,
-                            ch.old_mode, ch.new_mode))
-        ch.status = CH_NO_CHANGE_REQUESTED;
+                            ch.old_mode, ch.new_mode, ignore_missing,
+                            &file_missing))
+        {
+          if (file_missing)
+            return true;
+          ch.status = CH_NO_CHANGE_REQUESTED;
+        }
 
       if (ch.status == CH_SUCCEEDED || verbosity == V_high)
         describe_change (file_full_name, &ch);
index e5e355fdb88688e7808e390910ea725d5c81fbb2..21f1e3d97a575c7e5e60c9062dd81f3ddbcbd301 100644 (file)
@@ -25,6 +25,7 @@
 #include "system.h"
 #include "assure.h"
 #include "chown-core.h"
+#include "fts-missing.h"
 #include "ignore-value.h"
 #include "root-dev-ino.h"
 #include "xfts.h"
@@ -284,6 +285,10 @@ change_file_owner (FTS *fts, FTSENT *ent,
   char const *file_full_name = ent->fts_path;
   char const *file = ent->fts_accpath;
   bool ok = true;
+  bool ignore_missing = ignore_missing_fts_entry (ent);
+
+  if (ignore_missing && ignorable_fts_error (ent))
+    return true;
 
   switch (ent->fts_info)
     {
@@ -381,6 +386,9 @@ change_file_owner (FTS *fts, FTSENT *ent,
         {
           if (fstatat (fts->fts_cwd_fd, file, &stat_buf, 0) != 0)
             {
+              if (ignore_missing
+                  && ignorable_traversal_errno (errno))
+                return true;
               if (! chopt->force_silent)
                 error (0, errno, _("cannot dereference %s"),
                        quoteaf (file_full_name));
@@ -469,11 +477,17 @@ change_file_owner (FTS *fts, FTSENT *ent,
          by some other user and operating on files in a directory
          where M has write access.  */
 
-      if (do_chown && !ok && ! chopt->force_silent)
-        error (0, errno, (uid != (uid_t) -1
-                          ? _("changing ownership of %s")
-                          : _("changing group of %s")),
-               quoteaf (file_full_name));
+      if (do_chown && !ok)
+        {
+          if (ignore_missing
+              && ignorable_traversal_errno (errno))
+            return true;
+          if (! chopt->force_silent)
+            error (0, errno, (uid != (uid_t) -1
+                              ? _("changing ownership of %s")
+                              : _("changing group of %s")),
+                   quoteaf (file_full_name));
+        }
     }
 
   if (chopt->verbosity != V_off)
index 9b80633711251a8fd0d1e5fe13cbfa90b3177aa8..eefb24f05aab6d19d2e908c1aaed58af3f778166 100644 (file)
--- a/src/du.c
+++ b/src/du.c
@@ -32,6 +32,7 @@
 #include "assure.h"
 #include "di-set.h"
 #include "exclude.h"
+#include "fts-missing.h"
 #include "human.h"
 #include "mountlist.h"
 #include "quote.h"
@@ -554,6 +555,10 @@ process_file (FTS *fts, FTSENT *ent)
   char const *file = ent->fts_path;
   const struct stat *sb = ent->fts_statp;
   int info = ent->fts_info;
+  bool ignore_missing = ignore_missing_fts_entry (ent);
+
+  if (ignore_missing && ignorable_fts_error (ent))
+    return true;
 
   if (info == FTS_DNR)
     {
@@ -574,6 +579,9 @@ process_file (FTS *fts, FTSENT *ent)
               MAYBE_UNUSED FTSENT const *e = fts_read (fts);
               affirm (e == ent);
               info = ent->fts_info;
+
+              if (ignore_missing && ignorable_fts_error (ent))
+                return true;
             }
 
           if (info == FTS_NS || info == FTS_SLNONE)
diff --git a/src/fts-missing.h b/src/fts-missing.h
new file mode 100644 (file)
index 0000000..cb007bc
--- /dev/null
@@ -0,0 +1,39 @@
+/* fts-missing.h -- helpers for files missing during an FTS traversal.
+
+   Copyright (C) 2026 Free Software Foundation, Inc.
+
+   This program is free software: you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation, either version 3 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
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program.  If not, see <https://www.gnu.org/licenses/>.  */
+
+#ifndef FTS_MISSING_H
+# define FTS_MISSING_H
+
+# include "fts_.h"
+
+static inline bool
+ignore_missing_fts_entry (FTSENT const *ent)
+{
+  return (FTS_ROOTLEVEL < ent->fts_level
+          && ent->fts_info != FTS_SL
+          && ent->fts_info != FTS_SLNONE);
+}
+
+static inline bool
+ignorable_fts_error (FTSENT const *ent)
+{
+  return (ignorable_traversal_errno (ent->fts_errno)
+          && (ent->fts_info == FTS_NS || ent->fts_info == FTS_ERR
+              || ent->fts_info == FTS_DNR));
+}
+
+#endif /* FTS_MISSING_H */
index d7d6ae5096e1a34dcb38399048ec6d1a7bb3b0a3..db86cb5ea794a15c8af71a5ed0fb43f264fb998e 100644 (file)
@@ -50,6 +50,7 @@ noinst_HEADERS =              \
   src/find-mount-point.h       \
   src/fs.h                     \
   src/fs-is-local.h            \
+  src/fts-missing.h            \
   src/group-list.h             \
   src/ioblksize.h              \
   src/iopoll.h                 \
index 3d583b85f153dc38faf1c4dee153476ce920cece..a4be6fcea0e32f2636b8a3fa3ce3627ca2a1ab1d 100644 (file)
--- a/src/ls.c
+++ b/src/ls.c
@@ -279,7 +279,7 @@ static void attach (char *dest, char const *dirname, char const *name);
 static void clear_files (void);
 static void extract_dirs_from_files (char const *dirname,
                                      bool command_line_arg);
-static void get_link_name (char const *filename, struct fileinfo *f,
+static bool get_link_name (char const *filename, struct fileinfo *f,
                            bool command_line_arg);
 static void indent (size_t from, size_t to);
 static idx_t calculate_columns (bool by_columns);
@@ -2961,7 +2961,8 @@ print_dir (char const *name, char const *realname, bool command_line_arg)
   dirp = opendir (name);
   if (!dirp)
     {
-      file_failure (command_line_arg, _("cannot open directory %s"), name);
+      if (command_line_arg || ! ignorable_traversal_errno (errno))
+        file_failure (command_line_arg, _("cannot open directory %s"), name);
       return;
     }
 
@@ -2975,8 +2976,9 @@ print_dir (char const *name, char const *realname, bool command_line_arg)
            ? fstat_for_ino (fd, &dir_stat)
            : stat_for_ino (name, &dir_stat)) < 0)
         {
-          file_failure (command_line_arg,
-                        _("cannot determine device and inode of %s"), name);
+          if (! ignorable_traversal_errno (errno))
+            file_failure (command_line_arg,
+                          _("cannot determine device and inode of %s"), name);
           closedir (dirp);
           return;
         }
@@ -3008,7 +3010,8 @@ print_dir (char const *name, char const *realname, bool command_line_arg)
       if (print_hyperlink)
         {
           absolute_name = canonicalize_filename_mode (name, CAN_MISSING);
-          if (! absolute_name)
+          if (! absolute_name
+              && ! ignorable_traversal_errno (errno))
             file_failure (command_line_arg,
                           _("error canonicalizing %s"), name);
         }
@@ -3064,12 +3067,9 @@ print_dir (char const *name, char const *realname, bool command_line_arg)
           int err = errno;
           if (err == 0)
             break;
-          /* Some readdir()s do not absorb ENOENT (dir deleted but open).
-             This bug was fixed in glibc 2.3 (2002).  */
-#if ! (2 < __GLIBC__ + (3 <= __GLIBC_MINOR__))
-          if (err == ENOENT)
+          /* Ignore errors indicating that the directory was removed.  */
+          if (ignorable_traversal_errno (err))
             break;
-#endif
           file_failure (command_line_arg, _("reading directory %s"), name);
           if (err != EOVERFLOW)
             break;
@@ -3391,7 +3391,9 @@ gobble_file (char const *name, enum filetype type, ino_t inode,
         {
           f->absolute_name = canonicalize_filename_mode (full_name,
                                                          CAN_MISSING);
-          if (! f->absolute_name)
+          if (! f->absolute_name
+              && (command_line_arg
+                  || ! ignorable_traversal_errno (errno)))
             file_failure (command_line_arg,
                           _("error canonicalizing %s"), full_name);
         }
@@ -3439,6 +3441,22 @@ gobble_file (char const *name, enum filetype type, ino_t inode,
 
       if (err != 0)
         {
+          if (! command_line_arg && ignorable_traversal_errno (errno))
+            {
+              int stat_errno = errno;
+              struct stat linkstat;
+              bool dangling_link = (do_deref
+                                    && (type == symbolic_link
+                                        || type == unknown)
+                                    && do_lstat (full_name, &linkstat) == 0);
+              errno = stat_errno;
+              if (! dangling_link)
+                {
+                  free_ent (f);
+                  return 0;
+                }
+            }
+
           /* Failure to stat a command line argument leads to
              an exit status of 2.  For other files, stat failure
              provokes an exit status of 1.  */
@@ -3483,7 +3501,8 @@ gobble_file (char const *name, enum filetype type, ino_t inode,
          Also if a file is removed while we're reading ACL info,
          ACL_T_UNKNOWN is sufficient indication for that edge case.  */
       bool cannot_access_acl = n < 0
-           && (errno == EACCES || errno == ENOENT);
+           && (errno == EACCES
+               || ignorable_traversal_errno (errno));
 
       f->acl_type = (!have_scontext && !have_acl
                      ? (cannot_access_acl ? ACL_T_UNKNOWN : ACL_T_NONE)
@@ -3501,6 +3520,7 @@ gobble_file (char const *name, enum filetype type, ino_t inode,
              isn't on the right type of file system.  I.e., a getfilecon
              failure isn't in the same class as a stat failure.  */
           if (print_scontext && ai.scontext_err
+              && ! ignorable_traversal_errno (ai.scontext_err)
               && (! (is_ENOTSUP (ai.scontext_err)
                      || ai.scontext_err == ENODATA)))
             error (0, ai.scontext_err, "%s", quotef (full_name));
@@ -3522,7 +3542,11 @@ gobble_file (char const *name, enum filetype type, ino_t inode,
     {
       struct stat linkstats;
 
-      get_link_name (full_name, f, command_line_arg);
+      if (! get_link_name (full_name, f, command_line_arg))
+        {
+          free_ent (f);
+          return 0;
+        }
 
       /* Use the slower quoting path for this entry, though
          don't update CWD_SOME_QUOTED since alignment not affected.  */
@@ -3648,13 +3672,19 @@ is_linked_directory (const struct fileinfo *f)
    into the LINKNAME field of 'f'.  COMMAND_LINE_ARG indicates whether
    FILENAME is a command-line argument.  */
 
-static void
+static bool
 get_link_name (char const *filename, struct fileinfo *f, bool command_line_arg)
 {
   f->linkname = areadlink_with_size (filename, f->stat.st_size);
   if (f->linkname == NULL)
-    file_failure (command_line_arg, _("cannot read symbolic link %s"),
-                  filename);
+    {
+      if (! command_line_arg
+          && ignorable_traversal_errno (errno))
+        return false;
+      file_failure (command_line_arg, _("cannot read symbolic link %s"),
+                    filename);
+    }
+  return true;
 }
 
 /* Return true if the last component of NAME is '.' or '..'
index d0f2e0c44d5823e0f3783cfbef2a1231e22b2451..e96127d41f8137adbae1f50ae0517ee5ab0799c3 100644 (file)
 # define ENODATA (-1)
 #endif
 
+/* Return true if ERRNUM indicates that a file disappeared during a
+   directory traversal.  */
+static inline bool
+ignorable_traversal_errno (int errnum) { return errnum == ENOENT; }
+
 #include <stdlib.h>
 #include "version.h"
 
diff --git a/tests/chcon/traversal-missing.sh b/tests/chcon/traversal-missing.sh
new file mode 100755 (executable)
index 0000000..7286cc8
--- /dev/null
@@ -0,0 +1,48 @@
+#!/bin/sh
+# Test ignoring traversal races using strace fault injection.
+
+# Copyright (C) 2026 Free Software Foundation, Inc.
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 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
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+. "${srcdir=.}/tests/init.sh"; path_prepend_ ./src
+print_ver_ chcon
+require_strace_ getxattr
+
+mkdir d || framework_failure_
+touch d/foo || framework_failure_
+xattrs=getxattr,lgetxattr,setxattr,lsetxattr
+
+(cd d && chcon -R -t user_tmp_t . > /dev/null 2>&1) \
+  || skip_ 'chcon does not work on the test file system'
+
+# Skip if strace does not support path-filtered syscall injection.
+strace --quiet=all -o /dev/null -P /proc/self/fd/4/foo \
+  -e inject=$xattrs:error=ENOENT true 2> /dev/null \
+  || skip_ 'strace does not support the required options and syscalls'
+
+for errnum in ENOENT; do
+  rm -f err trace
+  (cd d && strace --quiet=all -o ../trace -P /proc/self/fd/4/foo \
+     -e inject=$xattrs:error=$errnum chcon -R -t user_tmp_t . \
+     > /dev/null 2> ../err)
+  status=$?
+
+  grep ' (INJECTED)' trace > /dev/null 2>&1 \
+    || skip_ 'strace did not inject the requested failure'
+  test $status -eq 0 || fail=1
+  test ! -s err || { cat err; fail=1; }
+done
+
+Exit $fail
index 18bf79861874f77e6320cbb10bcd964f425aebb3..9414dbf9da3d11929e683d5b8087ecf1c6ce9ba7 100644 (file)
@@ -185,6 +185,7 @@ all_tests =                                 \
   tests/misc/io-errors.sh                      \
   tests/misc/read-errors.sh                    \
   tests/misc/responsive.sh                     \
+  tests/misc/traversal-missing.sh              \
   tests/misc/warning-errors.sh                 \
   tests/misc/write-errors.sh                   \
   tests/tail/basic-seek.sh                     \
@@ -297,6 +298,7 @@ all_tests =                                 \
   tests/pwd/argument.sh                                \
   tests/pwd/pwd-option.sh                      \
   tests/chcon/chcon-fail.sh                    \
+  tests/chcon/traversal-missing.sh             \
   tests/misc/coreutils.sh                      \
   tests/cut/cut.pl                             \
   tests/cut/mb-non-utf8.sh                     \
diff --git a/tests/misc/traversal-missing.sh b/tests/misc/traversal-missing.sh
new file mode 100755 (executable)
index 0000000..7352331
--- /dev/null
@@ -0,0 +1,74 @@
+#!/bin/sh
+# Test ignoring traversal races using strace fault injection.
+
+# Copyright (C) 2026 Free Software Foundation, Inc.
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 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
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+. "${srcdir=.}/tests/init.sh"; path_prepend_ ./src
+print_ver_ ls chmod chown chgrp du
+require_strace_ stat
+
+mkdir d || framework_failure_
+touch d/foo || framework_failure_
+uid=$(id -u) || framework_failure_
+gid=$(id -g) || framework_failure_
+
+stats='stat'
+# List other _file name_ stat functions to increase coverage.
+other_stats='statx lstat stat64 lstat64 newfstatat fstatat64'
+for stat in $other_stats; do
+  strace -qe "$stat" true > /dev/null 2>&1 &&
+    stats="$stats,$stat"
+done
+
+chmods=chmod,fchmodat,fchmodat2
+chowns=chown,lchown,fchownat
+
+# Skip if strace does not support path-filtered syscall injection.
+strace --quiet=all -o /dev/null -P foo \
+  -e inject=$stats:error=ENOENT true 2> /dev/null \
+  || skip_ 'strace does not support the required options and syscalls'
+
+run_injected_ ()
+{
+  path=$1
+  syscalls=$2
+  shift 2
+  rm -f err trace
+  (cd d && strace --quiet=all -o ../trace -P "$path" \
+     -e inject="$syscalls:error=$errnum" "$@" > /dev/null 2> ../err)
+  status=$?
+
+  grep ' (INJECTED)' trace > /dev/null 2>&1 \
+    || skip_ 'strace did not inject the requested failure'
+  test $status -eq $expected_status || fail=1
+  if test $expected_status -eq 0; then
+    test ! -s err || { cat err; fail=1; }
+  else
+    test -s err || fail=1
+  fi
+}
+
+expected_status=0
+for errnum in ENOENT EIO; do
+  test $errnum = EIO && expected_status=1
+  run_injected_ foo "$stats" ls -l .
+  run_injected_ foo "$stats" du -a .
+  run_injected_ foo "$chmods" chmod -R 0700 .
+  run_injected_ foo "$chowns" chown -R "$uid" .
+  run_injected_ foo "$chowns" chgrp -R "$gid" .
+done
+
+Exit $fail