CVE-2026-106293
Overview
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 withtextarget.
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.
Attack Path
- ENTRY From WebGL with EXT_multisampled_render_to_texture, create a texture of one type (e.g. cube map).
- CONTROLLED INPUT
Call
glFramebufferTexture2DMultisampleEXTwith a mismatchedtextarget(e.g.GL_TEXTURE_2D). - MISSING CHECK
MSRTT did not validate
tex->getType()againsttextarget, so the mismatched attachment was accepted. - PRIMITIVE The backend then operates on the texture as the wrong type (type confusion).
- 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
Files Changed
src/tests/gl_tests/MultisampledRenderToTextureTest.cppsrc/libANGLE/validationES2.cpp
Audit Directions
- Validation parity across entry pointsAudit paired entry points (2D vs multisample, EXT variants) for validation present in one but missing in the other; share the check.
- Type/target matchEnumerate all framebuffer-attachment entry points and confirm each validates texture type against the target.
Patch
From 803dfde1e22ed047be3e05028bd094cc2c716267 Mon Sep 17 00:00:00 2001 From: Shahbaz Youssefi <[email protected]> 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 <[email protected]> Auto-Submit: Shahbaz Youssefi <[email protected]> Reviewed-by: Amirali Abdolrashidi <[email protected]> --- 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.
Regression Test / PoC
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.
Original Bug Report
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:
- 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.
- 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.
Is the security issue related to a crash?
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