diff --git a/src/maglev/maglev-graph-builder.cc b/src/maglev/maglev-graph-builder.cc index d6dcea8d4..d6d878af4 100644 --- a/src/maglev/maglev-graph-builder.cc +++ b/src/maglev/maglev-graph-builder.cc @@ -8531,6 +8531,13 @@ MaybeReduceResult MaglevGraphBuilder::TryReduceArrayIteratorPrototypeNext( if (map_inference.has_value()) { RETURN_IF_ABORT(map_inference->InsertMapChecks(zone())); + } else { + // Do not rely solely on the escape state of an inlined allocation here. + // An arbitrary call can expose an active inlined frame through legacy + // Function.prototype.arguments and mutate the iterated array without a + // graph-visible use. BuildCheckMaps folds when the map fact is still fresh + // and emits a check when side effects made it stale. + RETURN_IF_ABORT(BuildCheckMaps(iterated_object, base::VectorOf(maps))); } // Load the [[NextIndex]] from the {iterator}. diff --git a/test/mjsunit/maglev/regress-array-iterator-legacy-arguments.js b/test/mjsunit/maglev/regress-array-iterator-legacy-arguments.js new file mode 100644 index 000000000..d1b777441 --- /dev/null +++ b/test/mjsunit/maglev/regress-array-iterator-legacy-arguments.js @@ -0,0 +1,70 @@ +// 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 + +// Function.prototype.arguments can expose values from an active inlined +// frame without a graph-visible use. ArrayIteratorPrototypeNext must check the +// iterated array's map after such a call before emitting a representation- +// specific element load. + +let tagged_value = 1.1; +function materialize_tagged_arguments() { + const active_arguments = eval('tagged_inner.arguments'); + active_arguments[0][0] = tagged_value; +} +function tagged_inner(slot) { + const array = [1.1, 2.2]; + const iterator = array.values(); + slot = array; + materialize_tagged_arguments(); + return iterator.next().value; +} +function tagged_outer() { + return tagged_inner(0); +} + +let double_value = 1; +function materialize_double_arguments() { + with ({}) { + const active_arguments = double_inner.arguments; + active_arguments[0][0] = double_value; + } +} +const double_reflector = new Proxy(materialize_double_arguments, {}); +function double_inner(slot) { + const array = [1, 2]; + const iterator = array.values(); + slot = array; + double_reflector(); + return iterator.next().value; +} +function double_outer() { + return double_inner(0); +} + +%PrepareFunctionForOptimization(tagged_outer); +%PrepareFunctionForOptimization(double_outer); +for (let i = 0; i < 10; ++i) { + assertEquals(1.1, tagged_outer()); + assertEquals(1, double_outer()); +} + +%OptimizeMaglevOnNextCall(tagged_outer); +%OptimizeMaglevOnNextCall(double_outer); +assertEquals(1.1, tagged_outer()); +assertEquals(1, double_outer()); +assertOptimized(tagged_outer); +assertOptimized(double_outer); + +const marker = {}; +tagged_value = marker; +double_value = 1.5; + +// A stale double load would return the tagged pointer bits as a number. A +// stale tagged load would consume the low word of 1.5 as Smi 0. +const tagged_result = tagged_outer(); +const double_result = double_outer(); +assertSame(marker, tagged_result); +assertEquals(1.5, double_result);