Overview

High
Severity
4.7
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUninitialized resource in ANGLE
DescriptionUninitialized resource in ANGLE
ComponentANGLE
Sub-componentMetal
Bug ClassUninitialized Memory
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.
Tracker523470135
Fix commit9ebe6aecc5d8 (angle/angle) +160/-14
CWECWE-908
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:N/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-06-13 (per vendor advisory)
Fix landed2026-09-09 (public git)
Disclosed2026-09-15
Patch gap6 days — fix public in source before the advisory

Background

ANGLE
Chrome’s graphics abstraction layer that translates OpenGL ES calls onto native backends such as Metal, and here the metal backend is affected.
`RenderTargetMtl`
A Metal-backend object that binds a specific mtl texture image, mip level, and layer as the destination for render or copy operations.
Robust resource initialization
A GL security feature that guarantees newly-defined texture storage reads back as zero-initialized rather than leaking prior memory contents.
`imageDef.image`
The current native mtl image backing a given texture level/layer, which is replaced when the level is redefined via glTexImage/glCopyTexSubImage.

Root Cause Analysis

TextureMtl::getRenderTarget cached a RenderTargetMtl per image index in mRenderTargets and initialized it lazily, calling rtt.set() only when rtt.getTexture() returned null. This lazy-init guard was correct on first use but became stale whenever the texture level was redefined: a call such as glTexImage or glCopyTexSubImage allocates a NEW mtl image and updates imageDef.image, freeing or orphaning the old one. Because the cached rtt already held a non-null texture, getRenderTarget short-circuited and never re-pointed it at the new imageDef.image, leaving the render target aimed at the stale (freed/old) storage. Subsequent render or copy operations therefore wrote into or targeted the wrong image, so the newly-defined level was never actually initialized. A later sample or read of that level returned whatever uninitialized memory backed the mismatched storage, bypassing robust resource init.

The fix removes the null-guard entirely and unconditionally re-points rtt at the current imageDef.image on every call, guarded by CHECK(imageDef.image).

Key insight
A cache keyed only on “is the slot populated?” silently goes stale when the underlying resource is reallocated; the correct freshness signal was the current imageDef.image, not whether rtt already had any texture.

Attack Path

  1. Create mismatched level A page issues WebGL/WebGL2 calls that copy into a cubemap face (or texture level) at one size, allocating an mtl image and populating the cached RenderTargetMtl.
  2. Redefine the level The page redefines that same face to a new size via glTexImage/glCopyTexSubImage, allocating a fresh imageDef.image while the cached rtt still points at the old image.
  3. Trigger stale-targeted operation A render or copy that goes through getRenderTarget is diverted to the stale render target, so the newly-defined level’s storage is left uninitialized.
  4. Read back uninitialized data The page samples the redefined level, observing uninitialized GPU memory and defeating robust resource initialization.

Impact Assessment

This is an uninitialized-memory disclosure on the Metal backend that lets attacker-controlled WebGL content read residual GPU memory contents from a supposedly zero-initialized texture level. Because robust resource init is a cross-origin information-leak defense, the bypass can expose stale pixel or buffer data from prior allocations. Exploitation requires only standard WebGL API sequences reachable from an untrusted web page on Metal-backed (Apple) platforms.

Files Changed

  • src/tests/gl_tests/RobustResourceInitTest.cpp
  • src/libANGLE/renderer/metal/TextureMtl.mm

Audit Directions

  • Reallocation-invalidated caches
    Audit every cached backend object (render targets, views, framebuffer attachments) whose freshness is guarded by a null/populated check rather than by the identity of the underlying resource; redefinition should re-point or invalidate the cache.
  • Robust-init on redefinition paths
    Review glTexImage/glCopyTexSubImage and other level-redefine paths across backends to confirm the newly-allocated image, not a stale one, is the target of initialization and subsequent reads.
  • Lazy-init guards
    Flag if (!x.getY()) { init } patterns where the resource behind x can change lifetime independently, since the guard can permanently skip a required re-initialization.
From 9ebe6aecc5d8f9119c78fcb5804f6ba31f4d02e2 Mon Sep 17 00:00:00 2001
From: Le Hoang Quyen <[email protected]>
Date: Wed, 09 Sep 2026 22:54:35 +0800
Subject: [PATCH] Metal: Avoid using stale render target in TextureMtl::getRenderTarget

