Released August 17, 2026

CVE-2026-65332

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

Overview

Impact:

Description: Processing maliciously crafted web content may lead to an unexpected Safari crash

Researchers: OpenAI Codex Security - Amy Burnett

Key insight. endBlock forced the declared signature result type only for ’else’, so try/catch (and other structured control) ends accepted a subtype fallthrough; forcing the signature type for all of them prevents smuggling a narrower/mistyped value.

Patch Details

diff --git a/JSTests/wasm/regress/try-catch-result-type.js b/JSTests/wasm/regress/try-catch-result-type.js
new file mode 100644
index 000000000000..6d4c8a064ca4
--- /dev/null
+++ b/JSTests/wasm/regress/try-catch-result-type.js
@@ -0,0 +1,23 @@
+import * as assert from '../assert.js';
+
+const bytes = new Uint8Array([
+    0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00,
+    0x01, 0x8c, 0x80, 0x80, 0x80, 0x00,
+    0x03, 0x5e, 0x7e, 0x01, 0x60, 0x00, 0x00, 0x60, 0x01, 0x6f, 0x01, 0x7f,
+    0x03, 0x82, 0x80, 0x80, 0x80, 0x00, 0x01, 0x02,
+    0x0d, 0x83, 0x80, 0x80, 0x80, 0x00, 0x01, 0x00, 0x01,
+    0x07, 0x88, 0x80, 0x80, 0x80, 0x00, 0x01, 0x04, 0x6d, 0x61, 0x69, 0x6e, 0x00, 0x00,
+    0x0a, 0x99, 0x80, 0x80, 0x80, 0x00, 0x01, 0x93, 0x80, 0x80, 0x80, 0x00,
+    0x00, 0x06, 0x6e, 0x20, 0x00, 0xfb, 0x1a, 0x07, 0x00,
+    0x41, 0x01, 0xfb, 0x07, 0x00, 0x0b, 0xfb, 0x14, 0x6a, 0x0b,
+]);
+
+const instance = new WebAssembly.Instance(new WebAssembly.Module(bytes));
+
+const storage = new ArrayBuffer(8);
+const view = new DataView(storage);
+view.setBigUint64(0, 0x4141414141414141n - (1n << 49n), true);
+const nonCellExternref = view.getFloat64(0, true);
+
+for (let i = 0; i < wasmTestLoopCount; ++i)
+    assert.eq(instance.exports.main(nonCellExternref), 0);
\ No newline at end of file
diff --git a/Source/JavaScriptCore/wasm/WasmFunctionParser.h b/Source/JavaScriptCore/wasm/WasmFunctionParser.h
index 756c219bc5ca..4f4cf92ad41f 100644
--- a/Source/JavaScriptCore/wasm/WasmFunctionParser.h
+++ b/Source/JavaScriptCore/wasm/WasmFunctionParser.h
@@ -3855,13 +3855,14 @@ FOR_EACH_WASM_MEMORY_STORE_OP(CREATE_CASE)
             m_expressionStack.shrink(m_currentStackBegin);
             m_expressionStack.append(data.elseBlockStack.span());
         }
-        // When ending an 'if'/'else', including a synthetic 'else' added right above,
-        // the spec requires the output type of 'if' to be the type from the signature.
-        const bool shouldForceSignature = ControlType::isElse(data.controlData);
+
         // FIXME: endBlock may modify the expressionStack slice for the result of the block.
         // That's a little too effectful but we don't have a better API right now.
         // see: https://bugs.webkit.org/show_bug.cgi?id=164353
-        WASM_FAIL_IF_HELPER_FAILS(checkExpressionStack(data.controlData, shouldForceSignature));
+
+        // The spec requires the output type of a structured control instruction to be
+        // the result type from its signature, even when the fallthrough value is a subtype.
+        WASM_FAIL_IF_HELPER_FAILS(checkExpressionStack(data.controlData, true));
 
         const uint32_t parentBegin = parentEntryBegin();
         auto enclosedStack = m_expressionStack.mutableSpan().subspan(parentBegin);

Background

Advisory: Apple Security Advisory

Attribute Value
CVE CVE-2026-65332
Bugzilla 317450
Component JSC
Bug Class TypeConfusion
Severity medium
Commit 8f229fb72961093d…
Advisory Apple Advisory

Terminology

  • WASM structured control result type — Blocks/if/else/try declare a signature; per spec the block output must have the signature’s result type.
  • Subtyping / fallthrough value — With WASM GC, a fallthrough value may be a subtype of the result type; it must be treated as the declared type, not the subtype.
  • externref / non-cell value — A reference type; a raw non-cell bit pattern accepted where a reference is expected is a type confusion.

Root Cause Analysis

