High chrome UAF 🔧 Commit mapped

Overview

High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Turbolev
DescriptionUse after free in Turbolev
ComponentTurbolev
Sub-componentTurbolev
Bug ClassUAF
Tracker567175125
Fix commit2945e2e2966a (v8/v8) +169/-58
CISA KEVNot listed
CreditedOpenAI Codex Security (amyb)
Disclosed2026-10-20

Changed Functions

FunctionChangeNotes
MaglevGraphLabellerScope
src/maglev/maglev-graph-labeller.h
modified
V8_EXPORT_PRIVATE
src/maglev/maglev-graph-labeller.h
modified
DeoptInfo
src/maglev/maglev-ir.h
modified
if
src/maglev/maglev-known-node-aspects.cc
modified
erase_if
src/maglev/maglev-known-node-aspects.cc
modified
for
test/mjsunit/turbolev/loop-peel-effect-epochs.js
modified
MaglevGraphBuilderTest
test/unittests/maglev/maglev-graph-builder-unittest.cc
modified
TEST_F
test/unittests/maglev/maglev-graph-builder-unittest.cc
modified
for
test/unittests/maglev/maglev-graph-builder-unittest.cc
modified

Files Changed

  • src/maglev/maglev-interpreter-frame-state.h
  • src/maglev/maglev-ir.h
  • test/unittests/maglev/maglev-graph-processor-unittest.cc
  • src/maglev/maglev-graph-labeller.h
  • test/unittests/maglev/maglev-graph-builder-unittest.cc
  • src/maglev/maglev-graph.h
  • src/maglev/maglev-known-node-aspects.cc
  • src/maglev/maglev-kna-processor.h
  • src/maglev/maglev-compilation-info.h
  • test/mjsunit/turbolev/loop-peel-effect-epochs.js
  • src/compiler/heap-refs.h
  • src/maglev/maglev-known-node-aspects.h
  • src/maglev/maglev-interpreter-frame-state.cc
From 2945e2e2966a0d94ffbde4940ae2df65041dbc3a Mon Sep 17 00:00:00 2001
From: Jakob Linke <[email protected]>
Date: Tue, 06 Oct 2026 13:38:13 +0200
Subject: [PATCH] [turbolev] Fix expression invalidation at loop headers

MergeForLoop compares effect epochs from graph building and KNA
recomputation. These counters belong to different traversals, so equal
values can incorrectly preserve stale loads across loop effects.

Save the loop header's builder epoch and compare it with the saved
backedge epoch. During recomputation, invalidate epoch-dependent
expressions when that comparison indicates effects; otherwise retain
valid forward expressions. Treat epoch overflow conservatively.

This replaces backedge expression matching with loop-effect filtering.
Add a regression test and unit coverage for both backedge paths,
effectless loops, and stale forward expressions.

Fixed: 567175125
Change-Id: I3bf29885c7c7bd8c67475d1305c2fa8844cfdbed
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/8493491
Reviewed-by: Darius Mercadier <[email protected]>
Commit-Queue: Jakob Linke <[email protected]>
Auto-Submit: Jakob Linke <[email protected]>
Cr-Commit-Position: refs/heads/main@{#110355}
---
diff --git a/src/compiler/heap-refs.h b/src/compiler/heap-refs.h
index e4197ed..ab38af0 100644
--- a/src/compiler/heap-refs.h
+++ b/src/compiler/heap-refs.h
@@ -775,7 +775,7 @@
 
   IndirectHandle<HeapNumber> object() const;
 
-  double value() const;
+  V8_EXPORT_PRIVATE double value() const;
   uint64_t value_as_bits() const;
 };
 
diff --git a/src/maglev/maglev-compilation-info.h b/src/maglev/maglev-compilation-info.h
index 895ce35..b722b7b 100644
--- a/src/maglev/maglev-compilation-info.h
+++ b/src/maglev/maglev-compilation-info.h
@@ -176,7 +176,8 @@
   bool trace_json_enabled() const { return trace_json_enabled_; }
 
   bool has_graph_labeller() const { return !!graph_labeller_; }
