Skip to content

inspector: Runtime.evaluate with the contextId of a just-collected vm context segfaults #67138

Open

Description

@daviwil

Version

v22.23.4, v24.22.2, v26.13.0 with the repro below; v24.21.0 in the backtrace further down

Platform

Linux x64 (Ubuntu 22.04 container and Debian bookworm node images), Linux arm64 (node:26-slim)

Subsystem

inspector

What steps will reproduce the bug?

target.js:

const vm = require('node:vm');
let n = 0;
function step() {
  vm.runInNewContext('1');
  let junk = [];
  const t = Date.now();
  while (Date.now() - t < 3) {
    junk.push({ n });
    if (junk.length > 1e5) junk = [];
  }
  if (++n < 2000) setImmediate(step);
  else console.log('finished', n);
}
step();

client.mjs, in the same directory:

import { spawn } from 'node:child_process';

const child = spawn(process.execPath, ['--inspect-brk=127.0.0.1:0', new URL('target.js', import.meta.url).pathname], { stdio: ['ignore', 'inherit', 'pipe'] });
let ws;
child.stderr.on('data', (d) => {
  const url = String(d).match(/ws:\/\/\S+/);
  if (url && !ws) connect(url[0]);
  if (/Waiting for the debugger to disconnect/.test(d)) ws.close();
});
child.on('exit', (code, signal) => console.log(`target exited: code=${code} signal=${signal}`));

function connect(url) {
  ws = new WebSocket(url);
  let id = 0;
  const send = (method, params = {}) => ws.send(JSON.stringify({ id: ++id, method, params }));
  ws.onmessage = ({ data }) => {
    const msg = JSON.parse(data);
    if (msg.method === 'Runtime.executionContextCreated' && msg.params.context.id > 1) {
      send('Runtime.evaluate', { expression: '1', contextId: msg.params.context.id });
    }
  };
  ws.onopen = () => {
    send('Runtime.enable');
    send('Runtime.runIfWaitingForDebugger');
  };
}

Run node client.mjs.

How often does it reproduce? Is there a required condition?

Most runs on Linux: 14 of 15 on x64 (Docker on Apple Silicon) across 22, 24 and 26, and 9 of 10 on arm64 with 26.13.0. Without the Runtime.evaluate it always finishes. It didn't reproduce on macOS arm64 with v22.19.0 in 2 runs.

What is the expected behavior? Why is that the expected behavior?

finished 2000 and exit code 0. An evaluate in a context that no longer exists should fail with "Cannot find context with specified id".

What do you see instead?

target exited: code=null signal=SIGSEGV.

Additional information

Backtrace from a real crash (v24.21.0, x64):

#0  v8::Context::Enter()
#1  v8_inspector::InjectedScript::Scope::initialize()
#2  v8_inspector::V8RuntimeAgentImpl::evaluate(...)
...
#9  node::inspector::MainThreadInterface::DispatchMessages()
#14 v8::internal::StackGuard::HandleInterrupts(...)

The context pointer is null. In V8's src/inspector/inspected-context.cc (main today), the weak callback resetContext empties the context handle and posts a foreground task to discard the context. Until that task runs, session->findInjectedScript still finds the context id, and InjectedScript::Scope::initialize calls m_injectedScript->context()->context()->Enter() without checking for an empty handle. An evaluate dispatched by interrupt in that window crashes. If that's right the fix belongs in V8; happy to file it there instead.

We hit it through VS Code's JavaScript debugger, which evaluates a hook in every new context once a breakpoint is set, while require('repl') (pulled in by ts-node) creates a throwaway context: (js-debug issue link).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions