Medium watch CVSS 6.5 chrome Uninitialized Memory 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
6.5
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUninitialized resource in ANGLE
DescriptionUninitialized resource in ANGLE
ComponentANGLE
Bug ClassUninitialized Memory
Action tierwatch — information disclosure — security-relevant, below the RCE-class bar
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.
Tracker516824665
Fix commit4e0fcf14911c (angle/angle) +113/-58
CWECWE-908
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-05-26 (per vendor advisory)
Fix landed2026-07-07 (public git)
Disclosed2026-08-25
Patch gap49 days — fix public in source before the advisory

Background

`ValidateTexImageFormatCombination`
ANGLE’s ES3 validation routine that checks whether a format/type/internalFormat triple is a legal upload combination.
Load function
a generated per-format routine (in load_functions_table_autogen.cpp) that converts uploaded pixels into the texture’s storage layout; unmapped combinations map to UnimplementedLoadFunction/UnreachableLoadFunction.
`UNIMPLEMENTED()` / `UNREACHABLE()`
debug-only assertion macros that in release builds fall through without doing anything, leaving the destination buffer untouched.

Root Cause Analysis

For ES3 sub-image updates ANGLE validated the incoming format/type against the internalFormat argument but did not re-validate the pair against the allocated texture’s actual sized internal format, so a caller could pass a type inconsistent with how the texture was created. That mismatch could resolve to a load-function slot with no real conversion routine—UnimplementedLoadFunction or UnreachableLoadFunction—whose bodies used UNIMPLEMENTED()/UNREACHABLE(), macros that in release builds do nothing and return, leaving destination texture memory uninitialized (an uninitialized resource) while the upload appears to succeed.

The fix adds an explicit isSubImage check that recomputes the texture’s sized internal format (actualFormatInfo.sizedInternalFormat) and rejects the call with kTextureTypeMismatch unless format/type are valid for it via ValidES3FormatCombination or the new ValidES3ExtensionFormatCombination. It also refactors the extension-format allow-list into ValidES3ExtensionFormatCombination, corrects the GL_DEPTH_COMPONENT32_OES type mapping to GL_UNSIGNED_INT, adds glTexImage2D-only guards for depth32/2101010REV extension requirements, and replaces UNIMPLEMENTED()/UNREACHABLE() with FATAL() in the generated load stubs so any remaining unhandled combination aborts instead of silently leaving memory undefined.

Key insight
Sub-image type validation checked the caller-supplied internalFormat rather than the texture’s real allocated format, letting a mismatched type reach a no-op load function that left texture memory uninitialized; the fix validates the pair against the actual sized internal format and makes unhandled load functions FATAL() instead of silent no-ops.

Attack Path

  1. Allocate a texture Create and allocate an ES3 texture with a specific sized internal format via glTexStorage/glTexImage.
  2. Upload a mismatched sub-image Call glTexSubImage2D with a format/type combination that passes the old, weaker validation (checked against the passed internalFormat) but does not match the texture’s allocated format.
  3. Reach a stub load function The mismatch selects a load-function slot backed by UnimplementedLoadFunction/UnreachableLoadFunction, whose UNIMPLEMENTED()/UNREACHABLE() bodies are no-ops in release builds and return without writing.
  4. Leave/read uninitialized data The destination texture region is left uninitialized (uninitialized resource) yet usable; subsequent sampling or readback can expose that uninitialized GPU memory (inferred: exact readback primitive not shown in the diff).

Impact Assessment

An attacker supplying WebGL/ES3 texture calls (e.g., from a web page reaching the GPU process) could bypass sub-image type validation and leave texture memory uninitialized, exposing uninitialized resource contents. The advisory classifies this as an uninitialized resource in ANGLE; no exploited-in-the-wild status is stated. Any use of the leaked uninitialized data as an information-disclosure primitive beyond what the patch shows is (inferred).

