Medium CVSS 4.3 webkit OOB 🔧 Commit mapped

Overview

Medium
Severity
4.3
CVSS
No
Exploited ITW
Fixed
Fix Status
DescriptionProcessing maliciously crafted web content may lead to an unexpected process crash
ComponentJSC LLInt
Bug ClassOOB
Tracker298628
Fix commit127c1d5c4d40 (WebKit/WebKit) +24/-3
CWECWE-20, CWE-703 (Improper input validation)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:L
CISA KEVNot listed
CreditedGary Kwong, rheza (@ginggilBesel)
Disclosed2025-11-03

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.

Key insight
Overloading the sentinel m_min == INT32_MAX to mean ’list table’ collides with a legitimate dense switch whose case value is INT32_MAX; the fix is to carry an explicit m_isList flag (and serialize it) rather than infer table shape from a magic minimum value.

Attack Path

  1. 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).
  2. 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.
  3. 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).
  4. 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).
  5. (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

The commit establishes a jump-table misclassification bug in a core JS control-flow op whose demonstrated effect is an unexpected crash in the WebContent process when a dense switch with a case of INT32_MAX is treated as a sparse list, causing an incorrect branch-offset computation. Because the flawed path can index the jump table with a bogus (value - INT32_MAX) offset, there is a plausible out-of-bounds read of branch offsets and a resulting wild jump; the diff shows the misclassification and the indexing logic but not a working memory-disclosure or control primitive, so beyond the proven crash any exploitation is inference. It is confined to the JS engine in the WebContent process and does not itself cross the sandbox.

Changed Functions

FunctionChangeNotes
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 / isList
Source/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/decode
Source/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.js
  • Source/JavaScriptCore/bytecode/UnlinkedCodeBlock.h
  • Source/JavaScriptCore/bytecompiler/BytecodeGenerator.cpp
  • Source/JavaScriptCore/llint/LowLevelInterpreter32_64.asm
  • Source/JavaScriptCore/llint/LowLevelInterpreter64.asm
  • Source/JavaScriptCore/runtime/CachedTypes.cpp

Audit Directions

  • Same-op: op_switch_char / op_switch_string tables
    Check 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()/offsetForValue
    Grep 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 completeness
    Audit 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 elsewhere
    Search 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.

Original Bug Report

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