diff options
author | Paul Eggert <eggert@cs.ucla.edu> | 2022-09-21 14:05:49 -0700 |
---|---|---|
committer | Paul Eggert <eggert@cs.ucla.edu> | 2022-09-24 16:34:31 -0700 |
commit | eb7841426ce8cfb0150d4d2f4879ce455005fbbb (patch) | |
tree | f0a40ea685f49dd2d7de8716b84e99d0f9a72ee4 | |
parent | 5a14ccad485f62d759157e925c7fddf33b9a5829 (diff) | |
download | coreutils-eb7841426ce8cfb0150d4d2f4879ce455005fbbb.tar.gz |
rm: fix diagnostics on I/O error
I ran into this problem when attempting to recursively
remove a directory in a filesystem on flaky hardware.
Although the underlying readdir syscall failed with errno == EIO,
rm issued no diagnostic about the I/O error.
Without this patch I see this behavior:
$ rm -fr baddir
rm: cannot remove 'baddir': Directory not empty
$ rm -ir baddir
rm: descend into directory 'baddir'? y
rm: remove directory 'baddir'? y
rm: cannot remove 'baddir': Directory not empty
With this patch I see the following behavior, which
lets the user know about the I/O error when rm tries
to read baddir's directory entries:
$ rm -fr baddir
rm: cannot remove 'baddir': Input/output error
$ rm -ir baddir
rm: cannot remove 'baddir': Input/output error
* src/remove.c (Ternary): Remove. All uses removed.
(get_dir_status): New static function.
(prompt): Last arg is now directory status, not ternary.
Return RM_USER_ACCEPTED if user explicitly accepted.
All uses changed.
Report any significant error in directory status right away.
(prompt, rm_fts): Use get_dir_status to get directory status lazily.
(excise): Treat any FTS_DNR errno as being more descriptive, not
just EPERM and EACCESS. For example, EIO is more descriptive.
(rm_fts): Distinguish more clearly between explicit and implied
user OK.
* src/remove.h (RM_USER_ACCEPTED): New constant.
(VALID_STATUS): Treat it as valid.
* src/system.h (is_empty_dir): Remove, replacing with ...
(directory_status): ... this more-general function.
All uses changed. Avoid undefined behavior of looking at
a non-null readdir pointer after corresponding closedir.
* tests/rm/rm-readdir-fail.sh: Adjust test of internals
to match current behavior.
-rw-r--r-- | src/remove.c | 80 | ||||
-rw-r--r-- | src/remove.h | 4 | ||||
-rw-r--r-- | src/rmdir.c | 3 | ||||
-rw-r--r-- | src/system.h | 25 | ||||
-rwxr-xr-x | tests/rm/rm-readdir-fail.sh | 1 |
5 files changed, 58 insertions, 55 deletions
diff --git a/src/remove.c b/src/remove.c index 6756c409d..0b6754bf7 100644 --- a/src/remove.c +++ b/src/remove.c @@ -33,14 +33,6 @@ #include "xfts.h" #include "yesno.h" -enum Ternary - { - T_UNKNOWN = 2, - T_NO, - T_YES - }; -typedef enum Ternary Ternary; - /* The prompt function may be called twice for a given directory. The first time, we ask whether to descend into it, and the second time, we ask whether to remove it. */ @@ -168,9 +160,23 @@ write_protected_non_symlink (int fd_cwd, } } -/* Prompt whether to remove FILENAME (ent->, if required via a combination of +/* Return the status of the directory identified by FTS and ENT. + This is -1 if the directory is empty, 0 if it is nonempty, + and a positive error number if there was trouble determining the status, + e.g., it is not a directory, or permissions problems, or I/O errors. + Use *DIR_STATUS is a cache for the status. */ +static int +get_dir_status (FTS const *fts, FTSENT const *ent, int *dir_status) +{ + if (*dir_status < -1) + *dir_status = directory_status (fts->fts_cwd_fd, ent->fts_accpath); + return *dir_status; +} + +/* Prompt whether to remove FILENAME, if required via a combination of the options specified by X and/or file attributes. If the file may - be removed, return RM_OK. If the user declines to remove the file, + be removed, return RM_OK or RM_USER_ACCEPTED, the latter if the user + was prompted and accepted. If the user declines to remove the file, return RM_USER_DECLINED. If not ignoring missing files and we cannot lstat FILENAME, then return RM_ERROR. @@ -178,20 +184,16 @@ write_protected_non_symlink (int fd_cwd, 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_P 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_P when MODE == PA_REMOVE_DIR. */ + Use and update *DIR_STATUS as needed, via the conventions of + get_dir_status. */ static enum RM_status prompt (FTS const *fts, FTSENT const *ent, bool is_dir, struct rm_options const *x, enum Prompt_action mode, - Ternary *is_empty_p) + int *dir_status) { int fd_cwd = fts->fts_cwd_fd; char const *full_name = ent->fts_path; char const *filename = ent->fts_accpath; - if (is_empty_p) - *is_empty_p = T_UNKNOWN; - struct stat st; struct stat *sbuf = &st; cache_stat_init (sbuf); @@ -199,13 +201,6 @@ prompt (FTS const *fts, FTSENT const *ent, bool is_dir, int dirent_type = is_dir ? DT_DIR : DT_UNKNOWN; int write_protected = 0; - bool is_empty = false; - if (is_empty_p) - { - is_empty = is_empty_dir (fd_cwd, filename); - *is_empty_p = is_empty ? T_YES : T_NO; - } - /* When nonzero, this indicates that we failed to remove a child entry, either because the user declined an interactive prompt, or due to some other failure, like permissions. */ @@ -257,10 +252,12 @@ prompt (FTS const *fts, FTSENT const *ent, bool is_dir, /* Unless we're either deleting directories or deleting recursively, we want to raise an EISDIR error rather than prompting the user */ - if ( ! (x->recursive || (x->remove_empty_directories && is_empty))) + if ( ! (x->recursive + || (x->remove_empty_directories + && get_dir_status (fts, ent, dir_status) < 0))) { write_protected = -1; - wp_errno = EISDIR; + wp_errno = *dir_status <= 0 ? EISDIR : *dir_status; } break; } @@ -276,12 +273,17 @@ prompt (FTS const *fts, FTSENT const *ent, bool is_dir, /* Issue the prompt. */ if (dirent_type == DT_DIR && mode == PA_DESCEND_INTO_DIR - && !is_empty) + && get_dir_status (fts, ent, dir_status) == 0) fprintf (stderr, (write_protected ? _("%s: descend into write-protected directory %s? ") : _("%s: descend into directory %s? ")), program_name, quoted_name); + else if (0 < *dir_status) + { + error (0, *dir_status, _("cannot remove %s"), quoted_name); + return RM_ERROR; + } else { if (cache_fstatat (fd_cwd, filename, sbuf, AT_SYMLINK_NOFOLLOW) != 0) @@ -302,8 +304,7 @@ prompt (FTS const *fts, FTSENT const *ent, bool is_dir, program_name, file_type (sbuf), quoted_name); } - if (!yesno ()) - return RM_USER_DECLINED; + return yesno () ? RM_USER_ACCEPTED : RM_USER_DECLINED; } return RM_OK; } @@ -405,13 +406,12 @@ excise (FTS *fts, FTSENT *ent, struct rm_options const *x, bool is_dir) /* When failing to rmdir an unreadable directory, we see errno values like EISDIR or ENOTDIR (or, on Solaris 10, EEXIST), but they would be - meaningless in a diagnostic. When that happens and the errno value - from the failed open is EPERM or EACCES, use the earlier, more + meaningless in a diagnostic. When that happens, use the earlier, more descriptive errno value. */ if (ent->fts_info == FTS_DNR && (errno == ENOTEMPTY || errno == EISDIR || errno == ENOTDIR || errno == EEXIST) - && (ent->fts_errno == EPERM || ent->fts_errno == EACCES)) + && ent->fts_errno != 0) errno = ent->fts_errno; error (0, errno, _("cannot remove %s"), quoteaf (ent->fts_path)); mark_ancestor_dirs (ent); @@ -427,12 +427,14 @@ excise (FTS *fts, FTSENT *ent, struct rm_options const *x, bool is_dir) static enum RM_status rm_fts (FTS *fts, FTSENT *ent, struct rm_options const *x) { + int dir_status = -2; + switch (ent->fts_info) { case FTS_D: /* preorder directory */ if (! x->recursive && !(x->remove_empty_directories - && is_empty_dir (fts->fts_cwd_fd, ent->fts_accpath))) + && get_dir_status (fts, ent, &dir_status) < 0)) { /* This is the first (pre-order) encounter with a directory that we cannot delete. @@ -507,11 +509,10 @@ rm_fts (FTS *fts, FTSENT *ent, struct rm_options const *x) } { - Ternary is_empty_directory; enum RM_status s = prompt (fts, ent, true /*is_dir*/, x, - PA_DESCEND_INTO_DIR, &is_empty_directory); + PA_DESCEND_INTO_DIR, &dir_status); - if (s == RM_OK && is_empty_directory == T_YES) + if (s == RM_USER_ACCEPTED && dir_status == -1) { /* When we know (from prompt when in interactive mode) that this is an empty directory, don't prompt twice. */ @@ -520,7 +521,7 @@ rm_fts (FTS *fts, FTSENT *ent, struct rm_options const *x) fts_skip_tree (fts, ent); } - if (s != RM_OK) + if (! (s == RM_OK || s == RM_USER_ACCEPTED)) { mark_ancestor_dirs (ent); fts_skip_tree (fts, ent); @@ -553,8 +554,9 @@ rm_fts (FTS *fts, FTSENT *ent, struct rm_options const *x) } bool is_dir = ent->fts_info == FTS_DP || ent->fts_info == FTS_DNR; - enum RM_status s = prompt (fts, ent, is_dir, x, PA_REMOVE_DIR, NULL); - if (s != RM_OK) + enum RM_status s = prompt (fts, ent, is_dir, x, PA_REMOVE_DIR, + &dir_status); + if (! (s == RM_OK || s == RM_USER_ACCEPTED)) return s; return excise (fts, ent, x, is_dir); } diff --git a/src/remove.h b/src/remove.h index e926084e2..b92c63daa 100644 --- a/src/remove.h +++ b/src/remove.h @@ -79,13 +79,15 @@ enum RM_status { /* These must be listed in order of increasing seriousness. */ RM_OK = 2, + RM_USER_ACCEPTED, RM_USER_DECLINED, RM_ERROR, RM_NONEMPTY_DIR }; # define VALID_STATUS(S) \ - ((S) == RM_OK || (S) == RM_USER_DECLINED || (S) == RM_ERROR) + ((S) == RM_OK || (S) == RM_USER_ACCEPTED || (S) == RM_USER_DECLINED \ + || (S) == RM_ERROR) # define UPDATE_STATUS(S, New_value) \ do \ diff --git a/src/rmdir.c b/src/rmdir.c index f6284cbe2..283f332ec 100644 --- a/src/rmdir.c +++ b/src/rmdir.c @@ -101,8 +101,7 @@ ignorable_failure (int error_number, char const *dir) return (ignore_fail_on_non_empty && (errno_rmdir_non_empty (error_number) || (errno_may_be_non_empty (error_number) - && ! is_empty_dir (AT_FDCWD, dir) - && errno == 0 /* definitely non empty */))); + && directory_status (AT_FDCWD, dir) == 0))); } /* Remove any empty parent directories of DIR. diff --git a/src/system.h b/src/system.h index d36ca1115..91228ec13 100644 --- a/src/system.h +++ b/src/system.h @@ -287,37 +287,36 @@ readdir_ignoring_dot_and_dotdot (DIR *dirp) } } -/* Return true if DIR is determined to be an empty directory. - Return false with ERRNO==0 if DIR is a non empty directory. - Return false if not able to determine if directory empty. */ -static inline bool -is_empty_dir (int fd_cwd, char const *dir) +/* Return -1 if DIR is an empty directory, + 0 if DIR is a nonempty directory, + and a positive error number if there was trouble determining + whether DIR is an empty or nonempty directory. */ +static inline int +directory_status (int fd_cwd, char const *dir) { DIR *dirp; - struct dirent const *dp; + bool no_direntries; int saved_errno; int fd = openat (fd_cwd, dir, (O_RDONLY | O_DIRECTORY | O_NOCTTY | O_NOFOLLOW | O_NONBLOCK)); if (fd < 0) - return false; + return errno; dirp = fdopendir (fd); if (dirp == NULL) { + saved_errno = errno; close (fd); - return false; + return saved_errno; } errno = 0; - dp = readdir_ignoring_dot_and_dotdot (dirp); + no_direntries = !readdir_ignoring_dot_and_dotdot (dirp); saved_errno = errno; closedir (dirp); - errno = saved_errno; - if (dp != NULL) - return false; - return saved_errno == 0 ? true : false; + return no_direntries && saved_errno == 0 ? -1 : saved_errno; } /* Factor out some of the common --help and --version processing code. */ diff --git a/tests/rm/rm-readdir-fail.sh b/tests/rm/rm-readdir-fail.sh index a77d5225f..9dbf9380c 100755 --- a/tests/rm/rm-readdir-fail.sh +++ b/tests/rm/rm-readdir-fail.sh @@ -112,6 +112,7 @@ done # (with ENOENT in this case but it could be anything). cat <<EOF > exp rm: cannot remove 'dir' +Failed to get dirent rm: traversal failed: dir EOF sed 's/\(rm:.*\):.*/\1/' errt > err || framework_failure_ |