CVE-2025-43427
Overview
Background
- op_switch_imm
- The bytecode operation that dispatches a switch over integer cases using a jump table, with a fast path in the LLInt and a C++ slow path.
- UnlinkedSimpleJumpTable
- The compiled representation of a switch table, either dense (indexed by value - m_min) or list-style (sparse key-offset pairs).
- List (sparse) jump table
- A switch table used when case values are widely spread; lookups scan key-offset pairs instead of indexing a contiguous array, marked historically by m_min == INT32_MAX.
- offsetForValue
- UnlinkedSimpleJumpTable’s lookup that returns the branch offset for a case value, choosing dense indexing or list scanning based on isList().
- LLInt
- JavaScriptCore’s Low-Level Interpreter, whose offlineasm-generated fast paths (LowLevelInterpreter64.asm etc.) implement bytecode ops like op_switch_imm.
- Bytecode cache (CachedTypes)
- The mechanism that serializes UnlinkedCodeBlock structures (including jump tables) to disk and back, requiring every semantically-relevant field to be encoded/decoded.
Root Cause Analysis
The patch fixes a mis-detection of list-style switch jump tables in JavaScriptCore caused by overloading the sentinel value INT32_MAX. UnlinkedSimpleJumpTable supports two shapes: a dense/immediate table indexed as m_branchOffsets[value - m_min], and a ’list’ (sparse) table of key-offset pairs used when case values are too spread out. Before the patch the code distinguished them implicitly: isList() returned m_min == INT32_MAX, and BytecodeGenerator::endSwitch’s list builder set jumpTable.m_min = INT32_MAX as the marker.
The invariant that was violated is that m_min == INT32_MAX must unambiguously mean ’this is a list table’ — but a legitimate dense immediate switch whose (only/minimum) case value is literally 2147483647 (INT32_MAX) also produces m_min == INT32_MAX, colliding with the sentinel.
The added regression test switch-imm-int32-max.js switches on case 2147483647 across nested try/catch. When such a dense table is misclassified as a list, offsetForValue and the op_switch_imm LLInt fast path mishandle it: the LLInt fast path previously bailed to the slow path only when m_min == INT32_MAX (bieq t3, INT32_MAX, .opSwitchImmSlow), and the C++ isList()/offsetForValue path selects the wrong indexing scheme, so lookups compute value - m_min with a sentinel min or index the wrong structure, producing an incorrect/out-of-range branch offset lookup and a wild control-flow target that crashes.
The fix stops overloading m_min: it adds an explicit int32_t m_isList field to UnlinkedSimpleJumpTable, sets jumpTable.m_isList = true only in the actual list builder (endSwitch), makes isList() return !!m_isList, serializes/deserializes m_isList through CachedSimpleJumpTable (bytecode cache), and changes both LLInt op_switch_imm implementations to branch to the slow path on ‘btinz m_isList[t2]’ instead of comparing m_min to INT32_MAX. This decouples ‘is a list table’ from ‘minimum case value equals INT32_MAX’, so a dense switch with a case of INT32_MAX is no longer treated as a sparse list.
Attack Path
- Write a switch with case INT32_MAX Author JavaScript containing a switch whose case value is 2147483647 (INT32_MAX) so the generated op_switch_imm jump table has m_min == INT32_MAX, colliding with the old list sentinel (as in switch-imm-int32-max.js).
- Shape the table to be dense Arrange the cases so the bytecode generator emits an immediate (dense) jump table rather than intentionally a list, so the sentinel collision causes a dense table to be misclassified as a list.
- Execute op_switch_imm Run the switch so op_switch_imm evaluates; the LLInt fast path or the C++ offsetForValue selects the wrong lookup scheme because isList() falsely reports true (or the fast path mis-bails).
- Compute a bad branch offset The mismatched indexing yields an out-of-range/incorrect branch offset (value - INT32_MAX or wrong-structure access), sending control flow to an invalid target and crashing the process (the stated impact).
- (Inference) Consider caching/tier interactions Because m_isList is now also serialized in the bytecode cache, a pre-patch mismatch between cached and recomputed classification could compound the confusion; the diff shows the cache field was added but does not demonstrate a cache-only exploit, so this is inference.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
BytecodeGenerator::endSwitch (handleSwitchList lambda)Source/JavaScriptCore/bytecompiler/BytecodeGenerator.cpp |
modified | Now sets jumpTable.m_isList = true when building a list-style table (in addition to m_min = INT32_MAX), so list tables are marked explicitly rather than only by the min sentinel. |
UnlinkedSimpleJumpTable / isListSource/JavaScriptCore/bytecode/UnlinkedCodeBlock.h |
modified | Added int32_t m_isList field and changed isList() from 'm_min == INT32_MAX' to '!!m_isList', decoupling list detection from the minimum case value. |
CachedSimpleJumpTable::encode/decodeSource/JavaScriptCore/runtime/CachedTypes.cpp |
modified | Serializes and deserializes the new m_isList field (and adds the m_isList member) so the flag survives bytecode caching. |
op_switch_imm (LLInt handler)Source/JavaScriptCore/llint/LowLevelInterpreter64.asm |
modified | Fast path now bails to .opSwitchImmSlow on 'btinz m_isList[t2]' instead of comparing m_min to INT32_MAX before doing the dense value - m_min index. |
op_switch_imm (LLInt handler)Source/JavaScriptCore/llint/LowLevelInterpreter32_64.asm |
modified | Same fix for the 32/64-bit LLInt: checks m_isList to decide the slow path rather than the INT32_MAX min sentinel. |
Files Changed
JSTests/stress/switch-imm-int32-max.jsSource/JavaScriptCore/bytecode/UnlinkedCodeBlock.hSource/JavaScriptCore/bytecompiler/BytecodeGenerator.cppSource/JavaScriptCore/llint/LowLevelInterpreter32_64.asmSource/JavaScriptCore/llint/LowLevelInterpreter64.asmSource/JavaScriptCore/runtime/CachedTypes.cpp
Audit Directions
- Same-op: op_switch_char / op_switch_string tablesCheck the analogous jump-table types and their LLInt handlers (op_switch_char, op_switch_string) for any ‘is list’ / dense-vs-sparse decision that keys off a sentinel min/length instead of an explicit flag; grep for INT32_MAX, m_min, and isList across bytecode/UnlinkedCodeBlock and the .asm switch handlers.
- All consumers of isList()/offsetForValueGrep JSC for isList(), offsetForValue(, and m_min == INT32_MAX to ensure every DFG/FTL/Baseline switch lowering now uses m_isList and that none still infer list-ness from the min value or from m_branchOffsets layout.
- Bytecode-cache field completenessAudit CachedTypes.cpp Cached* classes for structs that gained fields (like m_isList) but whose encode/decode may be missing a member, grepping for recently added struct fields versus their Cached counterpart to catch classification/state that does not round-trip through the cache.
- Sentinel/magic-value overloading elsewhereSearch JSC for boundary constants used as in-band flags (INT32_MAX, UINT_MAX, -1 as ‘special’) where a legitimate data value could collide, e.g. min/max fields doubling as mode markers, and prefer an explicit boolean/enum as this patch does.