
Exploit for CVE-2025-8045, an Arm Mali GPU kernel driver UAF. Turns a freed GPU buffer into a dirty page table, enabling arbitrary kernel read/write and a root shell on Android.
A local privilege escalation exploit for CVE-2025-8045, a use-after-free in the
Arm Mali GPU kernel driver (kbase). A non-privileged local process races the
CSF KCPU queue dump path to free and reclaim a cpu_queue.buffer, turns the
dangling allocation into a page table it owns ("dirty pagetable"), and uses
that primitive to gain arbitrary kernel physical read/write — ending in a root
shell with SELinux set permissive.
Exploited on Pixel 7 Pro (build
BP3A.250905.014). The hardcoded offsets inexploit.care calibrated for this target; other devices/builds need the offsets re-derived (see Tuning & portability).
CVE-2025-8045 is a use-after-free in the Arm Valhall and 5th Gen GPU
Architecture kernel drivers (r53p0–r54p1). A local, unprivileged process
can drive GPU processing operations in a way that leaves the driver operating on
already-freed memory, opening the door to kernel memory disclosure, corruption,
and code execution.
This exploit reaches the bug through the CSF KCPU queue interface exposed by
/dev/mali0:
CQS_WAIT followed by a
FENCE_SIGNAL. The wait stalls the queue so the fence timer fires later, on a
predictable timer-wheel bucket boundary.KBASE_IOCTL_CS_CPU_QUEUE_DUMP drives kbase_csf_cpu_queue_dump_buffer().
Racing the queue's state transition (ISSUED → COMPLETE/PENDING) against the
dump path frees cpu_queue.buffer while it is still referenced — the UAF.The exploit is a timing exploit: it first calibrates the deadline, then predicts it every round and fires a "wave" of harmless dump calls to widen the race window around the moment of the free.
Because one page ends up being both a Mali allocation and a live CPU page table, the process parks forever instead of exiting — freeing it would be a double-free and panic the kernel.
# Android / arm64 (NDK)
aarch64-linux-android31-clang -O2 -o exploit exploit.c
./exploit
On success you get a root shell (uid=0) with SELinux permissive. Sample flow:
[*] pid=... cpus=8 wave=64 harvest cpu=7 rounds=256
[*] round 1 calibrate deadline t0+... ms phase ... ms
[*] round N read +... us
[+] UAF page: spray region R page P @ ..., 1 live descriptor
[+] entry 0 = ... -- a CPU page table, writable through the GPU
[+] alias: dirty pagetable
[+] selinux_state.enforcing = 0 @ phys ...
[+] sel_read_enforce patched @ phys ...
[+] root shell: uid=0 euid=0
If it runs all rounds without a win, cpu_queue.c:71 in dmesg indicates the
race is landing but the reclaim missed — tune the reclaim/wave parameters.
The values in exploit.c are calibrated for the Pixel 7 Pro, build
BP3A.250905.014. Offsets and constants are device/kernel-specific and must
be re-derived for any other target:
KERNEL_PA_BASE, SEL_READ_ENFORCE, INIT_CRED, COMMIT_CREDS,
SELINUX_STATE — kernel symbol/physical offsets.ARM64_PTE_FLAGS — last-level descriptor template for the target MMU config.WAVE_*, SWEEP_*, BUCKET_NS, WAIT_NS, spray sizes — race timing and
heap-grooming knobs.Update to the fixed driver release from Arm to remediate.
This code is published strictly for defensive security research, detection engineering, and education. Do not run it against systems you do not own or lack explicit written authorization to test. The author accepts no liability for misuse or for any damage caused. Using it against systems without permission is illegal.
| Stage | What happens |
|---|
| Calibrate | Round 1 measures the ISSUED→COMPLETE edge to learn the timer-wheel bucket phase. |
| Predict + sweep | Later rounds predict the deadline and sweep the read offset ±ms to land exactly on the free. |
| Wave | 64 threads pile CS_CPU_QUEUE_DUMP calls onto csf.lock to bracket the deadline and stretch the window. |
| Reclaim | Pre-drain UNMOVABLE free lists, then spray stamped GPU pages so the freed block comes back under our control. |
| Dirty pagetable | Free again and immediately spray CPU page tables into the same page; the GPU allocation now aliases a live last-level table. |
| Arbitrary R/W | A never-reused window PTE is pointed at kernel physical pages; TLB is force-flushed via a PROT_NONE round-trip on a large scratch mapping. |
| Root | Clear selinux_state.enforcing, patch sel_read_enforce with commit_creds(init_cred), release a parked child that execs a root shell. |