High chrome Type Confusion 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactType confusion in ANGLE
DescriptionType confusion in ANGLE
ComponentANGLE
Sub-componentInternals>GPU>ANGLE
Bug ClassType Confusion
Tracker547343108
Fix commit803dfde1e22e (angle/angle) +153/-109
CISA KEVNot listed
CreditedJinpyo Lee
Disclosed2026-10-06

Background

glFramebufferTexture2D / glFramebufferTexture2DMultisampleEXT (MSRTT)
attach a texture level to a framebuffer; the textarget (e.g. GL_TEXTURE_2D, a cube face, GL_TEXTURE_2D_MULTISAMPLE, external) must match the texture’s actual type.
Type-match validation
rejects attaching a texture whose getType() disagrees with textarget.

Root Cause Analysis

glFramebufferTexture2DMultisampleEXT (EXT_multisampled_render_to_texture) did not validate that the attached texture’s type matched the requested textarget — the switch that checks tex->getType() against each target existed only for glFramebufferTexture2D.

The fix extracts that logic into ValidateFramebufferTexture2DTarget(...) and calls it from both entry points, so e.g. attaching a cube-map texture with GL_TEXTURE_2D (or a 2D texture with a cube-map face) is now rejected with GL_INVALID_OPERATION. Without it, the backend would treat the attached texture as the wrong type — a type confusion. The advisory’s “type confusion” label matches the mechanism. Kept as TypeConfusion.

Key insight
MSRTT’s framebuffer-texture attachment skipped the texture-type/target match check that the non-multisample path had; adding the shared validation prevents attaching a texture as the wrong type.

Attack Path

  1. ENTRY From WebGL with EXT_multisampled_render_to_texture, create a texture of one type (e.g. cube map).
  2. CONTROLLED INPUT Call glFramebufferTexture2DMultisampleEXT with a mismatched textarget (e.g. GL_TEXTURE_2D).
  3. MISSING CHECK MSRTT did not validate tex->getType() against textarget, so the mismatched attachment was accepted.
  4. PRIMITIVE The backend then operates on the texture as the wrong type (type confusion).
  5. IMPACT Memory-safety fault in the GPU process from the type-confused texture access. (Downstream corruption inferred; the missing-validation root cause is shown by the patch.)

Impact Assessment

A content-reachable type confusion in ANGLE: a texture can be attached to an MSRTT framebuffer as the wrong type, after which the backend misinterprets it (GPU process). Found by Jinpyo Lee; severity high per the advisory; no ITW stated.

Files Changed

  • src/tests/gl_tests/MultisampledRenderToTextureTest.cpp
  • src/libANGLE/validationES2.cpp

Audit Directions

  • Validation parity across entry points
    Audit paired entry points (2D vs multisample, EXT variants) for validation present in one but missing in the other; share the check.
  • Type/target match
    Enumerate all framebuffer-attachment entry points and confirm each validates texture type against the target.
From 803dfde1e22ed047be3e05028bd094cc2c716267 Mon Sep 17 00:00:00 2001
From: Shahbaz Youssefi <syoussefi@chromium.org>
Date: Tue, 01 Sep 2026 16:21:05 -0400
Subject: [PATCH] Fix missing texture type match validation in MSRTT

Bug: chromium:547343108
Change-Id: I72f6e7702aa26aaf336eb190ae73c45518b475c5
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/8340208
Commit-Queue: Amirali Abdolrashidi <abdolrashidi@google.com>
Auto-Submit: Shahbaz Youssefi <syoussefi@chromium.org>
Reviewed-by: Amirali Abdolrashidi <abdolrashidi@google.com>
---
diff --git a/src/libANGLE/validationES2.cpp b/src/libANGLE/validationES2.cpp
index c3de868..b44bfc8 100644
--- a/src/libANGLE/validationES2.cpp
+++ b/src/libANGLE/validationES2.cpp
@@ -836,6 +836,113 @@
     return true;
 }
 
