home intel cve-2025-13952-imagination-gpu-shader-uaf
CVE Analysis 2026-01-24 · 9 min read

CVE-2025-13952: UAF in Imagination GPU Shader Compiler (CVSS 9.8)

A crafted WebGL shader triggers a write use-after-free in the Imagination GPU compiler process. On privileged compiler processes, this enables full device compromise.

#use-after-free#gpu-shader-compiler#memory-safety#remote-code-execution#privilege-escalation
Technical mode — for security professionals
▶ Attack flow — CVE-2025-13952 · Use After Free
ATTACKERRemote / unauthUSE AFTER FREECVE-2025-13952Cross-platform · CRITICALCODE EXECArbitrary coderuns as targetCOMPROMISEFull accessNo confirmed exploits

Vulnerability Overview

CVE-2025-13952 is a write use-after-free (UAF) in the Imagination Technologies PowerVR GPU shader compiler library. A remote attacker serving a web page containing a malformed GLSL or USC shader program can trigger the bug without any user interaction beyond page load. The compiler processes the shader, walks a stale pointer into freed heap memory, and writes attacker-influenced data to it. CVSS scores this at 9.8 Critical.

The threat model is particularly severe on platforms — primarily Android SoCs using PowerVR Series6, Series8XE, Series9XE, and IMG A-series cores — where the GPU compiler runs in-process inside a privileged driver context. In those configurations the write primitive lands inside a process that may hold CAP_SYS_ADMIN or equivalent, making exploitation a single-hop to kernel.

Root cause: The USC shader compiler retains a raw pointer to an intermediate representation (IR) node after that node is freed during a DCE/CSE optimization pass, then unconditionally writes through the stale pointer when emitting code for a dependent instruction.

Affected Component

The vulnerability lives inside libUSC (Unified Shader Compiler), Imagination's ahead-of-time compiler backend shared across all PowerVR driver stacks. The library is statically linked into the kernel module on older Android BSPs and dynamically loaded as a userspace library on newer ones. The affected symbol is inside the instruction scheduling and register-allocation pipeline.

  • Driver package: pvrsrvkm / pvr_km
  • Userspace compiler: libUSC.so / libpvr_dri_support.so
  • Entry path: WebGLANGLEglCompileShader → USC backend
  • Affected versions: See NVD / Imagination advisory at imaginationtech.com/gpu-driver-vulnerabilities/

Root Cause Analysis

The compiler represents shader programs as a doubly-linked list of USC_INST nodes. During optimization, dead-code elimination calls RemoveInst(), which unlinks and frees a node. A secondary optimization pass — common subexpression elimination — holds a cached USC_INST *psBestInst pointer to the same node that was just freed. When CSE later tries to rewrite register sources on a dependent instruction, it dereferences psBestInst and writes the replacement source operand directly into the freed object.


/*
 * Decompiled pseudocode: USC shader compiler backend
 * Function: CopySourceArguments / ApplyCSEForInst
 * libUSC.so (representative decompilation)
 */

typedef struct _USC_INST {
    /* +0x00 */ struct _USC_INST  *psPrev;
    /* +0x08 */ struct _USC_INST  *psNext;
    /* +0x10 */ uint32_t           uOpcode;
    /* +0x14 */ uint32_t           uFlags;
    /* +0x18 */ USC_ARGUMENT       asDest[2];
    /* +0x38 */ USC_ARGUMENT       asSrc[4];
    /* +0x78 */ uint32_t           uSourceCount;
    /* +0x7C */ uint32_t           uLiveChans;
    /* +0x80 */ void              *pvCSEData;  // CSE annotation pointer
} USC_INST;  // sizeof == 0x88

typedef struct _USC_ARGUMENT {
    /* +0x00 */ uint32_t  uType;
    /* +0x04 */ uint32_t  uNumber;
    /* +0x08 */ uint32_t  uChanMask;
    /* +0x0C */ uint32_t  uFlags;
} USC_ARGUMENT;  // sizeof == 0x10

/*
 * CSE pass: caches best matching instruction for a given
 * value equivalence class.
 */
static void ApplyCSEForGroup(USC_STATE *psState,
                             USC_INST  *psInst,
                             USC_INST **ppsBestInst)  // cached across DCE!
{
    USC_INST *psBestInst = *ppsBestInst;

    if (psBestInst == NULL) {
        *ppsBestInst = psInst;
        return;
    }

    /*
     * DCE may have freed psBestInst between the two calls.
     * No validity check is performed here.
     *
     * BUG: psBestInst is never validated against the live instruction
     * list. If DCE ran between iterations and freed this node,
     * the write below is a UAF write into freed heap memory.
     */
    for (uint32_t i = 0; i < psInst->uSourceCount; i++) {
        /* UAF WRITE: psBestInst->asSrc[i] may be freed memory */
        psInst->asSrc[i] = psBestInst->asSrc[i];  // BUG: stale pointer deref + copy
    }

    /*
     * Second UAF WRITE: marks the best-inst slot with replacement info.
     * Attacker controls register numbers via shader source operands.
     */
    psBestInst->asDest[0].uNumber = psInst->asDest[0].uNumber; // BUG: write to freed obj
}

The critical detail: USC_ARGUMENT.uNumber (the register number) is directly derived from the shader's operand encoding. An attacker who controls the register numbering in the shader controls the 32-bit value written into the freed object at offset +0x1C relative to psBestInst->asDest[0] (i.e., psBestInst + 0x18 + 0x04 == psBestInst + 0x1C).

Memory Layout


USC COMPILER HEAP — BEFORE DCE PASS:
  [0x7f4c002200]  USC_INST node A  (0x88 bytes)  <-- psBestInst cached here
                    psPrev   = 0x7f4c002100
                    psNext   = 0x7f4c002290
                    uOpcode  = 0x0000004E  (FMAD)
                    asDest[0].uNumber = 0x00000003  (r3)
                    asSrc[0].uNumber  = 0x00000007  (r7)
  [0x7f4c002290]  USC_INST node B  (0x88 bytes)  <-- psInst (current)
  [0x7f4c002320]  USC_INST node C  (0x88 bytes)

USC COMPILER HEAP — AFTER DCE FREES NODE A:
  [0x7f4c002200]  FREE CHUNK       (0x88 bytes)  <-- tcache/bin candidate
                    fd = 0x7f4c000010  (tcache next ptr)
                    bk = 0x0000000000  (unused)
                    [rest of 0x88 bytes = potentially reallocated]
  [0x7f4c002290]  USC_INST node B  (0x88 bytes)  <-- psInst still valid

CSE PASS RESUMES — UAF WRITE:
  psBestInst = 0x7f4c002200  (STALE — chunk is free or reallocated)
  write: *(uint32_t*)(0x7f4c002200 + 0x1C) = psInst->asDest[0].uNumber
                                            = ATTACKER_CONTROLLED_VALUE

  If allocator reused 0x7f4c002200 for another USC_INST:
    → Corrupts asDest[0].uNumber of the reallocated node
    → Propagates attacker register number through subsequent codegen
    → Enables type confusion in register allocator output

  If allocator reused 0x7f4c002200 for a USC_STATE sub-object:
    → Corrupts compiler state struct (function pointer table adjacent)
    → Potential control flow hijack within compiler process

Exploitation Mechanics

The UAF alone gives a 32-bit write into recycled heap memory at a predictable offset. Turning this into reliable code execution requires heap grooming to control what occupies the freed slot.


EXPLOIT CHAIN:

1. HEAP GROOMING via shader spam
   Attacker JS submits ~200 small benign shaders via WebGL to fill
   tcache bins for size 0x88 (sizeof USC_INST), draining the free list.

