CVE-2026-11113
Overview
Files Changed
src/libANGLE/Buffer.hsrc/libANGLE/ErrorStrings.hsrc/libANGLE/validationES.cppsrc/libANGLE/validationES.hsrc/libANGLE/validationES2.hsrc/libANGLE/validationES3.cppsrc/tests/gl_tests/StateChangeTest.cppsrc/tests/gl_tests/WebGLCompatibilityTest.cpp
Patch
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 @@
Regression Test / PoC
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)
{
Original Bug Report
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.cppthird_party/angle/src/libANGLE/renderer/gl/BufferGL.hthird_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:
- Establish Channel: A compromised renderer establishes a WebGL context via the passthrough command decoder.
- Create Shadow Copy: The renderer creates a buffer, binds it to
GL_ELEMENT_ARRAY_BUFFER(locking its ANGLE type toElementArray), and initializes it with zeros viaglBufferData. Because the native driver lacks robust access, ANGLE allocates a CPU-sidemShadowCopyfilled with zeros. - 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 massiveuint32indices (e.g.,0x3FFFFFFF) directly into the buffer in VRAM. - Stale State Achieved: After the draw call,
BufferGLfails to update or clear itsmShadowCopy, leaving it containing the original zeros, totally out-of-sync with VRAM. - Trigger OOB Read: The renderer binds the buffer back to
GL_ELEMENT_ARRAY_BUFFERand binds a very small 1-vertex buffer. It issues aglDrawElementscall. - Validation Bypass: ANGLE’s index validation (
BufferGL::getIndexRange) iterates over the stale CPU-sidemShadowCopycontaining zeros. It calculates the maximum index as0, successfully passing bounds validation against the 1-vertex buffer. - Exfiltration: The native OpenGL driver executes the draw call using the actual VRAM buffer. The GPU fetches vertices using the massive
0x3FFFFFFFindices, 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 viaglReadPixels.
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.