Medium CVSS 6.5 webkit Logic Error 🔧 Commit mapped

Overview

Medium
Severity
6.5
CVSS
No
Exploited ITW
Fixed
Fix Status
DescriptionProcessing web content may lead to a denial-of-service
ComponentWebKit
Sub-componentJSC Wasm
Bug ClassLogic Error
Tracker283889
Fix commit8964a07ea3fb (WebKit/WebKit)
CWECWE-79 (Cross-site scripting)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H
CISA KEVNot listed
CreditedQ1IQ (@q1iqF) of NUS CuriOSity and P1umer (@p1umer) of Imperial Global Singapore
Disclosed2025-01-27

Background

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.

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.

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.

Changed Functions

FunctionChangeNotes
for
Source/JavaScriptCore/wasm/WasmBBQJIT.cpp
modified

Files Changed

  • Source/JavaScriptCore/wasm/WasmBBQJIT.cpp
  • Source/JavaScriptCore/wasm/WasmBBQJIT.h
  • Source/JavaScriptCore/wasm/WasmBBQJIT32_64.cpp

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.
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);
 
     LOG_INSTRUCTION("ArrayInitElem", dstTypeIndex, dst, dstOffset, srcElementIndex, srcOffset, size);
 
@@ -1526,6 +1540,12 @@ PartialResult WARN_UNUSED_RETURN BBQJIT::addArrayInitData(uint32_t dstTypeIndex,
 {
     if (dst.isConst()) {
         ASSERT(dst.asI64() == JSValue::encode(jsNull()));
+
+        LOG_INSTRUCTION("ArrayInitData", dstTypeIndex, dst, dstOffset, srcDataIndex, srcOffset, size);
+
+        consume(dstOffset);
+        consume(srcOffset);
+        consume(size);
         emitThrowException(ExceptionType::NullArrayInitData);
         return { };
     }
@@ -1542,7 +1562,7 @@ PartialResult WARN_UNUSED_RETURN BBQJIT::addArrayInitData(uint32_t dstTypeIndex,
     };
     Value shouldThrow = topValue(TypeKind::I32);
     emitCCall(&operationWasmArrayInitData, arguments, shouldThrow);
-    Location shouldThrowLocation = allocate(shouldThrow);
+    Location shouldThrowLocation = loadIfNecessary(shouldThrow);
 
     LOG_INSTRUCTION("ArrayInitData", dstTypeIndex, dst, dstOffset, srcDataIndex, srcOffset, size);
 
@@ -1585,12 +1605,24 @@ PartialResult WARN_UNUSED_RETURN BBQJIT::addAnyConvertExtern(ExpressionType refe
     };
     result = topValue(TypeKind::Anyref);
     emitCCall(&operationWasmAnyConvertExtern, arguments, result);
+
+    LOG_INSTRUCTION("AnyConvertExtern", reference, RESULT(result));
     return { };
 }
 
 PartialResult WARN_UNUSED_RETURN BBQJIT::addExternConvertAny(ExpressionType reference, ExpressionType& result)
 {
-    result = reference;
+    auto referenceLocation = reference.isConst() ? Location::none() : loadIfNecessary(reference);
+    consume(reference);
+
+    result = topValue(TypeKind::Externref);
+    auto resultLocation = allocate(result);
+    if (reference.isConst())
+        emitMoveConst(reference, resultLocation);
+    else
+        emitMove(reference.type(), referenceLocation, resultLocation);
+
+    LOG_INSTRUCTION("ExternConvertAny", reference, RESULT(result));
     return { };
 }
 
@@ -4500,6 +4532,22 @@ ALWAYS_INLINE void BBQJIT::willParseOpcode()
         }
         m_disassembler->setOpcode(m_jit.label(), PrefixedOpcode(m_parser->currentOpcode()), m_parser->currentOpcodeStartingOffset());
     }
+
+#if ASSERT_ENABLED
+    if (shouldFuseBranchCompare && isCompareOpType(m_prevOpcode)
+        && (m_parser->currentOpcode() == OpType::BrIf || m_parser->currentOpcode() == OpType::If)) {
+        m_prevOpcode = m_parser->currentOpcode();
+        return;
+    }
+    for (Value value : m_justPoppedStack) {
+        // Temps should have been consumed, we should have removed them from this list.
+        ASSERT_WITH_MESSAGE(!value.isTemp(), "Temp(%u) was not consumed by the instruction that popped it!", value.asTemp());
+
+        ASSERT(!value.isLocal()); // Change this if/when we start register-allocating locals.
+    }
+    m_prevOpcode = m_parser->currentOpcode();
+    m_justPoppedStack.clear();
+#endif
 }
 
 ALWAYS_INLINE void BBQJIT::willParseExtendedOpcode()
@@ -4524,7 +4572,14 @@ bool BBQJIT::usesSIMD()
 
 void BBQJIT::dump(const ControlStack&, const Stack*) { }
 void BBQJIT::didFinishParsingLocals() { }
