Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactHeap buffer overflow in ANGLE
DescriptionHeap buffer overflow in ANGLE
ComponentANGLE
Bug ClassOOB
Tracker493256564
Fix commit6d8b704e2a18 (angle/angle) +141/-79
CISA KEVNot listed
Creditedcinzinga
Disclosed2026-04-07

Changed Functions

FunctionChangeNotes
NativeTextureWrapper
src/libANGLE/renderer/metal/TextureMtl.h
modified
NativeTextureWrapperWithViewSupport
src/libANGLE/renderer/metal/TextureMtl.h
modified
TextureMtl
src/libANGLE/renderer/metal/TextureMtl.mm
modified

Files Changed

  • src/libANGLE/renderer/metal/TextureMtl.h
  • src/libANGLE/renderer/metal/TextureMtl.mm
From 6d8b704e2a185c82430a339d70508742887a962f Mon Sep 17 00:00:00 2001
From: Geoff Lang <geofflang@chromium.org>
Date: Thu, 19 Mar 2026 15:17:08 -0400
Subject: [PATCH] Metal: Remove TextureMtl::mFormat

TextureMtl::mFormat is supposed to represent the format of the native
storage but it was updated to the last format set on any mip level, even
if it is not in the native storage. This is extremely error prone, a lot
of the Metal texturing code relied on it being properly set.

Remove mFormat and query it from the native storage or the specific
image desc.

Bug: chromium:493256564
Change-Id: I629c009b34c7ef7ca5fa7a97f5845accf22b13b8
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7684363
Commit-Queue: Geoff Lang <geofflang@chromium.org>
Reviewed-by: Shahbaz Youssefi <syoussefi@chromium.org>
---

diff --git a/src/libANGLE/renderer/metal/TextureMtl.h b/src/libANGLE/renderer/metal/TextureMtl.h
index 65283cd..2dfd58b 100644
--- a/src/libANGLE/renderer/metal/TextureMtl.h
+++ b/src/libANGLE/renderer/metal/TextureMtl.h
@@ -182,15 +182,14 @@
                                     int layer,
                                     GLenum format);
 
-    const mtl::Format &getFormat() const { return mFormat; }
-
   private:
     void deallocateNativeStorage(bool keepImages, bool keepSamplerStateAndFormat = false);
     angle::Result createNativeStorage(const gl::Context *context,
                                       gl::TextureType type,
                                       GLuint mips,
                                       GLuint samples,
-                                      const gl::Extents &size);
+                                      const gl::Extents &size,
+                                      const mtl::Format &format);
     angle::Result onBaseMaxLevelsChanged(const gl::Context *context);
     angle::Result ensureSamplerStateCreated(const gl::Context *context);
     // Ensure image at given index is created:
@@ -328,10 +327,10 @@
 
     angle::Result generateMipmapCPU(const gl::Context *context);
 
-    bool needsFormatViewForPixelLocalStorage(const ShPixelLocalStorageOptions &) const;
+    bool needsFormatViewForPixelLocalStorage(const ShPixelLocalStorageOptions &,
+                                             const mtl::Format &format) const;
     bool isImmutableOrPBuffer() const;
 
-    mtl::Format mFormat;
     egl::Surface *mBoundSurface = nullptr;
     class NativeTextureWrapper;
     class NativeTextureWrapperWithViewSupport;
diff --git a/src/libANGLE/renderer/metal/TextureMtl.mm b/src/libANGLE/renderer/metal/TextureMtl.mm
index 9301862..0dcbddd 100644
--- a/src/libANGLE/renderer/metal/TextureMtl.mm
+++ b/src/libANGLE/renderer/metal/TextureMtl.mm
@@ -777,8 +777,8 @@
 class TextureMtl::NativeTextureWrapper : angle::NonCopyable
 {
   public:
-    NativeTextureWrapper(mtl::TextureRef texture, GLuint baseGLLevel)
-        : mNativeTexture(std::move(texture)), mBaseGLLevel(baseGLLevel)
+    NativeTextureWrapper(mtl::TextureRef texture, GLuint baseGLLevel, const mtl::Format &format)
+        : mNativeTexture(std::move(texture)), mBaseGLLevel(baseGLLevel), mFormat(format)
     {
         ASSERT(mNativeTexture && mNativeTexture->valid());
     }
@@ -810,6 +810,7 @@
                                  getNativeLevel(glLevel), slice, dataOut);
     }
 
