From ba7b7f15c2a576062a480e6863abfa092b6e0e58 Mon Sep 17 00:00:00 2001 From: "lancedb-gatefixer[bot]" <313497061+lancedb-gatefixer[bot]@users.noreply.github.com> Date: Fri, 23 Oct 2026 01:07:18 +0800 Subject: [PATCH] fix(node): reject musl binaries with unresolved AVX-512 symbol (#3791) ## Summary - inspect the assembled x64 musl addon for the unresolved AVX-512 distance-table symbol before npm publication - fail the publish lifecycle with an actionable error if the broken symbol is present - add regression coverage using the dynamic-symbol output from the affected 0.27.x artifact ## Root cause LanceDB 0.27.x could enable the Rust AVX-512 distance-table callsite when the separate C kernel build did not emit its object, leaving `sum_4bit_dist_table_32bytes_batch_avx512` undefined in the musl addon. The current Lance dependency includes the upstream split-kernel cfg fix, and the published 0.33.0 musl artifact no longer has the undefined symbol, but the LanceDB publish path did not verify that property before releasing an addon. This adds a release guard around the corrected implementation so the same invalid binary cannot be published again. ## Validation - `pnpm lint` - `pnpm run build` - `pnpm test --runInBand` (680 passed, 5 skipped) - `pnpm run docs` - checked published artifacts: 0.27.1 is rejected for the unresolved symbol; 0.33.0 is accepted Fixes #3200 Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com> --- nodejs/.npmignore | 1 + nodejs/__test__/native_symbols.test.ts | 47 ++++++++++++++++ nodejs/package.json | 3 +- nodejs/scripts/check-native-symbols.js | 77 ++++++++++++++++++++++++++ 4 files changed, 127 insertions(+), 1 deletion(-) create mode 100644 nodejs/__test__/native_symbols.test.ts create mode 100644 nodejs/scripts/check-native-symbols.js diff --git a/nodejs/.npmignore b/nodejs/.npmignore index 41f6643a6..128c0d36d 100644 --- a/nodejs/.npmignore +++ b/nodejs/.npmignore @@ -15,6 +15,7 @@ renovate.json src lancedb examples +scripts nodejs-artifacts Cargo.toml biome.json diff --git a/nodejs/__test__/native_symbols.test.ts b/nodejs/__test__/native_symbols.test.ts new file mode 100644 index 000000000..5445867f6 --- /dev/null +++ b/nodejs/__test__/native_symbols.test.ts @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright The LanceDB Authors + +const { + checkNativeBinary, + findForbiddenUndefinedSymbols, +} = require("../scripts/check-native-symbols.js"); + +test("detects the unresolved AVX-512 symbol from broken musl binaries", () => { + const output = [ + " U napi_create_function", + " U sum_4bit_dist_table_32bytes_batch_avx512", + " U strlen", + ].join("\n"); + + expect(findForbiddenUndefinedSymbols(output)).toEqual([ + "sum_4bit_dist_table_32bytes_batch_avx512", + ]); +}); + +test("accepts native binaries without unresolved internal Lance symbols", () => { + const runNm = jest.fn(() => ({ + status: 0, + stdout: + " U napi_create_function\n U strlen\n", + stderr: "", + })); + + expect(() => checkNativeBinary("lancedb.node", runNm)).not.toThrow(); + expect(runNm).toHaveBeenCalledWith( + "nm", + ["-D", "--undefined-only", "lancedb.node"], + { encoding: "utf8" }, + ); +}); + +test("rejects native binaries with the unresolved AVX-512 symbol", () => { + const runNm = jest.fn(() => ({ + status: 0, + stdout: " U sum_4bit_dist_table_32bytes_batch_avx512\n", + stderr: "", + })); + + expect(() => checkNativeBinary("lancedb.node", runNm)).toThrow( + "lancedb.node contains unresolved internal Lance symbols: sum_4bit_dist_table_32bytes_batch_avx512", + ); +}); diff --git a/nodejs/package.json b/nodejs/package.json index 9bade7200..21f903235 100644 --- a/nodejs/package.json +++ b/nodejs/package.json @@ -90,7 +90,8 @@ "postdocs": "node typedoc_post_process.js", "lint": "biome check . && biome format .", "lint-fix": "biome check --write . && biome format --write .", - "prepublishOnly": "napi prepublish -t npm", + "check:native-symbols": "node scripts/check-native-symbols.js", + "prepublishOnly": "pnpm check:native-symbols && napi prepublish -t npm", "test": "jest --verbose", "integration": "S3_TEST=1 pnpm test", "universal": "napi universalize", diff --git a/nodejs/scripts/check-native-symbols.js b/nodejs/scripts/check-native-symbols.js new file mode 100644 index 000000000..2f6dbf9b8 --- /dev/null +++ b/nodejs/scripts/check-native-symbols.js @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright The LanceDB Authors + +const { spawnSync } = require("node:child_process"); +const { existsSync } = require("node:fs"); +const path = require("node:path"); + +const FORBIDDEN_UNDEFINED_SYMBOLS = new Set([ + "sum_4bit_dist_table_32bytes_batch_avx512", +]); + +function findForbiddenUndefinedSymbols(output) { + const found = new Set(); + + for (const line of output.split(/\r?\n/)) { + const columns = line.trim().split(/\s+/); + const symbol = columns.at(-1)?.split("@")[0]; + if (symbol && FORBIDDEN_UNDEFINED_SYMBOLS.has(symbol)) { + found.add(symbol); + } + } + + return [...found].sort(); +} + +function checkNativeBinary(binaryPath, runNm = spawnSync) { + const result = runNm("nm", ["-D", "--undefined-only", binaryPath], { + encoding: "utf8", + }); + + if (result.error) { + throw new Error(`Unable to inspect ${binaryPath}: ${result.error.message}`); + } + if (result.status !== 0) { + throw new Error( + `Unable to inspect ${binaryPath}: nm exited with status ${result.status}\n${result.stderr}`, + ); + } + + const forbidden = findForbiddenUndefinedSymbols(result.stdout); + if (forbidden.length > 0) { + throw new Error( + `${binaryPath} contains unresolved internal Lance symbols: ${forbidden.join( + ", ", + )}`, + ); + } +} + +function main() { + const binaryPath = path.resolve( + __dirname, + "..", + "npm", + "linux-x64-musl", + "lancedb.linux-x64-musl.node", + ); + + if (!existsSync(binaryPath)) { + throw new Error( + `Missing ${binaryPath}; assemble the native artifacts before publishing`, + ); + } + + checkNativeBinary(binaryPath); +} + +if (require.main === module) { + try { + main(); + } catch (error) { + console.error(error instanceof Error ? error.message : error); + process.exitCode = 1; + } +} + +module.exports = { checkNativeBinary, findForbiddenUndefinedSymbols };