From e0fef75fd3d693b4a9136b3b106a911c4c7d4971 Mon Sep 17 00:00:00 2001 From: Christian Aurich Date: Tue, 8 Sep 2026 12:35:54 -0300 Subject: [PATCH] inspector: allow JS when delivering to JS sessions An in-process inspector.Session delivers protocol messages by calling into JavaScript, and V8 produces some of them while it asserts that JavaScript is not executed: Debugger.paused is broadcast from the Isolate::RequestInterrupt handler that Debugger.pause schedules, before V8 opens its own AllowJavascriptExecutionScope for the pause message loop. So the process aborts with "Invoke in DisallowJavascriptExecutionScope" as soon as a debugger client pauses it while JavaScript is running. Open an AllowJavascriptExecutionScope on the synchronous delivery path, the same opt-out d8's own inspector channel uses for every protocol message it delivers, see InspectorFrontend::Send() in deps/v8/src/d8/d8.cc. The messages produced where calling into JavaScript is really unsafe, such as GC callbacks, take the queueing branch instead and are delivered from a point where it is allowed again. Signed-off-by: Christian Aurich --- src/inspector_js_api.cc | 8 +++ .../test-inspector-pause-with-js-session.js | 53 +++++++++++++++++++ 2 files changed, 61 insertions(+) create mode 100644 test/parallel/test-inspector-pause-with-js-session.js diff --git a/src/inspector_js_api.cc b/src/inspector_js_api.cc index 09f0d7e84ce6..0821e8dfd555 100644 --- a/src/inspector_js_api.cc +++ b/src/inspector_js_api.cc @@ -132,6 +132,14 @@ class JSBindingsConnection : public BaseObject { Local argument; if (!ToV8Value(env()->context(), message, isolate).ToLocal(&argument)) return; + // An in-process inspector channel delivers protocol messages by calling + // into JavaScript. Debugger.paused can be emitted from a V8 + // RequestInterrupt handler, before V8 opens its own + // AllowJavascriptExecutionScope for the pause message loop. d8's inspector + // channel makes the same opt-out in InspectorFrontend::Send(). Messages + // emitted where calling into JavaScript is unsafe, such as GC callbacks, + // take the queueing path above instead. + Isolate::AllowJavascriptExecutionScope allow_js(isolate); OnMessage(argument); } diff --git a/test/parallel/test-inspector-pause-with-js-session.js b/test/parallel/test-inspector-pause-with-js-session.js new file mode 100644 index 000000000000..b509d30c125f --- /dev/null +++ b/test/parallel/test-inspector-pause-with-js-session.js @@ -0,0 +1,53 @@ +'use strict'; +const common = require('../common'); + +common.skipIfInspectorDisabled(); + +const assert = require('assert'); +const { NodeInstance } = require('../common/inspector-helper.js'); + +// An in-process session observes Debugger.paused while the process is paused, +// so its callback runs from the Isolate::RequestInterrupt handler that +// Debugger.pause schedules, where V8 asserts that JS is not executed. +const script = ` +const { Session } = require('inspector'); +const session = new Session(); +let paused = false; +session.on('Debugger.paused', () => { + paused = true; +}); +session.connect(); +session.post('Debugger.enable'); +console.log('Ready'); +// Spin so that the pause is requested while JS is on the stack, which is what +// makes V8 break from the interrupt handler instead of on the next call. +const deadline = Date.now() + ${common.platformTimeout(10000)}; +while (!paused && Date.now() < deadline); +console.log(paused ? 'Notified' : 'Not notified'); +`; + +async function runTest() { + const child = new NodeInstance(undefined, script); + const session = await child.connectInspectorSession(); + await session.send({ method: 'NodeRuntime.enable' }); + await session.waitForNotification('NodeRuntime.waitingForDebugger'); + await session.send([ + { 'method': 'Runtime.enable' }, + { 'method': 'Debugger.enable' }, + ]); + await session.send({ method: 'Runtime.runIfWaitingForDebugger' }); + await session.send({ method: 'NodeRuntime.disable' }); + await session.waitForNotification('Debugger.paused', 'Break on start'); + await session.send({ 'method': 'Debugger.resume' }); + await session.waitForConsoleOutput('log', ['Ready']); + + await session.send({ 'method': 'Debugger.pause' }); + await session.waitForNotification('Debugger.paused', 'Paused'); + await session.send({ 'method': 'Debugger.resume' }); + await session.waitForConsoleOutput('log', ['Notified']); + + await session.waitForDisconnect(); + assert.strictEqual((await child.expectShutdown()).exitCode, 0); +} + +runTest().then(common.mustCall());