← All issues

[1] ANGLE Metal: stale texture views during size transitions

Severity: High | Component: ANGLE Metal backend | baeadcd

Rated High because the diff fixes a deterministic, web-reachable Metal validation abort in the GPU/renderer process and removes the optimistic stale-view reuse branch in TextureMtl::redefineImage. On Metal driver paths where validation is disabled, the same sequence would dispatch an upload with a stale region descriptor against the resized storage, yielding a bounded OOB pixel-data write into the Metal heap allocation backing the destination mip level.

When a texture base level size changes (e.g., 128x128 to 256x256), native storage is recreated but old mipmap views with wrong dimensions can remain in mTexImageDefs. Uploading to these stale views causes a Metal validation failure: (origin.x + size.width)(128) must be <= width(64). The fix clears mTexImageDefs entries in generateMipmap() and redefineImage() to ensure views are always recreated with correct dimensions from current storage.

Source/ThirdParty/ANGLE/src/libANGLE/renderer/metal/TextureMtl.mm

ANGLE_TRY(ensureNativeStorageCreated(context, false));
+ int numCubeFaces = static_cast<int>(mNativeTextureStorage->cubeFaces());
+ for (int face = 0; face < numCubeFaces; ++face)
+ {
+ const GLuint mips = mNativeTextureStorage->mipmapLevels();
+ for (mtl::MipmapNativeLevel mip = mtl::kZeroNativeMipLevel; mip.get() < mips; ++mip)
+ {
+ GLuint level = mNativeTextureStorage->getGLLevel(mip);
+ mTexImageDefs[face][level] = {};
+ }
+ }
ContextMtl *contextMtl = mtl::GetImpl(context);
+ contextMtl->invalidateCurrentTextures();
...
- bool imageWithinNativeStorageLevels = false;
- if (mNativeTextureStorage && mNativeTextureStorage->isGLLevelSupported(index.getLevelIndex()))
+ GLuint cubeFaceOrZero = GetImageCubeFaceIndexOrZeroFrom(index);
+ GLuint glLevel = index.getLevelIndex();
+ ImageDefinitionMtl &imageDef = mTexImageDefs[cubeFaceOrZero][glLevel];
+ if (mNativeTextureStorage && mNativeTextureStorage->isGLLevelSupported(glLevel))
{
- imageWithinNativeStorageLevels = true;
- GLuint glLevel = index.getLevelIndex();
- if (mNativeTextureStorage->getFormat() != mtlFormat ||
- size != mNativeTextureStorage->size(glLevel))
+ if (mNativeTextureStorage->getFormat() == mtlFormat && size == mNativeTextureStorage->size(glLevel))
{
- deallocateNativeStorage(/*keepImages=*/true);
+ return angle::Result::Continue;
}
+ deallocateNativeStorage(/*keepImages=*/true);
}
+ imageDef = {};
...
- if (mNativeTextureStorage && imageDef.image && imageWithinNativeStorageLevels)
- {
- ASSERT(...);
- }
- else
- {
- imageDef.formatID = mtlFormat.intendedFormatId;
- ...
- }
+ mtl::TextureRef image;
+ imageDef = {image, mtlFormat.intendedFormatId};

Source/ThirdParty/ANGLE/src/tests/gl_tests/MipmapTest.cpp

+TEST_P(MipmapTest, UploadAfterSizeTransition)
+{
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 128, 128, ...);
+ glGenerateMipmap(GL_TEXTURE_2D);
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 256, 256, ...);
+ glGenerateMipmap(GL_TEXTURE_2D);
+ glTexImage2D(GL_TEXTURE_2D, 1, GL_RGBA, 128, 128, ..., blueData.data());
+}

generateMipmap is restructured to iterate every face and level of the freshly allocated mNativeTextureStorage and zero the corresponding mTexImageDefs[face][level] slot, followed by contextMtl->invalidateCurrentTextures() so subsequent draws rebind sampler slots. redefineImage is rewritten with a different control flow: it computes the imageDef slot up front, takes an early-return when the existing storage already matches both format and size, and otherwise calls deallocateNativeStorage(/*keepImages=*/true) followed by an unconditional imageDef = {} reset before building a fresh mtl::TextureRef. The internal GetTextureImageType helper and the optimistic imageDef.image && imageWithinNativeStorageLevels reuse branch are deleted; an obsolete imageToTransfer = nullptr line inside ensureNativeStorageCreated is replaced with mTexImageDefs[face][imageMipLevel] = {}. Two regression tests exercise 128 to 256 to 128 size transitions and post-resize mipmap regeneration.

Cached derived view metadata not invalidated when its backing storage is reallocated, allowing operations to dispatch with stale dimensions against the new storage.

ANGLE is the OpenGL ES translator WebKit uses to implement WebGL; on Apple platforms it targets a Metal backend. TextureMtl is the ANGLE Metal object that backs a GLES texture and holds two parallel descriptions of texture state. mNativeTextureStorage is a single Metal MTLTexture representing the full mipmap chain once the texture becomes complete; mTexImageDefs[face][level] is a cache of per-image ImageDefinitionMtl values (a {mtl::TextureRef view, formatID} pair) used to service GLES image operations before or while the chain is being built.

glTexImage2D(level=0, w, h) may resize the base level and forces recreation of mNativeTextureStorage so subsequent levels can host the new chain. glGenerateMipmap requests that ANGLE allocate and populate the full chain. redefineImage is the ANGLE entry point invoked by glTexImage* to (re)allocate a per-level image. mtl::TextureRef is a reference-counted wrapper over id<MTLTexture>; assigning imageDef = {} drops the held reference. Metal's replaceRegion:mipmapLevel:withBytes:bytesPerRow: enforces that origin + size fit within that mip level's actual dimensions and raises a validation error on mismatch when the validation layer is active.

The bug class is stale cached metadata. TextureMtl keeps two parallel descriptions of the same texture, and the invariant that mutating mNativeTextureStorage must also invalidate every dependent mTexImageDefs entry was not enforced on every path. Specifically, when a glTexImage2D at level 0 with a new size triggered storage recreation, the per-level views in mTexImageDefs for non-base levels carried over from the old storage; their stored dimensions described the prior chain. A subsequent operation that consulted these cached views — most directly the optimistic reuse branch in redefineImage guarded by imageDef.image && imageWithinNativeStorageLevels — would dispatch a Metal upload with the old view's dimensions against the new per-level storage.

🔒

The boundary between a crash-only validation abort and a latent driver-level OOB depends on which build configuration is running — the analysis explores both branches and what an attacker controls in each.

Subscribe to read more

🔒

Four reusable patterns covering cache-vs-backing-store invariants, GPU-validation-as-only-barrier risks, and analogous fast paths in sibling ANGLE backends.

Subscribe to read more