Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 9 additions & 8 deletions src/lib/bun-runtime-preflight.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,10 @@ export class RuntimePreflightError extends Error {
}
}

type DirectoryIdentity = { dev: number; ino: number; isDirectory(): boolean; isSymbolicLink(): boolean };
// bigint: NTFS file IDs exceed 2^53, where adjacent IDs are equal as numbers.
type DirectoryIdentity = { dev: bigint; ino: bigint; isDirectory(): boolean; isSymbolicLink(): boolean };
export interface RuntimePreflightFs {
lstatSync: (path: string) => DirectoryIdentity;
lstatSync: (path: string, options: { bigint: true }) => DirectoryIdentity;
mkdirSync: (path: string, mode: number) => unknown;
rmdirSync: (path: string) => void;
readdirSync: (path: string) => readonly string[];
Expand All @@ -44,7 +45,7 @@ export interface RuntimePreflightDeps {
const probeFs: RuntimePreflightFs = { lstatSync, mkdirSync, rmdirSync, readdirSync };

function sameDirectory(fs: RuntimePreflightFs, path: string, owned: DirectoryIdentity): boolean {
const current = fs.lstatSync(path);
const current = fs.lstatSync(path, { bigint: true });
return current.isDirectory() && !current.isSymbolicLink()
&& current.dev === owned.dev && current.ino === owned.ino;
}
Expand All @@ -59,7 +60,7 @@ function removeOwnedEmptyDirectory(fs: RuntimePreflightFs, path: string, owned:
function prepareRoot(fs: RuntimePreflightFs, root: string, rootWasAbsent?: boolean): void {
try {
if (rootWasAbsent === undefined) {
try { fs.lstatSync(root); rootWasAbsent = false; }
try { fs.lstatSync(root, { bigint: true }); rootWasAbsent = false; }
catch (error) {
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
rootWasAbsent = true;
Expand All @@ -73,7 +74,7 @@ function prepareRoot(fs: RuntimePreflightFs, root: string, rootWasAbsent?: boole
// Use a concurrently created root, subject to the directory check below.
}
}
const current = fs.lstatSync(root);
const current = fs.lstatSync(root, { bigint: true });
if (!current.isDirectory() || current.isSymbolicLink()) throw new RuntimePreflightError("create");
} catch {
throw new RuntimePreflightError("create");
Expand All @@ -83,7 +84,7 @@ function prepareRoot(fs: RuntimePreflightFs, root: string, rootWasAbsent?: boole
function probeInProcess(fs: RuntimePreflightFs, path: string): void {
let owned: DirectoryIdentity | undefined;
try {
try { fs.mkdirSync(path, 0o700); owned = fs.lstatSync(path); }
try { fs.mkdirSync(path, 0o700); owned = fs.lstatSync(path, { bigint: true }); }
catch { throw new RuntimePreflightError("create"); }
try { fs.rmdirSync(path); }
catch { throw new RuntimePreflightError("remove"); }
Expand All @@ -96,10 +97,10 @@ function probeChild(runtime: DurableBunRuntime, path: string, nonce: string, spa
// The child emits only this invocation's nonce and a fixed result, never paths/errors.
const script = `const fs = require("node:fs"); const path = ${JSON.stringify(path)};
let result = "create", owned;
try { fs.mkdirSync(path, 0o700); owned = fs.lstatSync(path); result = "remove";
try { fs.mkdirSync(path, 0o700); owned = fs.lstatSync(path, { bigint: true }); result = "remove";
fs.rmdirSync(path); result = "ok";
} catch { /* result already names the failed step; details never leave the child */ } finally {
if (owned) { try { const now = fs.lstatSync(path);
if (owned) { try { const now = fs.lstatSync(path, { bigint: true });
if (now.isDirectory() && !now.isSymbolicLink() && now.dev === owned.dev && now.ino === owned.ino
&& fs.readdirSync(path).length === 0) fs.rmdirSync(path);
} catch { /* leave any probe entry that cannot be proven ours */ } }
Expand Down
32 changes: 29 additions & 3 deletions tests/lib/bun-runtime-preflight.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,18 +171,44 @@ describe("selected Bun write preflight", () => {
test("a replacement empty root with a different inode survives refusal", () => {
const parent = temporary();
const root = join(parent, "replaced");
let originalIno = 0;
let originalIno = 0n;
refusal(() => assertSelectedRuntimeWritable(runtime, root, {
...windows, rootWasAbsent: true, spawnSync: (() => {
originalIno = lstatSync(root).ino;
originalIno = lstatSync(root, { bigint: true }).ino;
renameSync(root, join(parent, "old")); mkdirSync(root);
return { status: 1, stdout: "nonce:create" };
}) as typeof spawnSync,
}), "create");
expect(lstatSync(root).ino).not.toBe(originalIno);
// NTFS file IDs exceed 2^53; as numbers, adjacent IDs compare equal.
expect(lstatSync(root, { bigint: true }).ino).not.toBe(originalIno);
expect(readdirSync(root)).toEqual([]);
});

test("a replacement probe whose file ID differs only above 2^53 is never removed", () => {
const root = temporary();
const probe = join(root, ".ocx-runtime-probe-nonce");
let replaced = false;
let removals = 0;
// Adjacent IDs above 2^53 collapse to one Number: the owned probe and its replacement.
const lstat = ((path: string, options?: { bigint?: boolean }) => {
const real = lstatSync(path, { bigint: true });
const ino = path === probe ? (replaced ? 2n ** 53n : 2n ** 53n + 1n) : real.ino;
return {
dev: options?.bigint ? real.dev : Number(real.dev), ino: options?.bigint ? ino : Number(ino),
isDirectory: () => real.isDirectory(), isSymbolicLink: () => real.isSymbolicLink(),
};
}) as RuntimePreflightFs["lstatSync"];
refusal(() => assertSelectedRuntimeWritable(standalone, root, {
...windows, fs: { ...fs, lstatSync: lstat, rmdirSync: path => {
if (removals++ > 0) { rmdirSync(path); return; }
renameSync(path, join(root, "owned-probe")); mkdirSync(path); replaced = true;
throw new Error("denied once");
} },
}), "remove");
expect(existsSync(probe)).toBe(true);
expect(removals).toBe(1);
});

test("refusal never inspects emptiness or deletes the config root", () => {
const root = join(temporary(), "root");
const cleanup: string[] = [];
Expand Down
11 changes: 8 additions & 3 deletions tests/providers/cc-switch-import.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1006,7 +1006,10 @@ describe("CC-Switch keyless provider import", () => {
try {
writer.run("PRAGMA journal_mode = WAL;");
writer.run("CREATE TABLE providers (id TEXT NOT NULL, app_type TEXT NOT NULL, name TEXT NOT NULL, settings_config TEXT NOT NULL, sort_index INTEGER, PRIMARY KEY (id, app_type))");
writer.prepare("INSERT INTO providers VALUES (?1, ?2, ?3, ?4, ?5)").run(
// writer.run finalizes its statement; an unfinalized prepare() keeps the db, -wal and -shm
// open past close(), and Windows then refuses to delete the temp tree (EBUSY).
writer.run(
"INSERT INTO providers VALUES (?1, ?2, ?3, ?4, ?5)",
"existing-prov",
"openai",
"Existing",
Expand All @@ -1017,10 +1020,12 @@ describe("CC-Switch keyless provider import", () => {

// Disable auto-checkpoint so subsequent UPDATE and INSERT stay exclusively in live-wal.db-wal.
writer.run("PRAGMA wal_autocheckpoint = 0;");
writer.prepare("UPDATE providers SET settings_config = ?1 WHERE id = 'existing-prov'").run(
writer.run(
"UPDATE providers SET settings_config = ?1 WHERE id = 'existing-prov'",
JSON.stringify({ baseUrl: "https://updated.acme.example/v1" }),
);
writer.prepare("INSERT INTO providers VALUES (?1, ?2, ?3, ?4, ?5)").run(
writer.run(
"INSERT INTO providers VALUES (?1, ?2, ?3, ?4, ?5)",
"uncheckpointed-prov",
"claude",
"Uncheckpointed",
Expand Down
Loading