Overview

Critical
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in WebGL
DescriptionUse after free in WebGL
ComponentWebGL
Bug ClassUAF
Tracker523591974
Fix commitfeb103bc1c01 (chromium/src) +172/-2
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-23

Changed Functions

FunctionChangeNotes
GLES2DecoderEnsurePrevFboWorkaroundTest
gpu/command_buffer/service/gles2_cmd_decoder_unittest_textures.cc
modified
if
gpu/command_buffer/service/gles2_cmd_decoder_unittest_textures.cc
modified
TEST_P
gpu/command_buffer/service/gles2_cmd_decoder_unittest_textures.cc
modified

Files Changed

  • gpu/command_buffer/service/gles2_cmd_decoder.cc
  • gpu/command_buffer/service/gles2_cmd_decoder_unittest_base.h
  • gpu/command_buffer/service/gles2_cmd_decoder_unittest_textures.cc
From feb103bc1c01d1a1e62383f906346d23bb725fb1 Mon Sep 17 00:00:00 2001
From: Ken Russell <kbr@chromium.org>
Date: Wed, 17 Jun 2026 18:06:08 -0700
Subject: [PATCH] Rebind original FBO earlier in ClearLevelUsingGL.

Avoid triggering problems on some drivers when deleting an in-use FBO.

Co-authored with jetski-cli.

Fixed: 523591974
Change-Id: I17239a547a16d8d2b2a754c9141f37b7f0012956
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7953879
Reviewed-by: Brandon Jones <bajones@chromium.org>
Commit-Queue: Kenneth Russell <kbr@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1648728}
---

diff --git a/gpu/command_buffer/service/gles2_cmd_decoder.cc b/gpu/command_buffer/service/gles2_cmd_decoder.cc
index 3a9353d..6c51ee8 100644
--- a/gpu/command_buffer/service/gles2_cmd_decoder.cc
+++ b/gpu/command_buffer/service/gles2_cmd_decoder.cc
@@ -12281,11 +12281,16 @@
     result = true;
   }
   RestoreClearState();
-  api()->glDeleteFramebuffersEXTFn(1, &fb);
+  // Restore the previous framebuffer binding *before* deleting the temporary
+  // FBO. Some Imagination/PowerVR drivers retain an internal reference to the
+  // previously-bound FBO across bind transitions; deleting it while bound and
+  // then rebinding can dereference freed driver state. See the
+  // ensure_previous_framebuffer_not_deleted workaround.
   Framebuffer* framebuffer = GetFramebufferInfoForTarget(fb_target);
   GLuint fb_service_id =
       framebuffer ? framebuffer->service_id() : GetBackbufferServiceId();
   BindFramebuffer(fb_target, fb_service_id);
+  api()->glDeleteFramebuffersEXTFn(1, &fb);
   return result;
 }
 
diff --git a/gpu/command_buffer/service/gles2_cmd_decoder_unittest_base.h b/gpu/command_buffer/service/gles2_cmd_decoder_unittest_base.h
index 87409811..9e4d6e3 100644
--- a/gpu/command_buffer/service/gles2_cmd_decoder_unittest_base.h
+++ b/gpu/command_buffer/service/gles2_cmd_decoder_unittest_base.h
@@ -543,6 +543,7 @@
   }
 
  protected:
+  virtual void SetupMockGLBehaviors();
   static const int kBackBufferWidth = 128;
   static const int kBackBufferHeight = 64;
 
@@ -787,7 +788,6 @@
     GLuint bound_vertex_array_object_;
   };  // class MockGLStates
 
-  void SetupMockGLBehaviors();
 
   GpuPreferences gpu_preferences_;
   ShaderTranslatorCache shader_translator_cache_;
diff --git a/gpu/command_buffer/service/gles2_cmd_decoder_unittest_textures.cc b/gpu/command_buffer/service/gles2_cmd_decoder_unittest_textures.cc
index 826c27a8..407f6b9f 100644
--- a/gpu/command_buffer/service/gles2_cmd_decoder_unittest_textures.cc
+++ b/gpu/command_buffer/service/gles2_cmd_decoder_unittest_textures.cc
@@ -3211,6 +3211,171 @@
   EXPECT_EQ(GL_INVALID_OPERATION, GetGLError());
 }
 
