Released June 29, 2026

CVE-2026-43745

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

Overview

Impact:

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

Researchers: OpenAI Codex Security - Amy Burnett, Khai Tran

Key insight. A validation invariant (reject modules mixing legacy exceptions and try_table) was enforced only on the non-streaming compile path, so streaming compilation accepted modules the engine otherwise forbids — a compile-path parity gap.

Patch Details

diff --git a/JSTests/wasm/stress/streaming-compile-try-table-agreement.js b/JSTests/wasm/stress/streaming-compile-try-table-agreement.js
new file mode 100644
index 000000000000..942083a56858
--- /dev/null
+++ b/JSTests/wasm/stress/streaming-compile-try-table-agreement.js
@@ -0,0 +1,106 @@
+// Streaming and non-streaming WebAssembly compilation should agree on the
+// outcome of a given module: both succeed, or both fail with
+// WebAssembly.CompileError. This test pins that equivalence for a couple of
+// modules that exercise try_table together with other exception handling
+// constructs.
+
+// (module
+//   (tag $e)
+//   (func (export "run")
+//     block $h
+//       try_table (catch_all $h)
+//         throw $e
+//       end
+//     end
+//   )
+// )
+const tryTableOnly = new Uint8Array([
+    0x00,0x61,0x73,0x6d, 0x01,0x00,0x00,0x00,
+    0x01,0x04, 0x01, 0x60,0x00,0x00,
+    0x03,0x02, 0x01, 0x00,
+    0x0d,0x03, 0x01, 0x00,0x00,
+    0x07,0x07, 0x01, 0x03,0x72,0x75,0x6e, 0x00,0x00,
+    0x0a,0x0f, 0x01, 0x0d,
+        0x00,
+        0x02,0x40,
+        0x1f,0x40, 0x01, 0x02,0x00,
+        0x08,0x00,
+        0x0b,
+        0x0b,
+        0x0b,
+]);
+
+// (module
+//   (tag $e)
+//   (func (export "run")
+//     try
+//       nop
+//     catch_all
+//       rethrow 0
+//     end
+//     block $h
+//       try_table (catch_all $h)
+//         throw $e
+//       end
+//     end
+//   )
+// )
+const mixedEH = new Uint8Array([
+    0x00,0x61,0x73,0x6d, 0x01,0x00,0x00,0x00,
+    0x01,0x04, 0x01, 0x60,0x00,0x00,
+    0x03,0x02, 0x01, 0x00,
+    0x0d,0x03, 0x01, 0x00,0x00,
+    0x07,0x07, 0x01, 0x03,0x72,0x75,0x6e, 0x00,0x00,
+    0x0a,0x16, 0x01, 0x14,
+        0x00,
+        0x06,0x40,
+        0x01,
+        0x19,
+        0x09,0x00,
+        0x0b,
+        0x02,0x40,
+        0x1f,0x40, 0x01, 0x02,0x00,
+        0x08,0x00,
+        0x0b,
+        0x0b,
+        0x0b,
+]);
+
+const cases = [tryTableOnly, mixedEH];
+
+function compileNonStreaming(bytes) {
+    try { new WebAssembly.Module(bytes); return null; }
+    catch (e) { return e; }
+}
+
+async function compileStreaming(bytes) {
+    try {
+        await $vm.createWasmStreamingCompilerForInstantiate(
+            (compiler) => { compiler.addBytes(bytes); });
+        return null;
+    } catch (e) { return e; }
+}
+
+async function main() {
+    for (const bytes of cases) {
+        const ns = compileNonStreaming(bytes);
+        const s = await compileStreaming(bytes);
+
+        const nsFailed = ns !== null;
+        const sFailed = s !== null;
+        if (nsFailed !== sFailed)
+            throw new Error(`compile-path mismatch: non-streaming=${ns}, streaming=${s}`);
+        if (nsFailed) {
+            if (!(ns instanceof WebAssembly.CompileError))
+                throw new Error("non-streaming threw non-CompileError: " + ns);
+            if (!(s instanceof WebAssembly.CompileError))
+                throw new Error("streaming threw non-CompileError: " + s);
+        }
+    }
+}
+
+main().catch(e => {
+    print(String(e));
+    print(String(e.stack));
+    $vm.abort();
+});
diff --git a/Source/JavaScriptCore/wasm/WasmEntryPlan.cpp b/Source/JavaScriptCore/wasm/WasmEntryPlan.cpp
index b6a0ddddbeba..abe34d949a22 100644
--- a/Source/JavaScriptCore/wasm/WasmEntryPlan.cpp
+++ b/Source/JavaScriptCore/wasm/WasmEntryPlan.cpp
@@ -229,10 +229,10 @@ void EntryPlan::compileFunctions()
     for (uint32_t index = functionIndex; index < functionIndexEnd; ++index)
         compileFunction(FunctionCodeIndex(index));
 
