Critical watch ~ plausible CVSS 8.3 chrome Input Validation 📄 Reporter bug report 🔧 Commit mapped

Overview

Critical
Severity
8.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in WebGL
DescriptionInsufficient validation of untrusted input in WebGL
ComponentWebGL
Bug ClassInput Validation
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety — revised by independent review by AI
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Independent review by AIPlausible (tier revised from patch-now)
Reviewer notes: Double-read fix matches diff, but winning the race requires a compromised renderer (as the analysis itself says), so 'default' preconditions overstate. Fix is limited to the GL backend (BufferGL). GPU-process memory corruption is asserted, not shown by the diff.
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker503615025
Fix commit557d67e3f66d (angle/angle) +19/-8
CWECWE-20 (Improper input validation)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-04-17 (per vendor advisory)
Fix landed2026-04-24 (public git)
Disclosed2026-05-27
Patch gap33 days — fix public in source before the advisory

Background

WebGL element array buffer
GPU buffer holding vertex indices that a draw call dereferences, so its contents must stay within the bounds of the bound vertex arrays.
Client/renderer-shared memory
the data pointer passed to BufferGL::setData/setSubData points into memory the untrusted renderer can still write, so its bytes can change out from under the GPU process.
Double-read / TOCTOU
a time-of-check-to-time-of-use flaw where the same untrusted source is read twice and can be mutated between reads, so the checked value and the used value differ.

Root Cause Analysis

Verify before building a PoC

This analysis is consistent with the fixing commit but not fully provable from it: part of the vulnerable path lives in code the fix commit does not touch. Confirm the trigger and the sink against the source before relying on it for exploit development.
What the commit does not establish: Double-read fix matches diff, but winning the race requires a compromised renderer (as the analysis itself says), so 'default' preconditions overstate. Fix is limited to the GL backend (BufferGL). GPU-process memory corruption is asserted, not shown by the diff.

BufferGL::setData and BufferGL::setSubData each read the client-supplied data buffer twice from independent code paths. The original setData first called functions->bufferData(target, size, data, usage), which makes the driver read data to populate the GPU buffer, and then separately performed a memcpy of data into mShadowCopy for later index-range validation. Because data points into renderer-shared memory, a concurrent renderer thread can rewrite those bytes in the window between the driver’s read and ANGLE’s memcpy.

The result is that the driver’s GPU copy of the index data and ANGLE’s mShadowCopy are populated from different byte values, so the shadow copy no longer reflects what the GPU will actually draw with. Since draw-call bounds checking validates index ranges against mShadowCopy, an attacker can arrange for the shadow to pass validation while the GPU draws with larger, out-of-bounds indices.

The fix reads the untrusted data exactly once into the shadow copy and then uploads to the driver from that shadow copy, guaranteeing the validated bytes and the drawn bytes are identical.

Key insight
The security boundary depends on the driver and ANGLE’s validator seeing byte-for-byte identical index data, but reading the mutable client buffer twice let those two views diverge under a race. Making the shadow copy the single source of truth for both the upload and the validation closes the TOCTOU.

Attack Path

  1. Prime a shadowed index buffer The renderer creates a WebGL element array buffer that ANGLE mirrors into mShadowCopy and issues bufferData/bufferSubData with data in shared memory.
  2. Race the two reads A second renderer-controlled thread rewrites the bytes of data in the interval between the driver’s bufferData read and ANGLE’s memcpy into mShadowCopy.
  3. Diverge validation from reality The mShadowCopy ends up holding small in-bounds indices while the driver’s GPU buffer holds large out-of-bounds indices (or vice versa).
  4. Issue a draw call Index-range validation checks the benign mShadowCopy and passes, but the GPU dereferences vertex arrays using the malicious out-of-bounds indices.
  5. Trigger OOB access The draw reads vertex data outside the bound arrays, yielding out-of-bounds memory access on the GPU/driver side driven entirely by untrusted input.

Impact Assessment

This is a logic error that defeats WebGL’s element-array bounds checking, allowing a compromised or malicious renderer to make the GPU draw with unvalidated, out-of-bounds indices. The consequence is out-of-bounds memory access in the GPU process, which can lead to information disclosure or memory corruption reachable from web content. Because it requires winning a race on shared memory, exploitation is probabilistic but repeatable and driven by attacker-controlled input.

Files Changed

  • src/libANGLE/renderer/gl/BufferGL.cpp

