Released May 12, 2025

CVE-2025-31204

CVSS 8.8 severity high CWE-119 vector CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H

Overview

Impact:

Description: Processing maliciously crafted web content may lead to memory corruption

Researchers: Nan Wang(@eternalsakura13)

Key insight. The array bounds check was 32-bit but the element address used the full 64-bit index register, so a 64-bit index with in-bounds low bits bypassed the check; zero-extending the index after the check ties the access to the validated value.

Patch Details

diff --git a/JSTests/wasm/stress/array-get-large-i64-index.js b/JSTests/wasm/stress/array-get-large-i64-index.js
new file mode 100644
index 000000000000..3ac909fa2ebb
--- /dev/null
+++ b/JSTests/wasm/stress/array-get-large-i64-index.js
@@ -0,0 +1,10 @@
+var wasm_code = new Uint8Array([
+    0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x89, 0x80, 0x80, 0x80, 0x00, 0x02, 0x5e, 0x78, 0x01, 0x60, 0x01, 0x7e, 0x01, 0x7f, 0x03, 0x82, 0x80, 0x80, 0x80, 0x00, 0x01, 0x01, 0x07, 0x88, 0x80, 0x80, 0x80, 0x00, 0x01, 0x04, 0x6d, 0x61, 0x69, 0x6e, 0x00, 0x00, 0x0a, 0x95, 0x80, 0x80, 0x80, 0x00, 0x01, 0x8f, 0x80, 0x80, 0x80, 0x00, 0x00, 0x41, 0x00, 0x41, 0x10, 0xfb, 0x06, 0x00, 0x20, 0x00, 0xa7, 0xfb, 0x0d, 0x00, 0x0b,
+]);
+var wasm_module = new WebAssembly.Module(wasm_code);
+var wasm_instance = new WebAssembly.Instance(wasm_module);
+var f = wasm_instance.exports.main;
+for (let i = 0; i < 10000; ++i) {
+    f(0x7fffffff00000000n);
+}
+
diff --git a/Source/JavaScriptCore/wasm/WasmBBQJIT64.cpp b/Source/JavaScriptCore/wasm/WasmBBQJIT64.cpp
index ecaeaf0590d3..e5c5e81a3bc4 100644
--- a/Source/JavaScriptCore/wasm/WasmBBQJIT64.cpp
+++ b/Source/JavaScriptCore/wasm/WasmBBQJIT64.cpp
@@ -1817,6 +1817,7 @@ PartialResult WARN_UNUSED_RETURN BBQJIT::addArrayGet(ExtGCOpType arrayGetKind, u
         indexLocation = loadIfNecessary(index);
         throwExceptionIf(ExceptionType::OutOfBoundsArrayGet,
             m_jit.branch32(MacroAssembler::AboveOrEqual, indexLocation.asGPR(), MacroAssembler::Address(arrayLocation.asGPR(), JSWebAssemblyArray::offsetOfSize())));
+        m_jit.zeroExtend32ToWord(indexLocation.asGPR(), indexLocation.asGPR());
     }
 
     emitArrayGetPayload(elementType, arrayLocation.asGPR(), wasmScratchGPR);
@@ -2001,6 +2002,7 @@ PartialResult WARN_UNUSED_RETURN BBQJIT::addArraySet(uint32_t typeIndex, Express
         Location indexLocation = loadIfNecessary(index);
         throwExceptionIf(ExceptionType::OutOfBoundsArraySet,
             m_jit.branch32(MacroAssembler::AboveOrEqual, indexLocation.asGPR(), MacroAssembler::Address(arrayLocation.asGPR(), JSWebAssemblyArray::offsetOfSize())));
+        m_jit.zeroExtend32ToWord(indexLocation.asGPR(), indexLocation.asGPR());
     }
 
     emitArraySetUnchecked(typeIndex, arrayref, index, value);

Background

Advisory: Apple Security Advisory

Attribute Value
CVE CVE-2025-31204
Bugzilla 291506
Component JSC
Bug Class OOB
Severity medium
Commit 265dbd5abf60768a…
Advisory Apple Advisory

Terminology

  • BBQ JIT — JavaScriptCore’s baseline WebAssembly JIT; the 64-bit variant keeps values in 64-bit registers.
  • branch32 bounds check — A 32-bit comparison; if the index register holds a 64-bit value, only its low 32 bits are validated.
  • zeroExtend32ToWord — Clears the upper 32 bits of a register so a subsequent 64-bit address computation matches the checked 32-bit value.