Files Changed

  • src/libANGLE/renderer/load_functions_table_autogen.cpp
  • src/libANGLE/es3_format_type_combinations.json
  • src/libANGLE/validationES3.cpp
  • scripts/code_generation_hashes/ANGLE_load_functions_table.json
  • src/libANGLE/format_map_autogen.cpp
  • scripts/code_generation_hashes/GL_format_map.json
  • src/libANGLE/renderer/gen_load_functions_table.py

Audit Directions

  • Sub-image vs. allocated-format mismatch
    Audit every texture/buffer sub-update path to confirm it re-validates the incoming format/type against the object’s actual allocated format, not just the parameters passed to the sub-update call.
  • Assertion macros as release no-ops
    Search for UNIMPLEMENTED()/UNREACHABLE()/ASSERT in code reachable by attacker input, where the release-build no-op leaves output buffers uninitialized or skips required work.
  • Generated format/load tables
    Review autogenerated format-combination and load-function tables for entries that resolve to stub/unimplemented handlers, which indicate combinations that validation must reject upstream.
From 4e0fcf14911c55b9f304c0cb36c7d6bf76a3a7b2 Mon Sep 17 00:00:00 2001
From: wangra <wangra@google.com>
Date: Fri, 26 Jun 2026 12:41:48 -0400
Subject: [PATCH] Validate sub-image update type against allocated texture format

Enforces type matching in ES3 sub-image updates to prevent validation
bypasses. Also replaces UNIMPLEMENTED() and UNREACHABLE() with FATAL()
in generated backend texture load functions to prevent undefined
behavior or potential memory leaks.

Test: angle_end2end_tests --gtest_filter=
"*Texture2DTestES3.InvalidFormatTypeCombosShouldFail*"
Bug: b/522715859, b/516824665
Change-Id: I20c9283687b22c231b1e59fb8e4482bdf38eb4b7
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/8008914
Reviewed-by: Amirali Abdolrashidi <abdolrashidi@google.com>
Reviewed-by: Geoff Lang <geofflang@chromium.org>
Commit-Queue: Ran Wang <wangra@google.com>
---
diff --git a/scripts/code_generation_hashes/ANGLE_load_functions_table.json b/scripts/code_generation_hashes/ANGLE_load_functions_table.json
index c10dbea..304c927 100644
--- a/scripts/code_generation_hashes/ANGLE_load_functions_table.json
+++ b/scripts/code_generation_hashes/ANGLE_load_functions_table.json
@@ -2,9 +2,9 @@
   "src/libANGLE/renderer/angle_format.py":
     "49c55a74e763b1cfd0d96c4b49b4e402",
   "src/libANGLE/renderer/gen_load_functions_table.py":
-    "73e96775a0bd1a424f3f8585b1c5175f",
+    "10e66c76a350a1c70fadd2e10a28981e",
   "src/libANGLE/renderer/load_functions_data.json":
     "cf538e24465732f6ca79cc3b91833451",
   "src/libANGLE/renderer/load_functions_table_autogen.cpp":
-    "f21075147182a86735b7837ecd60d26d"
+    "c10713aabfbb66dae7d9b7ee89454bc4"
 }
diff --git a/scripts/code_generation_hashes/GL_format_map.json b/scripts/code_generation_hashes/GL_format_map.json
index 73563bb..a01ecac 100644
--- a/scripts/code_generation_hashes/GL_format_map.json
+++ b/scripts/code_generation_hashes/GL_format_map.json
@@ -1,8 +1,8 @@
 {
   "src/libANGLE/es3_format_type_combinations.json":
-    "a1b6346f897ae0a78c900a1a7bf095bc",
+    "6db47ff74ca02136aae216441fb8ead3",
   "src/libANGLE/format_map_autogen.cpp":
-    "0af6c5aba2a2392d0cd96ef1b0ea3a00",
+    "832b3fbe708f00006fe35c9c6d5bba84",
   "src/libANGLE/format_map_data.json":
     "4234d7b249edacad5105d4b049339125",
   "src/libANGLE/gen_format_map.py":
diff --git a/src/libANGLE/es3_format_type_combinations.json b/src/libANGLE/es3_format_type_combinations.json
index 6ed0b8e..c61d893 100644
--- a/src/libANGLE/es3_format_type_combinations.json
+++ b/src/libANGLE/es3_format_type_combinations.json
@@ -163,7 +163,7 @@
     ],
     "From GL_ANGLE_depth_texture and OES_depth_texture":
     [
-        [ "GL_DEPTH_COMPONENT32_OES",  "GL_DEPTH_COMPONENT", "GL_UNSIGNED_INT_24_8" ],
+        [ "GL_DEPTH_COMPONENT32_OES",  "GL_DEPTH_COMPONENT", "GL_UNSIGNED_INT" ],
         [ "GL_DEPTH_COMPONENT",        "GL_DEPTH_COMPONENT", "GL_UNSIGNED_SHORT" ],
         [ "GL_DEPTH_COMPONENT",        "GL_DEPTH_COMPONENT", "GL_UNSIGNED_INT" ]
     ],
