CVE-2025-6554
Overview
Background
- Optional chaining (`?.`)
- a JS operator that short-circuits an expression, jumping past the remaining links to yield
undefinedwhen a link evaluates tonull/undefined. - Hole check
- a runtime guard the interpreter emits before reading a
let/constbinding, verifying it is not the internalthe_holesentinel (i.e., not in its Temporal Dead Zone). - `HoleCheckElisionScope`
- a
bytecode-generatorscope that suppresses redundant hole checks, valid only where an earlier check provably dominates the later access. - Type confusion
- treating a value of one representation as another; here the
the_holesentinel leaking into JS-visible values, which downstream code assumes can never occur.
Root Cause Analysis
Verify before building a PoC
What the commit does not establish: Diff+title confirm hole-check elision was wrongly scoped over the whole optional chain (the_hole leak -> type confusion). But analysis states the 'earlier links dominate later, linearly' premise 'is false'; the fix retains that exact comment, so that sub-claim is unsupported.
BytecodeGenerator::BuildOptionalChain created a single HoleCheckElisionScope elider spanning the entire optional-chain expression, on the premise that “links earlier in the chain dominate later links, linearly.” That premise is false: an optional chain short-circuits, so when an early link is nullish, control jumps to the null label (label_scope) and skips the intervening code — meaning a hole check emitted in an earlier link does not actually dominate accesses in later links. Because the elision scope covered the whole chain regardless of these short-circuit edges, a hole check for a TDZ (let/const) binding accessed inside the chain could be wrongly elided. The binding could then be read while still holding the the_hole sentinel without any guard firing, exposing that internal value to JavaScript.
The fix removes the chain-wide elider from BuildOptionalChain and instead makes HoleCheckElisionScope hole_check_scope_ a member of OptionalChainNullLabelScope, so the elision region is entered and torn down in lockstep with the null-label scope. This binds hole-check elision to exactly the control-flow region the null labels model, preventing elision from crossing the short-circuit boundaries of the chain.
the_hole sentinel as a real value.Attack Path
- Craft a chain over a TDZ binding
Construct an optional chain that references a
let/constbinding whose hole check the generator will consider elidable across links. - Force short-circuit control flow Arrange the chain so a link is nullish, causing a jump to the null label that bypasses the access the earlier (elided) check was assumed to dominate.
- Read the hole
The access executes without a hole check and returns the
the_holesentinel instead of throwing aReferenceError. - Trigger type confusion
Feed the leaked
the_holevalue into operations that assume a well-typed object, corrupting engine invariants. - Escalate Leverage the confusion for out-of-bounds/arbitrary read-write and ultimately code execution in the renderer.
Impact Assessment
the_hole sentinel, undermining type safety and enabling memory corruption leading to remote code execution in the renderer sandbox. It was exploited in the wild as a 0-day (CVE-2025-6554, high severity), so a single malicious web page could compromise a visiting browser. The bug requires no special privileges beyond loading crafted script.Changed Functions
| Function | Change | Notes |
|---|---|---|
hole_check_scope_src/interpreter/bytecode-generator.cc |
modified |
Files Changed
src/interpreter/bytecode-generator.cc
Audit Directions
- Elision-scope vs. control-flow mismatchAudit every
HoleCheckElisionScope(and analogous elision optimizations) for regions where short-circuit jumps,break/continue, exception edges, or label binds break the assumed domination relationship. - Optional-chain and short-circuit constructsReview
BuildOptionalChain, logical&&/||/??, and conditional expressions that mix jump-to-label semantics with TDZ (let/const) accesses for elided hole checks. - Sentinel leakageSearch for paths where
the_hole(or other internal sentinels) can reach JS-visible values because a guard was optimized away, and verify a hole check remains on every non-dominated access.
Patch
From 22e9d9621de58ec6fe6581b56215059a48451b9f Mon Sep 17 00:00:00 2001
From: Stephen Roettger <sroettger@google.com>
Date: Thu, 26 Jun 2025 10:33:16 +0200
Subject: [PATCH] [interpreter] don't elide hole checks across optional chain
Bug: 427663123
Change-Id: Iefdb15828d807bf9452b88e918a4b46cc2d422fa
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/6678591
Commit-Queue: Stephen Röttger <sroettger@google.com>
Reviewed-by: Toon Verwaest <verwaest@chromium.org>
Cr-Commit-Position: refs/heads/main@{#101050}
---
diff --git a/src/interpreter/bytecode-generator.cc b/src/interpreter/bytecode-generator.cc
index 921a8d0..1b8966c9 100644
--- a/src/interpreter/bytecode-generator.cc
+++ b/src/interpreter/bytecode-generator.cc
@@ -1221,7 +1221,8 @@
public:
explicit OptionalChainNullLabelScope(BytecodeGenerator* bytecode_generator)
: bytecode_generator_(bytecode_generator),
- labels_(bytecode_generator->zone()) {
+ labels_(bytecode_generator->zone()),
+ hole_check_scope_(bytecode_generator) {
prev_ = bytecode_generator_->optional_chaining_null_labels_;
bytecode_generator_->optional_chaining_null_labels_ = &labels_;
}
@@ -1236,6 +1237,9 @@
BytecodeGenerator* bytecode_generator_;
BytecodeLabels labels_;
BytecodeLabels* prev_;
+ // Use the same scope for the entire optional chain, as links earlier in the
+ // chain dominate later links, linearly.
+ HoleCheckElisionScope hole_check_scope_;
};
// LoopScope delimits the scope of {loop}, from its header to its final jump.
@@ -6483,9 +6487,6 @@
void BytecodeGenerator::BuildOptionalChain(ExpressionFunc expression_func) {
BytecodeLabel done;
OptionalChainNullLabelScope label_scope(this);
- // Use the same scope for the entire optional chain, as links earlier in the
- // chain dominate later links, linearly.
- HoleCheckElisionScope elider(this);
expression_func();
builder()->Jump(&done);
label_scope.labels()->Bind(builder());
Original Bug Report
[0-day] delete construct leaking TheHole
NOTE: We have evidence that the following bug is being used in the wild. Therefore, this bug is subject to a 7 day disclosure deadline
VULNERABILITY DETAILS
Context shared with saelo@ and tsuro@. The following construct is leaking the_hole value which, combined with a typer bug can lead to arbitrary r/w within the v8 heap sandbox.
delete {}.R?.O[{}.a = f];
%DebugPrint(f);
let f = {};
VERSION Chrome Version: HEAD + stable Operating System: all
REPRODUCTION CASE
The attached p.js triggers a crash in PushStackTraceAndDie. Samuel and Stephen are working on a reproducer triggering a memory corruption.
CREDIT INFORMATION Reporter credit: Clement Lecigne of Google’s Threat Analysis Group