diff options
Diffstat (limited to 'src/remove.c')
-rw-r--r-- | src/remove.c | 1565 |
1 files changed, 1565 insertions, 0 deletions
diff --git a/src/remove.c b/src/remove.c new file mode 100644 index 0000000..59ee9e5 --- /dev/null +++ b/src/remove.c @@ -0,0 +1,1565 @@ +/* remove.c -- core functions for removing files and directories + Copyright (C) 88, 90, 91, 1994-2007 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 2, 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, write to the Free Software Foundation, + Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ + +/* Extracted from rm.c and librarified, then rewritten by Jim Meyering. */ + +#include <config.h> +#include <stdio.h> +#include <sys/types.h> +#include <setjmp.h> +#include <assert.h> + +#include "system.h" +#include "cycle-check.h" +#include "dirfd.h" +#include "error.h" +#include "euidaccess.h" +#include "euidaccess-stat.h" +#include "file-type.h" +#include "hash.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 "yesno.h" + +/* Avoid shadowing warnings because these are functions declared + in dirname.h as well as locals used below. */ +#define dir_name rm_dir_name +#define dir_len rm_dir_len + +#define obstack_chunk_alloc malloc +#define obstack_chunk_free free + +/* 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 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 = 10 + }; + +/* 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 + { + 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. */ +enum Prompt_action + { + PA_DESCEND_INTO_DIR = 2, + PA_REMOVE_DIR + }; + +/* Initial capacity of per-directory hash table of entries that have + been processed but not been deleted. */ +enum { HT_UNREMOVABLE_INITIAL_CAPACITY = 13 }; + +/* An entry in the active directory stack. + Each entry corresponds to an `active' directory. */ +struct AD_ent +{ + /* For a given active directory, this is the set of names of + entries in that directory that could/should not be removed. + For example, `.' and `..', as well as files/dirs for which + unlink/rmdir failed e.g., due to access restrictions. */ + Hash_table *unremovable; + + /* Record the status for a given active directory; we need to know + whether an entry was not removed, either because of an error or + because the user declined. */ + enum RM_status status; + + /* The directory's dev/ino. Used to ensure that a malicious user does + not replace a directory we're about to process with a symlink to + some other directory. */ + struct dev_ino dev_ino; +}; + +extern char *program_name; + +struct dirstack_state +{ + /* The name of the directory (starting with and relative to a command + line argument) being processed. When a subdirectory is entered, a new + component is appended (pushed). Remove (pop) the top component + upon chdir'ing out of a directory. This is used to form the full + name of the current directory or a file therein, when necessary. */ + struct obstack dir_stack; + + /* Stack of lengths of directory names (including trailing slash) + appended to dir_stack. We have to have a separate stack of lengths + (rather than just popping back to previous slash) because the first + element pushed onto the dir stack may contain slashes. */ + struct obstack len_stack; + + /* Stack of active directory entries. + The first `active' directory is the initial working directory. + Additional active dirs are pushed onto the stack as we `chdir' + into each directory to be processed. When finished with the + hierarchy under a directory, pop the active dir stack. */ + struct obstack Active_dir; + + /* Used to detect cycles. */ + struct cycle_check_state cycle_check_state; + + /* Target of a longjmp in case rm has to stop processing the current + command-line argument. This happens 1) when rm detects a directory + cycle or 2) when it has processed one or more directories, but then + is unable to return to the initial working directory to process + additional `.'-relative command-line arguments. */ + jmp_buf current_arg_jumpbuf; +}; +typedef struct dirstack_state Dirstack_state; + +/* 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 == -1 - ST->st_size. 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 = -1 - errno; + if (0 <= st->st_size) + return 0; + errno = -1 - st->st_size; + 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) +{ + free (x); +} + +static bool +hash_compare_strings (void const *x, void const *y) +{ + return STREQ (x, y) ? true : false; +} + +static inline void +push_dir (Dirstack_state *ds, const char *dir_name) +{ + size_t len = strlen (dir_name); + + /* Append the string onto the stack. */ + obstack_grow (&ds->dir_stack, dir_name, len); + + /* Append a trailing slash. */ + obstack_1grow (&ds->dir_stack, '/'); + + /* Add one for the slash. */ + ++len; + + /* Push the length (including slash) onto its stack. */ + obstack_grow (&ds->len_stack, &len, sizeof (len)); +} + +/* 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) +{ + 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); + memcpy (q, p, top_len - 1); + q[top_len - 1] = 0; + return q; +} + +static inline void +pop_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); + + assert (n_lengths > 0); + size_t top_len = length[n_lengths - 1]; + assert (top_len >= 2); + + /* Pop the specified length of file name. */ + assert (obstack_object_size (&ds->dir_stack) >= top_len); + obstack_blank (&ds->dir_stack, -top_len); + + /* Pop the length stack, too. */ + assert (obstack_object_size (&ds->len_stack) >= sizeof (size_t)); + obstack_blank (&ds->len_stack, -(int) sizeof (size_t)); +} + +/* Copy the SRC_LEN bytes of data beginning at SRC into the DST_LEN-byte + buffer, DST, so that the last source byte is at the end of the destination + buffer. If SRC_LEN is longer than DST_LEN, then set *TRUNCATED. + Set *RESULT to point to the beginning of (the portion of) the source data + in DST. Return the number of bytes remaining in the destination buffer. */ + +static size_t +right_justify (char *dst, size_t dst_len, const char *src, size_t src_len, + char **result, bool *truncated) +{ + const char *sp; + char *dp; + + if (src_len <= dst_len) + { + sp = src; + dp = dst + (dst_len - src_len); + *truncated = false; + } + else + { + sp = src + (src_len - dst_len); + dp = dst; + src_len = dst_len; + *truncated = true; + } + + *result = memcpy (dp, sp, src_len); + return dst_len - src_len; +} + +/* 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. */ + +#define full_filename(Filename) full_filename_ (ds, Filename) +static char * +full_filename_ (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; + + if (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; + + 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; + } + + buf = new_buf; + } + + if (filename_len == 1 && *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); + *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); + } + + return buf; +} + +static inline size_t +AD_stack_height (Dirstack_state const *ds) +{ + return obstack_object_size (&ds->Active_dir) / sizeof (struct AD_ent); +} + +static inline struct AD_ent * +AD_stack_top (Dirstack_state const *ds) +{ + return (struct AD_ent *) + ((char *) obstack_next_free (&ds->Active_dir) - sizeof (struct AD_ent)); +} + +static void +AD_stack_pop (Dirstack_state *ds) +{ + assert (0 < AD_stack_height (ds)); + + /* operate on Active_dir. pop and free top entry */ + struct AD_ent *top = AD_stack_top (ds); + if (top->unremovable) + hash_free (top->unremovable); + obstack_blank (&ds->Active_dir, -(int) sizeof (struct AD_ent)); +} + +static void +AD_stack_clear (Dirstack_state *ds) +{ + while (0 < AD_stack_height (ds)) + { + AD_stack_pop (ds); + } +} + +static Dirstack_state * +ds_init (void) +{ + Dirstack_state *ds = xmalloc (sizeof *ds); + obstack_init (&ds->dir_stack); + obstack_init (&ds->len_stack); + obstack_init (&ds->Active_dir); + return ds; +} + +static void +ds_clear (Dirstack_state *ds) +{ + obstack_free (&ds->dir_stack, obstack_finish (&ds->dir_stack)); + obstack_free (&ds->len_stack, obstack_finish (&ds->len_stack)); + while (0 < AD_stack_height (ds)) + AD_stack_pop (ds); + obstack_free (&ds->Active_dir, obstack_finish (&ds->Active_dir)); +} + +static void +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 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 *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 + diagnostic and longjump out. + 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. + + 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) +{ + 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. */ + 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); + + assert (AD_stack_height (ds)); + + if (1 < AD_stack_height (ds)) + { + struct stat sb; + 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))); + goto next_cmdline_arg; + } + + /* 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); + + if (fd < 0) + { + error (0, errno, _("FATAL: cannot open .. from %s"), + quote (full_filename (prev_dir))); + goto next_cmdline_arg; + } + + if (fstat (fd, &sb)) + { + error (0, errno, + _("FATAL: cannot ensure %s (returned to via ..) is safe"), + quote (full_filename ("."))); + goto close_and_next; + } + + /* Ensure that post-chdir dev/ino match the stored ones. */ + if ( ! SAME_INODE (sb, top->dev_ino)) + { + error (0, 0, _("FATAL: directory %s changed dev/ino"), + quote (full_filename ("."))); + close_and_next:; + close (fd); + + next_cmdline_arg:; + free (prev_dir); + longjmp (ds->current_arg_jumpbuf, 1); + } + *fdp = fd; + } + else + { + if (closedir (dirp) != 0) + { + error (0, errno, _("FATAL: failed to close directory %s"), + quote (full_filename (prev_dir))); + goto next_cmdline_arg; + } + *fdp = AT_FDCWD; + } + + return prev_dir; +} + +/* Initialize *HT if it is NULL. Return *HT. */ +static Hash_table * +AD_ensure_initialized (Hash_table **ht) +{ + if (*ht == NULL) + { + *ht = hash_initialize (HT_UNREMOVABLE_INITIAL_CAPACITY, NULL, hash_pjw, + hash_compare_strings, hash_freer); + if (*ht == NULL) + xalloc_die (); + } + + 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. */ +static void +AD_mark_as_unremovable (Dirstack_state *ds, char const *filename) +{ + AD_mark_helper (&AD_stack_top(ds)->unremovable, xstrdup (filename)); +} + +/* Mark the current directory as unremovable. I.e., mark the entry + in the parent directory corresponding to `.'. + This happens e.g., when an opendir fails and the only name + the caller has conveniently at hand is `.'. */ +static void +AD_mark_current_as_unremovable (Dirstack_state *ds) +{ + struct AD_ent *top = AD_stack_top (ds); + char *curr = top_dir (ds); + + assert (1 < AD_stack_height (ds)); + + --top; + AD_mark_helper (&top->unremovable, curr); +} + +/* Push an initial dummy entry onto the stack. + This will always be the bottommost entry on the stack. */ +static void +AD_push_initial (Dirstack_state *ds) +{ + struct AD_ent *top; + + /* Extend the stack. */ + obstack_blank (&ds->Active_dir, sizeof (struct AD_ent)); + + /* Fill in the new values. */ + top = AD_stack_top (ds); + top->unremovable = NULL; + + /* These should never be used. + Give them values that might look suspicious + in a debugger or in a diagnostic. */ + top->dev_ino.st_dev = TYPE_MAXIMUM (dev_t); + top->dev_ino.st_ino = TYPE_MAXIMUM (ino_t); +} + +/* Push info about the current working directory (".") onto the + active directory stack. DIR is the ./-relative name through + which we've just `chdir'd to this directory. DIR_SB_FROM_PARENT + is the result of calling lstat on DIR from the parent of DIR. + Longjump out (skipping the entire command line argument we're + dealing with) if `fstat (FD_CWD, ...' fails or if someone has + replaced DIR with e.g., a symlink to some other directory. */ +static void +AD_push (int fd_cwd, Dirstack_state *ds, char const *dir, + struct stat const *dir_sb_from_parent) +{ + struct AD_ent *top; + + push_dir (ds, dir); + + /* If our uses of openat are guaranteed not to + follow a symlink, then we can skip this check. */ + if (! HAVE_WORKING_O_NOFOLLOW) + { + struct stat sb; + if (fstat (fd_cwd, &sb) != 0) + { + error (0, errno, _("FATAL: cannot enter directory %s"), + quote (full_filename ("."))); + longjmp (ds->current_arg_jumpbuf, 1); + } + + if ( ! SAME_INODE (sb, *dir_sb_from_parent)) + { + error (0, 0, + _("FATAL: just-changed-to directory %s changed dev/ino"), + quote (full_filename ("."))); + longjmp (ds->current_arg_jumpbuf, 1); + } + } + + 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)); + + /* 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); + top->dev_ino.st_dev = dir_sb_from_parent->st_dev; + top->dev_ino.st_ino = dir_sb_from_parent->st_ino; + top->unremovable = NULL; +} + +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. */ +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 | O_DIRECTORY + | O_NOCTTY | O_NOFOLLOW | O_NONBLOCK)); + + 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 -1 if FILE is an unwritable non-symlink, + 0 if it is writable or some other type of file, + a positive error number 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 int +write_protected_non_symlink (int fd_cwd, + char const *file, + Dirstack_state const *ds, + struct stat *buf) +{ + if (cache_fstatat (fd_cwd, file, buf, AT_SYMLINK_NOFOLLOW) != 0) + return errno; + if (S_ISLNK (buf->st_mode)) + return 0; + /* Here, we know FILE is not a symbolic link. */ + + /* In order to be reentrant -- i.e., to avoid changing the working + directory, and at the same time to be able to deal with alternate + access control mechanisms (ACLs, xattr-style attributes) and + arbitrarily deep trees -- we need a function like eaccessat, i.e., + like Solaris' eaccess, but fd-relative, in the spirit of openat. */ + + /* In the absence of a native eaccessat function, here are some of + the implementation choices [#4 and #5 were suggested by Paul Eggert]: + 1) call openat with O_WRONLY|O_NOCTTY + Disadvantage: may create the file and doesn't work for directory, + may mistakenly report `unwritable' for EROFS or ACLs even though + perm bits say the file is writable. + + 2) fake eaccessat (save_cwd, fchdir, call euidaccess, restore_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. + 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)); + 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. + + This avoids the O(N**2) algorithm of method (3), and it also avoids + the failure-due-to-too-long-file-names of method (3), but it's fast + in the normal shallow case. It also avoids the lack-of-reentrancy + and the save_cwd problems. + Disadvantage; it uses a process slot for very-long file names, + and would be very slow for hierarchies with many such files. + + 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)); + Otherwise: look just at the file bits. Perhaps issue a warning + the first time this occurs. + + This is like (4), except for the "Otherwise" case where it isn't as + "perfect" as (4) but is considerably faster. It conforms to current + POSIX, and is uniformly better than what Solaris and FreeBSD do (they + mess up with long file names). */ + + { + /* This implements #5: */ + size_t file_name_len + = obstack_object_size (&ds->dir_stack) + strlen (file); + + if (MIN (PATH_MAX, 8192) <= file_name_len) + return - euidaccess_stat (buf, W_OK); + if (euidaccess (full_filename (file), W_OK) == 0) + return 0; + if (errno == EACCES) + return -1; + + /* Perhaps some other process has removed the file, or perhaps this + is a buggy NFS client. */ + return errno; + } +} + +/* 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, + return RM_USER_DECLINED. If not ignoring missing files and we + cannot lstat FILENAME, then return RM_ERROR. + + 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. */ +static enum RM_status +prompt (int fd_cwd, Dirstack_state const *ds, char const *filename, + struct stat *sbuf, + struct rm_options const *x, enum Prompt_action mode, + Ternary *is_empty) +{ + int write_protected = 0; + + *is_empty = T_UNKNOWN; + + if (x->interactive == RMI_NEVER) + return RM_OK; + + if (!x->ignore_missing_files + & ((x->interactive == RMI_ALWAYS) | x->stdin_tty)) + write_protected = write_protected_non_symlink (fd_cwd, filename, ds, sbuf); + + if (write_protected || x->interactive == RMI_ALWAYS) + { + if (write_protected <= 0 + && cache_fstatat (fd_cwd, filename, sbuf, AT_SYMLINK_NOFOLLOW) != 0) + { + /* This happens, e.g., with `rm '''. */ + write_protected = errno; + } + + if (write_protected <= 0) + { + /* Using permissions doesn't make sense for symlinks. */ + if (S_ISLNK (sbuf->st_mode) && x->interactive != RMI_ALWAYS) + return RM_OK; + + if (S_ISDIR (sbuf->st_mode) && !x->recursive) + write_protected = EISDIR; + } + + char const *quoted_name = quote (full_filename (filename)); + + if (0 < write_protected) + { + error (0, write_protected, _("cannot remove %s"), quoted_name); + return RM_ERROR; + } + + /* 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 (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); + } + + 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, struct stat *st) +{ + int saved_errno = errno; + bool is_dir = + (cache_fstatat (AT_FDCWD, 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 + +#define DO_UNLINK(Fd_cwd, Filename, X) \ + do \ + { \ + if (unlinkat (Fd_cwd, Filename, 0) == 0) \ + { \ + if ((X)->verbose) \ + printf (_("removed %s\n"), quote (full_filename (Filename))); \ + return RM_OK; \ + } \ + \ + if (ignorable_missing (X, errno)) \ + return RM_OK; \ + } \ + while (0) + +#define DO_RMDIR(Fd_cwd, Filename, X) \ + do \ + { \ + if (unlinkat (Fd_cwd, Filename, AT_REMOVEDIR) == 0) /* rmdir */ \ + { \ + if ((X)->verbose) \ + printf (_("removed directory: %s\n"), \ + quote (full_filename (Filename))); \ + return RM_OK; \ + } \ + \ + if (ignorable_missing (X, errno)) \ + return RM_OK; \ + \ + if (errno == ENOTEMPTY || errno == EEXIST) \ + return RM_NONEMPTY_DIR; \ + } \ + 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 stat *st, + struct rm_options const *x, struct dirent const *dp) +{ + Ternary is_empty_directory; + enum RM_status s = prompt (fd_cwd, ds, filename, st, x, PA_DESCEND_INTO_DIR, + &is_empty_directory); + bool known_to_be_dir = (cache_stat_ok (st) && S_ISDIR (st->st_mode)); + + if (s != RM_OK) + return s; + + /* Why bother with the following if/else block? Because on systems with + an unlink function that *can* unlink directories, we must determine the + type of each entry before removing it. Otherwise, we'd risk unlinking + an entire directory tree simply by unlinking a single directory; then + all the storage associated with that hierarchy would not be freed until + the next fsck. Not nice. To avoid that, on such slightly losing + systems, we need to call lstat to determine the type of each entry, + and that represents extra overhead that -- it turns out -- we can + avoid on non-losing systems, since there, unlink will never remove + a directory. Also, on systems where unlink may unlink directories, + we're forced to allow a race condition: we lstat a non-directory, then + go to unlink it, but in the mean time, a malicious someone could have + replaced it with a directory. */ + + if (cannot_unlink_dir ()) + { + if (known_to_be_dir && ! x->recursive) + { + error (0, EISDIR, _("cannot remove %s"), + quote (full_filename (filename))); + return RM_ERROR; + } + + /* is_empty_directory is set iff it's ok to use rmdir. + Note that it's set only in interactive mode -- in which case it's + an optimization that arranges so that the user is asked just + once whether to remove the directory. */ + if (is_empty_directory == T_YES) + DO_RMDIR (fd_cwd, filename, x); + + /* 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)) || known_to_be_dir) + return RM_NONEMPTY_DIR; + + DO_UNLINK (fd_cwd, filename, x); + + /* 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, st)) + errno = EISDIR; + + if (! x->recursive + || (cache_stat_ok (st) && !S_ISDIR (st->st_mode))) + { + 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. */ + bool is_dir; + + if (cache_statted (st)) + is_dir = known_to_be_dir; + else + { + if (dp && DT_IS_KNOWN (dp)) + is_dir = DT_MUST_BE (dp, DT_DIR); + else + { + if (fstatat (fd_cwd, filename, st, AT_SYMLINK_NOFOLLOW)) + { + if (ignorable_missing (x, errno)) + return RM_OK; + + error (0, errno, _("cannot remove %s"), + quote (full_filename (filename))); + return RM_ERROR; + } + + is_dir = !! S_ISDIR (st->st_mode); + } + } + + if (! is_dir) + { + /* At this point, barring race conditions, FILENAME is known + to be a non-directory, so it's ok to try to unlink it. */ + DO_UNLINK (fd_cwd, filename, x); + + /* unlink failed with some other error code. report it. */ + error (0, errno, _("cannot remove %s"), + quote (full_filename (filename))); + return RM_ERROR; + } + + if (! x->recursive) + { + error (0, EISDIR, _("cannot remove %s"), + quote (full_filename (filename))); + return RM_ERROR; + } + + if (is_empty_directory == T_YES) + { + DO_RMDIR (fd_cwd, filename, x); + /* Don't diagnose any failure here. + It'll be detected when the caller tries another way. */ + } + } + + return RM_NONEMPTY_DIR; +} + +/* Given FD_CWD, the file descriptor for an open directory, + open its subdirectory F (F is already `known' to be a directory, + so if it is no longer one, someone is playing games), return a DIR* + pointer for F, and put F's `stat' data in *SUBDIR_SB. + Upon failure give a diagnostic and return NULL. + If PREV_ERRNO is nonzero, it is the errno value from a preceding failed + 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 use a nonnnull CWD_ERRNO the first + time this function is called for each command-line-specified directory. + 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, + int *cwd_errno ATTRIBUTE_UNUSED) +{ + 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 < 0) + return NULL; + else if (fstat (fd_sub, subdir_sb) != 0) + saved_errno = errno; + else if (S_ISDIR (subdir_sb->st_mode)) + { + 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; +} + +/* Remove entries in the directory open on DIRP + Upon finding a directory that is both non-empty and that can be chdir'd + into, return RM_OK and set *SUBDIR and fill in SUBDIR_SB, where + SUBDIR is the malloc'd name of the subdirectory if the chdir succeeded, + NULL otherwise (e.g., if opendir failed or if there was no subdirectory). + Likewise, SUBDIR_SB is the result of calling lstat on SUBDIR. + Return RM_OK if all entries are removed. Return RM_ERROR if any + entry cannot be removed. Otherwise, return RM_USER_DECLINED if + the user declines to remove at least one entry. Remove as much as + possible, continuing even if we fail to remove some entries. */ +static enum RM_status +remove_cwd_entries (DIR **dirp, + Dirstack_state *ds, char **subdir, struct stat *subdir_sb, + struct rm_options const *x) +{ + struct AD_ent *top = AD_stack_top (ds); + enum RM_status status = top->status; + size_t n_unlinked_since_opendir_or_last_rewind = 0; + + assert (VALID_STATUS (status)); + *subdir = NULL; + + while (1) + { + struct dirent const *dp; + enum RM_status tmp_status; + const char *f; + + /* Set errno to zero so we can distinguish between a readdir failure + and when readdir simply finds that there are no more entries. */ + errno = 0; + dp = readdir_ignoring_dot_and_dotdot (*dirp); + if (dp == NULL) + { + if (errno) + { + /* fall through */ + } + 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 + NULL-return may be the symptom of a buggy readdir. */ + rewinddir (*dirp); + n_unlinked_since_opendir_or_last_rewind = 0; + continue; + } + break; + } + + f = dp->d_name; + + /* Skip files we've already tried/failed to remove. */ + if ( ! AD_is_removable (ds, f)) + continue; + + /* Pass dp->d_type info to remove_entry so the non-glibc + 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. */ + cache_stat_init (subdir_sb); + tmp_status = remove_entry (dirfd (*dirp), ds, f, subdir_sb, x, dp); + switch (tmp_status) + { + case RM_OK: + /* Count how many files we've unlinked since the initial + opendir or the last rewinddir. On buggy systems, if you + remove too many, readdir returns NULL even though there + remain unprocessed directory entries. */ + ++n_unlinked_since_opendir_or_last_rewind; + break; + + case RM_ERROR: + case RM_USER_DECLINED: + AD_mark_as_unremovable (ds, f); + UPDATE_STATUS (status, tmp_status); + break; + + case RM_NONEMPTY_DIR: + { + DIR *subdir_dirp = fd_to_subdirp (dirfd (*dirp), f, + x, errno, subdir_sb, NULL); + if (subdir_dirp == NULL) + { + status = 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 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) + { + error (0, 0, _("failed to close directory %s"), + quote (full_filename ("."))); + status = RM_ERROR; + } + *dirp = subdir_dirp; + + break; + } + } + + /* Record status for this directory. */ + UPDATE_STATUS (top->status, status); + + if (*subdir) + break; + } + + /* Ensure that *dirp is not NULL and that its file descriptor is valid. */ + assert (*dirp != NULL); + assert (0 <= fcntl (dirfd (*dirp), F_GETFD)); + + return status; +} + +/* Do this after each call to AD_push or AD_push_initial. + Because the status = RM_OK bit is too remove-specific to + go into the general-purpose AD_* package. */ +#define AD_INIT_OTHER_MEMBERS() \ + do \ + { \ + AD_stack_top(ds)->status = RM_OK; \ + } \ + while (0) + +/* Remove the hierarchy rooted at DIR. + Do that by changing into DIR, then removing its contents, then + returning to the original working directory and removing DIR itself. + Don't use recursion. Be careful when using chdir ".." that we + return to the same directory from which we came, if necessary. + Return an RM_status value to indicate success or failure. */ + +static enum RM_status +remove_dir (int fd_cwd, Dirstack_state *ds, char const *dir, + struct stat *dir_st, + struct rm_options const *x, int *cwd_errno) +{ + enum RM_status status; + 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 + (unlinkat, in our caller) and fd_to_subdirp's openat call. But on most + systems, even those without openat, this isn't a problem, since we ensure + that opening a symlink will fail, when that is possible. Otherwise, + 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_st, cwd_errno); + + if (dirp == NULL) + { + /* 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))); + } + + 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_st); + AD_INIT_OTHER_MEMBERS (); + + status = RM_OK; + + while (1) + { + char *subdir = NULL; + struct stat subdir_sb; + enum RM_status tmp_status; + + tmp_status = remove_cwd_entries (&dirp, ds, &subdir, &subdir_sb, x); + + if (tmp_status != RM_OK) + { + UPDATE_STATUS (status, tmp_status); + AD_mark_current_as_unremovable (ds); + } + if (subdir) + { + 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); + 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 -- 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, &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. */ + struct stat empty_st; + Ternary is_empty; + enum RM_status s = prompt (fd, ds, empty_dir, + 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; + } + + if (unlinkat (fd, empty_dir, AT_REMOVEDIR) == 0) + { + if (x->verbose) + printf (_("removed directory: %s\n"), + quote (full_filename (empty_dir))); + } + else + { + error (0, errno, _("cannot remove directory %s"), + quote (full_filename (empty_dir))); + AD_mark_as_unremovable (ds, empty_dir); + status = RM_ERROR; + UPDATE_STATUS (AD_stack_top(ds)->status, status); + } + } + + free (empty_dir); + + 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); + } + } + } + + /* If the first/final hash table of unremovable entries was used, + free it here. */ + AD_stack_pop (ds); + + closedir_and_return:; + if (dirp != NULL && closedir (dirp) != 0) + { + error (0, 0, _("failed to close directory %s"), + quote (full_filename ("."))); + status = RM_ERROR; + } + + return status; +} + +/* Remove the file or directory specified by FILENAME. + Return RM_OK if it is removed, and RM_ERROR or RM_USER_DECLINED if not. */ + +static enum RM_status +rm_1 (Dirstack_state *ds, char const *filename, + struct rm_options const *x, int *cwd_errno) +{ + char const *base = last_component (filename); + if (dot_or_dotdot (base)) + { + error (0, 0, _(base == filename + ? "cannot remove directory %s" + : "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 (); + + enum RM_status status = remove_entry (AT_FDCWD, ds, filename, &st, x, NULL); + if (status == RM_NONEMPTY_DIR) + { + /* In the event that remove_dir->remove_cwd_entries detects + a directory cycle, arrange to fail, give up on this FILE, but + continue on with any other arguments. */ + if (setjmp (ds->current_arg_jumpbuf)) + status = RM_ERROR; + else + status = remove_dir (AT_FDCWD, ds, filename, &st, x, cwd_errno); + + AD_stack_clear (ds); + } + + ds_clear (ds); + return status; +} + +/* Remove all files and/or directories specified by N_FILES and FILE. + Apply the options in X. */ +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 (); + int cwd_errno = 0; + size_t i; + + for (i = 0; i < n_files; i++) + { + if (cwd_errno && IS_RELATIVE_FILE_NAME (file[i])) + { + error (0, 0, _("cannot remove relative-named %s"), quote (file[i])); + status = RM_ERROR; + } + 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_errno) + { + error (0, cwd_errno, + _("cannot restore current working directory")); + status = RM_ERROR; + } + + ds_free (ds); + + return status; +} |