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
Sub-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.

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/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 @@ private:
                     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 @@ private:
                 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 @@ private:
                     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 @@ private:
                     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 @@ private:
 
                     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 @@ private:
                 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 @@ private:
                 uint64_t amount2 = amount2Value->asInt();
                 uint64_t lsb = lsbValue->asInt();
                 uint64_t datasize = opcode == ExtractSignedBitfield32 ? 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));
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
Loading diff…

Regression Test / PoC

shipped with the fix
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
Loading diff…

Original Bug Report

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