Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactOut of bounds read in ANGLE
DescriptionOut of bounds read in ANGLE
ComponentANGLE
Bug ClassOOB
Tracker536648007
Fix commit72a097d38faf (angle/angle) +323/-4
CISA KEVNot listed
CreditedGoogle
Disclosed2026-09-08

Changed Functions

FunctionChangeNotes
WebGLGLSLValidationExtensionDisableTest
src/tests/gl_tests/GLSLValidationTest.cpp
modified

Files Changed

  • scripts/code_generation_hashes/ANGLE_load_functions_table.json
  • src/compiler/translator/ParseContext.cpp
  • src/libANGLE/renderer/load_functions_data.json
  • src/libANGLE/renderer/load_functions_table_autogen.cpp
  • src/tests/angle_end2end_tests_expectations.txt
  • src/tests/capture_replay_tests/capture_replay_expectations.txt
  • src/tests/gl_tests/GLSLValidationTest.cpp
  • src/tests/gl_tests/ImageTest.cpp
From 72a097d38fafd58d33d3ae34c3fe4ab75acb5595 Mon Sep 17 00:00:00 2001
From: Shahbaz Youssefi <syoussefi@chromium.org>
Date: Wed, 01 Apr 2026 14:19:28 -0400
Subject: [PATCH] Translator: Disallow arrays of external samplers

The Vulkan backend does not support it yet, and there are seemingly no
users.  For now, catch this unsupported case in the translator and fail
compilation.  This is only done for webgl/hardened contexts, as there
are dEQP tests that create such shaders, even though they don't draw
with them.

Test credit jerrylulu7 AT gmail DOT com

Bug: angleproject:158215272
Bug: chromium:536648007
Change-Id: Iaddcdbbfdb5bdaa180cb4924187723ca33a8a099
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7727387
Commit-Queue: Shahbaz Youssefi <syoussefi@chromium.org>
Auto-Submit: Shahbaz Youssefi <syoussefi@chromium.org>
Reviewed-by: Yuxin Hu <yuxinhu@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 304c927..9f90b2c5 100644
--- a/scripts/code_generation_hashes/ANGLE_load_functions_table.json
+++ b/scripts/code_generation_hashes/ANGLE_load_functions_table.json
@@ -4,7 +4,7 @@
   "src/libANGLE/renderer/gen_load_functions_table.py":
     "10e66c76a350a1c70fadd2e10a28981e",
   "src/libANGLE/renderer/load_functions_data.json":
-    "cf538e24465732f6ca79cc3b91833451",
+    "2e978236d74283de6b986580fea099b4",
   "src/libANGLE/renderer/load_functions_table_autogen.cpp":
-    "c10713aabfbb66dae7d9b7ee89454bc4"
+    "f06e85caeb22491b9dd010b7fac3cecb"
 }
diff --git a/src/compiler/translator/ParseContext.cpp b/src/compiler/translator/ParseContext.cpp
index 8207d7a..9abfb33 100644
--- a/src/compiler/translator/ParseContext.cpp
+++ b/src/compiler/translator/ParseContext.cpp
@@ -1894,6 +1894,20 @@
               typeString.c_str());
         return false;
     }
+
+    // Support for arrays of samplerExternalOES and __samplerExternal2DY2Y are broken in some
+    // backends.
+    if (mCompileOptions.rejectWebglShadersWithUndefinedBehavior &&
+        (elementType.getBasicType() == EbtSamplerExternalOES ||
+         elementType.getBasicType() == EbtSamplerExternal2DY2YEXT))
+    {
+        TInfoSinkBase typeString;
+        typeString << TType(elementType);
+        error(indexLocation, "arrays of external samplers are currently unsupported",
+              typeString.c_str());
+        return false;
+    }
+
     return checkIsValidQualifierForArray(indexLocation, elementType);
 }
 
