The race I saw mentioned is this, from Stéphane Chazelas's comment to another question:
Note that -exec rm {} +
has race condition vulnerabilities which -delete
(where available) doesn't have. So don't use it on directories that are writeable by others. Some finds also have a -execdir
that mitigates against those vulnerabilities.
I didn't actually see this elaborated there(*), but the race I can see there relates to how -exec rm
passes the full path to the file, causing the tree to be traversed again within the rm
process.
After find
traverses the tree and evaluates the conditions it's given on the command line, rm
traverses the tree again, and it doesn't know about the conditions given to find
, so now they are not checked. With -delete
and -execdir
, find
traverses the tree, makes whatever checks it's given, and then deletes the file, all the time keeping a file descriptor open on the directories.
Someone could get in between and rename a file or directory between find
evaluating its conditions and rm
running. So, something like this:
- root runs
find . -type f -user joe -exec rm -f {} \;
find
runs and finds directory ./this/
, holding some files owned by user joe
- another user renames
./this
and creates a symlink with the same name, pointing somewhere else
find
runs rm -f ./this/hello.txt
, now following the symlink.
hello.txt
could now be owned by anyone, not just user joe
, and since find
was launched as root
, rm
also is, and it happily removes the file at the other end of the symlink. This is a classic time-of-check to time-of-use (TOCTTOU) vulnerability.
One can probably come up worse examples, but that's a general idea.
With -delete
or -execdir
, this can't happen, since the file descriptor find
has open on ./this
still points to the same directory, even if that directory is renamed. -execdir
runs rm
with it's working directory set to that directory (there's the fchdir()
system call that changes the working directory by file descriptor, again not going through a name lookup), while -delete
similarly deletes the using a name relative to the containing directory (with unlinkat()
).
Note that by default (i.e. without the -L
option), find
doesn't follow symlinks found when traversing the directory tree. This doesn't help here, since again rm
just passes the path it was given to the underlying system call, and symlinks are followed there as usual. It does mean that the replaced directory (./this
above) has to be an actual directory replaced with a symlink (or another directory) though, not a symlink replaced by another symlink.
(*)20 minutes after writing this, I noticed Stéphane's answer (linked below) did have a link to the GNU manual describing the same race issue. (sigh.)
The issues with -delete
implying -depth
, and making -prune
ineffective
is unrelated to that, and not a race condition. This is mentioned in Why did find with -delete erase the files in my /save/ directory when find without delete was not able to locate them? GNU coreutils find
seems to have acquired an error message for that:
$ find a -name foo -prune -o -name hello.txt -delete
find: The -delete action automatically turns on -depth, but -prune does nothing when -depth is in effect. If you want to carry on anyway, just explicitly use the -depth option.
Another difference is that -exec rm -f -- {} +
uses only standard tools, while -delete
is not standard, even if somewhat commonly supported. E.g. FreeBSD and GNU support it, but Busybox doesn't. See: find: "-exec rm {} ;" vs. "-delete" - why is the former widely recommended? on superuser.com.