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

Overview

High
Severity
8.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactBuffer overflow in ANGLE
DescriptionBuffer overflow in ANGLE
ComponentANGLE
Bug ClassOOB
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker536581050
Fix commit8bbfb03f8fce (angle/angle) +28/-22
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-07-19 (per vendor advisory)
Fix landed2026-08-06 (public git)
Disclosed2026-08-18
Patch gap12 days — fix public in source before the advisory

Background

ANGLE internal format table
a generated/hand-maintained map describing every GL/ANGLE internal format, including per-component bit depths and the total pixelBytes (bytes per pixel) used to compute buffer sizes and strides.
`R10X6G10X6B10X6A10X6_UNORM` (`GL_R10X6G10X6B10X6A10X6_UNORM_ANGLEX`)
a 64-bit-per-pixel format with four 10-bit components, each followed by 6 bits of padding (24 padding bits total), corresponding to VK_FORMAT_R10X6G10X6B10X6A10X6_UNORM_4PACK16 (8 bytes/pixel).
`AddRGBAFormat` vs. `AddRGBAXFormat`
table-registration helpers; the X variant takes an FB<...>() bit-field spec that includes an explicit shared/padding-bits parameter, so padding contributes to the computed pixel size.
`angle_format.py` `get_component_sizes`
the code-generation helper that parses a format name’s channel tokens (e.g. R10, X6) into a per-letter bit map that determines the generated pixelBytes.

Root Cause Analysis

The format R10X6G10X6B10X6A10X6 was registered in formatutils.cpp via AddRGBAFormat(..., 10, 10, 10, 10, 0, ...), which accounts only for the four 10-bit color components and ignores the four 6-bit padding fields, so the computed pixel size undercounted the real 64-bit (8-byte) layout — reported as 5 bytes when queried from a vk::Format. Separately, the generator angle_format.py computed component bits with bits[token[0]] = int(token[1:]), which overwrites rather than accumulates repeated tokens, so the four X6 padding tokens collapsed to a single X6 (6 bits) instead of X24, again undersizing the format in Format_table_autogen.cpp (the R10X6... row’s pixelBytes was 6, and the related G10X6_B10X6R10X6_2PLANE_420_UNORM_3PACK16 row was 5). Because ANGLE uses this pixel size to compute allocation sizes, row strides, and copy extents, an undersized value causes buffers/strides to be smaller than the data actually written, producing a buffer overflow.

The fix registers the format with AddRGBAXFormat(..., FB<10,10,10,10,24,0>(), ...) so the 24 padding bits count toward the size (8 bytes), and changes the generator to bits[token[0]] = bits.get(token[0], 0) + int(token[1:]) so repeated padding tokens accumulate; the regenerated table then records 8 for R10X6G10X6B10X6A10X6_UNORM and 6 for G10X6_B10X6R10X6_2PLANE_420_UNORM_3PACK16.

Key insight
The single mistake was omitting each component’s 6 bits of padding from the format’s pixel-size — both in the hand-written AddRGBAFormat registration and in the token parser that overwrote instead of summing repeated X6 tokens — so the recorded byte size (5–6) was smaller than the true 8 bytes; the fix counts the padding via AddRGBAXFormat/FB<...,24,0>() and accumulating token bits.

Attack Path

  1. Use the mis-sized format Content (e.g. a WebGL/WebGPU-reachable path or texture-upload operation) creates or operates on a texture/image using GL_R10X6G10X6B10X6A10X6_UNORM_ANGLEX, whose ANGLE pixel size is recorded as 5–6 bytes instead of 8.
  2. Size a buffer/stride from the wrong pixel size ANGLE computes an allocation size, row stride, or copy extent from the undersized pixelBytes, producing a backing buffer smaller than the real 8-byte-per-pixel data.
  3. Write real 64-bit pixels When the driver/ANGLE reads or writes the actual R10X6G10X6B10X6A10X6 data (8 bytes/pixel), the transfer runs past the end of the undersized buffer.
  4. Buffer overflow The mismatch between the true 8-byte pixel stride and the 5–6-byte accounting yields an out-of-bounds read/write in the GPU process (inferred as to the exact sink; the diff shows only the corrected size, not a specific overflowing memcpy).

Impact Assessment

An attacker who can drive ANGLE to use the R10X6G10X6B10X6A10X6_UNORM format can cause buffer size/stride computations to undercount the pixel size, leading to a buffer overflow in the GPU process. The advisory classifies this as a buffer overflow in ANGLE; no exploited-in-the-wild status is stated, and any escalation to a controllable read/write primitive or code execution is (inferred). The precondition is reachability of this specific 10-bit-padded format through a rendering/upload path on a backend that exposes it.

Files Changed

  • scripts/code_generation_hashes/Vulkan_format.json
  • scripts/code_generation_hashes/D3D11_format.json
  • scripts/code_generation_hashes/GL_copy_conversion_table.json
  • scripts/code_generation_hashes/DXGI_format.json
  • scripts/code_generation_hashes/WebGPU_format.json
  • src/libANGLE/formatutils.cpp
  • scripts/code_generation_hashes/ANGLE_format.json
  • scripts/code_generation_hashes/Metal_default_shaders.json
  • src/libANGLE/renderer/metal/mtl_format_table_autogen.mm
  • scripts/code_generation_hashes/OpenGL_dispatch_table.json
  • scripts/code_generation_hashes/Metal_format_table.json
  • scripts/code_generation_hashes/ANGLE_load_functions_table.json
  • scripts/code_generation_hashes/GL_format_map.json
  • src/libANGLE/renderer/angle_format.py
  • src/libANGLE/renderer/Format_table_autogen.cpp
  • scripts/code_generation_hashes/Vulkan_mandatory_format_support_table.json

