Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactOut of bounds read in ANGLE
DescriptionOut of bounds read in ANGLE
ComponentANGLE
Bug ClassOOB
Tracker512962749
Fix commita1d8610fd2b4 (angle/angle) +32/-4
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-30

Files Changed

  • src/libANGLE/renderer/metal/TextureMtl.mm
  • src/tests/egl_tests/EGLIOSurfaceClientBufferTest.cpp
From a1d8610fd2b4de7607d4e805b38bbedcd2f06ac3 Mon Sep 17 00:00:00 2001
From: Le Hoang Quyen <[email protected]>
Date: Wed, 20 May 2026 22:47:54 +0800
Subject: [PATCH] Metal: Fix crash when setting non-zero base level on IOSurface.

- Clamped baseLevel and maxLevel to supported range in
  `createViewFromBaseToMaxLevel` to avoid creating invalid views.
- Added `SetNonZeroBaseLevel` test in
  `EGLIOSurfaceClientBufferTest.cpp`.

Bug: chromium:512962749
Change-Id: I0faff0414aa0230918ed7bce663f4a87d7bf376e
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7862465
Reviewed-by: Geoff Lang <[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 7c73e45..4c84667 100644
--- a/src/libANGLE/renderer/metal/TextureMtl.mm
+++ b/src/libANGLE/renderer/metal/TextureMtl.mm
@@ -1112,15 +1112,17 @@
 angle::Result TextureMtl::createViewFromBaseToMaxLevel()
 {
     ASSERT(mNativeTextureStorage);
+    uint32_t baseLevel =
+        std::min(mNativeTextureStorage->getMaxSupportedGLLevel(), mState.getEffectiveBaseLevel());
     uint32_t maxLevel =
         std::min(mNativeTextureStorage->getMaxSupportedGLLevel(), mState.getEffectiveMaxLevel());
 
     // In edge case where base level > max level, clamp up to base level.
-    maxLevel = std::max(maxLevel, mState.getEffectiveBaseLevel());
+    maxLevel = std::max(maxLevel, baseLevel);
 
     mtl::TextureRef nativeViewFromBaseToMaxLevelRef;
     if (maxLevel == mNativeTextureStorage->getMaxSupportedGLLevel() &&
-        mState.getEffectiveBaseLevel() == mNativeTextureStorage->getBaseGLLevel())
+        baseLevel == mNativeTextureStorage->getBaseGLLevel())
     {
         // If base & max level are the same in mNativeTextureStorage, we don't need
         // a dedicated view. Furthermore, Intel driver has some bugs when sampling a view
@@ -1129,9 +1131,9 @@
     }
     else
     {
-        uint32_t baseToMaxLevels = maxLevel - mState.getEffectiveBaseLevel() + 1;
+        uint32_t baseToMaxLevels = maxLevel - baseLevel + 1;
         nativeViewFromBaseToMaxLevelRef =
-            mNativeTextureStorage->createMipsView(mState.getEffectiveBaseLevel(), baseToMaxLevels);
+            mNativeTextureStorage->createMipsView(baseLevel, baseToMaxLevels);
     }
 
     mViewFromBaseToMaxLevel = std::make_unique<NativeTextureWrapper>(
diff --git a/src/tests/egl_tests/EGLIOSurfaceClientBufferTest.cpp b/src/tests/egl_tests/EGLIOSurfaceClientBufferTest.cpp
index 0979256..309bbe5 100644
--- a/src/tests/egl_tests/EGLIOSurfaceClientBufferTest.cpp
+++ b/src/tests/egl_tests/EGLIOSurfaceClientBufferTest.cpp
@@ -1415,6 +1415,32 @@
 // TODO([email protected]): Test setting width and height to less than the IOSurface's work as
 // expected.
 
+// Test that binding an IOSurface and setting base level to 3 does not cause an error.
+TEST_P(IOSurfaceClientBufferTest, SetNonZeroBaseLevel)
+{
+    ANGLE_SKIP_TEST_IF(!hasIOSurfaceExt());
+    ANGLE_SKIP_TEST_IF(getGLTextureTarget() != GL_TEXTURE_2D);
+    ANGLE_SKIP_TEST_IF(getClientMajorVersion() < 3);
+
+    // Create a 1x1 IOSurface
+    ScopedIOSurfaceRef ioSurface = CreateSinglePlaneIOSurface(1, 1, 'BGRA', 4);
+
+    // Bind it to a texture
+    EGLSurface pbuffer;
+    GLTexture texture;
+    bindIOSurfaceToTexture(ioSurface, 1, 1, 0, GL_BGRA_EXT, GL_UNSIGNED_BYTE, &pbuffer, &texture);
+
+    // Set base level to 3
+    glTexParameteri(getGLTextureTarget(), GL_TEXTURE_BASE_LEVEL, 3);
+    EXPECT_GL_NO_ERROR();
+
+    // Clean up
+    EGLBoolean result = eglReleaseTexImage(mDisplay, pbuffer, EGL_BACK_BUFFER);
+    EXPECT_EGL_TRUE(result);
+    result = eglDestroySurface(mDisplay, pbuffer);
+    EXPECT_EGL_TRUE(result);
+}
+
 ANGLE_INSTANTIATE_TEST(IOSurfaceClientBufferTest,
                        ES2_OPENGL(),
                        ES3_OPENGL(),
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/src/tests/egl_tests/EGLIOSurfaceClientBufferTest.cpp b/src/tests/egl_tests/EGLIOSurfaceClientBufferTest.cpp
index 0979256..309bbe5 100644
--- a/src/tests/egl_tests/EGLIOSurfaceClientBufferTest.cpp
+++ b/src/tests/egl_tests/EGLIOSurfaceClientBufferTest.cpp
@@ -1415,6 +1415,32 @@
 // TODO([email protected]): Test setting width and height to less than the IOSurface's work as
 // expected.
 
+// Test that binding an IOSurface and setting base level to 3 does not cause an error.
+TEST_P(IOSurfaceClientBufferTest, SetNonZeroBaseLevel)
+{
+    ANGLE_SKIP_TEST_IF(!hasIOSurfaceExt());
+    ANGLE_SKIP_TEST_IF(getGLTextureTarget() != GL_TEXTURE_2D);
+    ANGLE_SKIP_TEST_IF(getClientMajorVersion() < 3);
+
+    // Create a 1x1 IOSurface
+    ScopedIOSurfaceRef ioSurface = CreateSinglePlaneIOSurface(1, 1, 'BGRA', 4);
+
+    // Bind it to a texture
+    EGLSurface pbuffer;
+    GLTexture texture;
+    bindIOSurfaceToTexture(ioSurface, 1, 1, 0, GL_BGRA_EXT, GL_UNSIGNED_BYTE, &pbuffer, &texture);
+
+    // Set base level to 3
+    glTexParameteri(getGLTextureTarget(), GL_TEXTURE_BASE_LEVEL, 3);
+    EXPECT_GL_NO_ERROR();
+
+    // Clean up
+    EGLBoolean result = eglReleaseTexImage(mDisplay, pbuffer, EGL_BACK_BUFFER);
+    EXPECT_EGL_TRUE(result);
+    result = eglDestroySurface(mDisplay, pbuffer);
+    EXPECT_EGL_TRUE(result);
+}
+
 ANGLE_INSTANTIATE_TEST(IOSurfaceClientBufferTest,
                        ES2_OPENGL(),
                        ES3_OPENGL(),
Loading diff…

Original Bug Report

reported by [email protected]

Potential out-of-bounds Metal texture view via GL_TEXTURE_BASE_LEVEL

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 without the Chrome Security team. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: A logic error in ANGLE’s Metal backend allows a compromised renderer to create a Metal texture view with an out-of-bounds mipmap range. By setting a non-zero GL_TEXTURE_BASE_LEVEL on an IOSurface-backed texture, an attacker can potentially trigger an out-of-bounds read in the GPU process. This could lead to a cross-origin or cross-process information leak on macOS.

Affected files:

  • third_party/angle/src/libANGLE/renderer/metal/TextureMtl.mm
  • third_party/angle/src/libANGLE/renderer/metal/mtl_resources.mm

Estimated timestamp from git blame: Unknown (Google3 checkout)

Root Cause Analysis

A vulnerability exists in ANGLE’s Metal backend due to insufficient validation of the mipmap level range when creating texture views. Specifically, in TextureMtl::createViewFromBaseToMaxLevel(), the code attempts to compute a valid maxLevel but fails to account for cases where the effective base level exceeds the native storage’s maximum supported level.

In third_party/angle/src/libANGLE/renderer/metal/TextureMtl.mm:

angle::Result TextureMtl::createViewFromBaseToMaxLevel()
{
    ASSERT(mNativeTextureStorage);
    uint32_t maxLevel =
        std::min(mNativeTextureStorage->getMaxSupportedGLLevel(), mState.getEffectiveMaxLevel());

    // In edge case where base level > max level, clamp up to base level.
    maxLevel = std::max(maxLevel, mState.getEffectiveBaseLevel());
    ...
    else
    {
        uint32_t baseToMaxLevels = maxLevel - mState.getEffectiveBaseLevel() + 1;
        nativeViewFromBaseToMaxLevelRef =
            mNativeTextureStorage->createMipsView(mState.getEffectiveBaseLevel(), baseToMaxLevels);
    }
}

When a texture is backed by an egl::Surface (such as an IOSurface SharedImage), TextureMtl::bindTexImage() sets mBoundSurface. This causes TextureMtl::isImmutableOrPBuffer() to return true. When a user changes the texture’s parameters, TextureMtl::onBaseMaxLevelsChanged() avoids reallocating the underlying native texture and directly calls createViewFromBaseToMaxLevel().

If the attacker sets GL_TEXTURE_BASE_LEVEL to a value N that is greater than the IOSurface’s getMaxSupportedGLLevel() (which is 0, since IOSurfaces only have 1 mip level), maxLevel is improperly clamped up to N. This results in baseToMaxLevels being calculated as 1, and a call to createMipsView(N, 1) is issued.

This call propagates to mtl::Texture::createMipsView, which creates an Objective-C NSRange using the out-of-bounds base level: NSMakeRange(N, 1). The resulting range is passed to the Apple Metal API -[MTLTexture newTextureViewWithPixelFormat:textureType:levels:slices:]. Apple’s documentation requires that levels.location + levels.length <= parent.mipmapLevelCount. Violating this precondition results in undefined behavior at the driver level, often returning an aliased texture view pointing to adjacent, unowned GPU memory.

Potential Exploitation Steps

Note: These are suggested theoretical steps, as our tooling agent does not currently have the ability to execute code or build a working proof-of-concept.

  1. From a compromised macOS renderer process, use the CreateSharedImage IPC to allocate a SharedImage backed by an IOSurface, requesting SHARED_IMAGE_USAGE_GLES2_READ usage.
  2. Issue a CreateAndTexStorage2DSharedImageINTERNAL command to map the SharedImage to a GL_TEXTURE_2D texture ID.
  3. Issue a BeginSharedImageAccessDirectCHROMIUM command on this texture ID. This binds the IOSurface to the GL texture in ANGLE, setting mBoundSurface.
  4. Issue a TexParameteri command to set GL_TEXTURE_BASE_LEVEL to a non-zero value (e.g., 5). Because the surface is bound, ANGLE takes the immutable path and creates an out-of-bounds Metal texture view (NSMakeRange(5, 1)).
  5. Bind the texture to a texture unit and compile a WebGL fragment shader that samples from it using sampler2D.
  6. Issue a draw call to a WebGL framebuffer. The GPU fragment shader samples from the out-of-bounds texture view, reading adjacent GPU heap memory.
  7. Call glReadPixels() on the framebuffer to read the leaked GPU memory back into the compromised renderer process, completing the cross-origin information leak.

Impact

This vulnerability allows a compromised renderer to trigger an out-of-bounds read in the GPU process. Depending on the macOS Metal driver’s handling of the invalid NSRange, the driver may return a texture view that aliases adjacent GPU heap memory. Sampling from this view allows an attacker to read cross-origin or cross-process memory, resulting in a high-severity Information Leak.

Suggested Fix

In TextureMtl::createViewFromBaseToMaxLevel(), validate that the effective base level does not exceed the native storage’s maximum supported level before attempting to create the view. If mState.getEffectiveBaseLevel() > mNativeTextureStorage->getMaxSupportedGLLevel(), the code should safely clamp the effective base level to the maximum supported level, or safely fallback to a 1x1 default texture if the base level is completely out of bounds for the current storage.

Evaluated with Chrome root at commit: b3153093eb3c78c3e88ccf562bcbc20437a04b0e


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