CVE-2025-24189
Overview
Impact:
Description: Processing maliciously crafted web content may lead to memory corruption
Researchers: an anonymous researcher
Key insight. The source bound for array.init_data was checked in element units while the copy ran in bytes, so size*elementSize could exceed the validated range; checking the byte extent with overflow detection closes the OOB.
Patch Details
diff --git a/JSTests/wasm/stress/array-init-data-bounds.js b/JSTests/wasm/stress/array-init-data-bounds.js
new file mode 100644
index 000000000000..07fc1612530b
--- /dev/null
+++ b/JSTests/wasm/stress/array-init-data-bounds.js
@@ -0,0 +1,30 @@
+/*
+(module
+ (memory (export "memory") 1)
+ (type $vec (array (mut i32)))
+ (data $data0 "1234")
+
+ (func $main(result (ref null $vec))
+ (local $arr(ref null $vec))
+ (local.set $arr (array.new_default $vec (i32.const 4)))
+
+ (array.init_data $vec $data0
+ (local.get $arr)
+ (i32.const 0)
+ (i32.const -2)
+ (i32.const 1))
+
+ (local.get $arr)
+ )
+
+ (export "main" (func $main))
+)
+ */
+var wasm_code = new Uint8Array([0x00,0x61,0x73,0x6d,0x01,0x00,0x00,0x00,0x01,0x89,0x80,0x80,0x80,0x00,0x02,0x5e,0x7f,0x01,0x60,0x00,0x01,0x63,0x00,0x03,0x82,0x80,0x80,0x80,0x00,0x01,0x01,0x05,0x83,0x80,0x80,0x80,0x00,0x01,0x00,0x01,0x07,0x91,0x80,0x80,0x80,0x00,0x02,0x06,0x6d,0x65,0x6d,0x6f,0x72,0x79,0x02,0x00,0x04,0x6d,0x61,0x69,0x6e,0x00,0x00,0x0c,0x81,0x80,0x80,0x80,0x00,0x01,0x0a,0xa0,0x80,0x80,0x80,0x00,0x01,0x9a,0x80,0x80,0x80,0x00,0x01,0x01,0x63,0x00,0x41,0x04,0xfb,0x07,0x00,0x21,0x00,0x20,0x00,0x41,0x00,0x41,0x7e,0x41,0x01,0xfb,0x12,0x00,0x00,0x20,0x00,0x0b,0x0b,0x87,0x80,0x80,0x80,0x00,0x01,0x01,0x04,0x31,0x32,0x33,0x34]);
+var wasm_module = new WebAssembly.Module(wasm_code);
+var wasm_instance = new WebAssembly.Instance(wasm_module);
+var f = wasm_instance.exports.main;
+try {
+ f();
+} catch {
+}
diff --git a/Source/JavaScriptCore/wasm/WasmOperationsInlines.h b/Source/JavaScriptCore/wasm/WasmOperationsInlines.h
index 757287d4bf3c..3d6816316672 100644
--- a/Source/JavaScriptCore/wasm/WasmOperationsInlines.h
+++ b/Source/JavaScriptCore/wasm/WasmOperationsInlines.h
@@ -440,13 +440,15 @@ inline bool arrayInitData(JSWebAssemblyInstance* instance, EncodedJSValue dst, u
if (lastDstElementIndexChecked > dstObject->size())
return false;
- CheckedUint32 lastSrcElementIndexChecked = srcOffset;
- lastSrcElementIndexChecked += size;
+ size_t elementSize = dstObject->elementType().type.elementSize();
- if (lastSrcElementIndexChecked.hasOverflowed())
+ CheckedUint32 lastSrcByteChecked = size;
+ lastSrcByteChecked *= elementSize;
+ lastSrcByteChecked += srcOffset;
+
+ if (lastSrcByteChecked.hasOverflowed())
return false;
- size_t elementSize = dstObject->elementType().type.elementSize();
return instance->copyDataSegment(dstObject, srcDataIndex, srcOffset, size * elementSize, dstObject->data() + dstOffset * elementSize);
}
Background
Advisory: Apple Security Advisory
| Attribute | Value |
|---|---|
| CVE | CVE-2025-24189 |
| Bugzilla | 284332 |
| Component | WebKit |
| Bug Class | OOB |
| Severity | medium |
| Commit | 9cee5daeabd138d0… |
| Advisory | Apple Advisory |
Terminology
- WASM GC arrays / array.init_data — WebAssembly garbage-collected arrays; array.init_data copies bytes from a module data segment into an array.
- element vs byte extent — An array of N elements spans N*elementSize bytes; bounds must be checked in bytes, not element count.
- CheckedUint32 — An overflow-trapping unsigned type used to validate the multiplication size*elementSize.
Root Cause Analysis
This fixes an out-of-bounds read in the WebAssembly GC array.init_data implementation caused by a source-range bounds check performed in element units rather than bytes. arrayInitData copies size elements from a passed data segment (at srcOffset) into a WASM array whose elements are elementSize bytes. Pre-patch, after checking the destination range, it validated the source only as CheckedUint32 lastSrcElementIndexChecked = srcOffset + size (an element count) for overflow, then performed the copy over size * elementSize bytes (instance->copyDataSegment(…, size * elementSize, …)). Because the overflow/extent check ignored elementSize, the actual byte extent (size * elementSize + srcOffset) could overflow or exceed what was validated, so a large size with a multi-byte element type reads out of bounds from the data segment. The fix computes the byte extent with overflow checking — CheckedUint32 lastSrcByteChecked = size; lastSrcByteChecked = elementSize; lastSrcByteChecked += srcOffset; — and returns false if it overflows, before copying. The restored invariant is that the source byte range (sizeelementSize + srcOffset), not just the element count, is overflow-checked. The regression test invokes array.init_data with size = -2 (0xFFFFFFFE).
Attack Path
1. Define a WASM GC array + data segment
Create a module with a mutable array type and a data segment.
2. Call array.init_data with a huge size
Invoke array.init_data with a size (e.g. 0xFFFFFFFE) whose byte extent size*elementSize overflows the element-count-only check.
3. Read out of bounds
The copy reads size*elementSize bytes from the data segment beyond its bounds into the array.
4. Disclose / corrupt memory
Use the OOB-sourced bytes as an information leak or corruption primitive in the WebContent process.
Impact Assessment
An out-of-bounds read from a WASM data segment in the WebContent process, reachable from any page running WebAssembly. It yields a memory-disclosure primitive (and potential corruption depending on downstream use); the advisory rates it memory corruption. The controllable size makes it a reliable leak building block.
Audit Directions
Same file: element vs byte bounds
Audit arrayInit/arrayCopy/arrayFill and other WasmOperationsInlines routines for bounds checks in element units that feed byte-sized copies (size * elementSize).
Overflow in size math
Grep WASM operations for multiplications by elementSize / stride not wrapped in CheckedUint32 before an access.
Changed Functions
| Function | File | Change | Note |
|---|---|---|---|
arrayInitData |
Source/JavaScriptCore/wasm/WasmOperationsInlines.h |
modified | Computes the source byte extent (size * elementSize + srcOffset) with CheckedUint32 and returns false on overflow, instead of only checking srcOffset + size in element units. |
Regression Test
The fix ships this proof-of-concept / regression test:
diff --git a/JSTests/wasm/stress/array-init-data-bounds.js b/JSTests/wasm/stress/array-init-data-bounds.js
new file mode 100644
index 000000000000..07fc1612530b
--- /dev/null
+++ b/JSTests/wasm/stress/array-init-data-bounds.js
@@ -0,0 +1,30 @@
+/*
+(module
+ (memory (export "memory") 1)
+ (type $vec (array (mut i32)))
+ (data $data0 "1234")
+
+ (func $main(result (ref null $vec))
+ (local $arr(ref null $vec))
+ (local.set $arr (array.new_default $vec (i32.const 4)))
+
+ (array.init_data $vec $data0
+ (local.get $arr)
+ (i32.const 0)
+ (i32.const -2)
+ (i32.const 1))
+
+ (local.get $arr)
+ )
+
+ (export "main" (func $main))
+)
+ */
+var wasm_code = new Uint8Array([0x00,0x61,0x73,0x6d,0x01,0x00,0x00,0x00,0x01,0x89,0x80,0x80,0x80,0x00,0x02,0x5e,0x7f,0x01,0x60,0x00,0x01,0x63,0x00,0x03,0x82,0x80,0x80,0x80,0x00,0x01,0x01,0x05,0x83,0x80,0x80,0x80,0x00,0x01,0x00,0x01,0x07,0x91,0x80,0x80,0x80,0x00,0x02,0x06,0x6d,0x65,0x6d,0x6f,0x72,0x79,0x02,0x00,0x04,0x6d,0x61,0x69,0x6e,0x00,0x00,0x0c,0x81,0x80,0x80,0x80,0x00,0x01,0x0a,0xa0,0x80,0x80,0x80,0x00,0x01,0x9a,0x80,0x80,0x80,0x00,0x01,0x01,0x63,0x00,0x41,0x04,0xfb,0x07,0x00,0x21,0x00,0x20,0x00,0x41,0x00,0x41,0x7e,0x41,0x01,0xfb,0x12,0x00,0x00,0x20,0x00,0x0b,0x0b,0x87,0x80,0x80,0x80,0x00,0x01,0x01,0x04,0x31,0x32,0x33,0x34]);
+var wasm_module = new WebAssembly.Module(wasm_code);
+var wasm_instance = new WebAssembly.Instance(wasm_module);
+var f = wasm_instance.exports.main;
+try {
+ f();
+} catch {
+}