+class GLES2DecoderEnsurePrevFboWorkaroundTest
+    : public GLES2DecoderManualInitTest {
+ public:
+  GLES2DecoderEnsurePrevFboWorkaroundTest() = default;
+
+  void SetupMockGLBehaviors() override {
+    GLES2DecoderManualInitTest::SetupMockGLBehaviors();
+    if (enable_workaround_expectations_) {
+      EXPECT_CALL(*gl_, GenTextures(1, _))
+          .WillOnce(SetArgPointee<1>(kCompleteFboTextureId))
+          .RetiresOnSaturation();
+      EXPECT_CALL(*gl_, BindTexture(GL_TEXTURE_2D, kCompleteFboTextureId))
+          .Times(1)
+          .RetiresOnSaturation();
+      EXPECT_CALL(*gl_, TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 1, 1, 0, GL_RGBA,
+                                   GL_UNSIGNED_BYTE, nullptr))
+          .Times(1)
+          .RetiresOnSaturation();
+      EXPECT_CALL(*gl_, GenFramebuffersEXT(1, _))
+          .WillOnce(SetArgPointee<1>(kCompleteFboId))
+          .RetiresOnSaturation();
+      EXPECT_CALL(*gl_, BindFramebufferEXT(GL_FRAMEBUFFER, kCompleteFboId))
+          .Times(3)
+          .RetiresOnSaturation();
+      EXPECT_CALL(*gl_, FramebufferTexture2DEXT(
+                            GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
+                            kCompleteFboTextureId, 0))
+          .Times(1)
+          .RetiresOnSaturation();
+      EXPECT_CALL(*gl_, CheckFramebufferStatusEXT(GL_FRAMEBUFFER))
+          .WillOnce(Return(GL_FRAMEBUFFER_COMPLETE))
+          .RetiresOnSaturation();
+      EXPECT_CALL(*gl_, DeleteTextures(1, Pointee(kCompleteFboTextureId)))
+          .Times(1)
+          .RetiresOnSaturation();
+    }
+  }
+
+ protected:
+  bool enable_workaround_expectations_ = false;
+  static constexpr GLuint kCompleteFboTextureId = 999;
+  static constexpr GLuint kCompleteFboId = 998;
+};
+
+// The test uses a depth texture to force GLES2DecoderImpl::ClearLevel to use
+// the ClearLevelUsingGL path, which is required to trigger the FBO restoration
+// workaround logic. For color textures, ClearLevel may fallback to the
+// TexSubImage2D clear path which does not use a temporary FBO.
+TEST_P(GLES2DecoderEnsurePrevFboWorkaroundTest, ClearLevelUsingGLOrder) {
+  enable_workaround_expectations_ = true;
+  gpu::GpuDriverBugWorkarounds workarounds;
+  workarounds.ensure_previous_framebuffer_not_deleted = true;
+  InitState init;
+  init.extensions = "GL_ANGLE_depth_texture";
+  init.gl_version = "OpenGL ES 2.0";
+  init.has_depth = true;
+  init.request_depth = true;
+  InitDecoderWithWorkarounds(init, workarounds);
+
+  // Set up texture
+  DoBindTexture(GL_TEXTURE_2D, client_texture_id_, kServiceTextureId);
+  DoTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, 1, 1, 0,
+               GL_DEPTH_COMPONENT, GL_UNSIGNED_INT, 0, 0);
+
+  TextureRef* texture_ref =
+      group().texture_manager()->GetTexture(client_texture_id_);
+  ASSERT_TRUE(texture_ref != nullptr);
+  Texture* texture = texture_ref->texture();
+
+  // Expectations for ClearLevel
+  const GLuint kTempFboId = 777;
+  InSequence seq;
+
+  // 1. Gen temp FBO
+  EXPECT_CALL(*gl_, GenFramebuffersEXT(1, _))
+      .WillOnce(SetArgPointee<1>(kTempFboId))
+      .RetiresOnSaturation();
+
+  // 2. Bind temp FBO (wrapper triggers complete_fbo bind first)
+  EXPECT_CALL(*gl_, BindFramebufferEXT(GL_FRAMEBUFFER, kCompleteFboId))
+      .Times(1)
+      .RetiresOnSaturation();
+  EXPECT_CALL(*gl_, BindFramebufferEXT(GL_FRAMEBUFFER, kTempFboId))
+      .Times(1)
+      .RetiresOnSaturation();
+
+  // 3. Attach texture
+  EXPECT_CALL(*gl_,
+              FramebufferTexture2DEXT(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT,
+                                      GL_TEXTURE_2D, kServiceTextureId, 0))
+      .Times(1)
+      .RetiresOnSaturation();
+
+  // 4. Check status
+  EXPECT_CALL(*gl_, CheckFramebufferStatusEXT(GL_FRAMEBUFFER))
+      .WillOnce(Return(GL_FRAMEBUFFER_COMPLETE))
+      .RetiresOnSaturation();
+
+  // 5. Clear calls
+  if (GetParam()) {
+    EXPECT_CALL(*gl_, ColorMask(true, true, true, true))
+        .Times(1)
+        .RetiresOnSaturation();
+  }
+  EXPECT_CALL(*gl_, ClearColor(0.0f, 0.0f, 0.0f, 0.0f))
+      .Times(1)
+      .RetiresOnSaturation();
+  EXPECT_CALL(*gl_, ClearStencil(0)).Times(1).RetiresOnSaturation();
+  if (GetParam()) {
+    EXPECT_CALL(*gl_, StencilMaskSeparate(GL_FRONT, 0xFFFFFFFF))
+        .Times(1)
+        .RetiresOnSaturation();
+    EXPECT_CALL(*gl_, StencilMaskSeparate(GL_BACK, 0xFFFFFFFF))
+        .Times(1)
+        .RetiresOnSaturation();
+  }
+  EXPECT_CALL(*gl_, ClearDepth(1.0f)).Times(1).RetiresOnSaturation();
+  if (GetParam()) {
+    EXPECT_CALL(*gl_, DepthMask(true)).Times(1).RetiresOnSaturation();
+  }
+  EXPECT_CALL(*gl_, Enable(GL_SCISSOR_TEST)).Times(1).RetiresOnSaturation();
+  EXPECT_CALL(*gl_, Scissor(0, 0, 1, 1)).Times(1).RetiresOnSaturation();
+  EXPECT_CALL(*gl_, Clear(GL_DEPTH_BUFFER_BIT)).Times(1).RetiresOnSaturation();
+
+  // 6. Restore state
+  EXPECT_CALL(*gl_, ClearColor(0.0f, 0.0f, 0.0f, 0.0f))
+      .Times(1)
+      .RetiresOnSaturation();
+  EXPECT_CALL(*gl_, ClearStencil(0)).Times(1).RetiresOnSaturation();
+  EXPECT_CALL(*gl_, ClearDepth(1.0f)).Times(1).RetiresOnSaturation();
+  EXPECT_CALL(*gl_, Disable(GL_SCISSOR_TEST)).Times(1).RetiresOnSaturation();
+  EXPECT_CALL(*gl_,
+              Scissor(0, 0, 128, 64))  // 128x64 is the default backbuffer size
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/gpu/command_buffer/service/gles2_cmd_decoder_unittest_base.h b/gpu/command_buffer/service/gles2_cmd_decoder_unittest_base.h
index 87409811..9e4d6e3 100644
--- a/gpu/command_buffer/service/gles2_cmd_decoder_unittest_base.h
+++ b/gpu/command_buffer/service/gles2_cmd_decoder_unittest_base.h
@@ -543,6 +543,7 @@
   }
 
  protected:
