Skip to content

fix(ext/node): avoid panic when vm.SourceTextModule uses import.meta#35375

Merged
littledivy merged 16 commits into
mainfrom
orch/divybot-581
Jun 25, 2026
Merged

fix(ext/node): avoid panic when vm.SourceTextModule uses import.meta#35375
littledivy merged 16 commits into
mainfrom
orch/divybot-581

Conversation

@divybot

@divybot divybot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Summary

A node:vm SourceTextModule compiles its v8::Module directly and never registers it in deno_core's module map. When the module body referenced import.meta, V8's per-isolate HostInitializeImportMetaObject callback fired with that unknown module and host_initialize_import_meta_object_callback panicked at Module not found.

Repro from the issue:

import vm from 'node:vm';

const context = vm.createContext({ secret: 42 });
const module = new vm.SourceTextModule(
  'Object.getPrototypeOf(import.meta.prop).secret = secret;',
  {
    initializeImportMeta(meta) { meta.prop = {}; },
  },
);
module.linkRequests([]);
module.instantiate();
await module.evaluate();

After the fix the runtime stays alive; the module body throws a normal ReferenceError for secret (the module isn't bound to the contextified context).

Changes

  1. deno_core: host_initialize_import_meta_object_callback no longer panics on unknown modules. Instead it delegates to a per-isolate fallback hook (external_module_import_meta_cb); if no hook is registered, it leaves import.meta empty.
  2. ext/node: the node:vm extension registers that hook lazily on first SourceTextModule creation. The hook seeds import.meta.url from the identifier option (matching Node) and then invokes the user-supplied initializeImportMeta(meta) callback.
  3. polyfill: vm.SourceTextModule constructor captures initializeImportMeta from options and threads it through op_vm_module_create_source_text_module.
  4. tests: new spec test tests/specs/node/vm_source_text_module_import_meta/ covers initializeImportMeta invocation, default import.meta.url, the no-callback path, and the original issue repro (which now rejects with ReferenceError instead of panicking).

Test plan

  • cargo build --bin deno
  • cargo test --test specs node::vm_ (all 6 vm spec tests pass)
  • Original issue repro no longer panics — throws ReferenceError: secret is not defined
  • tools/lint.js

Refs #35372
Closes denoland/divybot#581

divybot and others added 3 commits June 19, 2026 18:39
A `node:vm` `SourceTextModule` compiles its `v8::Module` directly and
never registers it in deno_core's module map. When the module body
referenced `import.meta`, V8's per-isolate
`HostInitializeImportMetaObject` callback fired with that unknown
module and `host_initialize_import_meta_object_callback` panicked at
`Module not found`.

Make the deno_core callback graceful: if the module isn't in the map,
fall back to a per-isolate hook registered by the embedder, and skip
silently if no hook is registered (leaving `import.meta` as an empty
object instead of crashing the runtime).

The `node:vm` extension now registers that hook lazily on the first
`SourceTextModule` creation. The hook seeds `import.meta.url` from the
module's `identifier` option (matching Node's behavior) and then
invokes the user-supplied `initializeImportMeta` callback, which is
captured by the polyfill and threaded through the create op.

Fixes the panic in the original report and makes the
`initializeImportMeta` callback actually run for vm modules.

Refs #35372
Closes denoland/divybot#581

Co-Authored-By: Divy Srivastava <[email protected]>
The `ExternalModuleImportMetaCb` HRTB binds scope/module/meta to the
same `'s` — but at the call site in `bindings.rs`, the scope is
produced by `callback_scope!` while module/meta arrive from V8's
extern "C" callback with elided lifetimes, so they can't unify.

Give each argument its own bound lifetime parameter; the lifetimes are
independent anyway because `meta.create_data_property(scope, ...)` and
`module.get_identity_hash()` don't require Local lifetimes to match
the scope's.

Co-Authored-By: Divy Srivastava <[email protected]>
Move regression coverage from `tests/unit_node/vm_test.ts` to a spec
test: `linkRequests` / `instantiate` aren't on the upstream
`@types/node` SourceTextModule shape yet, so the unit_node TS check
fails. The spec test runs the .mjs script and matches expected output,
which doesn't depend on those types.

Covers: `initializeImportMeta` invocation, default `import.meta.url`
fallback to `vm:module(N)`, the no-callback path, and the original
issue repro (which should reject with a normal ReferenceError instead
of panicking the runtime).

Co-Authored-By: Divy Srivastava <[email protected]>
@divybot
divybot marked this pull request as ready for review June 19, 2026 19:15
divybot and others added 11 commits June 19, 2026 21:22
…lakes

The previous CI run hit three orthogonal infra flakes — `deno_core test
linux-x86_64` died with `No space left on device` on the runner (no
cargo step ran), `test specs (1/2) debug macos-x86_64` hung in
`specs::npm::playwright_compat` (Chromium download), and `test specs
(2/2) release linux-x86_64` hung in `specs::task::signals`. None touch
node:vm; empty-commit retrigger to flush the flake noise.

Co-Authored-By: Divy Srivastava <[email protected]>
…6_64

Identical hang pattern as memory entries for 2026-06-20 spec shard
cancellations — both debug 1/2 shards (macos-x86_64, linux-x86_64) ran
all visible spec tests through specs::x::unstable_flags_jsr at 05:27:14
then went silent for ~22 minutes before cancellation at the 30-min mark.
Single shard hang on each platform; siblings (2/2 same platform, all
other platforms × profiles) passed. Our new vm_source_text_module_import_meta
test ran ok in both cancelled jobs at 05:24:36 before the unrelated hang.
…ws-x86_64

