[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());
+}
Patch Details
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.
Background
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.
Analysis
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.
Aaa Aaaaaaaaaaaa Aaaaa Aa Aaaaaaaaaaaaaaaa Aaaaaa Aaa Aaaaa Aaaaa Aaa Aaaa Aaaaaaaaa a Aaaaaaa Aaaa Aaaaa Aaa Aaaaa Aaaaaaaaaaaaaaaaa Aaaaaaaaa Aaaaaa Aaaa Aaaa Aaaaa Aaa Aaa Aa Aaa Aa Aaa Aa Aaaa Aaaaa Aaaaaaaaaaaaaa Aa Aaaaa a Aaaa Aaaa Aaaa Aaaaaaa Aaaaaaaaaaaaaaaaaaaaaaa Aaaaaaaaaaa Aaa Aaa Aaaaaaa Aaaaa a Aa Aaa Aaaaaaaa Aaa Aaa Aaaaaaaaaaaa Aaaaaaaaaaaaaaaaaaaaa Aaaaa Aaaaa Aaaaaaaaa a Aaaaa Aaaa Aaaa Aaa Aaaaa Aaaaaaaa a Aaaaaaaaaaaaaa Aa Aaaaa a Aaaa Aaaa Aaa Aaaa Aaaaaa Aaaaaaaaaaaaaaaa Aaaa Aaa Aaaaaaaaaa Aaaaa Aaaaaaa Aaa Aaaaaaaaaa Aaaaaaaaaaaaaaa Aaaa Aaa Aaaaa Aaaaaaaa Aaaa Aaaaaaaa Aaaaaaa Aaa Aaa Aaaaaaaaa Aaaaaa Aaaaa Aaaaaaaaaa Aaaaaaa Aaa Aaaa Aaaa Aaa Aaaaaa Aaaaaa
Aaa Aaaaaaa Aaaaaaaaaaa Aa Aaaaaaaaaaaaaaaaaa Aaaaaa Aa a Aaaaaaaaaaaaa Aaaaaaaa Aa Aaaaaaaaaaa Aaaaa Aaaaaaaaa Aaaa Aaa Aaaaaaaaaaaaa Aaaa a a Aaaaaaaaaaaaaaa Aaa Aaaaaaa Aaa Aaa Aaaaaaaa Aa Aaaaa Aaaaaa Aaaaa Aaaaa Aaaaaaaaaa Aa Aaaaaaaaa Aaa Aaaa Aaaaaaa Aaaaa Aa Aaaaaa Aaaaaaa Aaa Aaaaaa Aaa Aaaaaaaaaaaa Aaaa Aaaaaa Aaaa Aaa Aaaa a Aaaaa Aa a Aaaaaaaaaaaaaaaa Aaaaaaaaa Aaa a Aaaaaaaaaaaa Aaaaaaaaaa Aa Aaaaaaaaaaaaaa Aaaaa Aaa Aaaaaa Aaaaaa Aaaaa Aaaaaa Aaa Aaaaaaaaaaa Aaaaaaa Aaaaaa Aaaaaaaaaa Aaa Aaaaa Aaaaa Aaaaaaaaaaaaaaaaa Aaaaa Aaaa Aaaa Aaa Aaaaaaaaaa Aaaaaaaaaaa Aaaaaaa Aa Aaa Aaaaa Aaaa Aaaaaaaaaa Aaaaaaa Aaaa Aaa Aaaaaa Aaa Aaaaaaaa Aaaaaaaa Aaaa Aaa Aaaaaa Aaaaaa Aaaaaaaa Aaaa Aaaaaaaaaaaa Aaaaa Aaa Aaa Aaaaaa Aaaaaaaaaaaa Aaaaaaaaa Aaaa Aaaaaa Aaaaaaaa Aaaaaa Aaaaaaaaaaaa Aa Aaa Aaaaaa Aaaaaaaaa Aa a Aaaaa Aaa Aaaaaaaaaa Aaaaa Aaaaaa Aaa Aaaaaaaaaaaaaa Aaaaaaaa
Aaaa Aaaaaaaaaaaaa Aaaaaaa Aaa Aaaaaaaaaaaaa Aaaaaaaa Aaaaaaa Aaaaa Aaaaaaa Aaa Aaa Aaa Aaaaaaaa Aaa Aaaaaaaaaa Aaaaa Aaaaa Aaaaaaa Aaaa Aaaa Aaaaaaa Aaaaa Aaaaaaaaaaa Aaaaaa Aaaaaaa Aaaaaaaaa Aaaaaaaaaaaa Aaaaaaaaa Aaaa Aaaaaaaaaa Aa Aaaaaaaa Aaaaaaaa Aaaaaa Aaaaaaa Aaaaaaaa Aaaaaaaa Aaa Aaaaaaaaaaaaa Aaaaaaa Aaaaaa a Aaaaaaaaaa Aaa Aaaaa Aaaaa Aaaaa Aaaaa Aaa Aaaaaaaa Aaaaaa Aaa Aaaaaaaaaaaaaa Aaaaaaa Aaa Aaaaa Aaaaaaa a Aaaaaaaa Aaaaaa Aa Aaaaa Aaa Aaaaaa Aa Aaaaaaa Aaaaaaaa
🔒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
Audit directions
a Aaaaaaaaaaaaaaaa Aaaaaaaaaaaa Aaaa Aaaaa a Aaaaaa Aaaaaaaaaaa Aaaaaaaaaa Aaaaa Aaaaaaaa Aaa Aaaaaaaaaaaaa Aaaaaaa Aaaaaaaa Aaaaa Aaaaa Aaaa Aa Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa Aaaa Aaaaaaa Aaaaaaaaaaaaaaaaaaaaaaa Aaaaaaaaaa Aaaaaaaaaaa Aaaaaaaaa Aaa Aaaaaa Aaaa Aa Aaaaaa Aaaa a Aaaa Aaaaa Aa Aaaaaaaaaaaaaaaa Aaaaa Aaaa Aaaaaaaaaaaaaaaaaaaaaaaaaa Aaaaaaaaaaaaaaaaaaaaaaaaaaaaa Aaaaaaaaaaaaaaa Aaaaaaaaaaaaa Aaaaaaaaaaaaaa Aaa Aaa Aaaa Aaaa Aaaaa Aaaaaaaaaaaaaaaaaaaaaaaa a Aaaaaaaaaaaaaaa a Aaaaaaaaaaaaaaaaaaaaa Aaaa Aaa Aaaaaaaaaaa Aa Aaaaaaaaaaaaaaaaaaaaaaa Aaa Aaaaaaa Aaaaaaaaaaaaaaa Aa Aaaaa Aa Aaaaa Aaaaaaaaa Aaaaaaa
a Aaaaaaaaaaaa Aaaaaaaaaa Aa Aaa Aaaa Aaaaaaa Aaaaaaa a Aaaaa Aaa Aa a Aaaaaaaaaaa Aaaaa Aaa Aa Aaa Aaaaaa Aaaaaaaaaaaa Aaaaa Aaaaa Aaaaa Aaaaaaaa Aaaaaaaaaaaaaaaaaaaa Aaaaaaaaaaaaaaaa Aaaaaaaaaaaaaaa Aaa Aaaaaaaaa Aaaaaaaaaaa Aaaaaaaaaaaa Aaaaaa Aaaaaaaaaaaaaaa a Aaaaaaaaaaaaaaaaa Aaa Aaaaa Aaaaaaaaaa Aaaaa Aaaaaaaa Aaaa Aaaa Aaaaaaaaaa Aaaaaa Aaaaaa Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa Aaa Aaa Aaaaa Aaaaaa Aaaaa Aaaaa Aaa Aaaaaaaa Aaa Aaaa Aaaa Aaaaaaaa Aaaaaa Aaa Aaaaaaaaaaaaaaaaaaaaaaaaaa Aaaaa Aaaaaaaaaaaaaaaaaaaaaaaaaa Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa Aaaaaa Aaaa Aaaaaa Aaaaaaaa
a Aaaaaaaaaaaa Aaaaa Aa Aaaaaa Aaaaaaaaaaaaa Aaaaaaa Aa a Aaaaa Aaaaaa Aaaaaaaa Aaaaaaaaaaaa Aaa Aaaaaaa Aaaaaaaaaaaaaaa Aa Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa Aaaaaa Aa Aaaaaaa Aaaa Aaaaaaaa Aaaa Aaaaa Aaa Aaaaaaa Aaa Aaaaaaaaaaaaaa Aa Aaaaaaaaaaaaa a Aaaaaaaaaaaa Aa Aaaa Aaaaaaa Aa Aa Aaaaaaaaaa Aaa Aaaaaa Aaa Aaaaaaaaaaaaaa Aaaaaaaaa Aa Aaaaaaaa Aaaa Aaa Aaaa Aaaaaaa Aaaaa Aaaaaa Aaaa a Aaaaaa Aaaaa Aaaaa Aaaa Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa Aaa Aaa Aaaaaaa Aa Aaaaaaaaaaaaaaaaa Aaaaaaaaaaaaa
a Aaaaaa Aaaaa Aaaaaaaaaaa Aaaa Aaaaaaaaaaa Aaaaaaa Aaaaaaa Aaaaaaa Aaaaaaaa Aaaaa Aaaaaaaaa Aaaaaaaa Aaaaa Aaaaaaaaaaaaaaaa Aaaaaaaaaaaaaaaaaa Aaaaaaaaaaaaaaaaaaaaa Aaaaaaaaaaaaa Aaaa Aaaaaaaa Aaaaaaa Aaa Aaaaaaaaaaaaaaaaaa Aaaaaaaaa Aaaaa Aaa Aaa Aaaa Aaaaa Aa Aaaaaaaaaa Aaaa Aaaaa Aaaaaaa Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa Aaaaaaaaaaaaaaaaaaaaaaaaaaa Aaa Aaaaaaaaaaaaaaaaaa Aaaaa Aaaaaaa Aa Aaaaaaaaaa Aaaa Aaaaaaaaaaaaaaa Aaaaaa
Aaaaaaaaa Aaaa Aaaaaaaaaaaaaa Aaaaaaa Aaaaaa Aaaaaaa Aaaaaaaa Aa Aaaaa Aaaaaa Aaaaa Aaaaa Aaaaaaaaaa Aa Aaaaaaaaa Aaa Aaaaaaaa Aaaa Aaa Aaaa Aaa Aaa Aaaaaa Aaaaaaaaaa Aaaaa Aaaaaa Aaaa Aaaaaaaa Aaaaaaa Aa Aaa Aaaaaaa Aaa Aaaa Aaaaaaaaa Aaaa Aaa Aaaaa Aaaaaaa Aaa Aaaaaaaaaaaa Aaaaaaaaa Aa Aaa Aaaaa Aaa Aaa Aaaaa Aaaaaaaaaa Aaaaaa
🔒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