Skip to content
KitploitKITPLOIT
ToolsBlog
Submit
ToolsBlog
Submit

Hacking, PenTest, and Cybersecurity Tools for Your Security Arsenal!

Kitploit is a directory of hacking, cybersecurity, and pentesting tools. Discover the latest project updates to find vulnerabilities, analyze systems, automate testing, and strengthen your security.

··Feeds·Contact·Privacy·© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
CVE-2026-43499-warhol-root — Local privilege escalation exploit targeting CVE-2026-43499 for the Xiaomi 17T Pro (warhol) on Android 16 with MediaTek MT6993 SoC. | Kitploit
Tools/GitHubGitHub/soralis0912/cve-2026-43499-warhol-root
Android SecurityPrivilege EscalationVulnerability AnalysisExploitationBinary Exploitation
GitHubsoralis0912/cve-2026-43499-warhol-root

CVE-2026-43499-warhol-root

Local privilege escalation exploit targeting CVE-2026-43499 for the Xiaomi 17T Pro (warhol) on Android 16 with MediaTek MT6993 SoC.

View Repository

Most Popular

View all →

Discover the most used tools by our community.

Explore all tools

Browse our collection of tools

View all tools →
Share
1125 days agoNot yet reviewed

warhol-root

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 pselect stack 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.py refuses 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, su installed. Root is not persistent: re-run the LD_PRELOAD line after each boot. The pselect race is not 100%: a failed run can panic the phone, and a retry after the reboot is normal. See WARHOL_PORT.md for the full ledger.

This device needs the kernel-MTE fix. Stock upstream popsicle cannot root it — it spins in kernel page retry N/12 forever. 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.


Target device

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.

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.


Why this base

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:

  • upstream reads 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.
  • MediaTek ships the kernel LZ4-legacy compressed inside boot.img rather than as a bare arm64 Image, so the generator decompresses it before analysis.

WARHOL_PORT.md §2 has the full derivation.


Preloader variant

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:

root@kitploit:~
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:

root@kitploit:~
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:

  • The BootROM accepts this eng preloader — it booted normally to Android, with ro.boot.verifiedbootstate=green and flash.locked=1 unchanged. That was an open question until it was tried; it is decided by the efuse'd root key hash, and this build is signed with the same keys as retail.
  • The eng preloader keeps META / factory download mode live: %s META DIS is a retail-only string, while eng carries a whole META implementation retail does not (Enable fastmeta., FAST META GPIO: %d, META_COM PORT: %d, read_meta_proinfo, …). It booted straight to Android here, but if an eng-flashed phone ever lands somewhere else, that is the likely cause and no target.h value is implicated.
  • If a later firmware moves mb_kernel, the two targets diverge — keeping them as separate directories is what stops that from being silent.

Kernel MTE

This kernel runs KASAN_HW_TAGS — /proc/cmdline carries kasan.stack_ring_size=524288 — so slab pointers carry an allocation tag in bits 59:56. Upstream popsicle assumes untagged pointers and therefore cannot root this device at all: it loops in [-] kernel page retry N/12 mode=1 indefinitely, with nothing in the log saying why.

Two lines in source/ fix it, and both are in warhol-mte-fix.patch:

Only the checks untag. The pointers themselves stay tagged, because the tag is the correct one for the memory they point at — which is exactly what a kernel dereference of them needs.

Do not read arm64.memtag.bootctl for this: that property is the userspace MTE control and says nothing about whether the kernel is tagging its own allocations.


Upstream / references

This repository is a port. Credit for the exploit chain belongs upstream.


Layout

root@kitploit:~
source/          exploit core (upstream popsicle + the kernel-MTE fix; see warhol-mte-fix.patch)
  src/           main.c slide.c fops.c pipe.c util.c preload.c su_daemon.c + kernelsnitch/
targets/         per-build generated target.h, one dir per fingerprint x preloader variant
  warhol-OS3.0.304.0.WPSJPXM/       PRELOADER=retail
  warhol-OS3.0.304.0.WPSJPXM-eng/   PRELOADER=eng
tools/
  payload_dump.py          extract partitions from an A/B payload.bin (verifies size+sha256)
  kernel_banner.py         read the Linux banner out of a boot.img
  bootinfo.py              dump boot / vendor_boot header fields
  generate_target.py       target.h generator; upstream's, MediaTek-only, plus kernel decompression
  preloader_memlayout.py   dump/diff a MediaTek preloader's static DRAM reservation table
  lz4legacy.py             LZ4 legacy-frame decompressor (kernel images)
out/             build artifacts (gitignored)

Build

Nothing builds until a target.h exists — the Makefile fails loudly rather than emitting a binary against the wrong kernel.

