Critical CVSS 8.8 webkit Integer Overflow 🔧 Commit mapped

Overview

Critical
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
DescriptionProcessing maliciously crafted web content may lead to arbitrary code execution
ComponentJSC B3
Bug ClassInteger Overflow
Tracker271491
Fix commit1ea4ef812727 (WebKit/WebKit) +34/-8
CWECWE-190 (Integer overflow)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedManfred Paul (@_manfp) working with Trend Micro Zero Day Initiative
Disclosed2024-05-13

Background

B3 / LowerToAir
JavaScriptCore’s optimizing JIT backend; LowerToAir turns B3 IR into Air (machine ops), including ARM64 bitfield instructions.
Bitfield instructions (SBFX/UBFX/BFI)
Extract/insert a field defined by a least-significant-bit offset and width; an out-of-range lsb/width accesses unintended bits.
safeAdd / overflow
WTF::safeAdd detects unsigned overflow; without it lsb + width can wrap below datasize and defeat the range check.

Root Cause Analysis

This fixes integer overflow/underflow in JavaScriptCore’s B3-to-Air lowering of bitfield instructions, which could emit an out-of-range ARM64 bitfield op and corrupt memory. In B3LowerToAir, several patterns lower to bitfield extract/insert instructions (SBFX/UBFX/BFI/EXTR and friends). Each validates the bit range with expressions like lsb + width > datasize (datasize is 32 or 64), where lsb and width derive from constant-folded, attacker-influenceable values held as uint64_t. Pre-patch these additions were unchecked, so lsb + width could overflow uint64 and wrap below datasize, passing the ‘> datasize’ guard even though the real range is out of bounds; the lowering then emits a bitfield instruction with an invalid lsb/width. Two of the signed variants additionally computed width = datasize - amount1 without checking amount1 < datasize, so amount1 >= datasize underflows width to a huge value. Either way the generated machine code operates on bits outside the intended field — a JIT miscompilation that reads/writes out of range and corrupts memory.

The fix replaces the additions with WTF::safeAdd(lsb, width, result) (bailing on overflow) across the extract/insert lowerings, and adds explicit if (amount1 >= datasize) return false guards before computing width = datasize - amount1.

The restored invariant is that a bitfield instruction is only emitted when its lsb+width range provably fits datasize without overflow/underflow. The regression test (sbfx-offset-overflow.js) drives the shift/mask pattern that lowers to a signed bitfield extract with a crafted offset.

Key insight
Bitfield-lowering range checks (lsb + width > datasize, and datasize - amount1) used unchecked 64-bit arithmetic, so overflow/underflow let an out-of-range field slip through; safeAdd plus an amount1 < datasize guard restores the bounds.

Attack Path

  1. Reach the B3/FTL JIT Run JS whose arithmetic (shifts/masks) the compiler lowers to an ARM64 bitfield instruction via B3LowerToAir.
  2. Craft the bit range Arrange constant-folded lsb/width (or amount1) so lsb + width overflows uint64 (or datasize - amount1 underflows).
  3. Bypass the range check The wrapped value passes the ‘> datasize’ guard, so the lowering emits a bitfield instruction with an out-of-range field.
  4. Corrupt memory The miscompiled instruction reads/writes bits outside the intended range, a memory-corruption primitive in WebContent toward code execution.

Impact Assessment

A critical JIT miscompilation in the WebContent process: an integer overflow/underflow lets attacker-shaped values emit an out-of-range bitfield instruction, a memory-corruption primitive the advisory rates as arbitrary code execution. JIT range-check overflows are historically strong routes to controlled read/write and RCE.

Changed Functions

FunctionChangeNotes
B3LowerToAir bitfield lowerings (ExtractUnsigned/SignedBitfield, ExtractRegister, InsertBitField, ExtractInsertBitfieldAtLowEnd, InsertUnsigned/SignedBitfieldInZero)
Source/JavaScriptCore/b3/B3LowerToAir.cpp
modified Replaces unchecked lsb + width / lowWidth + highWidth range checks with WTF::safeAdd (bail on overflow), and adds amount1 >= datasize guards before width = datasize - amount1 to prevent underflow, so no out-of-range bitfield instruction is emitted.

Files Changed

  • JSTests/stress/sbfx-offset-overflow.js
  • Source/JavaScriptCore/b3/B3LowerToAir.cpp

Audit Directions

  • Same file: remaining range math
    Audit B3LowerToAir for other lsb/width/shift computations feeding imm() operands that use raw addition/subtraction without safeAdd/underflow guards.
  • Constant-folded operands
    Grep the JIT for asInt()-derived widths/offsets used in emitted instruction encodings without overflow validation.
