
Android GKI 6.12 kernel exploit for CVE-2026-43499, chaining an rt_mutex rollback bug with pselect stack overwrite to gain root on Samsung and Pixel devices.
A Linux kernel exploit for the Android GKI 6.12 line (Samsung and Pixel
devices) targeting CVE-2026-43499: a buggy remove_waiter() rollback in
rt_mutex_start_proxy_lock() that uses current instead of waiter::task,
leaving a waiter's pi_blocked_on pointing at its (later-popped) kernel stack
rt_mutex_waiter. Combined with a pselect() fd_set stack overwrite and a
consumer sched_setattr-driven fake rb_tree walk, it yields a deterministic
kernel write primitive and full root.
The exploit runs as an LD_PRELOAD shared library (preload.so) and installs
a root su daemon plus wallpaper as post-root artifacts.
Status: active development. The m1q (ZF1) target is the bring-up focus. The pipei tmp_page-uname bootstrap is the current active route: the walk is proven clean on-device (build #33: per-child seeded rt_mutex regions) and the full 168-candidate sweep is in progress. The configfs CFI route is a dead end on m1q (Rust ashmem — no injectable fops slot) and remains the route for C-ashmem targets. Per-target offsets vary by device — verify against the actual kernel binary before trusting them.
When FUTEX_CMP_REQUEUE_PI requeues a waiter and the requeue's
deadlock-detection chain walk returns -EDEADLK, __rt_mutex_start_proxy_lock()
rolls back via remove_waiter() (rtmutex.c:1535). The rollback correctly
dequeues the waiter from the wait tree but clears the requeue caller's
pi_blocked_on instead of waiter->task->pi_blocked_on. The WAITER's
pi_blocked_on stays dangling at its stack rt_mutex_waiter, which is popped
once the futex times out.
After the timeout, the waiter's kernel stack region is reused: core_sys_select()
copies the three fd_sets into that stack buffer (the nfds < 344 stack path on
ZF1). A crafted fd_set word array re-materializes a fake rt_mutex_waiter /
fake task / fake rt_mutex (with an rb_tree root whose pointers are controlled).
A consumer thread then calls sched_setattr_tid(waiter) →
rt_mutex_adjust_pi() → rb_erase_cached, which produces an arbitrary
address write of a controlled value.
The whole primitive requires the PI chain cycle: the owner holds f_pi_target
and also blocks on f_pi_chain (held by the waiter), so the chain walk hits
owner → chain → waiter → target → owner and fails with -EDEADLK. Removing the
cycle makes the requeue return success with zero kernel effect.
sched_blocked_reason (m1q primary, on-device proven): a
blocked kworker's saved return PC (stack_trace_save_tsk) is read from
the ring buffer and compared against the compiled-in worker_thread
offset. Runs before any boot_id-route words are used so data-alias
write targets (data_addr() = p0 alias + slide_p0_offset) stay
correct at a nonzero slide.SLIDE_LOGGERS_0_1 into the boot_id sysctl data via a linear-map
alias; the leaked value reconstructs stext. Parked on m1q: its W1
target (SLIDE_RANDOM_BOOT_ID_DATA_OFF, low physical RAM) has
page-dependent writability — the slide moves the target across page
boundaries per boot, so ~6/7 runs fault. Only exercised explicitly via
SLIDE_FORCE_BOOTID (mechanism test) with the tree_pc/tree_left
redirected to the sprayed page.mm_struct-sized objects and use a futex-hash
collision to locate an mm_struct on the heap, leaking a kernel heap page
address used as the fake-object spray base. Waiters are detached (not
joined) on cleanup to avoid the kernel-stack OOM that crashed builds before
#26.write_iter slot and the ASHMEM_SET_NAME heap object is
a KVec whose layout does not line up with configfs_bin_write_iter's
private_data. m1q pivots to a kmalloc write instead: reclaim the leaked mm
order-3 block as pipe_inode_info objects and use the pselect W1 write
(*(tree_left) = tree_pc) to overwrite a candidate's tmp_page slot
(+0x90) with the UTS namespace page (init_uts_ns). A fanout write plants
a marker name at the sysname offset and uname() reports "CatOS" — a
verifiable kernel write with no static-object dependence. C-ashmem targets
keep the configfs path.preload.c installs the embedded su daemon (tmpfs-mounted into
/apex/com.android.virt/bin, plus adbd-namespace and local variants) and
swaps the wallpaper.The pselect fd_set words re-materialize a fake rt_mutex_waiter on the
waiter's kernel stack; a consumer thread's sched_setattr_tid(waiter) walks
it via rt_mutex_adjust_pi(). ZF1 word map (disasm-verified):
PSELECT_WAITER_WORD_SHIFT = 0, word 12 = task (@+0x50), word 13 = lock
(@+0x58), word 14 = wake_state (@+0x60 = 3). With a seeded fake rt_mutex
the walk completes cleanly: [7]'s rb_erase W1 *(tree_left)=tree_pc / W2
*(tree_pc&~3+8)=tree_left are the only kernel writes; [11] (setprio /
dequeue_pi) and the [9] wake are both skipped (local fake waiter at prio 100
keeps the stack node out of top-waiter). Every walk-entered completion since
build #19 survives on-device; the deterministic crashes before that were an
8-byte payload placement error (SKB_DATA_DELTA), not a walk-body fault.
Optional STAGE3=1 phase that forks a bridge child, swaps its mm->pgd
to a staged fake page table (3-level: PGD→L1→L2, RX loop leaf + RW stack leaf),
and lets the child run a register-only assembly blob that patches its own cred
through a 2MB physical scan window — all-RAM phys R/W without the configfs/pipe
primitives. Currently wired only into the m1q target and has not been run
on-device.
41 targets in src/targets/<codename>-<build>/, each requiring at minimum a
target.h (kernel symbol offsets, KIMAGE_TEXT_BASE, direct-map base, struct
offsets). Pixel targets (comet, tokay, tegu, caiman, komodo,
frankel, mustang, rango, stallion, blazer) override shared sources;
Samsung targets (m1q-*) add device-specific logic.
make list-projects # full list
Kernel struct layouts differ per device — offsets must be verified against the actual kernel binary / kallsyms for each target.
Output: build/<PROJECT>/bin/preload.so (shared lib loaded via LD_PRELOAD).
# Default project
CC=clang make
# Specific device target (default: blazer-CP2A.260605.012)
CC=clang make PROJECT=m1q-BP4A.251205.006
# Without CC=clang: uses NDK if found ($NDK_ROOT / $ANDROID_NDK_HOME /
# $ANDROID_NDK_ROOT), otherwise host clang + Android sysroot
make PROJECT=m1q-BP4A.251205.006
# Show build configuration
make info
# Clean
make clean
The build embeds a PIE su_daemon binary (src/su_daemon.c, built to
build/embed/su_daemon_aarch64_pie) and assets/wallpaper.webp into
preload.so via src/su_blob.S / src/wallpaper_blob.S.
Push the build outputs to /data/local/tmp, then run the exploit under
LD_PRELOAD. Run WITHOUT tee on a live bring-up target — tee buffers its own
stdio and loses the tail of the log on a kernel panic (exploit stdout is
unbuffered, so redirecting straight to a file preserves every line):