
CVE-2026-43499 (GhostLock) research on HUAWEI MatePad Pro 11 GOT-W29
Privilege escalation research on CVE-2026-43499 (rtmutex/futex-PI UAF, "GhostLock") targeting GOT-W29 (HarmonyOS 4.0, kernel 4.19.157-perf+).
Key findings:
sysctl_bootid and deriving the KASLR slide).| Item | Value |
|---|---|
| Model | HUAWEI MatePad Pro 11 GOT-W29 |
| SoC | Qualcomm kona (SM8250, Snapdragon 870) |
| OS | HarmonyOS 4.0 (104.0.0.136) |
| Kernel | 4.19.157-perf+ |
| VA | 39-bit, 4K pages, KASLR on |
In kernel/locking/rtmutex.c, remove_waiter() cleans up using current instead of waiter->task in the rt_mutex_start_proxy_lock() rollback path, leaving a dangling pi_blocked_on (stack UAF). Affects 2.6.39 ~ 7.1 (this kernel is in range). Upstream fix commit 3bfdc63936dd.
Confirmed on this device: source rtmutex.c:1110-1112, boot.elf decompilation, and real-device triggering all verified.
Create a PI cycle so that FUTEX_CMP_REQUEUE_PI returns -EDEADLK; the rollback triggers the remove_waiter bug, leaving a dangling pi_blocked_on (pointing to the rt_waiter on the waiter thread's kernel stack).
The old trigger had the waiter self-own the requeue target futex (self-own), which happened to hit this kernel's early owner==task check in task_blocks_on_rt_mutex (boot.elf 0x3808-0x3868), returning before writing pi_blocked_on → no dangling pointer is ever produced → the overlay placement was a misdiagnosis (no crash + boot_id unchanged).
PI cycle: owner FUTEX_LOCK_PI(target) holds the requeue target; waiter holds the chain futex; owner then blocks on the chain (cycle: waiter→target→owner→chain→waiter). During requeue, the chain walk detects rt_mutex_owner(chain)==top_task → -EDEADLK → rollback clears the wrong task's pi_blocked_on → the waiter's pi_blocked_on is left dangling. The owner must lower its priority (nice=10) so that after boost its prio differs from owner_waiter->prio; otherwise rt_mutex_waiter_equal returns early.
Under shell (uid 2000) with perf_event_paranoid=-1, perf_event_open(PERF_SAMPLE_IP, exclude_user=1) samples kernel text address clusters; aligning against known symbol offsets yields the slide.
samples=27651 kernel_ips=1685 lo=0xffffff948728176c hi=0xffffff9488ebfc7c
KASLR slide=0x147f200000 (40/40 IPs mapped into kernel text region verified)
runtime _stext=0xffffff9487280800
Tool: tools/perf_kaslr.c. Prerequisites: shell (Shizuku rish), no seccomp interception.
[M] CMP_REQUEUE_PI ret=-1 errno=35 (EDEADLK!)
[W] WAIT_REQUEUE_PI ret=-1 errno=110 (ETIMEDOUT) ← waiter returns
[M] waiter_returned=1 ← leaves dangling pi_blocked_on
Tool: tools/edeadlk_probe.c (variant 8+2+1 = 11, or 27).
rt_mutex_adjust_prio_chain step[7] performs rb_erase (single-left-child path) on the fake waiter: *(tree_left) = tree_pc + __rb_change_child incremental write. All offsets in target.h are measured from boot.elf disassembly.
See exploit/ghostlock-source/src/target.h. Key points:
A custom KPM (tools/kpm-debug/rtmutex-dbg.c, KernelPatch 0.13.5 inline-hook) rebuilds the overlay (tree/task/lock) during the fake walk and rewrites the next_lock parameter to empty_zero_page (KernelPatch _transit8 calls the original function with modified fargs), so that rt_mutex_adjust_prio_chain [3] next_lock==waiter->lock passes, [5] trylock succeeds on the zero lock, [6] is ownerless, and [7] rt_mutex_dequeue (rb_erase single-left-child) executes — sysctl_bootid is rewritten to &loggers[0][1], slide-kaslr-ok.
REPAIR3 waiter=0xffffff801ecdbc00 lock=0xffffffa7c4950000
slide boot_id_leaked_nfulnl_logger value=ffffffa7c4612320
slide-kaslr-ok base=ffffffa7c1280000 slide=00000027b9200000
Stack geometry is confirmed by boot.elf frame sizes: in __arm64_sys_futex(0x70) + do_futex(0x60+0x1a0), rt_waiter is at sp+0xc0 → depth 0x1b0; on the pselect path, stack_fds[0] is at depth 0x210, a difference of 0x60 = 12 words. Therefore word_i lands at stack_fds[12+i]: words 0-2 fall in the ex[2..4] input region (directly controllable), words 6-7 (task/lock) fall in res_in[3..4] (encoded via in[3..4] + POLLIN-ready fd), words 3-5/8-10 are left 0. pselect returns immediately due to the ready fd → the waiter busy-waits in userspace (signals disabled, zero syscalls) until the consumer triggers completion.
The overlay's task/lock words fall in res_in[3]/[4] (encoded by fd readiness). In practice, res_in[4] (lock) is deterministically overwritten on the pselect return path (rt_sigreturn frame residue) and never equals the payload's fake_lock; res_in[3] (task) is occasionally intact. Userspace mitigation (FP operations + sched_yield) can only reduce the do_notify_resume trigger rate to ~21%, but the lock overwrite is nearly certain.
Two other relevant facts:
rt_mutex_adjust_pi() has if (!owner) return 0;; when fake_lock has no owner, adjust_prio_chain is not called. popsicle (6.12) lacks this check. An owner-ful payload has been ported (fake_lock owner=fake_task|1), but cannot be reliably triggered because the lock word is always overwritten.After disassembling __arm64_sys_ppoll / do_sys_poll from boot.elf, it is confirmed: pollfd is a 16-byte structure (fd 4B + events 4B + revents 4B + pad 4B) that cannot carry the fake waiter's 64-bit task/lock words — the fd value is constrained (must be a real fd), events is only 4 bytes and non-contiguous, and revents is written by the kernel (not controllable).
smt878u / popsicle can be fully exploited: their stack geometry allows words to land in user-controllable in/out/ex (pselect's 3 fd_sets). The GOT-W29 waiter position (bits+0x60 → task/lock in res_in[3]/[4]) lacks this window. This is a kernel stack geometry difference, not an implementation flaw.
The app domain (untrusted_app) has no available KASLR channel (perf/kallsyms/pagemap/dmesg are all denied); CMP_REQUEUE_PI returns 1 (requeue success) without going through the EDEADLK rollback; the major_only hard short-circuit in non-root cpuset skips chain walk step[6]. The only entry point for changing QOS, /dev/iaware_qos_ctrl, is denied by SELinux. Therefore, normal app privileges cannot trigger this CVE.
A custom KernelPatch module (rtmutex-dbg) is used for real-device observation of the fake waiter and write primitive, compiled against LyraVoid/KernelPatch 0.13.5 (same source as FolkPatch) headers.
cd <KernelPatch>/kpms/rtmutex-dbg
make TARGET_COMPILE=aarch64-linux-android- \
CC=$PREFIX/bin/aarch64-linux-android-clang \
LD=$PREFIX/bin/aarch64-linux-android-ld
Produces rtmutex_dbg.kpm. Compile flags must include
-fno-pic -fno-pie -fno-asynchronous-unwind-tables -fno-unwind-tables
(already built into the Makefile): clang's default PIC produces GOT relocations, and the default .eh_frame generation produces R_AARCH64_PREL32 — neither is supported by the KPM loader → load fails with -1.
FolkPatch's superkey is su (not APatch's default KernelPatch). Use the sc_kpm_load tool (source sc_kpm_load.c):
adb shell /data/local/tmp/sc_kpm_load su /sdcard/Download/rtmutex_dbg.kpm # load
adb shell /data/local/tmp/sc_kpm_load unload rtmutex-dbg su # unload
adb shell /data/local/tmp/sc_kpm_load ctl rtmutex-dbg counts su # counters
run_rtmdbg_test.sh (on-device /data/local/tmp/ghostlock-test/): runs GhostLock as shell identity (GOT_SLIDE_NO_RT=1 real path), 0.5s sync loop to prevent log loss, dmesg -w written to disk, auto-collect after 90s (SIGSTOP to prevent soft-lock reboot). Before testing:
adb shell 'su -c "sh /sdcard/ghostlock-test/set_debug_no_reboot.sh"' # prevent reboot
[RTMDBG])REPAIR3: rebuilds overlay and rewrites next_lock parameter during write primitive verificationFAKEWALK skip: covered fake walk is skipped (no write, no crash)prio_chain[N] / prio_chain_ret: walk calls and return values (0=completed;
4294967261=-EDEADLK)do_select n=320: kernel-side res_in[3]/[4]futex op=13/14: CMP_REQUEUE_PI / WAIT_REQUEUE_PIHuawei oops/panic full stacks are recorded in /data/log/bbox/history.log.
tools/ verification tools (perf KASLR, EDEADLK probe, KPM toolchain)
target/ all measured offsets
exploit/ ported slide.c (including EDEADLK trigger modifications)
| hook | purpose |
|---|
rt_mutex_adjust_pi | records PI adjustments; skips when overlay is present (clears pi_blocked_on) |
rt_mutex_adjust_prio_chain | unconditionally skips fake walk; full waiter dump |
__arm64_sys_pselect6 / __arm64_sys_ppoll | clears _TIF_WORK_MASK on return path; fd_set observation |
do_select | kernel-side read of res_in[3]/[4] |
__arm64_sys_futex | traces WAIT_REQUEUE_PI / CMP_REQUEUE_PI |
rt_mutex_dequeue | confirms step[7] write primitive execution and tree shape |