+bool ValidateFramebufferTexture2DTarget(const Context *context,
+                                        angle::EntryPoint entryPoint,
+                                        Texture *tex,
+                                        TextureTarget textarget,
+                                        GLint level)
+{
+    const Caps &caps = context->getCaps();
+
+    switch (textarget)
+    {
+        case TextureTarget::_2D:
+        {
+            if (level > log2(caps.max2DTextureSize))
+            {
+                ANGLE_VALIDATION_ERROR(GL_INVALID_VALUE, kInvalidMipLevel);
+                return false;
+            }
+            if (tex->getType() != TextureType::_2D)
+            {
+                ANGLE_VALIDATION_ERROR(GL_INVALID_OPERATION, kInvalidTextureTarget);
+                return false;
+            }
+        }
+        break;
+
+        case TextureTarget::Rectangle:
+        {
+            if (level != 0)
+            {
+                ANGLE_VALIDATION_ERROR(GL_INVALID_VALUE, kInvalidMipLevel);
+                return false;
+            }
+            if (tex->getType() != TextureType::Rectangle)
+            {
+                ANGLE_VALIDATION_ERROR(GL_INVALID_OPERATION, kTextureTargetMismatch);
+                return false;
+            }
+        }
+        break;
+
+        case TextureTarget::CubeMapNegativeX:
+        case TextureTarget::CubeMapNegativeY:
+        case TextureTarget::CubeMapNegativeZ:
+        case TextureTarget::CubeMapPositiveX:
+        case TextureTarget::CubeMapPositiveY:
+        case TextureTarget::CubeMapPositiveZ:
+        {
+            if (level > log2(caps.maxCubeMapTextureSize))
+            {
+                ANGLE_VALIDATION_ERROR(GL_INVALID_VALUE, kInvalidMipLevel);
+                return false;
+            }
+            if (tex->getType() != TextureType::CubeMap)
+            {
+                ANGLE_VALIDATION_ERROR(GL_INVALID_OPERATION, kTextureTargetMismatch);
+                return false;
+            }
+        }
+        break;
+
+        case TextureTarget::_2DMultisample:
+        {
+            if (context->getClientVersion() < ES_3_1 &&
+                !context->getExtensions().textureMultisampleANGLE)
+            {
+                ANGLE_VALIDATION_ERROR(GL_INVALID_OPERATION,
+                                       kMultisampleTextureExtensionOrES31Required);
+                return false;
+            }
+
+            if (level != 0)
+            {
+                ANGLE_VALIDATION_ERROR(GL_INVALID_VALUE, kLevelNotZero);
+                return false;
+            }
+            if (tex->getType() != TextureType::_2DMultisample)
+            {
+                ANGLE_VALIDATION_ERROR(GL_INVALID_OPERATION, kTextureTargetMismatch);
+                return false;
+            }
+        }
+        break;
+
+        case TextureTarget::External:
+        {
+            if (!context->getExtensions().YUVTargetEXT)
+            {
+                ANGLE_VALIDATION_ERROR(GL_INVALID_OPERATION, kYUVTargetExtensionRequired);
+                return false;
+            }
+
+            if (tex->getType() != TextureType::External)
+            {
+                ANGLE_VALIDATION_ERROR(GL_INVALID_OPERATION, kTextureTargetMismatch);
+                return false;
+            }
+        }
+        break;
+
+        default:
+            ANGLE_VALIDATION_ERROR(GL_INVALID_ENUM, kInvalidTextureTarget);
+            return false;
+    }
+
+    return true;
+}
+
 bool ValidateSrcBlendFunc(const PrivateState &state,
                           ErrorSet *errors,
                           angle::EntryPoint entryPoint,
@@ -5504,108 +5611,15 @@
         Texture *tex = context->getTexture(texture);
         ASSERT(tex);
 
-        const Caps &caps = context->getCaps();
-
-        switch (textarget)
+        if (!ValidateFramebufferTexture2DTarget(context, entryPoint, tex, textarget, level))
         {
-            case TextureTarget::_2D:
-            {
-                if (level > log2(caps.max2DTextureSize))
-                {
-                    ANGLE_VALIDATION_ERROR(GL_INVALID_VALUE, kInvalidMipLevel);
-                    return false;
-                }
-                if (tex->getType() != TextureType::_2D)
-                {
-                    ANGLE_VALIDATION_ERROR(GL_INVALID_OPERATION, kInvalidTextureTarget);
-                    return false;
-                }
-            }
-            break;
+            return false;
+        }
 
-            case TextureTarget::Rectangle:
-            {
-                if (level != 0)
-                {
-                    ANGLE_VALIDATION_ERROR(GL_INVALID_VALUE, kInvalidMipLevel);
-                    return false;
-                }
-                if (tex->getType() != TextureType::Rectangle)
-                {
-                    ANGLE_VALIDATION_ERROR(GL_INVALID_OPERATION, kTextureTargetMismatch);
-                    return false;
-                }
-            }
-            break;
-
-            case TextureTarget::CubeMapNegativeX:
-            case TextureTarget::CubeMapNegativeY:
-            case TextureTarget::CubeMapNegativeZ:
-            case TextureTarget::CubeMapPositiveX:
-            case TextureTarget::CubeMapPositiveY:
-            case TextureTarget::CubeMapPositiveZ:
-            {
-                if (level > log2(caps.maxCubeMapTextureSize))
-                {
-                    ANGLE_VALIDATION_ERROR(GL_INVALID_VALUE, kInvalidMipLevel);
-                    return false;
-                }
-                if (tex->getType() != TextureType::CubeMap)
-                {
-                    ANGLE_VALIDATION_ERROR(GL_INVALID_OPERATION, kTextureTargetMismatch);
-                    return false;
-                }
-            }
-            break;
-
-            case TextureTarget::_2DMultisample:
-            {
-                if (context->getClientVersion() < ES_3_1 &&
-                    !context->getExtensions().textureMultisampleANGLE)
-                {
-                    ANGLE_VALIDATION_ERROR(GL_INVALID_OPERATION,
-                                           kMultisampleTextureExtensionOrES31Required);
-                    return false;
-                }
-
-                if (level != 0)
-                {
-                    ANGLE_VALIDATION_ERROR(GL_INVALID_VALUE, kLevelNotZero);
-                    return false;
-                }
-                if (tex->getType() != TextureType::_2DMultisample)
-                {
-                    ANGLE_VALIDATION_ERROR(GL_INVALID_OPERATION, kTextureTargetMismatch);
-                    return false;
-                }
-            }
-            break;
-
-            case TextureTarget::External:
-            {
-                if (!context->getExtensions().YUVTargetEXT)
-                {
-                    ANGLE_VALIDATION_ERROR(GL_INVALID_OPERATION, kYUVTargetExtensionRequired);
-                    return false;
-                }
-
-                if (attachment != GL_COLOR_ATTACHMENT0)
-                {
-                    ANGLE_VALIDATION_ERROR(GL_INVALID_OPERATION, kInvalidAttachment);
-                    return false;
-                }
-
-                if (tex->getType() != TextureType::External)
-                {
-                    ANGLE_VALIDATION_ERROR(GL_INVALID_OPERATION, kTextureTargetMismatch);
-                    return false;
-                }
-            }
-            break;
-
-            default:
-                ANGLE_VALIDATION_ERROR(GL_INVALID_ENUM, kInvalidTextureTarget);
-                return false;
+        if (textarget == TextureTarget::External && attachment != GL_COLOR_ATTACHMENT0)
+        {
+            ANGLE_VALIDATION_ERROR(GL_INVALID_OPERATION, kInvalidAttachment);
+            return false;
         }
     }
 
