CVE-2026-9879
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
Texture2DRGTestsrc/tests/gl_tests/TextureTest.cpp |
modified |
Files Changed
src/libANGLE/renderer/gl/FramebufferGL.cppsrc/tests/gl_tests/TextureTest.cpp
Patch
From c466bb31a41235ec702303d63464964af12e9619 Mon Sep 17 00:00:00 2001 From: Ken Russell <[email protected]> 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 <[email protected]> Auto-Submit: Kenneth Russell <[email protected]> Commit-Queue: Geoff Lang <[email protected]> --- 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(),
Regression Test / PoC
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(),
Original Bug Report
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.
- Initialize a WebGL 2 context and enable the
EXT_texture_norm16extension. - Create an
R16_EXTtexture and attach it to a framebuffer. - Create a WebGL buffer and bind it to
gl.PIXEL_PACK_BUFFER. - 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. - Call
gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_SHORT, TARGET_OFFSET), whereTARGET_OFFSETis the desired virtual address in the GPU process to be written to.
Suggested Fix
- Workaround Fix: Modify
FramebufferGL::readPixelsto prevent theEXT_texture_norm16workaround 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. - Error Handling Fix: Address the systemic issue in
BufferGL::setDatawhere native driver errors (like OOM) are silently ignored in release builds. TheANGLE_GL_TRYmacro 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.