Audit Directions

  • Padding/shared bits omitted from pixel size
    Search for packed formats with X/padding channels registered via AddRGBAFormat (no explicit padding parameter) where the total bit count is less than the format’s true storage width.
  • Overwrite-vs-accumulate in name parsers
    Audit generators/parsers that map repeated tokens or channels into a keyed structure (map[key] = value) to ensure repeated keys accumulate rather than clobber, especially for multi-planar or multiply-padded formats.
  • Cross-check format sizes against native APIs
    Compare ANGLE’s recorded pixelBytes/bit depths against the corresponding Vulkan/D3D/Metal format’s documented size (as done here vs. VK_FORMAT_..._4PACK16) to find other under- or over-sized format table entries.
From 8bbfb03f8fce362af0893d83bcf362b2cd08ac2a Mon Sep 17 00:00:00 2001
From: Amirali Abdolrashidi <abdolrashidi@google.com>
Date: Wed, 05 Aug 2026 13:59:45 -0700
Subject: [PATCH] Fix pixel size of RGBA10X6

  The format R10X6G10X6B10X6A10X6 is a 64-bit format with four 10-bit
components, each followed by 6 bits of padding. However, in the current
initialization in the internal format map, this format does not take
the padding spaces into account, resulting in the pixel size becoming
5 bytes (when inquired from a vk::Format object).

This change will apply the change to make sure the size of this format
is correctly set to 8 bytes.

* Updated the internal format map for the following format:
  GL_R10X6G10X6B10X6A10X6_UNORM_ANGLEX
  * (Corresponds to VK_FORMAT_R10X6G10X6B10X6A10X6_UNORM_4PACK16)
  * The padding is taken into account for all components, making the
    size of this format 8 bytes.

* Updated the script that generates the format table to fix the size
  of R10X6G10X6B10X6A10X6_UNORM.
  * (The size of format G10X6_B10X6R10X6_2PLANE_420_UNORM_3PACK16 was
    also fixed from 5 to 6 as a result.)

Bug: angleproject:409355677
Bug: chromium:536581050
Change-Id: I537522bb528ac6ded913ea44ece918dcd7fb13b8
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/8199952
Reviewed-by: Shahbaz Youssefi <syoussefi@chromium.org>
Reviewed-by: Yuxin Hu <yuxinhu@google.com>
Commit-Queue: Amirali Abdolrashidi <abdolrashidi@google.com>
---
diff --git a/scripts/code_generation_hashes/ANGLE_format.json b/scripts/code_generation_hashes/ANGLE_format.json
index e949b15..5a1b819 100644
--- a/scripts/code_generation_hashes/ANGLE_format.json
+++ b/scripts/code_generation_hashes/ANGLE_format.json
@@ -2,9 +2,9 @@
   "src/libANGLE/renderer/FormatID_autogen.h":
     "2b5ac80521dc3ea477b9c748d48da95d",
   "src/libANGLE/renderer/Format_table_autogen.cpp":
-    "afcde858216b9e9a771b4867863415bd",
+    "80a6465f6125501d0b272b94041eaeb0",
   "src/libANGLE/renderer/angle_format.py":
-    "49c55a74e763b1cfd0d96c4b49b4e402",
+    "ad644811f7f2e1d0b4947534f839a11b",
   "src/libANGLE/renderer/angle_format_data.json":
     "f82d30f65cebd8c787ac0d29fa4c0221",
   "src/libANGLE/renderer/angle_format_map.json":
