## https://sploitus.com/exploit?id=B7F4C122-DCF4-5F88-B125-FCF4479408A9
> **AI-assisted project.** This research, exploit development, and documentation
> were produced with AI assistance using the models **GLM-5.3** and
> **DeepSeek V4.1 Flash**.
# amazon-mustang-hack
Root exploit research for the **Amazon Fire 7 9th gen (mustang, MT8163, Mali-T720)** on the
final firmware β **Fire OS 7.3.3.1, PS7331.4463N, kernel 4.9.117 (built 2025-05-03, SPL 2024-08-01)**.
Goal: LineageOS. Bootloader path is dead on this unit (patched bootrom β preloader-only via
CMD short), so the only remaining route is a software kernel exploit.
## Quick start
```
# one-shot: build, run the exploit (retries across the probabilistic reclaim),
# install a setuid-root su and verify it as an unprivileged user
nix-shell -p android-tools --run './run.sh' # add -p zig too if no zig
```
On success:
```
/data/metrics/su id # run a command as root
/data/metrics/su # interactive root shell
```
The reclaim wins roughly **1 boot in 3** and a loss panics/reboots the tablet;
`run.sh` just waits for the reboot and retries. SELinux is forced Permissive as
part of the exploit, so root is **runtime-only** β a reboot restores stock and
you re-run `run.sh`.
## PRIMARY TARGET (since session 5): kbase CVE-2022-38181 β stage 2 PROVEN
GhostLock (below) is parked: MTK's BUG_ON rtmutex variant + zero kernel-address
disclosure from shell = architectural dead end on this build (sessions 2-4).
The kbase JIT UAF was re-diagnosed (the destroy-worker "unconditional panic"
was the JIT_FREE deref, log lost to adbd death mid-panic) and stage 2 is now
oracle-proven β see SESSION 5 section.
### PARKED: GhostLock, CVE-2026-43499
rtmutex `remove_waiter()` futex-PI stack-UAF (NebuSec disclosure 2026-07, fix `3bfdc63936dd`
landed 2026-04). Vulnerable range 2.6.39β7.1 β **our 4.9.117 (May 2025) is affected**.
Verified on our exact build:
- `CONFIG_FUTEX=y`, rtmutex compiled in, bug present verbatim:
`rtmutex.c:1108-1111` uses `current->pi_lock`/`current->pi_blocked_on` (should be
`waiter->task`); buggy call site `rtmutex.c:1723` (`rt_mutex_start_proxy_lock` error path)
- Trigger surface = pure futex syscalls (`WAIT_REQUEUE_PI`/`CMP_REQUEUE_PI`), no device node,
nothing SELinux-gated β the kbase path's fatal obstacles don't exist here
- Consumer: `sched_setattr` β `__sched_setscheduler` β `rt_mutex_adjust_pi(p)` at
`sched/core.c:4706` β derefs stale `pi_blocked_on` β
- Proxy waiter lives on the **waiter thread's own stack** (`futex.c:1975` passes `this->rt_waiter`,
declared in `futex_wait_requeue_pi` at `futex.c:2880`) β waiter stamps its own freed frame
via arm32 `select` (nr 142) fd_sets
- Exploitation climate: **no KASLR** (fixed base 0xc0008000), no PAN, `DEBUG_RT_MUTEXES` off
β compact 48-byte `rt_mutex_waiter` (tree_entry@0, pi_tree_entry@0xc, task@0x18,
lock@0x1c, prio@0x20, deadline@0x28)
- **Minimal chain**: 2 write-slots β `modprobe_path` @ **0xc111488c** (string self-located in
vmlinux; `KALLSYMS_ALL` off so data symbols need this trick) β unknown-binfmt exec β root
script (setenforce 0, disable OTA, su)
- References in `refs/`: NebuSec/CyberMeowfia (original), GhostLock-5.10 (Fire OS 8 port,
full 32-bit ARM trigger in `src/exp32/`), ghostlock-...-4.19-k40 (Qualcomm 4.19 Android port)
### TODO (port plan)
1. Write trigger (3-thread requeue-PI deadlock, cores 0-3) β port of `exp32/main.c`
2. Stamp geometry: `rt_waiter` frame offset vs `do_sys_select` fd_set area β disassemble
our vmlinux (`do_sys_select` stack_fds vs `futex_wait_requeue_pi` frame), expose
STAMP_NFDS/STAMP_WAITER_OFF as tunables
3. Fake-writer encoding for arm32 48-byte waiter β "write V to ADDR" slots
4. 2 slots β modprobe_path, fire, root script
5. Fallbacks if select-stamp can't reach: setsockopt(MCAST_JOIN_SOURCE_GROUP) stamp
## Status
- [x] Bootrom (amonet hardware method) β patched on this unit, dead end
- [x] mtk-su (CVE-2020-0069) β patched, `Failed critical init step 3`
- [x] Attack surface survey β `/dev/mali0` world-RW + SELinux `gpu_device`, kbase r26p0-01rel0
- [x] CVE-2022-38181 confirmed in exact-build source; stage-1 trigger works
- [x] CVE-2026-43499 (GhostLock) verified but blocked: MTK BUG_ON rtmutex
variant + no kernel-address disclosure from shell (sessions 2-4)
- [x] **CVE-2022-38181 stage 2 PROVEN (session 5): destroy-worker panic was
a misdiagnosis; UAF redirect onto sprayed region, oracle-verified**
- [ ] Stage 1: trigger + stamp + consumer (crash = chain live)
- [x] Stage 2 (kbase path): UAF redirect onto sprayed region β PROVEN session 5
- [ ] Stage 2b: raw-byte slot control (xattr stamp churn) β unlink write
- [x] **Stage 3: arbitrary kernel function call β ROOT (session 10)** β nf LOCAL_OUT
hook hijack, `selroot` 2-packet chain: zero `selinux_state.enforcing`, rewrite
fake entry to `commit_creds(&init_cred)`. `uid=0`, SELinux Permissive.
- [_] Stage 4: root script (su, permissive, OTA off) + persistence β root obtained;
persistence/verity work remains
- [ ] Stage 5: custom OS boot chain
## Key findings
### Device / firmware
- Model KFMUWI, device `mustang`, Fire OS 7.3.3.1 `PS7331.4463N/0031575863040`
- Kernel `4.9.117-g08fe75b-dirty`, built Sat May 3 01:25:15 UTC 2025 (Linaro GCC 6.3-2017.05)
- Amazon silently re-issued 7.3.3.1 in May 2025 (new incremental, same version string)
- Bootrom post-2020 revision: short-to-GND on eMMC CMD gives **preloader only** (patched)
- `/dev/kb`, `/dev/dkb` (Amazon kernel-backup partitions) root:drmrpc 0660 β locked
### Why CVE-2022-38181 applies
- Driver: `mali_kbase` **r26p0-01rel0** (Midgard, Mali-T720), **inside NVD affected range r4p0βr31p0**
- Amazon's May 2025 rebuild shipped the 2018 bug verbatim β no backport
- Exact vulnerable code, source-verified:
- `mali_kbase_mem.c:2721` `kbase_jit_destroy_worker` frees region, **never clears `kctx->jit_alloc[id]`**
- `mali_kbase_softjobs.c:1270` `kbase_jit_free_finish` derefs the stale `jit_alloc[ids[j]]`
- `mali_kbase_mem.c:3138` `kbase_jit_backing_lost` β destroy path (fires during reclaim)
### Exploitation climate (all verified from live config dump + OTA vmlinux)
- armv7 32-bit, non-LPAE β **no KASLR** (kernel at fixed `0xc0008000` VA / `0x40080000` PA)
- **No `ARM_SW_DOMAIN_PAN`** β ret2usr viable; `CONFIG_PANIC_ON_OOPS=y` (failed attempts = reboot)
- No `SLAB_FREELIST_RANDOM`/`HARDENED`, no `CONFIG_USER_NS`/`USERFAULTFD`/`NF_TABLES`
- `CONFIG_MODULES=y`, no `STATIC_USERMODEHELPER` β `modprobe_path` overwrite = root
- 1 GB RAM β direct reclaim (needed for eviction) trivially reachable; **pressure >~1 GB
panics the kernel on its own** (unrelated lowmem/OOM bug) β keep spray β€ 900 MB, use ~700 MB
### UAPI quirks hit during PoC development (r26p0, `_IOC_TYPE 0x80`)
- `MEM_ALLOC` union is 32 bytes (`in` has 4 Γ u64 incl. `extent`)
- flags must include `BASE_MEM_PROT_GPU_RD|WR` (bits 2|3), not legacy R|W
- **tracking-page mmap required before any alloc**: `mmap(fd, offset=3gpu_alloc_addr` (a GPU VA you must
pre-allocate and pass)
### Stage-1 differential (proof the bug fires)
| evictable object | pressure | result |
|---|---|---|
| none | 900 MB | survived |
| none | 1300 MB | panic (system lowmem bug β unrelated) |
| normal region + DONT_NEED | 700 MB | survived |
| **JIT region + DONT_NEED** | **700 MB** | **panic in reclaim path** |
Panic occurs during eviction itself (`evictable_reclaim_scan_objects` β `backing_lost` β
destroy worker) β the dangling refs (`jit_alloc[]`, evict list) are walked before we ever
submit `JIT_FREE`. Stage 2 must win the race: reallocate the freed `kbase_va_region` with our
own `MEM_ALLOC` spray while pressure is still running.
## Artifacts
- `poc/stage2.c` β stage-2 exploit (modes: `step`/`uaf`/`spstep`/`spfree`/`spray`/`keys`)
β `spray 700` = full oracle run; survives and pauses (kill to clean up)
- `poc/mustang_jit_uaf.c` β stage-1 PoC (modes: `jit N` / `control N` / `pressure N`)
- `poc/build.sh` β zig cross-build (static musl armv7)
- `kernel/vmlinux` β symbols recovered from the exact OTA build (vmlinux-to-elf)
- `kernel/config-*` β `/proc/config.gz` dump from the running device
- `ksrc/` β Amazon OSS source (platform.tar + extracted `midgard-r26p0` tree)
- OTA: `/tmp/opencode/mustang_ota.bin` (sha256 `6068515aβ¦` matches fireos-archive)
and 2.2 GB kernel-source tarball kept at `~/Desktop/amazon-mustang/`
- Source tree path: `ksrc/kernel/mediatek/mt8163/4.9/drivers/misc/mediatek/gpu/gpu_mali/mali_midgard/midgard-r26p0/`
## Build & run
```
nix-shell -p zig --run 'zig cc -target arm-linux-musleabihf -static -O2 -o juaf poc/mustang_jit_uaf.c'
adb push juaf /data/local/tmp/juaf && adb shell chmod 755 /data/local/tmp/juaf
adb shell /data/local/tmp/juaf jit 700 # full trigger (~reboots device)
adb shell /data/local/tmp/juaf control 700 # no-JIT control
adb shell /data/local/tmp/juaf pressure 700 # raw memory-pressure control
```
## References
- GHSL-2022-054 advisory: https://securitylab.github.com/advisories/GHSL-2022-054_Arm_Mali/
- Mo's Pixel 6 exploit writeup: https://github.blog/2023-01-23-pwning-the-all-google-phone-with-a-non-google-bug/
- Fire HD 10 (trona) precedent, same bug family: ericpardee.github.io/fire-hd-ownership
- Amazon OSS portal: amazon.com gp/help/customer/display.html nodeId=200203720
- XDA unlock thread (dead for this hw rev): xdaforums.com/t/fire-7-2019-mustang-unbrick-downgrade-unlock-root.3944365/
## Session 3 addendum (deep syscall-stamp survey)
Copy-source depths measured (absolute vs syscall-entry sp0; waiter spans -0x1d8..-0x1a8):
- sendto sockaddr @ -0xc4 | process_vm iov @ -0x104 | recvmsg iov @ -0x11c
- sendmsg iov @ -0x12c | recvmmsg iov @ -0x154 | select fds @ -0x174
- pselect6 fds @ -0x19c | **sendmmsg iov @ -0x1bc (BEST β 0x1c short of waiter+0x00)**
- poll entries @ -0x3e0 (entirely below; wrong side)
Ruled out this session:
- io_submit chain too shallow (~-0x130) | semtimedop: CONFIG_SYSVIPC=n (stub)
- configfs mounted but ZERO subsystems registered (no mkdir targets)
- /sys/kernel/debug, /config: SELinux-denied for shell
- /proc/sys/kernel: getdents works (29 entries listed), only pid_max OPENABLE;
kptr_restrict/hotplug/hostname/domainname reads all denied
- Write value is ALWAYS waiter+0 (kernel stack addr, executable, shellcode at
+0x1c): rb_link_node *link = node, insert_color writes parent-color β no
controlled-value variant possible without tree-field stamping (gap -0x1d8..-0x1bc)
- Double-deref dispatch fields read *(waiter+0)=1, *(waiter+4/+8)=0 β BLX 1/0
(nf_hooks, net_families, inet[6]_protos, seq_file->op all dead)
- timer_list.function@+0xc and work_struct.func@+0xc would read *(waiter+0xc) =
pi_tree self-ptr = EXECUTABLE waiter+0xc β but no path queues waiter+0 as
timer/work (link corruption / no indirect queue sources)
- Tree-root-nonzero (sysctl handler slot) = deterministic pointer-chase through
.text as rb-tree; terminates at a zero word β offline-simulable, but landing
in a useful writable slot is implausible
Remaining leads for session 4:
1. ioctl deep paths: dev_ioctl ifreq copy (40B user data) β measure
SyS_ioctlβsock_ioctlβdev_ioctl chain depth vs -0x1d8
2. Any other 0x1c-deeper copy than sendmmsg (nothing found yet)
3. If stamp-surface hunt fails: reconsider walk-chained constructs or
hunt writable-zero-called slot classes not yet enumerated
## SESSION 4 β THE TWO BREAKTHROUGHS
### 1. MTK's rtmutex_common.h is the whole mystery
MTK replaced upstream's NULL-safe rt_mutex_top_waiter with:
```c
w = rb_entry(lock->waiters_leftmost, struct rt_mutex_waiter, tree_entry);
BUG_ON(w->lock != lock); // compiled to: ldr sb,[lock+8]; ldr r3,[sb+0x1c]; cmp; bneβudf#0x12
```
NO NULL CHECK + BUG_ON. Every all-zero anchor dies at *(NULL+0x1c); garbage
anchors die at the udf. THE WALK REQUIRES: lock->waiters_leftmost (lock+8)
must point at a fake waiter W (writable) with W->lock (+0x1c) == lock.
Walk flow fully mapped (rt_mutex_adjust_prio_chain @ 0xc0189a58):
- 9b44-9b54: retry head; pi_blocked_on==NULL β clean exit ret 0
- 9abc-9adc: orig_waiter==NULL β skip pi_waiters checks (adjust_pi always passes NULL)
- 9b10-9b28: prio check (prio==task->prio + MIN β exit 9b58)
- 9b2c-9b38: trylock(lock+0) β ticket; fails β retry loop w/ counter bail (9a90-9aa8, limit @ *(0xc11189c8))
- 9ba4-9bc0: deadlock checks
- 9bcc-9bd8: THE BUG_ON (leftmostβWβW->lock==lock or die)
- 9bdc-9c04: dequeue (leftover tree RB_CLEAR_NODE'd = EMPTY β safe skip), prio/deadline write
- 9c04 bl: rt_mutex_enqueue β *link = waiter+0 at lock+4 β THE WRITE
- 9c3c+: owner==NULL β clean exit path
### 2. The kernel-stack-address leak (kills the address-free requirement)
/proc/self/task//stat field 28 (kstkesp) returns REAL kernel SP for
syscall-blocked threads from SHELL context (verified: nonzero values observed).
- waiter blocks in read(blocking_pipe) β stat β kstkesp
- stack base = kstkesp & ~0x1fff (arm32 THREAD_SIZE=8192)
- rt_waiter abs addr = base + fixed delta (computable: sp0 = base+0x2000-0x48 pt_regs; waiter = sp0-0x1d8)
- ALL self-referential stamp values become computable!
### Full self-consistent stamp (after leak):
- L = waiter+0x24 (fake lock IN THE WINDOW β all 4 words controllable)
- iov[0].base (waiter+0x1c lock) = L
- iov[0].len (waiter+0x20 prio) = 1 (β 139)
- W = waiter+0x1c; stamp *(W+0x1c) = *(waiter+0x38) = L (BUG_ON passes)
- lock+0 (waiter+0x24) = 0; lock+4 (waiter+0x28) = 0 (write lands here);
lock+8 (waiter+0x2c) = W; lock+0xc (waiter+0x30) = 0 (owner NULL)
### Oracle status
- crash during walk = walk ran (dead-lock probe: deterministic crash, clean code)
- clean walk + no write = trylock-fail retry-bailout (kptr anchor: runtime word nonzero)
- Everything is now deterministic post-pollution-fix.
### Next session TODO
1. Implement leak: waiter blocks on pipe, main reads stat, computes base
2. Stamp self-consistent window, fire walk β crash-free completion = write proven
3. Weaponize: write always lands at lock+4 (rb_link_node) β lock must live in
the window (only fully-controlled memory), so target selection research:
either find called-slot-in-window trick, or two-stage construction.
## SESSION 4 FINAL STATE β THE WALL (precisely characterized)
### The complete picture
The walk fires deterministically (dead-lock probe: crash every time, clean code).
The write cannot land because of a 3-way kernel-hardening coincidence:
1. **MTK rtmutex BUG_ON variant**: lock+8 (leftmost) MUST point at W with
*(W+0x1c)==lock. All-zero/garbage anchors die. No static self-referential
pattern exists (6571 candidates scanned, 0 hits). Runtime pointers unknown.
2. **No kernel-address disclosure from shell**:
- kstkesp on arm32 = USER SP (task_pt_regs->ARM_sp) β not kernel stack. DEAD.
- dmesg/pstore/pagetypeinfo/kallsyms/stack β all denied.
- kptr_restrict=1 at runtime (fops anchor words also runtime-nonzero β the
kptr anchor run exited via trylock-fail retry-bailout, not trylock success)
3. **fops tables in rodata**: trylock strex aborts (session-3 sweep crashes).
The stamp window (waiter+0x1c..0x5b) is the only controlled+known-content
memory, but its ADDRESS is the unknown we need. Self-referential constructions
all require stamping a kernel address as a constant β circular without a leak.
### gitchw comparison (why their ARM32 write worked, ours can't yet)
Their 5.4 kernel has UPSTREAM rtmutex_top_waiter (NULL-safe: `if (!leftmost)
return NULL`) β empty-tree anchors survive, their write landed on null_fops
(writable on their kernel). Even THEY are stuck at dispatch ("ioctl reboot").
Mustang's 4.9.117 MTK tree has the BUG_ON variant β Fire OS 8 tablets
(GhostLock-5.10) succeeded because their 5.10 kernels are upstream-style.
### Session-4 verified facts
- Walk retry loop has a counter bailout (limit @ *(0xc11189c8)); trylock-fail
on runtime-nonzero anchor words β clean retry-bailout exit (kptr anchor runs)
- No-requeue path (9ce4, FULL walk) also derefs leftmost at 9d64 β no escape
- RB_CLEAR_NODE self-pointers exist as leftovers in the window (waiter+0 and
+0xc contain their own addresses) but no check-comparison uses them in a
way that avoids stamping known addresses
- Real-mutex candidates (chain mutex has live waiter = BUG_ON would pass) β
but &chain_mutex is a heap address, unreachable without leak
### NEXT SESSION OPTIONS (ranked)
1. **logcat kernel-pointer hunt**: Amazon HALs/daemons are chatty; any logged
kernel pointer (even stale) unblocks the construction. Cheap to test.
2. **/proc/net %pK behavior on THIS build**: some 4.9 trees print unhashed
pointers in /proc/net/tcp,udp,unix for unprivileged readers. Test live.
3. **Thread-exit paths on dangling pi_blocked_on** (one-shot, different derefs).
4. Revisit shelved kbase JIT bug with accumulated 4.9 knowledge.
## SESSION 4 ADDENDUM β LEAK HUNT: EXHAUSTED (definitive)
Tested and dead from shell domain:
- /proc/net/{tcp,unix,packet,netlink,ptype}: %pK-hashed to 00000000 (kptr_restrict=1)
- /proc/timer_list: READABLE but pointers %pK-zeroed (symbols visible, no addrs)
- logcat: no kernel pointers in Amazon/wpa chatter
- kstkesp (stat f28): USER SP on arm32 (task_pt_regs->ARM_sp)
- MTK nodes (/proc/ged, mtk_cmdq_debug, mtktz, ptp, chip, aed, driver/*): all
SELinux-denied
- /proc/{iomem,vmstat,kmsg,keys,crypto,slabs...}: denied
- /sys/kernel/notes: denied
- CONFIG_VECTORS_BASE=0xffff0000 (high vectors β NULL+0x1c faults)
- CONFIG_KUSER_HELPERS=y (kuser at 0xffff0000, not page 0)
CONCLUSION: GhostLock on mustang requires a kernel-address disclosure that
this kernel does not expose to the shell domain. The self-referential fake
lock cannot be constructed without it.
## DECISION POINT
(a) Boot-deterministic grind: reboot β calibrate stack address via crash
oracle (~20-30 reboots), verify reproducibility. Long shot β late-boot
thread stack allocation unlikely stable.
(b) PIVOT back to kbase CVE-2022-38181 with accumulated assets: exact-build
vmlinux + full source + toolchain + O_SYNC trace discipline + deep 4.9
knowledge. Original blocker (destroy-worker panic during JIT eviction)
is a spray-timing problem, now better understood.
(c) Stop at honest ~45%: trigger proven, walk mapped to the instruction,
write blocked by MTK BUG_ON + no-leak.
Recommended: (b) β the kbase bug is verified-present in this exact source,
had a working trigger, and its blocker is mechanical, not architectural.
## SESSION 5 β STAGE 2 PROVEN (option b executed)
### Re-diagnosis: the "unconditional destroy panic" never existed
`step` mode (alloc id=1 β DONT_NEED β 700MB pressure β MEM_QUERY, NO free)
**survives**: query=-1 (region freed by destroy worker, rbtree-clean).
The worker path is byte-identical to the legal JIT_FREE-under-pressure flow.
Session-1's crash was always the `JIT_FREE` dangling deref; its log line was
lost because the panic kills adbd mid-flush. Verified twice more with `uaf`
mode (bare free β panic, same log cutoff). **The GHSL-2022-054 flow is fully
live on this build.**
### Complete primitive inventory (exact-vmlinux disassembly)
`kbase_jit_free(kctx, reg)` @ 0xc058495c with fully-controlled fake reg:
- `reg->cpu_alloc` NULL β backed size 0 β trim block skipped (0xc0584978)
- bin decrement: `kctx+0x147dd` (byte) + `kctx+0x147de+bin_id` (byte)
- `mark_reclaim(reg->gpu_alloc)` @ 0xc059b158: chain
`K=*(gpu_alloc+0x38)` β `*(K+0x1429c)==0` skips mm-atomics β
atomic_sub nents@K+0x141c8, `D=*(K+4)` β atomic_sub nents@D+0x538.
With nents=0 all writes are no-op stores (strex of same value).
- `reg->flags |= 0x100000` (write into fake, benign)
- shrink_cpu_mapping early-exits when new==old (nents=0 β return)
- `list_add(gpu_alloc->evict_node, &kctx->evict_list)`: evict_list head
@ kctx+0x1427c; writes into gpu_alloc+0x18/0x1c (must be writable)
- WARN path (0xc0584bd4) is nonfatal (no panic_on_warn) and CONTINUES
- **UNLINK @ 0xc0584b08/b0c**: `r3=*(reg+0x3c) prev, r2=*(reg+0x38) next`
β `*(next+4)=prev; *(prev+0)=next` β two arbitrary write-whats-wheres,
then relink of reg+0x38 into jit_pool_head @ kctx+0x148e8
### Static fake-gpu_alloc chain (offline vmlinux scan, /tmp/opencode/scan_s.py)
9 candidates; **S=0xc118b7ec** (xfrm data, dormant on this device):
`*(S+8)=0` (nents), `*(S+0x18)=S+0x18` (empty evict_node β no WARN),
`K=*(S+0x38)=0xc118b820` β `*(K+0x1429c)=0`, K/D+0x141c8/+0x538 all in
writable data. Oracle targets staged: `init_uts_ns.name.nodename=0xc110d561`
("(none)", readable via uname), scratch P=0xc118bd58 (xfrm zeros).
Avoid S=0xc111cba4 (tracepoint-adjacent). CONFIG_DEBUG_RODATA=y β all write
targets must be in .data/.bss (bss 0xc11d9000-0xc12d9000).
### Spray engineering (what worked, what didn't)
- `add_key` (CONFIG_KEYS=y): SELinux-denied for shell. Dead.
- `setxattr` value buffer: kvmalloc(96)+copy_from_user happens BEFORE the
SELinux check β alloc dance is SELinux-proof even when the call fails;
transient (freed at syscall end), bytes persist at +4..95 (freelist ptr
clobbers +0..3 = rblink, unused by kbase_jit_free)
- **kbase_va_region itself: kzalloc(72) β kmalloc-96!** MEM_ALLOC(va=0x40,
commit=0x10) puts ONLY the region in kmalloc-96 (phy alloc β 384) β
deterministic reclaim type. A real-region victim makes kbase_jit_free
complete through fully-legal state (empty jit_node β self-unlink).
- Sequential post-pressure spray: ALWAYS misses β the worker frees the slot
mid-pressure into partial slabs (SLUB: free to non-active slab β cpu
freelist); under pressure our allocs fail β zero net volume β no rotation
- Pinned sprayers + caps: still miss (512-cap exhausted before eviction;
worker may run on any cpu)
- **WINNER: commit_pages=0 spray** β no phys pages β MEM_ALLOCs succeed
through the whole pressure storm β ~6000 net allocations β partial-list
rotation guaranteed. 8 threads (2/cpu, cpus 0-3 hardcoded β /proc/cpuinfo
is shell-filtered to 1 core, use Cpus_allowed_list) + pressure child
pinned to cpu0 + 16-alloc retention batch after join.
- query(jit_va) trap: post-reclaim, spray regions reuse the freed VA in the
custom zone β query=0 is ambiguous (live-original vs spray-covering-VA)
### ORACLE HIT β machine-verified redirect
`spray 700` run 2026-09-11: 5895 regions sprayed during pressure, JIT_FREE
on dangling id=1 completed on a reclaimed region, then
`JIT_ALLOC(0x40, bin 0)` walked jit_pool_head and returned **sprayed region
#4251's VA (0x142701000)** β the exact region the dangling pointer consumed.
Process kill after: kctx teardown clean, no crash. **Stage 2 complete:
deterministic UAF redirect with controlled object type + contents.**
### Stage 3 plan (raw-byte unlink)
Region-type reclaim gives legal-dance survival but jit_node is INIT'd
self β no unlink primitive. Need raw bytes at +0x38/+0x3c:
1. xattr stamping: alternate region-alloc bursts (net volume β slab
rotation) with xattr storms (stamp every head slot, bytes persist
post-free) β quiet window β deref
2. or pinned sendmsg cmsgs (optmem_max=10240 β ~106 Γ 96B held)
3. then: W1 `*(N+4)=P` with P=userland shellcode page (no PAN!) β
candidates: const fops are .rodata (DEBUG_RODATA) β target non-const
fn ptr in .data, or binfmt `formats` list head, or sysctl proc_handler
(verify table writability). fallback: modprobe_path via byte-chained
writes (values must be writable addrs β use pointer-shaped targets)
4. no-KASLR + exact vmlinux: prepare_kernel_cred 0xc0149e3c,
commit_creds 0xc014993c
## SESSION 5B β STAGE 3: weapon built, reclaim race not yet won
### Done
- **Stage-3 weapon complete & staged** (`poc/stage3.c`):
- Target: `kern_table[pid_max].proc_handler @ 0xc1113f40` (writable
.data, verified via string-pointer scan + handler == proc_dointvec_minmax)
- N = shellcode entry 0x11111112 (mmap 0x11111000; W1 clobbers entry+4,
skipped by `b +8`; W2 writes N at P = handler field)
- arm32 ring0 shellcode hand-encoded: prepare_kernel_cred(0) +
commit_creds + ret 0; trigger = `read /proc/sys/kernel/pid_max`
(readable from shell); runs in own task context β creds apply to us
- benign oracle mode writes uts nodename (0xc110d561, unaligned ok)
- **Spray primitive selection**:
- NETLINK_USERSOCK sendmsg pins (msg_control kmalloc-96 copy, held
while blocked, never parsed): **SELinux-denied** (socket create EACCES)
- unix/UDP sendmsg: cmsg parsing poisons payload bytes β
- **inotify events**: `inotify_handle_event` kmallocs name_len+0x1d,
name bytes (fully controlled, NUL/slash-free constraint) at event+0x1c;
name_len=60 β kmalloc-96; queued β held; 4 instances β 4 events per
rename; SELinux-OK from shell. Fake redesigned NUL-free: cpu_alloc
points at S (nents@S+8 = 0 β same semantics as NULL)
- **Mechanical chain validated end-to-end** (drain4, no-eviction run):
20K drained events + 13.5K multi-cpu trailing renames + JIT_FREE +
oracle + pause, all clean. O_SYNC log (/data/local/tmp/s3.log) survives
panics β exact crash-point forensics.
### Reclaim attempts on the freed slot (all missed so far)
| variant | result |
|---|---|
| concurrent rename sprayers during storm | renames stall (journal/GFP_NOFS) β 128 total β garbage deref |
| pre-drain 12K events + pressure + small trailing | crash at deref |
| + kill-child-at-eviction (10ms poll) | crash at deref |
| + cpu0-pinned lifecycle (drain3) | crash at deref |
| stepped pressure (drain4 v1) | children freed memory on exit β no eviction (validated legal path) |
Working hypothesis: **storm-junk race** β between the destroy worker
freeing the slot (mid-storm) and child-kill/quiet, residual reclaim
activity takes the sole-free-slot of the victim slab with non-payload
bytes. Region spray (stage 2) wins because it allocates continuously
DURING the storm; renames can't.
### Gotchas hit
- spray toggle bug: rename source must be the payload name (was temp
name β ENOENT after 2 waves β only 512 events ever)
- device hostname is "localhost"/varies β oracle compares before/after
- paused st3 + pkill β device WEDGE (teardown with 33K events?!) β kill
paused processes only via reboot; second hard-wedge of the session
- /proc/cpuinfo shows 1 cpu to shell; use Cpus_allowed_list
### Next moves (ranked)
1. drain4 @ 500MB (eviction threshold confirmed there), 1ms poll, instant
kill, 4-cpu trailing Γ 3200 β shrink the storm window
2. xattr-stamp churn (setxattr alloc-copy happens BEFORE SELinux check β
SELinux-proof) concurrent with storm + region rotation, end-with-stamp
3. accept region-reclaim (proven) + find a second-stage primitive on the
region-victim state (double jit_free analysis negative so far)
## SESSION 5C β the blocker, precisely characterized
### Empirical results this session
- drain4@500 (1ms poll, instant kill, +4.5K renames): still crash at deref
- overlap trailing with kill (drain5): crashes EARLIER (renames during
kill-recovery storm hit a system-level fault) β overlap abandoned
- **ISOLATING EXPERIMENT (`iso` mode)**: identical timing, trailing with
commit-0 REGIONS + stage-2 pool-reuse oracle β **ORACLE HIT**
β timing/reachability are FINE; events are the problem
- mask-cycled events (MOVED_TO/CREATE/DELETE rotation to defeat
inotify_merge): still crash at deref
- CONFIG_MEMCG=n β events and regions share ONE kmalloc-96 (memcg theory
dead); inotify_merge compares names too (merge theory dead β our
toggling names never merged; events were queued and held all along)
- /proc/slabinfo absent; /proc/self/pagemap readable but PFN-zeroed
(post-4.0 masking, no CAP_SYS_ADMIN)
### THE ACTUAL BLOCKER (two parts, both proven)
1. **Soft-job finish runs in kbase job-scheduler WORKER context**
(jd_run_atom β js dispatch, mali_kbase_jd.c:81-112/677), not inline in
the submit ioctl β `current->mm` is a kernel thread's β the elegant
"point the fake's pointers into our own userspace mmap" design (no PAN!)
FAULTS nondeterministically. 5/5 crashes with an otherwise-perfect fake.
2. Therefore gpu_alloc must point at KERNEL memory with a survivable
runtime chain: `K=*(S+0x38)` readable, `*(K+0x1429c)==0` at runtime
(skips mm-atomics), `K+0x141c8` writable, `D=*(K+4)` β `D+0x538`
writable, nents `*(S+8)` preferably 0. The 9 offline S-candidates were
validated against FILE bytes β runtime drift (xfrm/tracepoint init)
makes them unverified. A wrong chain = crash = reboot (~3 min cycle).
### Session-6 plans (both fully specified)
A. **Physmap-spray fake (ret2dir, classic arm32 no-PAN)**: spray ~450MB of
user pages each containing the fake pattern baked for ONE guessed
address G (G&0xfff = 0x141 for NUL-free name bytes; S=G; K=G-0x141b4 so
K+4 lands in-page; K+0x141c8/+0x1429c β G+0x10/+0xd4 in-page; D=G+0x300;
stray sub-0 stores hit random mapped RAM - harmless with nents=0).
Spray doubles as the eviction pressure (dirty anon = unevictable β
only ~100-200MB extra needed). Odds β 45% (page hit) Γ ~50% (foreign
K+0x1429c word is zero... if K+0x1429c kept in-page per layout above,
odds = page-hit only). Miss = crash = reboot, retry.
B. **Brute-force the 9 static S-candidates** (xfrm 0xc118b7ec first,
tracepoint-adjacent 0xc111cba4 second...): 1 reboot per candidate,
benign-oracle payload first, weapon on hit.
C. pagemap-based exact G (dead: PFNs masked) β do not revisit.
## SESSION 5D β physmap fake built; G-sweep 0/3; confounds eliminated
### Established this session (all binary/device-verified)
- **Cache identity CONFIRMED same**: region = kmem_cache_alloc_trace(
kmalloc_caches[7], GFP|0x8000, 0x48) [kbase_alloc_free_region disasm];
event = __kmalloc(89, GFP) β kmalloc-96. Events and regions CAN share
the victim's cache. (MEMCG off; single cache set.)
- inotify queue: β₯5000 events held, no overflow, no merge collapse
(qmeas 1000 & 5000 runs) β the event spray persists
- **iso2 (physmap spray + region trailing + oracle): HIT** β the physmap
spray does NOT break region reclaim; machinery sound
- events vs regions reclaim: regions 3/3 (iso, iso2, stage-2), events 0/10
BUT the 3 pmap runs are explained by G-misses at 0.3-0.45 odds each
(P(3 misses|events-work) β 0.2-0.3 β not conclusive)
- mix mode confounded: 480MB spray suffocates post-kill renames (+0);
350MB OK (+4452); spinning failed-rename threads also disturb region
reclaim (mix crashed, iso2 clean)
- query_commit returns -1 on EINVAL too β instrumented; EINVAL observed =
rbtree lookup miss = genuinely freed β (not a false positive)
### Current pmap design (in stage3.c, mode pmap/mix/iso2)
- G baked into every sprayed page (offset 0x2a4): nents@+0x2ac=0,
evict self@+0x2bc/0x2c0, K=G+0x100@+0x2dc, D=G+0x200@+0x3a8;
K+0x141c8/-0x1429c land ~20 pages up (sub-0 store harmless / read must
be 0-or-valid). PM_SPRAY_MB 350, G sweep tried: c2a412a4, c2f4b2a4,
c2a7d2a4 β all crash at deref
- physmap range sanity: RAM 1GB β physmap ~0xc0000000-0xc3fffffff;
MTK carveouts (GPU/M4U/secure) may occupy chunks β G landmines
### Session-6 TODO (ranked)
1. **Extract the full kernel source** (2.2GB tarball at
~/Desktop/amazon-mustang/ β platform.tar): get arch/arm + mm/ +
drivers/of + MTK reserve mappings β compute the carveout map β
target G into verified-RAM physmap subranges; also verify kmalloc
cache geometry (ARCH_KMALLOC_MINALIGN!) and the 0x8000 GFP bit
2. G sweep with placement-informed guesses (multiple reboots, vary spray
size to decorrelate)
3. If G sweep exhausts: reconsider multi-G payload or S-candidates from
runtime-plausible statics (uts-adjacent pointer fields failed: NULL-K)
## SESSION 6 β zram discovery, source extraction, event question still open
### Full kernel source now extracted
`/tmp/opencode/ksrc2/kernel/mediatek/mt8163/4.9/` (arch/arm incl.
mustang.dtsi, mm/, fs/eventpoll.c, fs/notify) from ksrc/platform.tar.
Findings:
- 0x8000 GFP bit = ___GFP_ZERO (just kzalloc; no cache split)
- kmalloc-96 is a true 96-byte cache (no HWCACHE_ALIGN on kmalloc caches)
- mustang.dtsi: memory node 0x40000000/512MB β extended by preloader
(device shows MemTotal 977MB); CONFIG_VMSPLIT_3G, HIGHMEM=y
- **zram0 ACTIVE (SwapCached > 0)** β "dirty anon = unevictable" was
WRONG: the physmap spray swaps out under pressure β G alias goes stale
β added `physmap_retouch()` after the kill (faults all spray pages
back in before the trailing/deref)
### Runs this session (all O_SYNC logged, ~6 reboots)
| run | result |
|---|---|
| pmap G=c2a412a4 400MB | crash at deref |
| pmap G=c2f4b2a4 480MB | crash; +0 post-kill renames (480MB suffocates fs) |
| iso2 (spray + regions + oracle) | **REGION HIT β spray doesn't break reclaim** |
| mix v1 (concurrent events+regions) | crash; confounded (spinning event threads) |
| pmap G=c2a7d2a4 350MB + retouch | crash; +4452 renames OK |
| mix2 (sequential: 2s events THEN regions) | crash; +7126 renames (28K event allocs), 2715 regions |
### Verdict on events (Bayesian, honest)
Regions reclaim: 3/3. Events: 0/~12 attempts including 28K exclusive
allocations with head start and correct-by-construction fake. If events
reclaim with p_hit(G)β0.35, five pmap misses β 11.6% β possible but now
unlikely (~10-15%). Either events structurally cannot take this slot
(reason unknown β same cache, same context, same timing) or our G guesses
are systematically missing (highmem-boundary skew, allocator placement).
### Session-7 decision tree
1. **Settle G first** (cheap, no exploit): temporarily instrument the
ISO2 flow β region trailing + oracle β but make the PAYLOAD event a
physmap fake and check whether ANY G in a swept range produces a
nodename change without regions racing (pure pmap, G sweep over
~0xc1500000-0xc2a00000 lowmem center, 1 G per reboot, 4-5 reboots)
2. If G sweep exhausts β events declared dead β hunt alternative
raw-byte kmalloc-96 allocators reachable from shell (audit: seq_file,
tty ldisc, fdtable, sk_filter (blocked: code-field at +0x38),
netlink nlmsg (skb β), keys (denied)) β or revisit two-stage region
primitives (analysis negative so far)
3. Consider UART/ramoops unlock via root later; do not chase
## SESSION 7 β cmdline bombshells; event mystery now precisely bounded
### mustang_defconfig CONFIG_CMDLINE (ground truth for placement):
`vmalloc=496M slub_max_order=0 slub_debug=O loglevel=8 initcall_debug`
1. **vmalloc=496M β physmap = 0xc0008000..~0xc2080000 ONLY (low 520MB
of RAM)** β ALL prior G guesses (0xc2a4xxxx+) were in VMALLOC SPACE.
Every "G-miss" conclusion from sessions 5D/6 is invalidated; the
crash-interpretation stands but the sweep was aimed at the wrong map.
2. slub_max_order=0: all slab pages order-0
3. KMALLOC_MIN_SIZE = ARCH_KMALLOC_MINALIGN = 64 (L1_CACHE_SHIFT 6) β
kmalloc_index() special-cases for 96/192 are DISABLED β
region kzalloc(0x48=72) β caches[7]; event __kmalloc(89) β caches[7]
(disasm + include/linux/slab.h:287 verified) β BOTH in the merged
128-byte "kmalloc-128/96" cache. Cache identity: RE-CONFIRMED equal.
4. zram active β replaced anon physmap spray with **kbm_spray**: 160 Γ
2MB kbase MEM_ALLOC regions (GFP_KERNEL β ZONE_NORMAL β lowmem-only,
pinned β zram-immune), pattern written via CPU mmap β correct range,
~65% lowmem coverage
### Runs (O_SYNC logged)
- kbm + G=0xc16412a4: crash at deref (+4831 renames)
- kbm + G=0xc1c4b2a4 @200MB: NO eviction (query=16 β pressure
calibration varies with pinned spray; legal free, survived)
- kbm + G=0xc1c4b2a4 @400MB: eviction β, +4053 renames, crash at deref
- Valid-range G record: 0/2. If events work with ~65% coverage:
P(2 misses) β 12%. Question still open but narrower than ever.
### The question, final form
Regions take the victim slot 3/3; events 0/13. Same cache (proven at
source + disasm level), same process context, same pinned cpus, same
post-kill timing, thousands of allocations with exclusive head start.
Mechanism unknown. Remaining suspects: allocation-rate/frequency
correlation with partial-list rotation (region ioctls ~1ms apart vs
event renames ~100Β΅s apart β opposite directions?), or SLUB freelist
ordering details under slub_max_order=0 that favor... unclear.
### Session-8 TODO
1. Instrument-grade experiment: TWO event payload variants with
DIFFERENT N/P values alternating (two name sets) β if the nodename
ever changes, the LAST winner is identified; sweep G in
[0xc1200000..0xc2000000] with kbm spray, 3-4 reboots budget
2. If still 0/N: abandon events. Alternatives ranked:
a. **pipe_buf arrays via F_SETPIPE_SZ(4096 β 1 buf? no β 16 bufs =
kcalloc(16, 28)=448β512 β)** β dead
b. audit fs/notify + fs for other name/data-carrying kmalloc-128
objects (fanotify events? mq off; fanotify needs groups...)
c. **seq_file buffers** (kmalloc(PAGE_SIZE) β)
d. sock filters (code-field collision at +0x38 β)
e. accept the region-type reclaim + chain a SECOND bug/technique
3. Re-examine WHY regions win β maybe instrument via multiple jit ids:
N dangling slots, region-vs-event race per slot, oracle detects which
spray took which slot β statistical fingerprint of the mechanism
## SESSION 8 β ARBITRARY WRITE ACHIEVED ON DEVICE; dispatch mystery left
### THE MILESTONE
```
[+] uname.nodename="X?lhost" (was "localhost") highmem-free β surplus lands in ZONE_NORMAL (physmap)
2. First weapon attempts crashed: W1 target N+4 was a USER page β
unlink runs in kworker ctx (no mm) β fault. Fixed by baking the ring-0
shellcode INTO the physmap pattern at page+0x600 (direct map RWX on
arm32 non-LPAE) β kernel-resident code, no ret2usr needed
3. ctl_table offset bug: proc_handler is at entry+0x14, not +0x18 (the
original scan had it right; my define was wrong) β was writing extra1
4. Over-pressure regression found & reverted: kid budget 20Γ100MB +
trailing 10s broke the reclaim; the working config is 2 kids/200MB +
5s/+3200 trailing (benign hit 1/1 after revert)
5. **diag2: W2 β &pid_max global (0xc1114d7c) β read returns our value
(-1055861411 = 0xc110d55d as int32) β write + readback PROVEN**
### The remaining mystery (one experiment from closed)
diag1 with the CORRECT handler address (0xc1113f3c): W1 fires (nodename
changes), W2 must have executed (next instruction) β yet pid_max reads
still return clean values β the handler field we write is not the one
the inode dispatches through. diag3 (queued; needs a hit boot): W2 β
entry->data FIELD (0xc1113f2c) pointing at nodename β if the read then
shows nodename-bytes-as-int, our entry IS live and only the handler
offset is somehow wrong; if unaffected, the inode uses a shadow table
copy and we hunt the live one.
### Hit-rate reality
Per-boot coin flip (~25-40%), clustered; several safe-miss/crash boots
in a row is normal. Roughly 1 in 3-4 boots is a hit. Keep the working
config EXACTLY (2 kids, 5s trailing, 260-region kbm, G=0xc154b2a4).
### Session-9 TODO
1. Complete diag3 on a hit boot (roll until "W1 FIRED")
2. If entry live: re-check handler offset empirically (write
proc_dostring's address as handler via... N must equal a useful
value β use the unlink to write entry->data instead and pivot: e.g.,
data=selinux_enforcing-adjacent...)
3. If shadow table: locate the live one β kallsyms has no data symbols;
candidates: scan /proc/sys behavior, or find a second ctl_table
region via the header list pattern in .data (0x20-stride entries
with handler=proc_dointvec_minmax and maxlen=4 β enumerate ALL and
diag-write each)
4. Alternative target class that avoids dispatch entirely: .data
function pointers called from shell-reachable paths (audit needed)
5. The write primitive itself is DONE β any reliable kernel-address
target now suffices for root
## SESSION 9 β nf-WEAPON: reclaim+unlink+G-hit PROVEN IN-WEAPON; only the hook walk remains
### The new trigger design (replaces the sysctl-handler path entirely)
User's idea translated to kernel memory: no SUID file (system is
dm-verity RO; primitive writes kernel RAM). Instead: **fake netfilter
hook**. This kernel has the Android-common backport of the NEW
nf_hook_entries API, but implemented as a LINKED LIST (verified by
disasm of nf_hook_slow + helper 0xc09897d4):
- `__ip_local_out(net, sk, skb)` loads the entries CELL from
**[net+0x58c]**, stores into state+0x1c, calls nf_hook_slow
- walk: `entry = *cell`; while(entry){ if (state->[4] [0x20])
call entry->[0xc](entry->[0x14], skb, state); entry = entry->[0]; }
β i.e. **fn@entry+0x0c, priv@entry+0x14, priority@entry+0x20,
next@entry+0x00**; state+4 = INT_MIN threshold (always passes)
- init_net = **0xc1104548** (CONFIG_NET_NS=n β sock_net() inlines the
constant; 6678 movw/movt refs, histogram champion; cross-confirmed by
nf_hook_slow's own literal). TARGET: **[init_net+0x58c] = 0xc1104ad4**
- LOCAL_OUT hook runs in the SENDER's process context β our hookfn's
commit_creds(prepare_kernel_cred(0)) roots the process that sent the
packet. Trigger = sendto(127.0.0.1:9) UDP.
### nf mode layout (poc/stage3.c, mode `nf 200`)
- kbm pattern pages (page-relative, single source of truth β the old
weapon had THREE bugs now fixed: proc_handler@+0x14 not +0x18; W1
target must be KERNEL mem (kworker ctx, no mm); baked code at
page+0x600 vs entry G+0x600=page+0x8a4 mismatch):
- +0x2ac/+0x2bc/+0x2c0/+0x2dc/+0x3a8: fake phy-alloc chain (unchanged)
- +0x600: nf_code hookfn (marker store + prepare_kernel_cred +
commit_creds + return NF_ACCEPT(1))
- +0x700: fake entry {next=0, fn=PM_PAGE+0x600, priv=0, prio=0x100}
- +0x740: cell β PM_PAGE+0x700
- payload: N = PM_PAGE+0x740 (0xc154b740), P = 0xc1104ad4
β W1: *(cell+4)=P (lands in our page), W2: *(init_net+0x58c)=cell
### SELF-DIAGNOSING instrumentation (kbm_scan_for)
The kbm CPU mappings are kept; after the free we scan every sprayed
page for a known word:
- scan(HOOKS_PTR_ADDR) at page+0x744 β proves reclaim + unlink + reveals
which phys page backs the G guess
- scan(0x600d600d) at page+0x7f0 β proves the hookfn EXECUTED
(nf_code writes this marker as its 2nd action)
### THE RUN THAT MATTERS (2026-09-11, late session 9)
```
[+] W1 CONFIRMED: region 135 page+0x185000 -1 errno=1 uid=0 euid=0
[+] nf trigger done, uid=0 euid=0 [probe|uprobe|oc|chain|fc |notrig|selroot]`
- `selroot` is the working weapon. Key statics: `init_net=0xc1185040`,
`HOOKS_PTR_ADDR=0xc11855cc`, `ENFORCING_ADDR=0xc1213ea8`,
`ZERO_GADGET=0xc01d503c`, `commit_creds=0xc014993c`,
`init_cred=0xc1114f54`.
- Post-exploit is direct syscalls (no `system()`); keep the kctx alive
(`pause()`) to avoid teardown crash.
### Remaining (Stage 4/5)
- Persistence across reboot (verity / boot image / recovery), since the cell
hijack + permissive SELinux are runtime-only and re-running the exploit needs
the ~1/3 reclaim coin flip.
- `su` will need a non-nosuid home (`/system`) or a launcher that re-triggers.