diff --git a/src/libANGLE/renderer/load_functions_data.json b/src/libANGLE/renderer/load_functions_data.json
index 3c878af..97918f4 100644
--- a/src/libANGLE/renderer/load_functions_data.json
+++ b/src/libANGLE/renderer/load_functions_data.json
@@ -1313,7 +1313,7 @@
   },
   "GL_G8_B8_R8_3PLANE_420_UNORM_ANGLE": {
     "NONE": {
-      "GL_UNSIGNED_BYTE": "UnimplementedLoadFunction"
+      "GL_UNSIGNED_BYTE": "LoadYuvToNative"
     }
   },
   "GL_G10X6_B10X6R10X6_2PLANE_420_UNORM_3PACK16_ANGLE": {
diff --git a/src/libANGLE/renderer/load_functions_table_autogen.cpp b/src/libANGLE/renderer/load_functions_table_autogen.cpp
index 32d21e6..823043e 100644
--- a/src/libANGLE/renderer/load_functions_table_autogen.cpp
+++ b/src/libANGLE/renderer/load_functions_table_autogen.cpp
@@ -1988,7 +1988,7 @@
     switch (type)
     {
         case GL_UNSIGNED_BYTE:
-            return LoadImageFunctionInfo(UnimplementedLoadFunction, true);
+            return LoadImageFunctionInfo(LoadYuvToNative, true);
         default:
             UNREACHABLE();
             return LoadImageFunctionInfo(UnreachableLoadFunction, true);
diff --git a/src/tests/angle_end2end_tests_expectations.txt b/src/tests/angle_end2end_tests_expectations.txt
index 3d87f92..b2bf472 100644
--- a/src/tests/angle_end2end_tests_expectations.txt
+++ b/src/tests/angle_end2end_tests_expectations.txt
@@ -37,6 +37,8 @@
 532592076 : ImageTestES3RobustInit.RobustClearInTarget/* = SKIP
 532592076 : ImageTestES3RobustInit.NonZeroLevelAndSliceReadback/* = SKIP
 532592076 : ImageTestES3RobustInit.NonZeroLevelAndSliceReadbackRenderbuffer/* = SKIP
+// Array external samplers are not supported by Vulkan
+158215272 VULKAN : ImageTestES3.SourceYUVTextureTargetExternalRGBSampleArray/* = SKIP
 
 // Generate mipmap or draw on mismatched stale levels fails on some OpenGL/GLES drivers when robust init is enabled
 532617619 OPENGL : Texture2DTestES3RobustInit.MismatchedStaleLevel*/* = SKIP
@@ -369,6 +371,7 @@
 42265709 NVIDIA VULKAN : GLSLTest_ES31.TessellationControlShaderMatrixCopyBug/* = SKIP
 42265709 NVIDIA VULKAN : GLSLTest_ES31.TessellationControlShaderMatrixMultiplicationBug/* = SKIP
 537235696 NVIDIA OPENGL : ClipDistance*.ThreeClipDistancesRedeclaredAndPassedToFunction/* = SKIP
+540256225 NVIDIA VULKAN : ImageTestES3.SourceYUVTextureTargetExternalRGBSample/* = SKIP
 
 // Intel Vulkan
 
@@ -413,6 +416,7 @@
 536046166 INTEL VULKAN : FramebufferTest_ES31.MultisampleResolveWithBlitIntoMixedTexturesAfterFinish/* = SKIP
 536046166 INTEL VULKAN : FramebufferTest_ES31_MSAA.MultisampleResolveWithBlitIntoMixedTextures/* = SKIP
 536046166 WIN INTEL VULKAN : FramebufferTest_ES31_MSAA.MultisampleStencilSampling/*EmulatedPrerotation* = SKIP
+540256225 INTEL VULKAN : ImageTestES3.SourceYUVTextureTargetExternalRGBSample/* = SKIP
 
 // Robust init of OOB framebuffer reads is not zero'd
 500528706 VULKAN : CopyTexImageTestRobustResourceInit.OOBReadShouldBeZeroInit/* = SKIP
@@ -805,6 +809,7 @@
 40096826 PIXEL6 VULKAN : GLSLTest_ES31.ConsecutiveArraysOfArraysImage/* = SKIP
 40096826 PIXEL6 VULKAN : GeometryShaderTest.GeometryShaderImplementationDependentLimits/* = SKIP
 40096826 PIXEL6 VULKAN : ImageTestES3.RGBXAHBImport* = SKIP
+540256225 PIXEL6 VULKAN : ImageTestES3.SourceYUVTextureTargetExternalRGBSample/* = SKIP
 40096826 PIXEL6 VULKAN : IndexedBufferCopyTest.IndexRangeBug/* = SKIP
 40096826 PIXEL6 VULKAN : IndexedPointsTest* = SKIP
 40096826 PIXEL6 VULKAN : MultisampleTest.Point/* = SKIP
@@ -1516,6 +1521,7 @@
 
 // Fails on swangle builders
 42266918 SWIFTSHADER : WebGL2GLSLTest.TexelFetchLodOutOfBounds/* = SKIP
+540256225 SWIFTSHADER : ImageTestES3.SourceYUVTextureTargetExternalRGBSample/* = SKIP
 
 // HLSL compiler bug
 42261155 D3D11 : GLSLTest_ES3.ComplexCrossExpression/* = SKIP
diff --git a/src/tests/capture_replay_tests/capture_replay_expectations.txt b/src/tests/capture_replay_tests/capture_replay_expectations.txt
index bfdbd63..3a6a919 100644
--- a/src/tests/capture_replay_tests/capture_replay_expectations.txt
+++ b/src/tests/capture_replay_tests/capture_replay_expectations.txt
@@ -260,6 +260,7 @@
 # VVL error
 42264614 LINUX : VulkanExternalImageTestES31.UninitializedOnGLImportAndStorageWrite/* = SKIP_FOR_CAPTURE
 42264614 LINUX : VulkanExternalImageTest.NonRenderableWithFlags/* = SKIP_FOR_CAPTURE
+540256225 : ImageTestES3.SourceYUVTextureTargetExternalRGBSample/* = SKIP_FOR_CAPTURE
 
 # CoherentBufferTracker deadlocks and flaky crashes
 42266529 LINUX : BufferDataTestES3.* = SKIP_FOR_CAPTURE
diff --git a/src/tests/gl_tests/GLSLValidationTest.cpp b/src/tests/gl_tests/GLSLValidationTest.cpp
index df0e55d..b7240c7 100644
--- a/src/tests/gl_tests/GLSLValidationTest.cpp
+++ b/src/tests/gl_tests/GLSLValidationTest.cpp
@@ -6582,6 +6582,42 @@
     validateSuccess(GL_FRAGMENT_SHADER, kFS);
 }
 
+// External sampler arrays are not implemented correctly and so are forbidden for now.
+TEST_P(WebGL2GLSLValidationTest, SamplerExternalArray)
+{
+    ANGLE_SKIP_TEST_IF(!IsGLExtensionEnabled("GL_OES_EGL_image_external"));
+
+    constexpr char kFS[] = R"(#version 300 es
+#extension GL_OES_EGL_image_external_essl3 : require
+precision highp float;
+uniform samplerExternalOES textures[2];
+out vec4 fragColor;
+void main()
+{
+    fragColor = texture(textures[0], vec2(0))
+              + texture(textures[1], vec2(0));
+})";
+    validateError(GL_FRAGMENT_SHADER, kFS, "arrays of external samplers are currently unsupported");
+}
+
+// External Y2Y sampler arrays are not implemented correctly and so are forbidden for now.
+TEST_P(WebGL2GLSLValidationTest, SamplerExternalY2YArray)
+{
+    ANGLE_SKIP_TEST_IF(!IsGLExtensionEnabled("GL_EXT_YUV_target"));
+
+    constexpr char kFS[] = R"(#version 300 es
+#extension GL_EXT_YUV_target : require
+precision highp float;
+uniform __samplerExternal2DY2YEXT textures[2];
+out vec4 fragColor;
+void main()
+{
+    fragColor = texture(textures[0], vec2(0))
+              + texture(textures[1], vec2(0));
+})";
+    validateError(GL_FRAGMENT_SHADER, kFS, "arrays of external samplers are currently unsupported");
+}
+
 class WebGLGLSLValidationExtensionDisableTest : public WebGLGLSLValidationTest
 {};
 
diff --git a/src/tests/gl_tests/ImageTest.cpp b/src/tests/gl_tests/ImageTest.cpp
index 03826dd..c298ea7 100644
--- a/src/tests/gl_tests/ImageTest.cpp
+++ b/src/tests/gl_tests/ImageTest.cpp
@@ -4769,6 +4769,268 @@
     eglDestroyImageKHR(window->getDisplay(), image1);
 }
 
+// Test sampling from two YUV textures using GL_ANGLE_yuv_internal_format as external texture.
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/src/tests/angle_end2end_tests_expectations.txt b/src/tests/angle_end2end_tests_expectations.txt
index 3d87f92..b2bf472 100644
--- a/src/tests/angle_end2end_tests_expectations.txt
+++ b/src/tests/angle_end2end_tests_expectations.txt
@@ -37,6 +37,8 @@
 532592076 : ImageTestES3RobustInit.RobustClearInTarget/* = SKIP
 532592076 : ImageTestES3RobustInit.NonZeroLevelAndSliceReadback/* = SKIP
 532592076 : ImageTestES3RobustInit.NonZeroLevelAndSliceReadbackRenderbuffer/* = SKIP
+// Array external samplers are not supported by Vulkan
+158215272 VULKAN : ImageTestES3.SourceYUVTextureTargetExternalRGBSampleArray/* = SKIP
 
 // Generate mipmap or draw on mismatched stale levels fails on some OpenGL/GLES drivers when robust init is enabled
 532617619 OPENGL : Texture2DTestES3RobustInit.MismatchedStaleLevel*/* = SKIP
@@ -369,6 +371,7 @@
 42265709 NVIDIA VULKAN : GLSLTest_ES31.TessellationControlShaderMatrixCopyBug/* = SKIP
 42265709 NVIDIA VULKAN : GLSLTest_ES31.TessellationControlShaderMatrixMultiplicationBug/* = SKIP
 537235696 NVIDIA OPENGL : ClipDistance*.ThreeClipDistancesRedeclaredAndPassedToFunction/* = SKIP
+540256225 NVIDIA VULKAN : ImageTestES3.SourceYUVTextureTargetExternalRGBSample/* = SKIP
 
 // Intel Vulkan
 
@@ -413,6 +416,7 @@
 536046166 INTEL VULKAN : FramebufferTest_ES31.MultisampleResolveWithBlitIntoMixedTexturesAfterFinish/* = SKIP
 536046166 INTEL VULKAN : FramebufferTest_ES31_MSAA.MultisampleResolveWithBlitIntoMixedTextures/* = SKIP
 536046166 WIN INTEL VULKAN : FramebufferTest_ES31_MSAA.MultisampleStencilSampling/*EmulatedPrerotation* = SKIP
+540256225 INTEL VULKAN : ImageTestES3.SourceYUVTextureTargetExternalRGBSample/* = SKIP
 
 // Robust init of OOB framebuffer reads is not zero'd
 500528706 VULKAN : CopyTexImageTestRobustResourceInit.OOBReadShouldBeZeroInit/* = SKIP
@@ -805,6 +809,7 @@
 40096826 PIXEL6 VULKAN : GLSLTest_ES31.ConsecutiveArraysOfArraysImage/* = SKIP
 40096826 PIXEL6 VULKAN : GeometryShaderTest.GeometryShaderImplementationDependentLimits/* = SKIP
 40096826 PIXEL6 VULKAN : ImageTestES3.RGBXAHBImport* = SKIP
+540256225 PIXEL6 VULKAN : ImageTestES3.SourceYUVTextureTargetExternalRGBSample/* = SKIP
 40096826 PIXEL6 VULKAN : IndexedBufferCopyTest.IndexRangeBug/* = SKIP
 40096826 PIXEL6 VULKAN : IndexedPointsTest* = SKIP
 40096826 PIXEL6 VULKAN : MultisampleTest.Point/* = SKIP
@@ -1516,6 +1521,7 @@
 
 // Fails on swangle builders
 42266918 SWIFTSHADER : WebGL2GLSLTest.TexelFetchLodOutOfBounds/* = SKIP
+540256225 SWIFTSHADER : ImageTestES3.SourceYUVTextureTargetExternalRGBSample/* = SKIP
 
 // HLSL compiler bug
 42261155 D3D11 : GLSLTest_ES3.ComplexCrossExpression/* = SKIP
diff --git a/src/tests/capture_replay_tests/capture_replay_expectations.txt b/src/tests/capture_replay_tests/capture_replay_expectations.txt
index bfdbd63..3a6a919 100644
--- a/src/tests/capture_replay_tests/capture_replay_expectations.txt
+++ b/src/tests/capture_replay_tests/capture_replay_expectations.txt
@@ -260,6 +260,7 @@
 # VVL error
 42264614 LINUX : VulkanExternalImageTestES31.UninitializedOnGLImportAndStorageWrite/* = SKIP_FOR_CAPTURE
 42264614 LINUX : VulkanExternalImageTest.NonRenderableWithFlags/* = SKIP_FOR_CAPTURE
+540256225 : ImageTestES3.SourceYUVTextureTargetExternalRGBSample/* = SKIP_FOR_CAPTURE
 
 # CoherentBufferTracker deadlocks and flaky crashes
 42266529 LINUX : BufferDataTestES3.* = SKIP_FOR_CAPTURE
diff --git a/src/tests/gl_tests/GLSLValidationTest.cpp b/src/tests/gl_tests/GLSLValidationTest.cpp
index df0e55d..b7240c7 100644
--- a/src/tests/gl_tests/GLSLValidationTest.cpp
+++ b/src/tests/gl_tests/GLSLValidationTest.cpp
@@ -6582,6 +6582,42 @@
     validateSuccess(GL_FRAGMENT_SHADER, kFS);
 }
 
+// External sampler arrays are not implemented correctly and so are forbidden for now.
+TEST_P(WebGL2GLSLValidationTest, SamplerExternalArray)
+{
+    ANGLE_SKIP_TEST_IF(!IsGLExtensionEnabled("GL_OES_EGL_image_external"));
+
+    constexpr char kFS[] = R"(#version 300 es
+#extension GL_OES_EGL_image_external_essl3 : require
+precision highp float;
+uniform samplerExternalOES textures[2];
+out vec4 fragColor;
+void main()
+{
+    fragColor = texture(textures[0], vec2(0))
+              + texture(textures[1], vec2(0));
+})";
+    validateError(GL_FRAGMENT_SHADER, kFS, "arrays of external samplers are currently unsupported");
+}
+
+// External Y2Y sampler arrays are not implemented correctly and so are forbidden for now.
+TEST_P(WebGL2GLSLValidationTest, SamplerExternalY2YArray)
+{
+    ANGLE_SKIP_TEST_IF(!IsGLExtensionEnabled("GL_EXT_YUV_target"));
+
+    constexpr char kFS[] = R"(#version 300 es
+#extension GL_EXT_YUV_target : require
+precision highp float;
+uniform __samplerExternal2DY2YEXT textures[2];
+out vec4 fragColor;
+void main()
+{
+    fragColor = texture(textures[0], vec2(0))
+              + texture(textures[1], vec2(0));
+})";
+    validateError(GL_FRAGMENT_SHADER, kFS, "arrays of external samplers are currently unsupported");
+}
+
 class WebGLGLSLValidationExtensionDisableTest : public WebGLGLSLValidationTest
 {};
diff --git a/src/tests/gl_tests/ImageTest.cpp b/src/tests/gl_tests/ImageTest.cpp
index 03826dd..c298ea7 100644
--- a/src/tests/gl_tests/ImageTest.cpp
+++ b/src/tests/gl_tests/ImageTest.cpp
@@ -4769,6 +4769,268 @@
     eglDestroyImageKHR(window->getDisplay(), image1);
 }
 
+// Test sampling from two YUV textures using GL_ANGLE_yuv_internal_format as external texture.
+TEST_P(ImageTestES3, SourceYUVTextureTargetExternalRGBSample)
+{
+    ANGLE_SKIP_TEST_IF(!hasOESExt() || !hasBaseExt() || !has2DTextureExt() ||
+                       !hasYUVInternalFormatExt() || !hasExternalESSL3Ext());
+
+    // YUV color data for Red: 4 Y bytes + 2 CbCr bytes for a 2x2 NV12 image
+    constexpr GLubyte kYuv2PlaneColor[6] = {40, 40, 40, 40, 240, 109};
+    // YUV color data for Green : 4 Y bytes + 2 CbCr bytes for a 2x2 YV12 image
+    constexpr GLubyte kYuv3PlaneColor[6] = {144, 144, 144, 144, 54, 34};
+    constexpr size_t kWidth              = 2;
+    constexpr size_t kHeight             = 2;
+
+    // Create first 2-plane YUV texture
+    GLTexture yuvTexture0;
+    glBindTexture(GL_TEXTURE_2D, yuvTexture0);
+    glTexStorage2D(GL_TEXTURE_2D, 1, GL_G8_B8R8_2PLANE_420_UNORM_ANGLE, kWidth, kHeight);
+    ASSERT_GL_NO_ERROR();
+    glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, kWidth, kHeight, GL_G8_B8R8_2PLANE_420_UNORM_ANGLE,
+                    GL_UNSIGNED_BYTE, kYuv2PlaneColor);
+    ASSERT_GL_NO_ERROR();
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+    ASSERT_GL_NO_ERROR();
+
+    // Create second 2-plane YUV texture with a different format
+    GLTexture yuvTexture1;
+    glBindTexture(GL_TEXTURE_2D, yuvTexture1);
+    glTexStorage2D(GL_TEXTURE_2D, 1, GL_G8_B8_R8_3PLANE_420_UNORM_ANGLE, kWidth, kHeight);
+    ASSERT_GL_NO_ERROR();
+    glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, kWidth, kHeight, GL_G8_B8_R8_3PLANE_420_UNORM_ANGLE,
+                    GL_UNSIGNED_BYTE, kYuv3PlaneColor);
+    ASSERT_GL_NO_ERROR();
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+    ASSERT_GL_NO_ERROR();
+
+    // Create EGL images from the YUV textures
+    EGLWindow *window = getEGLWindow();
+    EGLImageKHR image0 =
+        eglCreateImageKHR(window->getDisplay(), window->getContext(), EGL_GL_TEXTURE_2D_KHR,
+                          reinterpretHelper<EGLClientBuffer>(yuvTexture0), kDefaultAttribs);
+    ASSERT_EGL_SUCCESS();
+
+    EGLImageKHR image1 =
+        eglCreateImageKHR(window->getDisplay(), window->getContext(), EGL_GL_TEXTURE_2D_KHR,
+                          reinterpretHelper<EGLClientBuffer>(yuvTexture1), kDefaultAttribs);
+    ASSERT_EGL_SUCCESS();
+
+    // Create external texture targets bound to the EGL images
+    GLTexture target0;
+    createEGLImageTargetTextureExternal(image0, target0);
+
+    GLTexture target1;
+    createEGLImageTargetTextureExternal(image1, target1);
+
+    // Use two samplerExternalOESs.
+    constexpr char kVS[] = R"(#version 300 es
+in vec4 position;
+out vec2 texcoord;
+void main()
+{
+    gl_Position = vec4(position.xy, 0.0, 1.0);
+    texcoord = (position.xy * 0.5) + 0.5;
+})";
+
+    constexpr char kFS[] = R"(#version 300 es
+#extension GL_OES_EGL_image_external_essl3 : require
+precision highp float;
+uniform sampler2D otherSampler;
+uniform samplerExternalOES texture0;
+uniform samplerExternalOES texture1;
+out vec4 fragColor;
+in vec2 texcoord;
+void main()
+{
+    fragColor = texture(otherSampler, texcoord) * 0.001
+              + max(texture(texture0, texcoord), vec4(0))
+              + max(texture(texture1, texcoord), vec4(0));
+})";
+
+    ANGLE_GL_PROGRAM(arrayProgram, kVS, kFS);
+    glUseProgram(arrayProgram);
+
+    // Create a separate RGBA texture for the regular sampler2D binding
+    const std::array<GLColor, kWidth * kHeight> kRGBAInitData = {
+        GLColor::red,
+        GLColor::red,
+        GLColor::red,
+        GLColor::red,
+    };
+    GLTexture rgbaTexture;
+    glBindTexture(GL_TEXTURE_2D, rgbaTexture);
+    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, kWidth, kHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE,
+                 kRGBAInitData.data());
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+    ASSERT_GL_NO_ERROR();
+
+    GLint rgbaLoc = glGetUniformLocation(arrayProgram, "otherSampler");
+    ASSERT_NE(-1, rgbaLoc);
+    GLint tex0Loc = glGetUniformLocation(arrayProgram, "texture0");
+    ASSERT_NE(-1, tex0Loc);
+    GLint tex1Loc = glGetUniformLocation(arrayProgram, "texture1");
+    ASSERT_NE(-1, tex1Loc);
+
+    // Bind the RGBA texture to unit 2
+    glActiveTexture(GL_TEXTURE2);
+    glBindTexture(GL_TEXTURE_2D, rgbaTexture);
+
+    // Bind YUV external textures to units 0 and 1
+    glActiveTexture(GL_TEXTURE0);
+    glBindTexture(GL_TEXTURE_EXTERNAL_OES, target0);
+    glActiveTexture(GL_TEXTURE1);
+    glBindTexture(GL_TEXTURE_EXTERNAL_OES, target1);
+    ASSERT_GL_NO_ERROR();
+
+    glUniform1i(rgbaLoc, 2);
+    glUniform1i(tex0Loc, 0);
+    glUniform1i(tex1Loc, 1);
+
+    drawQuad(arrayProgram, "position", 0.5f);
+    ASSERT_GL_NO_ERROR();
+
+    EXPECT_PIXEL_COLOR_NEAR(0, 0, GLColor::cyan, 1);
+
+    // Clean up
+    eglDestroyImageKHR(window->getDisplay(), image0);
+    eglDestroyImageKHR(window->getDisplay(), image1);
+}
+
+// Test sampling from two YUV textures using GL_ANGLE_yuv_internal_format as external texture, when
+// the shader uses an array samplerExternalOES declaration.
+TEST_P(ImageTestES3, SourceYUVTextureTargetExternalRGBSampleArray)
+{
+    ANGLE_SKIP_TEST_IF(!hasOESExt() || !hasBaseExt() || !has2DTextureExt() ||
+                       !hasYUVInternalFormatExt() || !hasExternalESSL3Ext());
+
+    // YUV color data for Red: 4 Y bytes + 2 CbCr bytes for a 2x2 NV12 image
+    constexpr GLubyte kYuv2PlaneColor[6] = {40, 40, 40, 40, 240, 109};
+    // YUV color data for Green : 4 Y bytes + 2 CbCr bytes for a 2x2 YV12 image
+    constexpr GLubyte kYuv3PlaneColor[6] = {144, 144, 144, 144, 54, 34};
+    constexpr size_t kWidth              = 2;
+    constexpr size_t kHeight             = 2;
+
+    // Create first 2-plane YUV texture
+    GLTexture yuvTexture0;
+    glBindTexture(GL_TEXTURE_2D, yuvTexture0);
+    glTexStorage2D(GL_TEXTURE_2D, 1, GL_G8_B8R8_2PLANE_420_UNORM_ANGLE, kWidth, kHeight);
+    ASSERT_GL_NO_ERROR();
+    glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, kWidth, kHeight, GL_G8_B8R8_2PLANE_420_UNORM_ANGLE,
+                    GL_UNSIGNED_BYTE, kYuv2PlaneColor);
+    ASSERT_GL_NO_ERROR();
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+    ASSERT_GL_NO_ERROR();
+
+    // Create second 2-plane YUV texture with a different format
+    GLTexture yuvTexture1;
+    glBindTexture(GL_TEXTURE_2D, yuvTexture1);
+    glTexStorage2D(GL_TEXTURE_2D, 1, GL_G8_B8_R8_3PLANE_420_UNORM_ANGLE, kWidth, kHeight);
+    ASSERT_GL_NO_ERROR();
+    glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, kWidth, kHeight, GL_G8_B8_R8_3PLANE_420_UNORM_ANGLE,
+                    GL_UNSIGNED_BYTE, kYuv3PlaneColor);
+    ASSERT_GL_NO_ERROR();
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+    ASSERT_GL_NO_ERROR();
+
+    // Create EGL images from the YUV textures
+    EGLWindow *window = getEGLWindow();
+    EGLImageKHR image0 =
+        eglCreateImageKHR(window->getDisplay(), window->getContext(), EGL_GL_TEXTURE_2D_KHR,
+                          reinterpretHelper<EGLClientBuffer>(yuvTexture0), kDefaultAttribs);
+    ASSERT_EGL_SUCCESS();
+
+    EGLImageKHR image1 =
+        eglCreateImageKHR(window->getDisplay(), window->getContext(), EGL_GL_TEXTURE_2D_KHR,
+                          reinterpretHelper<EGLClientBuffer>(yuvTexture1), kDefaultAttribs);
+    ASSERT_EGL_SUCCESS();
+
+    // Create external texture targets bound to the EGL images
+    GLTexture target0;
+    createEGLImageTargetTextureExternal(image0, target0);
+
+    GLTexture target1;
+    createEGLImageTargetTextureExternal(image1, target1);
+
+    // Use a samplerExternalOES *array*.
... (truncated)
Loading diff…

Original Bug Report

reported by aw...@chromium.org

Potential host-heap OOB read and wild pointer dereference in ANGLE Vulkan via unchecked sampler arrays

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: ANGLE’s Vulkan backend relies on debug-only assertions to ensure immutable sampler bindings have a descriptor count of 1. A compromised renderer can declare an array of external samplers, causing a host-heap out-of-bounds read and driver-level wild pointer dereference during vkCreateDescriptorSetLayout. On Android, this potentially leads to a GPU process sandbox escape.

Affected files:

  • third_party/angle/src/libANGLE/renderer/vulkan/ProgramExecutableVk.cpp
  • third_party/angle/src/libANGLE/renderer/vulkan/vk_cache_utils.cpp
  • third_party/angle/src/libANGLE/renderer/vulkan/vk_cache_utils.h

Estimated timestamp from git blame: 2024-05-03

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

ANGLE’s Vulkan backend relies exclusively on debug-only ASSERT() statements to enforce the assumption that immutable-sampler bindings have an array/descriptor size (descriptorCount) of exactly 1. When running release builds, there are no runtime checks to restrict or clamp this value when handling shader-declared arrays of external samplers.

A compromised renderer process (A-RENDERER) on Android can leverage this gap by compiling a shader that declares an array of external samplers (e.g., uniform samplerExternalOES vid[8];) and binding a YCbCr (such as an Android AHardwareBuffer-backed) texture to the first element (index 0). During the drawing sequence, ANGLE creates a pipeline layout where the DescriptorSetLayoutDesc has a descriptorCount of 8 but only writes a single VkSampler handle into mImmutableSamplers (an angle::FastVector<VkSampler, 1>).

When ANGLE unpacks the bindings to issue vkCreateDescriptorSetLayout, binding.pImmutableSamplers is configured to point into the mImmutableSamplers vector, but binding.descriptorCount remains 8. This directly violates Vulkan Specification VUID-VkDescriptorSetLayoutBinding-descriptorType-00282. The underlying Vulkan driver reads (8 - 1) * 8 = 56 bytes past the allocated bounds on the host heap (GPU-HOST-OOB read) and dereferences the resulting out-of-bounds bytes as non-dispatchable VkSampler handles. In the unsandboxed GPU process environment of Android, this driver-level wild pointer dereference results in memory corruption, enabling a potential sandbox escape.

Note: The exploitation steps described below outline a potential attack path based on static code analysis, as our tooling does not execute dynamic proofs-of-concept.

2. Proof-of-Concept & Detailed Execution Flow

Potential Attacker Steps & Execution Path:

  1. Context & Extension Initialization: The compromised renderer requests a Passthrough GLES2 context. On Android, ANGLE with the Vulkan backend is the default. During initialization, the GL_OES_EGL_image_external_essl3 extension is unconditionally requested and enabled (gpu/command_buffer/service/gles2_cmd_decoder_passthrough.cc:252).

  2. Shader Forgery: The attacker submits a fragment shader declaring an array of external samplers:

    #version 300 es
    #extension GL_OES_EGL_image_external_essl3 : require
    precision mediump float;
    uniform sampler2D pad;                 // Assigned bindingIndex = 0
    uniform samplerExternalOES vid[8];     // Assigned bindingIndex = 1, textureUnitsCount = 8
    out vec4 c;
    void main() {
        c = texture(vid[0], vec2(0));
    }
    

    The ANGLE frontend translator accepts arrays of samplerExternalOES. At link time, ProgramExecutable.cpp:1957 processes the uniform and sets samplerBinding.textureUnitsCount to 8.

  3. Texture Instantiation & Binding: The attacker procures a SharedImage mailbox for an AHardwareBuffer containing YCbCr/YUV data and imports it via CreateAndTexStorage2DSharedImageINTERNAL. The resulting Vulkan image has a valid mYcbcrConversionDesc (HardwareBufferImageSiblingVkAndroid.cpp:515-541), causing ImageHelper::hasImmutableSampler() to return true (vk_helpers.h:2960). The texture is bound to texture unit 0.

  4. Triggering Draw Call: The attacker sends gles2::cmds::DrawArrays. ContextVk::updateActiveTextures (ContextVk.cpp:7313) detects the new immutable sampler, setting recreatePipelineLayout = true (ContextVk.cpp:7401-7408).

  5. Bypassing Guard #1: executableVk->createPipelineLayout(...) invokes addTextureDescriptorSetDesc(...) (ProgramExecutableVk.cpp:1388). Here, arraySize is read as 8. The code queries the texture and confirms hasImmutableSampler() is true (ProgramExecutableVk.cpp:1400).

    ASSERT(samplerBinding.textureUnitsCount == 1); // Guard #1 - Compiles out in release
    // ...
    descOut->addBinding(info.binding, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, arraySize,
                        activeStages, &immutableSampler); // passes arraySize = 8
    
  6. Heap Buffer Allocation & Bypassing Guard #2: Inside vk_cache_utils.cpp:4991 (addBinding), bindingIndex = 1 and count = 8 are processed. The code resizes the vector: mImmutableSamplers.resize(bindingIndex + 1) (i.e., resize(2)). Since mImmutableSamplers is an angle::FastVector<VkSampler, 1>, resizing to 2 spills to a dynamically allocated heap array of 2 * sizeof(VkSampler) = 16 bytes (common/FastVector.h:574-597).

    ASSERT(count == 1); // Guard #2 - Compiles out in release
    SetBitField(packedBinding.hasImmutableSampler, 1);
    mImmutableSamplers[bindingIndex] = immutableSampler->getHandle(); // stores single handle
    

    The packedBinding.count bitfield is set to 8.

  7. Bypassing Guard #3 & OOB Pointer Setup: getDescriptorSetLayout calls desc.unpackBindings(&bindingVector) (vk_cache_utils.cpp:8487).

    binding.descriptorCount = packedBinding.count; // descriptorCount = 8
    // ...
    if (packedBinding.hasImmutableSampler) {
        ASSERT(packedBinding.count == 1); // Guard #3 - Compiles out in release
        binding.pImmutableSamplers = &mImmutableSamplers[bindingIndex]; 
    }
    

    pImmutableSamplers points to &mImmutableSamplers[1], the final 8-byte slot of the 16-byte heap allocation.

  8. Driver Sink Execution (OOB Read & Wild Pointer Deref): The binding vector is passed to vkCreateDescriptorSetLayout(device, &createInfo, nullptr, &mHandle) (vk_wrapper.h:2035). Per Vulkan spec, the driver reads descriptorCount (8) elements from pImmutableSamplers. It reads 8 * 8 = 64 bytes starting at &mImmutableSamplers[1], causing a 56-byte host heap OOB read. The driver dereferences these garbage bytes as VkSampler (non-dispatchable) handles, leading to memory corruption.

Suggested Fix

Convert the debug-only ASSERTs to release-build checks. For example, in ProgramExecutableVk::addTextureDescriptorSetDesc and DescriptorSetLayoutDesc::addBinding, if hasImmutableSampler is true and arraySize > 1 (or count > 1), return a validation error (e.g., vk::Result or angle::Result::Stop) and prevent the pipeline layout from being created.

3. Technical Verification Details (Automated Audit Logs)

> Verbatim Critic Verdict & Validation Notes: > * Severity: High (S1) > * Brief Notes / Reasoning: > The vulnerability is a host-heap OOB read leading to a driver-level wild pointer dereference in the GPU process. Root cause: ANGLE’s Vulkan backend relies on debug-only ASSERTs to verify that immutable-sampler bindings have a descriptorCount of 1. A compromised renderer can declare a shader with an array of external samplers (e.g., uniform samplerExternalOES vid[8]) and bind a YCbCr AHardwareBuffer texture. This causes vkCreateDescriptorSetLayout to receive a descriptorCount of 8 but a pImmutableSamplers pointer to a FastVector containing only a single VkSampler. The driver reads past the allocation bounds (host-heap OOB read) and dereferences the garbage handles (wild pointer dereference/write). >
> Severity Justification (High / S1): > - Sandbox Escape on Android: Per the severity guidelines and KB, GPU-process memory corruption reachable via forged command-buffer commands from a compromised renderer constitutes a Sandbox Escape on Android (where the GPU process is unsandboxed). This maps to S1. > - GPU Validating-Layer Gap: The report demonstrates a gap where an attacker-controlled value violates VUID-VkDescriptorSetLayoutBinding-descriptorType-00282 and reaches a driver backend call (vkCreateDescriptorSetLayout) leading to memory corruption (S1). > - Ceilings & Modifiers: This cannot be Critical (S0) because it is not directly reachable from web content (WebGL does not expose samplerExternalOES), thus it necessitates a compromised renderer. No mitigating factors apply: Vulkan is the default backend on Android, and MiraclePtr does not protect raw heap arrays read by driver code.

Automated Codebase Verification Highlights:

  • ParseContext.cpp verification: EbtSamplerExternalOES is categorized normally in BaseTypes.h:189-191, and ParseContext.cpp:7283-7287 confirms no backend rejections for arrays of external samplers.
  • HardwareBufferImageSiblingVkAndroid.cpp: initImpl (lines 314-572) validates that an external format populates conversionDesc which is assigned to mYcbcrConversionDesc (vk_helpers.cpp:5978), correctly resolving hasImmutableSampler() to true (vk_helpers.h:2960).
  • angle::FastVector: Evaluated third_party/angle/src/common/FastVector.h:183-187 and 570-594. FastVector<T, 1> with N=2 results in dynamic allocation of exactly 2 * 8 = 16 bytes on 64-bit architectures via new value_type[newSize].
  • Layout Sync: Checked vk_wrapper.h:2031-2036 and vk_cache_utils.cpp:5042-5052. Verified that no downstream sanity checks exist between unpackBindings returning descriptorCount = 8 and the raw vkCreateDescriptorSetLayout execution.

Evaluated with Chrome root at commit: b96d2ec58f4f5f92b540a723966b199d6e9951b4


Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:

  • If you are familiar with the severity guidelines, you may adjust the severity.
  • If this is a false positive, and there’s no work to be done, please close as WAI.
  • If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.

Data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.

View on issue tracker