diff --git a/scripts/code_generation_hashes/ANGLE_load_functions_table.json b/scripts/code_generation_hashes/ANGLE_load_functions_table.json
index 9f90b2c5..a06aa16 100644
--- a/scripts/code_generation_hashes/ANGLE_load_functions_table.json
+++ b/scripts/code_generation_hashes/ANGLE_load_functions_table.json
@@ -1,6 +1,6 @@
 {
   "src/libANGLE/renderer/angle_format.py":
-    "49c55a74e763b1cfd0d96c4b49b4e402",
+    "ad644811f7f2e1d0b4947534f839a11b",
   "src/libANGLE/renderer/gen_load_functions_table.py":
     "10e66c76a350a1c70fadd2e10a28981e",
   "src/libANGLE/renderer/load_functions_data.json":
diff --git a/scripts/code_generation_hashes/D3D11_format.json b/scripts/code_generation_hashes/D3D11_format.json
index 4dbdd3a..3abe5c6 100644
--- a/scripts/code_generation_hashes/D3D11_format.json
+++ b/scripts/code_generation_hashes/D3D11_format.json
@@ -1,6 +1,6 @@
 {
   "src/libANGLE/renderer/angle_format.py":
-    "49c55a74e763b1cfd0d96c4b49b4e402",
+    "ad644811f7f2e1d0b4947534f839a11b",
   "src/libANGLE/renderer/d3d/d3d11/gen_texture_format_table.py":
     "c4257516b0b8430d745f81b7307174ad",
   "src/libANGLE/renderer/d3d/d3d11/texture_format_data.json":
diff --git a/scripts/code_generation_hashes/DXGI_format.json b/scripts/code_generation_hashes/DXGI_format.json
index 214b818..6a58b2a 100644
--- a/scripts/code_generation_hashes/DXGI_format.json
+++ b/scripts/code_generation_hashes/DXGI_format.json
@@ -1,6 +1,6 @@
 {
   "src/libANGLE/renderer/angle_format.py":
-    "49c55a74e763b1cfd0d96c4b49b4e402",
+    "ad644811f7f2e1d0b4947534f839a11b",
   "src/libANGLE/renderer/angle_format_map.json":
     "abad08e462a0839d1600d83f83bcad8b",
   "src/libANGLE/renderer/dxgi_format_data.json":
diff --git a/scripts/code_generation_hashes/GL_copy_conversion_table.json b/scripts/code_generation_hashes/GL_copy_conversion_table.json
index af3566a..a2d2ea2 100644
--- a/scripts/code_generation_hashes/GL_copy_conversion_table.json
+++ b/scripts/code_generation_hashes/GL_copy_conversion_table.json
@@ -6,5 +6,5 @@
   "src/libANGLE/gen_copy_conversion_table.py":
     "3b1f0182a41af78b44930fb415ff0fb6",
   "src/libANGLE/renderer/angle_format.py":
-    "49c55a74e763b1cfd0d96c4b49b4e402"
+    "ad644811f7f2e1d0b4947534f839a11b"
 }
diff --git a/scripts/code_generation_hashes/GL_format_map.json b/scripts/code_generation_hashes/GL_format_map.json
index a01ecac..56c4854 100644
--- a/scripts/code_generation_hashes/GL_format_map.json
+++ b/scripts/code_generation_hashes/GL_format_map.json
@@ -8,5 +8,5 @@
   "src/libANGLE/gen_format_map.py":
     "32b43aedcbad2aa5eaa47052cc8573ac",
   "src/libANGLE/renderer/angle_format.py":
-    "49c55a74e763b1cfd0d96c4b49b4e402"
+    "ad644811f7f2e1d0b4947534f839a11b"
 }
diff --git a/scripts/code_generation_hashes/Metal_default_shaders.json b/scripts/code_generation_hashes/Metal_default_shaders.json
index 71c7fbc..9ac6063 100644
--- a/scripts/code_generation_hashes/Metal_default_shaders.json
+++ b/scripts/code_generation_hashes/Metal_default_shaders.json
@@ -1,6 +1,6 @@
 {
   "src/libANGLE/renderer/angle_format.py":
-    "49c55a74e763b1cfd0d96c4b49b4e402",
+    "ad644811f7f2e1d0b4947534f839a11b",
   "src/libANGLE/renderer/angle_format_map.json":
     "abad08e462a0839d1600d83f83bcad8b",
   "src/libANGLE/renderer/gen_angle_format_table.py":
diff --git a/scripts/code_generation_hashes/Metal_format_table.json b/scripts/code_generation_hashes/Metal_format_table.json
index a262947..42cc71d 100644
--- a/scripts/code_generation_hashes/Metal_format_table.json
+++ b/scripts/code_generation_hashes/Metal_format_table.json
@@ -1,6 +1,6 @@
 {
   "src/libANGLE/renderer/angle_format.py":
-    "49c55a74e763b1cfd0d96c4b49b4e402",
+    "ad644811f7f2e1d0b4947534f839a11b",
   "src/libANGLE/renderer/angle_format_map.json":
     "abad08e462a0839d1600d83f83bcad8b",
   "src/libANGLE/renderer/metal/gen_mtl_format_table.py":
@@ -8,5 +8,5 @@
   "src/libANGLE/renderer/metal/mtl_format_map.json":
     "cb72edc9237cf806792b2e4066a27627",
   "src/libANGLE/renderer/metal/mtl_format_table_autogen.mm":
-    "5e518281148d86f1fb0b90dd5d548e26"
+    "6d7764d3ce7fcde1fcab38755e7fc13e"
 }
diff --git a/scripts/code_generation_hashes/OpenGL_dispatch_table.json b/scripts/code_generation_hashes/OpenGL_dispatch_table.json
index 0deced8..872fc3d 100644
--- a/scripts/code_generation_hashes/OpenGL_dispatch_table.json
+++ b/scripts/code_generation_hashes/OpenGL_dispatch_table.json
@@ -1,6 +1,6 @@
 {
   "src/libANGLE/renderer/angle_format.py":
-    "49c55a74e763b1cfd0d96c4b49b4e402",
+    "ad644811f7f2e1d0b4947534f839a11b",
   "src/libANGLE/renderer/gl/DispatchTableGL_autogen.cpp":
     "63851b2d41e02271ba657de853d58fdc",
   "src/libANGLE/renderer/gl/DispatchTableGL_autogen.h":
diff --git a/scripts/code_generation_hashes/Vulkan_format.json b/scripts/code_generation_hashes/Vulkan_format.json
index 82fccd2..4b22917 100644
--- a/scripts/code_generation_hashes/Vulkan_format.json
+++ b/scripts/code_generation_hashes/Vulkan_format.json
@@ -1,6 +1,6 @@
 {
   "src/libANGLE/renderer/angle_format.py":
-    "49c55a74e763b1cfd0d96c4b49b4e402",
+    "ad644811f7f2e1d0b4947534f839a11b",
   "src/libANGLE/renderer/angle_format_map.json":
     "abad08e462a0839d1600d83f83bcad8b",
   "src/libANGLE/renderer/vulkan/gen_vk_format_table.py":
diff --git a/scripts/code_generation_hashes/Vulkan_mandatory_format_support_table.json b/scripts/code_generation_hashes/Vulkan_mandatory_format_support_table.json
index 2f12f92..fa5a332 100644
--- a/scripts/code_generation_hashes/Vulkan_mandatory_format_support_table.json
+++ b/scripts/code_generation_hashes/Vulkan_mandatory_format_support_table.json
@@ -1,6 +1,6 @@
 {
   "src/libANGLE/renderer/angle_format.py":
-    "49c55a74e763b1cfd0d96c4b49b4e402",
+    "ad644811f7f2e1d0b4947534f839a11b",
   "src/libANGLE/renderer/vulkan/gen_vk_mandatory_format_support_table.py":
     "a9fd6f3ff2b584aff382364489146c76",
   "src/libANGLE/renderer/vulkan/vk_format_map.json":
diff --git a/scripts/code_generation_hashes/WebGPU_format.json b/scripts/code_generation_hashes/WebGPU_format.json
index 5380ce4..f2b684a 100644
--- a/scripts/code_generation_hashes/WebGPU_format.json
+++ b/scripts/code_generation_hashes/WebGPU_format.json
@@ -1,6 +1,6 @@
 {
   "src/libANGLE/renderer/angle_format.py":
-    "49c55a74e763b1cfd0d96c4b49b4e402",
+    "ad644811f7f2e1d0b4947534f839a11b",
   "src/libANGLE/renderer/angle_format_map.json":
     "abad08e462a0839d1600d83f83bcad8b",
   "src/libANGLE/renderer/wgpu/gen_wgpu_format_table.py":
diff --git a/src/libANGLE/formatutils.cpp b/src/libANGLE/formatutils.cpp
index 0e6d9b3..52331f9 100644
--- a/src/libANGLE/formatutils.cpp
+++ b/src/libANGLE/formatutils.cpp
@@ -1109,7 +1109,10 @@
     // Special format which is not really supported, so always false for all supports.
     AddRGBAFormat(&map, GL_BGR565_ANGLEX,     true,  5,  6,  5,  0, 0, GL_BGRA_EXT,     GL_UNSIGNED_SHORT_5_6_5,           GL_UNSIGNED_NORMALIZED, false, NeverSupported,                                    NeverSupported,  NeverSupported,                                    NeverSupported,                                NeverSupported);
     AddRGBAFormat(&map, GL_BGR10_A2_ANGLEX,   true, 10, 10, 10,  2, 0, GL_BGRA_EXT,     GL_UNSIGNED_INT_2_10_10_10_REV,    GL_UNSIGNED_NORMALIZED, false, NeverSupported,                                    NeverSupported,  NeverSupported,                                    NeverSupported,                                NeverSupported);
-    AddRGBAFormat(&map, GL_R10X6G10X6B10X6A10X6_UNORM_ANGLEX,   true, 10, 10, 10,  10, 0, GL_RGBA,    GL_UNSIGNED_SHORT,   GL_UNSIGNED_NORMALIZED, false, RequireES<3, 0>,                                    NeverSupported,  NeverSupported,                                    NeverSupported,                                NeverSupported);
+
+    // R10X6G10X6B10X6A10X6 has no explicit support on GL. Each component has 10 bits of data and
+    // 6 bits of padding (24 bits of padding in total).
+    AddRGBAXFormat(&map, GL_R10X6G10X6B10X6A10X6_UNORM_ANGLEX,   true, FB< 10,  10,  10,  10, 24, 0>(), GL_RGBA,    GL_UNSIGNED_SHORT,   GL_UNSIGNED_NORMALIZED, false, RequireES<3, 0>,                                    NeverSupported,  NeverSupported,                                    NeverSupported,                                NeverSupported);
 
     // Special format to emulate RGB8 with RGBA8 within ANGLE.
     AddRGBAXFormat(&map, GL_RGBX8_ANGLE,      true,   FB< 8,  8,  8,  0, 8, 0>(), GL_RGB,          GL_UNSIGNED_BYTE,                  GL_UNSIGNED_NORMALIZED, false, RequireExt<&Extensions::rgbxInternalFormatANGLE>,   AlwaysSupported, RequireExt<&Extensions::rgbxInternalFormatANGLE>,   RequireExt<&Extensions::rgbxInternalFormatANGLE>, NeverSupported);
diff --git a/src/libANGLE/renderer/angle_format.py b/src/libANGLE/renderer/angle_format.py
index 71f1aab..662abe4 100644
--- a/src/libANGLE/renderer/angle_format.py
+++ b/src/libANGLE/renderer/angle_format.py
@@ -134,7 +134,10 @@
     else:
         tokens = get_channel_tokens(format_id)
         for token in tokens:
-            bits[token[0]] = int(token[1:])
+            # Some padding tokens can appear multiple times in a format name
+            # (e.g. R10X6G10X6B10X6A10X6 has four X6 tokens). They should be
+            # accumulated for the correct pixelBytes value.
+            bits[token[0]] = bits.get(token[0], 0) + int(token[1:])
     return bits
 
 
diff --git a/src/libANGLE/renderer/Format_table_autogen.cpp b/src/libANGLE/renderer/Format_table_autogen.cpp
index 044cb15..ca352e3 100644
--- a/src/libANGLE/renderer/Format_table_autogen.cpp
+++ b/src/libANGLE/renderer/Format_table_autogen.cpp
@@ -132,7 +132,7 @@
     { FormatID::ETC2_R8G8B8A8_UNORM_BLOCK, GL_COMPRESSED_RGBA8_ETC2_EAC, GL_COMPRESSED_RGBA8_ETC2_EAC, nullptr, NoCopyFunctions, nullptr, nullptr, GL_UNSIGNED_NORMALIZED, 8, 8, 8, 8, 0, 0, 0, 16, 0, true, false, false, false, false, gl::VertexAttribType::UnsignedByte },
     { FormatID::ETC2_R8G8B8_SRGB_BLOCK, GL_COMPRESSED_SRGB8_ETC2, GL_COMPRESSED_SRGB8_ETC2, nullptr, NoCopyFunctions, nullptr, nullptr, GL_UNSIGNED_NORMALIZED, 8, 8, 8, 0, 0, 0, 0, 8, 0, true, false, false, true, false, gl::VertexAttribType::Byte },
     { FormatID::ETC2_R8G8B8_UNORM_BLOCK, GL_COMPRESSED_RGB8_ETC2, GL_COMPRESSED_RGB8_ETC2, nullptr, NoCopyFunctions, nullptr, nullptr, GL_UNSIGNED_NORMALIZED, 8, 8, 8, 0, 0, 0, 0, 8, 0, true, false, false, false, false, gl::VertexAttribType::UnsignedByte },
-    { FormatID::G10X6_B10X6R10X6_2PLANE_420_UNORM_3PACK16, GL_G10X6_B10X6R10X6_2PLANE_420_UNORM_3PACK16_ANGLE, GL_G10X6_B10X6R10X6_2PLANE_420_UNORM_3PACK16_ANGLE, GenerateMip<R8G8B8>, NoCopyFunctions, ReadColor<R8G8B8, GLfloat>, WriteColor<R8G8B8, GLfloat>, GL_UNSIGNED_NORMALIZED, 10, 10, 10, 0, 0, 0, 0, 5, std::numeric_limits<GLuint>::max(), false, false, false, false, true, gl::VertexAttribType::InvalidEnum },
+    { FormatID::G10X6_B10X6R10X6_2PLANE_420_UNORM_3PACK16, GL_G10X6_B10X6R10X6_2PLANE_420_UNORM_3PACK16_ANGLE, GL_G10X6_B10X6R10X6_2PLANE_420_UNORM_3PACK16_ANGLE, GenerateMip<R8G8B8>, NoCopyFunctions, ReadColor<R8G8B8, GLfloat>, WriteColor<R8G8B8, GLfloat>, GL_UNSIGNED_NORMALIZED, 10, 10, 10, 0, 0, 0, 0, 6, std::numeric_limits<GLuint>::max(), false, false, false, false, true, gl::VertexAttribType::InvalidEnum },
     { FormatID::G8_B8R8_2PLANE_420_UNORM, GL_G8_B8R8_2PLANE_420_UNORM_ANGLE, GL_G8_B8R8_2PLANE_420_UNORM_ANGLE, GenerateMip<R8G8B8>, NoCopyFunctions, ReadColor<R8G8B8, GLfloat>, WriteColor<R8G8B8, GLfloat>, GL_UNSIGNED_NORMALIZED, 8, 8, 8, 0, 0, 0, 0, 3, 0, false, false, false, false, true, gl::VertexAttribType::UnsignedByte },
     { FormatID::G8_B8_R8_3PLANE_420_UNORM, GL_G8_B8_R8_3PLANE_420_UNORM_ANGLE, GL_G8_B8_R8_3PLANE_420_UNORM_ANGLE, GenerateMip<R8G8B8>, NoCopyFunctions, ReadColor<R8G8B8, GLfloat>, WriteColor<R8G8B8, GLfloat>, GL_UNSIGNED_NORMALIZED, 8, 8, 8, 0, 0, 0, 0, 3, 0, false, false, false, false, true, gl::VertexAttribType::UnsignedByte },
     { FormatID::L16A16_FLOAT, GL_LUMINANCE_ALPHA16F_EXT, GL_LUMINANCE_ALPHA16F_EXT, GenerateMip<L16A16F>, NoCopyFunctions, ReadColor<L16A16F, GLfloat>, WriteColor<L16A16F, GLfloat>, GL_FLOAT, 0, 0, 0, 16, 16, 0, 0, 4, std::numeric_limits<GLuint>::max(), false, false, false, false, false, gl::VertexAttribType::Float },
@@ -167,7 +167,7 @@
     { FormatID::R10G10B10A2_UNORM, GL_RGB10_A2, GL_RGB10_A2, GenerateMip<R10G10B10A2>, NoCopyFunctions, ReadColor<R10G10B10A2, GLfloat>, WriteColor<R10G10B10A2, GLfloat>, GL_UNSIGNED_NORMALIZED, 10, 10, 10, 2, 0, 0, 0, 4, std::numeric_limits<GLuint>::max(), false, false, false, false, false, gl::VertexAttribType::UnsignedInt2101010 },
     { FormatID::R10G10B10A2_USCALED, GL_RGB10_A2_USCALED_ANGLEX, GL_RGB10_A2_USCALED_ANGLEX, GenerateMip<R10G10B10A2>, NoCopyFunctions, ReadColor<R10G10B10A2, GLuint>, WriteColor<R10G10B10A2, GLuint>, GL_UNSIGNED_INT, 10, 10, 10, 2, 0, 0, 0, 4, std::numeric_limits<GLuint>::max(), false, false, true, false, false, gl::VertexAttribType::UnsignedInt2101010 },
     { FormatID::R10G10B10X2_UNORM, GL_RGB10_EXT, GL_RGB10_EXT, GenerateMip<R10G10B10X2>, NoCopyFunctions, ReadColor<R10G10B10X2, GLfloat>, WriteColor<R10G10B10X2, GLfloat>, GL_UNSIGNED_NORMALIZED, 10, 10, 10, 0, 0, 0, 0, 4, std::numeric_limits<GLuint>::max(), false, false, false, false, false, gl::VertexAttribType::UnsignedInt2101010 },
-    { FormatID::R10X6G10X6B10X6A10X6_UNORM, GL_R10X6G10X6B10X6A10X6_UNORM_ANGLEX, GL_R10X6G10X6B10X6A10X6_UNORM_ANGLEX, GenerateMip<R10X6G10X6B10X6A10X6>, NoCopyFunctions, ReadColor<R10X6G10X6B10X6A10X6, GLfloat>, WriteColor<R10X6G10X6B10X6A10X6, GLfloat>, GL_UNSIGNED_NORMALIZED, 10, 10, 10, 10, 0, 0, 0, 6, std::numeric_limits<GLuint>::max(), false, false, false, false, false, gl::VertexAttribType::InvalidEnum },
+    { FormatID::R10X6G10X6B10X6A10X6_UNORM, GL_R10X6G10X6B10X6A10X6_UNORM_ANGLEX, GL_R10X6G10X6B10X6A10X6_UNORM_ANGLEX, GenerateMip<R10X6G10X6B10X6A10X6>, NoCopyFunctions, ReadColor<R10X6G10X6B10X6A10X6, GLfloat>, WriteColor<R10X6G10X6B10X6A10X6, GLfloat>, GL_UNSIGNED_NORMALIZED, 10, 10, 10, 10, 0, 0, 0, 8, std::numeric_limits<GLuint>::max(), false, false, false, false, false, gl::VertexAttribType::InvalidEnum },
     { FormatID::R11G11B10_FLOAT, GL_R11F_G11F_B10F, GL_R11F_G11F_B10F, GenerateMip<R11G11B10F>, NoCopyFunctions, ReadColor<R11G11B10F, GLfloat>, WriteColor<R11G11B10F, GLfloat>, GL_FLOAT, 11, 11, 10, 0, 0, 0, 0, 4, std::numeric_limits<GLuint>::max(), false, false, false, false, false, gl::VertexAttribType::Float },
     { FormatID::R16G16B16A16_FLOAT, GL_RGBA16F, GL_RGBA16F, GenerateMip<R16G16B16A16F>, NoCopyFunctions, ReadColor<R16G16B16A16F, GLfloat>, WriteColor<R16G16B16A16F, GLfloat>, GL_FLOAT, 16, 16, 16, 16, 0, 0, 0, 8, 1, false, false, false, false, false, gl::VertexAttribType::HalfFloat },
     { FormatID::R16G16B16A16_SINT, GL_RGBA16I, GL_RGBA16I, GenerateMip<R16G16B16A16S>, NoCopyFunctions, ReadColor<R16G16B16A16S, GLint>, WriteColor<R16G16B16A16S, GLint>, GL_INT, 16, 16, 16, 16, 0, 0, 0, 8, 1, false, false, false, false, false, gl::VertexAttribType::Short },
diff --git a/src/libANGLE/renderer/metal/mtl_format_table_autogen.mm b/src/libANGLE/renderer/metal/mtl_format_table_autogen.mm
index 7ec88c5..c9db638 100644
--- a/src/libANGLE/renderer/metal/mtl_format_table_autogen.mm
+++ b/src/libANGLE/renderer/metal/mtl_format_table_autogen.mm
@@ -3865,10 +3865,10 @@
     const angle::FeaturesMtl &featuresMtl = display->getFeatures();
     // Skip auto resolve if either hasDepth/StencilAutoResolve or allowMultisampleStoreAndResolve
     // feature are disabled.
-    bool supportDepthAutoResolve = featuresMtl.hasDepthAutoResolve.enabled &&
-                                   featuresMtl.allowMultisampleStoreAndResolve.enabled;
-    bool supportStencilAutoResolve = featuresMtl.hasStencilAutoResolve.enabled &&
-                                     featuresMtl.allowMultisampleStoreAndResolve.enabled;
+    bool supportDepthAutoResolve        = featuresMtl.hasDepthAutoResolve.enabled &&
+                                          featuresMtl.allowMultisampleStoreAndResolve.enabled;
+    bool supportStencilAutoResolve      = featuresMtl.hasStencilAutoResolve.enabled &&
+                                          featuresMtl.allowMultisampleStoreAndResolve.enabled;
     bool supportDepthStencilAutoResolve = supportDepthAutoResolve && supportStencilAutoResolve;
 
     // Source: https://developer.apple.com/metal/Metal-Feature-Set-Tables.pdf
Loading diff…

Original Bug Report

reported by aw...@chromium.org

Potential GPU-Host-OOB write in ANGLE Vulkan backend via R10X6_4PACK16 table-data error

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: A format generation bug in ANGLE miscalculates the pixel size of R10X6G10X6B10X6A10X6_UNORM as 6 bytes instead of 8. When a compromised Android renderer forces a CPU-readback copy of an imported hardware buffer, ANGLE allocates a 33% undersized staging buffer. The Vulkan driver then writes 8 bytes per texel, causing an out-of-bounds write in the unsandboxed GPU process.

Affected files:

  • third_party/angle/src/libANGLE/renderer/Format_table_autogen.cpp
  • third_party/angle/src/libANGLE/renderer/angle_format.py
  • third_party/angle/src/libANGLE/renderer/gen_angle_format_table.py
  • third_party/angle/src/libANGLE/formatutils.cpp
  • third_party/angle/src/libANGLE/renderer/vulkan/vk_helpers.cpp

Estimated timestamp from git blame: 2025-04-23

1. Summary of the Issue (Meant for Human Triage)

A potential critical table-data generation error exists in ANGLE’s autogenerated format tables for the R10X6G10X6B10X6A10X6_UNORM format (which maps to Vulkan’s VK_FORMAT_R10X6G10X6B10X6A10X6_UNORM_4PACK16).

The true physical layout of this format utilizes 16-bit channels with 10 bits of color data and 6 bits of padding per channel, meaning 4 channels × 2 bytes/channel = 8 bytes per texel. However, due to a parser bug in angle_format.py, multiple X6 padding tokens collapse into a single dictionary entry. This causes the generator to compute a pixelBytes value of 6 instead of 8 in Format_table_autogen.cpp.

A compromised renderer can exploit this by importing an AHARDWAREBUFFER_FORMAT_R10G10B10A10_UNORM Android Hardware Buffer (AHB) and intentionally misreporting its format as kRGBA_8888. When the renderer subsequently triggers a CopySubTextureCHROMIUM command, ANGLE’s Vulkan backend falls back to a CPU-readback path. The staging buffer allocation is calculated using the incorrect pixelBytes = 6 value. The Vulkan driver then executes vkCmdCopyImageToBuffer using the hardware’s physical texel block size of 8 bytes.

This mismatch results in a 33% out-of-bounds (OOB) write of GPU-writable memory (up to 2 * width * height bytes) into adjacent suballocations of ANGLE’s shared host-mapped staging BufferPool. Because the GPU process is unsandboxed on Android, this heap OOB write represents a potential sandbox escape vulnerability.


2. Proof-of-Concept & Detailed Execution Flow

Note: These are suggested/potential steps to trigger the vulnerability, as our tooling agent doesn’t yet have the ability to run code. They trace the precise, confirmed logic paths in the current codebase.

The Vulnerability Trigger Path (A-RENDERER to GPU-Host-OOB Write)

  1. AHB Allocation: The attacker, acting as a compromised renderer process on an Android device (API 34+), allocates an Android Hardware Buffer (AHardwareBuffer) using AHardwareBuffer_allocate. They configure the AHardwareBuffer_Desc with format AHARDWAREBUFFER_FORMAT_R10G10B10A10_UNORM (0x3b), a target width and height (e.g., 1024x1024), and usage flags including AHARDWAREBUFFER_USAGE_GPU_FRAMEBUFFER and AHARDWAREBUFFER_USAGE_CPU_WRITE_RARELY. Crucially, they omit the AHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGE flag.
  2. Attacker Payload: The attacker locks the AHardwareBuffer memory and writes attacker-controlled payload data into the pixel buffer.
  3. IPC Forgery & Lack of Validation: The attacker wraps the AHardwareBuffer in a gfx::GpuMemoryBufferHandle and invokes gpu.mojom::SharedImageInterface::CreateSharedImage over IPC. They deliberately misreport the SharedImageFormat as viz::SinglePlaneFormat::kRGBA_8888.
    • In the GPU process, AHardwareBufferImageBackingFactory::CreateSharedImage (gpu/command_buffer/service/shared_image/ahardwarebuffer_image_backing_factory.cc:1102-1131) receives the request. It performs basic parameter validation but does not query the underlying AHardwareBuffer_Desc.format to cross-check against the attacker-supplied si_info.format. The backing is created successfully.
  4. Texture Storage Import: The attacker sends gles2::cmds::CreateAndTexStorage2DSharedImageINTERNAL, supplying the SharedImage mailbox. This triggers ANGLE to import the AHardwareBuffer.
    • In HardwareBufferImageSiblingVkAndroid::initImpl (third_party/angle/src/libANGLE/renderer/vulkan/android/HardwareBufferImageSiblingVkAndroid.cpp:314), vkGetAndroidHardwareBufferPropertiesANDROID retrieves the native Vulkan properties, populating bufferFormatProperties.format with VK_FORMAT_R10X6G10X6B10X6A10X6_UNORM_4PACK16.
    • CheckFormatSupport confirms the format is natively supported by the driver, setting isExternal to false.
    • The !isExternal code path (HardwareBufferImageSiblingVkAndroid.cpp:385-388) resolves the actual ANGLE format via vk::GetFormatIDFromVkFormat, which maps directly to angle::FormatID::R10X6G10X6B10X6A10X6_UNORM (third_party/angle/src/libANGLE/renderer/vulkan/vk_format_table_autogen.cpp:3502-3503).
    • AhbDescUsageToVkImageUsage is called (HardwareBufferImageSiblingVkAndroid.cpp:78-107). Because AHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGE was omitted, VK_IMAGE_USAGE_SAMPLED_BIT is omitted from the image usage. (On non-Samsung devices, forceSampleUsageForAhbBackedImages is disabled, ensuring this bit remains unset).
  5. Forcing CPU Fallback: The attacker creates a standard RGBA8 destination texture and issues gles2::cmds::CopySubTextureCHROMIUM to copy from the imported AHB to the RGBA8 texture.
    • The command reaches TextureVk::copySubTextureImpl (third_party/angle/src/libANGLE/renderer/vulkan/TextureVk.cpp:1726).
    • CanCopyWithTransferForCopyTexture fails because the formats differ.
    • CanCopyWithDraw fails because it checks (srcUsage & VK_IMAGE_USAGE_SAMPLED_BIT) != 0 (TextureVk.cpp:177-188).
    • ANGLE falls back to the CPU readback path: copyImageDataToBufferAndGetData -> ImageHelper::copyImageDataToBuffer (third_party/angle/src/libANGLE/renderer/vulkan/vk_helpers.cpp:10989).
  6. The Overrun Sink (ImageHelper::copyImageDataToBuffer):
    • ANGLE retrieves the angle::Format structure. Due to a generator bug in angle_format.py (lines 135-137), multiple X6 padding tokens collapse into a single dictionary entry bits['X'] = 6. This causes the autogenerated table Format_table_autogen.cpp:170 to incorrectly define pixelBytes as 6 instead of the actual physical block size of 8.
    • The staging buffer size is computed using the incorrect pixelBytes: bufferSize = width * height * depth * 6 * layerCount (vk_helpers.cpp:11007-11009). For a 1024x1024 texture, bufferSize is exactly 6,291,456 bytes.
    • contextVk->initBufferForImageCopy allocates roundUp(6291456 + 12, 12) bytes from getDefaultBufferPool(). This pool returns a suballocation out of a massive host-mapped VkBuffer shared with other concurrent GPU-to-CPU operations (ContextVk.cpp:7013-7035).
    • copyImageDataToBuffer configures a VkBufferImageCopy structure with regions.bufferRowLength = 0 (tightly packed), and invokes commandBuffer->copyImageToBuffer.
    • The Vulkan driver executes the tightly packed copy. Because the hardware format natively demands 8 bytes per texel block, the driver copies 1024 * 1024 * 8 = 8,388,608 bytes of attacker-controlled pixel data.
    • Since the allocated suballocation is only ~6.29 MB, the driver writes over 2 megabytes out-of-bounds into adjacent suballocations in the shared BufferPool, providing a deterministic GPU-Host-OOB write primitive.

Proposed Fix

  1. Format Generator (angle_format.py): Modify get_bits to handle multiple padding tokens without collapsing them into a single dictionary key (e.g., track bits in a list or uniquely identify each padding token like X0, X1).
  2. AHB Format Cross-validation: Update AHardwareBufferImageBackingFactory::CreateSharedImage to call AHardwareBuffer_describe() on the imported buffer and reject it if the underlying hardware format is incompatible with the requested viz::SharedImageFormat.
  3. Audit GL Internal Format: Ensure gl::InternalFormat::pixelBytes resolves correctly for GL_R10X6G10X6B10X6A10X6_UNORM_ANGLEX in formatutils.cpp.

3. Technical Verification Details (Automated Audit Logs)

> * Severity: High (S1) > * Brief Notes / Reasoning: > The vulnerability is a valid and highly exploitable GPU-HOST-OOB write in the ANGLE Vulkan backend, leading to memory corruption in the GPU process. The root cause is a parsing bug in angle_format.py where multiple X6 tokens collapse into a single dictionary entry (bits['X'] = 6), incorrectly calculating pixelBytes = 6 instead of 8 for R10X6G10X6B10X6A10X6_UNORM. >
> A compromised renderer (A-RENDERER) on Android can allocate an AHARDWAREBUFFER_FORMAT_R10G10B10A10_UNORM AHB, import it via SharedImageInterface claiming it’s kRGBA_8888 (since AHardwareBufferImageBackingFactory lacks format cross-validation), and invoke CopySubTextureCHROMIUM. Because the formats mismatch and the AHB lacks SAMPLED_BIT, ANGLE falls back to the CPU copy path (copyImageDataToBufferAndGetData). >
> During CPU fallback, ImageHelper::copyImageDataToBuffer allocates a host-mapped staging buffer suballocation in the shared BufferPool based on the incorrect pixelBytes = 6 (size ≈ W * H * 6). However, the Vulkan driver’s vkCmdCopyImageToBuffer executes the copy using the physical format’s true texel block size of 8 bytes. This results in a 33% (W * H * 2 bytes) out-of-bounds write of attacker-controlled pixel data into adjacent suballocations of the staging pool. >
> Since the GPU process is unsandboxed on Android, this memory corruption serves as a sandbox escape. MiraclePtr does not mitigate this, as the corruption occurs in host-mapped VkDeviceMemory managed by ANGLE’s BufferPool, not PartitionAlloc. Following the guidelines for memory corruption in an unsandboxed process (GPU on Android) reachable from a compromised renderer, this warrants a High (S1) severity.

Exhaustive Evidence Ledger:

  • Generator Bug Proof: Codebase Investigator confirmed angle_format.py lines 135-137 execute bits[token[0]] = int(token[1:]). For R10X6G10X6B10X6A10X6, the regex yields ['R10', 'X6', 'G10', 'X6', 'B10', 'X6', 'A10', 'X6']. The loop dictates bits['X'] = 6, overwriting itself 4 times. gen_angle_format_table.py sums kChannels to 46, creating pixelBytes = ceil(46/8) = 6. This is explicitly emitted in Format_table_autogen.cpp:170.
  • VkFormat Mapping: Confirmed via Codebase Investigator at third_party/angle/src/libANGLE/renderer/vulkan/vk_format_table_autogen.cpp:3502-3503: case VK_FORMAT_R10X6G10X6B10X6A10X6_UNORM_4PACK16: correctly maps to angle::FormatID::R10X6G10X6B10X6A10X6_UNORM.
  • AHB Import & Format Validation: Checked HardwareBufferImageSiblingVkAndroid::initImpl. Investigator verified that vkGetAndroidHardwareBufferPropertiesANDROID chains and populates bufferFormatProperties.format. If native formats are natively supported (formatHasNecessaryFormatSupport == true), the execution takes the !isExternal branch (:385-388), completely bypassing NativePixelFormatToGLInternalFormat. This confirms how an undocumented AHB format reaches internal representation unaltered.
  • Absence of SharedImage Format Validation: Investigated gpu/command_buffer/service/shared_image/ahardwarebuffer_image_backing_factory.cc:1102-1131. Confirmed AHardwareBufferImageBackingFactory::CreateSharedImage does not invoke AHardwareBuffer_describe to cross-check AHardwareBuffer_Desc.format against si_info.format.
  • Buffer Copy Alignment Logic: Evaluated GetImageCopyBufferAlignment in third_party/angle/src/libANGLE/renderer/vulkan/vk_format_utils.cpp:375-404. When pixelBytes is 6, the multiplier calculates as 6 % 2 != 0 ? 4 : 6 % 4 != 0 ? 2 : 1, resulting in multiplier = 2. The returned alignment is 2 * 6 = 12. This proves that the alignment padding logic is insufficient to accommodate the 33% (WH2 bytes) undersized calculation.
  • Environmental Prerequisites:
    • Android API 34+ is required for AHARDWAREBUFFER_FORMAT_R10G10B10A10_UNORM support (Hex 0x3b).
    • Requires a non-Samsung Vulkan driver to ensure forceSampleUsageForAhbBackedImages is false (vk_renderer.cpp:6868), preventing CanCopyWithDraw from running.

Evaluated with Chrome root at commit: b96d2ec58f4f5f92b540a723966b199d6e9951b4


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