DevTools: add support for system-unique execution context ids

This adds ExecutionContextDescription.uniqueId for a system-unique
way to identify an execution context and supports it in Runtime.evaluate.
This allows a client to avoid accidentally executing an expression
in a context different from that originally intended if a navigation
occurs while Runtime.evaluate is in flight.

Design doc: https://docs.google.com/document/d/1vGVWvKP9FTTX6kimcUJR_PAfVgDeIzXXITFpl0SyghQ

Bug: v8:11268, chromium:1101897
Change-Id: I4c6bec562ffc85312559316f639d641780144039
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/2594538
Commit-Queue: Andrey Kosyakov <caseq@chromium.org>
Reviewed-by: Dmitry Gozman <dgozman@chromium.org>
Reviewed-by: Benedikt Meurer <bmeurer@chromium.org>
Cr-Commit-Position: refs/heads/master@{#71869}
This commit is contained in:
Andrey Kosyakov 2020-12-22 19:54:47 -08:00 committed by Commit Bot
parent e51dcc5225
commit f656eab592
18 changed files with 269 additions and 80 deletions

View File

@ -1226,6 +1226,10 @@ domain Runtime
string origin
# Human readable name describing given context.
string name
# A system-unique execution context identifier. Unlike the id, this is unique accross
# multiple processes, so can be reliably used to identify specific context while backend
# performs a cross-process navigation.
experimental string uniqueId
# Embedder-specific auxiliary data.
optional object auxData
@ -1389,6 +1393,9 @@ domain Runtime
optional boolean silent
# Specifies in which execution context to perform evaluation. If the parameter is omitted the
# evaluation will be performed in the context of the inspected page.
# This is mutually exclusive with `uniqueContextId`, which offers an
# alternative way to identify the execution context that is more reliable
# in a multi-process environment.
optional ExecutionContextId contextId
# Whether the result is expected to be a JSON object that should be sent by value.
optional boolean returnByValue
@ -1415,6 +1422,13 @@ domain Runtime
# when called with non-callable arguments. This flag bypasses CSP for this
# evaluation and allows unsafe-eval. Defaults to true.
experimental optional boolean allowUnsafeEvalBlockedByCSP
# An alternative way to specify the execution context to evaluate in.
# Compared to contextId that may be reused accross processes, this is guaranteed to be
# system-unique, so it can be used to prevent accidental evaluation of the expression
# in context different than intended (e.g. as a result of navigation accross process
# boundaries).
# This is mutually exclusive with `contextId`.
experimental optional string uniqueContextId
returns
# Evaluation result.
RemoteObject result

View File

@ -124,6 +124,8 @@ v8_source_set("inspector") {
"v8-console.h",
"v8-debugger-agent-impl.cc",
"v8-debugger-agent-impl.h",
"v8-debugger-id.cc",
"v8-debugger-id.h",
"v8-debugger-script.cc",
"v8-debugger-script.h",
"v8-debugger.cc",

View File

@ -55,7 +55,8 @@ InspectedContext::InspectedContext(V8InspectorImpl* inspector,
m_contextGroupId(info.contextGroupId),
m_origin(toString16(info.origin)),
m_humanReadableName(toString16(info.humanReadableName)),
m_auxData(toString16(info.auxData)) {
m_auxData(toString16(info.auxData)),
m_uniqueId(V8DebuggerId::generate(inspector)) {
v8::debug::SetContextId(info.context, contextId);
m_weakCallbackData =
new WeakCallbackData(this, m_inspector, m_contextGroupId, m_contextId);

View File

@ -9,11 +9,11 @@
#include <unordered_map>
#include <unordered_set>
#include "include/v8.h"
#include "src/base/macros.h"
#include "src/debug/debug-interface.h"
#include "src/inspector/string-16.h"
#include "include/v8.h"
#include "src/inspector/v8-debugger-id.h"
namespace v8_inspector {
@ -37,6 +37,7 @@ class InspectedContext {
int contextGroupId() const { return m_contextGroupId; }
String16 origin() const { return m_origin; }
String16 humanReadableName() const { return m_humanReadableName; }
V8DebuggerId uniqueId() const { return m_uniqueId; }
String16 auxData() const { return m_auxData; }
bool isReported(int sessionId) const;
@ -66,6 +67,7 @@ class InspectedContext {
const String16 m_origin;
const String16 m_humanReadableName;
const String16 m_auxData;
const V8DebuggerId m_uniqueId;
std::unordered_set<int> m_reportedSessionIds;
std::unordered_map<int, std::unique_ptr<InjectedScript>> m_injectedScripts;
WeakCallbackData* m_weakCallbackData;

View File

@ -0,0 +1,45 @@
// Copyright 2020 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.
#include "src/inspector/v8-debugger-id.h"
#include "src/debug/debug-interface.h"
#include "src/inspector/v8-inspector-impl.h"
namespace v8_inspector {
V8DebuggerId::V8DebuggerId(std::pair<int64_t, int64_t> pair)
: m_first(pair.first), m_second(pair.second) {}
// static
V8DebuggerId V8DebuggerId::generate(V8InspectorImpl* inspector) {
return V8DebuggerId(std::make_pair(inspector->generateUniqueId(),
inspector->generateUniqueId()));
}
V8DebuggerId::V8DebuggerId(const String16& debuggerId) {
const UChar dot = '.';
size_t pos = debuggerId.find(dot);
if (pos == String16::kNotFound) return;
bool ok = false;
int64_t first = debuggerId.substring(0, pos).toInteger64(&ok);
if (!ok) return;
int64_t second = debuggerId.substring(pos + 1).toInteger64(&ok);
if (!ok) return;
m_first = first;
m_second = second;
}
String16 V8DebuggerId::toString() const {
return String16::fromInteger64(m_first) + "." +
String16::fromInteger64(m_second);
}
bool V8DebuggerId::isValid() const { return m_first || m_second; }
std::pair<int64_t, int64_t> V8DebuggerId::pair() const {
return std::make_pair(m_first, m_second);
}
} // namespace v8_inspector

View File

@ -0,0 +1,44 @@
// Copyright 2020 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.
#ifndef V8_INSPECTOR_V8_DEBUGGER_ID_H_
#define V8_INSPECTOR_V8_DEBUGGER_ID_H_
#include <utility>
#include "src/base/macros.h"
#include "src/inspector/protocol/Forward.h"
namespace v8_inspector {
class V8InspectorImpl;
// This debugger id tries to be unique by generating two random
// numbers, which should most likely avoid collisions.
// Debugger id has a 1:1 mapping to context group. It is used to
// attribute stack traces to a particular debugging, when doing any
// cross-debugger operations (e.g. async step in).
// See also Runtime.UniqueDebuggerId in the protocol.
class V8DebuggerId {
public:
V8DebuggerId() = default;
explicit V8DebuggerId(std::pair<int64_t, int64_t>);
explicit V8DebuggerId(const String16&);
V8DebuggerId(const V8DebuggerId&) V8_NOEXCEPT = default;
~V8DebuggerId() = default;
static V8DebuggerId generate(V8InspectorImpl*);
String16 toString() const;
bool isValid() const;
std::pair<int64_t, int64_t> pair() const;
private:
int64_t m_first = 0;
int64_t m_second = 0;
};
} // namespace v8_inspector
#endif // V8_INSPECTOR_V8_DEBUGGER_ID_H_

View File

@ -64,39 +64,6 @@ class MatchPrototypePredicate : public v8::debug::QueryObjectPredicate {
} // namespace
V8DebuggerId::V8DebuggerId(std::pair<int64_t, int64_t> pair)
: m_first(pair.first), m_second(pair.second) {}
// static
V8DebuggerId V8DebuggerId::generate(V8InspectorImpl* inspector) {
return V8DebuggerId(std::make_pair(inspector->generateUniqueId(),
inspector->generateUniqueId()));
}
V8DebuggerId::V8DebuggerId(const String16& debuggerId) {
const UChar dot = '.';
size_t pos = debuggerId.find(dot);
if (pos == String16::kNotFound) return;
bool ok = false;
int64_t first = debuggerId.substring(0, pos).toInteger64(&ok);
if (!ok) return;
int64_t second = debuggerId.substring(pos + 1).toInteger64(&ok);
if (!ok) return;
m_first = first;
m_second = second;
}
String16 V8DebuggerId::toString() const {
return String16::fromInteger64(m_first) + "." +
String16::fromInteger64(m_second);
}
bool V8DebuggerId::isValid() const { return m_first || m_second; }
std::pair<int64_t, int64_t> V8DebuggerId::pair() const {
return std::make_pair(m_first, m_second);
}
V8Debugger::V8Debugger(v8::Isolate* isolate, V8InspectorImpl* inspector)
: m_isolate(isolate),
m_inspector(inspector),

View File

@ -11,15 +11,15 @@
#include <unordered_set>
#include <vector>
#include "include/v8-inspector.h"
#include "src/base/macros.h"
#include "src/inspector/inspected-context.h"
#include "src/inspector/protocol/Debugger.h"
#include "src/inspector/protocol/Forward.h"
#include "src/inspector/protocol/Runtime.h"
#include "src/inspector/v8-debugger-id.h"
#include "src/inspector/v8-debugger-script.h"
#include "include/v8-inspector.h"
namespace v8_inspector {
class AsyncStackTrace;
@ -36,31 +36,6 @@ using protocol::Response;
using TerminateExecutionCallback =
protocol::Runtime::Backend::TerminateExecutionCallback;
// This debugger id tries to be unique by generating two random
// numbers, which should most likely avoid collisions.
// Debugger id has a 1:1 mapping to context group. It is used to
// attribute stack traces to a particular debugging, when doing any
// cross-debugger operations (e.g. async step in).
// See also Runtime.UniqueDebuggerId in the protocol.
class V8DebuggerId {
public:
V8DebuggerId() = default;
explicit V8DebuggerId(std::pair<int64_t, int64_t>);
explicit V8DebuggerId(const String16&);
V8DebuggerId(const V8DebuggerId&) V8_NOEXCEPT = default;
~V8DebuggerId() = default;
static V8DebuggerId generate(V8InspectorImpl*);
String16 toString() const;
bool isValid() const;
std::pair<int64_t, int64_t> pair() const;
private:
int64_t m_first = 0;
int64_t m_second = 0;
};
class V8Debugger : public v8::debug::DebugDelegate,
public v8::debug::AsyncEventDelegate {
public:

View File

@ -81,6 +81,11 @@ int V8InspectorImpl::contextGroupId(int contextId) const {
return it != m_contextIdToGroupIdMap.end() ? it->second : 0;
}
int V8InspectorImpl::resolveUniqueContextId(V8DebuggerId uniqueId) const {
auto it = m_uniqueIdToContextId.find(uniqueId.pair());
return it == m_uniqueIdToContextId.end() ? 0 : it->second;
}
v8::MaybeLocal<v8::Value> V8InspectorImpl::compileAndRunInternalScript(
v8::Local<v8::Context> context, v8::Local<v8::String> source) {
v8::Local<v8::UnboundScript> unboundScript;
@ -190,6 +195,11 @@ void V8InspectorImpl::contextCreated(const V8ContextInfo& info) {
auto* context = new InspectedContext(this, info, contextId);
m_contextIdToGroupIdMap[contextId] = info.contextGroupId;
DCHECK(m_uniqueIdToContextId.find(context->uniqueId().pair()) ==
m_uniqueIdToContextId.end());
m_uniqueIdToContextId.insert(
std::make_pair(context->uniqueId().pair(), contextId));
auto contextIt = m_contexts.find(info.contextGroupId);
if (contextIt == m_contexts.end())
contextIt = m_contexts
@ -233,14 +243,15 @@ void V8InspectorImpl::contextCollected(int groupId, int contextId) {
void V8InspectorImpl::resetContextGroup(int contextGroupId) {
m_consoleStorageMap.erase(contextGroupId);
m_muteExceptionsMap.erase(contextGroupId);
std::vector<int> contextIdsToClear;
forEachContext(contextGroupId,
[&contextIdsToClear](InspectedContext* context) {
contextIdsToClear.push_back(context->contextId());
});
auto contextsIt = m_contexts.find(contextGroupId);
// Context might have been removed already by discardContextScript()
if (contextsIt != m_contexts.end()) {
for (const auto& map_entry : *contextsIt->second)
m_uniqueIdToContextId.erase(map_entry.second->uniqueId().pair());
m_contexts.erase(contextsIt);
}
forEachSession(contextGroupId,
[](V8InspectorSessionImpl* session) { session->reset(); });
m_contexts.erase(contextGroupId);
}
void V8InspectorImpl::idleStarted() { m_isolate->SetIdle(true); }
@ -362,7 +373,9 @@ v8::Local<v8::Context> V8InspectorImpl::regexContext() {
void V8InspectorImpl::discardInspectedContext(int contextGroupId,
int contextId) {
if (!getContext(contextGroupId, contextId)) return;
auto* context = getContext(contextGroupId, contextId);
if (!context) return;
m_uniqueIdToContextId.erase(context->uniqueId().pair());
m_contexts[contextGroupId]->erase(contextId);
if (m_contexts[contextGroupId]->empty()) m_contexts.erase(contextGroupId);
}

View File

@ -68,6 +68,7 @@ class V8InspectorImpl : public V8Inspector {
int contextGroupId(v8::Local<v8::Context>) const;
int contextGroupId(int contextId) const;
uint64_t isolateId() const { return m_isolateId; }
int resolveUniqueContextId(V8DebuggerId uniqueId) const;
v8::MaybeLocal<v8::Value> compileAndRunInternalScript(v8::Local<v8::Context>,
v8::Local<v8::String>);
@ -178,6 +179,7 @@ class V8InspectorImpl : public V8Inspector {
ConsoleStorageMap m_consoleStorageMap;
std::unordered_map<int, int> m_contextIdToGroupIdMap;
std::map<std::pair<int64_t, int64_t>, int> m_uniqueIdToContextId;
std::unique_ptr<V8Console> m_console;

View File

@ -204,9 +204,21 @@ void innerCallFunctionOn(
}
Response ensureContext(V8InspectorImpl* inspector, int contextGroupId,
Maybe<int> executionContextId, int* contextId) {
Maybe<int> executionContextId,
Maybe<String16> uniqueContextId, int* contextId) {
if (executionContextId.isJust()) {
if (uniqueContextId.isJust()) {
return Response::InvalidParams(
"contextId and uniqueContextId are mutually exclusive");
}
*contextId = executionContextId.fromJust();
} else if (uniqueContextId.isJust()) {
V8DebuggerId uniqueId(uniqueContextId.fromJust());
if (!uniqueId.isValid())
return Response::InvalidParams("invalid uniqueContextId");
int id = inspector->resolveUniqueContextId(uniqueId);
if (!id) return Response::InvalidParams("uniqueContextId not found");
*contextId = id;
} else {
v8::HandleScope handles(inspector->isolate());
v8::Local<v8::Context> defaultContext =
@ -215,6 +227,7 @@ Response ensureContext(V8InspectorImpl* inspector, int contextGroupId,
return Response::ServerError("Cannot find default execution context");
*contextId = InspectedContext::contextId(defaultContext);
}
return Response::Success();
}
@ -238,13 +251,14 @@ void V8RuntimeAgentImpl::evaluate(
Maybe<bool> generatePreview, Maybe<bool> userGesture,
Maybe<bool> maybeAwaitPromise, Maybe<bool> throwOnSideEffect,
Maybe<double> timeout, Maybe<bool> disableBreaks, Maybe<bool> maybeReplMode,
Maybe<bool> allowUnsafeEvalBlockedByCSP,
Maybe<bool> allowUnsafeEvalBlockedByCSP, Maybe<String16> uniqueContextId,
std::unique_ptr<EvaluateCallback> callback) {
TRACE_EVENT0(TRACE_DISABLED_BY_DEFAULT("devtools.timeline"),
"EvaluateScript");
int contextId = 0;
Response response = ensureContext(m_inspector, m_session->contextGroupId(),
std::move(executionContextId), &contextId);
std::move(executionContextId),
std::move(uniqueContextId), &contextId);
if (!response.IsSuccess()) {
callback->sendFailure(response);
return;
@ -378,9 +392,9 @@ void V8RuntimeAgentImpl::callFunctionOn(
std::move(callback));
} else {
int contextId = 0;
Response response =
ensureContext(m_inspector, m_session->contextGroupId(),
std::move(executionContextId.fromJust()), &contextId);
Response response = ensureContext(m_inspector, m_session->contextGroupId(),
std::move(executionContextId.fromJust()),
/* uniqueContextId */ {}, &contextId);
if (!response.IsSuccess()) {
callback->sendFailure(response);
return;
@ -497,7 +511,8 @@ Response V8RuntimeAgentImpl::compileScript(
int contextId = 0;
Response response = ensureContext(m_inspector, m_session->contextGroupId(),
std::move(executionContextId), &contextId);
std::move(executionContextId),
/*uniqueContextId*/ {}, &contextId);
if (!response.IsSuccess()) return response;
InjectedScript::ContextScope scope(m_session, contextId);
response = scope.initialize();
@ -550,7 +565,8 @@ void V8RuntimeAgentImpl::runScript(
int contextId = 0;
Response response = ensureContext(m_inspector, m_session->contextGroupId(),
std::move(executionContextId), &contextId);
std::move(executionContextId),
/*uniqueContextId*/ {}, &contextId);
if (!response.IsSuccess()) {
callback->sendFailure(response);
return;
@ -626,7 +642,8 @@ Response V8RuntimeAgentImpl::globalLexicalScopeNames(
std::unique_ptr<protocol::Array<String16>>* outNames) {
int contextId = 0;
Response response = ensureContext(m_inspector, m_session->contextGroupId(),
std::move(executionContextId), &contextId);
std::move(executionContextId),
/*uniqueContextId*/ {}, &contextId);
if (!response.IsSuccess()) return response;
InjectedScript::ContextScope scope(m_session, contextId);
@ -864,6 +881,7 @@ void V8RuntimeAgentImpl::reportExecutionContextCreated(
.setId(context->contextId())
.setName(context->humanReadableName())
.setOrigin(context->origin())
.setUniqueId(context->uniqueId().toString())
.build();
const String16& aux = context->auxData();
if (!aux.isEmpty()) {

View File

@ -71,6 +71,7 @@ class V8RuntimeAgentImpl : public protocol::Runtime::Backend {
Maybe<bool> awaitPromise, Maybe<bool> throwOnSideEffect,
Maybe<double> timeout, Maybe<bool> disableBreaks,
Maybe<bool> replMode, Maybe<bool> allowUnsafeEvalBlockedByCSP,
Maybe<String16> uniqueContextId,
std::unique_ptr<EvaluateCallback>) override;
void awaitPromise(const String16& promiseObjectId, Maybe<bool> returnByValue,
Maybe<bool> generatePreview,

View File

@ -36,7 +36,7 @@ InspectorTest.logMessage = function(originalMessage) {
const nonStableFields = new Set([
'objectId', 'scriptId', 'exceptionId', 'timestamp', 'executionContextId',
'callFrameId', 'breakpointId', 'bindRemoteObjectFunctionId',
'formatterObjectId', 'debuggerId', 'bodyGetterId'
'formatterObjectId', 'debuggerId', 'bodyGetterId', 'uniqueId'
]);
const message = JSON.parse(JSON.stringify(originalMessage, replacer.bind(null, Symbol(), nonStableFields)));
if (message.id)

View File

@ -6,6 +6,7 @@ Checks createContext().
id : 1
name :
origin :
uniqueId : <uniqueId>
}
}
}
@ -16,6 +17,7 @@ Checks createContext().
id : 2
name :
origin :
uniqueId : <uniqueId>
}
}
}
@ -37,6 +39,7 @@ Reported script's execution id: 2
id : 1
name :
origin :
uniqueId : <uniqueId>
}
}
}
@ -47,6 +50,7 @@ Reported script's execution id: 2
id : 2
name :
origin :
uniqueId : <uniqueId>
}
}
}

View File

@ -0,0 +1,24 @@
Tests how Runtime.evaluate handles uniqueContextId argument
token in context 1: context 1
token in context 2: context 2
{
error : {
code : -32602
message : contextId and uniqueContextId are mutually exclusive
}
id : <messageId>
}
{
error : {
code : -32602
message : invalid uniqueContextId
}
id : <messageId>
}
{
error : {
code : -32602
message : uniqueContextId not found
}
id : <messageId>
}

View File

@ -0,0 +1,66 @@
// Copyright 2020 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 {Protocol, contextGroup} = InspectorTest.start(
`Tests how Runtime.evaluate handles uniqueContextId argument`);
(async function test(){
Protocol.Runtime.enable();
const context1 = (await Protocol.Runtime.onceExecutionContextCreated()).params.context;
contextGroup.createContext();
const context2 = (await Protocol.Runtime.onceExecutionContextCreated()).params.context;
Protocol.Runtime.evaluate({
expression: 'token = "context 1";',
contextId: context1.id
});
Protocol.Runtime.evaluate({
expression: 'token = "context 2";',
contextId: context2.id
});
{
const response = (await Protocol.Runtime.evaluate({
expression: 'token',
uniqueContextId: context1.uniqueId,
returnByValue: true
})).result.result.value;
InspectorTest.logMessage(`token in context 1: ${response}`);
}
{
const response = (await Protocol.Runtime.evaluate({
expression: 'token',
uniqueContextId: context2.uniqueId,
returnByValue: true
})).result.result.value;
InspectorTest.logMessage(`token in context 2: ${response}`);
}
// The following tests are for error handling.
{
const response = (await Protocol.Runtime.evaluate({
expression: 'token',
uniqueContextId: context1.uniqueId,
contextId: context1.id
}));
InspectorTest.logMessage(response);
}
{
const response = (await Protocol.Runtime.evaluate({
expression: 'token',
uniqueContextId: 'fubar',
}));
InspectorTest.logMessage(response);
}
{
const response = (await Protocol.Runtime.evaluate({
expression: 'token',
uniqueContextId: context1.uniqueId + 1,
}));
InspectorTest.logMessage(response);
}
InspectorTest.completeTest();
})();

View File

@ -8,6 +8,7 @@ Running test: testExecutionContextsNotificationsOnRestore
id : 1
name :
origin :
uniqueId : <uniqueId>
}
}
}
@ -24,6 +25,7 @@ will reconnect..
id : 1
name :
origin :
uniqueId : <uniqueId>
}
}
}

View File

@ -8,6 +8,7 @@ From session 1
id : 1
name :
origin :
uniqueId : <uniqueId>
}
}
}
@ -20,6 +21,7 @@ From session 2
id : 1
name :
origin :
uniqueId : <uniqueId>
}
}
}
@ -32,6 +34,7 @@ From session 2
id : 1
name :
origin :
uniqueId : <uniqueId>
}
}
}
@ -44,6 +47,7 @@ From session 1
id : 1
name :
origin :
uniqueId : <uniqueId>
}
}
}
@ -56,6 +60,7 @@ From session 3
id : 1
name :
origin :
uniqueId : <uniqueId>
}
}
}
@ -92,6 +97,7 @@ From session 2
id : 2
name :
origin :
uniqueId : <uniqueId>
}
}
}
@ -103,6 +109,7 @@ From session 1
id : 2
name :
origin :
uniqueId : <uniqueId>
}
}
}
@ -114,6 +121,7 @@ From session 3
id : 2
name :
origin :
uniqueId : <uniqueId>
}
}
}
@ -127,6 +135,7 @@ From session 4
id : 2
name :
origin :
uniqueId : <uniqueId>
}
}
}