CVE-2026-9924
Overview
Files Changed
src/image_util/loadimage.cppsrc/tests/gl_tests/TextureTest.cpp
Patch
From 42a140e616b14b75d264a1f5fab862ee5d2e1905 Mon Sep 17 00:00:00 2001 From: Geoff Lang <[email protected]> Date: Tue, 21 Apr 2026 13:45:34 -0400 Subject: [PATCH] Fix output stride for LoadLA8ToRGBA4 RGBA4 is 2 bytes per pixel, not 4. This function is only used for D3D11 when loading LA8 data into an RGBA4 texture. Fixed: chromium:500398345 Change-Id: I5c9add5d51d9d8a7f08ec66f2751d7c3cbe82052 Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7783166 Commit-Queue: Geoff Lang <[email protected]> Reviewed-by: Shahbaz Youssefi <[email protected]> --- diff --git a/src/image_util/loadimage.cpp b/src/image_util/loadimage.cpp index 58cd3d1..c8c4f23 100644 --- a/src/image_util/loadimage.cpp +++ b/src/image_util/loadimage.cpp @@ -342,8 +342,8 @@ { uint8_t l = source[2 * x + 0] >> 4; uint8_t a = source[2 * x + 1] >> 4; - dest[4 * x + 0] = l | l << 4; - dest[4 * x + 1] = l | a << 4; + dest[2 * x + 0] = l | l << 4; + dest[2 * x + 1] = l | a << 4; } } } diff --git a/src/tests/gl_tests/TextureTest.cpp b/src/tests/gl_tests/TextureTest.cpp index f213049..3d8efe5 100644 --- a/src/tests/gl_tests/TextureTest.cpp +++ b/src/tests/gl_tests/TextureTest.cpp @@ -5141,6 +5141,28 @@ EXPECT_GL_NO_ERROR(); } +// Regression test for covering the LoadLA8ToRGBA4 function when loading LA8 data into RGBA4 luma +// emulation texture. +TEST_P(Texture2DTestES3, L4A4Upload) +{ + ANGLE_SKIP_TEST_IF(!EnsureGLExtensionEnabled("GL_OES_required_internalformat")); + + GLTexture tex; + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, tex); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + + constexpr GLubyte data[8] = {64, 128, 64, 128, 64, 128, 64, 128}; + glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE4_ALPHA4_OES, 2, 2, 0, GL_LUMINANCE_ALPHA, + GL_UNSIGNED_BYTE, data); + EXPECT_GL_NO_ERROR(); + + drawQuad(mProgram, "position", 0.5f); + + EXPECT_PIXEL_COLOR_NEAR(0, 0, GLColor(64, 64, 64, 128), 8.0); +} + // Test to ensure that glTexStorage3D accepts ASTC sliced 3D. https://crbug.com/1060012 TEST_P(Texture3DTestES3, ImmutableASTCSliced3D) {
Regression Test / PoC
diff --git a/src/tests/gl_tests/TextureTest.cpp b/src/tests/gl_tests/TextureTest.cpp
index f213049..3d8efe5 100644
--- a/src/tests/gl_tests/TextureTest.cpp
+++ b/src/tests/gl_tests/TextureTest.cpp
@@ -5141,6 +5141,28 @@
EXPECT_GL_NO_ERROR();
}
+// Regression test for covering the LoadLA8ToRGBA4 function when loading LA8 data into RGBA4 luma
+// emulation texture.
+TEST_P(Texture2DTestES3, L4A4Upload)
+{
+ ANGLE_SKIP_TEST_IF(!EnsureGLExtensionEnabled("GL_OES_required_internalformat"));
+
+ GLTexture tex;
+ glActiveTexture(GL_TEXTURE0);
+ glBindTexture(GL_TEXTURE_2D, tex);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+
+ constexpr GLubyte data[8] = {64, 128, 64, 128, 64, 128, 64, 128};
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE4_ALPHA4_OES, 2, 2, 0, GL_LUMINANCE_ALPHA,
+ GL_UNSIGNED_BYTE, data);
+ EXPECT_GL_NO_ERROR();
+
+ drawQuad(mProgram, "position", 0.5f);
+
+ EXPECT_PIXEL_COLOR_NEAR(0, 0, GLColor(64, 64, 64, 128), 8.0);
+}
+
// Test to ensure that glTexStorage3D accepts ASTC sliced 3D. https://crbug.com/1060012
TEST_P(Texture3DTestES3, ImmutableASTCSliced3D)
{
Original Bug Report
Potential GPU Process OOB Write in ANGLE via LoadLA8ToRGBA4
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 D3D11 backend when loading GL_LUMINANCE4_ALPHA4_OES texture data. The LoadLA8ToRGBA4 format conversion function uses an incorrect 4-byte output stride while writing to a buffer allocated for a 2-byte-per-pixel D3D11 staging texture. This mismatch allows a compromised renderer to trigger an out-of-bounds write in the GPU process, potentially leading to arbitrary code execution.
Affected files:
third_party/angle/src/image_util/loadimage.cppthird_party/angle/src/libANGLE/renderer/d3d/d3d11/Image11.cpp
Estimated timestamp from git blame: 2024-12-03
Summary
A potential out-of-bounds (OOB) write vulnerability exists in the ANGLE D3D11 renderer when handling the GL_LUMINANCE4_ALPHA4_OES texture format.
The vulnerability stems from a logic error in the LoadLA8ToRGBA4 image conversion function, which assumes a 4-byte-per-pixel output stride when writing into a 16-bit (2-byte-per-pixel) staging buffer. A compromised renderer process can trigger this vulnerability to corrupt GPU process memory, potentially leading to arbitrary Remote Code Execution (RCE) and a sandbox escape.
Vulnerability Details
When a renderer creates a texture using glTexImage2D with the internal format GL_LUMINANCE4_ALPHA4_OES, type GL_UNSIGNED_BYTE, and format GL_LUMINANCE_ALPHA, the request is processed by the GPU process.
In third_party/angle/src/libANGLE/renderer/d3d/d3d11/Image11.cpp (Image11::loadData), ANGLE creates a CPU-writable D3D11 staging texture for the upload. If the underlying Windows hardware supports DXGI_FORMAT_B4G4R4A4_UNORM (a 16-bit, 2-byte-per-pixel format), the D3D11 staging texture is allocated with that format, resulting in a row pitch of approximately 2 * width bytes.
ANGLE then calls the format conversion function mapped to this operation: LoadLA8ToRGBA4 (third_party/angle/src/image_util/loadimage.cpp).
void LoadLA8ToRGBA4(..., uint8_t *output, ...)
{
// ... loops for z and y ...
for (size_t x = 0; x < width; x++)
{
uint8_t l = source[2 * x + 0] >> 4;
uint8_t a = source[2 * x + 1] >> 4;
// Bug: Uses a 4-byte stride for a 2-byte target format
dest[4 * x + 0] = l | l << 4;
dest[4 * x + 1] = l | a << 4;
}
}
The LoadLA8ToRGBA4 loop uses an output index of 4 * x, meaning it expects an output buffer stride of 4 bytes per pixel. However, the output pointer (dest) points to the mapped memory of the DXGI_FORMAT_B4G4R4A4_UNORM staging texture, which is only 2 * width bytes large per row.
As x iterates past width / 2, the dest[4 * x + 0] index exceeds the bounds of the driver-allocated staging buffer, resulting in a sparse out-of-bounds write of 2 bytes every 4 bytes. For example, a texture width of 8192 will result in writing roughly 16KB past the end of the staging buffer.
Reachability and Exploitation
This issue can be triggered from a compromised renderer process on Windows devices that support the GL_OES_required_internalformat extension and the DXGI_FORMAT_B4G4R4A4_UNORM hardware format.
The out-of-bounds write occurs in memory allocated by the graphics driver (mapped via D3D11_MAP_WRITE), not via Chromium’s PartitionAlloc. Therefore, memory safety mitigations like MiraclePtr (BRP) do not prevent exploitation.
By carefully grooming the GPU process’s driver-mapped memory space prior to issuing the malicious glTexImage2D call, an attacker could position critical metadata or pointers immediately following the staging texture allocation. The attacker has partial control over the out-of-bounds data via the source luminance and alpha values, allowing them to overwrite critical structures and achieve a sandbox escape.
(Note: These are potential steps an attacker might follow; our automated tooling has not executed a live proof-of-concept exploit.)
- Gain Renderer Execution: Exploit a V8 or Blink vulnerability to run arbitrary code in the sandboxed renderer.
- Verify Extension: Query for
GL_OES_required_internalformatsupport. - Heap Grooming: Use WebGL commands to groom the driver’s mapped memory, placing target objects adjacent to the expected allocation size of the staging buffer.
- Trigger OOB Write: Call
glTexImage2DwithGL_LUMINANCE4_ALPHA4_OES, a width of8192, height of1, and a payload crafted to cleanly overwrite target objects during the OOB write phase. - Achieve RCE: Trigger the corrupted objects to hijack control flow in the GPU process.
Proposed Fix
In third_party/angle/src/image_util/loadimage.cpp, update the indexing in LoadLA8ToRGBA4 to correctly use a 2-byte output stride for the 16-bit format. It is highly recommended to cast the dest pointer to a uint16_t* (as done in other 16-bit conversion functions like LoadRGBA8ToRGBA4) to avoid stride confusion.
// Suggested Fix in loadimage.cpp:
uint16_t *dest = priv::OffsetDataPointer<uint16_t>(output, y, z, outputRowPitch, outputDepthPitch);
for (size_t x = 0; x < width; x++)
{
uint16_t l = source[2 * x + 0] >> 4;
uint16_t a = source[2 * x + 1] >> 4;
dest[x] = (l | (l << 4)) | ((l | (a << 4)) << 8);
}
(Alternatively, adjust the byte offsets to dest[2 * x + 0] and dest[2 * x + 1] while keeping dest as uint8_t*).
Evaluated with Chrome root at commit: 137d451a126685dd5010e6609db9f6d4a78d8234
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.