nodejs / node

Node.js JavaScript runtime ✨🐢🚀✨

Home Page:https://nodejs.org

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Segmentation fault with `import()` instead of calling `importModuleDynamically`

nicolo-ribaudo opened this issue · comments

  • Version: 14.15.0, 15.0.1. It doesn't crash with 12.9.0.
  • Platform: Linux nicolo-XPS-15-9570 5.4.0-52-generic #57-Ubuntu SMP Thu Oct 15 10:57:00 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux
  • Subsystem: vm

What steps will reproduce the bug?

https://github.com/nicolo-ribaudo/babel/tree/node-segfault

I'm sorry but I can't create a smaller reproduction example (EDIT: #35889 (comment)): I managed to create a small test that reproduces the crash almost always, but it still far from being "self contained".

  1. Run make bootstrap to install dependencies and compile everything
  2. Run node --experimental-vm-modules ./node_modules/.bin/jest -i babel-core/test/segfault to see the segfault (try 2-3 times, sometimes the first run doesn't fail).

That test will run this file: I have placed a debugger; statement so that you can --inspect-brk it. After debugger, if you move into the import() call, it will crash.

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

Almost 100%

What is the expected behavior?

import() should call Jest's importModuleDynamically function.

What do you see instead?

@SimenB tried debugging this segfault and extracted this stacktrace (babel/babel#12288 (comment)):

PID 43445 received SIGSEGV for address: 0x0
0   segfault-handler.node               0x00000001055e3fa0 _ZL16segfault_handleriP9__siginfoPv + 304
1   libsystem_platform.dylib            0x00007fff6951f5fd _sigtramp + 29
2   ???                                 0x0000000000000007 0x0 + 7
3   node                                0x000000010033df4a _ZN2v88internal7Isolate38RunHostImportModuleDynamicallyCallbackENS0_6HandleINS0_6ScriptEEENS2_INS0_6ObjectEEE + 138
4   node                                0x00000001006f82f4 _ZN2v88internal25Runtime_DynamicImportCallEiPmPNS0_7IsolateE + 340
5   node                                0x0000000100a797b4 Builtins_CEntry_Return1_DontSaveFPRegs_ArgvInRegister_NoBuiltinExit + 52
[1]    43445 segmentation fault  node --experimental-vm-modules --inspect-brk ./node_modules/.bin/jest 

Additional information

  • When the test doesn't crash, it throws that file:///home/nicolo/Documenti/dev/babel/babel/packages/babel-core/test/fixtures/example.mjs doesn't exist even if it does. However, this might be caused by Jest?
  • If you can't reproduce the bug, you can try running node --experimental-vm-modules ./node_modules/.bin/jest -i babel-core/test/config-chain which is how I originally discovered this issue. You can stop right before crashing by adding a debugger; right before the compiled version (which will be generated in packages/babel-core/lib/config/files/import.js) of this import() call.

Similar bugs

  • #33233 is a segfault when using import(), but I don't think that it is the same bug because that one only happens in the REPL.
  • #25424 This is calling importModuleDynamically before crashing, while in my case I don't think it is.
  • When the test doesn't crash, it throws that file:///home/nicolo/Documenti/dev/babel/babel/packages/babel-core/test/fixtures/example.mjs doesn't exist even if it does. However, this might be caused by Jest?

Yes, Jest doesn't deal with file URLs, that's a bug that should be simple to fix. I can fix that right away.

EDIT: jestjs/jest#10744

@nicolo-ribaudo "Subsystem" is vm, btw

EDIT2: For anyone looking into this, the importModuleDynamically call is implemented here: https://github.com/facebook/jest/blob/2b748f67c25615a111330017a2bffc0baf51d558/packages/jest-runtime/src/index.ts#L1176-L1182

@devsnek sorry to ping you, but do you have any idea of the top of your head for what we could be doing wrong in Jest? It's probably something that should be tweaked in Node regardless since it segfaults, but I'm also quite certain it's Jest doing something unexpected 🙂

If it helps, I created a minimal (almost, it still uses Jest) reproduction: https://github.com/nicolo-ribaudo/node-segfault-jest-dynamic-import

Also @bmeck kindly told me that this is probably a known v8 issue, but I couldn't find it in the v8 bug tracker 😅


EDIT: I created a minimal reproduction without any dependency, but it's the first time I use the vm module so I might have done something wrong. https://github.com/nicolo-ribaudo/node-vm-dynamic-import-segfault

It is from the VM module dealing with https://bugs.chromium.org/p/v8/issues/detail?id=10284 , which means that these fake modules from source text alone don't get properly resolved and it segfaults

I can reproduce this bug on Node v16.9.1 using Webpack and Babel configured with ESM configuration files.

PID 19496 received SIGSEGV for address: 0x18
0   segfault-handler.node               0x0000000105b630aa _ZL16segfault_handleriP9__siginfoPv + 298
1   libsystem_platform.dylib            0x00007fff20399d7d _sigtramp + 29
2   ???                                 0x00002afc24f31c6b 0x0 + 47262440037483
3   node                                0x000000010314ada6 _ZN2v88internal7Isolate38RunHostImportModuleDynamicallyCallbackENS0_6HandleINS0_6ScriptEEENS2_INS0_6ObjectEEENS0_11MaybeHandleIS5_EE + 218
4   node                                0x000000010340c3de _ZN2v88internal25Runtime_DynamicImportCallEiPmPNS0_7IsolateE + 329
5   node                                0x0000000103696b54 Builtins_CEntry_Return1_DontSaveFPRegs_ArgvInRegister_NoBuiltinExit + 52
6   node                                0x00000001037223ce Builtins_CallRuntimeHandler + 78
7   node                                0x0000000103629fea Builtins_InterpreterEntryTrampoline + 202
zsh: segmentation fault  node --trace-event-categories v8,node,node.async_hooks 
commented

v8 has landed a partial fix for this in that they check for host_defined_options in the cache now, but this causes a large amount of cache misses and extra memory usage in both chrome and node, so they're still working on it.

To be honest, I am very upset with this problem, in fact it blocks the transition very much on ESM modules, for large projects it is very difficult to just take and translate the entire codebase to ESM, but small projects started to transition, and in order to keep versions up to day we need to use import(), but once we use it we cannot test anymore, jest just failed with Segmentation fault. I am incredibly surprised that this problem is still not on the top priority list.

It's not just about updating dependencies. This actually blocks the ecosystem from transitioning smoothly. Also, old package versions stop receiving updates, which we can potentially lead to security problems.

I really hope that the problem will be fixed in the near future.

Wouldn't describe myself as "very upset" per se, but it is stopping us from transitioning too atm

this is a critical issue for our organization as well.
facing it while testing with jest.
jestjs/jest#11438

v8 has landed a partial fix for this in that they check for host_defined_options in the cache now, but this causes a large amount of cache misses and extra memory usage in both chrome and node, so they're still working on it.

If I'm reading the issue correctly then it sounds like that change was reverted and things are still broken. And that bug can't be actively worked on at the moment because there's a blocker.

Maybe be need to ping somebody from V8 here...

Worth mentioning that it isn't only Jest that is affected too - our test framework which is built on top of mocha cannot run and furthermore am being unable to (practicably) upgrade to latest version of dependencies and get a completely rosy npm audit output when groovy people like e.g. @sindresorhus are taking everything to ESM...

@daniele-orlando

I can reproduce this bug on Node v16.9.1 using Webpack and Babel configured with ESM configuration files.

I can reproduce it the exact same way on Node v16.13.0. Have a webpack setup that's worked no problem forever, but just updated to chalk 5 which is ESM only now and it lead me down the rabbit hole of converting all my config files to ESM. Now I'm consistently running into this exact issue. We can probably just delay on testing or rollback to chalk 4 but would still be nice to be able to make this transition.

Is there no workaround? This appears to happen in all node version from 14 to 18

If anyone comes to this issue from Jest, I'm using https://github.com/nicolo-ribaudo/jest-light-runner which isn't affected by this bug.

@nicolo-ribaudo many many thanks! this ended my nightmares

commented

It took me way too long to figure out a perfectly fine file copy routine from copy-webpack-plugin caused my segfaults, because it's dynamically importing globby for some reason (webpack-contrib/copy-webpack-plugin#646).

Thank you very much for that project, @nicolo-ribaudo!

Is this issue resolved, as i was still get the segfault while running a test. The test was written for a component with a third party library called react-intl-tel-input. The tests are written in typescript, so is there any workaround for it?

Error: zsh: segmentation fault node 'node_modules/.bin/jest'

No, the issue is not resolved.

I do not think v8 know about its importance to the community as it's somewhat obscure and difficult to track down; I imagine many more are having problems than know this issue tracker (or the chromium bug tracker) exist.

We do use webpack copy plugin though so thanks to @Jaid for posting that; I don't think it'll be sufficient to fix our issue but will give it a punt - we really want to move a mature codebase to ESM and that just can't happen atm.

I have the same problem, currently it is impossible to use Jest with ESM. This issue is probably the number one blocker for the community to move to ESM.

Any ETA about this bug? 2+ years since we are blocked by this. And it’s becoming worse each passing day. I understand it’s more on the V8 side of things but it’s hard to understand where things stand at on this side.

@qraynaud Unfortunately that's exactly the problem. Until V8 get that sorted out there's nothing we can do on node about it.

Here is some workaround that seems to work for me:

const { Identifier } =
  process.env.NODE_ENV === "test"
    ? jest.requireActual("./some-module.mts")
    : await import("./some-module.mts");

@calebpitan What is this supposed to achieve? It just throws an error

Error: Must use import to load ES Module: test/vite.config.mjs

    at Runtime.requireModule (node_modules/jest-runtime/build/index.js:841:21)
    at Runtime.requireActual

I haven't seen a simple repro posted yet that always works, so here is one.

// Wrapper so it functions in Jest or raw Node
if (!globalThis.test) {
  globalThis.test = function(_, f) {
    f();
  }
}
// Create multiple layers of imports. This makes the failure happen much faster.
const wrap = str => `data:text/javascript,import('${encodeURIComponent(str.replaceAll("'", "%27"))}')`
const data = wrap(wrap(wrap(wrap("data:text/javascript,const%20a%3D__target__"))));

test("Repro Node segfault", async () => {
  for (let i = 0; i < 100000; ++i) {
    if (i % 1000 === 0) {
      console.log(i);
    }
    // Every import needs to be unique, so they're not just cached
    await import(data.replace("__target__", i));
  }
});

If you run this directly as node test.js, it will run all 100000 iterations, taking ~2GB (because modules aren't garbage collected) and a fair amount of time, but it won't crash.

If you run this as NODE_OPTIONS=--experimental_vm_modules npx jest test.js , it will usually crash immediately. On very rare occasions I've seen it get up to 13000 iterations before crashing.

You can use this to see if your test runner is buggy, etc.

@d0sboots with what version of node is that? I let it run for 50,000 iterations but no crash on the latest node v19, with or without the --experimental_vm_modules flag.

@bnoordhuis I was using 18.11.0. It also repros for me with Node 19.8.1 (actually even more reliably).

Are you running it under Jest? I suspect not, because if you run this under Jest without --experimental_vm_modules, it will fail immediately with the following error:

 FAIL  ./.test.js
  ✕ Repro Node segfault (16 ms)

  ● Repro Node segfault

    You need to run with a version of node that supports ES Modules in the VM API. See https://jestjs.io/docs/ecmascript-modules

As I mentioned in the instructions, if you run this directly under Node, it is expected that it works correctly without crashing.

Edit, More context: Because of the nature of this bug, AFAIK triggering it relies on using a "non-standard" importModuleDynamically callback, which only happens when using the experimental vm API to make modules. Pretty much all test frameworks do this, so the simplest path to repro is to run a dynamic import() under a test runner, as opposed to manually doing setup for vm. Reproing this way also makes it easier for both users and test infrastructure authors to see if they're affected.

Ah, okay. The "functions in Jest or raw Node" made me think it works in both.

I just spent 30 minutes trying to do whatever it is that Jest is doing but I can't reproduce. No crashes, no matter how convoluted, contorted and contrived the importModuleDynamically callback I throw at it.

Cards on the table, I don't plan to spend hours going over Jest's code base. I'll need a plain node test case.

I poked at it some myself, and I couldn't get a simple Node-only repro working either. One was mentioned up-thread (#35889 (comment)), but it didn't work for me.

However, I think a Node-only repro is mostly of academic interest at this point? We can repro the issue, and we know both where it's going wrong and what to do about it. It's just blocked on https://bugs.chromium.org/p/chromium/issues/detail?id=1244145 and it's friends (info is spread across multiple bugs, that's the one with the most action right now). Feel free to star, but please no "me-too" comments. We're in a holding pattern until that gets enough resources to finish being worked, in particular https://chromium-review.googlesource.com/c/v8/v8/+/3172764?tab=comments (which has been slow-rolling for a year and a half, oof). Or until someone comes up with a genius workaround that doesn't require V8 action.

I don't know. I tried hitting that code path - and I'm reasonably satisfied I did - but in both v18 and v19 I get a "Invalid host defined options" exception, not a segfault. Jest must be doing something else but what?

// Flags: --experimental_vm_modules
"use strict"
const vm = require("vm")
const context = vm.createContext({console})
const source = {
  foo: `console.log("foo"); import("bar")`,
  bar: `console.log("bar")`,
}
const options = {context, importModuleDynamically}
const cachedDatas = {
  foo: new vm.SourceTextModule(source.foo, options).createCachedData(),
  bar: new vm.SourceTextModule(source.bar, options).createCachedData(),
}
main()
async function main() {
  const module = new vm.SourceTextModule(`import("foo")`, options)
  await module.link(() => { throw "unreachable" })
  await module.evaluate()
}
async function importModuleDynamically(spec, script) {
  console.log("import", spec, script)
  const cachedData = cachedDatas[spec]
  const options = {context, cachedData, importModuleDynamically}
  const module = new vm.SourceTextModule(source[spec], options)
  await module.link(() => { throw "unreachable" })
  await module.evaluate()
  return module
}

For jest users:

Try running jest --clearCache before running your actual tests (equals to disable jest's transform cache).

This may help in some situations.

If this works for you, you can turn off jest's cache ability in jest.config with cache: false.

Is this the repro you're looking for? #39964 (comment)

Maybe? I can reproduce but:

  1. Your test case doesn't use importModuleDynamically
  2. It only seems to happen in the REPL, not outside of it

I'll reopen your issue because maybe it's a different bug after all, just one that looks a lot like this one.

@joyeecheung this can be closed as well 🙂

Closing as #48510 has landed and should fix this. We can re-open if that turns out to be incorrect (hopefully not).