[debug] Cleanup properly when microtask execution is terminated.

When a terminate_exception is raised while executing one of the promise
related jobs on the microtask queue, we don't clean up properly, leaving
the async stack in the inspector in an inconsistent state, not cleaning
up the promise stack on the Isolate, and also not resetting the global
current_microtask slot. This CL adds appropriate logic to perform the
correct cleanup.

Fixed: chromium:1297964
Change-Id: I4ec64405d4c66bfe1f0115e7039866447fb10f02
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/3471815
Auto-Submit: Benedikt Meurer <bmeurer@chromium.org>
Reviewed-by: Jaroslav Sevcik <jarin@chromium.org>
Commit-Queue: Jaroslav Sevcik <jarin@chromium.org>
Cr-Commit-Position: refs/heads/main@{#79162}
This commit is contained in:
Benedikt Meurer 2022-02-17 16:17:30 +01:00 committed by V8 LUCI CQ
parent dfab3f44e8
commit 824ae14c7a
5 changed files with 463 additions and 2 deletions

View File

@ -5001,6 +5001,54 @@ void Isolate::OnPromiseAfter(Handle<JSPromise> promise) {
if (debug()->is_active()) PopPromise();
}
void Isolate::OnTerminationDuringRunMicrotasks() {
// This performs cleanup for when RunMicrotasks (in
// builtins-microtask-queue-gen.cc) is aborted via a termination exception.
// This has to be kept in sync with the code in said file. Currently this
// includes:
//
// (1) Resetting the |current_microtask| slot on the Isolate to avoid leaking
// memory (and also to keep |current_microtask| not being undefined as an
// indicator that we're currently pumping the microtask queue).
// (2) Empty the promise stack to avoid leaking memory.
// (3) If the |current_microtask| is a promise reaction or resolve thenable
// job task, then signal the async event delegate and debugger that the
// microtask finished running.
//
// Reset the |current_microtask| global slot.
Handle<Microtask> current_microtask(
Microtask::cast(heap()->current_microtask()), this);
heap()->set_current_microtask(ReadOnlyRoots(this).undefined_value());
// Empty the promise stack.
while (PopPromise()) {
}
if (current_microtask->IsPromiseReactionJobTask()) {
Handle<PromiseReactionJobTask> promise_reaction_job_task =
Handle<PromiseReactionJobTask>::cast(current_microtask);
Handle<HeapObject> promise_or_capability(
promise_reaction_job_task->promise_or_capability(), this);
if (promise_or_capability->IsPromiseCapability()) {
promise_or_capability = handle(
Handle<PromiseCapability>::cast(promise_or_capability)->promise(),
this);
}
if (promise_or_capability->IsJSPromise()) {
OnPromiseAfter(Handle<JSPromise>::cast(promise_or_capability));
}
} else if (current_microtask->IsPromiseResolveThenableJobTask()) {
Handle<PromiseResolveThenableJobTask> promise_resolve_thenable_job_task =
Handle<PromiseResolveThenableJobTask>::cast(current_microtask);
Handle<JSPromise> promise_to_resolve(
promise_resolve_thenable_job_task->promise_to_resolve(), this);
OnPromiseAfter(promise_to_resolve);
}
SetTerminationOnExternalTryCatch();
}
void Isolate::SetPromiseRejectCallback(PromiseRejectCallback callback) {
promise_reject_callback_ = callback;
}

View File

@ -960,6 +960,7 @@ class V8_EXPORT_PRIVATE Isolate final : private HiddenFactory {
void OnPromiseThen(Handle<JSPromise> promise);
void OnPromiseBefore(Handle<JSPromise> promise);
void OnPromiseAfter(Handle<JSPromise> promise);
void OnTerminationDuringRunMicrotasks();
// Re-throw an exception. This involves no error reporting since error
// reporting was handled when the exception was thrown originally.

View File

@ -4,8 +4,8 @@
#include "src/execution/microtask-queue.h"
#include <stddef.h>
#include <algorithm>
#include <cstddef>
#include "src/api/api-inl.h"
#include "src/base/logging.h"
@ -187,7 +187,7 @@ int MicrotaskQueue::RunMicrotasks(Isolate* isolate) {
size_ = 0;
start_ = 0;
DCHECK(isolate->has_scheduled_exception());
isolate->SetTerminationOnExternalTryCatch();
isolate->OnTerminationDuringRunMicrotasks();
OnCompleted(isolate);
return -1;
}

View File

@ -26,6 +26,7 @@
'inspector/endless-loop': [SKIP],
'inspector/invalid': [SKIP],
'inspector/regress-1166549': [SKIP],
'inspector/regress-1297964': [SKIP],
}], # third_party_heap
]

