High chrome Logic Error ⚠️ Exploited in the wild 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
CVSS
Yes
Exploited ITW
Fixed
Fix Status
ImpactIncorrect validation of untrusted input in ANGLE and GPU
DescriptionIncorrect validation of untrusted input in ANGLE and GPU
ComponentANGLE and GPU
Bug ClassLogic Error
Tracker427162086
Fix commit2f8193ecfe1e (angle/angle) +77/-6
CISA KEVNot listed
CreditedClément Lecigne and Vlad Stolyarov of Google's Threat Analysis Group
Disclosed2025-07-15

Background

Transform feedback
A WebGL/OpenGL ES feature that captures vertex-shader outputs into buffer objects bound to indexed GL_TRANSFORM_FEEDBACK_BUFFER binding points during rendering.
`ValidateBufferData` / `ValidateBufferSubData`
ANGLE’s WebGL entry-point validators for glBufferData and glBufferSubData that must reject illegal writes before the GPU command is issued.
`hasWebGLXFBBindingConflict`
A Buffer helper that reports whether a buffer is simultaneously bound as a transform-feedback target and to another incompatible target, but not whether it is the target of the active transform feedback.
`INVALID_OPERATION`
The GL error that ANGLE must generate when an application tries to write to a buffer currently locked by active transform feedback.

Root Cause Analysis

When transform feedback was active, the GPU was concurrently streaming vertex outputs into the bound GL_TRANSFORM_FEEDBACK_BUFFER targets, so the ES spec makes any CPU-side write to those buffers undefined and recommends raising INVALID_OPERATION. The pre-patch ValidateBufferData and ValidateBufferSubData in validationES2.cpp only called buffer->hasWebGLXFBBindingConflict(context->isWebGL()), which detects a buffer aliased across conflicting bind points but does not detect that the buffer is bound to the currently active, non-paused transform-feedback object. That gap left the invariant “a buffer being written by active transform feedback must not be modified via glBufferData/glBufferSubData” unenforced, so such calls passed validation and reached the backend where a glBufferData reallocation or glBufferSubData write could race the in-flight transform-feedback output.

The fix adds TransformFeedback::isBufferBound and, inside a WebGL-only block, queries getCurrentTransformFeedback() and rejects the write with INVALID_OPERATION when the object is active, not paused, and has the target buffer bound. This closes the path by validating the actual active-transform-feedback state rather than only the coarse binding-conflict condition.

Key insight
The single mistake was validating only a generic binding conflict (hasWebGLXFBBindingConflict) instead of checking whether the buffer was bound to the active, non-paused transform-feedback object; the fix adds an explicit isBufferBound check against getCurrentTransformFeedback() so writes to a buffer under active capture are rejected with INVALID_OPERATION.

Attack Path

  1. Set up transform feedback A malicious WebGL2 page links a program with transform-feedback varyings and binds a buffer to GL_TRANSFORM_FEEDBACK_BUFFER via glBindBufferRange.
  2. Begin capture The page calls glBeginTransformFeedback so the GPU begins streaming vertex outputs into that buffer, marking the transform-feedback object active and not paused.
  3. Issue a racing write While capture is active, the page calls glBufferData (reallocating the buffer) or glBufferSubData (overwriting its contents) on the same buffer.
  4. Bypass validation Because hasWebGLXFBBindingConflict alone does not flag the active-capture case, ValidateBufferData / ValidateBufferSubData accept the call and forward it to the backend.
  5. Provoke undefined behavior The CPU-side buffer modification races the in-flight transform-feedback output, driving the GPU/driver into the spec’s undefined write-to-captured-buffer condition.

Impact Assessment

An attacker gains the ability, from untrusted WebGL2 content, to perform a buffer write that ANGLE’s spec requires to be illegal while the GPU is actively writing the same buffer, yielding undefined GPU/driver behavior in the GPU process. The precondition is a WebGL2 context with active, non-paused transform feedback and a scriptable glBufferData/glBufferSubData call targeting the captured buffer. This is a GPU-process logic/validation flaw exploitable by any web page that can run WebGL2, consistent with the high severity and “incorrect validation of untrusted input” classification.

