CVE-2025-12433
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
V8_NODISCARDsrc/interpreter/bytecode-generator.cc |
modified | |
merge_into_bitmap_src/interpreter/bytecode-generator.cc |
modified | |
BytecodeGeneratorsrc/interpreter/bytecode-generator.cc |
modified | |
control_builder_src/interpreter/bytecode-generator.cc |
modified | |
merge_elider_src/interpreter/bytecode-generator.cc |
modified | |
switchsrc/interpreter/bytecode-generator.cc |
modified |
Files Changed
src/interpreter/bytecode-generator.cc
Patch
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. -//
Regression Test / PoC
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),
Original Bug Report
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