Root Cause Analysis

This fixes an out-of-bounds access in the WebAssembly BBQ JIT (64-bit) because the array bounds check compared only the low 32 bits of the index while the element address used the full 64-bit register. In BBQJIT::addArrayGet and addArraySet, the index is bounds-checked with a 32-bit compare — branch32(AboveOrEqual, indexGPR, array->size) — but the index register can hold a 64-bit value whose upper 32 bits are non-zero. Pre-patch the code proceeded to compute the element address from the full 64-bit register, so an index such as 0x7fffffff00000000 passes the 32-bit check (low 32 bits = 0 < size) yet the actual access uses the full 64-bit value as the offset, reading or writing far outside the array — an out-of-bounds memory access. The fix inserts m_jit.zeroExtend32ToWord(indexGPR, indexGPR) immediately after the bounds check in both addArrayGet and addArraySet, clearing the upper 32 bits so the access uses only the validated low 32 bits. The restored invariant is that the index used for the element address matches the 32-bit value that was bounds-checked. The regression test passes 0x7fffffff00000000n to a wasm array access.

Attack Path

1. Reach the BBQ JIT

Run a WebAssembly module with array get/set so it is compiled by the BBQ (64-bit) tier.

2. Supply a 64-bit index with high bits set

Provide an index like 0x7fffffff00000000 whose low 32 bits are in-bounds but upper bits are non-zero.

3. Pass the 32-bit check

The branch32 bounds check only inspects the low 32 bits and succeeds.

4. Out-of-bounds access

The element address is computed from the full 64-bit register, reading/writing far outside the array — a controllable OOB in WebContent.

Impact Assessment

A controllable out-of-bounds read/write in the WebContent process via WebAssembly array access — the attacker chooses the high bits of the index, giving a strong, wide-reaching corruption primitive and a well-known route toward arbitrary read/write and code execution.

Audit Directions

Same file: 32-bit checks on 64-bit values

Audit WasmBBQJIT64 for other branch32 bounds/type checks on index/length registers that are later used as 64-bit addresses without zero-extension.

Index provenance

Grep for loadIfNecessary(index) uses where an i32 index may carry garbage/high bits from an i64 source before an address computation.

Changed Functions

Function File Change Note
BBQJIT::addArrayGet Source/JavaScriptCore/wasm/WasmBBQJIT64.cpp modified Zero-extends the index to the full word after the 32-bit bounds check so the element address uses only the validated low 32 bits.
BBQJIT::addArraySet Source/JavaScriptCore/wasm/WasmBBQJIT64.cpp modified Same zeroExtend32ToWord after the bounds check to prevent a 64-bit index bypassing the 32-bit check.

Files Changed

Source Files

  • Source/JavaScriptCore/wasm/WasmBBQJIT64.cpp

Test Files

  • JSTests/wasm/stress/array-get-large-i64-index.js

Regression Test

The fix ships this proof-of-concept / regression test:

diff --git a/JSTests/wasm/stress/array-get-large-i64-index.js b/JSTests/wasm/stress/array-get-large-i64-index.js
new file mode 100644
index 000000000000..3ac909fa2ebb
--- /dev/null
+++ b/JSTests/wasm/stress/array-get-large-i64-index.js
@@ -0,0 +1,10 @@
+var wasm_code = new Uint8Array([
+    0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x89, 0x80, 0x80, 0x80, 0x00, 0x02, 0x5e, 0x78, 0x01, 0x60, 0x01, 0x7e, 0x01, 0x7f, 0x03, 0x82, 0x80, 0x80, 0x80, 0x00, 0x01, 0x01, 0x07, 0x88, 0x80, 0x80, 0x80, 0x00, 0x01, 0x04, 0x6d, 0x61, 0x69, 0x6e, 0x00, 0x00, 0x0a, 0x95, 0x80, 0x80, 0x80, 0x00, 0x01, 0x8f, 0x80, 0x80, 0x80, 0x00, 0x00, 0x41, 0x00, 0x41, 0x10, 0xfb, 0x06, 0x00, 0x20, 0x00, 0xa7, 0xfb, 0x0d, 0x00, 0x0b,
+]);
+var wasm_module = new WebAssembly.Module(wasm_code);
+var wasm_instance = new WebAssembly.Instance(wasm_module);
+var f = wasm_instance.exports.main;
+for (let i = 0; i < 10000; ++i) {
+    f(0x7fffffff00000000n);
+}
+