Low watch CVSS 9.1 chrome Logic Error 🔧 Commit mapped

Overview

Low
Severity
9.1
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactOff-by-one error in DevTools
DescriptionOff-by-one error in DevTools
ComponentDevTools
Sub-componentInspector
Bug ClassLogic Error
Action tierwatch — dos-class only — no demonstrated corruption path
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.
Tracker532303080
Fix commit741c0f4ad7bd (v8/v8) +52/-1
CWECWE-193
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-07-08 (per vendor advisory)
Fix landed2026-07-16 (public git)
Disclosed2026-08-25
Patch gap40 days — fix public in source before the advisory

Background

`Debugger.setVariableValue`
a DevTools/CDP protocol command that sets the value of a variable in a specified scope of a paused call frame, addressed by a scopeNumber index into the scope chain.
`V8DebuggerAgentImpl::setVariableValue`
the V8 inspector handler (in src/inspector/v8-debugger-agent-impl.cc) that walks the scope chain with a scopeIterator to reach the requested scope.
Scope iterator
an object with Advance(), Done(), and SetVariableValue() that traverses a call frame’s N scopes; Done() is true once it has advanced past the last scope.
Off-by-one boundary
requesting scopeNumber == N (equal to the chain length) advances the iterator N times, reaching Done() while scopeNumber decrements exactly to 0.

Root Cause Analysis

V8DebuggerAgentImpl::setVariableValue loops --scopeNumber; scopeIterator->Advance(); to reach the requested scope, then validated the result with only if (scopeNumber != 0). When a client passed scopeNumber equal to the scope chain length N, the loop ran N times so scopeNumber reached 0 (passing the check) on the exact step the iterator advanced past the final scope, making scopeIterator->Done() true. Because the guard checked only that scopeNumber had counted down to 0 and not that the iterator was still valid, execution fell through to scopeIterator->SetVariableValue() on an exhausted iterator. That dereference of an invalid iterator triggered a DCHECK failure in debug builds and a null pointer dereference in release builds.

The fix strengthens the guard to if (scopeNumber != 0 || scopeIterator->Done()), so an exhausted iterator is rejected with a Response::ServerError("Could not find scope with given number") before any dereference.

Key insight
The single mistake was validating only that the countdown index reached 0 without also checking iterator exhaustion, so scopeNumber == N slipped past the boundary check; the fix adds || scopeIterator->Done() so an out-of-range scope number returns a protocol error instead of dereferencing an invalid iterator.

Attack Path

  1. Pause execution A DevTools client enables the debugger and pauses at a breakpoint in a function, obtaining a callFrameId and the scope chain length N.
  2. Request the out-of-bounds scope The client calls Debugger.setVariableValue with scopeNumber set to N (one past the last valid index N-1), a variable name, and a new value.
  3. Exhaust the iterator past the guard The loop advances the iterator N times, decrementing scopeNumber to 0 (satisfying the old scopeNumber != 0 check) while scopeIterator->Done() becomes true.
  4. Dereference the invalid iterator scopeIterator->SetVariableValue() is invoked on the exhausted iterator, causing a DCHECK failure (debug) or a null pointer dereference / crash (release).

Impact Assessment

A DevTools/CDP client that can pause script and issue Debugger.setVariableValue with an out-of-range scopeNumber triggers a null pointer dereference (crash / denial of service) in the process running the V8 inspector, per the advisory’s “Off-by-one error in DevTools.” The precondition is the ability to drive the debugger protocol against a target. The vendor advisory rates this low severity and does not report in-the-wild exploitation; NVD scores it CVSS 9.1. Any escalation beyond denial of service is (inferred), and the advisory notes no exploited-in-the-wild status or severity.

Changed Functions

FunctionChangeNotes
if
src/inspector/v8-debugger-agent-impl.cc
modified
if
test/inspector/debugger/set-variable-value-off-by-one.js
modified

Files Changed

  • test/inspector/debugger/set-variable-value-off-by-one-expected.txt
  • test/inspector/debugger/set-variable-value-off-by-one.js
  • src/inspector/v8-debugger-agent-impl.cc

Audit Directions

  • Countdown-index boundary checks
    Audit loops that decrement an index while advancing an iterator and then test only the index for 0; also verify the iterator/container is not exhausted (Done()/end) before use.
  • Off-by-one at chain/array length
    Review protocol handlers that index into scope chains, call frames, or arrays by a client-supplied number for acceptance of the value equal to the length (N vs N-1).
  • DCHECK-masked release crashes
    Look for paths where an invalid state trips a DCHECK in debug but a raw dereference in release; these often indicate a missing validation reachable from untrusted protocol input.
