CVE-2026-87604
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
WebGLGLSLValidationExtensionDisableTestsrc/tests/gl_tests/GLSLValidationTest.cpp |
modified |
Files Changed
scripts/code_generation_hashes/ANGLE_load_functions_table.jsonsrc/compiler/translator/ParseContext.cppsrc/libANGLE/renderer/load_functions_data.jsonsrc/libANGLE/renderer/load_functions_table_autogen.cppsrc/tests/angle_end2end_tests_expectations.txtsrc/tests/capture_replay_tests/capture_replay_expectations.txtsrc/tests/gl_tests/GLSLValidationTest.cppsrc/tests/gl_tests/ImageTest.cpp
Patch
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.
Regression Test / PoC
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)
Original Bug Report
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.cppthird_party/angle/src/libANGLE/renderer/vulkan/vk_cache_utils.cppthird_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:
-
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_essl3extension is unconditionally requested and enabled (gpu/command_buffer/service/gles2_cmd_decoder_passthrough.cc:252). -
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:1957processes the uniform and setssamplerBinding.textureUnitsCountto8. -
Texture Instantiation & Binding: The attacker procures a SharedImage mailbox for an
AHardwareBuffercontaining YCbCr/YUV data and imports it viaCreateAndTexStorage2DSharedImageINTERNAL. The resulting Vulkan image has a validmYcbcrConversionDesc(HardwareBufferImageSiblingVkAndroid.cpp:515-541), causingImageHelper::hasImmutableSampler()to returntrue(vk_helpers.h:2960). The texture is bound to texture unit 0. -
Triggering Draw Call: The attacker sends
gles2::cmds::DrawArrays.ContextVk::updateActiveTextures(ContextVk.cpp:7313) detects the new immutable sampler, settingrecreatePipelineLayout = true(ContextVk.cpp:7401-7408). -
Bypassing Guard #1:
executableVk->createPipelineLayout(...)invokesaddTextureDescriptorSetDesc(...)(ProgramExecutableVk.cpp:1388). Here,arraySizeis read as 8. The code queries the texture and confirmshasImmutableSampler()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 -
Heap Buffer Allocation & Bypassing Guard #2: Inside
vk_cache_utils.cpp:4991(addBinding),bindingIndex = 1andcount = 8are processed. The code resizes the vector:mImmutableSamplers.resize(bindingIndex + 1)(i.e.,resize(2)). SincemImmutableSamplersis anangle::FastVector<VkSampler, 1>, resizing to 2 spills to a dynamically allocated heap array of2 * sizeof(VkSampler) = 16bytes (common/FastVector.h:574-597).ASSERT(count == 1); // Guard #2 - Compiles out in release SetBitField(packedBinding.hasImmutableSampler, 1); mImmutableSamplers[bindingIndex] = immutableSampler->getHandle(); // stores single handleThe
packedBinding.countbitfield is set to 8. -
Bypassing Guard #3 & OOB Pointer Setup:
getDescriptorSetLayoutcallsdesc.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]; }pImmutableSamplerspoints to&mImmutableSamplers[1], the final 8-byte slot of the 16-byte heap allocation. -
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 readsdescriptorCount(8) elements frompImmutableSamplers. It reads8 * 8 = 64bytes starting at&mImmutableSamplers[1], causing a 56-byte host heap OOB read. The driver dereferences these garbage bytes asVkSampler(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.cppverification:EbtSamplerExternalOESis categorized normally inBaseTypes.h:189-191, andParseContext.cpp:7283-7287confirms no backend rejections for arrays of external samplers.HardwareBufferImageSiblingVkAndroid.cpp:initImpl(lines 314-572) validates that an external format populatesconversionDescwhich is assigned tomYcbcrConversionDesc(vk_helpers.cpp:5978), correctly resolvinghasImmutableSampler()totrue(vk_helpers.h:2960).angle::FastVector: Evaluatedthird_party/angle/src/common/FastVector.h:183-187and570-594.FastVector<T, 1>withN=2results in dynamic allocation of exactly2 * 8 = 16bytes on 64-bit architectures vianew value_type[newSize].- Layout Sync: Checked
vk_wrapper.h:2031-2036andvk_cache_utils.cpp:5042-5052. Verified that no downstream sanity checks exist betweenunpackBindingsreturningdescriptorCount = 8and the rawvkCreateDescriptorSetLayoutexecution.
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.