
Exploits CVE-2026-43499 on Android GKI 6.12 devices: deterministic arbitrary kernel read/write, KASLR bypass, and full root via LD_PRELOAD payload.
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. Per-target stack-depth offsets (
PSELECT_*word shifts) and slide behavior vary by device and are still being verified on-device. The m1q (ZF1) target is the current bring-up focus.
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): 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.SLIDE_LOGGERS_0_1 into the boot_id sysctl data via a linear-map
alias; the leaked value reconstructs stext.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.configfs_bin_write_iter
into the write_iter slot (+0x30) of the static .data ashmem
file_operations. Opening ashmem then yields arbitrary kernel read/write
through the configfs binary-fs path.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):
adb push build/m1q-BP4A.251205.006/bin/preload.so /data/local/tmp/preload.so
adb push build/embed/su_daemon_aarch64_pie /data/local/tmp/su_daemon_aarch64_pie
adb shell "chmod 755 /data/local/tmp/preload.so /data/local/tmp/su_daemon_aarch64_pie"
adb shell "LD_PRELOAD=/data/local/tmp/preload.so \
/data/local/tmp/su_daemon_aarch64_pie \
> /data/local/tmp/output.log 2>&1"
On success the daemon listens on /data/local/tmp/temp_su.sock and su is
installed under /apex/com.android.virt/bin.
exploit/
Makefile
assets/ # wallpaper.webp (embedded post-root artifact)
src/
main.c # run_exploit() entry point (shared)
preload.c # LD_PRELOAD constructor, su/wallpaper install
util.c # addr translation, page prep, configfs helpers
pipe.c # pipe-based physical read/write
root.c # cred patching / task walk
slide.c # KASLR slide (tracefs + boot_id routes)
fops.c # pselect corruption + fops manipulation
faketables.c # stage-3 fake page-table builders (3-level)
stage3.c # pgd-swap bridge child (no-op unless configured)
stage3_loop.S, stage3_poll.S # bridge blob + enter trampoline
su_daemon.c # root daemon (compiled as separate PIE)
su_blob.S, wallpaper_blob.S # embedded binaries
offset.h # pulls per-target target.h via -DTARGET_CONFIG_H
kernelsnitch/ # mm_struct futex-hash leak library
targets/<codename>-<build>/ # per-device target.h + optional overrides
poc/poc.c # standalone POC (separate from the exploit)
Targets override shared sources through the Makefile pick_src mechanism:
if src/targets/<PROJECT>/<file>.c exists it is used instead of the shared
src/<file>.c; otherwise the shared version is used.
-EDEADLK (errno 35). A missing cycle shows up as
success=1 with zero kernel effect.PSELECT_WAITER_WORD_SHIFT must be 0 (ZF1 stack depths verified
from ELF). Other targets still default to 1 — verify per device before
trusting them.p0_data_alias() uses DIRECT_MAP_BASE (the linear map), which differs
from P0_PAGE_OFFSET on ZF1. Data aliases for dereferenced kernel objects
must use the linear map.pr_error() is fatal (exit(-1)), unlike the warning/info macros.SLIDE_LOGGERS_0_1, not SLIDE_NFULNL_LOGGER;
the word is a non-randomized linear-map alias, so the boot_id route is
slide-independent (use it only at slide=0 — run the tracefs leak first).This is exploit code for Android devices and is intended for security research, authorized vulnerability testing, and defense work on hardware you own or have explicit permission to test. Misuse on devices you do not own is illegal. The author is not responsible for bricked devices, bricked kernels, or voided warranties.
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.| Variable | Default | Purpose |
|---|
PSELECT_ROUTE_SHIFT | compile-time | A/B fd_set word shift for the main route (m1q) |
SLIDE_SHIFT | compile-time | A/B fd_set word shift for the slide route |
PSELECT_ROUTE_DELAY_USEC | 50000 | Consumer delay before sched_setattr (must be > 0) |
SLIDE_CONSUME_USEC, SLIDE_ENTER_DELAY_USEC, SLIDE_CONSUMER_CORE | — | Slide route timing/pinning |
SKIP_SLIDE | 0 | Use direct-map fallback (slide = 0) |
SLIDE_ONLY | 0 | Run only the KASLR slide and exit |
SLIDE_P0_OFFSET, SLIDE_BOOTID_OFF | — | Override slide p0-alias / boot_id offsets |
KSNITCH_COLLISIONS | 4 | KernelSnitch collision count |
STAGE3 | 0 | Enable the pgd-swap bridge child phase (m1q) |
STAGE3_DRYRUN | 0 | Stage/verify tables then abort before the swap |
PAGE_RECLAIM_SENDS, PSELECT_SIMPLE_LAYOUT | — | Spray/fd-set layout tweaks |
TMP_PAGE_* / TMP_UNAME_* | — | tmp_page experiment route (m1q) |