-  void set_graph_labeller(MaglevGraphLabeller* graph_labeller);
+  V8_EXPORT_PRIVATE void set_graph_labeller(
+      MaglevGraphLabeller* graph_labeller);
   MaglevGraphLabeller* graph_labeller() const {
     DCHECK(has_graph_labeller());
     return graph_labeller_.get();
diff --git a/src/maglev/maglev-graph-labeller.h b/src/maglev/maglev-graph-labeller.h
index f490981..5eadaa5 100644
--- a/src/maglev/maglev-graph-labeller.h
+++ b/src/maglev/maglev-graph-labeller.h
@@ -103,7 +103,7 @@
   int next_node_label_ = 1;
 };
 
-class MaglevGraphLabellerScope {
+class V8_EXPORT_PRIVATE MaglevGraphLabellerScope {
  public:
   explicit MaglevGraphLabellerScope(MaglevGraphLabeller* graph_labeller);
   ~MaglevGraphLabellerScope();
diff --git a/src/maglev/maglev-graph.h b/src/maglev/maglev-graph.h
index d78d2fa..939d639 100644
--- a/src/maglev/maglev-graph.h
+++ b/src/maglev/maglev-graph.h
@@ -342,7 +342,7 @@
                                    constant.get_bits());
   }
 
-  HeapConstant* GetHeapNumberConstant(double constant);
+  V8_EXPORT_PRIVATE HeapConstant* GetHeapNumberConstant(double constant);
 
   RootConstant* GetRootConstant(RootIndex index) {
     return GetOrAddNewConstantNode(root_constants_, index);
diff --git a/src/maglev/maglev-interpreter-frame-state.cc b/src/maglev/maglev-interpreter-frame-state.cc
index 422f141..fc4fa2e 100644
--- a/src/maglev/maglev-interpreter-frame-state.cc
+++ b/src/maglev/maglev-interpreter-frame-state.cc
@@ -446,6 +446,7 @@
   DCHECK_EQ(predecessors_so_far_, 0);
   known_node_aspects_ = unmerged.known_node_aspects()->CloneForLoopHeader(
       optimistic_initial_state, loop_effects, graph->zone());
+  AsLoopHeader()->InitializeLoopEffectEpoch(*known_node_aspects_);
   unmerged.virtual_objects().Snapshot();
   const MaglevCompilationInfo* info = compilation_unit.info();
   TRACE(TraceColor::kInfo << "Initializing "
diff --git a/src/maglev/maglev-interpreter-frame-state.h b/src/maglev/maglev-interpreter-frame-state.h
index 66fda5f..4e8e458 100644
--- a/src/maglev/maglev-interpreter-frame-state.h
+++ b/src/maglev/maglev-interpreter-frame-state.h
@@ -387,7 +387,7 @@
     clear_is_loop();
   }
 
-  void RemovePredecessorAt(int predecessor_id);
+  V8_EXPORT_PRIVATE void RemovePredecessorAt(int predecessor_id);
 
   // Returns and clears the known node aspects on this state. Expects to only
   // ever be called once, when starting a basic block with this state.
@@ -580,7 +580,7 @@
                         Alternatives::List* per_predecessor_alternatives,
                         bool optimistic_loop_phis = false);
 
-  void ReducePhiPredecessorCount(unsigned num);
+  V8_EXPORT_PRIVATE void ReducePhiPredecessorCount(unsigned num);
 
   void MergeVirtualObjects(Graph* graph, bool is_tracing,
                            const MaglevCompilationUnit& compilation_unit,
@@ -662,6 +662,16 @@
   }
   const LoopEffects* loop_effects() const { return loop_effects_; }
 
