Skip to content

process.permission.drop('inspector') aborts Node with a HandleScope fatal during exit #66973

Open

Description

@nick-inkeep

Version

v26.12.1 (same result on v26.10.0 and v26.11.1; also reproduced on v24.22.1)

Platform

Darwin 27.0.0 arm64

Subsystem

Permission Model / inspector

What steps will reproduce the bug?

This uses only built-in Node APIs. Save as repro.mjs and run node repro.mjs.

import { spawn } from 'node:child_process';

const child = spawn(process.execPath, [
  '--inspect-wait=127.0.0.1:0',
  '--permission',
  '--allow-inspector',
  '--allow-fs-read=*',
  '--allow-fs-write=*',
  '--eval',
  "process.permission.drop('inspector'); console.log('clean-body');",
], { stdio: ['ignore', 'pipe', 'pipe'] });

let stderr = '';
let socket;
let nextId = 0;

child.stderr.on('data', (chunk) => {
  stderr += chunk;
  const endpoint = stderr.match(
    /Debugger listening on (ws:\/\/127\.0\.0\.1:\d+\/\S+)/
  )?.[1];

  if (endpoint && !socket) {
    socket = new WebSocket(endpoint);
    socket.onopen = () => {
      for (const method of [
        'Debugger.enable',
        'Runtime.enable',
        'Runtime.runIfWaitingForDebugger',
      ]) {
        socket.send(JSON.stringify({ id: ++nextId, method }));
      }
    };
  }

  if (stderr.includes('Waiting for the debugger to disconnect')) {
    socket?.close();
  }
});

child.on('close', (code, signal) => {
  console.error(stderr);
  console.log({ code, signal });
});

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

The dropped-permission cases reproduced in every supported-version run observed: 3 of 5 cases per run on v24.21.0, v24.22.1, v26.10.0, v26.11.1, and v26.12.1. The required condition is that the process starts with --permission --allow-inspector, then calls process.permission.drop('inspector') before inspector cleanup.

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

Dropping a permission should affect future access checks and report a denied operation as ERR_ACCESS_DENIED; the process should then exit cleanly. The Permission Model documentation describes enforce mode as throwing ERR_ACCESS_DENIED for denied operations and says permission.drop() affects future checks: doc/api/permissions.md at v26.12.1, doc/api/permissions.md at v26.12.1.

What do you see instead?

The child aborts with SIGABRT:

FATAL ERROR: v8::HandleScope::CreateHandle() Cannot create a handle without a HandleScope
...
node::permission::CreateAccessDeniedError
node::permission::Permission::ThrowAccessDenied
node::inspector::Agent::WaitForDisconnect
node::Environment::RunAtExitCallbacks

The control cases that retain inspector permission exit 0. A dropped-permission case that first prints an expected ERR_ACCESS_DENIED still aborts during exit.

Additional information

At v26.12.1, the inspector registers an AtExit callback that calls Agent::WaitForDisconnect() (src/inspector_agent.cc:896-901). WaitForDisconnect() begins with the inspector permission check (src/inspector_agent.cc:1053-1056). The permission macro calls ThrowAccessDenied (src/permission/permission.h:23-36), which constructs the V8 error (src/permission/permission.cc:259-288). The AtExit callback has no HandleScope, matching the fatal stack above.

The current main source at b12bad4 retains the same AtExit and WaitForDisconnect() permission-check path. The v26 changelog has no changes after v26.12.1 that mention this path.

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