From 741c0f4ad7bd87d50294e67e4f01bb9b382d853b Mon Sep 17 00:00:00 2001
From: Etienne Bergeron <[email protected]>
Date: Wed, 15 Jul 2026 17:47:35 +0000
Subject: [PATCH] [inspector] Fix off-by-one boundary check in setVariableValue

When V8DebuggerAgentImpl::setVariableValue iterates through scopes,
requesting a scopeNumber equal to the scope chain length (N) causes the
iterator to advance past the last scope so scopeIterator->Done()
becomes true, while scopeNumber simultaneously decrements to 0.

Because the boundary check only verified (scopeNumber != 0), it failed
to detect iterator exhaustion when scopeNumber reached 0 on the same
step as Done(). Subsequent calls to scopeIterator->SetVariableValue()
dereferenced an invalid iterator, triggering a DCHECK failure in debug
builds and a null pointer dereference in release builds.

This CL updates the boundary guard to check (scopeNumber != 0 ||
scopeIterator->Done()), rejecting out-of-bounds scope numbers with a
protocol error response.

Bug: 532303080
Change-Id: I86ab290c66deff16259b82832d29db5531d60599
Fixed: 532303080
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/8100344
Commit-Queue: Etienne Bergeron <[email protected]>
Reviewed-by: Simon Zünd <[email protected]>
Reviewed-by: Philip Pfaffe <[email protected]>
Cr-Commit-Position: refs/heads/main@{#108706}
---
diff --git a/src/inspector/v8-debugger-agent-impl.cc b/src/inspector/v8-debugger-agent-impl.cc
index abd10db..466b784 100644
--- a/src/inspector/v8-debugger-agent-impl.cc
+++ b/src/inspector/v8-debugger-agent-impl.cc
@@ -1836,7 +1836,7 @@
     --scopeNumber;
     scopeIterator->Advance();
   }
-  if (scopeNumber != 0) {
+  if (scopeNumber != 0 || scopeIterator->Done()) {
     return Response::ServerError("Could not find scope with given number");
   }
 
diff --git a/test/inspector/debugger/set-variable-value-off-by-one-expected.txt b/test/inspector/debugger/set-variable-value-off-by-one-expected.txt
new file mode 100644
index 0000000..0ee3ae6
--- /dev/null
+++ b/test/inspector/debugger/set-variable-value-off-by-one-expected.txt
@@ -0,0 +1,5 @@
+Tests setVariableValue with scopeNumber equal to scopeChain length (off-by-one boundary)
+Paused in test(). Scope chain length: 2
+Calling setVariableValue with scopeNumber = 2
+Received error (expected): Could not find scope with given number
+test() finished executing.
diff --git a/test/inspector/debugger/set-variable-value-off-by-one.js b/test/inspector/debugger/set-variable-value-off-by-one.js
new file mode 100644
index 0000000..262e17a
--- /dev/null
+++ b/test/inspector/debugger/set-variable-value-off-by-one.js
@@ -0,0 +1,46 @@
+// 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 { contextGroup, Protocol } = InspectorTest.start(
+  'Tests setVariableValue with scopeNumber equal to scopeChain length (off-by-one boundary)'
+);
+
+contextGroup.addScript(`
+function test() {
+  let a = 10;
+  debugger;
+  return a;
+}
+`);
+
+Protocol.Debugger.enable();
+Protocol.Debugger.onPaused(async message => {
+  const callFrame = message.params.callFrames[0];
+  const callFrameId = callFrame.callFrameId;
+  const numScopes = callFrame.scopeChain.length;
+
+  InspectorTest.log('Paused in test(). Scope chain length: ' + numScopes);
+
+  // Call setVariableValue with scopeNumber = numScopes (off-by-one out-of-bounds)
+  InspectorTest.log('Calling setVariableValue with scopeNumber = ' + numScopes);
+  const response = await Protocol.Debugger.setVariableValue({
+    scopeNumber: numScopes,
+    variableName: 'a',
+    newValue: { value: 99 },
+    callFrameId
+  });
+
+  if (response.error) {
+    InspectorTest.log('Received error (expected): ' + response.error.message);
+  } else {
+    InspectorTest.log('SUCCESS (UNEXPECTED! Should have returned error)');
+  }
+
+  await Protocol.Debugger.resume();
+});
+
+Protocol.Runtime.evaluate({ expression: 'test()' }).then(response => {
+  InspectorTest.log('test() finished executing.');
+  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.