@@ -192,5 +192,10 @@
         [ "GL_RGBX8_ANGLE", "GL_RGB", "GL_UNSIGNED_BYTE" ],
         [ "GL_RGBX8_SRGB_ANGLEX", "GL_RGB", "GL_UNSIGNED_BYTE" ],
         [ "GL_RGBX8_ANGLE", "GL_RGBA", "GL_UNSIGNED_BYTE" ]
+    ],
+    "From GL_ANGLE_yuv_internal_format":
+    [
+        [ "GL_G8_B8R8_2PLANE_420_UNORM_ANGLE", "GL_G8_B8R8_2PLANE_420_UNORM_ANGLE", "GL_UNSIGNED_BYTE" ],
+        [ "GL_G8_B8_R8_3PLANE_420_UNORM_ANGLE", "GL_G8_B8_R8_3PLANE_420_UNORM_ANGLE", "GL_UNSIGNED_BYTE" ]
     ]
 }
diff --git a/src/libANGLE/renderer/gen_load_functions_table.py b/src/libANGLE/renderer/gen_load_functions_table.py
index f9a2bed..1ec23cf 100755
--- a/src/libANGLE/renderer/gen_load_functions_table.py
+++ b/src/libANGLE/renderer/gen_load_functions_table.py
@@ -26,6 +26,7 @@
 //   Contains the GetLoadFunctionsMap for texture_format_util.h
 //
 
+#include "common/debug.h"
 #include "libANGLE/renderer/load_functions_table.h"
 
 #include "image_util/copyimage.h"
@@ -58,7 +59,7 @@
                                size_t outputRowPitch,
                                size_t outputDepthPitch)
 {{
-    UNIMPLEMENTED();
+    FATAL() << "UNIMPLEMENTED load function.";
 }}
 
 void UnreachableLoadFunction(const ImageLoadContext &context,
@@ -72,7 +73,7 @@
                              size_t outputRowPitch,
                              size_t outputDepthPitch)
 {{
-    UNREACHABLE();
+    FATAL() << "UNREACHABLE load function.";
 }}
 
 {load_functions_data}}}  // namespace
diff --git a/src/libANGLE/validationES3.cpp b/src/libANGLE/validationES3.cpp
index c090d57..1c83ce8 100644
--- a/src/libANGLE/validationES3.cpp
+++ b/src/libANGLE/validationES3.cpp
@@ -229,6 +229,23 @@
     return true;
 }
 
