mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-17 12:42:21 +03:00
fix(cli): verify better-sqlite3 native binary is actually loadable (port from 9router#2493)
isBetterSqliteBinaryValid() only checked the .node file's magic bytes (ELF/Mach-O/PE header), never whether the binary was built for the ABI (NODE_MODULE_VERSION) of the Node runtime that loads it. A stale or foreign-ABI binary passed the check and then segfaulted the process on the first database call instead of triggering a rebuild via npmInstallRuntime(). The fix adds a real load probe (require() in a throwaway subprocess) after the magic-byte check, so an incompatible binary is now correctly reported as invalid and the runtime self-heal reinstalls it. Reported-by: Manikandan (@mrprohack) (https://github.com/decolua/9router/issues/2493)
This commit is contained in:
@@ -6,6 +6,8 @@
|
||||
|
||||
## [3.8.49] — TBD
|
||||
|
||||
- **fix(cli):** the runtime self-heal now verifies a cached `better-sqlite3` native binary actually loads for the running Node before trusting it — the old check only inspected the file's magic bytes (ELF/Mach-O/PE header), so a binary built for a different Node ABI passed validation and segfaulted the process on first use instead of triggering a rebuild. (thanks @mrprohack)
|
||||
|
||||
---
|
||||
|
||||
## [3.8.48] — 2026-07-13
|
||||
|
||||
@@ -52,6 +52,31 @@ export function hasModule(name) {
|
||||
return existsSync(join(runtimeModules(), name, "package.json"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Probe whether a native addon (.node) file can actually be dlopen'd by the Node runtime that
|
||||
* is going to load it. Runs in a throwaway subprocess so a real ABI mismatch (which can segfault
|
||||
* the process instead of throwing) never takes down the caller — only the probe subprocess.
|
||||
*/
|
||||
function probeNativeBinaryLoadable(binary) {
|
||||
try {
|
||||
const res = spawnSync(
|
||||
process.execPath,
|
||||
[
|
||||
"-e",
|
||||
"try { require(process.argv[1]); process.exit(0); } catch (e) { process.exit(1); }",
|
||||
binary,
|
||||
],
|
||||
{ timeout: 10_000, stdio: "ignore" }
|
||||
);
|
||||
// status === 0 means require() (and therefore dlopen) succeeded. Anything else — a thrown
|
||||
// ERR_DLOPEN_FAILED/NODE_MODULE_VERSION mismatch (status 1) or a crash (status null with a
|
||||
// signal, e.g. SIGSEGV) — means the binary is not safe to load.
|
||||
return res.status === 0;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function isBetterSqliteBinaryValid() {
|
||||
const binary = join(
|
||||
runtimeModules(),
|
||||
@@ -68,10 +93,18 @@ export function isBetterSqliteBinaryValid() {
|
||||
closeSync(fd);
|
||||
const magic = buf.toString("hex");
|
||||
const os = platform();
|
||||
if (os === "linux") return magic.startsWith("7f454c46"); // ELF
|
||||
if (os === "darwin") return magic.startsWith("cffaedfe") || magic.startsWith("cefaedfe"); // Mach-O
|
||||
if (os === "win32") return magic.startsWith("4d5a"); // PE/MZ
|
||||
return true;
|
||||
let formatOk;
|
||||
if (os === "linux") formatOk = magic.startsWith("7f454c46"); // ELF
|
||||
else if (os === "darwin")
|
||||
formatOk = magic.startsWith("cffaedfe") || magic.startsWith("cefaedfe"); // Mach-O
|
||||
else if (os === "win32") formatOk = magic.startsWith("4d5a"); // PE/MZ
|
||||
else formatOk = true;
|
||||
if (!formatOk) return false;
|
||||
// File-format magic bytes alone do not guarantee the binary was built for the Node ABI
|
||||
// (NODE_MODULE_VERSION) that will load it — a stale/foreign-ABI binary passes the header
|
||||
// check and then crashes (segfault) on load instead of triggering a rebuild. Actually
|
||||
// attempt to load it, isolated in a subprocess.
|
||||
return probeNativeBinaryLoadable(binary);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -71,20 +71,60 @@ test("buildEnvWithRuntime preserva NODE_PATH existente", async () => {
|
||||
assert.ok(env.NODE_PATH.includes("/existing/path"), "NODE_PATH original deve ser preservado");
|
||||
});
|
||||
|
||||
test("isBetterSqliteBinaryValid detecta ELF magic bytes (Linux)", async () => {
|
||||
test("isBetterSqliteBinaryValid rejeita binário com magic bytes válidos mas ABI incompatível (regressão #2493)", async () => {
|
||||
// Regression for upstream 9router#2493: a binary that only "looks" native (correct ELF/Mach-O/PE
|
||||
// header) but was built for a different Node ABI (NODE_MODULE_VERSION) must NOT be reported as
|
||||
// valid — loading it crashes the process (segfault) instead of triggering a rebuild.
|
||||
const { getRuntimeNodeModules, isBetterSqliteBinaryValid } =
|
||||
await import("../../bin/cli/runtime/nativeDeps.mjs");
|
||||
const nm = getRuntimeNodeModules();
|
||||
const buildDir = join(nm, "better-sqlite3", "build", "Release");
|
||||
mkdirSync(buildDir, { recursive: true });
|
||||
const binary = join(buildDir, "better_sqlite3.node");
|
||||
const buf = Buffer.from([0x7f, 0x45, 0x4c, 0x46, 0x00, 0x00, 0x00, 0x00]);
|
||||
const { platform } = await import("node:os");
|
||||
const os = platform();
|
||||
// Correct file-format magic bytes for the current OS, but not a real, loadable native addon —
|
||||
// this is exactly what the old magic-bytes-only check let through.
|
||||
const magicByPlatform = {
|
||||
linux: [0x7f, 0x45, 0x4c, 0x46],
|
||||
darwin: [0xcf, 0xfa, 0xed, 0xfe],
|
||||
win32: [0x4d, 0x5a],
|
||||
};
|
||||
const magic = magicByPlatform[os] ?? magicByPlatform.linux;
|
||||
const buf = Buffer.concat([Buffer.from(magic), Buffer.alloc(64, 0)]);
|
||||
writeFileSync(binary, buf);
|
||||
const result = isBetterSqliteBinaryValid();
|
||||
const { platform } = await import("node:os");
|
||||
if (platform() === "linux") {
|
||||
assert.equal(result, true, "ELF magic bytes devem ser válidos no Linux");
|
||||
assert.equal(
|
||||
result,
|
||||
false,
|
||||
"binário com header válido mas ABI/conteúdo incompatível deve ser inválido"
|
||||
);
|
||||
rmSync(join(nm, "better-sqlite3"), { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test("isBetterSqliteBinaryValid aceita um binário nativo real e carregável", async () => {
|
||||
const { getRuntimeNodeModules, isBetterSqliteBinaryValid } =
|
||||
await import("../../bin/cli/runtime/nativeDeps.mjs");
|
||||
const { existsSync, copyFileSync } = await import("node:fs");
|
||||
const realBinary = join(
|
||||
process.cwd(),
|
||||
"node_modules",
|
||||
"better-sqlite3",
|
||||
"build",
|
||||
"Release",
|
||||
"better_sqlite3.node"
|
||||
);
|
||||
if (!existsSync(realBinary)) {
|
||||
// Ambient runtime without a compiled better-sqlite3 binary — nothing to assert here.
|
||||
return;
|
||||
}
|
||||
const nm = getRuntimeNodeModules();
|
||||
const buildDir = join(nm, "better-sqlite3", "build", "Release");
|
||||
mkdirSync(buildDir, { recursive: true });
|
||||
const binary = join(buildDir, "better_sqlite3.node");
|
||||
copyFileSync(realBinary, binary);
|
||||
const result = isBetterSqliteBinaryValid();
|
||||
assert.equal(result, true, "um binário real, compatível com o Node atual, deve ser válido");
|
||||
rmSync(join(nm, "better-sqlite3"), { recursive: true, force: true });
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user