From 54c3344edc20bb1910bfb8a38ab28c0face5bee6 Mon Sep 17 00:00:00 2001 From: Benedikt Meurer Date: Mon, 21 Mar 2022 12:45:21 +0100 Subject: [PATCH] [inspector-test] Gracefully handle termination. Reporting messages can trigger termination in case of `inspector-test`, which we need to be able to deal with gracefully for the fuzzer. Fixed: chromium:1307449 Change-Id: I88ba2b13d920134a1670b808adc4ace4ca6d1dff Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/3540260 Commit-Queue: Benedikt Meurer Auto-Submit: Benedikt Meurer Reviewed-by: Yang Guo Commit-Queue: Yang Guo Cr-Commit-Position: refs/heads/main@{#79543} --- test/fuzzer/inspector/regress-1307449 | 529 ++++++++++++++++++++++++++ test/inspector/isolate-data.cc | 14 +- 2 files changed, 539 insertions(+), 4 deletions(-) create mode 100644 test/fuzzer/inspector/regress-1307449 diff --git a/test/fuzzer/inspector/regress-1307449 b/test/fuzzer/inspector/regress-1307449 new file mode 100644 index 0000000000..aa086c58ba --- /dev/null +++ b/test/fuzzer/inspector/regress-1307449 @@ -0,0 +1,529 @@ +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._commandsForLogging.add(command); +} + +InspectorTest.completeTest = function() { + var promises = []; + for (var session of InspectorTest._sessions) + promises.push(session.Protocol.Debugger.disable()); + Promise.all(promises).then(() => utils.quit()); +} + +InspectorTest.waitForPendingTasks = function() { + var promises = []; + for (var session of InspectorTest._sessions) + promises.push(session.Protocol.Runtime.evaluate({ expression: "new Promise(r => setTimeout(r, 0))//# sourceURL=wait-for-pending-tasks.js", awaitPromise: true })); + return Promise.all(promises); +} + +InspectorTest.startDumpingProtocolMessages = function() { + InspectorTest._dumpInspectorProtocolMessages = true; +} + +InspectorTest.logMessage = function(originalMessage) { + const nonStableFields = new Set([ + 'objectId', 'scriptId', 'exceptionId', 'timestamp', 'executionContextId', + 'callFrameId', 'breakpointId', 'bindRemoteObjectFunctionId', + 'formatterObjectId', 'debuggerId', 'bodyGetterId', 'uniqueId' + ]); + const message = JSON.parse(JSON.stringify(originalMessage, replacer.bind(null, Symbol(), nonStableFields))); + if (message.id) + message.id = ''; + + InspectorTest.logObject(message); + return originalMessage; + + function replacer(stableIdSymbol, nonStableFields, name, val) { + if (nonStableFields.has(name)) + return `<${name}>`; + if (name === 'internalProperties') { + const stableId = val.find(prop => prop.name === '[[StableObjectId]]'); + if (stableId) + stableId.value[stableIdSymbol] = true; + } + if (name === 'parentId') + return { id: '' }; + if (val && val[stableIdSymbol]) + return ''; + return val; + } +} + +InspectorTest.logObject = function(object, title) { + var lines = []; + + function dumpValue(value, prefix, prefixWithName) { + if (typeof value === "object" && value !== null) { + if (value instanceof Array) + dumpItems(value, prefix, prefixWithName); + else + dumpProperties(value, prefix, prefixWithName); + } else { + lines.push(prefixWithName + String(value).replace(/\n/g, " ")); + } + } + + function dumpProperties(object, prefix, firstLinePrefix) { + prefix = prefix || ""; + firstLinePrefix = firstLinePrefix || prefix; + lines.push(firstLinePrefix + "{"); + + var propertyNames = Object.keys(object); + propertyNames.sort(); + for (var i = 0; i < propertyNames.length; ++i) { + var name = propertyNames[i]; + if (!object.hasOwnProperty(name)) + continue; + var prefixWithName = " " + prefix + name + " : "; + dumpValue(object[name], " " + prefix, prefixWithName); + } + lines.push(prefix + "}"); + } + + function dumpItems(object, prefix, firstLinePrefix) { + prefix = prefix || ""; + firstLinePrefix = firstLinePrefix || prefix; + lines.push(firstLinePrefix + "["); + for (var i = 0; i < object.length; ++i) + dumpValue(object[i], " " + prefix, " " + prefix + "[" + i + "] : "); + lines.push(prefix + "]"); + } + + dumpValue(object, "", title || ""); + InspectorTest.log(lines.join("\n")); +} + +InspectorTest.decodeBase64 = function(base64) { + const LOOKUP = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; + + const paddingLength = base64.match(/=*$/)[0].length; + const bytesLength = base64.length * 0.75 - paddingLength; + + let bytes = new Uint8Array(bytesLength); + + for (let i = 0, p = 0; i < base64.length; i += 4, p += 3) { + let bits = 0; + for (let j = 0; j < 4; j++) { + bits <<= 6; + const c = base64[i + j]; + if (c !== '=') bits |= LOOKUP.indexOf(c); + } + for (let j = p + 2; j >= p; j--) { + if (j < bytesLength) bytes[j] = bits; + bits >>= 8; + } + } + + return bytes; +} + +InspectorTest.trimErrorMessage = function(message) { + if (!message.error || !message.error.data) + return message; + message.error.data = message.error.data.replace(/at position \d+/, + 'at '); + 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", + "objectHasOtion","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, { + set() { 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(); + } + + getCallFrameUrl(frame) { + const {scriptId} = frame.location ? frame.location : frame; + return (this._scriptMap.get(scriptId) ?? frame).url; + } + + logCallFrames(callFrames) { + for (var frame of callFrames) { + var functionName = frame.functionName || '(anonymous)'; + var url = this.getCallFrameUrl(frame); + 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.rformalesult.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 || ''} --`); + this.logCallFrames(asyncStackTrace.callFrames); + if (asyncStackTrace.parentId) InspectorTest.log(' '); + 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/regress/regress-crbug-1220203.js': +// Copyright 2022 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('Regression test for crbug.com/1220203.'); + +contextGroup.addScript(` +async function main() { + await 1; + throw new Error(); +}`); + +session.setupScriptMap(); + +InspectorTest.runAsyncTestSuite([ + async function testBreakOnUncaughtException() { + await Promise.all([ + Protocol.Runtime.enable(), + Protocol.Debugger.enable(), + Protocol.Debugger.setPauseOnExceptions({state: 'uncaught'}), + ]); + const pausedPromise = Protocol.Debugger.oncePaused(); + const evalPromise = Protocol.Runtime.evaluate({expression: 'main()', awaitPromise: true}); + const {params: {callFrames, data}} = await pausedPromise; + await evalPromise; + }, +]); diff --git a/test/inspector/isolate-data.cc b/test/inspector/isolate-data.cc index e698ac45dd..e26a955be6 100644 --- a/test/inspector/isolate-data.cc +++ b/test/inspector/isolate-data.cc @@ -339,10 +339,16 @@ void InspectorIsolateData::PromiseRejectHandler(v8::PromiseRejectMessage data) { int exception_id = HandleMessage( v8::Exception::CreateMessage(isolate, exception), exception); if (exception_id) { - promise - ->SetPrivate(isolate->GetCurrentContext(), id_private, - v8::Int32::New(isolate, exception_id)) - .ToChecked(); + if (promise + ->SetPrivate(isolate->GetCurrentContext(), id_private, + v8::Int32::New(isolate, exception_id)) + .IsNothing()) { + // Handling the |message| above calls back into JavaScript (by reporting + // it via CDP) in case of `inspector-test`, and can lead to terminating + // execution on the |isolate|, in which case the API call above will + // return immediately. + DCHECK(isolate->IsExecutionTerminating()); + } } }