CVE-2026-87604
Overview
Background
- ANGLE translator
- The
src/compiler/translatorcomponent that parses GLSL ES source and validates/rewrites it before handing it to a backend (Vulkan, GL, D3D), including semantic checks inParseContext.cpp. - External sampler
- A
samplerExternalOES(or the YUV variant__samplerExternal2DY2YEXT) that samples an externally-supplied image such as a multi-plane YUV surface, and which ANGLE lowers into one or more backend sampler/plane bindings rather than a single sampler. - `rejectWebglShadersWithUndefinedBehavior`
- A compile option in
mCompileOptions, set for WebGL/hardened contexts, that makes the translator reject shader constructs whose backend behavior is undefined instead of compiling them. - `EbtSamplerExternalOES` / `EbtSamplerExternal2DY2YEXT`
- The
TBasicTypeenum values the translator uses to tag the two external-sampler element types being screened by the new check.
Root Cause Analysis
The ANGLE translator permitted GLSL ES shaders to declare arrays of external samplers (e.g. uniform samplerExternalOES textures[2];), but the Vulkan backend does not implement arrays of external samplers correctly — external samplers, especially YUV ones, are lowered into multiple per-plane bindings, and the array indexing/expansion of those bindings is broken. When such a shader was accepted and drawn with, the backend indexed sampler/descriptor state that was not laid out to match the declared array, producing an out-of-bounds read (inferred) in ANGLE.
The fix adds a check in the array-element-type validation path: when mCompileOptions.rejectWebglShadersWithUndefinedBehavior is set and the array element type is EbtSamplerExternalOES or EbtSamplerExternal2DY2YEXT, the translator emits the error "arrays of external samplers are currently unsupported" and returns false, failing compilation. The check is intentionally gated on the hardened/WebGL option so that non-hardened contexts and existing dEQP tests (which create but never draw with such shaders) still compile. This works because the malformed shader is now rejected at compile time, before any backend code that would perform the unsupported indexing ever runs.
Attack Path
- Craft a malicious shader
An attacker authors a WebGL2 fragment shader that declares an array of external samplers, e.g.
uniform samplerExternalOES textures[2];, and samples multiple elements of it. - Reach the translator
A web page submits the shader through WebGL; ANGLE’s translator validates it, and pre-patch the array-element-type check did not screen
EbtSamplerExternalOES/EbtSamplerExternal2DY2YEXT, so compilation succeeded. - Bind external images and draw The page binds YUV/external EGL images to the sampler array units and issues a draw, invoking the Vulkan backend path that lowers and indexes the external-sampler array bindings.
- Trigger the out-of-bounds read
Because the backend does not correctly handle arrays of external samplers, the draw indexes sampler/plane state outside the intended bounds, causing an out-of-bounds read in ANGLE
(inferred).
Impact Assessment
(inferred). The precondition is a context where external-sampler arrays could previously compile and be drawn (WebGL/hardened path) with the external-image and YUV extensions available. The advisory classifies this as an out-of-bounds read in ANGLE; any escalation beyond information disclosure is (inferred) and not demonstrated by the patch, and no in-the-wild exploitation is stated.Changed Functions
| Function | Change | Notes |
|---|---|---|
WebGLGLSLValidationExtensionDisableTestsrc/tests/gl_tests/GLSLValidationTest.cpp |
modified |
Files Changed
src/compiler/translator/ParseContext.cppsrc/libANGLE/renderer/load_functions_table_autogen.cppsrc/libANGLE/renderer/load_functions_data.jsonsrc/tests/gl_tests/ImageTest.cppsrc/tests/gl_tests/GLSLValidationTest.cppscripts/code_generation_hashes/ANGLE_load_functions_table.jsonsrc/tests/angle_end2end_tests_expectations.txtsrc/tests/capture_replay_tests/capture_replay_expectations.txt
Audit Directions
- Backend-unsupported constructs accepted by the translatorAudit other type/qualifier validation paths in
ParseContext.cppfor shader features (opaque types, arrays-of-arrays of samplers/images) that compile but are unimplemented or broken in a backend, since those defer failure to unguarded backend indexing. - External/YUV sampler loweringReview the Vulkan backend’s lowering and descriptor indexing for external and multi-plane YUV samplers, where a single declared sampler expands into multiple bindings and array indexing can desynchronize from the actual binding layout.
- Hardened-only gatingCheck that constructs gated behind
rejectWebglShadersWithUndefinedBehaviorare genuinely safe (or also rejected) in non-hardened contexts, and that similar undefined-behavior cases are consistently screened for WebGL rather than left to per-backend behavior.
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/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*.
+ 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 textures[2];
+out vec4 fragColor;
+in vec2 texcoord;
+void main()
+{
+ fragColor = texture(otherSampler, texcoord) * 0.001
+ + max(texture(textures[0], texcoord), vec4(0))
+ + max(texture(textures[1], texcoord), vec4(0));
+})";
+
+ ANGLE_GL_PROGRAM(arrayProgram, kVS, kFS);
+ glUseProgram(arrayProgram);
+
... (file diff truncated)
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.