
CVE-2026-43499 exploit adapter for MT6985 MediaTek Dimensity 9300 (vivo PD2241)
Conclusion: Spent 68M tokens, no root obtained.
Reason: KernelSnitch timing attack is unreliable on MTK Dimensity 9200, andCONFIG_PANIC_ON_OOPS=yleaves no room for trial and error.
This article documents the complete troubleshooting process for future reference.
| Item | Value |
|---|
| Device | vivo PD2241 (Dimensity 9200 / MT6985), Android 15 |
| Firmware | PD2241_A_15.2.10.2.W10.V000L1 |
| Kernel | 5.15.178-android13-8-gfb31f5bdd612-dirty |
| Bootloader | Locked (ro.boot.flash.locked=1) |
| SELinux | Enforcing |
| panic_on_oops | Enabled → any kernel OOPS = instant reboot |
| Exploit | CyberMeowfia — CVE-2026-43499 (IonStack) |
| Source tree | android_15.0_kernel_MT6985 (5.15.178) — does not match device version (source is android15 GKI, device runs android13 GKI) |
Extracted from arch/arm64/include/asm/memory.h + include/linux/fs.h + android/abi_gki_aarch64.xml:
KIMAGE_TEXT_BASE = 0xffffffc008000000, VA_BITS=39, DIRECT_MAP=256GBlayout-offset-in-bits)iopoll (android13 GKI), IOCTL=0x48, open=0x68atomic_t usage = 4 bytes, uid=0x04slab_cache=0x18Key finding: Source is android15 GKI, device is android13 GKI — task_struct offsets differ by 0x40~0x88 bytes, cannot blindly copy from source.
OTA zip (8.3GB)
→ payload.bin (8.2GB)
→ payload_dumper → boot.img (96MB, v4 header)
→ LZ4 decompress → Image (50MB ARM64)
→ kallsyms-finder → 187810 symbols
Extracted symbols from two firmware versions (15.2.7.6 / 15.2.10.2) — the same symbol differs by 10KB~200KB between versions, must use the correct version.
# In rt_mutex_adjust_pi:
LDR x21, [x19, #0x8b0] → pi_blocked_on = 0x8b0 (android15 value, not frankel)
This confirmed the task_struct layout is the android15 branch, not frankel's android13.
NDK r29, make PROJECT=android_15.0_kernel_MT6985 → preload.so (150KB).
[+] preload starting pid=25414
[+] p0 profile ... all symbols loaded correctly
[-] KernelSnitch mm_struct leak failed ← sometimes absent (occasional success)
[+] slide child context route=pselect ← slide KASLR leak child process started
[kernel panic] ← rt_mutex_adjust_prio_chain+0x1b0
Crash instruction (capstone):
ldar w8, [x27] ; x27 = waiter->lock (loaded from [x28, #0x38])
; x27 value is garbage → no page table mapping → translation fault
; → die() → panic → reboot
KernelSnitch is the entry point of the entire exploit — it leaks the mm_struct address through timing differences in the futex hash bucket:
MT6985 has CONFIG_KASAN_HW_TAGS=y → kernel tags slab allocations with MTE tags
mm_struct pointers carry KASAN tags → futex_hash computed on tagged pointer
But bruteforce scans the direct map (untagged addresses) → computed hash doesn't match
Even with MTE tag iteration (0-14, 15 total), on VA_BITS=39 systems
tag bits (bit56-59) overlap with sign-extension bits → some tag combinations produce invalid addresses → missed detections
Pixel devices don't have KASAN_HW_TAGS, so this mechanism works there. MTK doesn't.
CONFIG_PANIC_ON_OOPS Is the KillerPixel: kernel OOPS → dump_stack → keeps running → exploit can retry
MT6985: kernel OOPS → die() → panic() → instant reboot → no room for trial and error
A slight deviation in the π chain corruption causes total collapse; on Pixel a deviation just means "this attempt failed, try another set of addresses."
And the bootloader is locked (flash.locked=1) → cannot flash a custom kernel to remove this option.
Source tree: 5.15.178 android15 GKI
Device: 5.15.178-android13 (vivo vendor)
Although the major version is 5.15.178 for both, the GKI branches differ (android13 vs android15), so key struct layouts like task_struct/cred are inconsistent. After repeatedly switching between frankel/android15 offset sets, disassembly was ultimately required to lock it down.
| Change | Purpose | Result |
|---|---|---|
THRESHOLD_MULT 10→5→3 | Lower collision detection threshold | <5 too many false positives |
APPENDED_FUTEXES 4096→8192 | Increase hash chain difference | No effect |
REPEAT_MEASUREMENT/AVERAGE | Increase sampling precision | No effect |
MTE=1 | Make bruteforce iterate tags | Slower, actually fewer crashes |
MM_STRUCT_SZ 0x500→0x400 | Fix mm_struct stride | Necessary, ABI is actually 992 bytes |
IDENTITY_END 64GB→256GB | Expand scan range | Too slow (MTE iteration), still no match |
| TASK offsets: android15↔frankel | Lock down correct offsets | Disassembly confirms android15 |
| FOPS offsets: android15↔frankel | android13 has no iopoll | Used frankel |
In exploit/targets/android_15.0_kernel_MT6985/target.h:
| Category | Confidence | Verification Method |
|---|---|---|
| Memory layout | Correct | memory.h calculation + kallsyms _text verification |
| Symbol offsets (22) | Correct | Extracted from 15.2.10.2 boot.img |
| task_struct offsets | Correct | ABI XML + capstone disassembly (pi_blocked_on=0x8b0) |
| FOPS offsets | Incorrect (fixed 2026-07-31) | Original values copied from frankel (android13 has no iopoll); source tree ABI actually has iopoll@0x30, ioctl=0x50, open=0x70 — see VERIFICATION.md |
| CRED offsets | Correct (verified) | ABI XML: uid=0x04, securebits=0x24, caps=0x28, security=0x78 |
Assembly compiles, it runs, but loses at the last mile.
CONFIG_PANIC_ON_OOPS — either flash a custom kernel (requires unlocking the bootloader), or find an MT6985 device where this option is disabled by default/proc/self/pagemap — restricted on this device (returns all zeros)/proc/mtk_*) — exist but require further analysissymbols/kallsyms_PD2241_15.2.10.2.txt — complete 15.2.10.2 symbol table, directly usable by othersdevice_config.txt — actual device kernel config, shows what the vendor changedexploit/targets/android_15.0_kernel_MT6985/target.h — struct offsets verifiedscripts/server_compile.py — automated compilation, fast rebuild after parameter changestar -xf may fail on large zips, use Python zipfile or manually extract firstupdate_metadata_pb2.py requires protobuf 5.x, need to manually remove the runtime_version import line./preload.so directly segfaults: Must use /system/bin/linker64 /data/local/tmp/preload.solayout-offset-in-bits is computed by the compiler, 100x more accurate than manually counting 5000 bytesCONFIG_ANDROID_VENDOR_OEM_DATA=y, CONFIG_SCHED_INFO=y, CONFIG_RSC_* → deviates from standard GKIboot.img → kernel.bin → LZ4 decompress → Image → kallsyms-finder → symbol table07/28 Downloaded CyberMeowfia repository + MT6985 source
07/29 Source analysis (memory.h, fs.h, ABI XML, various structs)
Firmware unpacking (payload.bin → boot.img → Image)
Symbol extraction (kallsyms-finder → 187810 symbols)
Multiple compilation rounds + multiple crashes + disassembly verification
5 automatic retries → all failed
Wrote this article
-------------------------------------------
Total: ~68M tokens, 0 root shells
2026-07-29, lived to tell the tale
After obtaining android_15.0_kernel_MT6985.tar.gz (5.15.178 source tree + ABI XML), performed
full cross-verification, see VERIFICATION.md for details. Summary:
target.h has been corrected, with the exploit's
built-in leak_kernel_base() real-device self-check as a fallback.KSNITCH_MTE_ENABLED=1 actually
takes effect (original util.c hardcoded mte=0);rt_mutex_adjust_prio_chain+0x1b0 is in the pselect/pi chain stage, earlier
than the FOPS self-check; after the above fixes, it's worth re-testing on real hardware.Device (PD2241, compiler251203103903) 8+ rounds of real-device testing:
MM_STRUCT_SZ=0x400 (original 0x500 caused scan grid
misalignment, producing only false positives) + MTE tag 0..15 iteration (device mm pointer tags
change) + untagging forged addresses. Now reliably finds the real mm_struct.rt_mutex_adjust_prio_chain+0x1b0 every round: pselect/pi chain timing
race (forged waiter doesn't land at the correct offset on the kernel stack). vivo's RSC
scheduler modified the futex/pi path, potentially breaking this race fundamentally. Slide stack
alignment has been parameterized as the SLIDE_SHIFT environment variable, scan not completed.Final decision: downgraded to paid bootloader unlock (no longer relying on this path).