Skip to content

Async hooks cause programs with frozen promises to crash #42229

Open
@erights

Description

@erights

Version

v16.13.1

Platform

Darwin MacBook-Pro 21.3.0 Darwin Kernel Version 21.3.0: Wed Jan 5 21:37:58 PST 2022; root:xnu-8019.80.24~20/RELEASE_ARM64_T6000 arm64

Subsystem

No response

What steps will reproduce the bug?

Type the following interactively at the Node shell.

~$ node
Welcome to Node.js v16.13.1.
Type ".help" for more information.
> const p = Promise.resolve(8);
undefined
> const names = Reflect.ownKeys(p);
undefined
> names
[
  'domain',
  Symbol(async_id_symbol),
  Symbol(trigger_async_id_symbol),
  Symbol(destroyed)
]
> for (const name of names) {
...   delete p[name];
... }
true
> Object.freeze(p);
Promise { 8 }
> const q = p.then(x => console.log(x));
Uncaught:
TypeError: Cannot add property Symbol(async_id_symbol), object is not extensible
    at getOrSetAsyncId (node:internal/async_hooks:424:34)
    at trackPromise (node:internal/async_hooks:314:35)
    at promiseInitHook (node:internal/async_hooks:322:3)
    at promiseInitHookWithDestroyTracking (node:internal/async_hooks:329:3)
    at Promise.then (<anonymous>)
> undefined
> 8

The 'domain' property is not the main concern here. Rather it is the other three symbols. Hardened JS (aka SES) avoid loading the module that would add 'domain'. However, at the vscode debug terminal, we get a result much like the above but without 'domain' showing up in the list of names. Other differences between the following and the previous section are probably due to the differences caused by Hardened JS and might or might now be relevant. Other than the expected absence of 'domain', the underlying Node bug is the same:

In the debug console:

p = Promise.resolve(8);
Promise {[[PromiseState]]: 'fulfilled', [[PromiseResult]]: 8, Symbol(async_id_symbol): 3543, Symbol(trigger_async_id_symbol): 1783, Symbol(destroyed): {}}
names = Reflect.ownKeys(p);
(3) [Symbol(async_id_symbol), Symbol(trigger_async_id_symbol), Symbol(destroyed)]
for (const name of names) {
  delete p[name];
}
true
Object.freeze(p);
Promise {[[PromiseState]]: 'fulfilled', [[PromiseResult]]: 8}
q = p.then(x => console.log(x));

That last line will terminate the debug session, with the following error appearing on vscode's JavaScript Debug Terminal

Debugger attached.
  - range queries

  Uncaught exception in test/test-rankOrder.js

  TypeError: Cannot add property Symbol(async_id_symbol), object is not extensible
    at Promise.then (<anonymous>)
    at eval (eval at <anonymous> (packages/store/test/test-rankOrder.js:94:7), <anonymous>:1:13)
    at packages/store/test/test-rankOrder.js:94:7
    at packages/store/test/test-rankOrder.js:90:12
    at async Promise.all (index 7)

  › Promise.then (<anonymous>)
  › eval (eval at <anonymous> (packages/store/test/test-rankOrder.js:94:7), <anonymous>:1:13)
  › packages/store/test/test-rankOrder.js:94:7
  › packages/store/test/test-rankOrder.js:90:12

(TypeError#1)
Waiting for the debugger to disconnect...
TypeError#1: Cannot add property Symbol(async_id_symbol), object is not extensible
  at Promise.then (<anonymous>)
  at eval (eval at <anonymous> (packages/store/test/test-rankOrder.js:94:7), <anonymous>:1:13)
  at packages/store/test/test-rankOrder.js:94:7
  at packages/store/test/test-rankOrder.js:90:12
  at async Promise.all (index 7)

  ✖ test/test-rankOrder.js exited with a non-zero exit code: 1
  ─

  1 test skipped
  1 uncaught exception
Waiting for the debugger to disconnect...
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
Waiting for the debugger to disconnect...
Waiting for the debugger to disconnect...

Putting the same code in a .js file and executing it non-interactively does not trigger the bug. The names array in that case is empty, as it should be.

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

It reproduces reliably. The explanation above should be adequate to reliably reproduce it. Please let me know if you have any trouble reproducing it.

What is the expected behavior?

names should be empty.
q should be defined as a promise that eventually fulfills to p's fulfillment, 8.

What do you see instead?

As presented above:

~$ node
Welcome to Node.js v16.13.1.
Type ".help" for more information.
> const p = Promise.resolve(8);
undefined
> const names = Reflect.ownKeys(p);
undefined
> names
[
  'domain',
  Symbol(async_id_symbol),
  Symbol(trigger_async_id_symbol),
  Symbol(destroyed)
]
> for (const name of names) {
...   delete p[name];
... }
true
> Object.freeze(p);
Promise { 8 }
> const q = p.then(x => console.log(x));
Uncaught:
TypeError: Cannot add property Symbol(async_id_symbol), object is not extensible
    at getOrSetAsyncId (node:internal/async_hooks:424:34)
    at trackPromise (node:internal/async_hooks:314:35)
    at promiseInitHook (node:internal/async_hooks:322:3)
    at promiseInitHookWithDestroyTracking (node:internal/async_hooks:329:3)
    at Promise.then (<anonymous>)
> undefined
> 8

Additional information

Googling led me to
https://stackoverflow.com/questions/70742387/where-can-i-find-any-docs-on-why-node-14-changed-promise-into-promise-undefi
which led me to
f37c26b
and
#36394

However, that PR shows Closed rather than Merged, so it may not actually be the source of the bug.

Metadata

Metadata

Assignees

No one assigned

    Labels

    async_hooksIssues and PRs related to the async hooks subsystem.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions