
Local privilege escalation exploit targeting CVE-2026-43499 for the Xiaomi 17T Pro (warhol) on Android 16 with MediaTek MT6993 SoC.
CVE-2026-43499 (IonStack) local privilege escalation, ported to the Xiaomi 17T Pro
(warhol) — MediaTek MT6993, Android 16.
GKI 6.12 / android16 only. The waiter overlay, the
pselectstack layout and every struct offset are pinned to that branch, so nothing here transfers to 6.6, 6.1 or 5.10 — those need a base built for them.generate_target.pyrefuses any other banner rather than emit a header that would fault on device.
Status: working. Verified on device 2026-07-26 from a clean boot, under both preloader variants —
uid=0(root) context=u:r:kernel:s0, SELinux permissive,suinstalled. Root is not persistent: re-run theLD_PRELOADline after each boot. The pselect race is not 100%: a failed run can panic the phone, and a retry after the reboot is normal. SeeWARHOL_PORT.mdfor the full ledger.
This device needs the kernel-MTE fix. Stock upstream popsicle cannot root it — it spins in
kernel page retry N/12forever. See Kernel MTE.
Builds come in two preloader flavours, PRELOADER=retail (default) and PRELOADER=eng.
They select different target directories so neither can overwrite the other's addresses —
see Preloader variant.
Facts below were read out of the retail full-OTA package — metadata, the A/B payload
manifest, and the boot/vendor_boot/dtbo images extracted from payload.bin.
| Item | Value | Source |
|---|---|---|
| Codename | warhol | pre-device=warhol |
| Build fingerprint | Xiaomi/warhol_global/warhol:16/BP2A.250605.031.A3/OS3.0.304.0.WPSJPXM:user/release-keys | post-build |
| Incremental | OS3.0.304.0.WPSJPXM (JP global) | post-build-incremental |
| Android | 16, SDK 36 | post-sdk-level=36 |
| Security patch | 2026-05-01 | post-security-patch-level |
| OTA type | A/B (payload.bin, CrAU, 39 partitions) | ota-type=AB |
| SoC | MediaTek MT6993 (Dimensity 9500) | mediatek,mt6993-* compatibles in the vendor_boot DTB; cmdline bootopt=64S3,32N2,64N2 |
| Kernel | 6.12.38-android16-5-g1d46253471dd-ab15048002-4k, clang 19.0.1 | banner read from the extracted boot.img |
| boot image | header v4, kernel 18,898,125 B, LZ4-legacy compressed, no ramdisk | tools/bootinfo.py |
The kernel matters more than the SoC here: warhol sits on the same GKI branch as
upstream popsicle (android16-5, 4K pages), one patchlevel apart — 6.12.38 vs
popsicle's verified 6.12.23. Struct offsets still have to be regenerated per build; only
the exploit shape carries over. A different OS3.0.x build needs its own target.h.
The exploit chain is version-locked to the GKI branch, not to the SoC. The rt_mutex
waiter layout, the pselect stack overlay and the pipe_buffer struct offsets all track the
kernel, so a 6.12/android16 base beats a same-vendor base on an older branch.
x-spy/CVE-2026-43499-popsicle is the only public implementation verified on the
6.12/android16 GKI line (Xiaomi 17 / Pro / Ultra, kernel 6.12.23-android16-5), so
source/ is taken from it and kept as close to upstream as possible — two lines aside
(see Kernel MTE, which this device needs to work at all).
Most of the MediaTek delta is confined to build-time target generation, in two pieces:
p0_phys_offset and p0_kernel_phys_load out of a Qualcomm xbl_config
partition, which warhol does not have. generate_target.py --dtb reads the DRAM base
from the /memory node of the vendor_boot FDT instead, rounding it down the same way
arm64_memblock_init does. The kernel's physical load address is read out of the
preloader's mb_kernel reservation with --preloader; the delta comes out 0 for this
device, and lk refuses to boot a kernel placed anywhere else.boot.img rather than as a bare
arm64 Image, so the generator decompresses it before analysis.WARHOL_PORT.md §2 has the full derivation.
MediaTek lk does not load the kernel to a compile-time constant — it looks up a DRAM
reservation named mb_kernel and asserts it landed exactly on it. That reservation is made
by the preloader, which is why the preloader build the phone is running is a build
input here at all: it is the one thing outside boot.img that can move
P0_KERNEL_PHYS_LOAD, and a wrong value there means a wrong linear-map alias and a dead
phone rather than a failed exploit.
Hence two variants, kept as separate target directories:
PRELOADER= | Preloader on the phone | Target directory | Status |
|---|---|---|---|
retail (default) | the shipping preloader_<device>.bin, byte-identical to the fastboot ROM's preloader_raw.img | targets/warhol-OS3.0.304.0.WPSJPXM/ | ✅ verified on device 2026-07-26 |
eng | the engineering build, preloader_<device>_eng.bin | targets/warhol-OS3.0.304.0.WPSJPXM-eng/ | ✅ verified on device 2026-07-26 |
make preload # PRELOADER=retail -> out/preload-<device>.so
make PRELOADER=eng preload # -> out/preload-<device>-eng.so
make both # both of the above, and print their sha256
Both artifacts are kept side by side under distinct names. On this firmware they come out byte-identical — that is the result of the analysis below, not a shortcut around it, so the two are still built and named separately.
Read the two preloaders' memory-layout tables yourself with:
python3 tools/preloader_memlayout.py <preloader.bin> --diff <preloader_eng.bin>
For this firmware the two tables are identical, mb_kernel.start = 0x80000000 in both,
so the eng target.h comes out byte-identical to the retail one and both builds produce the
same preload.so.
What actually reaches the exploit is the difference
P0_KERNEL_PHYS_LOAD − P0_PHYS_OFFSET, and its two terms do not rest on equally hard
evidence:
P0_KERNEL_PHYS_LOAD — measured out of the eng image, as mb_kernel.start above.P0_PHYS_OFFSET — was argued rather than measured, since it is memstart_addr,
which the kernel takes from the /memory node lk emits at runtime from what the preloader
reports after DRAM init, not from the static FDT the generator reads. The argument was
that the two builds share the whole DRAM path — same dramc/emi/mblock/memory_layout
sources, no memory-layout code among the eng-only strings — and that the eng preloader's
one extra reservation, a 3 MiB dynamic security_fe_rsv with mapping=1, can neither
displace a fixed-address entry nor carve a hole at the bottom of DRAM. The device run
settled it: the linear-map aliases landed under eng, so the term is right.Full derivation in targets/warhol-OS3.0.304.0.WPSJPXM-eng/NOTES.md.
Notes from the device run: