/* This function is called once for every file system object that fts encounters. fts performs a depth-first traversal. A directory is usually processed twice, first with fts_info == FTS_D, and later, after all of its entries have been processed, with FTS_DP. Return RM_ERROR upon error, RM_USER_DECLINED for a negative response to an interactive prompt, and otherwise, RM_OK. */ static enum RM_status rm_fts (FTS *fts, FTSENT *ent, struct rm_options const *x) { 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))) { /* This is the first (pre-order) encounter with a directory that we cannot delete. Not recursive, and it's not an empty directory (if we're removing them) so arrange to skip contents. */ int err = x->remove_empty_directories ? ENOTEMPTY : EISDIR; error (0, err, _("cannot remove %s"), quote (ent->fts_path)); mark_ancestor_dirs (ent); fts_skip_tree (fts, ent); return RM_ERROR; } /* Perform checks that can apply only for command-line arguments. */ if (ent->fts_level == FTS_ROOTLEVEL) { if (strip_trailing_slashes (ent->fts_path)) ent->fts_pathlen = strlen (ent->fts_path); /* If the basename of a command line argument is "." or "..", diagnose it and do nothing more with that argument. */ if (dot_or_dotdot (last_component (ent->fts_accpath))) { error (0, 0, _("cannot remove directory: %s"), quote (ent->fts_path)); fts_skip_tree (fts, ent); return RM_ERROR; } /* If a command line argument resolves to "/" (and --preserve-root is in effect -- default) diagnose and skip it. */ if (ROOT_DEV_INO_CHECK (x->root_dev_ino, ent->fts_statp)) { ROOT_DEV_INO_WARN (ent->fts_path); fts_skip_tree (fts, ent); return RM_ERROR; } } { Ternary is_empty_directory; enum RM_status s = prompt (fts, ent, true /*is_dir*/, x, PA_DESCEND_INTO_DIR, &is_empty_directory); if (s == RM_OK && is_empty_directory == T_YES) { /* When we know (from prompt when in interactive mode) that this is an empty directory, don't prompt twice. */ s = excise (fts, ent, x, true); fts_skip_tree (fts, ent); } if (s != RM_OK) { mark_ancestor_dirs (ent); fts_skip_tree (fts, ent); } return s; } case FTS_F: /* regular file */ case FTS_NS: /* stat(2) failed */ case FTS_SL: /* symbolic link */ case FTS_SLNONE: /* symbolic link without target */ case FTS_DP: /* postorder directory */ case FTS_DNR: /* unreadable directory */ case FTS_NSOK: /* e.g., dangling symlink */ case FTS_DEFAULT: /* none of the above */ { /* With --one-file-system, do not attempt to remove a mount point. fts' FTS_XDEV ensures that we don't process any entries under the mount point. */ if (ent->fts_info == FTS_DP && x->one_file_system && FTS_ROOTLEVEL < ent->fts_level && ent->fts_statp->st_dev != fts->fts_dev) { mark_ancestor_dirs (ent); error (0, 0, _("skipping %s, since it's on a different device"), quote (ent->fts_path)); return RM_ERROR; } 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) return s; return excise (fts, ent, x, is_dir); } case FTS_DC: /* directory that causes cycles */ emit_cycle_warning (ent->fts_path); fts_skip_tree (fts, ent); return RM_ERROR; case FTS_ERR: /* Various failures, from opendir to ENOMEM, to failure to "return" to preceding directory, can provoke this. */ error (0, ent->fts_errno, _("traversal failed: %s"), quote (ent->fts_path)); fts_skip_tree (fts, ent); return RM_ERROR; default: error (0, 0, _("unexpected failure: fts_info=%d: %s\n" "please report to %s"), ent->fts_info, quote (ent->fts_path), PACKAGE_BUGREPORT); abort (); } }
/* Change the owner and/or group of the file specified by FTS and ENT to UID and/or GID as appropriate. If REQUIRED_UID is not -1, then skip files with any other user ID. If REQUIRED_GID is not -1, then skip files with any other group ID. CHOPT specifies additional options. Return true if successful. */ static bool change_file_owner (FTS *fts, FTSENT *ent, uid_t uid, gid_t gid, uid_t required_uid, gid_t required_gid, struct Chown_option const *chopt) { char const *file_full_name = ent->fts_path; char const *file = ent->fts_accpath; struct stat const *file_stats; struct stat stat_buf; bool ok = true; bool do_chown; bool symlink_changed = true; switch (ent->fts_info) { case FTS_D: if (chopt->recurse) { if (ROOT_DEV_INO_CHECK (chopt->root_dev_ino, ent->fts_statp)) { /* This happens e.g., with "chown -R --preserve-root 0 /" and with "chown -RH --preserve-root 0 symlink-to-root". */ ROOT_DEV_INO_WARN (file_full_name); /* Tell fts not to traverse into this hierarchy. */ fts_set (fts, ent, FTS_SKIP); /* Ensure that we do not process "/" on the second visit. */ ignore_value (fts_read (fts)); return false; } return true; } break; case FTS_DP: if (! chopt->recurse) return true; break; case FTS_NS: /* For a top-level file or directory, this FTS_NS (stat failed) indicator is determined at the time of the initial fts_open call. With programs like chmod, chown, and chgrp, that modify permissions, it is possible that the file in question is accessible when control reaches this point. So, if this is the first time we've seen the FTS_NS for this file, tell fts_read to stat it "again". */ if (ent->fts_level == 0 && ent->fts_number == 0) { ent->fts_number = 1; fts_set (fts, ent, FTS_AGAIN); return true; } if (! chopt->force_silent) error (0, ent->fts_errno, _("cannot access %s"), quote (file_full_name)); ok = false; break; case FTS_ERR: if (! chopt->force_silent) error (0, ent->fts_errno, "%s", quote (file_full_name)); ok = false; break; case FTS_DNR: if (! chopt->force_silent) error (0, ent->fts_errno, _("cannot read directory %s"), quote (file_full_name)); ok = false; break; case FTS_DC: /* directory that causes cycles */ if (cycle_warning_required (fts, ent)) { emit_cycle_warning (file_full_name); return false; } break; default: break; } if (!ok) { do_chown = false; file_stats = NULL; } else if (required_uid == (uid_t) -1 && required_gid == (gid_t) -1 && chopt->verbosity == V_off && ! chopt->root_dev_ino && ! chopt->affect_symlink_referent) { do_chown = true; file_stats = ent->fts_statp; } else { file_stats = ent->fts_statp; /* If this is a symlink and we're dereferencing them, stat it to get info on the referent. */ if (chopt->affect_symlink_referent && S_ISLNK (file_stats->st_mode)) { if (fstatat (fts->fts_cwd_fd, file, &stat_buf, 0) != 0) { if (! chopt->force_silent) error (0, errno, _("cannot dereference %s"), quote (file_full_name)); ok = false; } file_stats = &stat_buf; } do_chown = (ok && (required_uid == (uid_t) -1 || required_uid == file_stats->st_uid) && (required_gid == (gid_t) -1 || required_gid == file_stats->st_gid)); } /* This happens when chown -LR --preserve-root encounters a symlink-to-/. */ if (ok && FTSENT_IS_DIRECTORY (ent) && ROOT_DEV_INO_CHECK (chopt->root_dev_ino, file_stats)) { ROOT_DEV_INO_WARN (file_full_name); return false; } if (do_chown) { if ( ! chopt->affect_symlink_referent) { ok = (lchownat (fts->fts_cwd_fd, file, uid, gid) == 0); /* Ignore any error due to lack of support; POSIX requires this behavior for top-level symbolic links with -h, and implies that it's required for all symbolic links. */ if (!ok && errno == EOPNOTSUPP) { ok = true; symlink_changed = false; } } else { /* If possible, avoid a race condition with --from=O:G and without the (-h) --no-dereference option. If fts's stat call determined that the uid/gid of FILE matched the --from=O:G-selected owner and group IDs, blindly using chown(2) here could lead chown(1) or chgrp(1) mistakenly to dereference a *symlink* to an arbitrary file that an attacker had moved into the place of FILE during the window between the stat and chown(2) calls. If FILE is a regular file or a directory that can be opened, this race condition can be avoided safely. */ enum RCH_status err = restricted_chown (fts->fts_cwd_fd, file, file_stats, uid, gid, required_uid, required_gid); switch (err) { case RC_ok: break; case RC_do_ordinary_chown: ok = (chownat (fts->fts_cwd_fd, file, uid, gid) == 0); break; case RC_error: ok = false; break; case RC_inode_changed: /* FIXME: give a diagnostic in this case? */ case RC_excluded: do_chown = false; ok = false; break; default: abort (); } } /* On some systems (e.g., GNU/Linux 2.4.x), the chown function resets the `special' permission bits. Do *not* restore those bits; doing so would open a window in which a malicious user, M, could subvert a chown command run 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")), quote (file_full_name)); } if (chopt->verbosity != V_off) { bool changed = ((do_chown && ok && symlink_changed) && ! ((uid == (uid_t) -1 || uid == file_stats->st_uid) && (gid == (gid_t) -1 || gid == file_stats->st_gid))); if (changed || chopt->verbosity == V_high) { enum Change_status ch_status = (!ok ? CH_FAILED : !symlink_changed ? CH_NOT_APPLIED : !changed ? CH_NO_CHANGE_REQUESTED : CH_SUCCEEDED); char *old_usr = file_stats ? uid_to_name (file_stats->st_uid) : NULL; char *old_grp = file_stats ? gid_to_name (file_stats->st_gid) : NULL; describe_change (file_full_name, ch_status, old_usr, old_grp, chopt->user_name, chopt->group_name); free (old_usr); free (old_grp); } } if ( ! chopt->recurse) fts_set (fts, ent, FTS_SKIP); return ok; }