@@ -6060,19 +6074,28 @@
         return false;
     }
 
-    // EXT_multisampled_render_to_texture returns INVALID_OPERATION when a sample number higher than
-    // the maximum sample number supported by this format is passed.
-    // The getMaxSamples method is only guaranteed to be valid when the context is ES3.
-    if (texture.value != 0 && context->getClientVersion() >= ES_3_0)
+    if (texture.value != 0)
     {
-        Texture *tex                  = context->getTexture(texture);
-        GLenum sizedInternalFormat    = tex->getFormat(textarget, level).info->sizedInternalFormat;
-        const TextureCaps &formatCaps = context->getTextureCaps().get(sizedInternalFormat);
-        if (static_cast<GLuint>(samples) > formatCaps.sampleCounts.getMaxSamples())
+        Texture *tex = context->getTexture(texture);
+
+        if (!ValidateFramebufferTexture2DTarget(context, entryPoint, tex, textarget, level))
         {
-            ANGLE_VALIDATION_ERROR(GL_INVALID_OPERATION, kSamplesOutOfRange);
             return false;
         }
+
+        // EXT_multisampled_render_to_texture returns INVALID_OPERATION when a sample number higher
+        // than the maximum sample number supported by this format is passed.
+        // The getMaxSamples method is only guaranteed to be valid when the context is ES3.
+        if (context->getClientVersion() >= ES_3_0)
+        {
+            GLenum sizedInternalFormat = tex->getFormat(textarget, level).info->sizedInternalFormat;
+            const TextureCaps &formatCaps = context->getTextureCaps().get(sizedInternalFormat);
+            if (static_cast<GLuint>(samples) > formatCaps.sampleCounts.getMaxSamples())
+            {
+                ANGLE_VALIDATION_ERROR(GL_INVALID_OPERATION, kSamplesOutOfRange);
+                return false;
+            }
+        }
     }
 
     // Unless EXT_multisampled_render_to_texture2 is enabled, only color attachment 0 can be used.
