CVE-2024-27833
Overview
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.
Attack Path
- Reach the B3/FTL JIT Run JS whose arithmetic (shifts/masks) the compiler lowers to an ARM64 bitfield instruction via B3LowerToAir.
- Craft the bit range Arrange constant-folded lsb/width (or amount1) so lsb + width overflows uint64 (or datasize - amount1 underflows).
- Bypass the range check The wrapped value passes the ‘> datasize’ guard, so the lowering emits a bitfield instruction with an out-of-range field.
- Corrupt memory The miscompiled instruction reads/writes bits outside the intended range, a memory-corruption primitive in WebContent toward code execution.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
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.jsSource/JavaScriptCore/b3/B3LowerToAir.cpp
Audit Directions
- Same file: remaining range mathAudit B3LowerToAir for other lsb/width/shift computations feeding imm() operands that use raw addition/subtraction without safeAdd/underflow guards.
- Constant-folded operandsGrep the JIT for asInt()-derived widths/offsets used in emitted instruction encodings without overflow validation.
Patch
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)