Repository navigation
Conversation
|
Updated 6:43 PM PT - Oct 16th, 2026
❌ @robobun, your commit 0836b83 has 1 failures in 🧪 To try this PR locally: bunx bun-pr 42496That installs a local version of the PR into your bun-42496 --bun |
|
Status
|
5e97450 to
5f4e995
Compare
Two SourceTextModules with the same identifier and source text ran one shared ModuleProgramExecutable. The second module's import() called the first module's hook, its stack traces reported the first module's lineOffset, and only the first module's hook stayed alive with the code. The WebKit bump in #44500 (oven-sh/WebKit#639) gives each module its own executable again. Add tests for the hook, the referrer, the line offset and the hook lifetime.
5f4e995 to
0836b83
Compare
|
Warning Review limit reachedYou've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Next included review available in 1 minute. View limit details
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
| } | ||
| const shifted = await lineOfThrow("same-identifier-other-offset.mjs", 100); | ||
| const unshifted = await lineOfThrow("same-identifier-other-offset.mjs", 0); | ||
| expect(unshifted).toBe(await lineOfThrow("never-seen-identifier.mjs", 0)); |
There was a problem hiding this comment.
🟡 nit (optional): a wrong lineOffset applied to the second same-identifier module (off by one, doubled, partially shifted) still passes this test. The assertion at test/js/node/vm/vm.test.ts:1144 only checks shifted > unshifted, although lineOffset 100 must shift the reported line by exactly 100. Fix: assert the exact invariant, expect(shifted).toBe(unshifted + 100), which stays independent of absolute line numbers and matches the exact-line assertions the existing lineOffset test at vm.test.ts:2544 already makes. [also at: test/js/node/vm/vm.test.ts:1146 - nit: the lineOffset test only checks shifted is greater than unshifted, so a module that applied a wrong non-zero offset would still pass.]
Why this was flagged
The test at test/js/node/vm/vm.test.ts:1130-1145 compiles two SourceTextModules with identifier "same-identifier-other-offset.mjs", lineOffset 100 then 0, and reads the line from new Error("here").stack. At vm.test.ts:1144 it asserts only expect(shifted).toBeGreaterThan(unshifted). Any regression where the offset of a shared or re-used executable is applied wrongly but still positive (for example line 2, 50 or 201 instead of 101) passes. The repository review rules ask for the strongest invariant; the sibling test at vm.test.ts:2544 asserts the exact line for lineOffset 5. The base branch has no test here, so this is a weaker-than-necessary new assertion rather than a regression.
Verification: nit. Triggering condition: any regression that applies a SourceTextModule lineOffset of 100 as some other positive shift (off by one, doubled, partial) at function-call time. Mechanism verified: /home/claude/bun/test/js/node/vm/vm.test.ts:1144 reads expect(shifted).toBeGreaterThan(unshifted);. Line 1144 accepts 2, 50 or 201 just as readily as 101.
| test("each call their own importModuleDynamically", async () => { | ||
| const first = await load("first"); | ||
| const second = await load("second"); | ||
| const calls = [ | ||
| { specifier: "dep", referrerIsThisModule: true }, | ||
| { specifier: "dep", referrerIsThisModule: true }, | ||
| ]; | ||
| expect({ first, second }).toEqual({ | ||
| first: { calls, imported: ["first", "first"] }, | ||
| second: { calls, imported: ["second", "second"] }, | ||
| }); | ||
| }); |
There was a problem hiding this comment.
🟡 nit (optional): maintainers get coverage for same-identifier executable sharing only in the main context, while the sibling test directly above runs both contexts. The new describe block at test/js/node/vm/vm.test.ts:1079 never passes a context option, so both tests only exercise the Zig global object. Fix: run the new hook and lineOffset tests over the same ["the main context", false], ["a new context", true] matrix the sibling at vm.test.ts:1033 uses, passing context to every SourceTextModule and SyntheticModule it creates. The executable map that enabled the old sharing is per JSGlobalObject, so a vm context is a separate instance of the hazard. [also at: test/js/node/vm/vm.test.ts:1079 - nit: two same-identifier modules sharing one vm.createContext() are not covered, so a regression limited to non-main contexts would pass this suite.]
Why this was flagged
The existing test at test/js/node/vm/vm.test.ts:1033-1077 covers the same-identifier-and-source hazard with test.each over the main context and a createContext({}) context, because the module executable map lives on each JSGlobalObject. The new describe at test/js/node/vm/vm.test.ts:1079 adds the hook and lineOffset variants but load() at vm.test.ts:1091 and lineOfThrow at vm.test.ts:1131 never set context, so every module lands on the main Zig::GlobalObject. A vm context is a NodeVMGlobalObject with its own executable map and its own import() entry at src/jsc/bindings/NodeVM.cpp:1700. If executable sharing came back for records on a NodeVMGlobalObject only, the second module in a context would again run the first module's importModuleDynamically hook and report the first module's lineOffset, and neither new test would fail. On the base branch there is no coverage for either context; after merging there is coverage for one of the two the sibling test treats as distinct.
Verification: A future regression of same-identifier executable sharing that is specific to vm contexts would go uncaught, because the new tests only run on the main global. The new describe block (test/js/node/vm/vm.test.ts:1079-1145) constructs every SourceTextModule and SyntheticModule without a context option. The sibling test directly above (vm.test.ts:1033-1077) runs a test.each matrix over both contexts.
Behaviour change: none
Problem
mainhas no test for a regression that Upgrade WebKit to cf1b36ec8703 #42319 introduced and the WebKit bump in Bump WebKit to 1600131e46b5 #44500 removed.vm.SourceTextModulewith the same identifier and source text as an earlier one called the first module'simportModuleDynamicallyhook onimport()(hook-calls=0 got=first want=second). Its stack traces reported the first module'slineOffset.ModuleProgramExecutable, which keeps the first module's source. [JSC] Module records share an executable only when their sources have the same SourceOrigin and start position WebKit#639 ended that for sources with differentSourceOrigins.Fix
WEBKIT_VERSIONchange is gone, becausemain's pin contains [JSC] Module records share an executable only when their sources have the same SourceOrigin and start position WebKit#639.test/js/node/vm/vm.test.ts: each module calls its own hook with itself as referrer, and reports positions with its ownlineOffset.vm-script-fetcher-leak.test.ts: thealive-sameSourceModulesscenario. Each module's hook stays alive while that module's function is alive.mainat f8bc171 passes both files (335 pass, 0 fail). The release canary1.4.3-canary.1+367d939d9, from before the WebKit change, fails bothvm.test.tstests.Background
import()to the embedder with the running code'sSourceOrigin: a URL plus an optionalScriptFetcher, an embedder object.node:vmcreates oneNodeVMScriptFetcherper module. It holds the hook weakly, alive while code from that source is alive (node:vm: importModuleDynamically lives as long as the code that can import() #43724).Notes
Repro (
bun repro.mjs, ornode --experimental-vm-modules repro.mjs):maingot the change: [JSC] Module records share an executable only when their sources have the same SourceOrigin and start position WebKit#639 merged on 2026-10-02 as 52bda5ef48aa. The first pin that contains it is 1600131e46b5 (Bump WebKit to 1600131e46b5 #44500). The pin before it, fb1167ebf2cb, does not. The current pin 87ee10da087b contains it, and itsJSModuleRecord.cpphashasSameObservableSource. Checked withgit merge-base --is-ancestoron the WebKit history.hook-calls=1 got=second want=secondon a debug build ofmainat f8bc171 and on Node 26.3.0. The release canary1.4.3-canary.1+367d939d9(WebKit 000c48997255, before the change) printshook-calls=0 got=first want=second.main:bun bd test test/js/node/vm/vm.test.ts test/js/node/vm/vm-script-fetcher-leak.test.tsgives 335 pass, 4 skip, 61 todo, 0 fail.1.4.3-canary.1+367d939d9fails both newvm.test.tstests. The first receives the first module's hook calls andimported: ["first", "first"]for the second module. The second receives line 100 where it expects line 1.mainat 6d504dd (WebKit 299c5323, with node:vm: importModuleDynamically lives as long as the code that can import() #43724, without [JSC] Module records share an executable only when their sources have the same SourceOrigin and start position WebKit#639) failed the twovm.test.tstests and thealive-sameSourceModulesscenario, with and withoutBUN_JSC_collectContinuously=1. The second module's function answered{"result":"hooked by first"}. The same tree with the preview build of [JSC] Module records share an executable only when their sources have the same SourceOrigin and start position WebKit#639 passed them.alive-sameSourceModulesfixture scenario also runs under Node (--expose-gc --experimental-vm-modules --no-compilation-cache) and prints the expected output.Weakthat lives while an executable of that source is alive ([JSC] A live ScriptExecutable makes its source's ScriptFetcher an opaque root WebKit#712 makes the fetcher an opaque root of the executable). When the second module's functions ran the first module's executable, nothing rooted the second module's fetcher. The scenario keeps only each module's exported function, collects, and expects each function to reach its own hook with its own module as referrer.vm.Scriptandvm.compileFunctionwere correct.JSGlobalObject::m_moduleProgramExecutables) is aWeakGCMap. After a full collection the first executable could be dead, and then the second module linked its own. That is why the tests keep every module reachable and export a function from every source.lineOffsettest compares against a module with an identifier that was never seen, so it does not depend on the absolute line numbers.mainalready has a test named "SourceTextModules with the same identifier and source keep their own import bindings" (Upgrade WebKit to d3720d515e14: module linking without sorting import names or resolving every import up front #42823). It covers the import bindings of such records, not the hook or the offsets. The new block sits next to it.bun checkon the two changed.tsfiles reports no type errors.main. The cost of the WebKit change (identicalvm.SourceTextModules no longer share linked code) is measured in [JSC] Module records share an executable only when their sources have the same SourceOrigin and start position WebKit#639.no test proof · iteration 2 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/js/node/vm/vm.test.ts, test/js/node/vm/vm-script-fetcher-leak.test.ts