nodejs / nodejs/node

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

Abierto
#63,740 4 comentarios 0 reacciones 0 asignados Ver en GitHub

Nadie ha tomado este issue todavía.

node-api
Lenguaje dominante
JavaScript
Estrellas
122k
Forks
37.3k
Merge medio
4 d 2 h
PR fusionados (30 d)
283

Descripción

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.

Guía de contribución

Abrir la guía de contribución

Primeros pasos

  1. Lee el issue completo y luego la guía de contribución del proyecto.
  2. Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
  3. Haz un fork del repositorio y trabaja en una rama.
  4. Abre un pull request que haga referencia al número del issue.

Línea de trabajo

Comienza con el reproductor minimal.c y compara la salida de ldd de los addons que funcionan y que se bloquean. Lee src/node_binding.cc y la ruta de dlopen en lib/internal/modules/cjs/loader.js, y determina después cómo se puede comprobar la dependencia libnode.so del addon antes de napi_register_module_v1. Se considera terminado cuando un desajuste de ABI produce un Error claro en lugar de un segfault.

Escrito por el modelo de indexación a partir del texto del issue.

Evaluación

Stack tecnológico
c, cpp, javascript, linux, node.js
Área
backend, operating-systems
Tipo de issue
Error
Dificultad
5/5
Tiempo estimado
Más de una semana
Estado de actividad
Activo
Claridad
Bastante claro
Aptitud para principiantes
42/100

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.