+    const mtl::Format &getFormat() const { return mFormat; }
     GLuint getBaseGLLevel() const { return mBaseGLLevel; }
     // Get max addressable GL level that this texture supports.
     GLuint getMaxSupportedGLLevel() const { return mBaseGLLevel + mipmapLevels() - 1; }
@@ -855,14 +856,17 @@
   protected:
     mtl::TextureRef mNativeTexture;
     const GLuint mBaseGLLevel;
+    const mtl::Format mFormat;
 };
 
 // This class extends NativeTextureWrapper with support for view creation
 class TextureMtl::NativeTextureWrapperWithViewSupport : public NativeTextureWrapper
 {
   public:
-    NativeTextureWrapperWithViewSupport(mtl::TextureRef texture, GLuint baseGLLevel)
-        : NativeTextureWrapper(std::move(texture), baseGLLevel)
+    NativeTextureWrapperWithViewSupport(mtl::TextureRef texture,
+                                        GLuint baseGLLevel,
+                                        const mtl::Format &format)
+        : NativeTextureWrapper(std::move(texture), baseGLLevel, format)
     {}
 
     // Create a view of one slice at a level.
@@ -952,7 +956,6 @@
     if (!keepSamplerStateAndFormat)
     {
         mMetalSamplerState = nil;
-        mFormat            = mtl::Format();
     }
 }
 
@@ -976,9 +979,9 @@
     ANGLE_CHECK(contextMtl, desc.format.valid(), gl::err::kInternalError, GL_INVALID_OPERATION);
     angle::FormatID angleFormatId =
         angle::Format::InternalFormatToID(desc.format.info->sizedInternalFormat);
-    mFormat = contextMtl->getPixelFormat(angleFormatId);
+    mtl::Format format = contextMtl->getPixelFormat(angleFormatId);
 
-    ANGLE_TRY(createNativeStorage(context, mState.getType(), mips, 0, desc.size));
+    ANGLE_TRY(createNativeStorage(context, mState.getType(), mips, 0, desc.size, format));
 
     // Transfer data from defined images to actual texture object
     int numCubeFaces = static_cast<int>(mNativeTextureStorage->cubeFaces());
@@ -1019,44 +1022,46 @@
                                               gl::TextureType type,
                                               GLuint mips,
                                               GLuint samples,
