Android In-The-Wild: Unexpectedly Excavating a Kernel Exploit
Seth Jenkins (Security Researcher · Google Project Zero)
OffensiveCon 2025 · Day 2 · Main · Briefings
Overview
Starting from nothing but a set of kernel panic logs recovered from a Serbian activist's phone — logs that implicated Cellebrite's UFED tool and the Qualcomm ADSPRPC driver — Google Project Zero researcher Seth Jenkins found five new vulnerabilities in the driver over two and a half months, then reconstructed a plausible exploitation strategy that matches the in-the-wild crash artifacts with striking fidelity. The most likely in-the-wild bug is a use-after-free in fastrpcmmap structures caused by a handle/buffer reference-count mismatch, and the spray primitive appears to be inotifyeventinfo objects whose overlaid fields produce the anomalous timestamp-valued channel IDs seen in the original logs. ---

Key moments
- 0:37 Cellebrite UFED used on Serbian activist's phone; kernel panic logs implicate ADSPRPC driver
- 5:20 Invalid channel ID in crash log decodes as December 17, 2023 epoch timestamp: date of the attack
- 8:58 ADSPRPC driver reachable from UNTRUSTEDAPP SELinux context, making it directly app-exploitable
- 10:42 flIsCompat flag persists permanently on fd, enabling arbitrary kernel read via compat/64-bit ioctl mix
- 19:59 Handle-type vs buffer-type fastrpcmmap mismatch causes UAF: most likely in-the-wild exploit bug
- 36:38 inotifyeventinfo heap spray: freed mmap object reallocated with inode mtime field as channel ID
- 37:32 PoC reproduces crash logs with current-date epoch channel IDs, confirming exploit reconstruction
- 48:02 Qualcomm-to-OEM-to-Android supply chain delays: no bug fixed under 90 days from patch creation
Android In-The-Wild: Unexpectedly Excavating a Kernel Exploit
Speaker: Seth Jenkins (Google Project Zero)
Conference: OffensiveCon 2025 — May 16-17, 2025, Berlin
YouTube: https://www.youtube.com/watch?v=lnK1iACJ3-c
Reading time: ~10 minutes
TL;DR
Starting from nothing but a set of kernel panic logs recovered from a Serbian activist's phone — logs that implicated Cellebrite's UFED tool and the Qualcomm ADSPRPC driver — Google Project Zero researcher Seth Jenkins found five new vulnerabilities in the driver over two and a half months, then reconstructed a plausible exploitation strategy that matches the in-the-wild crash artifacts with striking fidelity. The most likely in-the-wild bug is a use-after-free in fast_rpc_mmap structures caused by a handle/buffer reference-count mismatch, and the spray primitive appears to be inotify_event_info objects whose overlaid fields produce the anomalous timestamp-valued channel IDs seen in the original logs.
Introduction
In-the-wild Android kernel exploits are rarely handed to researchers on a silver platter. When kernel logs from Luka Ivan Bijelić's confiscated Android phone reached Google's Threat Analysis Group (TAG) via Amnesty International in late 2023, what arrived was not an exploit binary — it was a handful of kernel panic log files and the name of a driver: ADSPRPC. Jenkins's talk at OffensiveCon 2025 is an unusually honest account of how a security researcher works backward from crash artifacts when the exploit itself is unavailable, and what productive misdirection looks like when the evidence is ambiguous.
The story matters beyond the technical details. It illustrates the state of Android driver security, the challenges of Qualcomm's long patch supply chain, and the forensic trails that commercial exploitation tools leave behind even when the tool's authors presumably wanted them erased.
The Crime Scene: Kernel Logs from a Targeted Phone
▶ Watch: Background on targeted individual and log artifacts (0:00)
On December 17, 2023, Serbian environmental activist and journalist Luka Ivan Bijelić was arrested off a bus during the country's general election, taken to the Ministry of Interior, and compelled to surrender his PIN code under warrant. Amnesty International's forensics team subsequently analyzed the device, identified indicators that Cellebrite UFED had been used against it, and turned a tranche of kernel crash logs over to TAG, which forwarded them to Project Zero.
Four kernel logs were available. Each shows crash activity touching the ADSPRPC driver — specifically fast_rpc_mmap_free and related functions. The most analytically productive anomaly appears in kernel log number two: fast_rpc_mmap_free reports an invalid channel ID with a value that should never exceed 4. The value in the log, when interpreted as a Linux epoch timestamp in seconds, resolves to December 17, 2023 — the exact date of the arrest. This observation, credited to Amnesty International, becomes a central thread in Jenkins's reconstruction.
Other logs show a crash in INotify and evidence of pipe buffer operations consistent with heap spray and allocation primitive activity. But the logs alone are insufficient to identify the triggering bug, and Jenkins eventually abandons the log-hunting approach in favor of a broader strategy: find any vulnerability in ADSPRPC, then reason about which bug best fits the evidence.
The ADSPRPC Driver and FastRPC mmap Structures
▶ Watch: ADSPRPC driver overview (8:00)
The ADSPRPC (Application Digital Signal Processor Remote Procedure Call) driver is accessible through /dev/adsprpc-smd and reachable from the UNTRUSTED_APP SELinux context, making it directly exploitable from a Play Store or sideloaded app with no additional permissions. It manages memory mappings and buffer sharing between the application processor and the digital signal processor (DSP), handling argument marshaling and asynchronous RPC execution.
At the heart of the driver's complexity are fast_rpc_mmap structures, kmalloc'd objects tracking each active memory mapping. These structures can appear in up to three different places simultaneously:
- The global list
gfa(global file-backed anonymous maps), protected by anhlockspinlock. - The per-file-descriptor list
fl->maps, protected by amap_mutex. - Per-context arrays (
ctx->maps[]), transient arrays created for the lifetime of an RPC invocation.
Each fast_rpc_mmap carries two reference counts: refs (all references) and context_refs (references from context arrays only). Understanding the invariants — and violations thereof — across these three storage locations is the key to every bug Jenkins found.
Bug 1: Persistent compat Flag Enables Arbitrary Kernel Read
▶ Watch: flIsCompat bug (10:00)
The first vulnerability is a compat flag mishandling in the kcopy_from_user macro. This macro selects between copy_from_user (for userland pointers) and memcpy/memmove (for kernel pointers) based on a kernel parameter. The value passed for this parameter is flIsCompat — a flag set on a file descriptor when the 32-bit ioctl interface is invoked.
The flaw: flIsCompat is set permanently on the file descriptor the moment the 32-bit ioctl path is called, and it is never cleared. If a process subsequently calls the 64-bit ioctl interface on the same file descriptor, the driver treats the 64-bit userland pointer as a kernel pointer and performs a direct memory copy from it — an arbitrary kernel read primitive reachable with a two-step sequence: invoke the 32-bit ioctl once, then make a 64-bit ioctl call. The resulting kernel splat crashes with a virtual address of 0x4141414141414141.
This bug was reported on June 11, 2024, and fixed on October 7, 2024 — 118 days, exceeding Project Zero's 90-day disclosure deadline.
Bugs 2 and 3: Use-After-Free via Reference Count Violations
▶ Watch: FastRPC mmap UAF bugs (12:00)
Bug 2 is a race condition on the global gfa list. fast_rpc_mmap_add and fast_rpc_mmap_create take an hlock spinlock only briefly when adding a map to gfa. A concurrent fast_rpc_internal_munmap on a separate file descriptor — using a different map_mutex that does not protect cross-FD global access — can free a fast_rpc_mmap that another thread is in the process of creating, producing a use-after-free.
Bug 3 exploits a refcount bypass through flag combinations. fast_rpc_internal_munmap_fd finds a map using fast_rpc_mmap_find and calls fast_rpc_mmap_free directly, without the usual pre-condition checks (refs == 1 and context_refs == 0) enforced by fast_rpc_mmap_remove. When a map is created with the flags FAST_RPC_MAP_FD_NO_MAP | FAST_RPC_ADD_OR_KEEPMAP, the reference count is not incremented to 2 as it would be in the normal path, leaving refs == 1. A concurrent context creation that begins but then fails to map memory to the DSP drops that reference to 1; a subsequent munmap_fd call then drops it to 0 and frees the object while the context still holds a dangling reference. This bug was reported and fixed in approximately 257 days — roughly 8.5 months.
Bug 4: The In-the-Wild Candidate
▶ Watch: In-the-wild bug reconstruction (22:00+)
The fourth bug is the one Jenkins, in consultation with the Qualcomm security team, assessed as the most likely candidate for the in-the-wild exploit. It exploits a fundamental mismatch in how handle-type and buffer-type RPC calls reference fast_rpc_mmap structures.
When making an RPC call:
- Buffer-type invocations call
fast_rpc_mmap_createdirectly and store the resulting map reference inctx->maps[index]. - Handle-type invocations call
fast_rpc_mmap_findto locate an existing map by matching virtual address range, length, and file descriptor, storing the found reference inctx->maps[index].
The vulnerability: fast_rpc_mmap_find will match any map on fl->maps whose VA range, length, and FD overlap — not necessarily the specific map the context intended to reference. The attack sequence:
- Create
mapAvia a handle-type invocation on ContextX. ContextX's maps array holds a reference tomapA. - Create
mapBwith the same VA range, length, and FD asmapA(a "colliding" map). A second context, ContextY, takes a buffer-type reference tomapB. - Destroy ContextX. The context cleanup code calls
fast_rpc_mmap_freeon the reference in ContextX's maps array. But due to the handle-type indirection throughfast_rpc_mmap_find, this reference points to mapB, not mapA — so ContextX dropsmapB's reference count to zero and frees it. - ContextY now holds a dangling reference to freed
mapB. Subsequent use of that context crashes infast_rpc_mmap_freewith use-after-free behavior.
This bug matches the kernel logs: crashes in fast_rpc_mmap_free, and the affected kernel version matches the device version identified from the in-the-wild artifacts. It was reported by two independent researchers (Jenkins and another, making it a duplicate), with the combined patch timeline reaching over 130 days on Android after accounting for the Qualcomm → OEM → Android Security Bulletin supply chain.
Reconstructing the Exploit: inotify Spray and Timestamp Channel IDs
▶ Watch: Exploit reconstruction and PoC (32:00+)
The anomalous December 17, 2023 epoch timestamp appearing as a channel ID in the kernel logs is the pivotal forensic clue. Jenkins's reconstruction explains it through inotify_event_info heap spray:
inotify_event_infoobjects are kmalloc'd ininotify_handle_event. Their size is controlled by the length of the filename being watched, making them a flexible allocator-size primitive.- An
inotify_event_infoobject's memory layout contains, at offset 16, a pointer to aninodestructure. - An
inodestructure has, at offset 104, thei_mtime.tv_secfield — the file's last modification time as a Linux epoch timestamp.
If a freed fast_rpc_mmap object is reallocated as an inotify_event_info (and the inode it references belongs to a recently written file), the field that the ADSPRPC driver reads as the channel ID corresponds exactly to i_mtime.tv_sec — the modification timestamp of the file whose inotify event is being processed. A file written during the exploit on December 17, 2023, would produce precisely the value observed in the crash logs.
Jenkins implemented a proof-of-concept demonstrating UAF followed by inotify spray on the same CPU, generating crash logs showing invalid channel IDs with current-date epoch timestamps — matching the in-the-wild log structure, including multi-second delays between repeated channel ID log lines consistent with inotify event batching behavior.
Pipe buffers appear in the logs as additional allocation and spray primitives, consistent with their documented use as heap control objects in Android LPE research. Jenkins notes that inotify has previously been used as an information leak primitive, suggesting the full exploit chain likely used it for a kernel ASLR defeat before pivoting to control-flow hijack.
Notable Quotes
"So those kernel logs ended up getting provided to Google Project Zero by TAG for deeper inspection. And that's where I come in." — Seth Jenkins, ▶ (2:00)
"It turns out this value, when you plug it in, is a Linux epoch timestamp in seconds, and that date corresponds to the day the exploit was thrown — December 17th, 2023. Why should a channel ID be a date? That should never be the case." — Seth Jenkins, ▶ (4:00)
"None of these bugs were fixed in under 90 days from patch creation. The Qualcomm to OEM to Android supply chain introduces significant delays at every step." — Seth Jenkins, ▶ (40:00+)
Key Takeaways
- Five vulnerabilities in the Qualcomm ADSPRPC driver were found in 2.5 months by widening focus from the specific in-the-wild bug to any vulnerability in the driver — a productive pivot when crash artifacts are insufficient to identify the root cause directly.
- The flIsCompat arbitrary-read bug (Bug 1) demonstrates a class of compat-interface errors where a flag intended to track 32/64-bit marshaling mode persists incorrectly for the lifetime of a file descriptor.
- The most likely in-the-wild bug (Bug 4) is a use-after-free caused by handle/buffer reference-count mismatch in
fast_rpc_mmapobjects — a subtle lifecycle management error in a highly concurrent structure stored across three different list types. - inotify event objects are a viable heap spray and forensic fingerprinting target: their embedded inode timestamps can corrupt fields in adjacent freed structures in ways that leave the exploitation timestamp visible in kernel crash logs long after the exploit has run.
- Qualcomm's patch supply chain — vendor fix to OEM integration to Android Security Bulletin — introduces delays of 100–260 days for critical local privilege escalation bugs reachable from the UNTRUSTED_APP context, leaving hundreds of millions of unpatched devices exposed.
Reviews
Dr. Zero (Offensive Security Researcher) — STRONG ACCEPT
Jenkins reverse-engineering a commercial exploit chain from nothing but kernel panic logs is exactly the kind of forensic detective work that OffensiveCon should platform — the inotify epoch-timestamp artifact as a forensic fingerprint of the UAF spray primitive is genuinely clever. Five new ADSPRPC vulnerabilities in 2.5 months, with the in-the-wild candidate correctly identified, makes this both technically solid and politically important. The 100–260 day patch supply chain data alone is worth keeping in the program.
Heather Calloway (CISO) — STRONG ACCEPT
Seth Jenkins reconstructed an in-the-wild exploit chain targeting a Serbian activist's device — five vulnerabilities in Qualcomm's ADSPRPC driver, likely used by Cellebrite UFED. The Qualcomm patch supply chain runs 100 to 260 days. The driver is reachable from the UNTRUSTEDAPP SELinux context with no additional permissions required.