Skip to content

Conversation

ericrannaud
Copy link

@ericrannaud ericrannaud commented Sep 8, 2025

The underlying behavior conforms to the spec, see Refs. We need a workaround on the Node side to at least enable the use of await on the return value of module.evaluate().

This series first adds a test that confirm the current incorrect behavior: I wrote the test so it passes with the current code.

Then I introduce a workaround (credit for the idea goes to @addaleax) and update the tests to verify that the behavior is now correct.

Then I add a subsection to doc/vm.md to explain how it can be a challenge to share promises between contexts with afterEvaluate; I also add more tests to demonstrate how promises behave in this case.

Fixes: #59541
Refs: https://issues.chromium.org/issues/441679231
Refs: https://groups.google.com/g/v8-dev/c/YIeRg8CUNS8/m/rEQdFuNZAAAJ
Refs: https://tc39.es/ecma262/#sec-newpromiseresolvethenablejob

@nodejs-github-bot
Copy link
Collaborator

Review requested:

  • @nodejs/loaders
  • @nodejs/vm

@nodejs-github-bot nodejs-github-bot added c++ Issues and PRs that require attention from people who are familiar with C++. lib / src Issues and PRs related to general changes in the lib or src directory. needs-ci PRs that need a full CI run. labels Sep 8, 2025
@ericrannaud ericrannaud force-pushed the vm-after-evaluate-link-queues branch from 9f3b583 to 9cb3575 Compare September 8, 2025 18:54
Copy link

codecov bot commented Sep 8, 2025

Codecov Report

❌ Patch coverage is 93.93939% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 88.28%. Comparing base (26607a6) to head (9cb3575).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
src/node_contextify.cc 92.30% 0 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #59801      +/-   ##
==========================================
+ Coverage   88.27%   88.28%   +0.01%     
==========================================
  Files         701      701              
  Lines      206753   206784      +31     
  Branches    39757    39760       +3     
==========================================
+ Hits       182512   182565      +53     
+ Misses      16285    16251      -34     
- Partials     7956     7968      +12     
Files with missing lines Coverage Δ
src/module_wrap.cc 74.59% <100.00%> (+0.08%) ⬆️
src/node_contextify.h 84.21% <100.00%> (+13.62%) ⬆️
src/node_contextify.cc 81.54% <92.30%> (+0.23%) ⬆️

... and 31 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@ericrannaud ericrannaud marked this pull request as draft September 8, 2025 23:00
Check that we lose the execution flow in the outer context, upon
resolving a promise created in in the inner context.
… outer context

Consider the default context A with a microtask queue QA, and a
context B with its own microtask queue QB.

Context B is constructed with vm.createContext(..., {microtaskMode:
"afterEvaluate"}). The evaluation in context B can be performed via
vm.Script or vm.SourceTextModule.

The standard (https://tc39.es/ecma262/#sec-newpromiseresolvethenablejob)
dictates that, when resolving a {promise} with {resolution}, from any
context, the {then} method on {promise} should be called within a task
enqueued on the microtask queue from the context associated with {then}.

Specifically, after evaluating a script or module in context B, any
promises created within B, if later resolved within A, will result in a
task to be enqueued back onto QB, even long after we are done evaluating
any code within B.

This creates a challenge for users of node:vm in "afterEvaluate" mode.
In ContextifyScript::EvalMachine() and in ModuleWrap::Evaluate(), we
only drain the microtask queue QB a single time after running the script
or evaluating the module. After that point, the queue will not be
drained unless another script or module is evaluated in the same
context.

In the following scenario, prior to this patch, the log statement will
not be printed:

	const microtaskMode = "afterEvaluate";
	const context = vm.createContext({}, {microtaskMode});
	const source = "";
	const module = new vm.SourceTextModule(source, {context});
	await module.link(() => null);
	await module.evaluate();
	console.log("NOT PRINTED");

To resolve the promise returned by evaluate(), which technically is the
top-level capability for {module}, a promise created within B, V8 will
enqueue a task on QB. But since this is after the PerformCheckpoint()
call in ModuleWrap::Evaluate(), the task in QB is never run. In the
meantime, since QA is empty, the Node process simply exits (with a
warning about the unsettled promise, if it happened to be a top-level
await).

While being unable to do `await module.evaluate()` is clearly a problem,
more generally, it is intended that in "afterEvaluate" mode, promises
created in the inner context cannot make progress if, and until, the
microtask queue of the inner context is checkpointed.

Therefore, to address this issue, the fix is narrow:

When the module has its own microtask queue, i.e. in "afterEvaluate"
mode, the inner-context promise returned by
v8::SourceTextModule::Evaluate() is first resolved to an outer-context
promise, then we checkpoint the microtask queue of the inner context,
then we return the outer-context promise we just built.

This ensures that in the statement `await module.evaluate()`, the
promise returned by `evaluate()` can be resolved within the outer
context, without involving the microtask queue in the inner context.

Fixes: nodejs#59541
Refs: https://issues.chromium.org/issues/441679231
Refs: https://groups.google.com/g/v8-dev/c/YIeRg8CUNS8/m/rEQdFuNZAAAJ
@ericrannaud ericrannaud force-pushed the vm-after-evaluate-link-queues branch from 9cb3575 to abf9d31 Compare September 10, 2025 19:38
@ericrannaud ericrannaud marked this pull request as ready for review September 10, 2025 19:39
@ericrannaud ericrannaud changed the title vm: with mode "afterEvaluate", must keep checkpointing separate queue vm: "afterEvaluate", make module.evaluate() return a promise from the outer context Sep 10, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
c++ Issues and PRs that require attention from people who are familiar with C++. lib / src Issues and PRs related to general changes in the lib or src directory. needs-ci PRs that need a full CI run.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

SourceTextModule w/ microtaskMode "afterEvaluate" breaks program execution flow
4 participants