-                                              const gl::Extents &size)
+                                              const gl::Extents &size,
+                                              const mtl::Format &format)
 {
     ASSERT(samples == 0 || mips == 0);
     ContextMtl *contextMtl = mtl::GetImpl(context);
 
     // Create actual texture object:
     mSlices              = 1;
-    bool allowFormatView = mFormat.hasDepthAndStencilBits() ||
-                           needsFormatViewForPixelLocalStorage(
-                               contextMtl->getDisplay()->getNativePixelLocalStorageOptions());
+    bool allowFormatView =
+        format.hasDepthAndStencilBits() ||
+        needsFormatViewForPixelLocalStorage(
+            contextMtl->getDisplay()->getNativePixelLocalStorageOptions(), format);
     mtl::TextureRef nativeTextureStorage;
     switch (type)
     {
         case gl::TextureType::_2D:
-            ANGLE_TRY(mtl::Texture::Make2DTexture(
-                contextMtl, mFormat, size.width, size.height, mips,
-                /** renderTargetOnly */ false, allowFormatView, &nativeTextureStorage));
+            ANGLE_TRY(mtl::Texture::Make2DTexture(contextMtl, format, size.width, size.height, mips,
+                                                  /** renderTargetOnly */ false, allowFormatView,
+                                                  &nativeTextureStorage));
             break;
         case gl::TextureType::CubeMap:
             mSlices = 6;
-            ANGLE_TRY(mtl::Texture::MakeCubeTexture(contextMtl, mFormat, size.width, mips,
+            ANGLE_TRY(mtl::Texture::MakeCubeTexture(contextMtl, format, size.width, mips,
                                                     /** renderTargetOnly */ false, allowFormatView,
                                                     &nativeTextureStorage));
             break;
         case gl::TextureType::_3D:
             ANGLE_TRY(mtl::Texture::Make3DTexture(
-                contextMtl, mFormat, size.width, size.height, size.depth, mips,
+                contextMtl, format, size.width, size.height, size.depth, mips,
                 /** renderTargetOnly */ false, allowFormatView, &nativeTextureStorage));
             break;
         case gl::TextureType::_2DArray:
             mSlices = size.depth;
             ANGLE_TRY(mtl::Texture::Make2DArrayTexture(
-                contextMtl, mFormat, size.width, size.height, mips, mSlices,
+                contextMtl, format, size.width, size.height, mips, mSlices,
                 /** renderTargetOnly */ false, allowFormatView, &nativeTextureStorage));
             break;
         case gl::TextureType::_2DMultisample:
             ANGLE_TRY(mtl::Texture::Make2DMSTexture(
-                contextMtl, mFormat, size.width, size.height, samples,
+                contextMtl, format, size.width, size.height, samples,
                 /** renderTargetOnly */ false, allowFormatView, &nativeTextureStorage));
             break;
         default:
@@ -1066,15 +1071,16 @@
     if (mState.getImmutableFormat())
     {
         mNativeTextureStorage = std::make_unique<NativeTextureWrapperWithViewSupport>(
-            std::move(nativeTextureStorage), /*baseGLLevel=*/0);
+            std::move(nativeTextureStorage), /*baseGLLevel=*/0, format);
     }
     else
     {
         mNativeTextureStorage = std::make_unique<NativeTextureWrapperWithViewSupport>(
-            std::move(nativeTextureStorage), /*baseGLLevel=*/mState.getEffectiveBaseLevel());
+            std::move(nativeTextureStorage), /*baseGLLevel=*/mState.getEffectiveBaseLevel(),
+            format);
     }
 
-    ANGLE_TRY(checkForEmulatedChannels(context, mFormat, *mNativeTextureStorage));
+    ANGLE_TRY(checkForEmulatedChannels(context, format, *mNativeTextureStorage));
 
     ANGLE_TRY(createViewFromBaseToMaxLevel());
 
@@ -1091,11 +1097,14 @@
         return angle::Result::Continue;
     }
 
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/src/tests/gl_tests/TextureTest.cpp b/src/tests/gl_tests/TextureTest.cpp
index acef103..7137425 100644
--- a/src/tests/gl_tests/TextureTest.cpp
+++ b/src/tests/gl_tests/TextureTest.cpp
@@ -2864,6 +2864,39 @@
     EXPECT_EQ(expected, actual);
 }
 
