High watch CVSS 8.3 chrome OOB 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
8.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactOut of bounds read in ANGLE
DescriptionOut of bounds read in ANGLE
ComponentANGLE
Sub-componentTranslator
Bug ClassOOB
Action tierwatch — information disclosure — security-relevant, below the RCE-class bar
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker536648007
Fix commit72a097d38faf (angle/angle) +323/-4
CWECWE-125 (Out-of-bounds read)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-07-19 (per vendor advisory)
Fix landed2026-07-31 (public git)
Disclosed2026-09-08
Patch gap39 days — fix public in source before the advisory

Background

ANGLE translator
The src/compiler/translator component that parses GLSL ES source and validates/rewrites it before handing it to a backend (Vulkan, GL, D3D), including semantic checks in ParseContext.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 TBasicType enum 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.

Key insight
The core mistake was accepting a shader construct — arrays of external samplers — that no hardened backend actually supports, letting compilation succeed and deferring the failure to a broken backend code path that reads out of bounds; the fix closes the gap by rejecting the construct up front in the translator for hardened/WebGL contexts.

Attack Path

  1. 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.
  2. 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.
  3. 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.
  4. 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

An attacker who can run WebGL2 shaders in a victim’s browser can trigger an out-of-bounds read in ANGLE, which runs in the GPU process, potentially leaking adjacent memory contents (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

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

Files Changed

  • src/compiler/translator/ParseContext.cpp
  • src/libANGLE/renderer/load_functions_table_autogen.cpp
  • src/libANGLE/renderer/load_functions_data.json
  • src/tests/gl_tests/ImageTest.cpp
  • src/tests/gl_tests/GLSLValidationTest.cpp
  • scripts/code_generation_hashes/ANGLE_load_functions_table.json
  • src/tests/angle_end2end_tests_expectations.txt
  • src/tests/capture_replay_tests/capture_replay_expectations.txt

Audit Directions

  • Backend-unsupported constructs accepted by the translator
    Audit other type/qualifier validation paths in ParseContext.cpp for 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 lowering
    Review 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 gating
    Check that constructs gated behind rejectWebglShadersWithUndefinedBehavior are 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.
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)
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