Released January 27, 2025

CVE-2025-24158

CVSS 6.5 severity medium CWE-79 vector CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H

Overview

Impact:

Description: Processing web content may lead to a denial-of-service

Researchers: Q1IQ (@q1iqF) of NUS CuriOSity and P1umer (@p1umer) of Imperial Global Singapore

Key insight. BBQ-JIT bulk table/memory ops called allocate() on values that were already located, driving the register allocator into an invalid state and crashing; using loadIfNecessary() restores consistency.

Patch Details

diff --git a/JSTests/wasm/stress/array-copy-with-constant-null.js b/JSTests/wasm/stress/array-copy-with-constant-null.js
new file mode 100644
index 000000000000..dded10f50c95
--- /dev/null
+++ b/JSTests/wasm/stress/array-copy-with-constant-null.js
@@ -0,0 +1,57 @@
+import * as assert from '../assert.js'
+
+/*
+(module
+    (type $A (array (mut i32)))
+
+    (func (export "null_src") (result i32) (local (ref $A))
+        i32.const 4
+        i32.const 42
+        array.new $A
+        local.tee 0
+        i32.const 0
+        ref.null $A
+        i32.const 0
+        i32.const 4
+        array.copy $A $A
+
+        local.get 0
+        i32.const 0
+        array.get $A
+    )
+
+    (func (export "null_dst") (result i32) (local (ref $A))
+        ref.null $A
+        i32.const 0
+        i32.const 4
+        i32.const 42
+        array.new $A
+        local.tee 0
+        i32.const 0
+        i32.const 4
+        array.copy $A $A
+
+        local.get 0
+        i32.const 0
+        array.get $A
+    )
+)
+*/
+
+const wasm = new Uint8Array([
+    0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x88, 0x80, 0x80, 0x80, 0x00, 0x02, 0x5e, 0x7f,
+    0x01, 0x60, 0x00, 0x01, 0x7f, 0x03, 0x83, 0x80, 0x80, 0x80, 0x00, 0x02, 0x01, 0x01, 0x07, 0x97, 0x80,
+    0x80, 0x80, 0x00, 0x02, 0x08, 0x6e, 0x75, 0x6c, 0x6c, 0x5f, 0x73, 0x72, 0x63, 0x00, 0x00, 0x08, 0x6e,
+    0x75, 0x6c, 0x6c, 0x5f, 0x64, 0x73, 0x74, 0x00, 0x01, 0x0a, 0xcd, 0x80, 0x80, 0x80, 0x00, 0x02, 0xa1,
+    0x80, 0x80, 0x80, 0x00, 0x01, 0x01, 0x64, 0x00, 0x41, 0x04, 0x41, 0x2a, 0xfb, 0x06, 0x00, 0x22, 0x00,
+    0x41, 0x00, 0xd0, 0x00, 0x41, 0x00, 0x41, 0x04, 0xfb, 0x11, 0x00, 0x00, 0x20, 0x00, 0x41, 0x00, 0xfb,
+    0x0b, 0x00, 0x0b, 0xa1, 0x80, 0x80, 0x80, 0x00, 0x01, 0x01, 0x64, 0x00, 0xd0, 0x00, 0x41, 0x00, 0x41,
+    0x04, 0x41, 0x2a, 0xfb, 0x06, 0x00, 0x22, 0x00, 0x41, 0x00, 0x41, 0x04, 0xfb, 0x11, 0x00, 0x00, 0x20,
+    0x00, 0x41, 0x00, 0xfb, 0x0b, 0x00, 0x0b
+]);
+
+let instance = WebAssembly.instantiate(wasm);
+for (let i = 0; i < 50000; i ++) {
+    assert.throws(() => instance.exports.null_src(), TypeError);
+    assert.throws(() => instance.exports.null_dst(), TypeError);
+}
diff --git a/Source/JavaScriptCore/wasm/WasmBBQJIT.cpp b/Source/JavaScriptCore/wasm/WasmBBQJIT.cpp
index a583c0fc29fe..7eaa5e9f9bfe 100644
--- a/Source/JavaScriptCore/wasm/WasmBBQJIT.cpp
+++ b/Source/JavaScriptCore/wasm/WasmBBQJIT.cpp
@@ -830,7 +830,7 @@ PartialResult WARN_UNUSED_RETURN BBQJIT::addTableSet(unsigned tableIndex, Value
 
     Value shouldThrow = topValue(TypeKind::I32);
     emitCCall(&operationSetWasmTableElement, arguments, shouldThrow);
-    Location shouldThrowLocation = allocate(shouldThrow);
+    Location shouldThrowLocation = loadIfNecessary(shouldThrow);
 
     LOG_INSTRUCTION("TableSet", tableIndex, index, value);
 
@@ -857,7 +857,7 @@ PartialResult WARN_UNUSED_RETURN BBQJIT::addTableInit(unsigned elementIndex, uns
     };
     Value shouldThrow = topValue(TypeKind::I32);
     emitCCall(&operationWasmTableInit, arguments, shouldThrow);
-    Location shouldThrowLocation = allocate(shouldThrow);
+    Location shouldThrowLocation = loadIfNecessary(shouldThrow);
 
     LOG_INSTRUCTION("TableInit", tableIndex, dstOffset, srcOffset, length);
 
@@ -921,7 +921,7 @@ PartialResult WARN_UNUSED_RETURN BBQJIT::addTableFill(unsigned tableIndex, Value
     };
     Value shouldThrow = topValue(TypeKind::I32);
     emitCCall(&operationWasmTableFill, arguments, shouldThrow);
-    Location shouldThrowLocation = allocate(shouldThrow);
+    Location shouldThrowLocation = loadIfNecessary(shouldThrow);
 
     LOG_INSTRUCTION("TableFill", tableIndex, fill, offset, count);
 
@@ -946,7 +946,7 @@ PartialResult WARN_UNUSED_RETURN BBQJIT::addTableCopy(unsigned dstTableIndex, un
     };
     Value shouldThrow = topValue(TypeKind::I32);
     emitCCall(&operationWasmTableCopy, arguments, shouldThrow);
-    Location shouldThrowLocation = allocate(shouldThrow);
+    Location shouldThrowLocation = loadIfNecessary(shouldThrow);
 
     LOG_INSTRUCTION("TableCopy", dstTableIndex, srcTableIndex, dstOffset, srcOffset, length);
 
@@ -1110,7 +1110,7 @@ PartialResult WARN_UNUSED_RETURN BBQJIT::addMemoryFill(Value dstAddress, Value t
     };
     Value shouldThrow = topValue(TypeKind::I32);
     emitCCall(&operationWasmMemoryFill, arguments, shouldThrow);
-    Location shouldThrowLocation = allocate(shouldThrow);
+    Location shouldThrowLocation = loadIfNecessary(shouldThrow);
 
     throwExceptionIf(ExceptionType::OutOfBoundsMemoryAccess, m_jit.branchTest32(ResultCondition::Zero, shouldThrowLocation.asGPR()));
 
@@ -1133,7 +1133,7 @@ PartialResult WARN_UNUSED_RETURN BBQJIT::addMemoryCopy(Value dstAddress, Value s
     };
     Value shouldThrow = topValue(TypeKind::I32);
     emitCCall(&operationWasmMemoryCopy, arguments, shouldThrow);
-    Location shouldThrowLocation = allocate(shouldThrow);
+    Location shouldThrowLocation = loadIfNecessary(shouldThrow);
 
     throwExceptionIf(ExceptionType::OutOfBoundsMemoryAccess, m_jit.branchTest32(ResultCondition::Zero, shouldThrowLocation.asGPR()));
 
@@ -1157,7 +1157,7 @@ PartialResult WARN_UNUSED_RETURN BBQJIT::addMemoryInit(unsigned dataSegmentIndex
     };
     Value shouldThrow = topValue(TypeKind::I32);
     emitCCall(&operationWasmMemoryInit, arguments, shouldThrow);
-    Location shouldThrowLocation = allocate(shouldThrow);
+    Location shouldThrowLocation = loadIfNecessary(shouldThrow);
 
     throwExceptionIf(ExceptionType::OutOfBoundsMemoryAccess, m_jit.branchTest32(ResultCondition::Zero, shouldThrowLocation.asGPR()));
 
@@ -1262,7 +1262,7 @@ PartialResult WARN_UNUSED_RETURN BBQJIT::atomicWait(ExtAtomicOpType op, Expressi
         emitCCall(&operationMemoryAtomicWait32, arguments, result);
     else
         emitCCall(&operationMemoryAtomicWait64, arguments, result);
-    Location resultLocation = allocate(result);
+    Location resultLocation = loadIfNecessary(result);
 
     LOG_INSTRUCTION(makeString(op), pointer, value, timeout, uoffset, RESULT(result));
 
@@ -1280,7 +1280,7 @@ PartialResult WARN_UNUSED_RETURN BBQJIT::atomicNotify(ExtAtomicOpType op, Expres
     };
     result = topValue(TypeKind::I32);
     emitCCall(&operationMemoryAtomicNotify, arguments, result);
-    Location resultLocation = allocate(result);
+    Location resultLocation = loadIfNecessary(result);
 
     LOG_INSTRUCTION(makeString(op), pointer, count, uoffset, RESULT(result));
 
@@ -1439,7 +1439,7 @@ void BBQJIT::pushArrayNewFromSegment(ArraySegmentOperation operation, uint32_t t
     };
     result = topValue(TypeKind::I64);
     emitCCall(operation, arguments, result);
-    Location resultLocation = allocate(result);
+    Location resultLocation = loadIfNecessary(result);
 
     emitThrowOnNullReference(exceptionType, resultLocation);
 }
@@ -1463,6 +1463,14 @@ PartialResult WARN_UNUSED_RETURN BBQJIT::addArrayCopy(uint32_t dstTypeIndex, Exp
     if (dst.isConst() || src.isConst()) {
         ASSERT_IMPLIES(dst.isConst(), dst.asI64() == JSValue::encode(jsNull()));
         ASSERT_IMPLIES(src.isConst(), src.asI64() == JSValue::encode(jsNull()));
+
+        LOG_INSTRUCTION("ArrayCopy", dstTypeIndex, dst, dstOffset, srcTypeIndex, src, srcOffset, size);
+
+        consume(dst);
+        consume(dstOffset);
+        consume(src);
+        consume(srcOffset);
+        consume(size);
         emitThrowException(ExceptionType::NullArrayCopy);
         return { };
     }
@@ -1480,7 +1488,7 @@ PartialResult WARN_UNUSED_RETURN BBQJIT::addArrayCopy(uint32_t dstTypeIndex, Exp
     };
     Value shouldThrow = topValue(TypeKind::I32);
     emitCCall(&operationWasmArrayCopy, arguments, shouldThrow);
-    Location shouldThrowLocation = allocate(shouldThrow);
+    Location shouldThrowLocation = loadIfNecessary(shouldThrow);
 
     LOG_INSTRUCTION("ArrayCopy", dstTypeIndex, dst, dstOffset, srcTypeIndex, src, srcOffset, size);
 
@@ -1495,6 +1503,12 @@ PartialResult WARN_UNUSED_RETURN BBQJIT::addArrayInitElem(uint32_t dstTypeIndex,
 {
     if (dst.isConst()) {
         ASSERT(dst.asI64() == JSValue::encode(jsNull()));
+
+        LOG_INSTRUCTION("ArrayInitElem", dstTypeIndex, dst, dstOffset, srcElementIndex, srcOffset, size);
+
+        consume(dstOffset);
+        consume(srcOffset);
+        consume(size);
         emitThrowException(ExceptionType::NullArrayInitElem);
         return { };
     }
@@ -1511,7 +1525,7 @@ PartialResult WARN_UNUSED_RETURN BBQJIT::addArrayInitElem(uint32_t dstTypeIndex,
     };
     Value shouldThrow = topValue(TypeKind::I32);
     emitCCall(&operationWasmArrayInitElem, arguments, shouldThrow);
-    Location shouldThrowLocation = allocate(shouldThrow);
+    Location shouldThrowLocation = loadIfNecessary(shouldThrow);

Background

Advisory: Apple Security Advisory

Attribute Value
CVE CVE-2025-24158
Bugzilla 283889
Component WebKit
Bug Class LogicError
Severity medium
Commit 8964a07ea3fb6e4a…
Advisory Apple Advisory

Terminology

  • BBQ JIT — JavaScriptCore’s baseline optimizing wasm compiler with its own register allocator.
  • allocate vs loadIfNecessary — allocate() forces a fresh register for a value; loadIfNecessary() reuses the value’s existing location if it already has one.
  • shouldThrow value — A computed flag operand for bulk table/memory ops that must reside in a register for the trap check.
  • Register pressure — When few registers are free, forcing an allocation on an already-located value breaks allocator invariants.

Root Cause Analysis

JavaScriptCore’s Wasm BBQ JIT emits table and memory bulk operations (addTableSet, addTableInit, addTableFill, addTableCopy, addMemoryFill, addMemoryCopy, addMemoryInit) and some atomics/array-segment ops that materialize a ‘shouldThrow’/result value into a register. These call sites used allocate(shouldThrow) / allocate(result), which unconditionally allocates a fresh register location for the value. When the value is already materialized in a location (or when register pressure means allocate cannot proceed as assumed), forcing a new allocation drives the BBQ register allocator into an inconsistent/invalid state, hitting a release assertion or otherwise aborting – a reliably reachable crash (denial-of-service). The fix replaces allocate(...) with loadIfNecessary(...) at these sites, which brings the value into a register only if it is not already in one, matching how the value is actually used. The restored invariant is that these bulk table/memory operation operands are materialized with loadIfNecessary rather than a forced allocate, keeping the register allocator consistent and avoiding the crash.

Attack Path

1. Craft wasm with bulk table/memory ops

Build a module whose functions use table.set/init/fill/copy or memory.fill/copy/init under register pressure.

2. Compile via BBQ JIT

Instantiation compiles the ops, which call allocate() on a value that is already located.

3. Corrupt allocator state

The forced allocate drives the register allocator into an invalid state.

4. Denial-of-service

A release assertion/abort crashes the WebContent process (reliable DoS).

Impact Assessment

A denial-of-service (reliable crash) from BBQ-JIT register mishandling in bulk table/memory ops, reachable from crafted wasm in the WebContent process. Per the advisory the impact is DoS rather than memory corruption; the fix restores allocator consistency. Confined to WebContent; rated medium.

Audit Directions

Other allocate() misuse

grep WasmBBQJIT for allocate(value) where the value may already be materialized and loadIfNecessary is the correct call.

Bulk op operand handling

Audit table/memory/array bulk-op operand materialization for consistent load/allocate/consume discipline.

Register-exhaustion asserts

Review RELEASE_ASSERTs in the BBQ allocator that a crafted module could deterministically trip.

Changed Functions

Function File Change Note
BBQJIT::addTableSet/Init/Fill/Copy, addMemoryFill/Copy/Init, atomicWait/Notify, pushArrayNewFromSegment Source/JavaScriptCore/wasm/WasmBBQJIT.cpp modified Uses loadIfNecessary(shouldThrow)/loadIfNecessary(result) instead of allocate(…), materializing the value only when needed and keeping the register allocator consistent.

Regression Test

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

diff --git a/JSTests/wasm/stress/array-copy-with-constant-null.js b/JSTests/wasm/stress/array-copy-with-constant-null.js
new file mode 100644
index 000000000000..dded10f50c95
--- /dev/null
+++ b/JSTests/wasm/stress/array-copy-with-constant-null.js
@@ -0,0 +1,57 @@
+import * as assert from '../assert.js'
+
+/*
+(module
+    (type $A (array (mut i32)))
+
+    (func (export "null_src") (result i32) (local (ref $A))
+        i32.const 4
+        i32.const 42
+        array.new $A
+        local.tee 0
+        i32.const 0
+        ref.null $A
+        i32.const 0
+        i32.const 4
+        array.copy $A $A
+
+        local.get 0
+        i32.const 0
+        array.get $A
+    )
+
+    (func (export "null_dst") (result i32) (local (ref $A))
+        ref.null $A
+        i32.const 0
+        i32.const 4
+        i32.const 42
+        array.new $A
+        local.tee 0
+        i32.const 0
+        i32.const 4
+        array.copy $A $A
+
+        local.get 0
+        i32.const 0
+        array.get $A
+    )
+)
+*/
+
+const wasm = new Uint8Array([
+    0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x88, 0x80, 0x80, 0x80, 0x00, 0x02, 0x5e, 0x7f,
+    0x01, 0x60, 0x00, 0x01, 0x7f, 0x03, 0x83, 0x80, 0x80, 0x80, 0x00, 0x02, 0x01, 0x01, 0x07, 0x97, 0x80,
+    0x80, 0x80, 0x00, 0x02, 0x08, 0x6e, 0x75, 0x6c, 0x6c, 0x5f, 0x73, 0x72, 0x63, 0x00, 0x00, 0x08, 0x6e,
+    0x75, 0x6c, 0x6c, 0x5f, 0x64, 0x73, 0x74, 0x00, 0x01, 0x0a, 0xcd, 0x80, 0x80, 0x80, 0x00, 0x02, 0xa1,
+    0x80, 0x80, 0x80, 0x00, 0x01, 0x01, 0x64, 0x00, 0x41, 0x04, 0x41, 0x2a, 0xfb, 0x06, 0x00, 0x22, 0x00,
+    0x41, 0x00, 0xd0, 0x00, 0x41, 0x00, 0x41, 0x04, 0xfb, 0x11, 0x00, 0x00, 0x20, 0x00, 0x41, 0x00, 0xfb,
+    0x0b, 0x00, 0x0b, 0xa1, 0x80, 0x80, 0x80, 0x00, 0x01, 0x01, 0x64, 0x00, 0xd0, 0x00, 0x41, 0x00, 0x41,
+    0x04, 0x41, 0x2a, 0xfb, 0x06, 0x00, 0x22, 0x00, 0x41, 0x00, 0x41, 0x04, 0xfb, 0x11, 0x00, 0x00, 0x20,
+    0x00, 0x41, 0x00, 0xfb, 0x0b, 0x00, 0x0b
+]);
+
+let instance = WebAssembly.instantiate(wasm);
+for (let i = 0; i < 50000; i ++) {
+    assert.throws(() => instance.exports.null_src(), TypeError);
+    assert.throws(() => instance.exports.null_dst(), TypeError);
+}