diff --git a/src/tests/gl_tests/MultisampledRenderToTextureTest.cpp b/src/tests/gl_tests/MultisampledRenderToTextureTest.cpp
index 226e93c..128d4f7 100644
--- a/src/tests/gl_tests/MultisampledRenderToTextureTest.cpp
+++ b/src/tests/gl_tests/MultisampledRenderToTextureTest.cpp
@@ -331,6 +331,19 @@
         assertErrorIfNotMSRTT2(GL_INVALID_ENUM);
     }
 
+    // Attachment not 2D texture
+    GLTexture cube;
+    glBindTexture(GL_TEXTURE_CUBE_MAP, cube);
+    for (GLenum face = 0; face < 6; face++)
+    {
+        glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + face, 0, GL_RGBA, 64, 64, 0, GL_RGBA,
+                     GL_UNSIGNED_BYTE, nullptr);
+        ASSERT_GL_NO_ERROR();
+    }
+    glFramebufferTexture2DMultisampleEXT(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, cube,
+                                         0, 4);
+    ASSERT_GL_ERROR(GL_INVALID_OPERATION);
+
     // Target not framebuffer
     glFramebufferTexture2DMultisampleEXT(GL_RENDERBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
                                          texture, 0, 4);
@@ -426,6 +439,14 @@
         // the next larger sample count supported by the implementation"
         EXPECT_GE(param, 4);
     }
+
+    // Attachment not cubemap texture
+    GLTexture notcube;
+    glBindTexture(GL_TEXTURE_2D, notcube);
+    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 64, 64, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
+    glFramebufferTexture2DMultisampleEXT(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
+                                         GL_TEXTURE_CUBE_MAP_POSITIVE_X, notcube, 0, 4);
+    ASSERT_GL_ERROR(GL_INVALID_OPERATION);
 }
 
 // Checking for framebuffer completeness using extension methods.
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/src/tests/gl_tests/MultisampledRenderToTextureTest.cpp b/src/tests/gl_tests/MultisampledRenderToTextureTest.cpp
index 226e93c..128d4f7 100644
--- a/src/tests/gl_tests/MultisampledRenderToTextureTest.cpp
+++ b/src/tests/gl_tests/MultisampledRenderToTextureTest.cpp
@@ -331,6 +331,19 @@
         assertErrorIfNotMSRTT2(GL_INVALID_ENUM);
     }
 
