Overview

Critical
Severity
9.6
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in WebGL
DescriptionUse after free in WebGL
ComponentWebGL
Bug ClassUAF
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable — revised by independent review by AI
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Independent review by AIPlausible (tier revised from patch-now)
Reviewer notes: 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.
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker546260492
Fix commitf4cb4a33442a (chromium/src) +60/-2
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-08-14 (per vendor advisory)
Fix landed2026-08-24 (public git)
Disclosed2026-09-01
Patch gap8 days — fix public in source before the advisory

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 additionally paused().
Buffer respecification
Calling glBufferData on an existing buffer reallocates (frees and re-creates) its underlying storage, invalidating whatever the driver previously tracked.
Validating command decoder
The GPU-process component (BufferManager in buffer_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.
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

  1. Set up transform feedback Create a transform-feedback object, bind a buffer to it, and begin transform feedback so the object becomes active().
  2. Pause it Pause the transform feedback so the object is active() and paused(), leaving the driver still tracking the bound buffer’s storage.
  3. Respecify the buffer Call glBufferData on the still-bound buffer; the pre-fix !paused() exemption lets it pass, freeing and reallocating the storage.
  4. 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

FunctionChangeNotes
TEST_P
gpu/command_buffer/service/gles2_cmd_decoder_unittest_buffers.cc
modified

Files Changed

  • gpu/command_buffer/service/buffer_manager.cc
  • gpu/command_buffer/service/gles2_cmd_decoder_unittest_buffers.cc

Audit Directions

  • Pause/resume as a bypass of "active" guards
    Audit 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 resources
    Review 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 divergence
    Flag 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.
From f4cb4a33442a3b89acdb1d03f000970bd77514f6 Mon Sep 17 00:00:00 2001
From: Brandon Jones <bajones@chromium.org>
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 <kbr@chromium.org>
Commit-Queue: Brandon Jones <bajones@chromium.org>
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 xi...@google.com

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

View on issue tracker