-    if (m_moduleInformation->m_usesModernExceptions.loadRelaxed() && m_moduleInformation->m_usesLegacyExceptions.loadRelaxed()) {
+    {
         Locker locker { m_lock };
-        fail(makeString("Module uses both legacy exceptions and try_table"_s));
-        return;
+        if (failIfMixedExceptionHandlingProposals())
+            return;
     }
 
     if (!areWasmToWasmStubsCompiled) {
@@ -267,6 +267,16 @@ void EntryPlan::complete()
     }
 }
 
+bool EntryPlan::failIfMixedExceptionHandlingProposals()
+{
+    if (m_moduleInformation->m_usesModernExceptions.loadRelaxed()
+        && m_moduleInformation->m_usesLegacyExceptions.loadRelaxed()) {
+        fail(makeString("Module uses both legacy exceptions and try_table"_s));
+        return true;
+    }
+    return false;
+}
+
 bool EntryPlan::completeSyncIfPossible()
 {
     Locker locker { m_lock };
diff --git a/Source/JavaScriptCore/wasm/WasmEntryPlan.h b/Source/JavaScriptCore/wasm/WasmEntryPlan.h
index d5b458d96fcc..c588ef36cdcb 100644
--- a/Source/JavaScriptCore/wasm/WasmEntryPlan.h
+++ b/Source/JavaScriptCore/wasm/WasmEntryPlan.h
@@ -123,6 +123,8 @@ protected:
     bool isComplete() const override { return m_state == State::Completed; }
     void complete() WTF_REQUIRES_LOCK(m_lock) override;
 
+    bool failIfMixedExceptionHandlingProposals() WTF_REQUIRES_LOCK(m_lock);
+
     virtual bool prepareImpl() = 0;
     virtual void compileFunction(FunctionCodeIndex functionIndex) = 0;
     virtual void didCompleteCompilation() WTF_REQUIRES_LOCK(m_lock) = 0;
diff --git a/Source/JavaScriptCore/wasm/WasmIPIntPlan.cpp b/Source/JavaScriptCore/wasm/WasmIPIntPlan.cpp
index a11bfd15c004..b085b37a3d12 100644
--- a/Source/JavaScriptCore/wasm/WasmIPIntPlan.cpp
+++ b/Source/JavaScriptCore/wasm/WasmIPIntPlan.cpp
@@ -166,6 +166,8 @@ void IPIntPlan::didCompleteCompilation()
 void IPIntPlan::completeInStreaming()
 {
     Locker locker { m_lock };
+    if (failIfMixedExceptionHandlingProposals())
+        return;
     complete();
 }
 

Background

Advisory: Apple Security Advisory

Attribute Value
CVE CVE-2026-43745
Bugzilla 315365
Component JSC
Bug Class LogicError
Severity medium
Commit 0f0de8f2a0582988…
Advisory Apple Advisory

Terminology

  • Legacy vs modern wasm exceptions — The original exception-handling proposal (try/catch/rethrow) and the newer try_table construct; a module must not mix them.
  • Streaming vs non-streaming compilation — Wasm can be compiled from a full buffer (WebAssembly.Module) or incrementally as bytes arrive (instantiateStreaming); both must reach the same accept/reject decision.
  • EntryPlan / IPIntPlan — JSC compilation plans that drive wasm function compilation; IPIntPlan handles the in-place interpreter/streaming completion.
  • Validation parity — The security-relevant property that all compilation entry points enforce identical validation, so no path accepts a module another rejects.

Root Cause Analysis

WebAssembly modules may not mix the legacy exception-handling proposal (try/catch, rethrow) with the modern try_table proposal in the same module; JSC rejects such modules with a CompileError. That guard was implemented only in the non-streaming compilation path: EntryPlan::compileFunctions() checked m_moduleInformation->m_usesModernExceptions && m_usesLegacyExceptions and called fail(...). The streaming compilation path (IPIntPlan::completeInStreaming()) had no equivalent check, so streaming and non-streaming compilation disagreed on the same bytes: a module mixing both EH schemes was rejected when compiled normally but accepted when compiled via the streaming API. Accepting such a module runs it through exception-handling codegen/metadata that assumes a single EH scheme, violating an invariant the rest of the EH machinery relies on. The fix factors the check into EntryPlan::failIfMixedExceptionHandlingProposals() (guarded by m_lock) and calls it from both the non-streaming path and IPIntPlan::completeInStreaming(), restoring the invariant that streaming and non-streaming compilation agree — both fail with a CompileError — for a module that mixes legacy exceptions and try_table. The added JSTests pin that streaming/non-streaming outcomes match for try_table-only and mixed-EH modules. That downstream EH handling of a wrongly-accepted mixed module is unsafe is an inference; the patch establishes the validation-parity gap and closes it.

Attack Path

1. Craft a mixed-EH module

Build a wasm module that uses both a legacy try/catch (or rethrow) and a modern try_table in its functions.

2. Compile via the streaming API

Instantiate it through streaming compilation (WebAssembly.instantiateStreaming / the streaming compiler), whose completion path lacked the mixed-EH guard.

3. Bypass validation

Streaming accepts the module that non-streaming would reject with a CompileError, so it is compiled and instantiable.

4. Exercise inconsistent EH codegen

Running the module drives exception-handling code paths that assume a single EH scheme (inference), risking mis-compiled EH and a crash/memory unsafety in WebContent.

Impact Assessment

The patch establishes a validation-parity gap: streaming compilation failed to reject modules that mix legacy and modern exception handling, which non-streaming compilation rejects. Running a wrongly-accepted mixed-EH module exercises exception-handling codegen that assumes a single scheme, which is a plausible route to memory unsafety (inference) rather than a demonstrated primitive; the concrete, patch-supported effect is inconsistent accept/reject behavior. Confined to WebContent (the wasm compiler). Rated medium.

Audit Directions

Other single-path validation

Diff EntryPlan/BBQ/OMG/IPInt streaming vs non-streaming completion for other checks present in one path only; grep for fail(makeString( guards not mirrored in completeInStreaming.

Proposal-mixing guards

Audit uses of m_usesModernExceptions/m_usesLegacyExceptions and other ‘usesX’ module flags to ensure every consumer enforces the same constraints.

Streaming completion paths

Review all *Plan::completeInStreaming()/completeSyncIfPossible() to confirm they run the full set of module-level validations before complete().

Changed Functions

Function File Change Note
EntryPlan::failIfMixedExceptionHandlingProposals Source/JavaScriptCore/wasm/WasmEntryPlan.cpp added Extracts the ‘uses both legacy exceptions and try_table’ check into one lock-guarded helper that fails the plan.
EntryPlan::compileFunctions Source/JavaScriptCore/wasm/WasmEntryPlan.cpp modified Now calls failIfMixedExceptionHandlingProposals() instead of an inline check.
IPIntPlan::completeInStreaming Source/JavaScriptCore/wasm/WasmIPIntPlan.cpp modified Adds the previously-missing failIfMixedExceptionHandlingProposals() call so streaming rejects mixed-EH modules too.
EntryPlan::failIfMixedExceptionHandlingProposals (decl) Source/JavaScriptCore/wasm/WasmEntryPlan.h modified Declares the helper WTF_REQUIRES_LOCK(m_lock).

Files Changed

Source Files

  • Source/JavaScriptCore/wasm/WasmEntryPlan.cpp
  • Source/JavaScriptCore/wasm/WasmEntryPlan.h
  • Source/JavaScriptCore/wasm/WasmIPIntPlan.cpp

Test Files

  • JSTests/wasm/stress/streaming-compile-try-table-agreement.js

Regression Test

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

diff --git a/JSTests/wasm/stress/streaming-compile-try-table-agreement.js b/JSTests/wasm/stress/streaming-compile-try-table-agreement.js
new file mode 100644
index 000000000000..942083a56858
--- /dev/null
+++ b/JSTests/wasm/stress/streaming-compile-try-table-agreement.js
@@ -0,0 +1,106 @@
+// Streaming and non-streaming WebAssembly compilation should agree on the
+// outcome of a given module: both succeed, or both fail with
+// WebAssembly.CompileError. This test pins that equivalence for a couple of
+// modules that exercise try_table together with other exception handling
+// constructs.
+
+// (module
+//   (tag $e)
+//   (func (export "run")
+//     block $h
+//       try_table (catch_all $h)
+//         throw $e
+//       end
+//     end
+//   )
+// )
+const tryTableOnly = new Uint8Array([
+    0x00,0x61,0x73,0x6d, 0x01,0x00,0x00,0x00,
+    0x01,0x04, 0x01, 0x60,0x00,0x00,
+    0x03,0x02, 0x01, 0x00,
+    0x0d,0x03, 0x01, 0x00,0x00,
+    0x07,0x07, 0x01, 0x03,0x72,0x75,0x6e, 0x00,0x00,
+    0x0a,0x0f, 0x01, 0x0d,
+        0x00,
+        0x02,0x40,
+        0x1f,0x40, 0x01, 0x02,0x00,
+        0x08,0x00,
+        0x0b,
+        0x0b,
+        0x0b,
+]);
+
+// (module
+//   (tag $e)
+//   (func (export "run")
+//     try
+//       nop
+//     catch_all
+//       rethrow 0
+//     end
+//     block $h
+//       try_table (catch_all $h)
+//         throw $e
+//       end
+//     end
+//   )
+// )
+const mixedEH = new Uint8Array([
+    0x00,0x61,0x73,0x6d, 0x01,0x00,0x00,0x00,
+    0x01,0x04, 0x01, 0x60,0x00,0x00,
+    0x03,0x02, 0x01, 0x00,
+    0x0d,0x03, 0x01, 0x00,0x00,
+    0x07,0x07, 0x01, 0x03,0x72,0x75,0x6e, 0x00,0x00,
+    0x0a,0x16, 0x01, 0x14,
+        0x00,
+        0x06,0x40,
+        0x01,
+        0x19,
+        0x09,0x00,
+        0x0b,
+        0x02,0x40,
+        0x1f,0x40, 0x01, 0x02,0x00,
+        0x08,0x00,
+        0x0b,
+        0x0b,
+        0x0b,
+]);
+
+const cases = [tryTableOnly, mixedEH];
+
+function compileNonStreaming(bytes) {
+    try { new WebAssembly.Module(bytes); return null; }
+    catch (e) { return e; }
+}
+
+async function compileStreaming(bytes) {
+    try {
+        await $vm.createWasmStreamingCompilerForInstantiate(
+            (compiler) => { compiler.addBytes(bytes); });
+        return null;
+    } catch (e) { return e; }
+}
+
+async function main() {
+    for (const bytes of cases) {
+        const ns = compileNonStreaming(bytes);
+        const s = await compileStreaming(bytes);
+
+        const nsFailed = ns !== null;
+        const sFailed = s !== null;
+        if (nsFailed !== sFailed)
+            throw new Error(`compile-path mismatch: non-streaming=${ns}, streaming=${s}`);
+        if (nsFailed) {
+            if (!(ns instanceof WebAssembly.CompileError))
+                throw new Error("non-streaming threw non-CompileError: " + ns);
+            if (!(s instanceof WebAssembly.CompileError))
+                throw new Error("streaming threw non-CompileError: " + s);
+        }
+    }
+}
+
+main().catch(e => {
+    print(String(e));
+    print(String(e.stack));
+    $vm.abort();
+});