Overview

Critical
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactOut of bounds write in ANGLE
DescriptionOut of bounds write in ANGLE
ComponentANGLE
Bug ClassOOB
Tracker499129768
Fix commitc466bb31a412 (angle/angle) +29/-2
CISA KEVNot listed
CreditedGoogle
Disclosed2026-05-27

Changed Functions

FunctionChangeNotes
Texture2DRGTest
src/tests/gl_tests/TextureTest.cpp
modified

Files Changed

  • src/libANGLE/renderer/gl/FramebufferGL.cpp
  • src/tests/gl_tests/TextureTest.cpp
From c466bb31a41235ec702303d63464964af12e9619 Mon Sep 17 00:00:00 2001
From: Ken Russell <kbr@chromium.org>
Date: Tue, 21 Apr 2026 15:12:00 -0700
Subject: [PATCH] Disable norm16 readback workaround if PBO is bound.

This workaround was required only on a limited set of GPUs and doesn't
work if the destination is a PBO - only client side memory.

Verified that the new test catches the bug in an ASAN build on Linux.

Fixed: chromium:499129768
Change-Id: Ib2a5fd77ea62de600ebc263791c722420e5e958d
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7783582
Reviewed-by: Geoff Lang <geofflang@chromium.org>
Auto-Submit: Kenneth Russell <kbr@chromium.org>
Commit-Queue: Geoff Lang <geofflang@chromium.org>
---

diff --git a/src/libANGLE/renderer/gl/FramebufferGL.cpp b/src/libANGLE/renderer/gl/FramebufferGL.cpp
index ecea51f..6889e4c 100644
--- a/src/libANGLE/renderer/gl/FramebufferGL.cpp
+++ b/src/libANGLE/renderer/gl/FramebufferGL.cpp
@@ -311,9 +311,11 @@
         ContextGL *contextGL              = GetImplAs<ContextGL>(context);
         const angle::FeaturesGL &features = GetFeaturesGL(context);
 
+        // This workaround does not work if the destination is a Pixel Buffer Object.
         enabled = features.readPixelsUsingImplementationColorReadFormatForNorm16.enabled &&
                   type == GL_UNSIGNED_SHORT && originalReadFormat == GL_RGBA &&
-                  (format == GL_RED || format == GL_RG);
+                  (format == GL_RED || format == GL_RG) &&
+                  context->getState().getTargetBuffer(gl::BufferBinding::PixelPack) == nullptr;
 
         clientPixels = pixels;
 
diff --git a/src/tests/gl_tests/TextureTest.cpp b/src/tests/gl_tests/TextureTest.cpp
index 83201fc..d8a1736 100644
--- a/src/tests/gl_tests/TextureTest.cpp
+++ b/src/tests/gl_tests/TextureTest.cpp
@@ -12317,6 +12317,28 @@
     testNorm16RenderAndReadPixels(GL_RGBA16_EXT, GL_RGBA, GL_UNSIGNED_SHORT);
 }
 
