Overview

High
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactOut of bounds memory access in V8
DescriptionOut of bounds memory access in V8
ComponentV8
Sub-componentInterpreter
Bug ClassOOB
Action tierpatch-now — corruption primitive, web-reachable in default config (confirmed) — upheld by independent review by AI
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Independent review by AIConfirmed
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker374627491
Fix commit5c3b50c26c50 (v8/v8) +39/-6
CWECWE-122 (Heap buffer overflow)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
Creditedddme
Reported2024-10-21 · bounty $7000 (per vendor advisory)
Fix landed2024-11-19 (public git)
Disclosed2025-01-14
Patch gap56 days — fix public in source before the advisory

Background

Temporal Dead Zone (TDZ)
the region in which a let/const binding, including this in a derived-class constructor before super(), still holds the hole sentinel so any read must be guarded by a hole check that throws ReferenceError.
`HoleCheckElisionScope`
an RAII scope in bytecode-generator.cc that marks a region where a binding is provably initialized so the generator may omit otherwise-redundant hole checks.
Switch jump table
a jump_table dispatch structure that maps small-integer discriminants straight to case bodies, leaving any non-integer case labels to be tested afterward by conditionally executed comparison jumps.
Comparison jump
a bytecode sequence emitted per remaining case label that evaluates the label with VisitForAccumulatorValue and performs switch_builder.JumpToCaseIfTrue, counted by n_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.

Key insight
The single mistake was using 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

  1. Craft a mixed switch In a derived-class constructor, before super() runs, write a switch whose small-integer case 0..9 labels form a jump_table and whose final case this: requires a comparison jump.
  2. Force the jump-table path Because the integer cases populate jump_table, the case this: label is compiled as the first comparison jump, which is only reachable when the discriminant misses the table and is therefore conditionally executed.
  3. Trigger the missing scope With the buggy first_jump_emitted logic no HoleCheckElisionScope is opened for that first conditional comparison, so the generator elides the hole check on this, which is still the TDZ hole.
  4. Bypass the ReferenceError Evaluating case this: reads the uninitialized this binding without throwing, leaking the hole sentinel into the interpreter as if it were a valid object value.
  5. 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.js instead asserts the corrected behavior, a thrown ReferenceError).

Impact Assessment

An attacker running attacker-controlled JavaScript in the V8/renderer process gains an out-of-bounds memory access by leaking the hole sentinel past an elided TDZ check, a primitive that can be built into type confusion and memory corruption. The precondition is only that the script define a derived-class constructor with a 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

FunctionChangeNotes
if
src/interpreter/bytecode-generator.cc
modified
B
test/mjsunit/regress/regress-374627491.js
modified
C
test/mjsunit/regress/regress-374627491.js
modified
constructor
test/mjsunit/regress/regress-374627491.js
modified
switch
test/mjsunit/regress/regress-374627491.js
modified

Files Changed

  • src/interpreter/bytecode-generator.cc
  • test/mjsunit/regress/regress-374627491.js

Audit Directions

  • Conditional dominance assumptions
    Review every HoleCheckElisionScope placement that keys off “first iteration” or “unconditionally executed” flags, since a preceding jump_table or other early-exit dispatch can make the supposedly-dominating branch conditional.
  • Jump-table plus comparison mixes
    Audit other bytecode-generator.cc control-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/subexpressions
    Look for places where let/const or this bindings are read inside case labels, default expressions, or other conditionally emitted code, verifying that hole checks are not elided across a non-dominating boundary.
From 5c3b50c26c50e68dbedf8ff991249e75e46ef06e Mon Sep 17 00:00:00 2001
From: Shu-yu Guo <syg@chromium.org>
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 <leszeks@chromium.org>
Commit-Queue: Shu-yu Guo <syg@chromium.org>
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);
Loading diff…

Regression Test / PoC

shipped with the fix
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);
Loading diff…

Original Bug Report

reported by dd...@gmail.com

DCHECK failure in Handle<To> v8::internal::Cast(Handle<From>, const v8::SourceLocation &) [To = v8

Steps to reproduce the problem

  1. download d8-asan-linux-debug-v8-component-96641
  2. unzip that
  3. 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

View on issue tracker
Uploaded PoC files