CVE-2025-6558
Overview
Background
- Transform feedback
- A WebGL/OpenGL ES feature that captures vertex-shader outputs into buffer objects bound to indexed
GL_TRANSFORM_FEEDBACK_BUFFERbinding points during rendering. - `ValidateBufferData` / `ValidateBufferSubData`
- ANGLE’s WebGL entry-point validators for
glBufferDataandglBufferSubDatathat must reject illegal writes before the GPU command is issued. - `hasWebGLXFBBindingConflict`
- A
Bufferhelper 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.
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
- Set up transform feedback
A malicious WebGL2 page links a program with transform-feedback varyings and binds a buffer to
GL_TRANSFORM_FEEDBACK_BUFFERviaglBindBufferRange. - Begin capture
The page calls
glBeginTransformFeedbackso the GPU begins streaming vertex outputs into that buffer, marking the transform-feedback object active and not paused. - Issue a racing write
While capture is active, the page calls
glBufferData(reallocating the buffer) orglBufferSubData(overwriting its contents) on the same buffer. - Bypass validation
Because
hasWebGLXFBBindingConflictalone does not flag the active-capture case,ValidateBufferData/ValidateBufferSubDataaccept the call and forward it to the backend. - 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
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.cppsrc/libANGLE/TransformFeedback.hsrc/libANGLE/validationES2.cppsrc/tests/gl_tests/WebGLCompatibilityTest.cpp
Audit Directions
- Coarse-vs-specific state checksAudit other validators that rely on a generic binding/conflict helper such as
hasWebGLXFBBindingConflictand confirm they also verify the actual active object state (isActive,isPaused) rather than assuming the coarse check subsumes it. - Active-resource write protectionReview 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 placementVerify 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.
Patch
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)
{
Regression Test / PoC
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)
{
Original Bug Report
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