diff --git a/scripts/test-run-lock.ts b/scripts/test-run-lock.ts index 77e5bfa99d..edabe8ccfd 100644 --- a/scripts/test-run-lock.ts +++ b/scripts/test-run-lock.ts @@ -12,9 +12,17 @@ import { writeFileSync, } from "node:fs"; import { hostname, tmpdir } from "node:os"; -import { isAbsolute, join, posix, win32 } from "node:path"; +import { join, posix, win32 } from "node:path"; +import type { UserIdentity } from "../src/codex/convergence-types"; +import { + resolveEffectiveUserIdentity, + resolveEffectiveUserRuntimeRoot, +} from "../src/codex/user-identity"; +import { hardenSecretDir } from "../src/lib/windows-secret-acl"; export const TEST_RUN_ID_ENV = "OCX_TEST_RUN_ID"; +export const TEST_RUN_LOCK_PATH_ENV = "OCX_TEST_RUN_LOCK_PATH"; +export const TEST_RUN_LOCK_TOKEN_ENV = "OCX_TEST_RUN_LOCK_TOKEN"; export const TEST_RUN_NO_QUEUE_ENV = "OCX_TEST_NO_QUEUE"; const OWNER_FILE = "owner.json"; const MEMBERS_DIR = "members"; @@ -30,7 +38,7 @@ interface RuntimeDirectoryEntry { export interface TestRunRuntimeFileSystem { lstatSync(path: string): RuntimeDirectoryEntry; - mkdirSync(path: string, options: { mode: number }): void; + mkdirSync(path: string, options: { mode?: number; recursive?: boolean }): void; accessSync(path: string, mode: number): void; } @@ -41,6 +49,9 @@ export interface ResolveDefaultTestRunLockPathOptions { tempDir?: string; hostName?: string; fileSystem?: TestRunRuntimeFileSystem; + resolveIdentity?: () => UserIdentity; + resolveRuntimeRoot?: (identity: Extract) => string; + hardenWindowsDirectory?: (path: string) => void; } const runtimeFileSystem: TestRunRuntimeFileSystem = { @@ -67,6 +78,8 @@ export interface AcquireTestRunLockOptions { runId: string; ownerPid?: number; lockPath?: string; + validatedRuntimePath?: boolean; + joinExistingOwnerToken?: string; pollMs?: number; maxWaitMs?: number; env?: NodeJS.ProcessEnv; @@ -74,6 +87,23 @@ export interface AcquireTestRunLockOptions { onAcquiredAfterWait?: (elapsedMs: number) => void; } +export interface ResolveInheritedTestRunLockOptions { + wrappedRunId?: string; + env?: NodeJS.ProcessEnv; + platform?: NodeJS.Platform; + hostName?: string; +} + +export interface InheritedTestRunLock { + lockPath: string; + ownerToken: string; +} + +export interface ResolveWrappedTestRunLockPathOptions { + env?: NodeJS.ProcessEnv; + resolve?: (options: ResolveDefaultTestRunLockPathOptions) => string; +} + export interface BareTestRunIdentity { ownerPid: number; runId: string; @@ -89,11 +119,13 @@ function inspectRuntimeDirectory(options: { fileSystem: TestRunRuntimeFileSystem; expectedUid?: number; requirePrivateMode?: boolean; + allowMissing?: boolean; }): string | null { let entry: RuntimeDirectoryEntry; try { entry = options.fileSystem.lstatSync(options.path); } catch (error) { + if (options.allowMissing && errorCode(error) === "ENOENT") return null; return `cannot be inspected (${errorCode(error)})`; } if (entry.isSymbolicLink() || !entry.isDirectory()) return "is not a real directory"; @@ -117,11 +149,55 @@ function machineDiscriminator(hostName: string): string { return createHash("sha256").update(normalized).digest("hex").slice(0, 16); } +/** + * Read a Windows-wrapper-provided lock path without repeating effective-user + * discovery in every Bun worker. Bare and POSIX runs never trust this environment + * value. Wrapped Windows paths are constrained to the exact host-specific lock + * filename and namespace shape; the wrapper remains responsible for resolving + * and validating the directory. + */ +export function resolveInheritedTestRunLock( + options: ResolveInheritedTestRunLockOptions, +): InheritedTestRunLock | undefined { + if (!options.wrappedRunId) return undefined; + const platform = options.platform ?? process.platform; + if (platform !== "win32") return undefined; + if (options.env?.[TEST_RUN_NO_QUEUE_ENV] === "1") return undefined; + const candidate = options.env?.[TEST_RUN_LOCK_PATH_ENV]?.trim(); + const ownerToken = options.env?.[TEST_RUN_LOCK_TOKEN_ENV]?.trim(); + if (!candidate || !ownerToken) { + throw new Error("The wrapped Bun test lock capability is incomplete; refusing inherited lock access."); + } + + const expectedName = `opencodex-bun-test-${machineDiscriminator(options.hostName ?? hostname())}.lock`; + if ( + !win32.isAbsolute(candidate) + || win32.basename(candidate) !== expectedName + || win32.basename(win32.dirname(candidate)) !== "bun-test-locks" + || !/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(ownerToken) + ) { + throw new Error("The wrapped Bun test lock capability is invalid; refusing inherited lock access."); + } + return { lockPath: candidate, ownerToken }; +} + +/** Resolve once in the wrapper, preserving the no-queue escape hatch as a true no-op. */ +export function resolveWrappedTestRunLockPath( + options: ResolveWrappedTestRunLockPathOptions = {}, +): string | undefined { + const env = options.env ?? process.env; + if (env[TEST_RUN_NO_QUEUE_ENV] === "1") return undefined; + return (options.resolve ?? resolveDefaultTestRunLockPath)({ env }); +} + /** * Resolve a user-scoped, machine-local default lock path without relying on HOME. * + * Windows uses the effective-token SID and known-folder runtime root, then + * hardens a dedicated child with the repository's required ACL policy. * POSIX XDG runtime directories are accepted only after an ownership and access - * check. The fallback is a private UID namespace under the OS temp directory. + * check, including exact mode 0700. The fallback is a private UID namespace + * under the OS temp directory. * The hostname digest remains part of the lock name in either case: even if an * administrator redirects either root to shared storage, host-local PID liveness * checks can never reclaim or join another machine's lock. @@ -144,18 +220,84 @@ export function resolveDefaultTestRunLockPath( } if (platform === "win32") { - if (!win32.isAbsolute(tempDir)) { + let identity: UserIdentity; + try { + identity = (options.resolveIdentity ?? resolveEffectiveUserIdentity)(); + } catch (cause) { throw new Error( - "Cannot resolve a safe user-scoped Bun test lock: the Windows OS temp/profile path is not absolute.", + "Cannot resolve a safe user-scoped Bun test lock: the Windows effective identity is unavailable.", + { cause }, ); } - const issue = inspectRuntimeDirectory({ path: tempDir, fileSystem }); - if (issue) { + if (identity.platform !== "win32") { throw new Error( - `Cannot resolve a safe user-scoped Bun test lock: the Windows OS temp/profile directory ${issue}.`, + "Cannot resolve a safe user-scoped Bun test lock: the effective identity does not match Windows.", ); } - return win32.join(tempDir, `opencodex-bun-test-${discriminator}.lock`); + + let runtimeRoot: string; + try { + runtimeRoot = (options.resolveRuntimeRoot ?? resolveEffectiveUserRuntimeRoot)(identity); + } catch (cause) { + throw new Error( + "Cannot resolve a safe user-scoped Bun test lock: the Windows effective-user runtime is unavailable.", + { cause }, + ); + } + if (!win32.isAbsolute(runtimeRoot)) { + throw new Error( + "Cannot resolve a safe user-scoped Bun test lock: the Windows effective-user runtime path is not absolute.", + ); + } + + const lockRoot = win32.join(runtimeRoot, "bun-test-locks"); + const issueBeforeCreate = inspectRuntimeDirectory({ + path: lockRoot, + fileSystem, + allowMissing: true, + }); + if (issueBeforeCreate) { + throw new Error( + `Cannot resolve a safe user-scoped Bun test lock: the Windows lock directory ${issueBeforeCreate}.`, + ); + } + + try { + fileSystem.mkdirSync(lockRoot, { recursive: true }); + } catch (cause) { + throw new Error( + "Cannot resolve a safe user-scoped Bun test lock: the Windows lock directory cannot be created.", + { cause }, + ); + } + const issueBeforeHardening = inspectRuntimeDirectory({ path: lockRoot, fileSystem }); + if (issueBeforeHardening) { + throw new Error( + `Cannot resolve a safe user-scoped Bun test lock: the Windows lock directory ${issueBeforeHardening}.`, + ); + } + try { + const harden = options.hardenWindowsDirectory + ?? ((path: string) => { + if (!hardenSecretDir(path, { required: true }).ok) { + throw new Error("required ACL hardening did not complete"); + } + }); + harden(lockRoot); + } catch (cause) { + throw new Error( + "Cannot resolve a safe user-scoped Bun test lock: the Windows lock directory cannot be secured.", + { cause }, + ); + } + + const issueAfterHardening = inspectRuntimeDirectory({ path: lockRoot, fileSystem }); + if (issueAfterHardening) { + throw new Error( + `Cannot resolve a safe user-scoped Bun test lock: the Windows lock directory ${issueAfterHardening}.`, + ); + } + return win32.join(lockRoot, `opencodex-bun-test-${discriminator}.lock`); } const uid = options.uid ?? (typeof process.getuid === "function" ? process.getuid() : undefined); @@ -168,20 +310,21 @@ export function resolveDefaultTestRunLockPath( const failures: string[] = []; const xdgRuntimeDir = env.XDG_RUNTIME_DIR?.trim(); if (xdgRuntimeDir) { - if (!isAbsolute(xdgRuntimeDir)) { + if (!posix.isAbsolute(xdgRuntimeDir)) { failures.push("XDG_RUNTIME_DIR is not absolute"); } else { const issue = inspectRuntimeDirectory({ path: xdgRuntimeDir, fileSystem, expectedUid: uid, + requirePrivateMode: true, }); if (!issue) return posix.join(xdgRuntimeDir, `opencodex-bun-test-${discriminator}.lock`); failures.push(`XDG_RUNTIME_DIR ${issue}`); } } - if (!isAbsolute(tempDir)) { + if (!posix.isAbsolute(tempDir)) { failures.push("the OS temporary directory is not absolute"); } else { const fallback = posix.join(tempDir, `opencodex-test-runtime-${uid}`); @@ -206,7 +349,7 @@ export function resolveDefaultTestRunLockPath( throw new Error( "Cannot resolve a safe user-scoped Bun test lock. " - + "Ensure XDG_RUNTIME_DIR is an existing writable directory owned by the current uid, " + + "Ensure XDG_RUNTIME_DIR is an existing writable mode-0700 directory owned by the current uid, " + `or make the OS temporary directory usable for a mode-0700 UID runtime (${failures.join("; ")}).`, ); } @@ -328,7 +471,7 @@ export async function acquireTestRunLock(options: AcquireTestRunLockOptions): Pr return { acquired: false, owner: null, release() {} }; } - const usesDefaultLockPath = options.lockPath === undefined; + const usesDefaultLockPath = options.lockPath === undefined || options.validatedRuntimePath === true; const lockPath = options.lockPath ?? resolveDefaultTestRunLockPath({ env }); const ownerPid = options.ownerPid ?? process.pid; const pollMs = Math.max(1, options.pollMs ?? 5_000); @@ -336,6 +479,22 @@ export async function acquireTestRunLock(options: AcquireTestRunLockOptions): Pr const startedAt = Date.now(); let announced = false; + if (options.joinExistingOwnerToken !== undefined) { + const current = readOwner(lockPath); + if ( + current?.runId === options.runId + && current.token === options.joinExistingOwnerToken + && lockIsLive(lockPath, current) + && registerMember(lockPath, current, process.pid) + ) { + return { acquired: false, owner: current, release() {} }; + } + throw new Error( + "Cannot join the wrapper-owned Bun test lock because its exact live owner no longer matches; " + + "refusing to create or reclaim an inherited path.", + ); + } + for (;;) { const owner: TestRunLockOwner = { version: 1, @@ -367,7 +526,7 @@ export async function acquireTestRunLock(options: AcquireTestRunLockOptions): Pr if (usesDefaultLockPath && ["EACCES", "ENOENT", "EPERM", "EROFS"].includes(code ?? "")) { throw new Error( "Cannot acquire the user-scoped Bun test lock because its validated runtime directory " - + "became unavailable or unwritable. Check XDG_RUNTIME_DIR and the OS temporary directory.", + + "became unavailable or unwritable. Check the effective-user runtime directory and its permissions.", { cause: error }, ); } diff --git a/scripts/test.ts b/scripts/test.ts index 8569518fab..3e34655175 100644 --- a/scripts/test.ts +++ b/scripts/test.ts @@ -2,7 +2,13 @@ import { randomUUID } from "node:crypto"; import { existsSync, mkdirSync, mkdtempSync, rmSync } from "node:fs"; import { homedir, tmpdir } from "node:os"; import { join } from "node:path"; -import { acquireTestRunLock, TEST_RUN_ID_ENV } from "./test-run-lock"; +import { + acquireTestRunLock, + resolveWrappedTestRunLockPath, + TEST_RUN_ID_ENV, + TEST_RUN_LOCK_PATH_ENV, + TEST_RUN_LOCK_TOKEN_ENV, +} from "./test-run-lock"; export interface IsolatedTestEnvironment { root: string; @@ -383,8 +389,18 @@ function waitWithTimeout(promise: Promise, timeoutMs: number): Promise { - const isolated = createIsolatedTestEnvironment({ ...process.env, [TEST_RUN_ID_ENV]: runId }); +async function runTestLane( + lane: BunTestLane, + runId: string, + inheritedLock: { lockPath: string; ownerToken: string } | undefined, + capture = false, +): Promise<{ exitCode: number; output: string }> { + const isolated = createIsolatedTestEnvironment({ + ...process.env, + [TEST_RUN_ID_ENV]: runId, + [TEST_RUN_LOCK_PATH_ENV]: inheritedLock?.lockPath, + [TEST_RUN_LOCK_TOKEN_ENV]: inheritedLock?.ownerToken, + }); const startedAt = Date.now(); let interrupted: NodeJS.Signals | null = null; const child = Bun.spawn([process.execPath, "test", ...lane.args], { @@ -501,8 +517,11 @@ if (import.meta.main) { ); } const runId = randomUUID(); + const lockPath = resolveWrappedTestRunLockPath({ env: process.env }); const lock = await acquireTestRunLock({ runId, + lockPath, + validatedRuntimePath: lockPath !== undefined, onWait: owner => console.warn( `[test] another Bun test run${owner ? ` (pid ${owner.pid})` : ""} holds the user lock; waiting. ` + "Set OCX_TEST_NO_QUEUE=1 only for intentional overlap.", @@ -511,10 +530,13 @@ if (import.meta.main) { }); const startedAt = Date.now(); try { + const inheritedLock = process.platform === "win32" && lockPath && lock.owner + ? { lockPath, ownerToken: lock.owner.token } + : undefined; let exitCode = 0; let captured = ""; for (const lane of resolveBunTestPlan(requestedTests, changedRun?.comparisonCommit)) { - const result = await runTestLane(lane, runId, Boolean(changedRun)); + const result = await runTestLane(lane, runId, inheritedLock, Boolean(changedRun)); captured += result.output; if (result.exitCode !== 0 && exitCode === 0) exitCode = result.exitCode; if ([124, 130, 143].includes(result.exitCode)) break; diff --git a/src/codex/user-identity.ts b/src/codex/user-identity.ts index 2fed73b54d..a0f021dba4 100644 --- a/src/codex/user-identity.ts +++ b/src/codex/user-identity.ts @@ -437,6 +437,18 @@ function resolveWindowsRuntimeRoot(identity: Extract { )); }); +test("the effective-user runtime root is an absolute canonical private namespace", () => { + const identity = resolveEffectiveUserIdentity(); + const runtimeRoot = resolveEffectiveUserRuntimeRoot(identity); + const entry = lstatSync(runtimeRoot); + + expect(isAbsolute(runtimeRoot)).toBe(true); + expect(samePathIdentity(realpathSync.native(runtimeRoot), runtimeRoot)).toBe(true); + expect(entry.isDirectory()).toBe(true); + expect(entry.isSymbolicLink()).toBe(false); + expect(parse(runtimeRoot).ext).not.toBe(".sqlite"); + if (identity.platform === "posix") { + expect(parse(runtimeRoot).base).toBe(`opencodex-runtime-v1-${identity.uid}`); + expect(entry.uid).toBe(identity.uid); + expect(entry.mode & 0o777).toBe(0o700); + } else { + expect(parse(runtimeRoot).base).toBe(identity.sid.toUpperCase()); + expect(parse(parse(runtimeRoot).dir).base).toBe("v1"); + } + + expect(() => resolveEffectiveUserRuntimeRoot({ + platform: "win32", + sid: "not-a-sid", + })).toThrow("invalid SID"); +}); + test("real processes resolve one identity and coordinator path across every home/runtime environment", async () => { const canonicalHome = realpathSync.native(codexHome); const environmentRoots = ["a", "b"].map(label => { diff --git a/tests/preload.ts b/tests/preload.ts index dd04c5c33c..6e2baa0ed1 100644 --- a/tests/preload.ts +++ b/tests/preload.ts @@ -13,7 +13,12 @@ */ import { isTestHomeGuardArmed, protectedHomeForTests } from "../src/lib/test-home-guard"; import { createIsolatedTestEnvironment } from "../scripts/test"; -import { acquireTestRunLock, resolveBareTestRunIdentity, TEST_RUN_ID_ENV } from "../scripts/test-run-lock"; +import { + acquireTestRunLock, + resolveBareTestRunIdentity, + resolveInheritedTestRunLock, + TEST_RUN_ID_ENV, +} from "../scripts/test-run-lock"; import { rmSync } from "node:fs"; // `scripts/test.ts` owns the lock for wrapped runs. A bare `bun test` has no wrapper, @@ -29,10 +34,17 @@ const bareIdentity = resolveBareTestRunIdentity({ workerId: process.env.BUN_TEST_WORKER_ID, }); const runId = wrappedRunId || bareIdentity.runId; +const inheritedLock = resolveInheritedTestRunLock({ + wrappedRunId, + env: process.env, +}); process.env[TEST_RUN_ID_ENV] = runId; await acquireTestRunLock({ runId, ownerPid: bareIdentity.ownerPid, + lockPath: inheritedLock?.lockPath, + validatedRuntimePath: inheritedLock !== undefined, + joinExistingOwnerToken: inheritedLock?.ownerToken, onWait: owner => console.warn( `[test] bare Bun worker ${process.pid} is waiting for test run${owner ? ` pid ${owner.pid}` : ""} to release the user lock.`, ), diff --git a/tests/test-runner.test.ts b/tests/test-runner.test.ts index f81f0e6ba4..84c41c8f76 100644 --- a/tests/test-runner.test.ts +++ b/tests/test-runner.test.ts @@ -16,6 +16,10 @@ import { acquireTestRunLock, resolveBareTestRunIdentity, resolveDefaultTestRunLockPath, + resolveInheritedTestRunLock, + resolveWrappedTestRunLockPath, + TEST_RUN_LOCK_PATH_ENV, + TEST_RUN_LOCK_TOKEN_ENV, TEST_RUN_NO_QUEUE_ENV, type TestRunRuntimeFileSystem, } from "../scripts/test-run-lock"; @@ -47,11 +51,15 @@ function pathIsContainedBy(parent: string, candidate: string, platform: "posix" && relative !== ".." && !path.isAbsolute(relative)); } -function acceptingRuntimeFileSystem(uid: number, writable = true): TestRunRuntimeFileSystem { +function acceptingRuntimeFileSystem( + uid: number, + writable = true, + modes: Readonly> = {}, +): TestRunRuntimeFileSystem { return { - lstatSync: () => ({ + lstatSync: path => ({ uid, - mode: 0o700, + mode: modes[path] ?? 0o700, isDirectory: () => true, isSymbolicLink: () => false, }), @@ -421,21 +429,227 @@ describe("bun test user lock", () => { expect(pathIsContainedBy(common.env.HOME, secondHost, "posix")).toBe(false); }); - test("Windows uses the OS temp/profile result when USER is absent", () => { + test("Windows scopes the lock to the effective SID runtime and hardens its directory", () => { + const hardened: string[] = []; const common = { platform: "win32" as const, - tempDir: "C:\\Users\\Alice\\AppData\\Local\\Temp", + tempDir: "C:\\Windows\\Temp", hostName: "desktop-1", fileSystem: acceptingRuntimeFileSystem(0), + resolveRuntimeRoot: (identity: { platform: "win32"; sid: string }) => + `C:\\Runtime\\${identity.sid}`, + hardenWindowsDirectory: (path: string) => { hardened.push(path); }, }; - const withoutUser = resolveDefaultTestRunLockPath({ ...common, env: {} }); - const withUnrelatedUser = resolveDefaultTestRunLockPath({ + const alice = resolveDefaultTestRunLockPath({ + ...common, + env: {}, + resolveIdentity: () => ({ platform: "win32", sid: "S-1-5-21-1001" }), + }); + const aliceWithHostileEnvironment = resolveDefaultTestRunLockPath({ + ...common, + env: { + USER: "someone-else", + USERNAME: "someone-else", + USERDOMAIN: "hostile", + TEMP: "C:\\Windows\\Temp", + TMP: "C:\\Windows\\Temp", + LOCALAPPDATA: "C:\\Windows\\Temp", + }, + resolveIdentity: () => ({ platform: "win32", sid: "S-1-5-21-1001" }), + }); + const bob = resolveDefaultTestRunLockPath({ + ...common, + env: {}, + resolveIdentity: () => ({ platform: "win32", sid: "S-1-5-21-1002" }), + }); + + expect(aliceWithHostileEnvironment).toBe(alice); + expect(bob).not.toBe(alice); + expect(pathIsContainedBy("C:\\Runtime\\S-1-5-21-1001\\bun-test-locks", alice, "win32")) + .toBe(true); + expect(pathIsContainedBy(common.tempDir, alice, "win32")).toBe(false); + expect(hardened).toEqual([ + "C:\\Runtime\\S-1-5-21-1001\\bun-test-locks", + "C:\\Runtime\\S-1-5-21-1001\\bun-test-locks", + "C:\\Runtime\\S-1-5-21-1002\\bun-test-locks", + ]); + }); + + test("rejects a group-writable XDG root in favor of the private UID fallback", () => { + const xdg = "/run/user/1001"; + const fallback = "/tmp/opencodex-test-runtime-1001"; + const lockPath = resolveDefaultTestRunLockPath({ + platform: "linux", + env: { XDG_RUNTIME_DIR: xdg }, + uid: 1001, + tempDir: "/tmp", + hostName: "builder-1", + fileSystem: acceptingRuntimeFileSystem(1001, true, { + [xdg]: 0o733, + [fallback]: 0o700, + }), + }); + + expect(dirname(lockPath)).toBe(fallback); + }); + + test("Windows refuses before returning a path when identity or ACL hardening fails", () => { + const common = { + platform: "win32" as const, + tempDir: "C:\\Windows\\Temp", + hostName: "desktop-1", + fileSystem: acceptingRuntimeFileSystem(0), + }; + expect(() => resolveDefaultTestRunLockPath({ ...common, - env: { USER: "someone-else" }, + resolveIdentity: () => { throw new Error("identity unavailable"); }, + })).toThrow("the Windows effective identity is unavailable"); + + expect(() => resolveDefaultTestRunLockPath({ + ...common, + resolveIdentity: () => ({ platform: "win32", sid: "S-1-5-21-1001" }), + resolveRuntimeRoot: () => "C:\\Runtime\\S-1-5-21-1001", + hardenWindowsDirectory: () => { throw new Error("ACL unavailable"); }, + })).toThrow("the Windows lock directory cannot be secured"); + }); + + test("Windows rejects a redirected lock directory before ACL hardening", () => { + let hardenCalls = 0; + const fileSystem: TestRunRuntimeFileSystem = { + lstatSync: () => ({ + uid: 0, + mode: 0o700, + isDirectory: () => true, + isSymbolicLink: () => true, + }), + mkdirSync() {}, + accessSync() {}, + }; + + expect(() => resolveDefaultTestRunLockPath({ + platform: "win32", + hostName: "desktop-1", + fileSystem, + resolveIdentity: () => ({ platform: "win32", sid: "S-1-5-21-1001" }), + resolveRuntimeRoot: () => "C:\\Runtime\\S-1-5-21-1001", + hardenWindowsDirectory: () => { hardenCalls += 1; }, + })).toThrow("is not a real directory"); + expect(hardenCalls).toBe(0); + }); + + test("Windows creates a missing lock directory before validating and hardening it", () => { + let created = false; + let hardenCalls = 0; + const missing = Object.assign(new Error("missing"), { code: "ENOENT" }); + const fileSystem: TestRunRuntimeFileSystem = { + lstatSync: () => { + if (!created) throw missing; + return { + uid: 0, + mode: 0o700, + isDirectory: () => true, + isSymbolicLink: () => false, + }; + }, + mkdirSync() { created = true; }, + accessSync() {}, + }; + + const lockPath = resolveDefaultTestRunLockPath({ + platform: "win32", + hostName: "desktop-1", + fileSystem, + resolveIdentity: () => ({ platform: "win32", sid: "S-1-5-21-1001" }), + resolveRuntimeRoot: () => "C:\\Runtime\\S-1-5-21-1001", + hardenWindowsDirectory: () => { hardenCalls += 1; }, }); - expect(withoutUser).toBe(withUnrelatedUser); - expect(pathIsContainedBy(common.tempDir, withoutUser, "win32")).toBe(true); + expect(lockPath).toContain("\\bun-test-locks\\opencodex-bun-test-"); + expect(created).toBe(true); + expect(hardenCalls).toBe(1); + }); + + test("wrapped workers reuse one validated Windows lock path", () => { + let identityCalls = 0; + let runtimeRootCalls = 0; + let hardenCalls = 0; + const lockPath = resolveDefaultTestRunLockPath({ + platform: "win32", + hostName: "desktop-1", + fileSystem: acceptingRuntimeFileSystem(0), + resolveIdentity: () => { + identityCalls += 1; + return { platform: "win32", sid: "S-1-5-21-1001" }; + }, + resolveRuntimeRoot: () => { + runtimeRootCalls += 1; + return "C:\\Runtime\\S-1-5-21-1001"; + }, + hardenWindowsDirectory: () => { hardenCalls += 1; }, + }); + const ownerToken = "57f44b0e-b750-4bd2-b23d-4a035e75da18"; + const env = { + [TEST_RUN_LOCK_PATH_ENV]: lockPath, + [TEST_RUN_LOCK_TOKEN_ENV]: ownerToken, + }; + + const workers = ["worker-a", "worker-b", "worker-c"].map(wrappedRunId => + resolveInheritedTestRunLock({ + wrappedRunId, + env, + platform: "win32", + hostName: "desktop-1", + })); + + expect(workers).toEqual([ + { lockPath, ownerToken }, + { lockPath, ownerToken }, + { lockPath, ownerToken }, + ]); + expect(identityCalls).toBe(1); + expect(runtimeRootCalls).toBe(1); + expect(hardenCalls).toBe(1); + expect(() => resolveInheritedTestRunLock({ + wrappedRunId: "wrapped", + env: {}, + platform: "win32", + hostName: "desktop-1", + })).toThrow("capability is incomplete"); + expect(resolveInheritedTestRunLock({ + wrappedRunId: "wrapped", + env: { [TEST_RUN_NO_QUEUE_ENV]: "1" }, + platform: "win32", + hostName: "desktop-1", + })).toBeUndefined(); + expect(resolveInheritedTestRunLock({ + wrappedRunId: "wrapped", + env, + platform: "linux", + hostName: "desktop-1", + })).toBeUndefined(); + expect(() => resolveInheritedTestRunLock({ + wrappedRunId: "wrapped", + env: { + [TEST_RUN_LOCK_PATH_ENV]: "C:\\Runtime\\bun-test-locks\\wrong.lock", + [TEST_RUN_LOCK_TOKEN_ENV]: ownerToken, + }, + platform: "win32", + hostName: "desktop-1", + })).toThrow("inherited lock access"); + }); + + test("the no-queue wrapper path performs no identity or runtime mutation", () => { + let resolveCalls = 0; + const lockPath = resolveWrappedTestRunLockPath({ + env: { [TEST_RUN_NO_QUEUE_ENV]: "1" }, + resolve: () => { + resolveCalls += 1; + return "C:\\Runtime\\bun-test-locks\\unexpected.lock"; + }, + }); + + expect(lockPath).toBeUndefined(); + expect(resolveCalls).toBe(0); }); test("falls back from an unsafe XDG root to a validated mode-0700 UID directory", () => { @@ -530,6 +744,41 @@ describe("bun test user lock", () => { } }); + test("an inherited worker can only join the exact live wrapper owner", async () => { + const root = mkdtempSync(join(tmpdir(), "opencodex-test-lock-")); + const lockPath = join(root, "suite.lock"); + try { + const owner = await acquireTestRunLock({ runId: "wrapped", lockPath, pollMs: 5, maxWaitMs: 50 }); + expect(owner.owner).not.toBeNull(); + const sibling = await acquireTestRunLock({ + runId: "wrapped", + lockPath, + joinExistingOwnerToken: owner.owner!.token, + }); + expect(sibling.acquired).toBe(false); + const wrongToken = owner.owner!.token === "57f44b0e-b750-4bd2-b23d-4a035e75da18" + ? "6ab28966-06a7-4ef8-a0d9-23667d5d9ef5" + : "57f44b0e-b750-4bd2-b23d-4a035e75da18"; + + await expect(acquireTestRunLock({ + runId: "wrapped", + lockPath, + joinExistingOwnerToken: wrongToken, + })).rejects.toThrow("refusing to create or reclaim"); + + owner.release(); + expect(existsSync(lockPath)).toBe(false); + await expect(acquireTestRunLock({ + runId: "wrapped", + lockPath, + joinExistingOwnerToken: owner.owner!.token, + })).rejects.toThrow("refusing to create or reclaim"); + expect(existsSync(lockPath)).toBe(false); + } finally { + rmSync(root, { recursive: true, force: true }); + } + }); + test("a dead owner is reclaimed even when the next bare invocation derives the same run ID", async () => { const root = mkdtempSync(join(tmpdir(), "opencodex-test-lock-")); const lockPath = join(root, "suite.lock");