Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in ANGLE
DescriptionInsufficient validation of untrusted input in ANGLE
ComponentANGLE
Bug ClassLogic Error
Tracker500560764
Fix commitd2dc653690e0 (angle/angle) +170/-3
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Files Changed

  • src/libANGLE/Buffer.h
  • src/libANGLE/ErrorStrings.h
  • src/libANGLE/validationES.cpp
  • src/libANGLE/validationES.h
  • src/libANGLE/validationES2.h
  • src/libANGLE/validationES3.cpp
  • src/tests/gl_tests/StateChangeTest.cpp
  • src/tests/gl_tests/WebGLCompatibilityTest.cpp
From d2dc653690e09755fff52de5e72c7a61226a8576 Mon Sep 17 00:00:00 2001
From: Geoff Lang <[email protected]>
Date: Tue, 21 Apr 2026 10:02:25 -0400
Subject: [PATCH] Validate WebGL buffer binding rules.

WebGL disallows buffers that have been bound to ELEMENT_ARRAY targets to
be bound to other targets (with some exceptions). This allows for
optimized index range caching with shadow buffers.

This validation was done in the WebGL layer and the validating command
decoder but not in ANGLE.

Fixed: chromium:500560764
Change-Id: Ib1b7948cce86284c8ed3f6f05b5e15f6c7f0b1b0
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7782484
Reviewed-by: Kenneth Russell <[email protected]>
Commit-Queue: Kenneth Russell <[email protected]>
Reviewed-by: Shahbaz Youssefi <[email protected]>
---

diff --git a/src/libANGLE/Buffer.h b/src/libANGLE/Buffer.h
index 2132159..853ec51 100644
--- a/src/libANGLE/Buffer.h
+++ b/src/libANGLE/Buffer.h
@@ -183,6 +183,8 @@
     GLboolean isImmutable() const { return mState.mImmutable; }
     GLbitfield getStorageExtUsageFlags() const { return mState.mStorageExtUsageFlags; }
 
+    WebGLBufferType getWebGLType() const { return mState.mWebGLType; }
+
     // Buffers are always initialized immediately when allocated
     InitState initState() const { return InitState::Initialized; }
 
diff --git a/src/libANGLE/ErrorStrings.h b/src/libANGLE/ErrorStrings.h
index 136d6b0..1ed3394 100644
--- a/src/libANGLE/ErrorStrings.h
+++ b/src/libANGLE/ErrorStrings.h
@@ -688,6 +688,7 @@
 inline constexpr const char *kTextureCompressionASTCDecodeModeExtensionRequired = "GL_EXT_texture_compression_astc_decode_mode not enabled.";
 inline constexpr const char *kTextureCompressionASTCDecodeModeRGB9E5ExtensionRequired = "GL_EXT_texture_compression_astc_decode_mode_rgb9e5 not enabled.";
 inline constexpr const char *kProgramNotValid = "Program is not a program object.";
+inline constexpr const char *kWebGLBufferTypeMismatch = "Invalid operation between WebGL buffer types.";
 // clang-format on
 
 }  // namespace err
diff --git a/src/libANGLE/validationES.cpp b/src/libANGLE/validationES.cpp
index c919e77..6018028 100644
--- a/src/libANGLE/validationES.cpp
+++ b/src/libANGLE/validationES.cpp
@@ -8543,4 +8543,56 @@
             return false;
     }
 }