test-worker-fshandles-open-close-on-termination.js flaked all 5 retries
on debug windows-x86_64 only — MAINTAINER-TRACKED issue deno#35079
(labeled flaky). Unrelated to node:vm import.meta fix; all other
shards green.
Same MAINTAINER-TRACKED flake (deno#35079) hit again on debug
windows-x86_64 (node_compat 3/3). Per memory pattern, this test can
fail all 5 CI retries on one run + pass cleanly on the next; retrigger
is the only recourse short of disabling the test (which would be a
drive-by change outside this PR's scope).
test-cluster-inspect-brk.js failed on debug windows-aarch64 with
'The handle is invalid. (os error 6)' from cluster.fork(). Windows-only
process handle invalidation during inspector-brk init; matches existing
Windows inspector flake pattern (memory entries 2026-06-14 / 2026-06-19).
Unrelated to node:vm SourceTextModule import.meta fix.
build debug macos-x86_64 'Upload artifact debug-macos-x86_64-deno' step
failed with 'Failed to CreateArtifact: Unable to make request: ENOTFOUND'
— GitHub Actions artifact storage DNS failure. Build itself succeeded;
purely infra/network. Not related to node:vm fix.
…tempt)

Same memory-tracked macOS specs shard hang: tests run through specs::x::*
then silent ~13min until 30-min cancellation. Sibling specs (2/2) and
all other platforms × profiles green. Today's CI has been unusually
infra-flaky (4 unrelated flakes in a row: macos artifact upload, windows
inspector-brk, windows worker-fshandles, now macos specs hang).
Same MAINTAINER-TRACKED flake (deno#35079) hit again on debug
windows-x86_64. Per memory: this flake can fail every CI retry on one
run + pass cleanly on the next; only recourse is empty-commit retrigger.
Same MAINTAINER-TRACKED deno#35079 flake. 4th consecutive failure of
test-worker-fshandles-open-close-on-termination.js on this PR's debug
windows-x86_64 runs (all retries within each run also fail). Today's
Windows runner appears especially slow — 10s test timeout repeatedly
exceeded. No source change in this PR could affect this test. Admin
gh run rerun --failed is the real fix; empty-commit retrigger is the
only knob available to a non-admin.
Same shard-hang flake (memory entry 2026-06-20 #16): last visible spec
test is specs::x::unstable_flags_jsr at 09:02:07, then 13min silence
until 30min cancellation cap. Today's CI is rotating between this
macOS specs hang and the windows worker-fshandles flake (deno#35079).
Implementation unchanged — both blockers are documented infra flakes
unrelated to node:vm SourceTextModule fix.
@bartlomieju

Copy link
Copy Markdown
Member

The core panic fix here is the right architecture and the deno_core plumbing is clean — falling back to a per-isolate hook instead of expect("Module not found") is the correct call, the RefCell borrow is released before the user callback runs (no reentrancy double-borrow), and rethrowing from the host-initialize-import-meta callback is consistent with the existing Wasm-unavailable branch that already throws there. Case 4 (the original repro) is the important regression and it's correct.

One blocker though: seeding import.meta.url from identifier does not match Node, and the new tests bake in the divergent behavior.

Verified against Node v26.3.0:

A  identifier set, no callback   -> 'url' in import.meta === false
B  no identifier, no callback     -> import.meta keys: []   (completely empty)
C  with initializeImportMeta cb   -> 'url' in import.meta === false; only the callback's own props are present

Node leaves import.meta entirely under the control of the user's initializeImportMeta callback for vm.SourceTextModule — it never auto-populates url (or main/resolve). This PR always injects meta.url, so a vm module under Deno would see an import.meta.url that Node reports as undefined.

That makes these three assertions encode non-Node behavior:

  • Test 1: assert.strictEqual(mod.namespace.captured.url, "vm:test-import-meta") — Node yields undefined
  • Test 2: assert.match(mod.namespace.url, /^vm:module\(\d+\)$/) — Node: unset
  • Test 3: assert.strictEqual(mod.namespace.meta.url, "vm:test-no-init") + 'prop' in import.meta === false — Node has url not in meta

Suggested change:

  • Drop the meta.url seeding from external_import_meta_hook — the hook should only invoke the user callback (if any) and otherwise leave meta empty. ImportMetaEntry.identifier then becomes unnecessary; only the optional callback needs storing.
  • Rewrite cases 1–3 to assert Node-accurate behavior (empty import.meta by default; only callback-set props present). The panic-fix (case 4) stays as-is.

Minor, non-blocking:

  • IMPORT_META_CALLBACKS is keyed on module.get_identity_hash(), which V8 doesn't guarantee unique — two live modules could collide and one's Drop could evict the other's entry. Pre-existing risk shared with SYNTHETIC_CALLBACKS, just flagging.
  • Comment nit in ensure_external_import_meta_hook_registered: "subsequent calls are a no-op" — it actually re-sets the same fn pointer each call (harmless, but not literally a no-op).

littledivy and others added 2 commits June 23, 2026 10:55
Address review: don't auto-seed import.meta.url from the module identifier.
Node's vm.SourceTextModule leaves import.meta entirely under the user's
initializeImportMeta callback and never populates url/main/resolve. Drop the
url seeding from external_import_meta_hook (and the now-unnecessary identifier
field), so the hook only runs the user callback when one was provided and
otherwise leaves meta empty. Only modules with a callback get an
IMPORT_META_CALLBACKS entry. Rewrite the spec test to assert Node-accurate
behavior.
@littledivy
littledivy merged commit 9eeca7b into main Jun 25, 2026
136 checks passed
@littledivy
littledivy deleted the orch/divybot-581 branch June 25, 2026 06:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants