Released May 13, 2024

CVE-2024-27833

CVSS 8.8 severity high CWE-190 vector CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H

Overview

Impact:

Description: Processing maliciously crafted web content may lead to arbitrary code execution

Researchers: Manfred Paul (@_manfp) working with Trend Micro Zero Day Initiative

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.

Patch Details

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)

Background

Advisory: Apple Security Advisory

Attribute Value
CVE CVE-2024-27833
Bugzilla 271491
Component JSC
Bug Class IntegerOverflow
Severity critical
Commit 1ea4ef8127276fd0…
Advisory Apple Advisory

Terminology

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

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.

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.

Changed Functions

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

Source Files

  • Source/JavaScriptCore/b3/B3LowerToAir.cpp

Test Files

  • JSTests/stress/sbfx-offset-overflow.js

Regression Test

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

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