+
+bool ValidateWebGLBufferBinding(const Context *context,
+                                angle::EntryPoint entryPoint,
+                                BufferBinding target,
+                                BufferID bufferId)
+{
+    ASSERT(context->isWebGL());
+
+    WebGLBufferType bufferType = WebGLBufferType::Undefined;
+    if (Buffer *buffer = context->getBuffer(bufferId))
+    {
+        bufferType = buffer->getWebGLType();
+    }
+
+    switch (bufferType)
+    {
+        case WebGLBufferType::Undefined:
+            // Valid. A buffer that has not been bound yet can be bound to any valid binding point
+            break;
+
+        case WebGLBufferType::ElementArray:
+        {
+            // Once a buffer has been bound to ELEMENT_ARRAY_BUFFER, it can only be bound to
+            // ELEMENT_ARRAY_BUFFER and COPY_READ/WRITE_BUFFER bindings.
+            constexpr angle::PackedEnumBitSet<BufferBinding> kValidElementArrayBufferBindingTargets(
+                {
+                    BufferBinding::ElementArray,
+                    BufferBinding::CopyRead,
+                    BufferBinding::CopyWrite,
+                });
+
+            if (!kValidElementArrayBufferBindingTargets.test(target))
+            {
+                ANGLE_VALIDATION_ERROR(GL_INVALID_OPERATION, err::kWebGLBufferTypeMismatch);
+                return false;
+            }
+        }
+        break;
+
+        case WebGLBufferType::OtherData:
+            // After being bound to non ELEMENT_ARRAY_BUFFER target, a buffer cannot be bound to
+            // ELEMENT_ARRAY_BUFFER target.
+            if (target == BufferBinding::ElementArray)
+            {
+                ANGLE_VALIDATION_ERROR(GL_INVALID_OPERATION, err::kWebGLBufferTypeMismatch);
+                return false;
+            }
+            break;
+    }
+
+    return true;
+}
 }  // namespace gl
diff --git a/src/libANGLE/validationES.h b/src/libANGLE/validationES.h
index 8b9fbcf..155d5f5 100644
--- a/src/libANGLE/validationES.h
+++ b/src/libANGLE/validationES.h
@@ -105,6 +105,11 @@
                                       const void *ptr,
                                       bool pureInteger);
 
+bool ValidateWebGLBufferBinding(const Context *context,
+                                angle::EntryPoint entryPoint,
+                                BufferBinding target,
+                                BufferID buffer);
+
 // Validation of transform feedback buffer output size for various DrawArrays calls.
 // `primcounts` can be null for non-instanced calls.
 // If this function returns false, an error has been generated.
diff --git a/src/libANGLE/validationES2.h b/src/libANGLE/validationES2.h
index 6aec92e..5d7782f 100644
--- a/src/libANGLE/validationES2.h
+++ b/src/libANGLE/validationES2.h
@@ -549,6 +549,12 @@
         return false;
     }
 
+    if (context->isWebGL() && !ValidateWebGLBufferBinding(context, entryPoint, target, buffer))
+    {
+        // Error already generated
+        return false;
+    }
+
     return true;
 }
 
diff --git a/src/libANGLE/validationES3.cpp b/src/libANGLE/validationES3.cpp
index 2c2bb25..bf41582 100644
--- a/src/libANGLE/validationES3.cpp
+++ b/src/libANGLE/validationES3.cpp
@@ -2012,6 +2012,12 @@
         return false;
     }
 
+    if (context->isWebGL() && !ValidateWebGLBufferBinding(context, entryPoint, target, buffer))
+    {
+        // Error already generated
+        return false;
+    }
+
     const Caps &caps = context->getCaps();
     switch (target)
     {
@@ -3280,6 +3286,17 @@
         }
     }
 
+    // WebGL2 spec:
+    // 6.2 Copying Buffers
+    // Attempting to use copyBufferSubData to copy between buffers that have element array and other
+    // data WebGL buffer types as specified in section Buffer Object Binding generates an
+    // INVALID_OPERATION error and no copying is performed.
+    if (context->isWebGL() && readBuffer->getWebGLType() != writeBuffer->getWebGLType())
+    {
+        ANGLE_VALIDATION_ERROR(GL_INVALID_OPERATION, err::kWebGLBufferTypeMismatch);
+        return false;
+    }
+
     return true;
 }
 
diff --git a/src/tests/gl_tests/StateChangeTest.cpp b/src/tests/gl_tests/StateChangeTest.cpp
index 37e710f..669c715 100644
--- a/src/tests/gl_tests/StateChangeTest.cpp
+++ b/src/tests/gl_tests/StateChangeTest.cpp
@@ -6644,14 +6644,14 @@
     glDrawArrays(GL_TRIANGLES, 0, 6);
 
     // Bind transform feedback buffer to another binding point. Should cause a conflict.
