CVE-2026-79148
Overview
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
scopeNumberindex into the scope chain. - `V8DebuggerAgentImpl::setVariableValue`
- the V8 inspector handler (in
src/inspector/v8-debugger-agent-impl.cc) that walks the scope chain with ascopeIteratorto reach the requested scope. - Scope iterator
- an object with
Advance(),Done(), andSetVariableValue()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, reachingDone()whilescopeNumberdecrements 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.
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
- Pause execution
A DevTools client enables the debugger and pauses at a breakpoint in a function, obtaining a
callFrameIdand the scope chain length N. - Request the out-of-bounds scope
The client calls
Debugger.setVariableValuewithscopeNumberset to N (one past the last valid index N-1), a variable name, and a new value. - Exhaust the iterator past the guard
The loop advances the iterator N times, decrementing
scopeNumberto 0 (satisfying the oldscopeNumber != 0check) whilescopeIterator->Done()becomes true. - Dereference the invalid iterator
scopeIterator->SetVariableValue()is invoked on the exhausted iterator, causing aDCHECKfailure (debug) or a null pointer dereference / crash (release).
Impact Assessment
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
| Function | Change | Notes |
|---|---|---|
ifsrc/inspector/v8-debugger-agent-impl.cc |
modified | |
iftest/inspector/debugger/set-variable-value-off-by-one.js |
modified |
Files Changed
test/inspector/debugger/set-variable-value-off-by-one-expected.txttest/inspector/debugger/set-variable-value-off-by-one.jssrc/inspector/v8-debugger-agent-impl.cc
Audit Directions
- Countdown-index boundary checksAudit 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 lengthReview 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 crashesLook for paths where an invalid state trips a
DCHECKin debug but a raw dereference in release; these often indicate a missing validation reachable from untrusted protocol input.
Patch
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(); +});