Audit Directions

  • Double-read of shared memory
    Flag any code that passes a client/renderer-shared pointer to both a driver upload and a separate memcpy/validation read; require a single read into a trusted buffer used by both.
  • Validate-then-use divergence
    Wherever a shadow or cache backs security validation, confirm the validated bytes are the exact bytes handed to the lower layer, not a re-read of mutable input.
  • Shared-memory TOCTOU in GPU commands
    Audit other BufferGL/*GL upload paths and any mShadowCopy consumers for reads of untrusted memory that can be mutated concurrently between check and use.
From 557d67e3f66d99c231aeffcb5bfed3e85c5ecda6 Mon Sep 17 00:00:00 2001
From: Ken Russell <[email protected]>
Date: Wed, 22 Apr 2026 17:20:26 -0700
Subject: [PATCH] Read element array data exactly once for WebGL.

If the buffer's shadow copy is present, copy the client data to it, and
upload from the shadow copy, to avoid potential double-read races.

Co-authored with jetski-cli and geofflang@.

Bug: chromium:503615025
Change-Id: I7c9d77f04a0d0677143b10449df6dc528a8e5ba5
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7788210
Auto-Submit: Kenneth Russell <[email protected]>
Reviewed-by: Geoff Lang <[email protected]>
Commit-Queue: Kenneth Russell <[email protected]>
Reviewed-by: Shahbaz Youssefi <[email protected]>
---

diff --git a/src/libANGLE/renderer/gl/BufferGL.cpp b/src/libANGLE/renderer/gl/BufferGL.cpp
index 53760da..28f2da5 100644
--- a/src/libANGLE/renderer/gl/BufferGL.cpp
+++ b/src/libANGLE/renderer/gl/BufferGL.cpp
@@ -95,10 +95,6 @@
     const FunctionsGL *functions = GetFunctionsGL(context);
     StateManagerGL *stateManager = GetStateManagerGL(context);
 
-    stateManager->bindBuffer(DestBufferOperationTarget, mBufferID);
-    ANGLE_GL_TRY(context, functions->bufferData(gl::ToGLenum(DestBufferOperationTarget), size, data,
-                                                ToGLenum(usage)));
-
     // Initialize the shadow buffer if needed. Don't delete existing shadow data. WebGL allows users
     // to bind as an element array buffer first and then copy source/dest later (but not the other
     // way around).
@@ -107,6 +103,10 @@
         mShadowCopy = angle::MemoryBuffer();
     }
 
+    stateManager->bindBuffer(DestBufferOperationTarget, mBufferID);
+
+    const void *uploadData = data;
+
     if (mShadowCopy.has_value())
     {
         ANGLE_CHECK_GL_ALLOC(contextGL, mShadowCopy->resize(size));
@@ -114,9 +114,13 @@
         if (size > 0 && data != nullptr)
         {
             memcpy(mShadowCopy->data(), data, size);
+            uploadData = mShadowCopy->data();
         }
     }
 
+    ANGLE_GL_TRY(context, functions->bufferData(gl::ToGLenum(DestBufferOperationTarget), size,
+                                                uploadData, ToGLenum(usage)));
+
     mBufferSize = size;
 
     contextGL->markWorkSubmitted();
@@ -136,14 +140,21 @@
     StateManagerGL *stateManager = GetStateManagerGL(context);
 
     stateManager->bindBuffer(DestBufferOperationTarget, mBufferID);
-    ANGLE_GL_TRY(context, functions->bufferSubData(gl::ToGLenum(DestBufferOperationTarget), offset,
-                                                   size, data));
 
-    if (mShadowCopy.has_value() && size > 0)
+    const void *uploadData = data;
+
+    if (mShadowCopy.has_value())
     {
-        memcpy(mShadowCopy->data() + offset, data, size);
+        if (size > 0 && data != nullptr)
+        {
+            memcpy(mShadowCopy->data() + offset, data, size);
+            uploadData = mShadowCopy->data() + offset;
+        }
     }
 
+    ANGLE_GL_TRY(context, functions->bufferSubData(gl::ToGLenum(DestBufferOperationTarget), offset,
+                                                   size, uploadData));
+
     contextGL->markWorkSubmitted();
 
     return angle::Result::Continue;
Loading diff…

Original Bug Report

reported by [email protected]

Potential double-fetch in GPU command buffer allows WebGL index bounds validation bypass

Flapjack, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports without the Chrome Security team. Please see go/chrome-ai-generated-security-bugs-faq for more information.

Overview: The GPU process’s validating command buffer contains a double-fetch vulnerability when processing buffer updates like glBufferSubData. It reads from client-controlled shared memory twice: once to update a CPU-side shadow buffer used for validation, and again to send data to the GL driver. A compromised renderer can concurrently modify this shared memory to bypass index bounds validation, potentially causing an out-of-bounds access in the GPU hardware.

Affected files:

  • gpu/command_buffer/service/buffer_manager.cc
  • gpu/command_buffer/service/gles2_cmd_decoder.cc

Estimated timestamp from git blame: 2025-11-28

Vulnerability Details

To prevent malicious WebGL content from causing out-of-bounds (OOB) memory accesses in the GPU driver, the validating command buffer maintains a CPU-side “shadow” copy of GL_ELEMENT_ARRAY_BUFFER (index buffer) data. During a glDrawElements call, the service scans this shadow buffer to find the maximum index accessed, ensuring it fits within the bounds of the currently bound vertex attributes.

However, several buffer update functions exhibit a time-of-check to time-of-use (TOCTOU) / double-fetch vulnerability when reading from shared memory. For example, in BufferManager::DoBufferSubData:

void BufferManager::DoBufferSubData(
    Buffer* buffer, GLenum target, GLintptr offset, GLsizeiptr size,
    const GLvoid* data) {
  // FETCH 1: Copies data from shared memory `data` to the CPU shadow buffer
  buffer->SetRange(offset, size, data);

  if (!buffer->IsClientSideArray()) {
    // FETCH 2: Driver reads directly from the shared memory `data` to GPU memory
    glBufferSubData(target, offset, size, data);
  }
}

The data pointer originates from a transfer buffer (base::UnsafeSharedMemoryRegion) mapped by both the renderer and the GPU process. Because there are no cross-process locks, the renderer can modify the memory between SetRange and glBufferSubData. Similar vulnerable patterns exist in GLES2DecoderImpl::DoFlushMappedBufferRange and GLES2DecoderImpl::UnmapBufferHelper.

Potential Exploitation Steps

(Note: These are suggested/potential steps, as our tooling agent does not have the ability to run code or verify a live Proof of Concept.)

  1. Renderer Compromise: An attacker gains code execution in a sandboxed renderer process.
  2. Setup: The attacker initializes a WebGL context, creating a small vertex buffer and an element array buffer.
  3. Race Preparation: The attacker maps a shared memory transfer buffer and issues a BufferSubData command pointing to it.
  4. Concurrent Modification: A worker thread in the renderer rapidly alternates the contents of the shared memory between “safe” indices (within vertex buffer bounds) and “malicious” indices (large, out-of-bounds values).
  5. Winning the Race: If the GPU process executes buffer->SetRange while the memory contains safe indices, the shadow buffer is populated safely. Microseconds later, the renderer thread overwrites the shared memory with malicious indices. The GPU process then executes glBufferSubData, uploading the malicious indices to GPU VRAM.
  6. Triggering the OOB: The attacker issues a glDrawElements command. The GPU service validates the draw call using the safe indices in the shadow buffer, allowing the call to proceed. The underlying GPU hardware executes the draw using the malicious VRAM buffer, resulting in an OOB memory access.

Because the validating command decoder is the default on Android, and the GPU process is unsandboxed on that platform, an OOB access in the GPU driver can potentially lead to a full sandbox escape.

Suggested Fix

Do not pass the raw shared memory pointer to the underlying GL driver if the buffer is shadowed. Instead, copy the data once into service-side memory (which the SetRange call already does by populating shadow_), and pass the safe, service-side pointer to the GL driver.

For example, in BufferManager::DoBufferSubData:

  buffer->SetRange(offset, size, data);
  if (!buffer->IsClientSideArray()) {
    // Pass the pointer from the shadow buffer, not the shared memory
    glBufferSubData(target, offset, size, buffer->GetRange(offset, size));
  }

Similar refactoring should be applied to DoFlushMappedBufferRange and UnmapBufferHelper to ensure that data written to the GPU strictly matches the data validated in the shadow buffer.

Evaluated with Chrome root at commit: c0eb5541aebfa4ea08806eaf6e94bcc69f87ab2f


Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.

View on issue tracker