CVE-2025-24158
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);
+}