Bug 1: Many XNU drivers save task_t's on the heap without bumping their refcount.

1. Attacker creates process A and B
2. B->A send task port Bt
3. A->XNU request IOKit framebuffer client for Bt
4. A ditches Bt, retains client
5. Kill B; Bt in client now dangling
6. Trigger creation of privileged C, unrelated to A & B
7. C inherits memory once used by Bt
8. A use retained framebuffer client to write C's memory

What's important to understand is that this is not just a single UAF, but a pattern of UAFs scattered throughout XNU.

Fix: at step 3, check to make sure the task being given to IOKit is owned by the task making the IOKit request.

Bug 2: IOKit drivers cache task details on their stack; the lifetime of that cached task is the lifetime of the IOKit kernel object, not of the program that made the request. In particular: if you execve() an SUID, the task_t is repurposed.

1. Attacker creates process A and B
2. B->XNU request IOKit framebuffer for Bt, Bc
3. B->A send client Bc
4. B execve /bin/su. B is now running as root.
5. A use retained framebuffer client to write B's memory

The tricky thing here is that this isn't just one bug, but a pattern of bugs: every place where a driver stashes a task_t on the heap and exposes functionality through a passable object is a place where colluding processes can potentially take advantage of SUIDs to raise privileges.

Fix: Lifetime of IOKit clients now tied to lifetime of creating process.

Bug 3: Even if a driver doesn't save a task_t on the heap, they're saved on the stack during the servicing of system calls and kernel mach message handlers, so there are race conditions.

1. Attacker creates process A and B
2. B->A send task port Bt
3a. A->XNU task_threads(Bt), retrieving thread ports for Bt
3b. (simultaneously) execve /bin/su. B is now running as root.
4a. task_threads converts Bt to a task_t
4b. execve modifies the same task_t to replace thread ports
4c. task_threads retrieves the (now privileged) thread ports.
5. A uses thread ports to overwrite registers and take control of B.

Fix: Kernel objects now check to see if a task_t has been touched by execve before returning them to userland. Even if you win the race, that failsafe prevents the kernel from giving you privileged objects.

Bug 4: You don't need the kernel to give you a privileged object directly; all you need is to be able to influence a privileged object.

1. Attacker creates process A and B
2. B->A send task port Bt
3a. A->XNU task_set_exception_port(Bt), wiring A to B's exceptions
3b. (simultaneously) execve /bin/su with rlimited stack. B is now running as root, briefly.
4a. task_set_exception_port converts Bt to a task_t
4b. execve modifies the same task_t to replace thread ports
4c. task_set_exception_port rewrites the exception port.
5. stack access in B, running /bin/su as root, causes a SEGV
6. XNU generates an exception message, passing with it the thread ports, to A
7. A uses thread ports to overwrite registers and take control of B.

Fix: table flip. Rewrite execve so it generates entirely new task_ts when loading binaries, rather than repurposing old task_t.

This is all pretty magnificent. What's best about it is that it totally justifies the title of the post: pretty much every place in XNU where they save a task_t creates a TOCTTOU bug.