+// Test that readback with PBO correctly uses the offset, and doesn't dereference it as a pointer.
+TEST_P(Texture2DNorm16TestES3, ReadbackWithPBO)
+{
+    ANGLE_SKIP_TEST_IF(!IsGLExtensionEnabled("GL_EXT_texture_norm16"));
+
+    GLTexture tex;
+    glBindTexture(GL_TEXTURE_2D, tex);
+    glTexStorage2D(GL_TEXTURE_2D, 1, GL_R16_EXT, 1, 1);
+
+    GLFramebuffer fbo;
+    glBindFramebuffer(GL_FRAMEBUFFER, fbo);
+    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex, 0);
+    EXPECT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+
+    GLBuffer pbo;
+    glBindBuffer(GL_PIXEL_PACK_BUFFER, pbo);
+    glBufferData(GL_PIXEL_PACK_BUFFER, 16, nullptr, GL_STREAM_READ);
+
+    glReadPixels(0, 0, 1, 1, GL_RGBA, GL_UNSIGNED_SHORT, reinterpret_cast<void *>(8));
+    EXPECT_GL_NO_ERROR();
+}
+
 class Texture2DRGTest : public Texture2DTest
 {
   protected:
@@ -19618,7 +19640,10 @@
 ANGLE_INSTANTIATE_TEST_ES2(TextureLimitsTest);
 
 GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(Texture2DNorm16TestES3);
-ANGLE_INSTANTIATE_TEST_ES3(Texture2DNorm16TestES3);
+ANGLE_INSTANTIATE_TEST_ES3_AND(
+    Texture2DNorm16TestES3,
+    ES3_OPENGL().enable(Feature::ReadPixelsUsingImplementationColorReadFormatForNorm16),
+    ES3_OPENGLES().enable(Feature::ReadPixelsUsingImplementationColorReadFormatForNorm16));
 
 ANGLE_INSTANTIATE_TEST_ES2_AND_ES3_AND(Texture2DRGTest,
                                        ES2_EMULATE_COPY_TEX_IMAGE_VIA_SUB(),
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/src/tests/gl_tests/TextureTest.cpp b/src/tests/gl_tests/TextureTest.cpp
index 83201fc..d8a1736 100644
--- a/src/tests/gl_tests/TextureTest.cpp
+++ b/src/tests/gl_tests/TextureTest.cpp
@@ -12317,6 +12317,28 @@
     testNorm16RenderAndReadPixels(GL_RGBA16_EXT, GL_RGBA, GL_UNSIGNED_SHORT);
 }
 
+// Test that readback with PBO correctly uses the offset, and doesn't dereference it as a pointer.
+TEST_P(Texture2DNorm16TestES3, ReadbackWithPBO)
+{
+    ANGLE_SKIP_TEST_IF(!IsGLExtensionEnabled("GL_EXT_texture_norm16"));
+
+    GLTexture tex;
+    glBindTexture(GL_TEXTURE_2D, tex);
+    glTexStorage2D(GL_TEXTURE_2D, 1, GL_R16_EXT, 1, 1);
+
+    GLFramebuffer fbo;
+    glBindFramebuffer(GL_FRAMEBUFFER, fbo);
+    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex, 0);
+    EXPECT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+
+    GLBuffer pbo;
+    glBindBuffer(GL_PIXEL_PACK_BUFFER, pbo);
+    glBufferData(GL_PIXEL_PACK_BUFFER, 16, nullptr, GL_STREAM_READ);
+
+    glReadPixels(0, 0, 1, 1, GL_RGBA, GL_UNSIGNED_SHORT, reinterpret_cast<void *>(8));
+    EXPECT_GL_NO_ERROR();
+}
+
 class Texture2DRGTest : public Texture2DTest
 {
   protected:
@@ -19618,7 +19640,10 @@
 ANGLE_INSTANTIATE_TEST_ES2(TextureLimitsTest);
 
 GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(Texture2DNorm16TestES3);
-ANGLE_INSTANTIATE_TEST_ES3(Texture2DNorm16TestES3);
+ANGLE_INSTANTIATE_TEST_ES3_AND(
+    Texture2DNorm16TestES3,
+    ES3_OPENGL().enable(Feature::ReadPixelsUsingImplementationColorReadFormatForNorm16),
+    ES3_OPENGLES().enable(Feature::ReadPixelsUsingImplementationColorReadFormatForNorm16));
 
 ANGLE_INSTANTIATE_TEST_ES2_AND_ES3_AND(Texture2DRGTest,
                                        ES2_EMULATE_COPY_TEX_IMAGE_VIA_SUB(),
Loading diff…

Original Bug Report

reported by vm...@google.com

Arbitrary Write in ANGLE via EXT_texture_norm16 Readback Workaround

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 without the security team.

Overview: An out-of-bounds write vulnerability exists in ANGLE’s OpenGL backend when using the EXT_texture_norm16 readback workaround with a Pixel Buffer Object (PBO). The workaround misinterprets an attacker-controlled PBO offset as a host pointer and writes to it directly, potentially leading to an arbitrary write primitive and RCE in the GPU process.

Affected files:

  • third_party/angle/src/libANGLE/renderer/gl/FramebufferGL.cpp

Estimated timestamp from git blame: 2020-01-03

Description

A vulnerability exists in ANGLE’s OpenGL backend implementation of the EXT_texture_norm16 readback workaround, located in src/libANGLE/renderer/gl/FramebufferGL.cpp. This workaround is designed to handle glReadPixels calls using the RGBA/UNSIGNED_SHORT format/type from textures with R16_EXT or RG16_EXT formats, a combination that some native drivers handle incorrectly.

The workaround implementation, specifically RearrangeEXTTextureNorm16Pixels, fails to account for cases where a Pixel Buffer Object (PBO) is bound. When a PBO is bound, the pixels parameter to glReadPixels represents a byte offset into the PBO, not a host memory address. However, the workaround casts this offset to a CPU pointer and writes pixel data directly to it, resulting in a “wild write” at a controlled virtual address.

Bypass Mechanism and Trigger

To exploit this, an attacker must provide a high offset (e.g., 0x70000000) to glReadPixels. Both Blink and ANGLE validate that offset + read_size <= PBO_size. However, an attacker can bypass these checks by requesting a massive buffer allocation (e.g., 2GB) using gl.bufferData().

On constrained systems (like 32-bit Android devices), the native OpenGL driver will likely fail to allocate 2GB and return a GL_OUT_OF_MEMORY error. Crucially, in release builds of ANGLE, the ANGLE_GL_TRY macro is a no-op, meaning BufferGL::setData ignores the native driver’s OOM error and reports success to the frontend.

Consequently, both Blink and ANGLE’s frontend state incorrectly record the buffer size as 2GB. When the attacker subsequently calls glReadPixels with a high offset, the validation checks pass. The native driver will fail the read operation (because the actual buffer is non-existent), but ANGLE_GL_TRY ignores this error as well. Execution then proceeds to RearrangeEXTTextureNorm16Pixels, which writes to the attacker-controlled offset.

Impact

The write pattern is a fixed 8-byte sequence per pixel: 00 00 00 00 00 00 FF FF. On 32-bit GPU process configurations (common on Android Mali/Adreno devices where this workaround is enabled), this allows a genuine fixed-pattern write primitive against predictable heap structures or executable memory, potentially leading to full Remote Code Execution (RCE) in the GPU process.

Potential Reproduction Steps

Note: These are theoretical steps based on code analysis.

  1. Initialize a WebGL 2 context and enable the EXT_texture_norm16 extension.
  2. Create an R16_EXT texture and attach it to a framebuffer.
  3. Create a WebGL buffer and bind it to gl.PIXEL_PACK_BUFFER.
  4. Call gl.bufferData(gl.PIXEL_PACK_BUFFER, 2000000000, gl.STATIC_DRAW) to request a massive buffer, intentionally causing a native driver OOM that ANGLE will ignore.
  5. Call gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_SHORT, TARGET_OFFSET), where TARGET_OFFSET is the desired virtual address in the GPU process to be written to.

Suggested Fix

  1. Workaround Fix: Modify FramebufferGL::readPixels to prevent the EXT_texture_norm16 workaround from executing when a PBO is bound, or update the workaround to properly handle PBOs by performing the pixel expansion via native OpenGL commands (e.g., using a compute shader or intermediate buffer object) rather than CPU-side pointer manipulation.
  2. Error Handling Fix: Address the systemic issue in BufferGL::setData where native driver errors (like OOM) are silently ignored in release builds. The ANGLE_GL_TRY macro or the error handling logic in the backend should ensure critical failures are propagated to the frontend so that internal buffer size tracking remains accurate.

Evaluated with Chrome root at commit: ff3d2b74fa39431785bd60e51463b08fcc71ee33


Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; 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