CVE-2026-28913
Overview
Background
- Block-compressed texture format
- Formats like ETC/ETC2 store fixed-size blocks covering multiple texels, so pitch is block-based, not pixelBytes*width.
- Row/depth pitch
- Byte stride between rows/slices of texture data; must match the format layout or copies go out of bounds.
- ANGLE Metal backend
- Implements WebGL/GLES on Apple Metal, including texture sub-image uploads.
- ANGLE_CHECK_GL_MATH
- Overflow-checked arithmetic used when computing compressed image pitches.
Root Cause Analysis
ANGLE’s Metal backend uploads sub-image texture data in TextureMtl::convertAndSetPerSliceSubImage. It computed the destination row and depth pitch as dstRowPitch = dstFormat.pixelBytes * mtlArea.size.width and dstDepthPitch = dstRowPitch * mtlArea.size.height. That formula is correct only for uncompressed (per-pixel) formats. For BLOCK-compressed formats (e.g. ETC/ETC2), data is laid out in fixed-size blocks covering several texels, so pixelBytes*width drastically underestimates the true row pitch; using that too-small pitch to index/copy the compressed sub-image writes and reads outside the allocated staging buffer — an out-of-bounds access surfacing as a process crash.
The fix branches on dstFormat.isBlock: for block formats it computes the row and depth pitch with the format’s block-aware helpers computeCompressedImageRowPitch() / computeCompressedImageDepthPitch(), guarded by ANGLE_CHECK_GL_MATH (overflow-checked), and only falls back to pixelBytes*width for uncompressed formats.
The restored invariant is that the destination pitch matches the actual (block-based) layout of the format being uploaded, so the copy stays within bounds.
Attack Path
- Use a compressed texture in WebGL On a Metal/ANGLE backend, upload a block-compressed texture (e.g. ETC2) via a sub-image update.
- Hit the per-slice convert path convertAndSetPerSliceSubImage computes dstRowPitch as pixelBytes*width, far smaller than the compressed layout needs.
- Under-sized pitch The copy uses the wrong pitch to index the destination, walking outside the staging buffer.
- Out-of-bounds / crash The mis-pitched copy reads/writes out of bounds in the GPU/WebContent process.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
TextureMtl::convertAndSetPerSliceSubImageSource/ThirdParty/ANGLE/src/libANGLE/renderer/metal/TextureMtl.mm |
modified | Computes dstRowPitch/dstDepthPitch via computeCompressedImageRowPitch/DepthPitch (ANGLE_CHECK_GL_MATH) for block-compressed formats instead of pixelBytes*width, fixing the under-sized pitch. |
Files Changed
Source/ThirdParty/ANGLE/src/libANGLE/renderer/metal/TextureMtl.mmSource/ThirdParty/ANGLE/src/tests/gl_tests/ETCTextureTest.cpp
Audit Directions
- Other pixelBytes*width pitchesgrep ANGLE metal/ for rowPitch/depthPitch computed as pixelBytes*width without an isBlock branch.
- Compressed-format pathsAudit texture upload/readback/copy code for compressed formats using per-pixel pitch math.
- Overflow checksEnsure compressed pitch computations use ANGLE_CHECK_GL_MATH so large dimensions can’t overflow the pitch.