-    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, transformFeedbackBuffer);
+    glBindBuffer(GL_UNIFORM_BUFFER, transformFeedbackBuffer);
     ASSERT_GL_NO_ERROR();
     glDrawArrays(GL_TRIANGLES, 0, 6);
     glEndTransformFeedback();
     EXPECT_GL_ERROR(GL_INVALID_OPERATION) << "Simultaneous element buffer binding should fail";
 
     // Reset to valid state.
-    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
+    glBindBuffer(GL_UNIFORM_BUFFER, 0);
     glBeginTransformFeedback(GL_TRIANGLES);
     glDrawArrays(GL_TRIANGLES, 0, 6);
     glEndTransformFeedback();
diff --git a/src/tests/gl_tests/WebGLCompatibilityTest.cpp b/src/tests/gl_tests/WebGLCompatibilityTest.cpp
index 8b13fd0..d45f65a 100644
--- a/src/tests/gl_tests/WebGLCompatibilityTest.cpp
+++ b/src/tests/gl_tests/WebGLCompatibilityTest.cpp
@@ -1831,7 +1831,7 @@
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/src/tests/gl_tests/StateChangeTest.cpp b/src/tests/gl_tests/StateChangeTest.cpp
index 37e710f..669c715 100644
--- a/src/tests/gl_tests/StateChangeTest.cpp
+++ b/src/tests/gl_tests/StateChangeTest.cpp
@@ -6644,14 +6644,14 @@
     glDrawArrays(GL_TRIANGLES, 0, 6);
 
     // Bind transform feedback buffer to another binding point. Should cause a conflict.
-    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, transformFeedbackBuffer);
+    glBindBuffer(GL_UNIFORM_BUFFER, transformFeedbackBuffer);
     ASSERT_GL_NO_ERROR();
     glDrawArrays(GL_TRIANGLES, 0, 6);
     glEndTransformFeedback();
     EXPECT_GL_ERROR(GL_INVALID_OPERATION) << "Simultaneous element buffer binding should fail";
 
     // Reset to valid state.
-    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
+    glBindBuffer(GL_UNIFORM_BUFFER, 0);
     glBeginTransformFeedback(GL_TRIANGLES);
     glDrawArrays(GL_TRIANGLES, 0, 6);
     glEndTransformFeedback();
diff --git a/src/tests/gl_tests/WebGLCompatibilityTest.cpp b/src/tests/gl_tests/WebGLCompatibilityTest.cpp
index 8b13fd0..d45f65a 100644
--- a/src/tests/gl_tests/WebGLCompatibilityTest.cpp
+++ b/src/tests/gl_tests/WebGLCompatibilityTest.cpp
@@ -1831,7 +1831,7 @@
     };
 
     GLBuffer indexBuffer;
-    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vertexBuffer);
+    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexBuffer);
     glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(kIndexData), kIndexData, GL_DYNAMIC_DRAW);
 
     EXPECT_GL_NO_ERROR();
@@ -4229,6 +4229,90 @@
     EXPECT_GL_ERROR(GL_INVALID_ENUM);
 }
 
