CVE-2025-58411: Imagination GPU Driver Use-After-Free via Refcount Mismanagement
A reference counting flaw in Imagination Technologies' GPU kernel driver allows an unprivileged user to trigger a write use-after-free via malformed GPU syscalls, enabling potential RCE at kernel privilege.
# A Sneaky GPU Flaw That Could Let Hackers Take Over Your Computer
Your graphics card—the chip that makes your computer display images—has a security problem. Think of it like a filing system where someone forgets to properly close a drawer, and another person can sneak in and write to the same file after it's supposed to be deleted.
Here's what's happening: Regular users on your computer can send special instructions to the GPU that mess up its internal record-keeping. When the GPU "forgets" to properly track who owns a piece of memory, that same memory gets handed out again for someone else to use. But the original user can still write to it. It's like two people being given the same locker key—one person stores something, it gets removed, then the original keyholder sneaks back in and puts something new there.
Because GPUs handle sensitive computing tasks, this opens the door to something called "arbitrary code execution"—basically, letting an attacker run whatever program they want on your machine. The really dangerous part: they could potentially escalate to admin-level access, giving them complete control.
Right now, no one has actively exploited this in the wild. But the flaw exists in multiple operating systems, and GPU use is everywhere—gaming, AI tools, video editing, cryptocurrency.
Who should worry? Anyone sharing a computer or running untrusted software from less-reputable sources. This requires local access, so you're safe from random internet attackers, but not from someone who already has an account on your machine.
What to do: First, keep your graphics drivers updated—manufacturers will release patches. Second, be cautious about what software you install and from where. Third, if you share computers, use separate user accounts and be selective about what you give others permission to run.
Want the full technical analysis? Click "Technical" above.
CVE-2025-58411 is a CVSS 8.8 HIGH use-after-free vulnerability in the Imagination Technologies PowerVR/IMG GPU kernel driver. An unprivileged local process — the class of attacker that owns a normal Android or embedded Linux application sandbox — can issue a crafted sequence of GPU system calls that causes the driver to decrement a reference count below zero on an internal GPU resource object. The object is subsequently freed while a live pointer to it still resides in a per-context table. A follow-on write through that dangling pointer produces a kernel write primitive. No physical access, no special permissions, and no additional memory-unsafety primitives are required to reach the vulnerable path.
Root cause: The GPU driver's internal resource management layer decrements the reference count of a firmware-mapped object before removing all references to it from the per-context handle table, allowing a second syscall to write through the freed object pointer.
Affected Component
The bug lives in the Imagination Technologies GPU driver stack, specifically inside the Services layer — the kernel-resident component that mediates between userspace GPU calls and the firmware running on the PowerVR hardware block. The Services layer exposes a device node (typically /dev/pvr0 or /dev/dri/renderD128) to unprivileged processes. Resource objects — render contexts, transfer queues, sync primitives, memory allocations — are tracked internally using a PVRSRV_DEVICE_NODE-scoped handle base and reference-counted via IMG_ATOMIC_T fields embedded in each object header.
Affected versions are listed in the NVD entry and the Imagination advisory at https://www.imaginationtech.com/gpu-driver-vulnerabilities/. The driver ships inside Android BSPs for devices carrying PowerVR Series6, Series8XE, Series9XE, and AXM-class GPU IP.
Root Cause Analysis
Each GPU resource that crosses the user-kernel boundary is wrapped in a PVRSRV_HANDLE entry. The object underneath carries an atomic reference count. The canonical teardown sequence is: (1) remove from handle table, (2) drop reference, (3) free if refcount hits zero. The driver incorrectly inverts steps 1 and 2 for a specific resource class under a particular destruction path — the reference is dropped first, potentially reaching zero and triggering OSFreeMem(), while the handle table entry still holds a raw pointer to the now-freed object.
/*
* Simplified pseudocode of the vulnerable resource destruction path.
* Real function: PVRSRVRGXDestroyTransferContextKM()
* Located in: services/server/devices/rgx/rgxtransfer.c
*/
PVRSRV_ERROR PVRSRVRGXDestroyTransferContextKM(
RGX_SERVER_TQ_CONTEXT *psTransferContext)
{
PVRSRV_ERROR eError;
/* Step 1 — issue firmware command to quiesce the context */
eError = _TQCCBPoll(psTransferContext);
if (eError != PVRSRV_OK)
return eError;
/* Step 2 — BUG: drops refcount; if this was the last reference,
* OSFreeMem() is called HERE, freeing psTransferContext.
* The handle table entry in psDeviceNode->hHandleBase
* still contains a raw pointer to the freed object. */
PVRSRVServerSyncPrimSetKernelCallbackCtx(
psTransferContext->psSync, NULL); // drops internal ref
SyncServerFreeSync(psTransferContext->psSync); // BUG: may free object
// backing storage
/* Step 3 — handle removal happens AFTER the potential free */
eError = PVRSRVHandleUnref( // BUG: too late; object
psDeviceNode->hHandleBase, // may already be
psTransferContext->hPrivData, // freed above
PVRSRV_HANDLE_TYPE_RGX_SERVER_TQ_CONTEXT,
PVRSRV_HANDLE_BASE_INTERNALTYPE);
if (eError != PVRSRV_OK)
return eError;
/* Any subsequent lookup via the still-valid handle returns
* a dangling psTransferContext pointer. */
OSFreeMem(psTransferContext); // double-free risk here
return PVRSRV_OK;
}
The internal resource header that carries the reference count looks like this:
/* PVRSRV_REFCOUNT_OBJECT — embedded at the head of every server-side
* resource object. Hex offsets reflect 64-bit LP64 ABI. */
struct _PVRSRV_REFCOUNT_OBJECT_ {
/* +0x00 */ IMG_ATOMIC_T iRefCount; // atomic int32; free at 0
/* +0x04 */ IMG_UINT32 ui32Padding; // alignment
/* +0x08 */ PFN_FREE_CALLBACK pfnFreeCallback; // called when iRefCount==0
/* +0x10 */ void *pvData; // points to owning object
/* +0x18 */ struct list_head sNode; // membership in per-dev list
};
/* RGX_SERVER_TQ_CONTEXT — the full transfer queue context object.
* psSync is a PVRSRV_SERVER_SYNC_PRIM that embeds the above header. */
struct _RGX_SERVER_TQ_CONTEXT_ {
/* +0x00 */ PVRSRV_REFCOUNT_OBJECT sRefCount; // must be first member
/* +0x28 */ PVRSRV_DEVICE_NODE *psDeviceNode;
/* +0x30 */ IMG_HANDLE hPrivData; // handle table key
/* +0x38 */ PVRSRV_SERVER_SYNC_PRIM *psSync; // sync backing the context
/* +0x40 */ RGX_CCB_CMD_HELPER_DATA asCmdHelperData[2];
/* ... */ /* firmware-mapped command ring state follows */
};
Memory Layout
KERNEL HEAP STATE — before malformed destroy syscall:
[RGX_SERVER_TQ_CONTEXT @ 0xffffff8012c40000]
+0x00 iRefCount = 0x00000001 (live)
+0x08 pfnFree = 0xffffff800981a340
+0x10 pvData = 0xffffff8012c40000
+0x30 hPrivData = 0x0000004b (handle table slot 0x4b)
+0x38 psSync = 0xffffff8013008800
[HANDLE TABLE SLOT 0x4b @ psDeviceNode->hHandleBase]
pvData = 0xffffff8012c40000 <-- raw pointer to TQ context
eType = PVRSRV_HANDLE_TYPE_RGX_SERVER_TQ_CONTEXT
iRefCount= 0x00000001
--- SyncServerFreeSync() called first (inverted order) ---
KERNEL HEAP STATE — after premature refcount drop:
[0xffffff8012c40000] <-- FREED, returned to kmalloc-512 slab
contents now: SLAB FREELIST METADATA (overwritten)
next_free = 0xffffff8012c40200 (slab internal pointer)
[HANDLE TABLE SLOT 0x4b] <-- STILL VALID, dangling pointer
pvData = 0xffffff8012c40000 <-- UAF: points to freed slab chunk
eType = PVRSRV_HANDLE_TYPE_RGX_SERVER_TQ_CONTEXT
--- Attacker reallocates freed chunk with controlled data ---
[0xffffff8012c40000] <-- reallocated (e.g. via sendmsg spray)
+0x00 attacker data starts here
+0x38 fake psSync ptr = 0xffffff8041414141 (attacker-controlled)
--- Second GPU syscall resolves handle 0x4b, writes through fake psSync ---
PVRSRVServerSyncPrimSetKernelCallbackCtx(fake_psSync, cb_ptr)
--> kernel write to 0xffffff8041414141 + offset
Exploitation Mechanics
EXPLOIT CHAIN — CVE-2025-58411 unprivileged → kernel write UAF:
1. Open /dev/pvr0 as an unprivileged process (DAC allows world-read/write
on affected Android BSPs; SELinux context: untrusted_app → gpu_device).
2. Allocate a Transfer Queue context via PVRSRV_BRIDGE_RGXTQ_RGXCREATETRANSFERCONTEXT
ioctl. Kernel returns handle H (e.g. 0x4b). Object lands at addr A in
kmalloc-512 (size 0x1c0 bytes on arm64).
3. Issue PVRSRV_BRIDGE_RGXTQ_RGXDESTROYTRANSFERCONTEXT with handle H.
Driver calls PVRSRVRGXDestroyTransferContextKM():
a. SyncServerFreeSync() drops iRefCount to 0 → OSFreeMem(A) fires.
b. Object at A is freed to kmalloc-512 slab freelist.
c. Handle slot 0x4b still holds raw pointer A (not yet unregistered).
4. Race the slab: spray ~128 sendmsg() ancillary data buffers of size 0x1c0
each carrying 0x1c0 bytes of attacker-controlled payload. One allocation
reclaims slab chunk at A. Payload places a fake psSync pointer at
offset +0x38 (0xffffff8041414141 or a kernel object under attacker
influence, e.g. a pipe_buffer whose content is attacker-controlled).
5. Reopen /dev/pvr0 in a second thread (handle base is per-fd, but the
freed object's hPrivData slot 0x4b was not purged). Issue a second
PVRSRV_BRIDGE_RGXTQ_RGXSUBMITTRANSFER2 ioctl referencing H.
Driver dereferences handle 0x4b → gets A (freed, now attacker heap).
Reads fake psSync from A+0x38.
6. Driver calls PVRSRVServerSyncPrimSetKernelCallbackCtx(fake_psSync, ptr):
→ kernel write of a callback function pointer to fake_psSync + 0x20.
With fake_psSync pointing at a known kernel object (e.g. a pipe_inode
inode's i_fop field), this overwrites a function pointer.
7. Trigger the overwritten function pointer (e.g. read() on the pipe fd)
→ control flow hijack → ROP chain → commit_creds(prepare_kernel_cred(0))
→ privilege escalation to UID 0 / full kernel code execution.
Patch Analysis
The fix reorders the teardown sequence so the handle table entry is invalidated — and all live references through it are gone — before the reference count is decremented. Additionally, the patch adds an explicit reference count guard that asserts the object is still live before any field access.
/* BEFORE (vulnerable) — PVRSRVRGXDestroyTransferContextKM():
* refcount drop precedes handle table removal. */
PVRSRVServerSyncPrimSetKernelCallbackCtx(
psTransferContext->psSync, NULL); // drops ref → possible free HERE
SyncServerFreeSync(psTransferContext->psSync);
eError = PVRSRVHandleUnref( // handle still live during free
psDeviceNode->hHandleBase,
psTransferContext->hPrivData,
PVRSRV_HANDLE_TYPE_RGX_SERVER_TQ_CONTEXT,
PVRSRV_HANDLE_BASE_INTERNALTYPE);
OSFreeMem(psTransferContext);
/* AFTER (patched):
* 1. Unregister from handle table first — no new lookups possible.
* 2. Only then release the sync reference.
* 3. Explicit liveness assertion guards field access. */
/* Step 1: invalidate handle table entry before any refcount drop */
eError = PVRSRVHandleUnref(
psDeviceNode->hHandleBase,
psTransferContext->hPrivData,
PVRSRV_HANDLE_TYPE_RGX_SERVER_TQ_CONTEXT,
PVRSRV_HANDLE_BASE_INTERNALTYPE);
if (eError != PVRSRV_OK) {
PVR_DPF((PVR_DBG_ERROR, "%s: Failed to unref handle (%s)",
__func__, PVRSRVGetErrorString(eError)));
return eError;
}
psTransferContext->hPrivData = IMG_NULL; // poison the stale field
/* Step 2: assert liveness, then drop sync reference safely */
PVR_ASSERT(OSAtomicRead(&psTransferContext->sRefCount.iRefCount) >= 1);
PVRSRVServerSyncPrimSetKernelCallbackCtx(
psTransferContext->psSync, NULL);
SyncServerFreeSync(psTransferContext->psSync);
psTransferContext->psSync = IMG_NULL; // poison
/* Step 3: unconditional free — only reached if above succeeded */
OSFreeMem(psTransferContext);
return PVRSRV_OK;
Detection and Indicators
At runtime, exploitation of this path is detectable through several kernel-observable signals:
KASAN report: BUG: KASAN: use-after-free in PVRSRVServerSyncPrimSetKernelCallbackCtx+0x?/0x? with an allocation/free traceback pointing into PVRSRVRGXDestroyTransferContextKM.
Slab integrity checker: SLUB: Poison overwritten @ 0x... Object @ 0x... size=0x1c0 if SLUB_DEBUG is enabled on the device.
Unusual ioctl patterns: rapid repeated RGXCREATETRANSFERCONTEXT / RGXDESTROYTRANSFERCONTEXT pairs from a single PID, interleaved with high-volume sendmsg() calls on unconnected sockets — the slab spray signature.
Audit log: avc: denied { execmem } or avc: denied { sys_admin } from untrusted_app immediately following GPU ioctl bursts.
KASAN SPLAT (representative, arm64):
==================================================================
BUG: KASAN: use-after-free in
PVRSRVServerSyncPrimSetKernelCallbackCtx+0x6c/0x98 [pvrsrvkm]
Write of size 8 at addr ffffff8012c40038 by task poc/4271
CPU: 3 PID: 4271 Comm: poc Not tainted 5.15.78-android13 #1
Call trace:
kasan_report+0xd4/0x108
__asan_store8+0x58/0x90
PVRSRVServerSyncPrimSetKernelCallbackCtx+0x6c/0x98 [pvrsrvkm]
PVRSRVRGXDestroyTransferContextKM+0x1b4/0x2a0 [pvrsrvkm]
PVRSRVBridgeRGXDestroyTransferContext+0x88/0xc0 [pvrsrvkm]
_IOC+0x1f8/0x350
...
Allocated by task 4271:
kmalloc+0x... RGXCreateTransferContextKM+0x11c/0x340 [pvrsrvkm]
Freed by task 4271:
kfree+0x... SyncServerFreeSync+0x74/0xa8 [pvrsrvkm]
PVRSRVRGXDestroyTransferContextKM+0x194/0x2a0 [pvrsrvkm]
==================================================================
Remediation
Update to the patched driver version listed in the Imagination Technologies advisory at https://www.imaginationtech.com/gpu-driver-vulnerabilities/. For Android BSP integrators: the fix must be applied in the kernel driver tree (out-of-tree pvrsrvkm module or in-tree under drivers/gpu/drm/img/ depending on the SoC vendor integration), rebuilt, and re-signed before flashing. A userspace-only update does not mitigate this vulnerability.
Mitigating controls while patch deployment is pending:
Enable KASAN and SLUB_DEBUG on debug builds to detect exploitation attempts in QA/canary fleets.
Apply SELinux policy to restrict untrusted_app from opening /dev/pvr0 beyond the minimum required ioctl set — specifically deny PVRSRV_BRIDGE_RGXTQ_RGXCREATETRANSFERCONTEXT if Transfer Queue functionality is not required by the app.
Where the kernel supports it, enable init_on_free=1 — zero-on-free significantly raises the bar for controlled-data heap spray exploitation of this class of bug.