+    // Attachment not 2D texture
+    GLTexture cube;
+    glBindTexture(GL_TEXTURE_CUBE_MAP, cube);
+    for (GLenum face = 0; face < 6; face++)
+    {
+        glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + face, 0, GL_RGBA, 64, 64, 0, GL_RGBA,
+                     GL_UNSIGNED_BYTE, nullptr);
+        ASSERT_GL_NO_ERROR();
+    }
+    glFramebufferTexture2DMultisampleEXT(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, cube,
+                                         0, 4);
+    ASSERT_GL_ERROR(GL_INVALID_OPERATION);
+
     // Target not framebuffer
     glFramebufferTexture2DMultisampleEXT(GL_RENDERBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
                                          texture, 0, 4);
@@ -426,6 +439,14 @@
         // the next larger sample count supported by the implementation"
         EXPECT_GE(param, 4);
     }
+
+    // Attachment not cubemap texture
+    GLTexture notcube;
+    glBindTexture(GL_TEXTURE_2D, notcube);
+    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 64, 64, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
+    glFramebufferTexture2DMultisampleEXT(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
+                                         GL_TEXTURE_CUBE_MAP_POSITIVE_X, notcube, 0, 4);
+    ASSERT_GL_ERROR(GL_INVALID_OPERATION);
 }
 
 // Checking for framebuffer completeness using extension methods.
Loading diff…

Original Bug Report

reported by bi...@gmail.com

ANGLE: glFramebufferTexture2DMultisampleEXT missing textarget/texture-type validation → GPU-process OOB write (type confusion)


Report description

ANGLE: glFramebufferTexture2DMultisampleEXT missing textarget/texture-type validation → GPU-process OOB write (type confusion)


Bug location

Where do you want to report your vulnerability?

Chrome VRP – Report security issues affecting the Chrome browser. See program rules

Which URL (or repository) have you found the vulnerability in?

https://chromium.googlesource.com/angle/angle


The problem

Please describe the technical details of the vulnerability

▶ Summary ValidateFramebufferTexture2DMultisampleEXT (src/libANGLE/validationES2.cpp:6038) is the only one of 15 framebuffer-texture attach validators in ANGLE that does not verify that textarget matches the texture’s actual type. Its sibling glFramebufferTexture2D performs this check; MSRTT does not.

As a result a cube-map-face textarget can be attached to a 3D (or 2D) texture. The GPU-process ANGLE then builds a Vulkan image view pointing at a nonexistent array layer / Z-slice, producing an out-of-bounds render (out-of-bounds write on the color attachment’s backing memory).

  • Type: CWE-843 Type Confusion → CWE-787 Out-of-bounds Write
  • Process: GPU
  • Reachability: from a compromised renderer (this entry point is not exposed to WebGL — see below)
  • Affected backends: Vulkan (default on Android, primary on Linux), GL, Metal
  • Commit audited: e4499e6b2835a6996507f1b99920bc56f0122573 (also present on main aa192212af54)

▶ Reachability glFramebufferTexture2DMultisampleEXT exists as a GLES2 command-buffer entry point (gpu/command_buffer/common/gles2_cmd_format_autogen.h), has a client-side emit function (gpu/command_buffer/client/gles2_implementation_impl_autogen.h), and GL_EXT_multisampled_render_to_texture is in the passthrough decoder’s kValidRequestableExtensions, so a renderer can enable it itself. However no WebGL extension exposes this function to JavaScript (verified across all 268 files of Blink modules/webgl; no MSRTT IDL).

Therefore this is reachable only from a compromised renderer, not from ordinary web content. The attached patch simulates that compromised renderer per the VRP “compromised-renderer simulation patch” guidance, using a git patch with a –type=renderer process guard.

