nodejs / nodejs/node

process.dlopen() should throw a clear Error (not segfault) when addon's libnode.so ABI mismatches the running Node

Open
#63,740 4 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

node-api
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

  1. 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.

  2. The error is easy to detect. dl_iterate_phdr is already in use elsewhere in Node. A small extension would catch this.

  3. 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.

  4. 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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.