CVE-2026-5868
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
NativeTextureWrappersrc/libANGLE/renderer/metal/TextureMtl.h |
modified | |
NativeTextureWrapperWithViewSupportsrc/libANGLE/renderer/metal/TextureMtl.h |
modified | |
TextureMtlsrc/libANGLE/renderer/metal/TextureMtl.mm |
modified |
Files Changed
src/libANGLE/renderer/metal/TextureMtl.hsrc/libANGLE/renderer/metal/TextureMtl.mm
Patch
From 6d8b704e2a185c82430a339d70508742887a962f Mon Sep 17 00:00:00 2001 From: Geoff Lang <[email protected]> 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 <[email protected]> Reviewed-by: Shahbaz Youssefi <[email protected]> --- 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; }
Regression Test / PoC
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)
{
Original Bug Report
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?
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
Is the security issue related to a crash?
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