Previously, TextureMtl::getRenderTarget skipped calling rtt.set() if
rtt.getTexture() was already non-null. When a texture image was
redefined with a new image, this caused mRenderTargets to retain a stale
render target pointing to the old image, diverting subsequent render or
copy operations to the stale render target instead of the newly defined
image.

Update TextureMtl::getRenderTarget to always set rtt with the current
imageDef.image and add CHECK(imageDef.image).

Bug: angleproject:523470135
Change-Id: I7a49e2206cb33eaf5b6818bd1e4b65a9620a345d
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/8377204
Reviewed-by: Shahbaz Youssefi <[email protected]>
Commit-Queue: Quyen Le <[email protected]>
---
diff --git a/src/libANGLE/renderer/metal/TextureMtl.mm b/src/libANGLE/renderer/metal/TextureMtl.mm
index 7894742..ab28138 100644
--- a/src/libANGLE/renderer/metal/TextureMtl.mm
+++ b/src/libANGLE/renderer/metal/TextureMtl.mm
@@ -1419,21 +1419,15 @@
 
     GLuint layer         = GetImageLayerIndexFrom(imageIndex);
     RenderTargetMtl &rtt = mRenderTargets[imageIndex][renderToTextureIndex];
-    if (!rtt.getTexture())
+    CHECK(imageDef.image);
+    if (imageIndex.getType() == gl::TextureType::CubeMap)
     {
-        // Lazy initialization of render target:
-        if (imageDef.image)
-        {
-            if (imageIndex.getType() == gl::TextureType::CubeMap)
-            {
-                // Cube map is special, the image is already the view of its layer
-                rtt.set(imageDef.image, mtl::kZeroNativeMipLevel, 0, format);
-            }
-            else
-            {
-                rtt.set(imageDef.image, mtl::kZeroNativeMipLevel, layer, format);
-            }
-        }
+        // Cube map is special, the image is already the view of its layer
+        rtt.set(imageDef.image, mtl::kZeroNativeMipLevel, 0, format);
+    }
+    else
+    {
+        rtt.set(imageDef.image, mtl::kZeroNativeMipLevel, layer, format);
     }
 
     if (implicitSamples > 1 && !rtt.getImplicitMSTexture())
diff --git a/src/tests/gl_tests/RobustResourceInitTest.cpp b/src/tests/gl_tests/RobustResourceInitTest.cpp
index 7631497..f2bbb80 100644
--- a/src/tests/gl_tests/RobustResourceInitTest.cpp
+++ b/src/tests/gl_tests/RobustResourceInitTest.cpp
@@ -4444,6 +4444,158 @@
     EXPECT_GL_NO_ERROR();
 }
 
