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 |
|---|---|---|
IPInt::BlockMetadata (struct)Source/JavaScriptCore/wasm/WasmIPIntGenerator.h |
modified | deltaPC/deltaMC changed from uint32_t to int32_t; comment added noting field order matters (loadpairi) and negative deltas need sign extension. |
IPInt::IfMetadata (struct)Source/JavaScriptCore/wasm/WasmIPIntGenerator.h |
modified | Kept unsigned but comments clarified as 'added to PC/MC' and that field order is significant for the paired load; behavior unchanged (else deltas are always forward). |
IPIntGenerator::resolveEntryTargetSource/JavaScriptCore/wasm/WasmIPIntGenerator.cpp |
modified | Casts loc.pc-src.pc and loc.mc-src.mc to int32_t when writing BlockMetadata for entry and (loop) branch targets, which can be backward. |
IPIntGenerator::resolveExitTargetSource/JavaScriptCore/wasm/WasmIPIntGenerator.cpp |
modified | Same int32_t cast for exit-target and non-loop branch-target metadata writes. |
IPIntGenerator::coalesceControlFlowSource/JavaScriptCore/wasm/WasmIPIntGenerator.cpp |
modified | Casts here.pc-src.pc / here.mc-src.mc to int32_t for exit-handler coalescing (the try_table catch path that can branch backward). |
IPIntGenerator (loop-metadata writer, ~line 579)Source/JavaScriptCore/wasm/WasmIPIntGenerator.cpp |
modified | Casts the loop entry-target deltas (m_entryTarget.pc/mc - loc.pc/mc) to int32_t; these are the canonical backward-delta case guarded by isLoop. |
IPInt interpreter branch dispatch (InPlaceInterpreter64.asm)Source/JavaScriptCore/llint/InPlaceInterpreter64.asm |
modified | Adds sxi2q sign-extension of t0/t1 after loadi in the one backward-capable dispatch; annotates the five forward-only dispatches that sign extension is unnecessary. |
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.