+bool ValidES3ExtensionFormatCombination(GLenum format, GLenum type, GLenum internalFormat)
+{
+    switch (internalFormat)
+    {
+        case GL_LUMINANCE4_ALPHA4_OES:
+            return (type == GL_UNSIGNED_BYTE && format == GL_LUMINANCE_ALPHA);
+        case GL_DEPTH_COMPONENT32_OES:
+            return (type == GL_UNSIGNED_INT && format == GL_DEPTH_COMPONENT);
+        case GL_RGB10_EXT:
+        case GL_RGB8_OES:
+        case GL_RGB565_OES:
+            return (type == GL_UNSIGNED_INT_2_10_10_10_REV_EXT && format == GL_RGB);
+        default:
+            return false;
+    }
+}
+
 }  // anonymous namespace
 
 bool ValidateTexImageFormatCombination(const Context *context,
@@ -299,44 +316,11 @@
     }
     else
     {
-        if (!ValidES3FormatCombination(format, type, internalFormat))
+        if (!ValidES3FormatCombination(format, type, internalFormat) &&
+            !ValidES3ExtensionFormatCombination(format, type, internalFormat))
         {
-            bool extensionFormatsAllowed = false;
-            switch (internalFormat)
-            {
-                case GL_LUMINANCE4_ALPHA4_OES:
-                    if (context->getExtensions().requiredInternalformatOES &&
-                        type == GL_UNSIGNED_BYTE && format == GL_LUMINANCE_ALPHA)
-                    {
-                        extensionFormatsAllowed = true;
-                    }
-                    break;
-                case GL_DEPTH_COMPONENT32_OES:
-                    if ((context->getExtensions().requiredInternalformatOES &&
-                         context->getExtensions().depth32OES) &&
-                        type == GL_UNSIGNED_INT && format == GL_DEPTH_COMPONENT)
-                    {
-                        extensionFormatsAllowed = true;
-                    }
-                    break;
-                case GL_RGB10_EXT:
-                case GL_RGB8_OES:
-                case GL_RGB565_OES:
-                    if (context->getExtensions().requiredInternalformatOES &&
-                        context->getExtensions().textureType2101010REVEXT &&
-                        type == GL_UNSIGNED_INT_2_10_10_10_REV_EXT && format == GL_RGB)
-                    {
-                        extensionFormatsAllowed = true;
-                    }
-                    break;
-                default:
-                    break;
-            }
-            if (!extensionFormatsAllowed)
-            {
-                ANGLE_VALIDATION_ERROR(GL_INVALID_OPERATION, kInvalidFormatCombination);
-                return false;
-            }
+            ANGLE_VALIDATION_ERROR(GL_INVALID_OPERATION, kInvalidFormatCombination);
+            return false;
         }
     }
 