+// Tests that glCopyTexSubImage2D into a cubemap face of mismatched size, followed by
+// redefining the face to match the cubemap size, and copying into it with full coverage,
+// correctly updates the face and does not leave uninitialized data when sampled.
+TEST_P(RobustResourceInitTestES3, CubeMapCopySubImageMismatchedFaceRedefine)
+{
+    ANGLE_SKIP_TEST_IF(!hasGLExtension());
+
+    constexpr GLint kFaceSize = 16;
+    constexpr GLint kHalfSize = 8;
+
+    // Copy sources: green HxH and white SxS read framebuffers.
+    std::vector<GLColor> greenColors(kHalfSize * kHalfSize, GLColor::green);
+    GLTexture srcGreenTex;
+    glBindTexture(GL_TEXTURE_2D, srcGreenTex);
+    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kHalfSize, kHalfSize, 0, GL_RGBA, GL_UNSIGNED_BYTE,
+                 greenColors.data());
+    GLFramebuffer srcGreenFbo;
+    glBindFramebuffer(GL_FRAMEBUFFER, srcGreenFbo);
+    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, srcGreenTex, 0);
+    ASSERT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+
+    std::vector<GLColor> whiteColors(kFaceSize * kFaceSize, GLColor::white);
+    GLTexture srcWhiteTex;
+    glBindTexture(GL_TEXTURE_2D, srcWhiteTex);
+    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kFaceSize, kFaceSize, 0, GL_RGBA, GL_UNSIGNED_BYTE,
+                 whiteColors.data());
+    GLFramebuffer srcWhiteFbo;
+    glBindFramebuffer(GL_FRAMEBUFFER, srcWhiteFbo);
+    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, srcWhiteTex, 0);
+    ASSERT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+
+    // Destination framebuffer to sample cubemap into.
+    GLTexture dstTex;
+    glBindTexture(GL_TEXTURE_2D, dstTex);
+    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kFaceSize, kFaceSize, 0, GL_RGBA, GL_UNSIGNED_BYTE,
+                 nullptr);
+    GLFramebuffer dstFbo;
+    glBindFramebuffer(GL_FRAMEBUFFER, dstFbo);
+    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, dstTex, 0);
+    ASSERT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+
+    // Program to sample cubemap face.
+    constexpr char kVS[] = R"(#version 300 es
+in vec4 aPosition;
+void main()
+{
+    gl_Position = aPosition;
+})";
+
+    constexpr char kFS[] = R"(#version 300 es
+precision highp float;
+precision highp int;
+uniform highp samplerCube uCube;
+uniform int uFace;
+uniform vec2 uSize;
+out vec4 fragColor;
+void main()
+{
+    vec2 st = gl_FragCoord.xy / uSize;
+    float sc = st.x * 2.0 - 1.0;
+    float tc = st.y * 2.0 - 1.0;
+    vec3 d;
+    if (uFace == 0)      d = vec3( 1.0, -tc, -sc);
+    else if (uFace == 1) d = vec3(-1.0, -tc,  sc);
+    else if (uFace == 2) d = vec3(  sc,  1.0,  tc);
+    else if (uFace == 3) d = vec3(  sc, -1.0, -tc);
+    else if (uFace == 4) d = vec3(  sc, -tc,  1.0);
+    else                 d = vec3( -sc, -tc, -1.0);
+    fragColor = texture(uCube, d);
+})";
+
+    ANGLE_GL_PROGRAM(sampleProg, kVS, kFS);
+    glUseProgram(sampleProg);
+    GLint cubeLoc = glGetUniformLocation(sampleProg, "uCube");
+    ASSERT_NE(cubeLoc, -1);
+    glUniform1i(cubeLoc, 0);
+    GLint faceLoc = glGetUniformLocation(sampleProg, "uFace");
+    ASSERT_NE(faceLoc, -1);
+    GLint sizeLoc = glGetUniformLocation(sampleProg, "uSize");
+    ASSERT_NE(sizeLoc, -1);
+    glUniform2f(sizeLoc, static_cast<float>(kFaceSize), static_cast<float>(kFaceSize));
+
+    // (a) Define cubemap with mismatched face sizes: +X is S x S, -X is H x H.
+    GLTexture cubeTex;
+    glBindTexture(GL_TEXTURE_CUBE_MAP, cubeTex);
+    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
+    glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X, 0, GL_RGBA8, kFaceSize, kFaceSize, 0, GL_RGBA,
+                 GL_UNSIGNED_BYTE, nullptr);
+    glTexImage2D(GL_TEXTURE_CUBE_MAP_NEGATIVE_X, 0, GL_RGBA8, kHalfSize, kHalfSize, 0, GL_RGBA,
+                 GL_UNSIGNED_BYTE, nullptr);
+    ASSERT_GL_NO_ERROR();
+
+    // (b) Full-coverage glCopyTexSubImage2D into -X (H x H).
+    glBindFramebuffer(GL_READ_FRAMEBUFFER, srcGreenFbo);
+    glCopyTexSubImage2D(GL_TEXTURE_CUBE_MAP_NEGATIVE_X, 0, 0, 0, 0, 0, kHalfSize, kHalfSize);
+    ASSERT_GL_NO_ERROR();
+
+    // (c) Redefine -X to S x S with null data.
+    glBindTexture(GL_TEXTURE_CUBE_MAP, cubeTex);
+    glTexImage2D(GL_TEXTURE_CUBE_MAP_NEGATIVE_X, 0, GL_RGBA8, kFaceSize, kFaceSize, 0, GL_RGBA,
+                 GL_UNSIGNED_BYTE, nullptr);
+    ASSERT_GL_NO_ERROR();
+
+    // (d) Full-coverage glCopyTexSubImage2D (WHITE) into -X (S x S).
+    glBindFramebuffer(GL_READ_FRAMEBUFFER, srcWhiteFbo);
+    glCopyTexSubImage2D(GL_TEXTURE_CUBE_MAP_NEGATIVE_X, 0, 0, 0, 0, 0, kFaceSize, kFaceSize);
+    ASSERT_GL_NO_ERROR();
+
+    // (e) Complete the cubemap with null data.
+    glBindTexture(GL_TEXTURE_CUBE_MAP, cubeTex);
+    glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_Y, 0, GL_RGBA8, kFaceSize, kFaceSize, 0, GL_RGBA,
+                 GL_UNSIGNED_BYTE, nullptr);
+    glTexImage2D(GL_TEXTURE_CUBE_MAP_NEGATIVE_Y, 0, GL_RGBA8, kFaceSize, kFaceSize, 0, GL_RGBA,
+                 GL_UNSIGNED_BYTE, nullptr);
+    glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_Z, 0, GL_RGBA8, kFaceSize, kFaceSize, 0, GL_RGBA,
+                 GL_UNSIGNED_BYTE, nullptr);
+    glTexImage2D(GL_TEXTURE_CUBE_MAP_NEGATIVE_Z, 0, GL_RGBA8, kFaceSize, kFaceSize, 0, GL_RGBA,
+                 GL_UNSIGNED_BYTE, nullptr);
+    ASSERT_GL_NO_ERROR();
+
+    // Sample each cube face and verify contents.
+    glBindFramebuffer(GL_FRAMEBUFFER, dstFbo);
+    glViewport(0, 0, kFaceSize, kFaceSize);
+    glUseProgram(sampleProg);
+    glActiveTexture(GL_TEXTURE0);
+    glBindTexture(GL_TEXTURE_CUBE_MAP, cubeTex);
+
+    // Face 1 (-X) was fully overwritten with white, so it must be white.
+    glUniform1i(faceLoc, 1);
+    glClearColor(0.0f, 0.0f, 1.0f, 1.0f);
+    glClear(GL_COLOR_BUFFER_BIT);
+    drawQuad(sampleProg, "aPosition", 0.5f);
+    EXPECT_GL_NO_ERROR();
+    EXPECT_PIXEL_RECT_EQ(0, 0, kFaceSize, kFaceSize, GLColor::white);
+
+    // Remaining faces (+X, +Y, -Y, +Z, -Z) were initialized with null data and never written,
+    // so robust resource init must ensure they are transparent black.
+    for (int face : {0, 2, 3, 4, 5})
+    {
+        glUniform1i(faceLoc, face);
+        glClearColor(0.0f, 0.0f, 1.0f, 1.0f);
+        glClear(GL_COLOR_BUFFER_BIT);
+        drawQuad(sampleProg, "aPosition", 0.5f);
+        EXPECT_GL_NO_ERROR();
+        EXPECT_PIXEL_RECT_EQ(0, 0, kFaceSize, kFaceSize, GLColor::transparentBlack);
+    }
+}
+
 ANGLE_INSTANTIATE_TEST_ES2_AND_ES3_AND(
     RobustResourceInitTest,
     ES3_METAL().enable(Feature::EmulateDontCareLoadWithRandomClear),
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/src/tests/gl_tests/RobustResourceInitTest.cpp b/src/tests/gl_tests/RobustResourceInitTest.cpp
index 7631497..f2bbb80 100644
--- a/src/tests/gl_tests/RobustResourceInitTest.cpp
+++ b/src/tests/gl_tests/RobustResourceInitTest.cpp
@@ -4444,6 +4444,158 @@
     EXPECT_GL_NO_ERROR();
 }
 
