** 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".]
#include "system.h"
#include "dev-ino.h"
+#include "fts-missing.h"
#include "ignore-value.h"
#include "quote.h"
#include "root-dev-ino.h"
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);
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;
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));
+ }
}
}
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)
{
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;
}
#include "assure.h"
#include "dev-ino.h"
#include "filemode.h"
+#include "fts-missing.h"
#include "ignore-value.h"
#include "modechange.h"
#include "quote.h"
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
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;
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)
{
{
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));
ch.status = CH_SUCCEEDED;
else
{
+ if (ignore_missing
+ && ignorable_traversal_errno (errno))
+ return true;
if (! is_ENOTSUP (errno))
{
if (! force_silent)
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);
#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"
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)
{
{
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));
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)
#include "assure.h"
#include "di-set.h"
#include "exclude.h"
+#include "fts-missing.h"
#include "human.h"
#include "mountlist.h"
#include "quote.h"
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)
{
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)
--- /dev/null
+/* 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 */
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 \
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);
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;
}
? 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;
}
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);
}
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;
{
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);
}
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. */
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)
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));
{
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. */
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 '..'
# 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"
--- /dev/null
+#!/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
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 \
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 \
--- /dev/null
+#!/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