2. TRIGGER SHADER LOAD
   Load the crafted shader. The USC compiler allocates fresh 0x88 chunks
   for the IR graph. Two FMAD instructions are constructed such that:
     - Node A and Node B are in the same CSE equivalence class
     - A later DCE pattern kills Node A (zero-use after peephole)
     - Node B has a src operand whose register number = target value

3. DCE FREES NODE A
   RemoveInst() unlinks Node A from the instruction list and calls
   free(NodeA). The 0x88 chunk returns to tcache[0x88].

4. GROOM: REALLOCATE freed slot with a USC_REGALLOC_DATA object
   A second concurrent shader compilation (Web Workers) allocates a
   USC_REGALLOC_DATA struct which also happens to be 0x88 bytes.
   This lands at the address previously held by Node A.

   struct USC_REGALLOC_DATA {
     /* +0x00 */ void     *pfnAllocCallback;   // function pointer!
     /* +0x08 */ void     *pfnFreeCallback;
     /* +0x10 */ uint32_t  uFlags;
     /* +0x14 */ uint32_t  uRegCount;
     /* +0x18 */ uint32_t  uSpillAreaBase;     // <-- +0x18 target
     /* +0x1C */ uint32_t  uSpillAreaSize;     // <-- UAF WRITE LANDS HERE
     ...
   };

5. UAF WRITE FIRES
   CSE pass resumes, dereferences psBestInst (now = USC_REGALLOC_DATA*),
   writes attacker-chosen register number to offset +0x1C:
     uSpillAreaSize = ATTACKER_VALUE (e.g. 0xFFFFFFFF)

6. OVERFLOW IN SPILL ALLOCATION
   Register allocator calls:
     pSpillBuf = malloc(psData->uSpillAreaSize * 4)
                = malloc(0xFFFFFFFF * 4)  → wraps to tiny allocation
   Subsequent spill slot writes overflow pSpillBuf into adjacent heap.

7. HEAP METADATA CORRUPTION → ARBITRARY WRITE
   Overflow corrupts next chunk's size/fd pointer.
   On Android with jemalloc/scudo, craft fd to point to GOT entry for
   a frequently-called libUSC function pointer.

8. CONTROL FLOW HIJACK
   Next malloc() returns attacker-controlled address.
   Compiler writes function pointer into that address.
   Next codegen call dispatches to attacker shellcode.

9. PRIVILEGE ESCALATION (platform-dependent)
   On privileged compiler processes (Android kernel driver context):
   shellcode calls commit_creds(prepare_kernel_cred(0)) via ROP.

Patch Analysis

The fix is conceptually simple: validate the cached psBestInst pointer against the live instruction list before use, or — more robustly — nullify the cache entry when the instruction is removed.


// BEFORE (vulnerable):
// RemoveInst() frees the USC_INST node but never notifies CSE state.
static void RemoveInst(USC_STATE *psState, USC_INST *psInst)
{
    psInst->psPrev->psNext = psInst->psNext;
    psInst->psNext->psPrev = psInst->psPrev;
    UscFree(psState, psInst);  // freed — no CSE cache invalidation
}

static void ApplyCSEForGroup(USC_STATE *psState,
                             USC_INST  *psInst,
                             USC_INST **ppsBestInst)
{
    USC_INST *psBestInst = *ppsBestInst;
    if (psBestInst == NULL) { *ppsBestInst = psInst; return; }

    for (uint32_t i = 0; i < psInst->uSourceCount; i++) {
        psInst->asSrc[i] = psBestInst->asSrc[i];  // UAF if DCE freed psBestInst
    }
    psBestInst->asDest[0].uNumber = psInst->asDest[0].uNumber;  // UAF WRITE
}

// AFTER (patched):
// RemoveInst() now clears any CSE back-reference stored in the node,
// and the CSE equivalence class entry is explicitly invalidated.
static void RemoveInst(USC_STATE *psState, USC_INST *psInst)
{
    psInst->psPrev->psNext = psInst->psNext;
    psInst->psNext->psPrev = psInst->psPrev;

    /* Invalidate CSE equivalence class entry for this instruction */
    if (psInst->pvCSEData != NULL) {
        USC_CSE_ENTRY *psEntry = (USC_CSE_ENTRY *)psInst->pvCSEData;
        psEntry->psBestInst = NULL;  // FIX: nullify cache on free
        psInst->pvCSEData   = NULL;
    }

    UscFree(psState, psInst);
}