+// Tests that glCopyTexSubImage2D into a cubemap face of mismatched size, followed by
+// redefining the face to match the cubemap size, and copying into it with full coverage,
+// correctly updates the face and does not leave uninitialized data when sampled.
+TEST_P(RobustResourceInitTestES3, CubeMapCopySubImageMismatchedFaceRedefine)
+{
+    ANGLE_SKIP_TEST_IF(!hasGLExtension());
+
+    constexpr GLint kFaceSize = 16;
+    constexpr GLint kHalfSize = 8;
+
+    // Copy sources: green HxH and white SxS read framebuffers.
+    std::vector<GLColor> greenColors(kHalfSize * kHalfSize, GLColor::green);
+    GLTexture srcGreenTex;
+    glBindTexture(GL_TEXTURE_2D, srcGreenTex);
+    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kHalfSize, kHalfSize, 0, GL_RGBA, GL_UNSIGNED_BYTE,
+                 greenColors.data());
+    GLFramebuffer srcGreenFbo;
+    glBindFramebuffer(GL_FRAMEBUFFER, srcGreenFbo);
+    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, srcGreenTex, 0);
+    ASSERT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+
+    std::vector<GLColor> whiteColors(kFaceSize * kFaceSize, GLColor::white);
+    GLTexture srcWhiteTex;
+    glBindTexture(GL_TEXTURE_2D, srcWhiteTex);
+    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kFaceSize, kFaceSize, 0, GL_RGBA, GL_UNSIGNED_BYTE,
+                 whiteColors.data());
+    GLFramebuffer srcWhiteFbo;
+    glBindFramebuffer(GL_FRAMEBUFFER, srcWhiteFbo);
+    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, srcWhiteTex, 0);
+    ASSERT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+
+    // Destination framebuffer to sample cubemap into.
+    GLTexture dstTex;
+    glBindTexture(GL_TEXTURE_2D, dstTex);
+    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kFaceSize, kFaceSize, 0, GL_RGBA, GL_UNSIGNED_BYTE,
+                 nullptr);
+    GLFramebuffer dstFbo;
+    glBindFramebuffer(GL_FRAMEBUFFER, dstFbo);
+    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, dstTex, 0);
+    ASSERT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+
+    // Program to sample cubemap face.
+    constexpr char kVS[] = R"(#version 300 es
+in vec4 aPosition;
+void main()
+{
+    gl_Position = aPosition;
+})";
+
+    constexpr char kFS[] = R"(#version 300 es
+precision highp float;
+precision highp int;
+uniform highp samplerCube uCube;
+uniform int uFace;
+uniform vec2 uSize;
+out vec4 fragColor;
+void main()
+{
+    vec2 st = gl_FragCoord.xy / uSize;
+    float sc = st.x * 2.0 - 1.0;
+    float tc = st.y * 2.0 - 1.0;
+    vec3 d;
+    if (uFace == 0)      d = vec3( 1.0, -tc, -sc);
+    else if (uFace == 1) d = vec3(-1.0, -tc,  sc);
+    else if (uFace == 2) d = vec3(  sc,  1.0,  tc);
+    else if (uFace == 3) d = vec3(  sc, -1.0, -tc);
+    else if (uFace == 4) d = vec3(  sc, -tc,  1.0);
+    else                 d = vec3( -sc, -tc, -1.0);
+    fragColor = texture(uCube, d);
+})";
+
+    ANGLE_GL_PROGRAM(sampleProg, kVS, kFS);
+    glUseProgram(sampleProg);
+    GLint cubeLoc = glGetUniformLocation(sampleProg, "uCube");
+    ASSERT_NE(cubeLoc, -1);
+    glUniform1i(cubeLoc, 0);
+    GLint faceLoc = glGetUniformLocation(sampleProg, "uFace");
+    ASSERT_NE(faceLoc, -1);
+    GLint sizeLoc = glGetUniformLocation(sampleProg, "uSize");
+    ASSERT_NE(sizeLoc, -1);
+    glUniform2f(sizeLoc, static_cast<float>(kFaceSize), static_cast<float>(kFaceSize));
+
+    // (a) Define cubemap with mismatched face sizes: +X is S x S, -X is H x H.
+    GLTexture cubeTex;
+    glBindTexture(GL_TEXTURE_CUBE_MAP, cubeTex);
+    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
+    glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X, 0, GL_RGBA8, kFaceSize, kFaceSize, 0, GL_RGBA,
+                 GL_UNSIGNED_BYTE, nullptr);
+    glTexImage2D(GL_TEXTURE_CUBE_MAP_NEGATIVE_X, 0, GL_RGBA8, kHalfSize, kHalfSize, 0, GL_RGBA,
+                 GL_UNSIGNED_BYTE, nullptr);
+    ASSERT_GL_NO_ERROR();
+
+    // (b) Full-coverage glCopyTexSubImage2D into -X (H x H).
+    glBindFramebuffer(GL_READ_FRAMEBUFFER, srcGreenFbo);
+    glCopyTexSubImage2D(GL_TEXTURE_CUBE_MAP_NEGATIVE_X, 0, 0, 0, 0, 0, kHalfSize, kHalfSize);
+    ASSERT_GL_NO_ERROR();
+
+    // (c) Redefine -X to S x S with null data.
+    glBindTexture(GL_TEXTURE_CUBE_MAP, cubeTex);
+    glTexImage2D(GL_TEXTURE_CUBE_MAP_NEGATIVE_X, 0, GL_RGBA8, kFaceSize, kFaceSize, 0, GL_RGBA,
+                 GL_UNSIGNED_BYTE, nullptr);
+    ASSERT_GL_NO_ERROR();
+
+    // (d) Full-coverage glCopyTexSubImage2D (WHITE) into -X (S x S).
+    glBindFramebuffer(GL_READ_FRAMEBUFFER, srcWhiteFbo);
+    glCopyTexSubImage2D(GL_TEXTURE_CUBE_MAP_NEGATIVE_X, 0, 0, 0, 0, 0, kFaceSize, kFaceSize);
+    ASSERT_GL_NO_ERROR();
+
+    // (e) Complete the cubemap with null data.
+    glBindTexture(GL_TEXTURE_CUBE_MAP, cubeTex);
+    glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_Y, 0, GL_RGBA8, kFaceSize, kFaceSize, 0, GL_RGBA,
+                 GL_UNSIGNED_BYTE, nullptr);
+    glTexImage2D(GL_TEXTURE_CUBE_MAP_NEGATIVE_Y, 0, GL_RGBA8, kFaceSize, kFaceSize, 0, GL_RGBA,
+                 GL_UNSIGNED_BYTE, nullptr);
+    glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_Z, 0, GL_RGBA8, kFaceSize, kFaceSize, 0, GL_RGBA,
+                 GL_UNSIGNED_BYTE, nullptr);
+    glTexImage2D(GL_TEXTURE_CUBE_MAP_NEGATIVE_Z, 0, GL_RGBA8, kFaceSize, kFaceSize, 0, GL_RGBA,
+                 GL_UNSIGNED_BYTE, nullptr);
+    ASSERT_GL_NO_ERROR();
+
+    // Sample each cube face and verify contents.
+    glBindFramebuffer(GL_FRAMEBUFFER, dstFbo);
+    glViewport(0, 0, kFaceSize, kFaceSize);
+    glUseProgram(sampleProg);
+    glActiveTexture(GL_TEXTURE0);
+    glBindTexture(GL_TEXTURE_CUBE_MAP, cubeTex);
+
+    // Face 1 (-X) was fully overwritten with white, so it must be white.
+    glUniform1i(faceLoc, 1);
+    glClearColor(0.0f, 0.0f, 1.0f, 1.0f);
+    glClear(GL_COLOR_BUFFER_BIT);
+    drawQuad(sampleProg, "aPosition", 0.5f);
+    EXPECT_GL_NO_ERROR();
+    EXPECT_PIXEL_RECT_EQ(0, 0, kFaceSize, kFaceSize, GLColor::white);
+
+    // Remaining faces (+X, +Y, -Y, +Z, -Z) were initialized with null data and never written,
+    // so robust resource init must ensure they are transparent black.
+    for (int face : {0, 2, 3, 4, 5})
+    {
+        glUniform1i(faceLoc, face);
+        glClearColor(0.0f, 0.0f, 1.0f, 1.0f);
+        glClear(GL_COLOR_BUFFER_BIT);
+        drawQuad(sampleProg, "aPosition", 0.5f);
+        EXPECT_GL_NO_ERROR();
+        EXPECT_PIXEL_RECT_EQ(0, 0, kFaceSize, kFaceSize, GLColor::transparentBlack);
+    }
+}
+
 ANGLE_INSTANTIATE_TEST_ES2_AND_ES3_AND(
     RobustResourceInitTest,
     ES3_METAL().enable(Feature::EmulateDontCareLoadWithRandomClear),
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.