+  void InitializeLoopEffectEpoch(const KnownNodeAspects& aspects) {
+    loop_header_effect_epoch_ = aspects.effect_epoch_;
+  }
+  bool loop_has_effects() const {
+    return loop_header_effect_epoch_ ==
+               KnownNodeAspects::kEffectEpochOverflow ||
+           loop_header_effect_epoch_ !=
+               backedge_known_node_aspects()->effect_epoch_;
+  }
+
   DeoptFrame* backedge_deopt_frame() const { return backedge_deopt_frame_; }
 
   KnownNodeAspects* backedge_known_node_aspects() const {
@@ -685,6 +695,7 @@
   const LoopEffects* loop_effects_ = nullptr;
   // The KNA from the backedge (end of the loop).
   KnownNodeAspects* backedge_known_node_aspects_ = nullptr;
+  uint32_t loop_header_effect_epoch_ = KnownNodeAspects::kEffectEpochOverflow;
   // The deopt frame for the backedge, in case we want to insert a deopting
   // conversion during phi untagging. It is set when visiting the JumpLoop.
   DeoptFrame* backedge_deopt_frame_ = nullptr;
@@ -706,7 +717,7 @@
 // These asserts only exist to avoid accidentally bloating the merge states;
 // the sizes can be increased if more fields are actually needed.
 static_assert(sizeof(MergePointInterpreterFrameState) == 104);
-static_assert(sizeof(LoopMergePointInterpreterFrameState) == 128);
+static_assert(sizeof(LoopMergePointInterpreterFrameState) == 136);
 #endif
 
 struct LoopEffects {
diff --git a/src/maglev/maglev-ir.h b/src/maglev/maglev-ir.h
index b46280e..9f6b33f 100644
--- a/src/maglev/maglev-ir.h
+++ b/src/maglev/maglev-ir.h
@@ -1775,8 +1775,8 @@
 
 class DeoptInfo {
  protected:
-  DeoptInfo(Zone* zone, DeoptFrame* top_frame,
-            compiler::FeedbackSource feedback_to_update);
+  V8_EXPORT_PRIVATE DeoptInfo(Zone* zone, DeoptFrame* top_frame,
+                              compiler::FeedbackSource feedback_to_update);
 
  public:
   DeoptFrame& top_frame() { return *top_frame_; }
@@ -2732,7 +2732,7 @@
   // HoleyFloat64 gives a meaning to. Only false if that is provably not the
   // case, so that whoever writes those bits somewhere they would regain that
   // meaning (a double array, mainly) can skip canonicalizing them.
-  bool MayBeHoleOrUndefinedNan() const;
+  V8_EXPORT_PRIVATE bool MayBeHoleOrUndefinedNan() const;
 
 #ifdef V8_COMPRESS_POINTERS
   constexpr bool decompresses_tagged_result() const {
@@ -2795,8 +2795,8 @@
   compiler::OptionalHeapObjectRef TryGetConstant(
       compiler::JSHeapBroker* broker);
 
-  NodeType GetStaticType(compiler::JSHeapBroker* broker);
-  Range GetStaticRange() const;
+  V8_EXPORT_PRIVATE NodeType GetStaticType(compiler::JSHeapBroker* broker);
+  V8_EXPORT_PRIVATE Range GetStaticRange() const;
 
   bool StaticTypeIs(compiler::JSHeapBroker* broker, NodeType type) {
     return NodeTypeIs(GetStaticType(broker), type);
@@ -10205,8 +10205,8 @@
   // Records a use hint for this Phi. If {force_same_loop} is true, the hint
   // is recorded as a same-loop use, which is important for loop-related
   // optimizations like Phi untagging.
-  void RecordUseReprHint(UseRepresentationSet repr_mask,
-                         bool force_same_loop = false);
+  V8_EXPORT_PRIVATE void RecordUseReprHint(UseRepresentationSet repr_mask,
+                                           bool force_same_loop = false);
 
   UseRepresentationSet use_repr_hints() { return use_repr_hints_; }
   UseRepresentationSet same_loop_use_repr_hints() {
diff --git a/src/maglev/maglev-kna-processor.h b/src/maglev/maglev-kna-processor.h
index bf3c66c..a94b426 100644
--- a/src/maglev/maglev-kna-processor.h
+++ b/src/maglev/maglev-kna-processor.h
@@ -109,7 +109,8 @@
       backedge_known_node_aspects->UnwrapIdentitiesAndPhisInKeys(zone());
       known_node_aspects_->MergeForLoop(
           *backedge_known_node_aspects, zone(),
-          block->state()->AsLoopHeader()->loop_effects());
+          block->state()->AsLoopHeader()->loop_effects(),
+          block->state()->AsLoopHeader()->loop_has_effects());
     } else if (block->has_state()) {
       known_node_aspects_ = block->state()->TakeKnownNodeAspects();
     } else if (block->is_edge_split_block()) {
diff --git a/src/maglev/maglev-known-node-aspects.cc b/src/maglev/maglev-known-node-aspects.cc
index 11a86dc..4b59e64 100644
--- a/src/maglev/maglev-known-node-aspects.cc
+++ b/src/maglev/maglev-known-node-aspects.cc
@@ -393,8 +393,8 @@
 // loop-invariant values the peeler cloned), invalidating only what the loop
 // body can change.
 void KnownNodeAspects::MergeForLoop(const KnownNodeAspects& backedge,
-                                    Zone* zone,
-                                    const LoopEffects* loop_effects) {
+                                    Zone* zone, const LoopEffects* loop_effects,
+                                    bool loop_has_effects) {
   if (side_effects_require_invalidation_ &&
       (loop_effects == nullptr || loop_effects->unstable_aspects_cleared ||
        loop_effects->elements_kind_transitioned)) {
@@ -407,45 +407,23 @@
     side_effects_require_invalidation_ = false;
   }
 
-  if (effect_epoch_ != backedge.effect_epoch_) {
-    effect_epoch_ = std::max(effect_epoch_, backedge.effect_epoch_) + 1;
-  }
-  // Forward entries for pure (epoch-exempt) expressions are kept even without
-  // a backedge equivalent: the forward predecessor dominates the loop body and
-  // loop effects cannot invalidate a pure expression. Other entries need a
-  // structurally equal backedge entry with a still-valid epoch.
-  for (auto it = available_expressions_.begin();
-       it != available_expressions_.end();) {
-    const AvailableExpression& lhs = it->second;
-    DCHECK_NE(lhs.effect_epoch, kEffectEpochOverflow);
-    DCHECK_IMPLIES(!lhs.node->Is<Identity>(),
-                   Node::needs_epoch_check(lhs.node->opcode()) ==
-                       (lhs.effect_epoch != kEffectEpochForPureInstructions));
-    if (lhs.node->Is<Identity>()) {
-      it = available_expressions_.erase(it);
-      continue;
-    }
-    if (lhs.effect_epoch == kEffectEpochForPureInstructions) {
-      ++it;
-      continue;
-    }
-    bool keep = false;
-    auto rhs_it = backedge.available_expressions_.find(it->first);
-    if (rhs_it != backedge.available_expressions_.end()) {
-      const AvailableExpression& rhs = rhs_it->second;
-      DCHECK_IMPLIES(lhs.node == rhs.node,
-                     lhs.effect_epoch == rhs.effect_epoch);
-      ValueNode* rhs_value = rhs.node->TryCast<ValueNode>();
-      NodeBase* rhs_node = rhs_value ? rhs_value->UnwrapIdentities() : rhs.node;
-      keep = lhs.node->IsStructurallyEqualTo(rhs_node) &&
-             lhs.effect_epoch >= effect_epoch_;
-    }
-    if (keep) {
-      ++it;
-    } else {
-      it = available_expressions_.erase(it);
-    }
-  }
+  // TODO(jgruber): Recompute loop effects after lazy inlining, which can
+  // replace effectful calls with pure operations.
+  if (loop_has_effects) increment_effect_epoch();
+
+  std::erase_if(available_expressions_, [&](const auto& entry) {
+    const AvailableExpression& expr = entry.second;
+    DCHECK_NE(expr.effect_epoch, kEffectEpochOverflow);
+    DCHECK_IMPLIES(!expr.node->Is<Identity>(),
+                   Node::needs_epoch_check(expr.node->opcode()) ==
+                       (expr.effect_epoch != kEffectEpochForPureInstructions));
+    if (expr.node->Is<Identity>()) return true;
+    if (expr.effect_epoch == kEffectEpochForPureInstructions) return false;
+    // The graph builder's backedge epoch is not comparable with the recomputed
+    // forward epoch. loop_has_effects was determined during graph building.
+    if (loop_has_effects) return true;
+    return expr.effect_epoch < effect_epoch_;
+  });
 
   const bool keep_invariant_loads =
       loop_effects != nullptr && !loop_effects->unstable_aspects_cleared;
diff --git a/src/maglev/maglev-known-node-aspects.h b/src/maglev/maglev-known-node-aspects.h
index 3534466..b7f0391 100644
--- a/src/maglev/maglev-known-node-aspects.h
+++ b/src/maglev/maglev-known-node-aspects.h
@@ -563,8 +563,10 @@
   }
 
   void Merge(const KnownNodeAspects& other, Zone* zone);
-  void MergeForLoop(const KnownNodeAspects& backedge, Zone* zone,
-                    const LoopEffects* loop_effects);
+  V8_EXPORT_PRIVATE void MergeForLoop(const KnownNodeAspects& backedge,
+                                      Zone* zone,
+                                      const LoopEffects* loop_effects,
+                                      bool loop_has_effects);
 
   // If IsCompatibleWithLoopHeader(other) returns true, it means that
   // Merge(other) would not remove any information from `this`.
diff --git a/test/mjsunit/turbolev/loop-peel-effect-epochs.js b/test/mjsunit/turbolev/loop-peel-effect-epochs.js
new file mode 100644
index 0000000..708ef92
--- /dev/null
+++ b/test/mjsunit/turbolev/loop-peel-effect-epochs.js
@@ -0,0 +1,37 @@
+// 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 --turbolev --turbofan
+
+let shrink = false;
+function callback(array, iteration) {
+  if (shrink && iteration === 1) array.length = 2;
+}
+%NeverOptimizeFunction(callback);
+
+function sum(array, n, callback) {
+  const at = Array.prototype.at;
+  let result = 0;
+  // This loop aligns the effect epochs at the second loop header.
+  for (let i = 0; i < 3; ++i) {
+    callback(null, 9);
+    callback(null, 9);
+  }
+  for (let i = 0; i < n; ++i) {
+    if (i === 0) result += array.length;
+    result += at.call(array, 3);
+    callback(array, i);
+    result += at.call(array, 0);
+    result += array[0];
+  }
+  return result;
+}
+
+const array = [11, 12, 13, 14];
+%PrepareFunctionForOptimization(sum);
+assertEquals(148, sum(array, 4, callback));
+assertEquals(148, sum(array, 4, callback));
+%OptimizeFunctionOnNextCall(sum);
+shrink = true;
+assertTrue(Number.isNaN(sum(array, 4, callback)));
diff --git a/test/unittests/maglev/maglev-graph-builder-unittest.cc b/test/unittests/maglev/maglev-graph-builder-unittest.cc
index 23a9925..caeb4f9 100644
--- a/test/unittests/maglev/maglev-graph-builder-unittest.cc
+++ b/test/unittests/maglev/maglev-graph-builder-unittest.cc
@@ -8,8 +8,10 @@
 
 #include "src/maglev/maglev-compilation-info.h"
 #include "src/maglev/maglev-compiler.h"
+#include "src/maglev/maglev-graph-labeller.h"
 #include "src/maglev/maglev-ir-inl.h"
 #include "src/maglev/maglev-ir.h"
+#include "test/common/flag-utils.h"
 #include "test/unittests/maglev/maglev-test.h"
 
 namespace v8 {
@@ -18,6 +20,55 @@
 
 class MaglevGraphBuilderTest : public TestWithNativeContextAndZone {};
 
+TEST_F(MaglevGraphBuilderTest, LoopEffectEpochComparison) {
+  FlagScope<bool> allow_natives_syntax(&v8_flags.allow_natives_syntax, true);
+  FlagScope<bool> non_eager_inlining(&v8_flags.maglev_non_eager_inlining, true);
+  FlagScope<bool> optimistic_loops(&v8_flags.maglev_optimistic_peeled_loops,
+                                   true);
+  for (bool peeling : {false, true}) {
+    FlagScope<bool> loop_peeling(&v8_flags.maglev_loop_peeling, peeling);
+    for (bool has_effects : {false, true}) {
+      HandleScope scope(isolate());
+      std::string script = R"(
+        function f(object, n) {
+          let result = 0;
+          for (let i = 0; i < n; ++i) {
+            result += i;
+      )";
+      if (has_effects) script += "object.x = i;";
+      script += R"(
+          }
+          return result;
+        }
+        %PrepareFunctionForOptimization(f);
+        f({x: 0}, 10);
+        (f)
+      )";
+      Handle<JSFunction> function = RunJS<JSFunction>(script.c_str());
+      auto info = MaglevCompilationInfo::New(isolate(), function,
+                                             BytecodeOffset::None());
+      info->set_graph_labeller(new MaglevGraphLabeller());
+      MaglevGraphLabellerScope graph_labeller_scope(info->graph_labeller());
+      Graph* graph = Graph::New(info.get());
+      compiler::CurrentHeapBrokerScope current_broker(info->broker());
+      MaglevGraphBuilder graph_builder(isolate()->AsLocalIsolate(),
+                                       info->toplevel_compilation_unit(),
+                                       graph);
+      PersistentHandlesScope persistent_scope(isolate());
+      ASSERT_TRUE(graph_builder.Build());
... (file diff truncated)
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/test/mjsunit/turbolev/loop-peel-effect-epochs.js b/test/mjsunit/turbolev/loop-peel-effect-epochs.js
new file mode 100644
index 0000000..708ef92
--- /dev/null
+++ b/test/mjsunit/turbolev/loop-peel-effect-epochs.js
@@ -0,0 +1,37 @@
+// 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 --turbolev --turbofan
+
+let shrink = false;
+function callback(array, iteration) {
+  if (shrink && iteration === 1) array.length = 2;
+}
+%NeverOptimizeFunction(callback);
+
+function sum(array, n, callback) {
+  const at = Array.prototype.at;
+  let result = 0;
+  // This loop aligns the effect epochs at the second loop header.
+  for (let i = 0; i < 3; ++i) {
+    callback(null, 9);
+    callback(null, 9);
+  }
+  for (let i = 0; i < n; ++i) {
+    if (i === 0) result += array.length;
+    result += at.call(array, 3);
+    callback(array, i);
+    result += at.call(array, 0);
+    result += array[0];
+  }
+  return result;
+}
+
+const array = [11, 12, 13, 14];
+%PrepareFunctionForOptimization(sum);
+assertEquals(148, sum(array, 4, callback));
+assertEquals(148, sum(array, 4, callback));
+%OptimizeFunctionOnNextCall(sum);
+shrink = true;
+assertTrue(Number.isNaN(sum(array, 4, callback)));
diff --git a/test/unittests/maglev/maglev-graph-builder-unittest.cc b/test/unittests/maglev/maglev-graph-builder-unittest.cc
index 23a9925..caeb4f9 100644
--- a/test/unittests/maglev/maglev-graph-builder-unittest.cc
+++ b/test/unittests/maglev/maglev-graph-builder-unittest.cc
@@ -8,8 +8,10 @@
 
 #include "src/maglev/maglev-compilation-info.h"
 #include "src/maglev/maglev-compiler.h"
+#include "src/maglev/maglev-graph-labeller.h"
 #include "src/maglev/maglev-ir-inl.h"
 #include "src/maglev/maglev-ir.h"
+#include "test/common/flag-utils.h"
 #include "test/unittests/maglev/maglev-test.h"
 
 namespace v8 {
@@ -18,6 +20,55 @@
 
 class MaglevGraphBuilderTest : public TestWithNativeContextAndZone {};
 
+TEST_F(MaglevGraphBuilderTest, LoopEffectEpochComparison) {
+  FlagScope<bool> allow_natives_syntax(&v8_flags.allow_natives_syntax, true);
+  FlagScope<bool> non_eager_inlining(&v8_flags.maglev_non_eager_inlining, true);
+  FlagScope<bool> optimistic_loops(&v8_flags.maglev_optimistic_peeled_loops,
+                                   true);
+  for (bool peeling : {false, true}) {
+    FlagScope<bool> loop_peeling(&v8_flags.maglev_loop_peeling, peeling);
+    for (bool has_effects : {false, true}) {
+      HandleScope scope(isolate());
+      std::string script = R"(
+        function f(object, n) {
+          let result = 0;
+          for (let i = 0; i < n; ++i) {
+            result += i;
+      )";
+      if (has_effects) script += "object.x = i;";
+      script += R"(
+          }
+          return result;
+        }
+        %PrepareFunctionForOptimization(f);
+        f({x: 0}, 10);
+        (f)
+      )";
+      Handle<JSFunction> function = RunJS<JSFunction>(script.c_str());
+      auto info = MaglevCompilationInfo::New(isolate(), function,
+                                             BytecodeOffset::None());
+      info->set_graph_labeller(new MaglevGraphLabeller());
+      MaglevGraphLabellerScope graph_labeller_scope(info->graph_labeller());
+      Graph* graph = Graph::New(info.get());
+      compiler::CurrentHeapBrokerScope current_broker(info->broker());
+      MaglevGraphBuilder graph_builder(isolate()->AsLocalIsolate(),
+                                       info->toplevel_compilation_unit(),
+                                       graph);
+      PersistentHandlesScope persistent_scope(isolate());
+      ASSERT_TRUE(graph_builder.Build());
+      int loop_count = 0;
+      for (BasicBlock* block : graph->blocks()) {
+        if (!block->is_loop()) continue;
+        ++loop_count;
+        EXPECT_EQ(has_effects,
+                  block->state()->AsLoopHeader()->loop_has_effects());
+      }
+      EXPECT_EQ(1, loop_count);
+      persistent_scope.Detach();
+    }
+  }
+}
+
 template <typename T>
 static T* getUniqueNode(Graph* graph) {
   T* target = nullptr;
diff --git a/test/unittests/maglev/maglev-graph-processor-unittest.cc b/test/unittests/maglev/maglev-graph-processor-unittest.cc
index d552380..df1ad514 100644
--- a/test/unittests/maglev/maglev-graph-processor-unittest.cc
+++ b/test/unittests/maglev/maglev-graph-processor-unittest.cc
@@ -159,6 +159,35 @@
   persistent_scope.Detach();
 }
 
+TEST_F(MaglevGraphProcessorTest, LoopHeaderAvailableExpressions) {
+  RunSubgraphTest([&](MaglevReducer<MaglevGraphOptimizer>& reducer,
+                      BasicBlock* block) {
+    ValueNode* object = reducer.GetRootConstant(RootIndex::kUndefinedValue);
+    auto* load =
+        reducer.AddNewNodeNoInputConversion<LoadDataViewByteLength>({object});
+    ValueNode* value = reducer.GetFloat64Constant(1.0);
+    auto* pure =
+        reducer.AddNewNodeNoInputConversion<Float64Add>({value, value});
+    std::array<ValueNode*, 1> load_inputs{object};
+    std::array<ValueNode*, 2> pure_inputs{value, value};
+    KnownNodeAspects backedge(zone());
+    for (bool loop_has_effects : {false, true}) {
+      for (bool stale : {false, true}) {
+        KnownNodeAspects forward(zone());
+        forward.AddExpression(1, load);
+        forward.AddExpression(2, pure);
+        if (stale) forward.increment_effect_epoch();
+        forward.MergeForLoop(backedge, zone(), nullptr, loop_has_effects);
+        EXPECT_EQ(
+            !loop_has_effects && !stale ? load : nullptr,
+            forward.FindExpression<LoadDataViewByteLength>(1, load_inputs));
+        EXPECT_EQ(pure, forward.FindExpression<Float64Add>(2, pure_inputs));
+      }
+    }
+    reducer.FlushNodesToBlock();
+  });
+}
+
 TEST_F(MaglevGraphProcessorTest, SubgraphBufferStashingNodesFromBefore) {
   RunSubgraphTest(
       [](MaglevReducer<MaglevGraphOptimizer>& reducer, BasicBlock* block) {
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.