+// Regression test for TextureMtl::mFormat becoming mismatched with the native storage format when
+// updating mips outside of the storage.
+TEST_P(Texture2DTestES3, StaleFormatCacheOutOrRangeMip)
+{
+
+    GLTexture tex;
+    glBindTexture(GL_TEXTURE_2D, tex);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 4);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+
+    // Create RGBA8 texture with one mip.
+    constexpr GLuint kWidth = 24;
+    std::vector<GLColor> pixels(kWidth, GLColor::red);
+    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kWidth, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE,
+                 pixels.data());
+
+    // Force native storage allocation with a draw
+    ANGLE_GL_PROGRAM(program, essl3_shaders::vs::Simple(), essl3_shaders::fs::Red());
+    drawQuad(program, essl3_shaders::PositionAttrib(), 0.5f);
+
+    // Set data on a high mip with a different format causing mFormat to change in TextureMtl
+    glTexImage2D(GL_TEXTURE_2D, 10, GL_R8, 1, 1, 0, GL_RED, GL_UNSIGNED_BYTE, nullptr);
+
+    GLFramebuffer fbo;
+    glBindFramebuffer(GL_FRAMEBUFFER, fbo);
+    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex, 0);
+    ASSERT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+
+    // Check that ReadPixels reads RGBA data from mip 0 correctly.
+    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::red);
+}
+
 // Almost mirrors UnitTest_DMSAA_dst_read test from Android skqp test suite
 TEST_P(Texture2DTestES3, UnitTest_DMSAA_dst_read)
 {
Loading diff…

Original Bug Report

reported by ci...@gmail.com

ANGLE Metal Stale mFormat Cache causes GPU OOB WRITE


Report description

ANGLE Metal Stale mFormat Cache causes GPU OOB WRITE


Bug location

Where do you want to report your vulnerability?

Chrome VRP – Report security issues affecting the Chrome browser. See program rules

Which URL (or repository) have you found the vulnerability in?

https://source.chromium.org/chromium/chromium/src/+/main:third_party/angle/src/libANGLE/renderer/metal/TextureMtl.mm


The problem

Please describe the technical details of the vulnerability

Heap buffer overflow with attacker-controlled data in the GPU process via stale mFormat cache in ANGLE’s Metal backend. Incomplete fix for chromium:435683799.

TextureMtl::redefineImage (TextureMtl.mm:2067) unconditionally sets mFormat for any texImage2D level, including out-of-range levels that do not trigger storage reallocation. Calling texImage2D at mip level 14 (TEXTURE_MAX_LEVEL=4) with format R8 poisons mFormat to R8 (1 byte/pixel) while the native Metal texture remains RGBA8 (4 bytes/pixel).

readPixelsImpl then sizes readPixelRowBuffer as Wx1 bytes. Metal’s getBytes writes Wx4 bytes of actual RGBA8 data, overflowing 3xW bytes of attacker-controlled pixel data past the buffer.

Steps to Reproduce

Stable (Chrome 145.0.7632.160, macOS x86_64, Intel GPU, Metal backend):

/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome file:///path/to/poc_write_crash.html

No flags required. Default configuration. The page auto-reloads and the GPU process crashes within 1-2 page loads.

chrome://crashes IDs:

  • 897c7d4318e7beb6
  • 63aee33eb14f4815

Terminal output from the crash:

*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[__NSCFNumber storageMode]: unrecognized selector sent to instance 0x4141414141414141'

The 0x4141414141414141 value is attacker-controlled pixel data from the overflow reaching ObjC dispatch. Full symbolized stack trace attached as crash_trace.txt.

Proposed Fix

Guard the mFormat cache update so it only applies to levels within native storage:

    // Cache last defined image format:
-   mFormat                      = mtlFormat;
+   if (imageWithinNativeStorageLevels)
+   {
+       mFormat                  = mtlFormat;
+   }

The variable imageWithinNativeStorageLevels is already computed at line 2043 and gates the storage reallocation at line 2052. It should also gate the format cache update.

Bisect

The unconditional mFormat assignment was introduced in ANGLE commit fe26bae452 (“Metal backend implementation pt 2”, 2019-10-10). Present since the Metal backend was first implemented, approximately Chrome 80.

The incomplete fix was commit 86a8d11c82 (“Metal: Fix potential incorrect format used for texSubImage”, 2025-08-11), which patched setPerSliceSubImage and convertAndSetPerSliceSubImage but left line 2067 unfixed.

Impact analysis

  • Web-accessible via WebGL2 (no compromised renderer required).
  • GPU process heap overflow with attacker-controlled data (pixel values).
  • macOS only (Metal backend, Intel GPUs).
  • Seven affected code paths from the same stale mFormat root cause.

The cause

What version of Chrome have you found the security issue in?

145.0.7632.160 Stable

Yes, it is related to a crash.

Choose the type of vulnerability

Memory Corruption (in a sandboxed process)

How would you like to be publicly acknowledged for your report?

cinzinga

View on issue tracker