Files Changed

  • src/libANGLE/TransformFeedback.cpp
  • src/libANGLE/TransformFeedback.h
  • src/libANGLE/validationES2.cpp
  • src/tests/gl_tests/WebGLCompatibilityTest.cpp

Audit Directions

  • Coarse-vs-specific state checks
    Audit other validators that rely on a generic binding/conflict helper such as hasWebGLXFBBindingConflict and confirm they also verify the actual active object state (isActive, isPaused) rather than assuming the coarse check subsumes it.
  • Active-resource write protection
    Review all buffer/resource mutation entry points (glBufferData, glBufferSubData, mapping, and similar) for cases where a resource is concurrently in use by the GPU (transform feedback, in-flight draws) and ensure a per-object bound check gates the write.
  • WebGL-only guard placement
    Verify that spec-mandated WebGL validation is inside the context->isWebGL() branch and consistently duplicated across sibling validators, so one path is not hardened while another remains permissive.
From 2f8193ecfe1ed464374ae56235cfdc112343f9c3 Mon Sep 17 00:00:00 2001
From: Geoff Lang <geofflang@chromium.org>
Date: Wed, 25 Jun 2025 13:17:47 -0400
Subject: [PATCH] Validate buffers bound for transform feedback are not modified.

The ES spec says it is undefined to write to a buffer that is currently
being used for transform feedback output but recommends generating an
error. Generate INVALID_OPERATION in this case.

Bug: chromium:427162086
Change-Id: I727d18c2035509fe2e5d60680eb5198e40a60e33
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/6673310
Commit-Queue: Geoff Lang <geofflang@chromium.org>
Reviewed-by: Vasiliy Telezhnikov <vasilyt@chromium.org>
---

diff --git a/src/libANGLE/TransformFeedback.cpp b/src/libANGLE/TransformFeedback.cpp
index e205a10..c408047 100644
--- a/src/libANGLE/TransformFeedback.cpp
+++ b/src/libANGLE/TransformFeedback.cpp
@@ -324,6 +324,18 @@
     return false;
 }
 
+bool TransformFeedback::isBufferBound(BufferID bufferID) const
+{
+    for (const auto &buffer : mState.mIndexedBuffers)
+    {
+        if (buffer.id() == bufferID)
+        {
+            return true;
+        }
+    }
+    return false;
+}
+
 void TransformFeedback::onBindingChanged(const Context *context, bool bound)
 {
     for (auto &buffer : mState.mIndexedBuffers)
diff --git a/src/libANGLE/TransformFeedback.h b/src/libANGLE/TransformFeedback.h
index 5c9ffd0..0c32221 100644
--- a/src/libANGLE/TransformFeedback.h
+++ b/src/libANGLE/TransformFeedback.h
@@ -102,6 +102,10 @@
     // Returns true if any buffer bound to this object is also bound to another target.
     bool buffersBoundForOtherUseInWebGL() const;
 
+    // Returns true if the buffer is bound to any of the indexed binding points in this transform
+    // feedback.
+    bool isBufferBound(BufferID bufferID) const;
+
     angle::Result detachBuffer(const Context *context, BufferID bufferID);
 
     rx::TransformFeedbackImpl *getImplementation() const { return mImplementation; }
diff --git a/src/libANGLE/validationES2.cpp b/src/libANGLE/validationES2.cpp
index 95adc0c..c56bc93 100644
--- a/src/libANGLE/validationES2.cpp
+++ b/src/libANGLE/validationES2.cpp
@@ -3656,10 +3656,24 @@
         return false;
     }
 