@@ -559,15 +543,38 @@
                                                  ? *texture->getFormat(target, level).info
                                                  : GetInternalFormatInfo(internalformat, type);
     {
-        // Compressed formats are not valid internal formats for glTexImage*D
         if (!isSubImage)
         {
+            // Compressed formats are not valid internal formats for glTexImage*D
             const InternalFormat &internalFormatInfo = GetSizedInternalFormatInfo(internalformat);
             if (internalFormatInfo.compressed)
             {
                 ANGLE_VALIDATION_ERRORF(GL_INVALID_VALUE, kInvalidInternalFormat, internalformat);
                 return false;
             }
+
+            // GL_DEPTH_COMPONENT32_OES texture support is enabled if GL_OES_depth_texture is
+            // supported (to support GL_EXT_texture_storage in ES2). But for ES3 glTexImage2D, it is
+            // only valid if GL_OES_depth32 is also supported.
+            if (internalformat == GL_DEPTH_COMPONENT32_OES &&
+                (!context->getExtensions().requiredInternalformatOES ||
+                 !context->getExtensions().depth32OES))
+            {
+                ANGLE_VALIDATION_ERROR(GL_INVALID_OPERATION, kInvalidFormatCombination);
+                return false;
+            }
+
+            // GL_RGB8_OES and GL_RGB565_OES paired with GL_UNSIGNED_INT_2_10_10_10_REV_EXT require
+            // GL_EXT_texture_type_2_10_10_10_REV and GL_OES_required_internalformat for image
+            // creation.
+            if ((internalformat == GL_RGB8_OES || internalformat == GL_RGB565_OES) &&
+                type == GL_UNSIGNED_INT_2_10_10_10_REV_EXT &&
+                (!context->getExtensions().requiredInternalformatOES ||
+                 !context->getExtensions().textureType2101010REVEXT))
+            {
+                ANGLE_VALIDATION_ERROR(GL_INVALID_OPERATION, kInvalidFormatCombination);
+                return false;
+            }
         }
 
         if (!ValidateTexImageFormatCombination(context, entryPoint, texType, actualInternalFormat,
@@ -575,6 +582,17 @@
         {
             return false;
         }
+
+        if (isSubImage)
+        {
+            const GLenum textureSizedInternalFormat = actualFormatInfo.sizedInternalFormat;
+            if (!ValidES3FormatCombination(format, type, textureSizedInternalFormat) &&
+                !ValidES3ExtensionFormatCombination(format, type, textureSizedInternalFormat))
+            {
+                ANGLE_VALIDATION_ERROR(GL_INVALID_OPERATION, kTextureTypeMismatch);
+                return false;
+            }
+        }
     }
 
     // Validate sub image parameters
diff --git a/src/libANGLE/format_map_autogen.cpp b/src/libANGLE/format_map_autogen.cpp
index 60bdec6..8ff3dd7 100644
--- a/src/libANGLE/format_map_autogen.cpp
+++ b/src/libANGLE/format_map_autogen.cpp
@@ -586,6 +586,8 @@
         case GL_BGRA_EXT:
         case GL_DEPTH_COMPONENT:
         case GL_DEPTH_STENCIL:
+        case GL_G8_B8R8_2PLANE_420_UNORM_ANGLE:
+        case GL_G8_B8_R8_3PLANE_420_UNORM_ANGLE:
         case GL_LUMINANCE:
         case GL_LUMINANCE_ALPHA:
         case GL_RED:
@@ -762,18 +764,8 @@
                     {
                         case GL_DEPTH_COMPONENT24:
                         case GL_DEPTH_COMPONENT16:
-                        case GL_DEPTH_COMPONENT:
-                            return true;
-                        default:
-                            break;
-                    }
-                    break;
-                }
-                case GL_UNSIGNED_INT_24_8:
-                {
-                    switch (internalFormat)
-                    {
                         case GL_DEPTH_COMPONENT32_OES:
+                        case GL_DEPTH_COMPONENT:
                             return true;
                         default:
                             break;
@@ -828,6 +820,44 @@
             }
             break;
 
+        case GL_G8_B8R8_2PLANE_420_UNORM_ANGLE:
+            switch (type)
+            {
+                case GL_UNSIGNED_BYTE:
+                {
+                    switch (internalFormat)
+                    {
+                        case GL_G8_B8R8_2PLANE_420_UNORM_ANGLE:
+                            return true;
+                        default:
+                            break;
+                    }
+                    break;
+                }
+                default:
+                    break;
+            }
+            break;
+
+        case GL_G8_B8_R8_3PLANE_420_UNORM_ANGLE:
+            switch (type)
+            {
+                case GL_UNSIGNED_BYTE:
+                {
+                    switch (internalFormat)
+                    {
+                        case GL_G8_B8_R8_3PLANE_420_UNORM_ANGLE:
+                            return true;
+                        default:
+                            break;
+                    }
+                    break;
+                }
+                default:
+                    break;
+            }
+            break;
+
         case GL_LUMINANCE:
             switch (type)
             {
diff --git a/src/libANGLE/renderer/load_functions_table_autogen.cpp b/src/libANGLE/renderer/load_functions_table_autogen.cpp
index 345756f..32d21e6 100644
--- a/src/libANGLE/renderer/load_functions_table_autogen.cpp
+++ b/src/libANGLE/renderer/load_functions_table_autogen.cpp
@@ -9,6 +9,7 @@
 //   Contains the GetLoadFunctionsMap for texture_format_util.h
 //
 
+#include "common/debug.h"
 #include "libANGLE/renderer/load_functions_table.h"
 
 #include "image_util/copyimage.h"
@@ -41,7 +42,7 @@
                                size_t outputRowPitch,
                                size_t outputDepthPitch)
 {
-    UNIMPLEMENTED();
+    FATAL() << "UNIMPLEMENTED load function.";
 }
 
 void UnreachableLoadFunction(const ImageLoadContext &context,
@@ -55,7 +56,7 @@
                              size_t outputRowPitch,
                              size_t outputDepthPitch)
 {
-    UNREACHABLE();
+    FATAL() << "UNREACHABLE load function.";
 }
 
 LoadImageFunctionInfo A1RGB5_ANGLEX_to_A1R5G5B5_UNORM(GLenum type)
Loading diff…

Original Bug Report

reported by vm...@google.com

ANGLE ES2 glTexSubImage2D type validation bypass leading to GPU memory disclosure

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 potential validation gap in ANGLE’s ES2 sub-image path allows non-WebGL contexts to bypass type-to-internalformat consistency checks. A mismatched type parameter causes the Vulkan backend to fallback to a no-op conversion function, leaving dynamic staging memory uninitialized. This recycled memory, which can contain cross-origin GPU data, is then uploaded to the texture and can potentially be read back by an attacker.

Affected files:

  • third_party/angle/src/libANGLE/validationES2.cpp
  • third_party/angle/src/libANGLE/renderer/vulkan/TextureVk.cpp
  • third_party/angle/src/libANGLE/renderer/vulkan/vk_helpers.cpp
  • third_party/angle/src/libANGLE/Texture.cpp
  • third_party/angle/src/libANGLE/renderer/load_functions_table_autogen.cpp

Estimated timestamp from git blame: 2024-11-25

Description

A potential security vulnerability has been identified in ANGLE’s GLES2 validation layer, which may allow a compromised renderer to bypass texture type validation and disclose recycled GPU memory containing cross-origin data.

Root Cause Analysis

In third_party/angle/src/libANGLE/validationES2.cpp, within ValidateES2TexImageParametersBase, the validation path for glTexSubImage2D (isSubImage == true) checks format compatibility but fails to enforce type compatibility for non-WebGL contexts:

        bool formatsMatch = format == textureInternalFormat.format;
        ...
        if (!formatsMatch)
        {
            ANGLE_VALIDATION_ERROR(GL_INVALID_OPERATION, kTextureFormatMismatch);
            return false;
        }

        if (context->isWebGL())
        {
            ...
            isValid = (GetInternalFormatInfo(format, type).sizedInternalFormat ==
                       textureSizedInternalFormat);
            ...
        }

For non-WebGL ES2 contexts (e.g., created with CONTEXT_TYPE_OPENGLES2), context->isWebGL() evaluates to false. This bypasses the strict consistency check between the caller-provided type and the actual texture’s internal format.

When this occurs, the Vulkan backend (in TextureVk::setSubImageImpl) attempts to load the update. Because the format and type are mismatched, vkFormat.getTextureLoadFunction(formatSupport, type) maps to UnreachableLoadFunction via the autogenerated load function table (load_functions_table_autogen.cpp).

In official release builds, UNREACHABLE() expands to a no-op because ANGLE_ENABLE_ASSERTS is disabled. Consequently, UnreachableLoadFunction returns immediately without writing anything to the mapped staging buffer memory.

ANGLE allocates staging buffers from a shared vk::BufferPool without zero-initialization. Since the load function does not write to the buffer, the stale, recycled memory contents remain completely unmodified and are directly copied into the destination texture on the GPU.

Potential Steps to Trigger

Note: These are potential steps based on static analysis, as our tooling cannot execute code.

  1. From a compromised renderer, establish a non-WebGL ES2 context via the Command Buffer Mojo interface.
  2. Allocate a texture with a standard format (e.g., GL_RGBA and GL_UNSIGNED_BYTE).
  3. Call glTexSubImage2D with a mismatched type (e.g., GL_UNSIGNED_SHORT_4_4_4_4). Validation succeeds due to the WebGL-only guard check.
  4. The backend utilizes the uninitialized staging buffer, copying recycled GPU memory into the texture.
  5. Bind the texture to a Framebuffer and read back the contents via glReadPixels or shader sampling to retrieve stale GPU memory.

Suggested Remediation

We suggest enforcing format and type validation consistency checks for all contexts (not just WebGL contexts) inside ValidateES2TexImageParametersBase when handling sub-image updates, or ensuring that the load function table safely handles or rejects invalid type configurations in non-assert builds instead of executing a no-op UnreachableLoadFunction.

Evaluated with Chrome root at commit: b1520ef4a76878853a31f0943b565e42060edec8


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