This fixes a WebAssembly type-confusion where a structured control instruction’s result was accepted as a subtype instead of its declared signature type. In WasmFunctionParser::endBlock, when ending an if/else/try(/catch) block the WASM spec requires the block’s output to have the RESULT TYPE from its signature, even when the fallthrough value on the stack is a subtype of that result type. Pre-patch, the code only forced the signature type for the ’else’ case: const bool shouldForceSignature = ControlType::isElse(data.controlData); and passed that to checkExpressionStack. For other structured control ends (notably try/catch), shouldForceSignature was false, so a fallthrough value of a NARROWER subtype was accepted and left on the stack as-is rather than being widened to the declared result type. Downstream code then treats that value as the declared (wider) type — a type confusion; the reproducer builds a try/catch that yields a non-cell externref (a raw float64 bit pattern) where a reference type is expected, so a non-object bit pattern is used as a reference. The fix always forces the signature result type: WASM_FAIL_IF_HELPER_FAILS(checkExpressionStack(data.controlData, true)) for all structured control ends. The restored invariant is that a structured control instruction’s output type is its signature result type, so a subtype fallthrough cannot smuggle a narrower/mistyped value past validation.

Attack Path

1. Craft a try/catch block

Build a WASM module whose try/catch (structured control) block declares a result type but yields a subtype fallthrough value.

2. Bypass the type check

Because endBlock did not force the signature type for try/catch, the narrower value is accepted on the stack.

3. Produce a mistyped reference

Yield a non-cell externref (a crafted float64 bit pattern) where a reference type is expected.

4. Type confusion

Downstream use treats the raw bit pattern as a valid reference, a memory-safety violation crashing the WebContent process.

Impact Assessment

A WebAssembly type-confusion in the WebContent process, reachable from any page running crafted WASM. Accepting a subtype (or a raw non-cell value) where the declared result type is expected lets an attacker use a mistyped reference — a memory-safety primitive the advisory rates as a crash but which can underpin stronger corruption.

Audit Directions

Structured-control result typing

Audit WasmFunctionParser end-of-block handling for other cases that skip forcing the signature result type or that special-case only ’else'.

Subtype-to-supertype coercions

Grep WASM validation for places a subtype value is left on the stack without being widened to the declared block/return type.

Changed Functions

Function File Change Note
IPIntGenerator/FunctionParser endBlock (checkExpressionStack) Source/JavaScriptCore/wasm/WasmFunctionParser.h modified Always forces the signature result type when ending a structured control instruction (checkExpressionStack(…, true)) instead of only for ’else’, so a subtype fallthrough cannot be accepted as the declared type.

Files Changed

Source Files

  • Source/JavaScriptCore/wasm/WasmFunctionParser.h

Test Files

  • JSTests/wasm/regress/try-catch-result-type.js

Regression Test

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

diff --git a/JSTests/wasm/regress/try-catch-result-type.js b/JSTests/wasm/regress/try-catch-result-type.js
new file mode 100644
index 000000000000..6d4c8a064ca4
--- /dev/null
+++ b/JSTests/wasm/regress/try-catch-result-type.js
@@ -0,0 +1,23 @@
+import * as assert from '../assert.js';
+
+const bytes = new Uint8Array([
+    0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00,
+    0x01, 0x8c, 0x80, 0x80, 0x80, 0x00,
+    0x03, 0x5e, 0x7e, 0x01, 0x60, 0x00, 0x00, 0x60, 0x01, 0x6f, 0x01, 0x7f,
+    0x03, 0x82, 0x80, 0x80, 0x80, 0x00, 0x01, 0x02,
+    0x0d, 0x83, 0x80, 0x80, 0x80, 0x00, 0x01, 0x00, 0x01,
+    0x07, 0x88, 0x80, 0x80, 0x80, 0x00, 0x01, 0x04, 0x6d, 0x61, 0x69, 0x6e, 0x00, 0x00,
+    0x0a, 0x99, 0x80, 0x80, 0x80, 0x00, 0x01, 0x93, 0x80, 0x80, 0x80, 0x00,
+    0x00, 0x06, 0x6e, 0x20, 0x00, 0xfb, 0x1a, 0x07, 0x00,
+    0x41, 0x01, 0xfb, 0x07, 0x00, 0x0b, 0xfb, 0x14, 0x6a, 0x0b,
+]);
+
+const instance = new WebAssembly.Instance(new WebAssembly.Module(bytes));
+
+const storage = new ArrayBuffer(8);
+const view = new DataView(storage);
+view.setBigUint64(0, 0x4141414141414141n - (1n << 49n), true);
+const nonCellExternref = view.getFloat64(0, true);
+
+for (let i = 0; i < wasmTestLoopCount; ++i)
+    assert.eq(instance.exports.main(nonCellExternref), 0);
\ No newline at end of file