CVE-2026-76046
Overview
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
Xvariant takes anFB<...>()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 generatedpixelBytes.
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.
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
- 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. - 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. - 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.
- 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
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.jsonscripts/code_generation_hashes/D3D11_format.jsonscripts/code_generation_hashes/GL_copy_conversion_table.jsonscripts/code_generation_hashes/DXGI_format.jsonscripts/code_generation_hashes/WebGPU_format.jsonsrc/libANGLE/formatutils.cppscripts/code_generation_hashes/ANGLE_format.jsonscripts/code_generation_hashes/Metal_default_shaders.jsonsrc/libANGLE/renderer/metal/mtl_format_table_autogen.mmscripts/code_generation_hashes/OpenGL_dispatch_table.jsonscripts/code_generation_hashes/Metal_format_table.jsonscripts/code_generation_hashes/ANGLE_load_functions_table.jsonscripts/code_generation_hashes/GL_format_map.jsonsrc/libANGLE/renderer/angle_format.pysrc/libANGLE/renderer/Format_table_autogen.cppscripts/code_generation_hashes/Vulkan_mandatory_format_support_table.json
Audit Directions
- Padding/shared bits omitted from pixel sizeSearch for packed formats with
X/padding channels registered viaAddRGBAFormat(no explicit padding parameter) where the total bit count is less than the format’s true storage width. - Overwrite-vs-accumulate in name parsersAudit 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 APIsCompare 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.
Patch
From 8bbfb03f8fce362af0893d83bcf362b2cd08ac2a Mon Sep 17 00:00:00 2001 From: Amirali Abdolrashidi <[email protected]> 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 <[email protected]> Reviewed-by: Yuxin Hu <[email protected]> Commit-Queue: Amirali Abdolrashidi <[email protected]> --- 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
Original Bug Report
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.cppthird_party/angle/src/libANGLE/renderer/angle_format.pythird_party/angle/src/libANGLE/renderer/gen_angle_format_table.pythird_party/angle/src/libANGLE/formatutils.cppthird_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)
- AHB Allocation: The attacker, acting as a compromised renderer process on an Android device (API 34+), allocates an Android Hardware Buffer (
AHardwareBuffer) usingAHardwareBuffer_allocate. They configure theAHardwareBuffer_Descwith formatAHARDWAREBUFFER_FORMAT_R10G10B10A10_UNORM(0x3b), a target width and height (e.g., 1024x1024), and usage flags includingAHARDWAREBUFFER_USAGE_GPU_FRAMEBUFFERandAHARDWAREBUFFER_USAGE_CPU_WRITE_RARELY. Crucially, they omit theAHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGEflag. - Attacker Payload: The attacker locks the
AHardwareBuffermemory and writes attacker-controlled payload data into the pixel buffer. - IPC Forgery & Lack of Validation: The attacker wraps the
AHardwareBufferin agfx::GpuMemoryBufferHandleand invokesgpu.mojom::SharedImageInterface::CreateSharedImageover IPC. They deliberately misreport theSharedImageFormatasviz::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 underlyingAHardwareBuffer_Desc.formatto cross-check against the attacker-suppliedsi_info.format. The backing is created successfully.
- In the GPU process,
- Texture Storage Import: The attacker sends
gles2::cmds::CreateAndTexStorage2DSharedImageINTERNAL, supplying theSharedImagemailbox. This triggers ANGLE to import theAHardwareBuffer.- In
HardwareBufferImageSiblingVkAndroid::initImpl(third_party/angle/src/libANGLE/renderer/vulkan/android/HardwareBufferImageSiblingVkAndroid.cpp:314),vkGetAndroidHardwareBufferPropertiesANDROIDretrieves the native Vulkan properties, populatingbufferFormatProperties.formatwithVK_FORMAT_R10X6G10X6B10X6A10X6_UNORM_4PACK16. CheckFormatSupportconfirms the format is natively supported by the driver, settingisExternalto false.- The
!isExternalcode path (HardwareBufferImageSiblingVkAndroid.cpp:385-388) resolves the actual ANGLE format viavk::GetFormatIDFromVkFormat, which maps directly toangle::FormatID::R10X6G10X6B10X6A10X6_UNORM(third_party/angle/src/libANGLE/renderer/vulkan/vk_format_table_autogen.cpp:3502-3503). AhbDescUsageToVkImageUsageis called (HardwareBufferImageSiblingVkAndroid.cpp:78-107). BecauseAHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGEwas omitted,VK_IMAGE_USAGE_SAMPLED_BITis omitted from the image usage. (On non-Samsung devices,forceSampleUsageForAhbBackedImagesis disabled, ensuring this bit remains unset).
- In
- Forcing CPU Fallback: The attacker creates a standard
RGBA8destination texture and issuesgles2::cmds::CopySubTextureCHROMIUMto copy from the imported AHB to theRGBA8texture.- The command reaches
TextureVk::copySubTextureImpl(third_party/angle/src/libANGLE/renderer/vulkan/TextureVk.cpp:1726). CanCopyWithTransferForCopyTexturefails because the formats differ.CanCopyWithDrawfails 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).
- The command reaches
- The Overrun Sink (
ImageHelper::copyImageDataToBuffer):- ANGLE retrieves the
angle::Formatstructure. Due to a generator bug inangle_format.py(lines 135-137), multipleX6padding tokens collapse into a single dictionary entrybits['X'] = 6. This causes the autogenerated tableFormat_table_autogen.cpp:170to incorrectly definepixelBytesas 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,bufferSizeis exactly 6,291,456 bytes. contextVk->initBufferForImageCopyallocatesroundUp(6291456 + 12, 12)bytes fromgetDefaultBufferPool(). This pool returns a suballocation out of a massive host-mappedVkBuffershared with other concurrent GPU-to-CPU operations (ContextVk.cpp:7013-7035).copyImageDataToBufferconfigures aVkBufferImageCopystructure withregions.bufferRowLength = 0(tightly packed), and invokescommandBuffer->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,608bytes 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.
- ANGLE retrieves the
Proposed Fix
- Format Generator (
angle_format.py): Modifyget_bitsto 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 likeX0,X1). - AHB Format Cross-validation: Update
AHardwareBufferImageBackingFactory::CreateSharedImageto callAHardwareBuffer_describe()on the imported buffer and reject it if the underlying hardware format is incompatible with the requestedviz::SharedImageFormat. - Audit GL Internal Format: Ensure
gl::InternalFormat::pixelBytesresolves correctly forGL_R10X6G10X6B10X6A10X6_UNORM_ANGLEXinformatutils.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.pylines 135-137 executebits[token[0]] = int(token[1:]). ForR10X6G10X6B10X6A10X6, the regex yields['R10', 'X6', 'G10', 'X6', 'B10', 'X6', 'A10', 'X6']. The loop dictatesbits['X'] = 6, overwriting itself 4 times.gen_angle_format_table.pysumskChannelsto 46, creatingpixelBytes = ceil(46/8) = 6. This is explicitly emitted inFormat_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 toangle::FormatID::R10X6G10X6B10X6A10X6_UNORM. - AHB Import & Format Validation: Checked
HardwareBufferImageSiblingVkAndroid::initImpl. Investigator verified thatvkGetAndroidHardwareBufferPropertiesANDROIDchains and populatesbufferFormatProperties.format. If native formats are natively supported (formatHasNecessaryFormatSupport== true), the execution takes the!isExternalbranch (:385-388), completely bypassingNativePixelFormatToGLInternalFormat. 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. ConfirmedAHardwareBufferImageBackingFactory::CreateSharedImagedoes not invokeAHardwareBuffer_describeto cross-checkAHardwareBuffer_Desc.formatagainstsi_info.format. - Buffer Copy Alignment Logic: Evaluated
GetImageCopyBufferAlignmentinthird_party/angle/src/libANGLE/renderer/vulkan/vk_format_utils.cpp:375-404. WhenpixelBytesis 6, themultipliercalculates as6 % 2 != 0 ? 4 : 6 % 4 != 0 ? 2 : 1, resulting inmultiplier = 2. The returned alignment is2 * 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_UNORMsupport (Hex0x3b). - Requires a non-Samsung Vulkan driver to ensure
forceSampleUsageForAhbBackedImagesis false (vk_renderer.cpp:6868), preventingCanCopyWithDrawfrom running.
- Android API 34+ is required for
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.