root@kitploit:~
# 1. extract boot.img straight out of the OTA zip (payload.bin is stored uncompressed,
#    so --base seeks into the zip; no need to unpack 7.6 GB first)
python3 tools/payload_dump.py <ota.zip> --base 5081 -p boot,vendor_boot,dtbo -o <dir>

# 2. confirm the kernel banner — every offset downstream is pinned to it
python3 tools/kernel_banner.py <dir>/boot.img
python3 tools/bootinfo.py <dir>/boot.img

# 3. generate the target header. --preloader reads the kernel's physical load address
#    out of the preloader's mb_kernel reservation; pass the preloader the phone runs.
python3 tools/generate_target.py \
  --boot <dir>/boot.img --dtb <dir>/vendor_boot.img --preloader <preloader.bin> \
  -o targets/warhol-OS3.0.304.0.WPSJPXM/target.h

# 4. build
make preload            # DEVICE=warhol-OS3.0.304.0.WPSJPXM, PRELOADER=retail by default

Without a preloader image to hand, --kernel-phys-delta 0 is the older form and gives the same header for this firmware — but it is an assertion rather than a reading, so prefer --preloader. Passing both makes the generator cross-check them and fail on a mismatch.

--base 5081 is the local-header offset of payload.bin for this particular OTA; read it from ota-property-files in META-INF/com/android/metadata for any other package.

Requires an Android NDK (NDK_ROOT / ANDROID_NDK_HOME) and llvm-objdump.

Run

root@kitploit:~
# <device> is the target name: <fingerprint> for PRELOADER=retail, <fingerprint>-eng for eng
adb push out/preload-<device>.so /data/local/tmp/preload.so
adb shell chmod 0644 /data/local/tmp/preload.so
adb shell "LD_PRELOAD=/data/local/tmp/preload.so /system/bin/true"
adb shell "/data/local/tmp/su -c id"

cred and the SELinux state are patched in memory, so this has to be repeated after every boot. A bootloader unlock is not required.


Disclaimer

For research on hardware you own. Running this can bootloop or brick a device and will void the warranty. No warranty of any kind.

Upstream projects are licensed by their respective authors; this port carries their terms.

Download Tool
ItemValueSource
Codenamewarholpre-device=warhol
Build fingerprintXiaomi/warhol_global/warhol:16/BP2A.250605.031.A3/OS3.0.304.0.WPSJPXM:user/release-keyspost-build
IncrementalOS3.0.304.0.WPSJPXM (JP global)post-build-incremental
Android16, SDK 36post-sdk-level=36
Security patch2026-05-01post-security-patch-level
OTA typeA/B (payload.bin, CrAU, 39 partitions)ota-type=AB
SoCMediaTek MT6993 (Dimensity 9500)mediatek,mt6993-* compatibles in the vendor_boot DTB; cmdline bootopt=64S3,32N2,64N2
Kernel6.12.38-android16-5-g1d46253471dd-ab15048002-4k, clang 19.0.1banner read from the extracted boot.img
boot imageheader v4, kernel 18,898,125 B, LZ4-legacy compressed, no ramdisktools/bootinfo.py
PRELOADER=Preloader on the phoneTarget directoryStatus
retail (default)the shipping preloader_<device>.bin, byte-identical to the fastboot ROM's preloader_raw.imgtargets/warhol-OS3.0.304.0.WPSJPXM/✅ verified on device 2026-07-26
engthe engineering build, preloader_<device>_eng.bintargets/warhol-OS3.0.304.0.WPSJPXM-eng/✅ verified on device 2026-07-26
FileChangeWhy
src/util.ckernelsnitch_setup(..., mte_enabled=1)with 0, the mm_struct search only tries untagged candidates and can never reach a tagged pointer — a structural miss, not bad luck
src/util.cis_kernel_ptr() / is_direct_ptr() untag before the range checka tagged pointer read out of kernel memory is otherwise rejected as not-in-linear-map (direct-entry-fatal reason=bad-task-or-cpu)
src/kernelsnitch/kernelsnitch.htag sweep < 15 → < 16tag 0xf is the untagged/match-all pointer, so the sweep now also covers a kernel without MTE — mte_enabled=1 is safe either way
RepositoryRole here
https://github.com/MobiusM/CVE-2026-43499Original CVE-2026-43499 PoC / crash trigger
https://github.com/x-spy/CVE-2026-43499-popsicleBase of this port. Xiaomi 17 Pro Max (popsicle), kernel 6.12.23-android16-5; source/ and tools/generate_target.py come from here
https://github.com/Kananosa/CVE-2026-43499-For-Xiaomi-17T-chagallClosest sibling device (Xiaomi 17T, chagall, also MediaTek). popsicle-derived, ships a prebuilt preload.so — its compiled-in constants confirmed the physical load offset
https://github.com/MiCode/Xiaomi_Kernel_OpenSourceXiaomi kernel sources, for cross-checking struct layout