▶ Root cause glFramebufferTexture2DMultisampleEXT(GL_TEXTURE_CUBE_MAP_NEGATIVE_X, <3D texture>, level=1) -> ValidateFramebufferTexture2DMultisampleEXT: no textarget-vs-type check -> passes -> attachment stores ImageIndex{ type = CubeMapFace, layerIndex = face } -> FramebufferAttachment::layer() returns 0 for a cube face (has3DLayer()==false), which DISABLES the completeness layer bound in CheckAttachmentCompleteness (Framebuffer.cpp: if (attachment.layer() >= size.depth) INCOMPLETE) -> TextureVk::getRenderTargetLayerCountAndIndex forces imageLayerCount = 6 (kCubeFaceCount) regardless of the real image -> vk_helpers.cpp GetLevelLayerImageView indexes the view vector with face -> vkCreateImageView(viewType=2D, baseMipLevel=L, baseArrayLayer=face, layerCount=1) A 3D image’s level L has depth = depth0 >> L; face beyond that violates VUID-VkImageViewCreateInfo-image-02724, pointing at a nonexistent slice.

The sibling glFramebufferTexture2D rejects the identical arguments with INVALID_OPERATION.

▶ Suggested fix

Add the same textarget ↔ tex->getType() check that ValidateFramebufferTexture2D (validationES2.cpp:5478) already performs in its switch (textarget) block; factor it into a shared helper. The check must live outside the clientVersion >= ES_3_0 guard, since it is also required for ES 2.0 contexts.

▶ Reproduction Build steps, the exact Chrome command line, expected results, and the compromised-renderer patch are in the attached renderer_msrtt_oob.patch, poc.html, and REPRODUCE.md.

Impact analysis

Who / what they gain: An attacker who has already compromised a renderer process (the standard Chrome threat model for GPU-process bugs) can, by issuing a single command-buffer call, cause the GPU process to perform an out-of-bounds write — writing fully attacker-controlled data to GPU memory that was never attached, or crashing the GPU process. This is a step toward escaping the renderer sandbox into the more-privileged GPU process.

Process affected: GPU process. Reachability: from a compromised renderer — glFramebufferTexture2DMultisampleEXT is not exposed to WebGL (verified across all 268 Blink modules/webgl files), so it cannot be triggered by ordinary web content; a compromised renderer issues it via the command buffer.

Two variants, both from the same missing textarget-vs-type check:

  1. 2D texture — indexes ANGLE’s std::vector<ImageView> out of bounds.
  • Under Chrome’s shipped config (_LIBCPP_HARDENING_MODE_EXTENSIVE): controlled vector.h hardening abort → GPU-process crash (DoS).
  • With hardening off: genuine heap-buffer-overflow WRITE at ImageViewHelper::getLevelLayerDrawImageView (vk_helpers.cpp:184), stack through FramebufferVk::startNewRenderPass → ContextVk::drawArrays.
  1. 3D texture — the out-of-bounds slice lands on a different mip level inside the same image allocation. No GL error, no hardening abort: attacker-controlled content (clear color or fragment-shader output) is written to a subresource that was never attached, and is read back via the normal glFramebufferTextureLayer + glReadPixels path.
  • Write offset: chosen by the cube-map face index (0..5).
  • Write size: one slice = (width >> level)² × 4 bytes, chosen via texture dimensions.
  • Write content: fully attacker-controlled.

The sibling glFramebufferTexture2D rejects the identical arguments with INVALID_OPERATION, confirming the intended validation is missing only here.


The cause

What version of Chrome have you found the security issue in?

153.0.8010.0 (dev/trunk) — patch base commit 1eed44bbb4b6 . Also present in the audited ANGLE checkout e4499e6b2835 and on ANGLE main aa192212af54; the vulnerable validator is unchanged, so all current Stable/Beta/Dev channels are affected.

Yes, it is related to a crash.

Choose the type of vulnerability

Memory Corruption (in a sandboxed process)

How would you like to be publicly acknowledged for your report?

Jinpyo Lee

View on issue tracker