Critical chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Critical
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in WebGL
DescriptionInsufficient validation of untrusted input in WebGL
ComponentWebGL
Bug ClassLogic Error
Tracker503615025
Fix commit557d67e3f66d (angle/angle) +19/-8
CISA KEVNot listed
CreditedGoogle
Disclosed2026-05-27

Files Changed

  • src/libANGLE/renderer/gl/BufferGL.cpp
From 557d67e3f66d99c231aeffcb5bfed3e85c5ecda6 Mon Sep 17 00:00:00 2001
From: Ken Russell <kbr@chromium.org>
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 <kbr@chromium.org>
Reviewed-by: Geoff Lang <geofflang@chromium.org>
Commit-Queue: Kenneth Russell <kbr@chromium.org>
Reviewed-by: Shahbaz Youssefi <syoussefi@chromium.org>
---

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 rj...@google.com

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