← All issues

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

Severity: High | Component: ANGLE Metal backend | baeadcd

High로 평가된 근거를 정리하면 다음과 같습니다. diff는 GPU/renderer process에서 web content를 통해 유발 가능한, 매번 재현 가능한 Metal validation abort를 수정하고, TextureMtl::redefineImage의 optimistic stale-view reuse 분기도 제거합니다. Metal driver에서 validation이 비활성화된 경로에서는 동일한 흐름이 stale region descriptor로 resize된 storage에 업로드를 요청하게 되며, destination mip level을 backing하는 Metal heap allocation 내에서 bounded OOB pixel-data write가 발생할 가능성이 있습니다.

Texture의 base level 크기가 변경될 때(예: 128x128에서 256x256으로), native storage는 재생성되지만 mTexImageDefs에는 이전 크기를 기반으로 한 mipmap view가 그대로 남을 수 있습니다. 이 stale view에 업로드를 시도하면 Metal validation이 (origin.x + size.width)(128) must be <= width(64) 오류를 발생시킵니다. 패치는 generateMipmap()redefineImage()에서 mTexImageDefs 항목을 초기화하여 view가 항상 현재 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은 새로 할당된 mNativeTextureStorage의 모든 face와 level을 순회하여 대응하는 mTexImageDefs[face][level] 슬롯을 초기화하도록 재구성되었습니다. 이후 contextMtl->invalidateCurrentTextures()를 호출해 후속 draw 시 sampler slot이 다시 바인딩됩니다.

redefineImage는 제어 흐름이 새롭게 작성되었습니다. 먼저 imageDef 슬롯을 미리 계산하고, 기존 storage의 format과 size가 모두 일치하면 즉시 반환하는 early-return 구조로 바뀌었습니다. 일치하지 않으면 deallocateNativeStorage(/*keepImages=*/true)를 호출하고, 이어서 imageDef = {}를 무조건 초기화한 뒤 새로운 mtl::TextureRef를 생성합니다.

내부 GetTextureImageType 헬퍼와 optimistic reuse 분기(imageDef.image && imageWithinNativeStorageLevels)는 제거되었습니다. ensureNativeStorageCreated 내부의 불필요한 imageToTransfer = nullptr 코드는 mTexImageDefs[face][imageMipLevel] = {}로 교체되었고, 128 → 256 → 128 크기 전환 및 resize 후 mipmap 재생성을 검증하는 regression test 두 건이 추가되었습니다.

Backing storage 재할당 시 파생 view metadata가 무효화되지 않아, stale 크기 기반으로 새 storage에 작업이 전달되는 패턴.

ANGLE은 WebKit이 WebGL 구현에 사용하는 OpenGL ES 번역 레이어로, Apple 플랫폼에서는 Metal backend를 대상으로 합니다. TextureMtl은 GLES texture를 지원하는 ANGLE Metal 객체이며, texture 상태에 대한 두 가지 설명을 병렬로 관리합니다.

mNativeTextureStorage는 texture가 완성된 시점에 전체 mipmap chain을 나타내는 단일 Metal MTLTexture입니다. mTexImageDefs[face][level]는 mipmap chain이 구성되기 전후로 GLES image 작업을 처리하기 위한 per-image ImageDefinitionMtl 값의 캐시입니다. 이 값은 {mtl::TextureRef view, formatID} 쌍으로 구성됩니다.

glTexImage2D(level=0, w, h) 호출은 base level을 resize할 수 있으며, 이에 따라 mNativeTextureStorage가 재생성되어 후속 level들이 새로운 chain을 수용합니다. glGenerateMipmap은 ANGLE에 전체 chain의 할당 및 채우기를 요청합니다. redefineImageglTexImage* 호출 시 per-level image를 (재)할당하기 위해 진입하는 ANGLE entry point입니다. mtl::TextureRefid<MTLTexture>에 대한 reference-counted wrapper로, imageDef = {}를 할당하면 보유한 reference가 해제됩니다. Metal의 replaceRegion:mipmapLevel:withBytes:bytesPerRow:origin + size가 해당 mip level의 실제 크기 내에 들어와야 함을 강제합니다. validation layer가 활성화된 상태에서 이 조건이 위반되면 validation 오류가 발생합니다.

이 버그의 유형은 stale cached metadata에 해당합니다. TextureMtl은 동일한 texture에 대해 두 가지 설명을 병렬로 유지하는데, mNativeTextureStorage가 변경될 때마다 이에 의존하는 모든 mTexImageDefs 항목을 무효화해야 한다는 불변식이 일부 코드 경로에서 지켜지지 않았습니다.

구체적으로, level 0에서 새로운 크기로 glTexImage2D를 호출하면 storage 재생성이 유발됩니다. 이때 mTexImageDefs의 non-base level에 해당하는 per-level view는 이전 storage에서 넘어온 채로 남아, 이전 chain의 크기를 그대로 반영합니다. 이 stale view를 참조하는 후속 작업에서 문제가 발생합니다. 가장 직접적인 경로는 redefineImage의 optimistic reuse 분기인데, imageDef.image && imageWithinNativeStorageLevels 조건으로 보호되어 있습니다. 이 분기에 진입하면 이전 view의 크기 정보를 기반으로 새 per-level storage에 Metal 업로드가 요청됩니다.

🔒

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.

더 확인하려면 구독해 주세요

🔒

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

더 확인하려면 구독해 주세요