1

I want to run a cron job which deletes .txt files older than 7 days. I have two commands. The first command:

/usr/bin/find /var/www/example.com/wp-content/targetdir -name "*.txt" -type f -mtime +7 -exec rm -f {} \;

The second command:

/usr/bin/find /var/www/example.com/wp-content/targetdir -name "*.txt" -type f -mtime +7 -delete

Both the commands can be used be delete .txt files older than 7 days. I have read that the first command is associated with a race condition. Can one please elaborate the advantages of using the second command over the first command? What are the pros and cons of using one above the other?

1

1 Answer 1

4

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:

  1. root runs find . -type f -user joe -exec rm -f {} \;
  2. find runs and finds directory ./this/, holding some files owned by user joe
  3. another user renames ./this and creates a symlink with the same name, pointing somewhere else
  4. 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.

10
  • Can you elaborate on the working of exec vs execdir? From your answer execdir sounds like a good option Jan 8, 2022 at 11:51
  • @DawsonSmith, if you are in e.g. /tmp/ and run find . -type f -exec rm {} \;, when that ./this/hello.txt is encountered, find runs rm ./this/hello.txt from /tmp. But the same with -execdir changes directory to ./this (/tmp/this) first, and then runs rm hello.txt. Since find has an fd open on ./this, it can do it safely. It doesn't actually call chdir("./test"): that would involve the name lookup again; instead if calls fchdir(fd_of_dir_currently_processed).
    – ilkkachu
    Jan 8, 2022 at 11:56
  • So, according to the above explanation, /usr/bin/find /var/www/example.com/wp-content/targetdir -name ".txt" -type f -mtime +7 -exec rm -f {} \; and /usr/bin/find /var/www/example.com/wp-content/targetdir -name ".txt" -type f -mtime +7 -execdir rm -f {} \; perform the same functionality but it's just that execdir is much safer because of no call to chdir Jan 8, 2022 at 11:58
  • 1
    One of the races is explained in the Q&A I linked to in my first comment above: directories can be replaced with symlinks (as shown in your scenario). This is where execdir helps, as you explain. It might be worth mentioning find’s default handling of symlinks, since that plays a part too. Jan 8, 2022 at 12:08
  • 1
    @StephenKitt, though if they wanted to delete partial subtrees, then using -execdir rm -rf -- {} \; -prune instead of -delete could be useful. (depending on the conditions they want to set.) But with just files like here, -delete is simpler, like you said.
    – ilkkachu
    Jan 8, 2022 at 12:34

Not the answer you're looking for? Browse other questions tagged .