diff --git a/JSTests/stress/sbfx-offset-overflow.js b/JSTests/stress/sbfx-offset-overflow.js
new file mode 100644
index 000000000000..9d7f7cd7790a
--- /dev/null
+++ b/JSTests/stress/sbfx-offset-overflow.js
@@ -0,0 +1,16 @@
+function foo(a,b,c) { let x = a | 0; let y = b | 0; let z = c &15;
+z = (x<<y)^(x<<(y&0x10ff)); let r = z^0xf01;
+let s = z^0xf1f;
+return (((a>>>r)<<s)>>s);
+}
+let LEN = 100000000-1;
+let res = 0;
+res = foo((LEN&127),456,789);
+
+if (res != -1)
+    throw "Wrong result: " + res
+
+for (let i = 0; i <= LEN; i++) res = foo((i&127),456,789);
+
+if (res != -1)
+    throw "Wrong result: " + res
\ No newline at end of file
diff --git a/Source/JavaScriptCore/b3/B3LowerToAir.cpp b/Source/JavaScriptCore/b3/B3LowerToAir.cpp
index 33adcbd1c25a..e8ff89dc84c7 100644
--- a/Source/JavaScriptCore/b3/B3LowerToAir.cpp
+++ b/Source/JavaScriptCore/b3/B3LowerToAir.cpp
@@ -3301,7 +3301,8 @@ class LowerToAir {
                     return false;
                 uint64_t width = WTF::bitCount(mask);
                 uint64_t datasize = opcode == ExtractUnsignedBitfield32 ? 32 : 64;
-                if (lsb + width > datasize)
+                uint64_t resultDataSize = 0;
+                if (!WTF::safeAdd(lsb, width, resultDataSize) || resultDataSize > datasize)
                     return false;
 
                 append(opcode, tmp(srcValue), imm(lsbValue), imm(width), tmp(m_value));
@@ -3390,9 +3391,8 @@ class LowerToAir {
                 uint64_t highWidth = highWidthValue->asInt();
                 uint64_t lowWidth = lowWidthValue->asInt();
                 uint64_t datasize = opcode == ExtractRegister32 ? 32 : 64;
-                // Note that when `lowWidth == datasize` we cannot turn it to `MOV Rd Rn` since
-                // `m >>> lowWidth` means `m >>> (lowWidth % datasize)` in JavaScript.
-                if (lowWidth + highWidth != datasize || maskBitCount != lowWidth || lowWidth == datasize)
+                uint64_t resultWidth = 0;
+                if (!WTF::safeAdd(lowWidth, highWidth, resultWidth) || resultWidth != datasize || maskBitCount != lowWidth || lowWidth == datasize)
                     return false;
 
                 ASSERT(lowWidth < datasize);
@@ -3429,7 +3429,8 @@ class LowerToAir {
                     return false;
                 uint64_t datasize = opcode == InsertBitField32 ? 32 : 64;
                 uint64_t width = WTF::bitCount(mask1);
-                if (lsb + width > datasize)
+                uint64_t resultDataSize = 0;
+                if (!WTF::safeAdd(lsb, width, resultDataSize) || resultDataSize > datasize)
                     return false;
 
                 uint64_t mask2 = maskValue2->asInt();
@@ -3479,7 +3480,8 @@ class LowerToAir {
                     return false;
                 uint64_t width = WTF::bitCount(mask1);
                 uint64_t datasize = opcode == ExtractInsertBitfieldAtLowEnd32 ? 32 : 64;
-                if (lsb + width > datasize)
+                uint64_t resultDataSize = 0;
+                if (!WTF::safeAdd(lsb, width, resultDataSize) || resultDataSize > datasize)
                     return false;
                 uint64_t mask2 = maskValue2->asInt();
 
@@ -3653,7 +3655,8 @@ class LowerToAir {
 
                     uint64_t width = WTF::bitCount(mask);
                     uint64_t datasize = opcode == InsertUnsignedBitfieldInZero32 ? 32 : 64;
-                    if (lsb + width > datasize)
+                    uint64_t resultDataSize = 0;
+                    if (!WTF::safeAdd(lsb, width, resultDataSize) || resultDataSize > datasize)
                         return false;
 
                     append(opcode, tmp(nValue), imm(right), imm(width), tmp(m_value));
@@ -3715,8 +3718,13 @@ class LowerToAir {
                 uint64_t amount2 = amount2Value->asInt();
                 uint64_t lsb = lsbValue->asInt();
                 uint64_t datasize = opcode == InsertSignedBitfieldInZero32 ? 32 : 64;
+
+                if (amount1 >= datasize)
+                    return false;
+
                 uint64_t width = datasize - amount1;
-                if (amount1 != amount2 || !width || lsb + width > datasize)
+                uint64_t resultDataSize = 0;
+                if (!WTF::safeAdd(lsb, width, resultDataSize) || amount1 != amount2 || !width || resultDataSize > datasize)
                     return false;
 
                 append(opcode, tmp(srcValue), imm(lsbValue), imm(width), tmp(m_value));
@@ -3763,8 +3771,13 @@ class LowerToAir {
                 uint64_t amount2 = amount2Value->asInt();
                 uint64_t lsb = lsbValue->asInt();
                 uint64_t datasize = opcode == ExtractSignedBitfield32 ? 32 : 64;
+
+                if (amount1 >= datasize)
Loading diff…

Original Bug Report

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