Chrome · V8
CVE-2026-17948
Type Confusion in V8
Overview
Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
Btest/inspector/debugger/set-variable-value-class-synthetic.js |
modified | |
Ctest/inspector/debugger/set-variable-value-class-synthetic.js |
modified | |
footest/inspector/debugger/set-variable-value-class-synthetic.js |
modified | |
constructortest/inspector/debugger/set-variable-value-class-synthetic.js |
modified | |
runtest/inspector/debugger/set-variable-value-class-synthetic.js |
modified | |
fortest/inspector/debugger/set-variable-value-class-synthetic.js |
modified |
Files Changed
src/debug/debug-scopes.cctest/inspector/debugger/set-variable-value-class-synthetic-expected.txttest/inspector/debugger/set-variable-value-class-synthetic.js
Patch
From bee4f84e3b1258e903e26236ca0260f7bfbc2699 Mon Sep 17 00:00:00 2001
From: Simon Zünd <szuend@chromium.org>
Date: Tue, 23 Jun 2026 11:12:29 +0000
Subject: [PATCH] [debug] Disallow setVariableValue on synthetic context slots
Synthetic, compiler-introduced variables (e.g. ".brand",
".generator_object", "#method") are filtered from the locals that
ScopeIterator exposes to the debugger frontend, but the write side only
filtered private method/accessor modes. The class ".brand" slot has
VariableMode::kConst, so a Debugger.setVariableValue request naming
".brand" would overwrite the slot and the next constructor invocation
would use the bogus value as a property key when stamping the private
brand on the new instance.
Reject all synthetic variable names at the
ScopeIterator::SetVariableValue entry point so the read and write sides
agree, and drop the now-redundant per-mode check in
SetContextVariableValue.
R=pfaffe@chromium.org
Fixed: 516849257
Change-Id: Id1c7352b70a26550b0853806f863b35eebace900
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/7984385
Reviewed-by: Philip Pfaffe <pfaffe@chromium.org>
Commit-Queue: Simon Zünd <szuend@chromium.org>
Cr-Commit-Position: refs/heads/main@{#108170}
---
diff --git a/src/debug/debug-scopes.cc b/src/debug/debug-scopes.cc
index 5c2fe8b5..4624daf 100644
--- a/src/debug/debug-scopes.cc
+++ b/src/debug/debug-scopes.cc
@@ -696,6 +696,9 @@
DirectHandle<Object> value) {
DCHECK(!Done());
name = isolate_->factory()->InternalizeString(name);
+ // Synthetic variables are compiler-introduced and not exposed to the user, so
+ // they may carry values outside the JSAny type and must not be overwritten.
+ if (ScopeInfo::VariableIsSynthetic(*name)) return false;
switch (Type()) {
case ScopeTypeGlobal:
case ScopeTypeWith:
@@ -1216,13 +1219,8 @@
bool ScopeIterator::SetContextVariableValue(DirectHandle<String> variable_name,
DirectHandle<Object> new_value) {
- VariableLookupResult lookup_result;
- int slot_index =
- context_->scope_info()->ContextSlotIndex(*variable_name, &lookup_result);
+ int slot_index = context_->scope_info()->ContextSlotIndex(*variable_name);
if (slot_index < 0) return false;
- if (IsPrivateMethodOrAccessorVariableMode(lookup_result.mode)) {
- return false;
- }
Context::Set(context_, slot_index, new_value, isolate_);
return true;
}
diff --git a/test/inspector/debugger/set-variable-value-class-synthetic-expected.txt b/test/inspector/debugger/set-variable-value-class-synthetic-expected.txt
new file mode 100644
index 0000000..0b7135d
--- /dev/null
+++ b/test/inspector/debugger/set-variable-value-class-synthetic-expected.txt
@@ -0,0 +1,31 @@
+Checks that Debugger.setVariableValue cannot overwrite synthetic class context slots
+Class block scope found: true
+Setting .brand to "foo"
+{
+ error : {
+ code : -32603
+ message : Internal error
+ }
+ id : <messageId>
+}
+Own property names of inst:
+{
+ id : <messageId>
+ result : {
+ result : {
+ type : string
+ value : []
+ }
+ }
+}
+inst.run():
+{
+ id : <messageId>
+ result : {
+ result : {
+ description : 42
+ type : number
+ value : 42
+ }
+ }
+}
diff --git a/test/inspector/debugger/set-variable-value-class-synthetic.js b/test/inspector/debugger/set-variable-value-class-synthetic.js
new file mode 100644
index 0000000..ddf8c9f
--- /dev/null
+++ b/test/inspector/debugger/set-variable-value-class-synthetic.js
@@ -0,0 +1,59 @@
+// 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.
+
+const {session, contextGroup, Protocol} = InspectorTest.start(
+ 'Checks that Debugger.setVariableValue cannot overwrite synthetic class context slots');
+
+const source = `
+class B {}
+class C extends B {
+ #m() { return 42; }
+ foo() { return C; }
+ constructor() { debugger; super(); }
+ run() { return this.#m(); }
+}
+globalThis.inst = new C();
+globalThis.inst.run();
+`;
+
+(async () => {
+ await Protocol.Debugger.enable();
+
+ const evalPromise = Protocol.Runtime.evaluate({expression: source});
+ const {params: {callFrames}} = await Protocol.Debugger.oncePaused();
+ const {callFrameId, scopeChain} = callFrames[0];
+
+ let scopeNumber = -1;
+ for (let i = 0; i < scopeChain.length; ++i) {
+ if (scopeChain[i].type !== 'block') continue;
+ const {result: {result}} = await Protocol.Runtime.getProperties(
+ {objectId: scopeChain[i].object.objectId});
+ if (result.some(p => p.name === 'C')) {
+ scopeNumber = i;
+ break;
+ }
+ }
+ InspectorTest.log('Class block scope found: ' + (scopeNumber >= 0));
+
+ InspectorTest.log('Setting .brand to "foo"');
+ InspectorTest.logMessage(await Protocol.Debugger.setVariableValue({
+ scopeNumber,
+ variableName: '.brand',
+ newValue: {value: 'foo'},
+ callFrameId,
+ }));
+
+ await Protocol.Debugger.resume();
+ await evalPromise;
+
+ InspectorTest.log('Own property names of inst:');
+ InspectorTest.logMessage(await Protocol.Runtime.evaluate(
+ {expression: 'JSON.stringify(Object.getOwnPropertyNames(globalThis.inst))'}));
+
+ InspectorTest.log('inst.run():');
+ InspectorTest.logMessage(await Protocol.Runtime.evaluate(
+ {expression: 'globalThis.inst.run()'}));
+
+ InspectorTest.completeTest();
+})();
Loading diff…
Original Bug Report
The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.
References
On This Page