static void ApplyCSEForGroup(USC_STATE *psState,
                             USC_INST  *psInst,
                             USC_INST **ppsBestInst)
{
    USC_INST *psBestInst = *ppsBestInst;

    /* FIX: explicit NULL check — RemoveInst() will have cleared this
     * if psBestInst was freed during DCE */
    if (psBestInst == NULL) { *ppsBestInst = psInst; return; }

    for (uint32_t i = 0; i < psInst->uSourceCount; i++) {
        psInst->asSrc[i] = psBestInst->asSrc[i];  // safe: psBestInst is live
    }
    psBestInst->asDest[0].uNumber = psInst->asDest[0].uNumber;  // safe
}

The secondary hardening applied by Imagination is wrapping UscFree() to poison freed USC_INST memory with 0xDEADBEEF in debug builds and to use a generation counter scheme in production builds, so any stale pointer dereference traps before writing.

Detection and Indicators

Because the vulnerability lives in a compiler library, runtime detection is difficult. The following signals are worth monitoring:


CRASH SIGNATURE (ASan / KASAN output):
  ==ERROR: AddressSanitizer: heap-use-after-free on address 0x7f4c00221c
  WRITE of size 4 at 0x7f4c00221c thread T0
    #0 ApplyCSEForGroup       libUSC.so
    #1 RunCSEOnBlock          libUSC.so
    #2 OptimiseShader         libUSC.so
    #3 glCompileShader_impl   libGLESv2.so

SHADER CHARACTERISTICS that trigger the path:
  - Two or more FMAD / FMA instructions sharing identical source operands
    (same register, same swizzle) — needed to form a CSE class
  - A conditional discard or dynamic loop that causes one instruction
    to be DCE-eliminated after CSE class is established
  - Unusual register pressure: many live variables forcing the compiler
    into the spill path after the corrupted uSpillAreaSize is consumed

WEBGL INDICATOR (browser console):
  WebGL: CONTEXT_LOST_WEBGL  (crash in GPU process)
  Followed immediately by: navigator.gpu undefined (process restart)

ANDROID TOMBSTONE:
  signal 11 (SIGSEGV), code 2 (SEGV_ACCERR)
  fault addr 0x00000000deadbeXX  (if poison is active)
  backtrace:
    #00  libUSC.so (ApplyCSEForGroup+0x???)
    #01  libpvrsrvkm.ko (PVRSRVBridgeRGXKickTA3D)

Remediation

  • Update immediately. Apply the driver package version listed in the Imagination advisory at imaginationtech.com/gpu-driver-vulnerabilities/. OEM BSP integrators must pull the updated libUSC into their kernel/HAL builds.
  • Browser-level mitigation. Chromium's --disable-webgl flag or blocklist_entry for affected GPU IDs removes the remote attack surface entirely until drivers are patched.
  • Process isolation. On Android, ensure the GPU compiler runs under isolated_app SELinux domain rather than in the system_server or kernel module context. This does not fix the bug but contains the blast radius.
  • Heap hardening. Enable Scudo's chunk header integrity checks (SCUDO_OPTIONS=release_to_os_interval_ms=0) and Android's heapprofd in canary builds to detect UAF writes pre-production.
  • Shader validation. Deploy a pre-compilation shader validator (e.g., glslang + custom IR checker) that rejects shader programs exhibiting the specific FMAD + conditional-discard pattern before they reach libUSC.
CB
CypherByte Research
Mobile security intelligence · cypherbyte.io
// RELATED RESEARCH
// WEEKLY INTEL DIGEST

Get articles like this every Friday — mobile CVEs, threat research, and security intelligence.

Subscribe Free →