-void BBQJIT::didPopValueFromStack(ExpressionType, ASCIILiteral) { }
+
+void BBQJIT::didPopValueFromStack(Value value, ASCIILiteral)
+{
+    UNUSED_PARAM(value);
+#if ASSERT_ENABLED
+    m_justPoppedStack.append(value);
+#endif
+}
 
 void BBQJIT::finalize()
 {
@@ -4694,15 +4749,6 @@ Location BBQJIT::allocateWithHint(Value value, Location hint)
     if (value.isPinned())
         return value.asPinned();
 
-    // It's possible, in some circumstances, that the value in question was already assigned to a register.
-    // The most common example of this is returned values (that use registers from the calling convention) or
-    // values passed between control blocks. In these cases, we just leave it in its register unmoved.
-    Location existingLocation = locationOf(value);
-    if (existingLocation.isRegister()) {
-        ASSERT(value.isFloat() == existingLocation.isFPR());
-        return existingLocation;
-    }
-
     Location reg = hint;
     if (reg.kind() == Location::None
         || value.isFloat() != reg.isFPR()
@@ -4775,6 +4821,12 @@ void BBQJIT::consume(Value value)
     Location location = locationOf(value);
     if (value.isTemp() && location != canonicalSlot(value))
         unbind(value, location);
+
+#if ASSERT_ENABLED
+    m_justPoppedStack.removeFirstMatching([&](Value& popped) -> bool {
+        return popped.isTemp() && value.isTemp() && popped.asTemp() == value.asTemp();
+    });
+#endif
 }
 
 Location BBQJIT::allocateRegister(TypeKind type)
diff --git a/Source/JavaScriptCore/wasm/WasmBBQJIT.h b/Source/JavaScriptCore/wasm/WasmBBQJIT.h
index ad1ced3e5c1d..3debbe62957b 100644
--- a/Source/JavaScriptCore/wasm/WasmBBQJIT.h
+++ b/Source/JavaScriptCore/wasm/WasmBBQJIT.h
@@ -2294,6 +2294,11 @@ private:
 
     PCToCodeOriginMapBuilder m_pcToCodeOriginMapBuilder;
     std::unique_ptr<BBQDisassembler> m_disassembler;
+
+#if ASSERT_ENABLED
+    Vector<Value, 8> m_justPoppedStack;
+    OpType m_prevOpcode;
+#endif
 };
 
 using LocalOrTempIndex = BBQJIT::LocalOrTempIndex;
diff --git a/Source/JavaScriptCore/wasm/WasmBBQJIT32_64.cpp b/Source/JavaScriptCore/wasm/WasmBBQJIT32_64.cpp
index 03676a366152..7f82551ca8ee 100644
--- a/Source/JavaScriptCore/wasm/WasmBBQJIT32_64.cpp
+++ b/Source/JavaScriptCore/wasm/WasmBBQJIT32_64.cpp
@@ -221,7 +221,7 @@ PartialResult WARN_UNUSED_RETURN BBQJIT::addTableGet(unsigned tableIndex, Value
     };
     result = topValue(returnType);
     emitCCall(&operationGetWasmTableElement, arguments, result);
-    Location resultLocation = allocate(result);
+    Location resultLocation = loadIfNecessary(result);
 
     LOG_INSTRUCTION("TableGet", tableIndex, index, RESULT(result));
 
