process.dlopen() should throw a clear Error (not segfault) when addon's libnode.so ABI mismatches the running Node
Nobody has claimed this yet.
- Dominant language
- JavaScript
- Stars
- 122k
- Forks
- 37.3k
- Avg merge
- 4d 2h
- Merged PRs (30d)
- 283
Description
Summary
When a Node.js native addon is loaded into a process where the addon's linked libnode.so.{} ABI version does not match the running node binary's process.config.variables.node_module_version, the addon segfaults during napi_register_module_v1 instead of producing a useful error.
This is a common, easily-misdiagnosed failure mode (I personally misdiagnosed it as a WSL2/libuv bug for hours before finding the real cause with a 10-line reproducer). Node could detect the mismatch and throw a clear Error before the segfault, saving users significant debugging time.
The Failure (What Currently Happens)
On a system with a stale libnode.so.109 from an old libnode109 package, plus Node 22 (ABI 127), building any NAPI addon with default node-gyp flags produces a .node file that segfaults on load:
/* minimal.c — 10-line NAPI addon, no SIMD, no threading */
#include <node_api.h>
napi_value Hello(napi_env env, napi_callback_info info) {
napi_value result;
napi_create_string_utf8(env, "hello from native", NAPI_AUTO_LENGTH, &result);
return result;
}
NAPI_MODULE_INIT() {
napi_value fn;
napi_create_function(env, "hello", NAPI_AUTO_LENGTH, Hello, NULL, &fn);
napi_set_named_property(env, exports, "hello", fn);
return exports;
}
# Build with default node-gyp flags (-lnode) — segfaults
$ gcc -shared -fPIC -I/usr/include/node minimal.c -lnode -o minimal_lnode.node
$ node -e "require('./minimal_lnode.node')"
Segmentation fault (core dumped)
# Build without -lnode (rare in practice, since node-gyp adds it) — works
$ gcc -shared -fPIC -I/usr/include/node minimal.c -o minimal.node
$ node -e "require('./minimal.node')"
# works fine
The ldd difference:
$ ldd minimal.node
libc.so.6
(no libnode)
$ ldd minimal_lnode.node
libnode.so.109 => /lib/x86_64-linux-gnu/libnode.so.109
libuv.so.1 => /lib/x86_64-linux-gnu/libuv.so.1
...
The system has libnode.so.109 (from Node 21) at the standard library path. node-gyp defaults to -lnode, so the addon's ELF needs that exact file. Node 22 (ABI 127) maps its own internal libnode.so.127 into the same address space, and the two libraries disagree on V8/libuv global layouts. The first call that touches shared state (napi_register_module_v1) segfaults.
ldd /usr/bin/node on the same system shows Node 22 itself linked to libnode.so.109, which is its own can of worms — but the addon-level segfault is what the user actually sees.
The Request
In process.dlopen() (or wherever the addon is mapped and napi_register_module_v1 is invoked), detect a libnode.so ABI mismatch and throw a clear Error instead of segfaulting.
Concretely, something like:
Error: Cannot load native addon './minimal_lnode.node':
the addon was linked against libnode.so.109, but this Node.js
process is built against libnode.so.127 (Node.js 22.22.2).
This usually means the system has an outdated libnode package
(e.g. libnode109 on Ubuntu/Debian). Try:
sudo apt remove libnode109 libnode-dev
or rebuild the addon with the correct headers for this Node version.
at Object.Module._extensions..node (node:internal/modules/cjs/loader:1429:18)
at Module.load (node:internal/modules/cjs/loader:1034:32)
...
This is a one-time check at dlopen() time and would be invaluable for debugging. The cost is negligible; the gain is a much friendlier onboarding / debugging experience for users hitting this trap.
Suggested Implementation Sketch
After dlopen() succeeds, walk the addon's DT_NEEDED entries (via dl_iterate_phdr or by parsing DT_NEEDED directly from the addon's own .dynamic section) and check whether any of them look like libnode.so.<number>. If found, compare that number to process.config.variables.node_module_version (or equivalent) and throw before calling napi_register_module_v1.
Approximate location: src/node_binding.cc / dlopen in lib/internal/modules/cjs/loader.js, around the call to napi_register_module_v1.
A non-fatal variant could just emit process.emitWarning(...) rather than throw, if maintainers prefer non-breaking behaviour. But throwing is more discoverable.
Why This Matters
-
The segfault is silent and unactionable. Users see "Segmentation fault" and assume Node, WSL2, their CPU, or the addon is broken. They try downgrades, rebuilds, BIOS settings — none of which help. The real fix is a one-liner on the system admin side.
-
The error is easy to detect.
dl_iterate_phdris already in use elsewhere in Node. A small extension would catch this. -
It avoids duplicate issues. Searching the issue tracker, this class of bug appears repeatedly under different framings (WSL2, libuv, Hyper-V, CPU, etc.). A clear error message at load time would let users self-diagnose in seconds instead of opening duplicate bug reports.
-
I have the minimal reproducer and a 100%-confirmed root cause. I was the original reporter; I can supply additional diagnostics, machine state, or test cases on request.
Related / Similar Reports
These may all be the same root cause with different symptom descriptions:
- nodejs/node#58690 — WSL2 segfault on Node 24.2.0
- microsoft/Foundry-Local#626 — Native lib segfault on WSL2 during
catalog.getModel() - bun#11882 — Bun segfaults on WSL2 (worked on v1.0.9)
If any of those reporters can share their ldd output for the affected binary, I'd bet they show the same libnode.so.<wrong-version> linkage. Adding a friendly error in Node would at minimum give Bun/Foundry-Local users a head start.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the minimal.c reproducer and compare the ldd output for the working and crashing addons. Read src/node_binding.cc and the dlopen path in lib/internal/modules/cjs/loader.js, then determine how the addon’s libnode.so dependency can be checked before napi_register_module_v1. Done means an ABI mismatch produces a clear Error instead of a segfault.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- c, cpp, javascript, linux, node.js
- Domain
- backend, operating-systems
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 42/100