From d2a5196a2f951b1b100e1f7dec13d548f83aa5f4 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 18 Sep 2026 13:43:21 -0700 Subject: [PATCH] fix: probe SQLite when Node denies worker threads (#152075) --- node-sqlite.mjs | 11 ++-- src/infra/sqlite-capabilities.test.ts | 78 +++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 3 deletions(-) diff --git a/node-sqlite.mjs b/node-sqlite.mjs index d225894fe51b..ffa5be325cec 100644 --- a/node-sqlite.mjs +++ b/node-sqlite.mjs @@ -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]); } diff --git a/src/infra/sqlite-capabilities.test.ts b/src/infra/sqlite-capabilities.test.ts index a8c9f3b40cde..a6515ca1b1ec 100644 --- a/src/infra/sqlite-capabilities.test.ts +++ b/src/infra/sqlite-capabilities.test.ts @@ -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 }, + }); + }, + ); });