Released November 3, 2025

CVE-2025-43427

CVSS 4.3 severity medium CWE-20CWE-703 vector CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:L

Overview

Impact:

Description: Processing maliciously crafted web content may lead to an unexpected process crash

Researchers: Gary Kwong, rheza (@ginggilBesel)

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.

Patch Details

diff --git a/JSTests/stress/switch-imm-int32-max.js b/JSTests/stress/switch-imm-int32-max.js
new file mode 100644
index 000000000000..db7949d90eda
--- /dev/null
+++ b/JSTests/stress/switch-imm-int32-max.js
@@ -0,0 +1,16 @@
+//@ runDefault("--useConcurrentJIT=false", "--thresholdForJITAfterWarmUp=10")
+try {
+    switch (1) {
+        case 2147483647:
+        case 2147483647:
+        case 2147483647:
+    }
+} catch(e39) {
+    switch (1) {
+        case 2147483647:
+            try {} catch(e80) {}
+    }
+    switch (1) {
+        case 2147483647:
+    }
+}
diff --git a/Source/JavaScriptCore/bytecode/UnlinkedCodeBlock.h b/Source/JavaScriptCore/bytecode/UnlinkedCodeBlock.h
index 2035018f8aa3..fd8388c8aeac 100644
--- a/Source/JavaScriptCore/bytecode/UnlinkedCodeBlock.h
+++ b/Source/JavaScriptCore/bytecode/UnlinkedCodeBlock.h
@@ -115,6 +115,7 @@ struct UnlinkedSimpleJumpTable {
     FixedVector<int32_t> m_branchOffsets;
     int32_t m_min { 0 };
     int32_t m_defaultOffset { 0 };
+    int32_t m_isList { 0 };
 
     inline int32_t offsetForValue(int32_t value) const
     {
@@ -135,7 +136,7 @@ struct UnlinkedSimpleJumpTable {
     int32_t defaultOffset() const { return m_defaultOffset; }
 
     // Returns true if this is a list-style jump table (key-offset pairs), used for sparse switches.
-    bool isList() const { return m_min == INT32_MAX; }
+    bool isList() const { return !!m_isList; }
 };
 
 class UnlinkedCodeBlock : public JSCell {
diff --git a/Source/JavaScriptCore/bytecompiler/BytecodeGenerator.cpp b/Source/JavaScriptCore/bytecompiler/BytecodeGenerator.cpp
index 8b561526e282..c5eee8e4c0f9 100644
--- a/Source/JavaScriptCore/bytecompiler/BytecodeGenerator.cpp
+++ b/Source/JavaScriptCore/bytecompiler/BytecodeGenerator.cpp
@@ -4464,6 +4464,7 @@ void BytecodeGenerator::endSwitch(const Vector<Ref<Label>, 8>& labels, Expressio
     auto handleSwitchList = [&](auto bytecode) {
         UnlinkedSimpleJumpTable& jumpTable = m_codeBlock->unlinkedSwitchJumpTable(bytecode.m_tableIndex);
         jumpTable.m_min = INT32_MAX;
+        jumpTable.m_isList = true;
 
         Vector<int32_t> branchOffsets;
         branchOffsets.reserveInitialCapacity(labels.size() * 2);
diff --git a/Source/JavaScriptCore/llint/LowLevelInterpreter32_64.asm b/Source/JavaScriptCore/llint/LowLevelInterpreter32_64.asm
index dbb989b4cf5d..e3359cf1fae6 100644
--- a/Source/JavaScriptCore/llint/LowLevelInterpreter32_64.asm
+++ b/Source/JavaScriptCore/llint/LowLevelInterpreter32_64.asm
@@ -2239,8 +2239,8 @@ llintOpWithJump(op_switch_imm, OpSwitchImm, macro (size, get, jump, dispatch)
 
     bineq t1, Int32Tag, .opSwitchImmNotInt
 
+    btinz UnlinkedSimpleJumpTable::m_isList[t2], .opSwitchImmSlow
     loadi UnlinkedSimpleJumpTable::m_min[t2], t3
-    bieq t3, (constexpr INT32_MAX), .opSwitchImmSlow
 
     subi t3, t0
     loadp UnlinkedSimpleJumpTable::m_branchOffsets + Int32FixedVector::m_storage[t2], t3
diff --git a/Source/JavaScriptCore/llint/LowLevelInterpreter64.asm b/Source/JavaScriptCore/llint/LowLevelInterpreter64.asm
index 2c6a766dd31e..5f87e445d0e8 100644
--- a/Source/JavaScriptCore/llint/LowLevelInterpreter64.asm
+++ b/Source/JavaScriptCore/llint/LowLevelInterpreter64.asm
@@ -2407,8 +2407,8 @@ llintOpWithJump(op_switch_imm, OpSwitchImm, macro (size, get, jump, dispatch)
 
     bqb t1, numberTag, .opSwitchImmNotInt
 
+    btinz UnlinkedSimpleJumpTable::m_isList[t2], .opSwitchImmSlow
     loadi UnlinkedSimpleJumpTable::m_min[t2], t3
-    bieq t3, (constexpr INT32_MAX), .opSwitchImmSlow
 
     subi t3, t1
     loadp UnlinkedSimpleJumpTable::m_branchOffsets + Int32FixedVector::m_storage[t2], t3
diff --git a/Source/JavaScriptCore/runtime/CachedTypes.cpp b/Source/JavaScriptCore/runtime/CachedTypes.cpp
index 7b3c4d958db4..94aa81f90bb3 100644
--- a/Source/JavaScriptCore/runtime/CachedTypes.cpp
+++ b/Source/JavaScriptCore/runtime/CachedTypes.cpp
@@ -890,6 +890,7 @@ public:
     {
         m_min = jumpTable.m_min;
         m_defaultOffset = jumpTable.m_defaultOffset;
+        m_isList = jumpTable.m_isList;
         m_branchOffsets.encode(encoder, jumpTable.m_branchOffsets);
     }
 
@@ -897,12 +898,14 @@ public:
     {
         jumpTable.m_min = m_min;
         jumpTable.m_defaultOffset = m_defaultOffset;
+        jumpTable.m_isList = m_isList;
         m_branchOffsets.decode(decoder, jumpTable.m_branchOffsets);
     }
 
 private:
     int32_t m_min;
     int32_t m_defaultOffset;
+    int32_t m_isList;
     CachedVector<int32_t> m_branchOffsets;
 };
 

Background

Advisory: Apple Security Advisory

Attribute Value
CVE CVE-2025-43427
Bugzilla 298628
Component JSC
Bug Class OOB
Severity medium
Commit 127c1d5c4d40d679…
Advisory Apple Advisory

Terminology

  • 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

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.

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.

Changed Functions

Function File Change Note
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

Source Files

  • 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

Test Files

  • JSTests/stress/switch-imm-int32-max.js

Regression Test

The fix ships this proof-of-concept / regression test:

diff --git a/JSTests/stress/switch-imm-int32-max.js b/JSTests/stress/switch-imm-int32-max.js
new file mode 100644
index 000000000000..db7949d90eda
--- /dev/null
+++ b/JSTests/stress/switch-imm-int32-max.js
@@ -0,0 +1,16 @@
+//@ runDefault("--useConcurrentJIT=false", "--thresholdForJITAfterWarmUp=10")
+try {
+    switch (1) {
+        case 2147483647:
+        case 2147483647:
+        case 2147483647:
+    }
+} catch(e39) {
+    switch (1) {
+        case 2147483647:
+            try {} catch(e80) {}
+    }
+    switch (1) {
+        case 2147483647:
+    }
+}