High CVSS 8.3 chrome OOB 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
8.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactHeap buffer overflow in ANGLE
DescriptionHeap buffer overflow in ANGLE
ComponentANGLE
Bug ClassOOB
Tracker500398345
Fix commit42a140e616b1 (angle/angle) +24/-2
CWECWE-122 (Heap buffer overflow)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-04-07 (per vendor advisory)
Fix landed2026-04-23 (public git)
Disclosed2026-05-27
Patch gap34 days — fix public in source before the advisory

Background

RGBA4 / `DXGI_FORMAT_B4G4R4A4_UNORM`
A 16-bit packed color format, 2 bytes per pixel. The D3D11 staging texture ANGLE allocates for the upload has a row pitch of roughly 2 * width bytes.
Driver-mapped memory
The dest buffer is memory mapped via D3D11_MAP_WRITE by the graphics driver, not allocated by Chromium’s PartitionAlloc, so MiraclePtr/BRP does not cover it.

Root Cause Analysis

LoadLA8ToRGBA4 wrote its 16-bit RGBA4 output using a 4-byte per-pixel stride. For each source pixel it computed dest[4 * x + 0] and dest[4 * x + 1], i.e. it advanced the destination by 4 bytes per pixel even though the target DXGI_FORMAT_B4G4R4A4_UNORM staging texture is only 2 bytes per pixel (row pitch ≈ 2 * width). As x grew past width/2, dest[4 * x + ...] ran past the end of the driver-allocated staging row, producing a sparse out-of-bounds write (2 bytes every 4 bytes) into adjacent driver-mapped memory. For a wide texture (e.g. width 8192) this overruns roughly 16 KB beyond the buffer. The attacker partially controls the written bytes through the source luminance/alpha nibbles.

The patch corrects the output stride to match the 2-byte format: dest[4 * x + 0]/dest[4 * x + 1] become dest[2 * x + 0]/dest[2 * x + 1], so consecutive pixels are packed into contiguous 2-byte slots and writes stay within the 2 * width row. (Upstream’s own commit message confirms “RGBA4 is 2 bytes per pixel, not 4”; a regression test L4A4Upload uploads a 2x2 GL_LUMINANCE4_ALPHA4_OES texture and samples it.) The bug class is OOB heap write (CWE-122/787), exactly as the advisory states — a straightforward stride miscalculation.

Key insight
The LA8→RGBA4 converter indexed its destination as if RGBA4 were 4 bytes per pixel, but the D3D11 staging buffer is 2 bytes per pixel, so writes stepped twice as fast as the buffer and overran it past the halfway point of each row.

Attack Path

  1. Renderer execution Attacker runs code in the sandboxed renderer (e.g. via a separate V8/Blink bug) able to issue GL commands to the GPU process (inferred prerequisite).
  2. Confirm format support On Windows, verify GL_OES_required_internalformat and hardware DXGI_FORMAT_B4G4R4A4_UNORM support so the 2-byte staging path is taken.
  3. Groom driver memory Use WebGL texture operations to place target structures immediately after the expected staging-buffer allocation.
  4. Trigger OOB write Call glTexImage2D with internalformat GL_LUMINANCE4_ALPHA4_OES, type GL_UNSIGNED_BYTE, format GL_LUMINANCE_ALPHA, a large width (e.g. 8192, height 1), and crafted LA source bytes; Image11::loadData maps the 2-byte staging texture and LoadLA8ToRGBA4 writes past it with partially controlled data.
  5. Corrupt and escalate Overwrite adjacent driver metadata/pointers and leverage the corruption for control-flow hijack / RCE and sandbox escape in the GPU process (inferred escalation beyond the diff).

Impact Assessment

The out-of-bounds write occurs in the GPU process, in driver-mapped (D3D11) memory, when a renderer uploads an GL_LUMINANCE4_ALPHA4_OES texture on Windows hardware that exposes the 2-byte B4G4R4A4 staging format (the gating precondition). Privilege required is renderer-level GL access; the advisory’s AC:H reflects the hardware/format gating and grooming needed. The confirmed primitive is a linear-but-sparse heap/driver-buffer overflow (2 bytes per 4-byte stride) with partial attacker control over both offset reach (via width) and written data (via source nibbles). The advisory’s escalation to RCE/sandbox escape is plausible given adjacency control but is (inferred) and not shown by the diff. MiraclePtr does not help because the overrun target is driver memory, not PartitionAlloc.

Files Changed

  • src/image_util/loadimage.cpp
  • src/tests/gl_tests/TextureTest.cpp

Audit Directions

  • Stride/format mismatches in loadimage.cpp
    Audit every LoadXToY converter in src/image_util/loadimage.cpp where the destination is indexed with an explicit multiplier (dest[N * x + ...]) and confirm N matches the destination format’s bytes-per-pixel; prefer the uint16_t*/OffsetDataPointer<uint16_t> idiom used by LoadRGBA8ToRGBA4 to avoid byte/stride confusion.
  • Emulated/luma formats on D3D11
    Review other GL_LUMINANCE*/GL_ALPHA*/GL_LUMINANCE_ALPHA emulation paths in Image11.cpp whose staging format differs in size from the nominal GL format.
  • Width-driven overruns
    Check that converters bound their loops against the actual outputRowPitch/mapped size rather than assuming a stride, especially for large-width single-row uploads.
From 42a140e616b14b75d264a1f5fab862ee5d2e1905 Mon Sep 17 00:00:00 2001
From: Geoff Lang <geofflang@chromium.org>
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 <geofflang@chromium.org>
Reviewed-by: Shahbaz Youssefi <syoussefi@chromium.org>
---

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)
 {
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 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)
 {
Loading diff…

Original Bug Report

reported by vm...@google.com

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.cpp
  • third_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.)

  1. Gain Renderer Execution: Exploit a V8 or Blink vulnerability to run arbitrary code in the sandboxed renderer.
  2. Verify Extension: Query for GL_OES_required_internalformat support.
  3. 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.
  4. Trigger OOB Write: Call glTexImage2D with GL_LUMINANCE4_ALPHA4_OES, a width of 8192, height of 1, and a payload crafted to cleanly overwrite target objects during the OOB write phase.
  5. 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.

View on issue tracker