Chrome · WebGL
CVE-2026-84352
UAF in WebGL
Critical
watch
~ plausible
CVSS 9.6
chrome
UAF
📄 Reporter bug report
🔧 Commit mapped
🧬 A cached GPU object survives a redefine or reallocation
Overview
Critical
Severity
9.6
CVSS
No
Exploited ITW
Fixed
Fix Status
Background
- Transform Feedback
- A GLES 3.0 feature that captures vertex-shader outputs into GPU buffer objects during a draw; a transform-feedback object can be
active()and additionallypaused(). - Buffer respecification
- Calling
glBufferDataon an existing buffer reallocates (frees and re-creates) its underlying storage, invalidating whatever the driver previously tracked. - Validating command decoder
- The GPU-process component (
BufferManagerinbuffer_manager.cc) that validates client GL calls before forwarding them to the driver/ANGLE. - UAF
- Use after free; here the driver reads or writes storage that was freed and reallocated out from under it by a respec.
Root Cause Analysis
Verify before building a PoC
This analysis is consistent with the fixing commit but not fully provable from it: part of the vulnerable path lives in code the fix commit does not touch. Confirm the trigger and the sink against the source before relying on it for exploit development.
What the commit does not establish: Commit limits fix to validating decoder (ANGLE/passthrough already blocks it) and a Mali driver bug; 'default config / ordinary WebGL2' overstated. Driver-frees-tracked-storage UAF mechanism stated as fact but not shown in diff.
What the commit does not establish: Commit limits fix to validating decoder (ANGLE/passthrough already blocks it) and a Mali driver bug; 'default config / ordinary WebGL2' overstated. Driver-frees-tracked-storage UAF mechanism stated as fact but not shown in diff.
The
glBufferData validator in BufferManager rejects respecifying a buffer that is bound for transform feedback, but the pre-fix guard only fired when the transform-feedback object was active() and !paused(). Because of the !paused() condition, a buffer bound to an ACTIVE-but-PAUSED transform-feedback object was incorrectly exempted and allowed to be respecified. Respecifying (glBufferData) frees and reallocates the buffer’s backing storage while the driver still holds and tracks the original storage for the paused transform-feedback output. When the transform feedback later resumes and the driver writes to (or reads from) that tracked storage, it touches the freed/reallocated allocation, producing a use-after-free. This condition is undefined behavior per ES 3.0 and is already blocked by ANGLE, and it additionally triggers a driver bug on Mali GPUs. Removing the !paused() clause makes the validator reject respec whenever transform feedback is merely active(), closing the pause window.Key insight
The pause state does not sever the driver’s ownership of the buffer’s storage, so exempting the paused case let a respec free memory the driver still tracked; “active” alone — not “active and not paused” — is the correct condition that forbids respecification.
Attack Path
- Set up transform feedback
Create a transform-feedback object, bind a buffer to it, and begin transform feedback so the object becomes
active(). - Pause it
Pause the transform feedback so the object is
active()andpaused(), leaving the driver still tracking the bound buffer’s storage. - Respecify the buffer
Call
glBufferDataon the still-bound buffer; the pre-fix!paused()exemption lets it pass, freeing and reallocating the storage. - Resume and draw Resume transform feedback and issue a draw so the driver writes captured vertex output into the stale (freed) storage, triggering the use-after-free.
Impact Assessment
A use-after-free in the GPU process reachable from WebGL, exploitable by a malicious page via ordinary WebGL2 transform-feedback calls. Because the freed storage can be reallocated for other data before the driver touches it, this can lead to memory corruption in the GPU process, with the Mali driver bug adding a further platform-specific failure mode.
Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Pgpu/command_buffer/service/gles2_cmd_decoder_unittest_buffers.cc |
modified |
Files Changed
gpu/command_buffer/service/buffer_manager.ccgpu/command_buffer/service/gles2_cmd_decoder_unittest_buffers.cc
Audit Directions
- Pause/resume as a bypass of "active" guardsAudit other GL state validators that exempt
paused()or otherwise treat a paused/suspended object as inactive; the driver may still own resources across pause, so pause should rarely relax a safety check. - Respecification vs. in-use resourcesReview every path that frees/reallocates GPU storage (
glBufferData, texture/renderbuffer respec, buffer detach) for objects still bound to transform feedback, framebuffers, or other consumers the driver tracks. - Spec/ANGLE divergenceFlag places where the validating decoder is more permissive than ES 3.0 undefined behavior or ANGLE already blocks, since those gaps can expose native driver bugs.
Patch
From f4cb4a33442a3b89acdb1d03f000970bd77514f6 Mon Sep 17 00:00:00 2001 From: Brandon Jones <[email protected]> Date: Mon, 24 Aug 2026 11:57:18 -0700 Subject: [PATCH] Prevent buffer respec during paused Transform Feedback Prevents buffer respec while an active transform feedback is paused when using the validating command decoder. This is undefined behavior according to ES 3.0 and already blocked by ANGLE. Avoids a driver bug on Mali GPUs. Fixed: 546260492 Change-Id: I5edebfcd3d76c41deea6370610001dbb41a2e9d4 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8281207 Reviewed-by: Kenneth Russell <[email protected]> Commit-Queue: Brandon Jones <[email protected]> Cr-Commit-Position: refs/heads/main@{#1684886} --- diff --git a/gpu/command_buffer/service/buffer_manager.cc b/gpu/command_buffer/service/buffer_manager.cc index 5b733c5..a2e80c5f 100644 --- a/gpu/command_buffer/service/buffer_manager.cc +++ b/gpu/command_buffer/service/buffer_manager.cc @@ -428,11 +428,10 @@ if (context_state->bound_transform_feedback && context_state->bound_transform_feedback->active() && - !context_state->bound_transform_feedback->paused() && buffer->IsBoundForTransformFeedback()) { ERRORSTATE_SET_GL_ERROR(error_state, GL_INVALID_OPERATION, "glBufferData", "buffer is bound for transform feedback that is " - "currently active and not paused"); + "currently active"); return; } diff --git a/gpu/command_buffer/service/gles2_cmd_decoder_unittest_buffers.cc b/gpu/command_buffer/service/gles2_cmd_decoder_unittest_buffers.cc index 8da31f35..07ae3167 100644 --- a/gpu/command_buffer/service/gles2_cmd_decoder_unittest_buffers.cc +++ b/gpu/command_buffer/service/gles2_cmd_decoder_unittest_buffers.cc @@ -127,5 +127,64 @@ } } +TEST_P(GLES3DecoderTest, BufferDataWhileTransformFeedbackPausedFails) { + const GLenum kTarget = GL_TRANSFORM_FEEDBACK_BUFFER; + const GLsizeiptr kBufferSize = 64; + + EXPECT_CALL(*gl_, BindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, 0)) + .Times(1) + .RetiresOnSaturation(); + DoBindTransformFeedback(GL_TRANSFORM_FEEDBACK, client_transformfeedback_id_, + kServiceTransformFeedbackId); + + DoBindBuffer(kTarget, client_buffer_id_, kServiceBufferId); + EXPECT_CALL(*gl_, BindBufferBase(kTarget, 0, kServiceBufferId)); + SpecializedSetup<cmds::BindBufferBase, 0>(true); + cmds::BindBufferBase bind_base_cmd; + bind_base_cmd.Init(kTarget, 0, client_buffer_id_); + EXPECT_EQ(error::kNoError, ExecuteCmd(bind_base_cmd)); + EXPECT_EQ(GL_NO_ERROR, GetGLError()); + + DoBufferData(kTarget, kBufferSize); + + TransformFeedback* tf = GetTransformFeedback(client_transformfeedback_id_); + ASSERT_TRUE(tf); + + EXPECT_CALL(*gl_, BeginTransformFeedback(GL_POINTS)).Times(1); + EXPECT_CALL(*gl_, PauseTransformFeedback()).Times(1); + tf->DoBeginTransformFeedback(GL_POINTS); + tf->DoPauseTransformFeedback(); + EXPECT_TRUE(tf->active()); + EXPECT_TRUE(tf->paused()); + + // BufferData (respecification) must fail with GL_INVALID_OPERATION even while + // paused. + cmds::BufferData buffer_data_cmd; + buffer_data_cmd.Init(kTarget, kBufferSize * 2, 0, 0, GL_STREAM_DRAW); + EXPECT_EQ(error::kNoError, ExecuteCmd(buffer_data_cmd)); + EXPECT_EQ(GL_INVALID_OPERATION, GetGLError()); + + // BufferSubData while paused is permitted by ES 3.0. + EXPECT_CALL(*gl_, BufferSubData(kTarget, 0, 4, _)).Times(1); + cmds::BufferSubData buffer_sub_data_cmd; + buffer_sub_data_cmd.Init(kTarget, 0, 4, shared_memory_id_, + shared_memory_offset_); + EXPECT_EQ(error::kNoError, ExecuteCmd(buffer_sub_data_cmd)); + EXPECT_EQ(GL_NO_ERROR, GetGLError()); + + // End transform feedback and ensure BufferData succeeds again. + EXPECT_CALL(*gl_, EndTransformFeedback()).Times(1); + tf->DoEndTransformFeedback(); + EXPECT_FALSE(tf->active()); + + EXPECT_CALL(*gl_, GetError()) + .WillOnce(Return(GL_NO_ERROR)) + .WillOnce(Return(GL_NO_ERROR)); + EXPECT_CALL(*gl_, BufferData(kTarget, kBufferSize * 2, _, GL_STREAM_DRAW)) + .Times(1); + EXPECT_EQ(error::kNoError, ExecuteCmd(buffer_data_cmd)); + EXPECT_EQ(GL_NO_ERROR, GetGLError()); +} + } // namespace gles2 } // namespace gpu
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/gpu/command_buffer/service/gles2_cmd_decoder_unittest_buffers.cc b/gpu/command_buffer/service/gles2_cmd_decoder_unittest_buffers.cc
index 8da31f35..07ae3167 100644
--- a/gpu/command_buffer/service/gles2_cmd_decoder_unittest_buffers.cc
+++ b/gpu/command_buffer/service/gles2_cmd_decoder_unittest_buffers.cc
@@ -127,5 +127,64 @@
}
}
+TEST_P(GLES3DecoderTest, BufferDataWhileTransformFeedbackPausedFails) {
+ const GLenum kTarget = GL_TRANSFORM_FEEDBACK_BUFFER;
+ const GLsizeiptr kBufferSize = 64;
+
+ EXPECT_CALL(*gl_, BindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, 0))
+ .Times(1)
+ .RetiresOnSaturation();
+ DoBindTransformFeedback(GL_TRANSFORM_FEEDBACK, client_transformfeedback_id_,
+ kServiceTransformFeedbackId);
+
+ DoBindBuffer(kTarget, client_buffer_id_, kServiceBufferId);
+ EXPECT_CALL(*gl_, BindBufferBase(kTarget, 0, kServiceBufferId));
+ SpecializedSetup<cmds::BindBufferBase, 0>(true);
+ cmds::BindBufferBase bind_base_cmd;
+ bind_base_cmd.Init(kTarget, 0, client_buffer_id_);
+ EXPECT_EQ(error::kNoError, ExecuteCmd(bind_base_cmd));
+ EXPECT_EQ(GL_NO_ERROR, GetGLError());
+
+ DoBufferData(kTarget, kBufferSize);
+
+ TransformFeedback* tf = GetTransformFeedback(client_transformfeedback_id_);
+ ASSERT_TRUE(tf);
+
+ EXPECT_CALL(*gl_, BeginTransformFeedback(GL_POINTS)).Times(1);
+ EXPECT_CALL(*gl_, PauseTransformFeedback()).Times(1);
+ tf->DoBeginTransformFeedback(GL_POINTS);
+ tf->DoPauseTransformFeedback();
+ EXPECT_TRUE(tf->active());
+ EXPECT_TRUE(tf->paused());
+
+ // BufferData (respecification) must fail with GL_INVALID_OPERATION even while
+ // paused.
+ cmds::BufferData buffer_data_cmd;
+ buffer_data_cmd.Init(kTarget, kBufferSize * 2, 0, 0, GL_STREAM_DRAW);
+ EXPECT_EQ(error::kNoError, ExecuteCmd(buffer_data_cmd));
+ EXPECT_EQ(GL_INVALID_OPERATION, GetGLError());
+
+ // BufferSubData while paused is permitted by ES 3.0.
+ EXPECT_CALL(*gl_, BufferSubData(kTarget, 0, 4, _)).Times(1);
+ cmds::BufferSubData buffer_sub_data_cmd;
+ buffer_sub_data_cmd.Init(kTarget, 0, 4, shared_memory_id_,
+ shared_memory_offset_);
+ EXPECT_EQ(error::kNoError, ExecuteCmd(buffer_sub_data_cmd));
+ EXPECT_EQ(GL_NO_ERROR, GetGLError());
+
+ // End transform feedback and ensure BufferData succeeds again.
+ EXPECT_CALL(*gl_, EndTransformFeedback()).Times(1);
+ tf->DoEndTransformFeedback();
+ EXPECT_FALSE(tf->active());
+
+ EXPECT_CALL(*gl_, GetError())
+ .WillOnce(Return(GL_NO_ERROR))
+ .WillOnce(Return(GL_NO_ERROR));
+ EXPECT_CALL(*gl_, BufferData(kTarget, kBufferSize * 2, _, GL_STREAM_DRAW))
+ .Times(1);
+ EXPECT_EQ(error::kNoError, ExecuteCmd(buffer_data_cmd));
+ EXPECT_EQ(GL_NO_ERROR, GetGLError());
+}
+
} // namespace gles2
} // namespace gpu
Loading diff…
Original Bug Report
reported by [email protected]
Mali: Begin-baked buffer instance CPU+GPU UAF via glBufferData respec while transform feedback paused
- Attack surface: WebGL2 transform feedback from an untrusted web page, Chrome for Android on Mali in the native-GLES configuration. 0
- Impact: use-after-free in the unsandboxed GPU process with a demonstrated arbitrary indirect-branch-target hijack, plus a multi-megabyte GPU write into the address space shared by all tabs' WebGL.
Android internal bug: b/545367411
References
On This Page