View File

@ -0,0 +1,411 @@
utils = new Proxy(utils, {
get: function(target, prop) {
if (prop in target) return target[prop];
return i=>i;
}
});
// Loaded from 'test/inspector/protocol-test.js':
// Copyright 2016 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.
InspectorTest = {};
InspectorTest._dumpInspectorProtocolMessages = false;
InspectorTest._commandsForLogging = new Set();
InspectorTest._sessions = new Set();
InspectorTest.log = utils.print.bind(utils);
InspectorTest.quitImmediately = utils.quit.bind(utils);
InspectorTest.logProtocolCommandCalls = function(command) {
InspectorTest._commandsForLbytes;
}
InspectorTest.trimErrorMessage = function(message) {
if (!message.error || !message.error.data)
return message;
message.error.data = message.error.data.replace(/at position \d+/,
'at <some position>');
return message;
}
InspectorTest.ContextGroup = class {
constructor() {
this.id = utils.createContextGroup();
}
createContext(name) {
utils.createContext(this.id, name || '');
}
schedulePauseOnNextStatement(reason, details) {
utils.schedulePauseOnNextStatement(this.id, reason, details);
}
cancelPauseOnNextStatement() {
utils.cancelPauseOnNextStatement(this.id);
}
addScript(string, lineOffset, columnOffset, url) {
utils.compileAndRunWithOrigin(this.id, string, url || '', lineOffset || 0, columnOffset || 0, false);
}
addInlineScript(string, url) {
const match = (new Error().stack).split('\n')[2].match(/([0-9]+):([0-9]+)/);
this.addScript(
string, match[1] * 1, match[1] * 1 + '.addInlineScript('.length, url);
}
addModule(string, url, lineOffset, columnOffset) {
utils.compileAndRunWithOrigin(this.id, string, url, lineOffset || 0, columnOffset || 0, true);
}
loadScript(fileName) {
this.addScript(utils.read(fileName));
}
connect() {
return new InspectorTest.Session(this);
}
reset() {
utils.resetContextGroup(this.id);
}
setupInjectedScriptEnvironment(session) {
let scriptSource = '';
let getters = ["length","internalConstructorName","subtype","getProperty",
"objectHasOwnProperty","nullifyPrototype","primitiveTypes",
"closureTypes","prototype","all","RemoteObject","bind",
"PropertyDescriptor","object","get","set","value","configurable",
"enumerable","symbol","getPrototypeOf","nativeAccessorDescriptor",
"isBuiltin","hasGetter","hasSetter","getOwnPropertyDescriptor",
"description","isOwn","name",
"typedArrayProperties","keys","getOwnPropertyNames",
"getOwnPropertySymbols","isPrimitiveValue","com","toLowerCase",
"ELEMENT","trim","replace","DOCUMENT","size","byteLength","toString",
"stack","substr","message","indexOf","key","type","unserializableValue",
"objectId","className","preview","proxyTargetValue","customPreview",
"CustomPreview","resolve","then","console","error","header","hasBody",
"stringify","ObjectPreview","ObjectPreviewType","properties",
"ObjectPreviewSubtype","getInternalProperties","wasThrown","indexes",
"overflow","valuePreview","entries"];
scriptSource += `(function installSettersAndGetters() {
let defineProperty = Object.defineProperty;
let ObjectPrototype = Object.prototype;
let ArrayPrototype = Array.prototype;
defineProperty(ArrayPrototype, 0, {
{ debugger; throw 42; }, get() { debugger; throw 42; },
__proto__: null
});`,
scriptSource += getters.map(getter => `
defineProperty(ObjectPrototype, '${getter}', {
set() { debugger; throw 42; }, get() { debugger; throw 42; },
__proto__: null
});
`).join('\n') + '})();';
this.addScript(scriptSource);
if (session) {
InspectorTest.log('WARNING: setupInjectedScriptEnvironment with debug flag for debugging only and should not be landed.');
session.setupScriptMap();
session.Protocol.Debugger.enable();
session.Protocol.Debugger.onPaused(message => {
let callFrames = message.params.callFrames;
session.logSourceLocations(callFrames.map(frame => frame.location));
})
}
}
};
InspectorTest.Session = class {
constructor(contextGroup) {
this.contextGroup = contextGroup;
this._dispatchTable = new Map();
this._eventHandlers = new Map();
this._requestId = 0;
this.Protocol = this._setupProtocol();
InspectorTest._sessions.add(this);
this.id = utils.connectSession(contextGroup.id, '', this._dispatchMessage.bind(this));
}
disconnect() {
InspectorTest._sessions.delete(this);
utils.disconnectSession(this.id);
}
reconnect() {
var state = utils.disconnectSession(this.id);
this.id = utils.connectSession(this.contextGroup.id, state, this._dispatchMessage.bind(this));
}
async addInspectedObject(serializable) {
return this.Protocol.Runtime.evaluate({expression: `inspector.addInspectedObject(${this.id}, ${JSON.stringify(serializable)})`});
}
sendRawCommand(requestId, command, handler) {
if (InspectorTest._dumpInspectorProtocolMessages)
utils.print("frontend: " + command);
this._dispatchTable.set(requestId, handler);
utils.sendMessageToBackend(this.id, command);
}
setupScriptMap() {
if (this._scriptMap)
return;
this._scriptMap = new Map();
}
logCallFrames(callFrames) {
for (var frame of callFrames) {
var functionName = frame.functionName || '(anonymous)';
var scriptId = frame.location ? frame.location.scriptId : frame.scriptId;
var url = frame.url ? frame.url : this._scriptMap.get(scriptId).url;
var lineNumber = frame.location ? frame.location.lineNumber : frame.lineNumber;
var columnNumber = frame.location ? frame.location.columnNumber : frame.columnNumber;
InspectorTest.log(`${functionName} (${url}:${lineNumber}:${columnNumber})`);
}
}
async getScriptWithSource(scriptId, forceSourceRequest) {
var script = this._scriptMap.get(scriptId);
if (forceSourceRequest || !(script.scriptSource || script.bytecode)) {
var message = await this.Protocol.Debugger.getScriptSource({ scriptId });
script.scriptSource = message.result.scriptSource;
if (message.result.bytecode) {
script.bytecode = InspectorTest.decodeBase64(message.result.bytecode);
}
}
return script;
}
async logSourceLocation(location, forceSourceRequest) {
var scriptId = location.scriptId;
if (!this._scriptMap || !this._scriptMap.has(scriptId)) {
InspectorTest.log("setupScriptMap should be called before Protocol.Debugger.enable.");
InspectorTest.completeTest();
}
var script = await this.getScriptWithSource(scriptId, forceSourceRequest);
if (script.bytecode) {
if (location.lineNumber != 0) {
InspectorTest.log('Unexpected wasm line number: ' + location.lineNumber);
}
let wasm_opcode = script.bytecode[location.columnNumber];
let opcode_str = wasm_opcode.toString(16);
if (opcode_str.length % 2) opcode_str = `0${opcode_str}`;
if (InspectorTest.getWasmOpcodeName) {
opcode_str += ` (${InspectorTest.getWasmOpcodeName(wasm_opcode)})`;
}
InspectorTest.log(`Script ${script.url} byte offset ${
location.columnNumber}: Wasm opcode 0x${opcode_str}`);
} else {
var lines = script.scriptSource.split('\n');
var line = lines[location.lineNumber];
line = line.slice(0, location.columnNumber) + '#' + (line.slice(location.columnNumber) || '');
lines[location.lineNumber] = line;
lines = lines.filter(line => line.indexOf('//# sourceURL=') === -1);
InspectorTest.log(lines.slice(Math.max(location.lineNumber - 1, 0), location.lineNumber + 2).join('\n'));
InspectorTest.log('');
}
}
logSourceLocations(locations) {
if (locations.length == 0) return Promise.resolve();
return this.logSourceLocation(locations[0]).then(() => this.logSourceLocations(locations.splice(1)));
}
async logBreakLocations(inputLocations) {
let locations = inputLocations.slice();
let scriptId = locations[0].scriptId;
let script = await this.getScriptWithSource(scriptId);
let lines = script.scriptSource.split('\n');
locations = locations.sort((loc1, loc2) => {
if (loc2.lineNumber !== loc1.lineNumber) return loc2.lineNumber - loc1.lineNumber;
return loc2.columnNumber - loc1.columnNumber;
});
for (let location of locations) {
let line = lines[location.lineNumber];
line = line.slice(0, location.columnNumber) + locationMark(location.type) + line.slice(location.columnNumber);
lines[location.lineNumber] = line;
}
lines = lines.filter(line => line.indexOf('//# sourceURL=') === -1);
InspectorTest.log(lines.join('\n') + '\n');
return inputLocations;
function locationMark(type) {
if (type === 'return') return '|R|';
if (type === 'call') return '|C|';
if (type === 'debuggerStatement') return '|D|';
return '|_|';
}
}
async logTypeProfile(typeProfile, source) {
let entries = typeProfile.entries;
// Sort in reverse order so we can replace entries without invalidating
// the other offsets.
entries = entries.sort((a, b) => b.offset - a.offset);
for (let entry of entries) {
source = source.slice(0, entry.offset) + typeAnnotation(entry.types) +
source.slice(entry.offset);
}
InspectorTest.log(source);
return typeProfile;
function typeAnnotation(types) {
return `/*${types.map(t => t.name).join(', ')}*/`;
}
}
logAsyncStackTrace(asyncStackTrace) {
while (asyncStackTrace) {
InspectorTest.log(`-- ${asyncStackTrace.description || '<empty>'} --`);
this.logCallFrames(asyncStackTrace.callFrames);
if (asyncStackTrace.parentId) InspectorTest.log(' <external stack>');
asyncStackTrace = asyncStackTrace.parent;
}
}
_sendCommandPromise(method, params) {
if (typeof params !== 'object')
utils.print(`WARNING: non-object params passed to invocation of method ${method}`);
if (InspectorTest._commandsForLogging.has(method))
utils.print(method + ' called');
var requestId = ++this._requestId;
var messageObject = { "id": requestId, "method": method, "params": params };
return new Promise(fulfill => this.sendRawCommand(requestId, JSON.stringify(messageObject), fulfill));
}
_setupProtocol() {
return new Proxy({}, { get: (target, agentName, receiver) => new Proxy({}, {
get: (target, methodName, receiver) => {
const eventPattern = /^on(ce)?([A-Z][A-Za-z0-9]+)/;
var match = eventPattern.exec(methodName);
if (!match)
return args => this._sendCommandPromise(`${agentName}.${methodName}`, args || {});
var eventName = match[2];
eventName = eventName.charAt(0).toLowerCase() + eventName.slice(1);
if (match[1])
return numOfEvents => this._waitForEventPromise(
`${agentName}.${eventName}`, numOfEvents || 1);
return listener => this._eventHandlers.set(`${agentName}.${eventName}`, listener);
}
})});
}
_dispatchMessage(messageString) {
var messageObject = JSON.parse(messageString);
if (InspectorTest._dumpInspectorProtocolMessages)
utils.print("backend: " + JSON.stringify(messageObject));
const kMethodNotFound = -32601;
if (messageObject.error && messageObject.error.code === kMethodNotFound) {
InspectorTest.log(`Error: Called non-existent method. ${
messageObject.error.message} code: ${messageObject.error.code}`);
InspectorTest.completeTest();
}
try {
var messageId = messageObject["id"];
if (typeof messageId === "number") {
var handler = this._dispatchTable.get(messageId);
if (handler) {
handler(messageObject);
this._dispatchTable.delete(messageId);
}
} else {
var eventName = messageObject["method"];
var eventHandler = this._eventHandlers.get(eventName);
if (this._scriptMap && eventName === "Debugger.scriptParsed")
this._scriptMap.set(messageObject.params.scriptId, JSON.parse(JSON.stringify(messageObject.params)));
if (eventName === "Debugger.scriptParsed" && messageObject.params.url === "wait-for-pending-tasks.js")
return;
if (eventHandler)
eventHandler(messageObject);
}
} catch (e) {
InspectorTest.log("Exception when dispatching message: " + e + "\n" + e.stack + "\n message = " + JSON.stringify(messageObject, null, 2));
InspectorTest.completeTest();
}
};
_waitForEventPromise(eventName, numOfEvents) {
let events = [];
return new Promise(fulfill => {
this._eventHandlers.set(eventName, result => {
--numOfEvents;
events.push(result);
if (numOfEvents === 0) {
delete this._eventHandlers.delete(eventName);
fulfill(events.length > 1 ? events : events[0]);
}
});
});
}
};
InspectorTest.runTestSuite = function(testSuite) {
function nextTest() {
if (!testSuite.length) {
InspectorTest.completeTest();
return;
}
var fun = testSuite.shift();
InspectorTest.log("\nRunning test: " + fun.name);
fun(nextTest);
}
nextTest();
}
InspectorTest.runAsyncTestSuite = async function(testSuite) {
const selected = testSuite.filter(test => test.name.startsWith('f_'));
if (selected.length)
testSuite = selected;
for (var test of testSuite) {
InspectorTest.log("\nRunning test: " + test.name);
try {
await test();
} catch (e) {
utils.print(e.stack);
}
}
InspectorTest.completeTest();
}
InspectorTest.start = function(description) {
try {
InspectorTest.log(description);
var contextGroup = new InspectorTest.ContextGroup();
var session = contextGroup.connect();
return { session: session, contextGroup: contextGroup, Protocol: session.Protocol };
} catch (e) {
utils.print(e.stack);
}
}
// Loaded from 'test/inspector/debugger/step-into-break-on-async-call.js':
// Copyright 2017 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.
let {session, contextGroup, Protocol} =
InspectorTest.start('Test for Debugger.stepInto with breakOnAsyncCall.');
InspectorTest.runAsyncTestSuite([
async function testSetTimeout() {
Protocol.Debugger.enable();
Protocol.Debugger.pause();
let pausedPromise = Protocol.Debugger.oncePaused();
Protocol.Runtime.evaluate({ expression: 'setTimeout(() => 42, 0)//# sourceURL=setTimeout.js' });
await pausedPromise;
Protocol.Debugger.stepInto({breakOnAsyncCall: true});
await Protocol.Debugger.oncePaused();
},
async function testPromiseThen() {
Protocol.Debugger.setAsyncCallStackDepth({maxDepth: 1});
Protocol.Runtime.evaluate({expression: 'Promise.resolve().then(() => 21)//# sourceURL=promiseThen.js'});
}
]);