-    if (buffer->hasWebGLXFBBindingConflict(context->isWebGL()))
+    // Do some additional WebGL-specific validation
+    if (ANGLE_UNLIKELY(context->isWebGL()))
     {
-        ANGLE_VALIDATION_ERROR(GL_INVALID_OPERATION, kBufferBoundForTransformFeedback);
-        return false;
+        if (buffer->hasWebGLXFBBindingConflict(true))
+        {
+            ANGLE_VALIDATION_ERROR(GL_INVALID_OPERATION, kBufferBoundForTransformFeedback);
+            return false;
+        }
+
+        const TransformFeedback *transformFeedbackObject =
+            context->getState().getCurrentTransformFeedback();
+        if (transformFeedbackObject && transformFeedbackObject->isActive() &&
+            !transformFeedbackObject->isPaused() &&
+            transformFeedbackObject->isBufferBound(buffer->id()))
+        {
+            ANGLE_VALIDATION_ERROR(GL_INVALID_OPERATION, kBufferBoundForTransformFeedback);
+            return false;
+        }
     }
 
     if (buffer->isImmutable())
@@ -3714,10 +3728,24 @@
         return false;
     }
 
-    if (buffer->hasWebGLXFBBindingConflict(context->isWebGL()))
+    // Do some additional WebGL-specific validation
+    if (ANGLE_UNLIKELY(context->isWebGL()))
     {
-        ANGLE_VALIDATION_ERROR(GL_INVALID_OPERATION, kBufferBoundForTransformFeedback);
-        return false;
+        if (buffer->hasWebGLXFBBindingConflict(true))
+        {
+            ANGLE_VALIDATION_ERROR(GL_INVALID_OPERATION, kBufferBoundForTransformFeedback);
+            return false;
+        }
+
+        const TransformFeedback *transformFeedbackObject =
+            context->getState().getCurrentTransformFeedback();
+        if (transformFeedbackObject && transformFeedbackObject->isActive() &&
+            !transformFeedbackObject->isPaused() &&
+            transformFeedbackObject->isBufferBound(buffer->id()))
+        {
+            ANGLE_VALIDATION_ERROR(GL_INVALID_OPERATION, kBufferBoundForTransformFeedback);
+            return false;
+        }
     }
 
     if (buffer->isImmutable() &&
diff --git a/src/tests/gl_tests/WebGLCompatibilityTest.cpp b/src/tests/gl_tests/WebGLCompatibilityTest.cpp
index 66c7061..8758ca4 100644
--- a/src/tests/gl_tests/WebGLCompatibilityTest.cpp
+++ b/src/tests/gl_tests/WebGLCompatibilityTest.cpp
@@ -5760,6 +5760,33 @@
     EXPECT_GL_ERROR(GL_INVALID_OPERATION);
 }
 
+// Writing to the contents of a currently active transform feedback buffer is invalid
+TEST_P(WebGL2CompatibilityTest, TransformFeedbackBufferModification)
+{
+    constexpr char kVS[] = R"(attribute float a; varying float b; void main() { b = a; })";
+    constexpr char kFS[] = R"(void main(){})";
+    ANGLE_GL_PROGRAM(program, kVS, kFS);
+    static const char *varyings[] = {"b"};
+    glTransformFeedbackVaryings(program, 1, varyings, GL_SEPARATE_ATTRIBS);
+    glLinkProgram(program);
+    glUseProgram(program);
+    ASSERT_GL_NO_ERROR();
+
+    // Bind the transform feedback varyings to non-overlapping regions of the same buffer.
+    GLBuffer buffer;
+    glBindBufferRange(GL_TRANSFORM_FEEDBACK_BUFFER, 0, buffer, 0, 4);
+    glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, 8, nullptr, GL_STATIC_DRAW);
+    glBeginTransformFeedback(GL_POINTS);
+    ASSERT_GL_NO_ERROR();
+
+    glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, 8, nullptr, GL_STATIC_DRAW);
+    EXPECT_GL_ERROR(GL_INVALID_OPERATION);
+
+    constexpr uint8_t data[8] = {0};
+    glBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0, 8, data);
+    EXPECT_GL_ERROR(GL_INVALID_OPERATION);
+}
+
 // Check the return type of a given parameter upon getting the active uniforms.
 TEST_P(WebGL2CompatibilityTest, UniformVariablesReturnTypes)
 {
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/src/tests/gl_tests/WebGLCompatibilityTest.cpp b/src/tests/gl_tests/WebGLCompatibilityTest.cpp
index 66c7061..8758ca4 100644
--- a/src/tests/gl_tests/WebGLCompatibilityTest.cpp
+++ b/src/tests/gl_tests/WebGLCompatibilityTest.cpp
@@ -5760,6 +5760,33 @@
     EXPECT_GL_ERROR(GL_INVALID_OPERATION);
 }
 
