CVE-2024-4558
Overview
Background
- ANGLE
- The Almost Native Graphics Layer Engine, a translation layer that implements WebGL/GLES on top of native GL/Direct3D/Metal backends, here the GL backend used by WebKit’s GPU process.
- PixelUnpackState
- The set of GL parameters (row length, alignment, image height, skip rows/pixels, and bound unpack buffer) that describe how pixel data is laid out and read from client memory during an upload.
- Compressed sub-image upload
- glCompressedTexSubImage2D/3D, which copies a block of pre-compressed texel data into a region of an existing texture.
- StateManager::setPixelUnpackState
- ANGLE’s helper that programs the native GL context’s unpack state (and binds/unbinds the unpack buffer) so a subsequent upload reads exactly the region ANGLE validated.
- Out-of-bounds read
- A memory-safety fault where code reads past the end of a buffer, here the source pixel data, potentially crashing or disclosing adjacent memory.
Root Cause Analysis
The patch adds a single line, ANGLE_TRY(stateManager->setPixelUnpackState(context, unpack)), to TextureGL::setCompressedSubImage in ANGLE’s OpenGL backend, immediately after binding the texture and before the code dispatches to the native glCompressedTexSubImage2D/3D path. setCompressedSubImage uploads a block of application-supplied compressed pixel data into an existing texture region. The gl::PixelUnpackState (the unpack parameter passed to the function) carries client-controlled parameters such as GL_UNPACK_ROW_LENGTH, alignment, skip rows/pixels, and, importantly, the bound GL_PIXEL_UNPACK_BUFFER used as the data source. The invariant ANGLE’s GL backend relies on is that before issuing a native texture-upload call, the GL state manager’s cached/native unpack state must be synchronized to the unpack parameters ANGLE resolved for this call; the non-compressed upload paths already do this via setPixelUnpackState, but the compressed path was missing the call. Because of that omission, the driver performed the compressed sub-image upload using stale or default unpack state that did not match the dimensions and byte-count ANGLE validated against, so the amount and layout of data the driver read from the source (client memory or the unpack buffer) could disagree with what was actually available. That mismatch lets the underlying GL driver read past the intended bounds of the source data, i.e. an out-of-bounds read during the upload.
The fix restores the invariant by explicitly programming the pixel-unpack state before the compressed upload, exactly as the uncompressed paths do, so the driver’s read geometry matches the validated buffer. It is worth noting (inference) that the precise faulting read happens inside the platform GL driver, which is not in the diff; the diff establishes only the missing state-synchronization call in ANGLE. Apple characterizes the observable result as an unexpected process crash, consistent with an OOB read rather than a write primitive.
Attack Path
- Obtain a WebGL context Malicious web content creates a WebGL/WebGL2 context, which routes GPU commands through ANGLE’s OpenGL backend in the GPU process.
- Set up a compressed texture Create and bind a texture with a compressed internal format supported by the path, so subsequent uploads take the setCompressedSubImage route.
- Configure unpack parameters and/or an unpack buffer Set pixel-unpack parameters (row length, alignment, skip) and optionally bind a PIXEL_UNPACK_BUFFER so that the intended read geometry differs from the stale unpack state ANGLE would leave in place.
- Call compressedTexSubImage2D/3D Issue a compressed sub-image upload; because setPixelUnpackState was not applied, the native driver reads the source using unsynchronized unpack state and accesses memory beyond the validated data extent (out-of-bounds read).
- Cause a crash or leak The OOB read faults, crashing the GPU process (the observed impact); in principle a controlled OOB read could disclose adjacent GPU-process memory, but the diff supports only the crash/read, not a write or code-execution primitive.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
TextureGL::setCompressedSubImageSource/ThirdParty/ANGLE/src/libANGLE/renderer/gl/TextureGL.cpp |
modified | Adds ANGLE_TRY(stateManager->setPixelUnpackState(context, unpack)) after bindTexture and before the native compressed upload, synchronizing the driver's pixel-unpack state to the call's unpack parameters as the uncompressed paths already do. |
Audit Directions
- Other upload paths in TextureGL missing setPixelUnpackStateIn TextureGL.cpp compare setImage/setSubImage/setCompressedImage/setCompressedSubImage and any 3D/array variants: grep each for a setPixelUnpackState (or setPixelPackState for readback) call before the native texImage/texSubImage dispatch and flag any upload/readback that binds the texture but skips the state sync.
- Same missing-state pattern in other ANGLE backendsCheck the Vulkan/Metal/D3D TextureXX.cpp implementations for equivalent compressed-vs-uncompressed asymmetry where unpack/pack parameters or the unpack buffer are applied on one path but not the parallel compressed path.
- State-manager sync omissions generallyAcross renderer/gl grep for native glTexSubImage*/glReadPixels*/glCompressed* calls and verify each is preceded by the corresponding stateManager->setPixel{Unpack,Pack}State; a bind-then-upload sequence without a preceding state sync is the concrete tell for this bug class.
- Unpack-buffer-aware validation vs. dispatch mismatchesAudit places that resolve a gl::PixelUnpackState (row length/skip/buffer) for validation but then dispatch to the driver without programming that state, since the validated byte-count and the driver’s actual read geometry can diverge and produce OOB reads.