fix: probe SQLite when Node denies worker threads (#152075)

This commit is contained in:
Peter Steinberger
2026-09-18 13:43:21 -07:00
committed by GitHub
parent f953dbdf47
commit d2a5196a2f
2 changed files with 86 additions and 3 deletions
+8 -3
View File
@@ -56,12 +56,17 @@ function isSqliteCapabilities(value) {
);
}
async function probeCurrentSqliteInWorker() {
async function probeCurrentSqlite() {
let worker;
try {
if (typeof process.getBuiltinModule?.("node:sqlite")?.DatabaseSync !== "function") {
const DatabaseSync = process.getBuiltinModule?.("node:sqlite")?.DatabaseSync;
if (typeof DatabaseSync !== "function") {
return unavailableSqliteCapabilities(new Error("node:sqlite is unavailable"));
}
// Keep the real capability check when Node's permission policy forbids workers.
if (!process.versions.bun && process.permission?.has("worker") === false) {
return probeSqlite(DatabaseSync);
}
const env = Object.fromEntries(
Object.entries(process.env).filter(([name]) => !/^(NODE_OPTIONS|BUN_OPTIONS)$/i.test(name)),
);
@@ -126,7 +131,7 @@ async function probeCurrentSqliteInWorker() {
export function detectCurrentSqliteCapabilities() {
// Publish the Promise before Worker construction can notify another startup caller.
globalThis[capabilityCacheKey] ??= Promise.resolve().then(probeCurrentSqliteInWorker);
globalThis[capabilityCacheKey] ??= Promise.resolve().then(probeCurrentSqlite);
return Promise.resolve(globalThis[capabilityCacheKey]);
}
+78
View File
@@ -3,6 +3,7 @@ import { DatabaseSync } from "node:sqlite";
import { runInNewContext } from "node:vm";
import { describe, expect, it, vi } from "vitest";
import { SQLITE_CAPABILITY_PROBE } from "../../node-sqlite.mjs";
import { resolveTestNodeExecPath } from "../test-utils/node-process.js";
function probeSqlite(Database: unknown): unknown {
return runInNewContext(SQLITE_CAPABILITY_PROBE, {
@@ -142,4 +143,81 @@ describe("SQLite NUL capability probe", () => {
counts: { prepare: 0, exec: 0, get: 0, all: 0, run: 0, iterate: 0 },
});
});
it.each([false, true])(
"validates real SQLite when Node denies workers (SQL failure=%s)",
(sqlFailure) => {
const moduleUrl = new URL("../../node-sqlite.mjs", import.meta.url).href;
const output = execFileSync(
resolveTestNodeExecPath(),
[
"--input-type=module",
"-e",
`
import { DatabaseSync } from "node:sqlite";
import { Worker } from "node:worker_threads";
let closed = 0;
const close = DatabaseSync.prototype.close;
DatabaseSync.prototype.close = function (...args) {
closed++;
return Reflect.apply(close, this, args);
};
if (${sqlFailure}) {
DatabaseSync.prototype.exec = function () {
throw new Error("injected SQL failure");
};
}
let workersStarted = 0;
process.on("worker", () => workersStarted++);
const { detectCurrentSqliteCapabilities, nodeRuntimeFailure } = await import(${JSON.stringify(moduleUrl)});
const pending = detectCurrentSqliteCapabilities();
const samePending = pending === detectCurrentSqliteCapabilities();
const capabilities = await pending;
const sameResult = capabilities === await detectCurrentSqliteCapabilities();
let workerDenied;
try {
new Worker("", { eval: true });
} catch (error) {
workerDenied = { code: error.code, permission: error.permission };
}
process.stdout.write(JSON.stringify({
asynchronous: pending instanceof Promise, samePending, sameResult,
capabilities, failure: nodeRuntimeFailure(process.versions.node, capabilities),
closed, workersStarted, workerDenied,
permissions: {
worker: process.permission.has("worker"),
write: process.permission.has("fs.write"),
child: process.permission.has("child"),
},
}));
`,
],
{
encoding: "utf8",
timeout: 10_000,
env: { ...process.env, NODE_OPTIONS: "--permission --allow-fs-read=*" },
},
);
expect(JSON.parse(output)).toMatchObject({
asynchronous: true,
samePending: true,
sameResult: true,
capabilities: {
available: true,
version: expect.any(String),
text: !sqlFailure,
blob: !sqlFailure,
json: !sqlFailure,
...(sqlFailure ? { error: "injected SQL failure" } : {}),
},
failure: sqlFailure
? expect.stringContaining("node:sqlite NUL round-trip capability probe failed")
: null,
closed: 1,
workersStarted: 0,
workerDenied: { code: "ERR_ACCESS_DENIED", permission: "WorkerThreads" },
permissions: { worker: false, write: false, child: false },
});
},
);
});