+  virtual void SetupMockGLBehaviors();
   static const int kBackBufferWidth = 128;
   static const int kBackBufferHeight = 64;
 
@@ -787,7 +788,6 @@
     GLuint bound_vertex_array_object_;
   };  // class MockGLStates
 
-  void SetupMockGLBehaviors();
 
   GpuPreferences gpu_preferences_;
   ShaderTranslatorCache shader_translator_cache_;
diff --git a/gpu/command_buffer/service/gles2_cmd_decoder_unittest_textures.cc b/gpu/command_buffer/service/gles2_cmd_decoder_unittest_textures.cc
index 826c27a8..407f6b9f 100644
--- a/gpu/command_buffer/service/gles2_cmd_decoder_unittest_textures.cc
+++ b/gpu/command_buffer/service/gles2_cmd_decoder_unittest_textures.cc
@@ -3211,6 +3211,171 @@
   EXPECT_EQ(GL_INVALID_OPERATION, GetGLError());
 }
 
+class GLES2DecoderEnsurePrevFboWorkaroundTest
+    : public GLES2DecoderManualInitTest {
+ public:
+  GLES2DecoderEnsurePrevFboWorkaroundTest() = default;
+
+  void SetupMockGLBehaviors() override {
+    GLES2DecoderManualInitTest::SetupMockGLBehaviors();
+    if (enable_workaround_expectations_) {
+      EXPECT_CALL(*gl_, GenTextures(1, _))
+          .WillOnce(SetArgPointee<1>(kCompleteFboTextureId))
+          .RetiresOnSaturation();
+      EXPECT_CALL(*gl_, BindTexture(GL_TEXTURE_2D, kCompleteFboTextureId))
+          .Times(1)
+          .RetiresOnSaturation();
+      EXPECT_CALL(*gl_, TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 1, 1, 0, GL_RGBA,
+                                   GL_UNSIGNED_BYTE, nullptr))
+          .Times(1)
+          .RetiresOnSaturation();
+      EXPECT_CALL(*gl_, GenFramebuffersEXT(1, _))
+          .WillOnce(SetArgPointee<1>(kCompleteFboId))
+          .RetiresOnSaturation();
+      EXPECT_CALL(*gl_, BindFramebufferEXT(GL_FRAMEBUFFER, kCompleteFboId))
+          .Times(3)
+          .RetiresOnSaturation();
+      EXPECT_CALL(*gl_, FramebufferTexture2DEXT(
+                            GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
+                            kCompleteFboTextureId, 0))
+          .Times(1)
+          .RetiresOnSaturation();
+      EXPECT_CALL(*gl_, CheckFramebufferStatusEXT(GL_FRAMEBUFFER))
+          .WillOnce(Return(GL_FRAMEBUFFER_COMPLETE))
+          .RetiresOnSaturation();
+      EXPECT_CALL(*gl_, DeleteTextures(1, Pointee(kCompleteFboTextureId)))
+          .Times(1)
+          .RetiresOnSaturation();
+    }
+  }
+
+ protected:
+  bool enable_workaround_expectations_ = false;
+  static constexpr GLuint kCompleteFboTextureId = 999;
+  static constexpr GLuint kCompleteFboId = 998;
+};
+
+// The test uses a depth texture to force GLES2DecoderImpl::ClearLevel to use
+// the ClearLevelUsingGL path, which is required to trigger the FBO restoration
+// workaround logic. For color textures, ClearLevel may fallback to the
+// TexSubImage2D clear path which does not use a temporary FBO.
+TEST_P(GLES2DecoderEnsurePrevFboWorkaroundTest, ClearLevelUsingGLOrder) {
+  enable_workaround_expectations_ = true;
+  gpu::GpuDriverBugWorkarounds workarounds;
+  workarounds.ensure_previous_framebuffer_not_deleted = true;
+  InitState init;
+  init.extensions = "GL_ANGLE_depth_texture";
+  init.gl_version = "OpenGL ES 2.0";
+  init.has_depth = true;
+  init.request_depth = true;
+  InitDecoderWithWorkarounds(init, workarounds);
+
+  // Set up texture
+  DoBindTexture(GL_TEXTURE_2D, client_texture_id_, kServiceTextureId);
+  DoTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, 1, 1, 0,
+               GL_DEPTH_COMPONENT, GL_UNSIGNED_INT, 0, 0);
+
+  TextureRef* texture_ref =
+      group().texture_manager()->GetTexture(client_texture_id_);
+  ASSERT_TRUE(texture_ref != nullptr);
+  Texture* texture = texture_ref->texture();
+
+  // Expectations for ClearLevel
+  const GLuint kTempFboId = 777;
+  InSequence seq;
+
+  // 1. Gen temp FBO
+  EXPECT_CALL(*gl_, GenFramebuffersEXT(1, _))
+      .WillOnce(SetArgPointee<1>(kTempFboId))
+      .RetiresOnSaturation();
+
+  // 2. Bind temp FBO (wrapper triggers complete_fbo bind first)
+  EXPECT_CALL(*gl_, BindFramebufferEXT(GL_FRAMEBUFFER, kCompleteFboId))
+      .Times(1)
+      .RetiresOnSaturation();
+  EXPECT_CALL(*gl_, BindFramebufferEXT(GL_FRAMEBUFFER, kTempFboId))
+      .Times(1)
+      .RetiresOnSaturation();
+
+  // 3. Attach texture
+  EXPECT_CALL(*gl_,
+              FramebufferTexture2DEXT(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT,
+                                      GL_TEXTURE_2D, kServiceTextureId, 0))
+      .Times(1)
+      .RetiresOnSaturation();
+
+  // 4. Check status
+  EXPECT_CALL(*gl_, CheckFramebufferStatusEXT(GL_FRAMEBUFFER))
+      .WillOnce(Return(GL_FRAMEBUFFER_COMPLETE))
+      .RetiresOnSaturation();
+
+  // 5. Clear calls
+  if (GetParam()) {
+    EXPECT_CALL(*gl_, ColorMask(true, true, true, true))
+        .Times(1)
+        .RetiresOnSaturation();
+  }
+  EXPECT_CALL(*gl_, ClearColor(0.0f, 0.0f, 0.0f, 0.0f))
+      .Times(1)
+      .RetiresOnSaturation();
+  EXPECT_CALL(*gl_, ClearStencil(0)).Times(1).RetiresOnSaturation();
+  if (GetParam()) {
+    EXPECT_CALL(*gl_, StencilMaskSeparate(GL_FRONT, 0xFFFFFFFF))
+        .Times(1)
+        .RetiresOnSaturation();
+    EXPECT_CALL(*gl_, StencilMaskSeparate(GL_BACK, 0xFFFFFFFF))
+        .Times(1)
+        .RetiresOnSaturation();
+  }
+  EXPECT_CALL(*gl_, ClearDepth(1.0f)).Times(1).RetiresOnSaturation();
+  if (GetParam()) {
+    EXPECT_CALL(*gl_, DepthMask(true)).Times(1).RetiresOnSaturation();
+  }
+  EXPECT_CALL(*gl_, Enable(GL_SCISSOR_TEST)).Times(1).RetiresOnSaturation();
+  EXPECT_CALL(*gl_, Scissor(0, 0, 1, 1)).Times(1).RetiresOnSaturation();
+  EXPECT_CALL(*gl_, Clear(GL_DEPTH_BUFFER_BIT)).Times(1).RetiresOnSaturation();
+
+  // 6. Restore state
+  EXPECT_CALL(*gl_, ClearColor(0.0f, 0.0f, 0.0f, 0.0f))
+      .Times(1)
+      .RetiresOnSaturation();
+  EXPECT_CALL(*gl_, ClearStencil(0)).Times(1).RetiresOnSaturation();
+  EXPECT_CALL(*gl_, ClearDepth(1.0f)).Times(1).RetiresOnSaturation();
+  EXPECT_CALL(*gl_, Disable(GL_SCISSOR_TEST)).Times(1).RetiresOnSaturation();
+  EXPECT_CALL(*gl_,
+              Scissor(0, 0, 128, 64))  // 128x64 is the default backbuffer size
+      .Times(1)
+      .RetiresOnSaturation();
+
+  // 7. Restore FBO binding (CRITICAL: Must happen BEFORE delete)
+  // Wrapper triggers complete_fbo bind first
+  EXPECT_CALL(*gl_, BindFramebufferEXT(GL_FRAMEBUFFER, kCompleteFboId))
+      .Times(1)
+      .RetiresOnSaturation();
+  EXPECT_CALL(*gl_, BindFramebufferEXT(GL_FRAMEBUFFER, 0))
+      .Times(1)
+      .RetiresOnSaturation();
+
+  // 8. Delete temp FBO
+  EXPECT_CALL(*gl_, DeleteFramebuffersEXT(1, Pointee(kTempFboId)))
+      .Times(1)
+      .RetiresOnSaturation();
+
+  // Call ClearLevel
+  EXPECT_TRUE(decoder_->ClearLevel(texture, GL_TEXTURE_2D, 0,
+                                   GL_DEPTH_COMPONENT, GL_UNSIGNED_INT, 0, 0, 1,
+                                   1));
+
+  DoDeleteTexture(client_texture_id_, kServiceTextureId);
+}
+
+// The boolean parameter controls whether ignore_cached_state_for_test is
+// enabled in the base fixture GLES2DecoderTestBase. This forces GL state
+// restoration and helps test state-dirtying paths.
+INSTANTIATE_TEST_SUITE_P(Service,
+                         GLES2DecoderEnsurePrevFboWorkaroundTest,
+                         ::testing::Bool());
+
 TEST_P(GLES2DecoderManualInitTest, DrawWithGLImageExternal) {
   InitState init;
   init.extensions = "GL_OES_EGL_image_external";
Loading diff…

Original Bug Report

reported by rj...@google.com

Potential UAF in PowerVR GPU driver via FBO deletion order in ClearLevelUsingGL

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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: An incorrect order of operations in GLES2DecoderImpl::ClearLevelUsingGL deletes a temporary Framebuffer Object (FBO) while it is actively bound. On Android devices with PowerVR GPUs, restoring the previous FBO subsequently triggers a driver bug, leading to a Use-After-Free (UAF) in the GPU process. This can be reached from malicious WebGL content.

Affected files:

  • gpu/command_buffer/service/gles2_cmd_decoder.cc

Estimated timestamp from git blame: Unknown (Google3 checkout)

Background

In the validating command decoder, GLES2DecoderImpl::ClearLevelUsingGL is used to clear uninitialized texture levels. It achieves this by creating a temporary Framebuffer Object (FBO), binding it, attaching the texture, clearing it, and then tearing down the temporary FBO.

Certain Imagination (PowerVR) GPU drivers contain a bug where they retain internal references to the ‘currently bound’ or ‘previously bound’ FBO across state transitions. To mitigate this, Chrome implements the ensure_previous_framebuffer_not_deleted workaround (enabled in gpu_driver_bug_list.json for Imagination.*). This workaround wraps FBO binding operations inside GLES2DecoderImpl::BindFramebuffer to ensure a known-good internal FBO (complete_fbo_) is bound during transitions, preventing the driver from accessing deleted state.

The Vulnerability

An incorrect sequence of OpenGL calls in GLES2DecoderImpl::ClearLevelUsingGL defeats this workaround, creating a condition that triggers the PowerVR driver bug.

The vulnerable code flow in gpu/command_buffer/service/gles2_cmd_decoder.cc is as follows:

  1. A temporary FBO fb is created and bound using the BindFramebuffer wrapper.
  2. The uninitialized texture is attached and cleared using glClearFn.
  3. The flaw: api()->glDeleteFramebuffersEXTFn(1, &fb) is called while fb is still the currently bound FBO on the GL context.
  4. Although the OpenGL ES spec mandates that deleting the bound FBO implicitly binds FBO 0, the buggy PowerVR driver fails to completely update its internal FBO state, leaving a dangling pointer to the now-freed fb.
  5. Immediately after deletion, BindFramebuffer(fb_target, fb_service_id) is called to restore the original FBO binding.
  6. Inside the BindFramebuffer wrapper, the PowerVR workaround triggers, attempting to bind the internal complete_fbo_ first.
  7. This state transition from the deleted fb to complete_fbo_ causes the driver to access its dangling pointer representing the previously bound FBO, resulting in a Use-After-Free (UAF).

Because the GPU process is unsandboxed on Android devices, memory corruption in the driver can lead to a direct sandbox escape.

Potential Attacker Steps (Theoretical)

Note: These are suggested theoretical steps as our tooling agent cannot execute code to verify a full chain.

  1. Heap Grooming: The attacker manipulates the GPU process heap via WebGL, creating and freeing FBOs or other driver objects to prepare the memory layout.
  2. Trigger Allocation: The attacker allocates a depth texture using WebGL (e.g., texImage2D with null data) on an ES2 context.
  3. Trigger the Clear: The attacker issues a WebGL draw call (e.g., drawArrays) that relies on the uninitialized depth texture. The validation command decoder will intercept this and force a clear of the texture, invoking ClearLevelUsingGL.
  4. Exploitation: The driver dereferences the freed FBO state during the subsequent BindFramebuffer call. If the freed memory has been replaced by attacker-controlled data, this can lead to code execution in the GPU process.

The restoration of the original FBO binding must occur before the temporary FBO is deleted. This ensures the driver transitions its internal state away from the temporary FBO while the FBO is still validly allocated, allowing the ensure_previous_framebuffer_not_deleted workaround to function correctly.

// gpu/command_buffer/service/gles2_cmd_decoder.cc - GLES2DecoderImpl::ClearLevelUsingGL

RestoreClearState();

// 1. Restore the old binding first
Framebuffer* framebuffer = GetFramebufferInfoForTarget(fb_target);
GLuint fb_service_id =
    framebuffer ? framebuffer->service_id() : GetBackbufferServiceId();
BindFramebuffer(fb_target, fb_service_id);

// 2. Now it is safe to delete the temporary FBO
api()->glDeleteFramebuffersEXTFn(1, &fb);

return result;

Evaluated with Chrome root at commit: 65b3256311f3ab6fb9870eaa522de7e6dd2663bb


Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:

  • If you are familiar with the severity guidelines, you may adjust the severity.
  • If this is a false positive, and there’s no work to be done, please close as WAI.
  • If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.

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.

View on issue tracker