+// Test the WebGL buffer binding rules. Index buffers cannot be bound to GPU writeable bindings and
+// vice versa
+TEST_P(WebGLCompatibilityTest, BufferBindingTypeRules)
+{
+    {
+        GLBuffer buffer;
+        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, buffer);
+        EXPECT_GL_NO_ERROR();
+
+        glBindBuffer(GL_ARRAY_BUFFER, buffer);
+        EXPECT_GL_ERROR(GL_INVALID_OPERATION);
+
+        if (getClientMajorVersion() > 2)
+        {
+            glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, buffer);
+            EXPECT_GL_ERROR(GL_INVALID_OPERATION);
+
+            glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 1, buffer);
+            EXPECT_GL_ERROR(GL_INVALID_OPERATION);
+
+            glBindBuffer(GL_UNIFORM_BUFFER, buffer);
+            EXPECT_GL_ERROR(GL_INVALID_OPERATION);
+
+            // CopyRead and CopyWrite are allowed
+            glBindBuffer(GL_COPY_READ_BUFFER, buffer);
+            EXPECT_GL_NO_ERROR();
+
+            glBindBuffer(GL_COPY_WRITE_BUFFER, buffer);
+            EXPECT_GL_NO_ERROR();
+        }
+    }
+
+    {
+        GLBuffer buffer;
+        glBindBuffer(GL_ARRAY_BUFFER, buffer);
+        EXPECT_GL_NO_ERROR();
+
+        if (getClientMajorVersion() > 2)
+        {
+            // Other buffer types can be bound freely
+            glBindBuffer(GL_UNIFORM_BUFFER, buffer);
+            EXPECT_GL_NO_ERROR();
+
+            glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 1, buffer);
+            EXPECT_GL_NO_ERROR();
+        }
+
+        // ... except to element array buffer bindings
+        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, buffer);
+        EXPECT_GL_ERROR(GL_INVALID_OPERATION);
+    }
+}
+
+// Cannot copy between buffers of different WebGL types
+TEST_P(WebGL2CompatibilityTest, CopyBufferSubDataBufferTypeRules)
+{
+    GLBuffer elementArrayBuffer;
+    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, elementArrayBuffer);
+    glBufferData(GL_ELEMENT_ARRAY_BUFFER, 128, nullptr, GL_STATIC_DRAW);
+
+    GLBuffer arrayBuffer;
+    glBindBuffer(GL_ARRAY_BUFFER, arrayBuffer);
+    glBufferData(GL_ARRAY_BUFFER, 128, nullptr, GL_STATIC_DRAW);
+
+    GLBuffer uniformBuffer;
+    glBindBuffer(GL_UNIFORM_BUFFER, uniformBuffer);
+    glBufferData(GL_UNIFORM_BUFFER, 128, nullptr, GL_STATIC_DRAW);
+
+    glCopyBufferSubData(GL_ELEMENT_ARRAY_BUFFER, GL_ARRAY_BUFFER, 0, 0, 128);
+    EXPECT_GL_ERROR(GL_INVALID_OPERATION);
+
+    glCopyBufferSubData(GL_ARRAY_BUFFER, GL_ELEMENT_ARRAY_BUFFER, 0, 0, 128);
+    EXPECT_GL_ERROR(GL_INVALID_OPERATION);
+
+    glCopyBufferSubData(GL_ELEMENT_ARRAY_BUFFER, GL_UNIFORM_BUFFER, 0, 0, 128);
+    EXPECT_GL_ERROR(GL_INVALID_OPERATION);
+
+    glCopyBufferSubData(GL_UNIFORM_BUFFER, GL_ELEMENT_ARRAY_BUFFER, 0, 0, 128);
+    EXPECT_GL_ERROR(GL_INVALID_OPERATION);
+
+    glCopyBufferSubData(GL_UNIFORM_BUFFER, GL_ARRAY_BUFFER, 0, 0, 128);
+    EXPECT_GL_NO_ERROR();
+}
+
 // Verify framebuffer attachments return expected types when in an inconsistant state.
 TEST_P(WebGLCompatibilityTest, FramebufferAttachmentConsistancy)
 {
Loading diff…

Original Bug Report

reported by [email protected]

Potential stale shadow copy in ANGLE GL backend allows bypass of index validation and OOB GPU read

Project Fortify, 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 security team.

Overview: A logic flaw in ANGLE’s GL backend allows a compromised renderer to bypass WebGL index validation. When a buffer is modified on the GPU via Transform Feedback, ANGLE fails to invalidate the CPU-side shadow copy used for validation. This allows a compromised renderer to trick ANGLE’s software validation, leading to out-of-bounds VRAM reads by the native driver.

Affected files:

  • third_party/angle/src/libANGLE/renderer/gl/BufferGL.cpp
  • third_party/angle/src/libANGLE/renderer/gl/BufferGL.h
  • third_party/angle/src/libANGLE/Buffer.cpp

Estimated timestamp from git blame: 2026-01-06

Description

In the ANGLE GL backend, when running on a native driver that does not support robust buffer access (common on many Linux, ChromeOS, and Android configurations), ANGLE emulates this security boundary by maintaining a CPU-side shadow copy of element array buffers (mShadowCopy in BufferGL). This shadow copy is used to validate that indices used in draw calls are within the bounds of the associated vertex buffers.

A comment in BufferGL.cpp states: > “WebGL element array buffers cannot be bound to other binding points or written to on the GPU so the shadowed data will never be invalidated.”

However, this invariant is not strictly enforced in the GPU process when using the passthrough command decoder. A compromised renderer can bypass Blink’s high-level target compatibility checks and send IPC messages to the passthrough decoder, which forwards them directly to ANGLE. Because ANGLE’s ValidateBindBufferCommon does not enforce the WebGL “initial target” rule, an attacker can freely bind an ElementArray buffer to a GL_TRANSFORM_FEEDBACK_BUFFER target and write to it on the GPU.

When a buffer is written to via Transform Feedback, TransformFeedback::onVerticesDrawn notifies the buffer via Buffer::onDataChanged. This clears the index cache, but because the OpenGL backend (BufferGL) does not override onDataChanged(), the mShadowCopy remains entirely untouched and becomes permanently stale.

Potential Exploitation Steps

Our tooling agent cannot run code, but the following sequence illustrates how this could potentially be exploited from a compromised renderer process:

  1. Establish Channel: A compromised renderer establishes a WebGL context via the passthrough command decoder.
  2. Create Shadow Copy: The renderer creates a buffer, binds it to GL_ELEMENT_ARRAY_BUFFER (locking its ANGLE type to ElementArray), and initializes it with zeros via glBufferData. Because the native driver lacks robust access, ANGLE allocates a CPU-side mShadowCopy filled with zeros.
  3. GPU Write (Bypassing Checks): The renderer unbinds the buffer and binds it to GL_TRANSFORM_FEEDBACK_BUFFER. The renderer executes a Transform Feedback draw call using a shader that writes massive uint32 indices (e.g., 0x3FFFFFFF) directly into the buffer in VRAM.
  4. Stale State Achieved: After the draw call, BufferGL fails to update or clear its mShadowCopy, leaving it containing the original zeros, totally out-of-sync with VRAM.
  5. Trigger OOB Read: The renderer binds the buffer back to GL_ELEMENT_ARRAY_BUFFER and binds a very small 1-vertex buffer. It issues a glDrawElements call.
  6. Validation Bypass: ANGLE’s index validation (BufferGL::getIndexRange) iterates over the stale CPU-side mShadowCopy containing zeros. It calculates the maximum index as 0, successfully passing bounds validation against the 1-vertex buffer.
  7. Exfiltration: The native OpenGL driver executes the draw call using the actual VRAM buffer. The GPU fetches vertices using the massive 0x3FFFFFFF indices, reading far out of bounds into other VRAM regions. This leaked data can be passed to a fragment shader, written to a render target, and read back via glReadPixels.

Impact

This vulnerability potentially allows a compromised renderer to read arbitrary data from GPU memory (VRAM), which may include data from other origins, tabs, or desktop applications.

Suggested Fix

BufferGL should override BufferImpl::onDataChanged(). When called, if mShadowCopy is present, it should be invalidated or re-synchronized from the GPU to ensure subsequent validations use up-to-date data. Additionally, consider explicitly blocking sequential binding of ElementArray buffers to Transform Feedback targets within ANGLE’s validation layer to harden defense-in-depth.

Evaluated with Chrome root at commit: 137d451a126685dd5010e6609db9f6d4a78d8234


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