@@ -1505,7 +1505,7 @@ PartialResult WARN_UNUSED_RETURN BBQJIT::addArrayNewFixed(uint32_t typeIndex, Ar
     Value allocationResult = Value::fromTemp(TypeKind::Arrayref, currentControlData().enclosedHeight() + currentControlData().implicitSlots() + m_parser->expressionStack().size() + args.size());
     emitCCall(operationWasmArrayNewEmpty, arguments, allocationResult);
 
-    Location allocationResultLocation = allocate(allocationResult);
+    Location allocationResultLocation = loadIfNecessary(allocationResult);
     emitThrowOnNullReference(ExceptionType::BadArrayNew, allocationResultLocation);
 
     for (uint32_t i = 0; i < args.size(); ++i) {
@@ -1518,14 +1518,18 @@ PartialResult WARN_UNUSED_RETURN BBQJIT::addArrayNewFixed(uint32_t typeIndex, Ar
     }
 
     result = topValue(TypeKind::Arrayref);
-    Location resultLocation = allocate(result);
-    emitMove(allocationResult, resultLocation);
+    Location resultLocation;
 
     // If args.isEmpty() then allocationResult.asTemp() == result.asTemp() so we will consume our result.
     if (args.size()) {
         consume(allocationResult);
+        resultLocation = allocate(result);
+        emitMove(allocationResult.type(), allocationResultLocation, resultLocation);
         if (isRefType(getArrayElementType(typeIndex).unpacked()))
             emitWriteBarrier(resultLocation.asGPRlo());
+    } else {
+        RELEASE_ASSERT(result.asTemp() == allocationResult.asTemp());
+        resultLocation = allocationResultLocation;
     }
 
     LOG_INSTRUCTION("ArrayNewFixed", typeIndex, args.size(), RESULT(result));
@@ -1789,6 +1793,9 @@ PartialResult WARN_UNUSED_RETURN BBQJIT::addArraySet(uint32_t typeIndex, Express
 {
     if (arrayref.isConst()) {
         ASSERT(arrayref.asI64() == JSValue::encode(jsNull()));
+
+        LOG_INSTRUCTION("ArraySet", typeIndex, arrayref, index, value);
+        consume(value);
         emitThrowException(ExceptionType::NullArraySet);
         return { };
     }
@@ -1842,6 +1849,12 @@ PartialResult WARN_UNUSED_RETURN BBQJIT::addArrayFill(uint32_t typeIndex, Expres
 {
     if (arrayref.isConst()) {
         ASSERT(arrayref.asI64() == JSValue::encode(jsNull()));
+
+        LOG_INSTRUCTION("ArrayFill", typeIndex, arrayref, offset, value, size);
+
+        consume(offset);
+        consume(value);
+        consume(size);
         emitThrowException(ExceptionType::NullArrayFill);
         return { };
     }
@@ -1858,7 +1871,7 @@ PartialResult WARN_UNUSED_RETURN BBQJIT::addArrayFill(uint32_t typeIndex, Expres
         size
     };
     emitCCall(&operationWasmArrayFill, arguments, shouldThrow);
-    Location shouldThrowLocation = allocate(shouldThrow);
+    Location shouldThrowLocation = loadIfNecessary(shouldThrow);
 
     LOG_INSTRUCTION("ArrayFill", typeIndex, arrayref, offset, value, size);
 
@@ -1951,7 +1964,7 @@ PartialResult WARN_UNUSED_RETURN BBQJIT::addStructNewDefault(uint32_t typeIndex,
     emitCCall(operationWasmStructNewEmpty, arguments, result);
 
     const auto& structType = *m_info.typeSignatures[typeIndex]->expand().template as<StructType>();
-    Location structLocation = allocate(result);
+    Location structLocation = loadIfNecessary(result);
     emitThrowOnNullReference(ExceptionType::BadStructNew, structLocation);
     m_jit.loadPtr(MacroAssembler::Address(structLocation.asGPRlo(), JSWebAssemblyStruct::offsetOfPayload()), wasmScratchGPR);
     for (StructFieldCount i = 0; i < structType.fieldCount(); ++i) {
@@ -1979,7 +1992,7 @@ PartialResult WARN_UNUSED_RETURN BBQJIT::addStructNew(uint32_t typeIndex, Argume
     emitCCall(operationWasmStructNewEmpty, arguments, allocationResult);
 
     const auto& structType = *m_info.typeSignatures[typeIndex]->expand().template as<StructType>();
-    Location structLocation = allocate(allocationResult);
+    Location structLocation = loadIfNecessary(allocationResult);
     emitThrowOnNullReference(ExceptionType::BadStructNew, structLocation);
     m_jit.loadPtr(MacroAssembler::Address(structLocation.asGPRlo(), JSWebAssemblyStruct::offsetOfPayload()), wasmScratchGPR);
     bool hasRefTypeField = false;
@@ -1993,11 +2006,16 @@ PartialResult WARN_UNUSED_RETURN BBQJIT::addStructNew(uint32_t typeIndex, Argume
         emitWriteBarrier(structLocation.asGPRlo());
 
     result = topValue(TypeKind::Structref);
-    Location resultLocation = allocate(result);
-    emitMove(allocationResult, resultLocation);
+    Location resultLocation;
     // If args.isEmpty() then allocationResult.asTemp() == result.asTemp() so we will consume our result.
-    if (args.size())
+    if (args.size()) {
         consume(allocationResult);
+        resultLocation = allocate(result);
+        emitMove(allocationResult.type(), structLocation, resultLocation);
+    } else {
+        RELEASE_ASSERT(result.asTemp() == allocationResult.asTemp());
+        resultLocation = structLocation;
+    }
 
     LOG_INSTRUCTION("StructNew", typeIndex, args, RESULT(result));
 
@@ -2079,8 +2097,10 @@ PartialResult WARN_UNUSED_RETURN BBQJIT::addStructSet(Value structValue, const S
     if (structValue.isConst()) {
         // This is the only constant struct currently possible.
         ASSERT(JSValue::decode(structValue.asRef()).isNull());
-        emitThrowException(ExceptionType::NullStructSet);
+
         LOG_INSTRUCTION("StructSet", structValue, fieldIndex, value, "Exception");
+        consume(value);
+        emitThrowException(ExceptionType::NullStructSet);
         return { };
     }
 
@@ -2105,7 +2125,8 @@ PartialResult WARN_UNUSED_RETURN BBQJIT::addRefCast(ExpressionType reference, bo
     };
     result = topValue(TypeKind::Ref);
... (file diff truncated)
Loading diff…

Regression Test / PoC

shipped with the fix
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);
+}
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker.