Plenty of kernel bugs are "exploitable in theory": a use-after-free where you must reclaim the freed object with controlled data before the kernel touches it again, or a double-fetch where a value must change between two reads. The window is microseconds — racing it blind is a coin flip. The trick is to stop flipping coins and pause the kernel at exactly the wrong moment for it and the right one for you.
- arch
- amd64-64-little
- bug
- UAF — narrow race between free and reuse
- goal
- turn the race into a deterministic, single-shot reclaim
userfaultfd: handle your own page faults#
userfaultfd(2) lets a process register a memory range and handle the page
faults for it in userland. When anything — including the kernel doing
copy_from_user on your behalf — touches an unpopulated page in that range, the
faulting context blocks until your handler resolves it. That blocked context is
your lever:
|
The kernel is stuck inside the vulnerable operation while you rearrange the world beneath it. A 2-microsecond race becomes a deliberate sequence:
|
when userfaultfd is locked down: FUSE#
Defenders noticed. vm.unprivileged_userfaultfd=0 (default on many distros since
~5.2) restricts uffd, and seccomp sandboxes routinely block the syscall outright.
The fallback gives the same primitive through the filesystem:
- back a file with a FUSE (filesystem-in-userspace) mount;
mmapthat file and pass the mapping to the kernel;- when the kernel reads an un-cached page, it calls into your FUSE
readhandler — which blocks exactly like the uffd handler, same controllable window.
FUSE needs no special privilege beyond a user mount, so it survives where uffd doesn't. Same idea, different doorway.
a coin flip becomes a single deterministic shot
With the copy frozen, the reclaim isn't a race at all — it's a step:
the defensive view#
vm.unprivileged_userfaultfd=0and seccomp-blockinguserfaultfdclose the easy door; FUSE andmadvise(MADV_DONTNEED)tricks keep a narrower one open.- The deeper fix is the bug class: don't
copy_from_userinto a structure whose lifetime another thread can change mid-copy, and don't double-fetch user memory. The race primitive only matters because the window exists at all.
These aren't bugs themselves — they're race amplifiers. A flaky one-in-a- thousand reclaim becomes a 100% deterministic exploit because you stopped letting the kernel run at full speed and made one of its faults wait for your permission.