+// Writing to the contents of a currently active transform feedback buffer is invalid
+TEST_P(WebGL2CompatibilityTest, TransformFeedbackBufferModification)
+{
+    constexpr char kVS[] = R"(attribute float a; varying float b; void main() { b = a; })";
+    constexpr char kFS[] = R"(void main(){})";
+    ANGLE_GL_PROGRAM(program, kVS, kFS);
+    static const char *varyings[] = {"b"};
+    glTransformFeedbackVaryings(program, 1, varyings, GL_SEPARATE_ATTRIBS);
+    glLinkProgram(program);
+    glUseProgram(program);
+    ASSERT_GL_NO_ERROR();
+
+    // Bind the transform feedback varyings to non-overlapping regions of the same buffer.
+    GLBuffer buffer;
+    glBindBufferRange(GL_TRANSFORM_FEEDBACK_BUFFER, 0, buffer, 0, 4);
+    glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, 8, nullptr, GL_STATIC_DRAW);
+    glBeginTransformFeedback(GL_POINTS);
+    ASSERT_GL_NO_ERROR();
+
+    glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, 8, nullptr, GL_STATIC_DRAW);
+    EXPECT_GL_ERROR(GL_INVALID_OPERATION);
+
+    constexpr uint8_t data[8] = {0};
+    glBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0, 8, data);
+    EXPECT_GL_ERROR(GL_INVALID_OPERATION);
+}
+
 // Check the return type of a given parameter upon getting the active uniforms.
 TEST_P(WebGL2CompatibilityTest, UniformVariablesReturnTypes)
 {
Loading diff…

Original Bug Report

reported by he...@google.com

Chrome sandbox escape via libGLES_mali.so exploited in the wild

NOTE: This bug is similar to b/402078335 but is targeting Mali devices. NOTE: We have evidence that the following bug is being used in the wild.

VULNERABILITY DETAILS

Google Threat Intelligence Group discovered an in the wild Chrome sandbox escape targeting Android devices with Mali chipsets. The vulnerability appears to be in libGLES_mali.so. We haven’t yet concluded a root cause analysis, but our testing hints at a a possible type confusion caused by updating the texture type and format in glTexImage2D(..GL_RGB, GL_UNSIGNED_BYTE..) -> glTexStorage2D(..GL_RGB9_E5..) -> glTexSubImage2D(..GL_RGB, GL_HALF_FLOAT..) leading to an OOB write in gles_context*.

We were able to reproduce the crash on a Pixel 7 (Mali-G710 MP7) with latest updates. (mostly due to our current poc only supporting 32-bit devices)

We have just reported this vulnerability to ARM, but based on our experience it could take weeks, or even months, before patches roll out to Android devices - which is why we would also like to report this vulnerability also to Chrome security, in order to see if there is anything we can do in the short term on the Chrome side to block the exploit.

The sandbox escape was chained with an exploit against the Chrome renderer (CVE-2025-5419) and a privilege escalation in Sigqueue (https://kernel.dance/#f90fff1e152dedf52b932240ebbd670d83330eca).

CREDIT

Clément Lecigne and Vlad Stolyarov of Google’s Threat Analysis Group.

VERSION

Chrome Version: latest (since the vuln is targeting a Mali library)
Operating System: Android on Mali chipsets

REPRODUCTION CASE

  • Compile poc.c (attached) with: $NDK/toolchains/llvm/prebuilt/linux-x86_64/bin/clang poc.c -lGLESv3 -lGLESv2 -lEGL -lm --target=armv7a-linux-android34 -g -O0 -o poc_32
    • Exploit discovered in-the-wild was targeting 32-bit Chrome, but the vulnerability is likely present in 64-bit Chrome as well.
  • Run with adb push ./poc_32 /data/local/tmp && adb shell /data/local/tmp/poc_32
  • Observe crash in the logcat output.

FOR CRASHES, PLEASE INCLUDE THE FOLLOWING ADDITIONAL INFORMATION

Type of crash: browser Crash State:

06-23 11:38:42.751 10308 10308 F libc    : Fatal signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x31336 in tid 10308 (poc_32), pid 10308 (poc_32)
06-23 11:38:42.904 10317 10317 F DEBUG   : *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
06-23 11:38:42.904 10317 10317 F DEBUG   : Build fingerprint: 'google/panther/panther:16/BP2A.250605.031.A2/13578606:user/release-keys'
06-23 11:38:42.904 10317 10317 F DEBUG   : Revision: 'MP1.0'
06-23 11:38:42.904 10317 10317 F DEBUG   : ABI: 'arm'
06-23 11:38:42.904 10317 10317 F DEBUG   : Timestamp: 2025-06-23 11:38:42.802837105+0200
06-23 11:38:42.904 10317 10317 F DEBUG   : Process uptime: 1s
06-23 11:38:42.904 10317 10317 F DEBUG   : Cmdline: /data/local/tmp/poc_32
06-23 11:38:42.904 10317 10317 F DEBUG   : pid: 10308, tid: 10308, name: poc_32  >>> /data/local/tmp/poc_32 <<<
06-23 11:38:42.904 10317 10317 F DEBUG   : uid: 2000
06-23 11:38:42.904 10317 10317 F DEBUG   : signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x00031336
06-23 11:38:42.904 10317 10317 F DEBUG   :     r0  f3f41db0  r1  00000000  r2  00000000  r3  00031337
06-23 11:38:42.904 10317 10317 F DEBUG   :     r4  f331fc7c  r5  f775a2d0  r6  00000000  r7  f775a2dc
06-23 11:38:42.904 10317 10317 F DEBUG   :     r8  f331fbd8  r9  00000000  r10 f3314290  r11 00000001
06-23 11:38:42.904 10317 10317 F DEBUG   :     ip  00000001  sp  ff92b1b0  lr  f48cd5af  pc  00031336
06-23 11:38:42.904 10317 10317 F DEBUG   : 5 total frames
06-23 11:38:42.904 10317 10317 F DEBUG   : backtrace:
06-23 11:38:42.904 10317 10317 F DEBUG   :   NOTE: Function names and BuildId information is missing for some frames due
06-23 11:38:42.904 10317 10317 F DEBUG   :   NOTE: to unreadable libraries. For unwinds of apps, only shared libraries
06-23 11:38:42.904 10317 10317 F DEBUG   :   NOTE: found under the lib/ directory are readable.
06-23 11:38:42.904 10317 10317 F DEBUG   :   NOTE: On this device, run setenforce 0 to make the libraries readable.
06-23 11:38:42.904 10317 10317 F DEBUG   :   NOTE: Unreadable libraries:
06-23 11:38:42.904 10317 10317 F DEBUG   :   NOTE:   /data/local/tmp/poc_32
06-23 11:38:42.904 10317 10317 F DEBUG   :       #00 pc 00031336  <unknown>
06-23 11:38:42.904 10317 10317 F DEBUG   :       #01 pc 006185ad  /vendor/lib/egl/libGLES_mali.so (gles_drawp_handle_dependencies(gles_context*, gles_draw_call*, glescore_submission*)+196) (BuildId: da7af0632b11c153bc5a10aa30fdc8a1314007c5)
06-23 11:38:42.904 10317 10317 F DEBUG   :       #02 pc 00619a6b  /vendor/lib/egl/libGLES_mali.so (gles_drawp_draw_common+794) (BuildId: da7af0632b11c153bc5a10aa30fdc8a1314007c5)
06-23 11:38:42.904 10317 10317 F DEBUG   :       #03 pc 005d1373  /vendor/lib/egl/libGLES_mali.so (gles_draw_draw_arrays+32) (BuildId: da7af0632b11c153bc5a10aa30fdc8a1314007c5)
06-23 11:38:42.904 10317 10317 F DEBUG   :       #04 pc 000025e4  /data/local/tmp/poc_32
View on issue tracker