import { constants } from "node:fs"; import { link, lstat, mkdir, open, realpath, unlink, type FileHandle } from "node:fs/promises"; import { isAbsolute, join, relative, resolve, sep } from "node:path"; import type { Readable } from "node:stream"; export class WorkspaceFileBoundaryError extends Error { constructor( message: string, readonly requestedPath: string, readonly reason: "not_found" | "boundary" | "io" | "limit" = "boundary", options?: ErrorOptions, ) { super(message, options); this.name = "WorkspaceFileBoundaryError"; } } export interface WorkspaceFileSnapshot { readonly path: string; readonly name: string; readonly data: Buffer; } export interface WorkspaceFileWriteResult { readonly path: string; readonly device: number; readonly inode: number; } interface OpenDirectory { readonly handle: FileHandle; readonly displayPath: string; } /** Read a stable snapshot without following any symlink below the workspace. */ export async function readWorkspaceFileNoFollow( workspaceRoot: string, workspaceDir: string, requestedPath: string, maxBytes?: number, ): Promise { const workspace = await canonicalWorkspace(workspaceRoot, workspaceDir); const components = fileComponents(workspace, requestedPath); const name = components.at(-1)!; const directories = components.slice(0, -1); const parent = await openDirectoryTree(workspace, directories, false, requestedPath); const openedPath = childPath(parent, name); let file: FileHandle | undefined; let result: WorkspaceFileSnapshot | undefined; let failure: unknown; try { file = await open(openedPath, constants.O_RDONLY | constants.O_NOFOLLOW); const metadata = await file.stat(); if (!metadata.isFile()) { throw new WorkspaceFileBoundaryError(`deliverable is not a regular file: ${requestedPath}`, requestedPath); } if (maxBytes !== undefined && metadata.size > maxBytes) { throw new WorkspaceFileBoundaryError( `workspace file exceeds ${maxBytes} bytes: ${requestedPath}`, requestedPath, "limit", ); } await assertNameStillReferences(parent, name, metadata.dev, metadata.ino, requestedPath); const data = await readFileSnapshot(file, maxBytes, requestedPath); result = { path: join(workspace, ...components), name, data }; } catch (error) { failure = boundaryError(error, requestedPath, "cannot read workspace file without following symlinks"); } await finishWithCleanup( failure, [file, parent.handle], `workspace file read cleanup failed: ${requestedPath}`, ); return result!; } async function readFileSnapshot( file: FileHandle, maxBytes: number | undefined, requestedPath: string, ): Promise { if (maxBytes === undefined) return file.readFile(); const chunks: Buffer[] = []; let total = 0; let position = 0; while (true) { const remainingWithSentinel = maxBytes - total + 1; const chunk = Buffer.allocUnsafe(Math.min(64 * 1024, remainingWithSentinel)); const { bytesRead } = await file.read(chunk, 0, chunk.length, position); if (bytesRead === 0) return Buffer.concat(chunks, total); total += bytesRead; if (total > maxBytes) { throw new WorkspaceFileBoundaryError( `workspace file exceeds ${maxBytes} bytes: ${requestedPath}`, requestedPath, "limit", ); } chunks.push(chunk.subarray(0, bytesRead)); position += bytesRead; } } /** * Create one inbound file exclusively below the workspace and stream into its * already-open descriptor. Linux uses /proc/self/fd-relative traversal so a * concurrent directory rename/symlink swap cannot redirect the write. */ export async function writeNewWorkspaceFileNoFollow( workspaceRoot: string, workspaceDir: string, requestedPath: string, source: Readable, maxBytes?: number, ): Promise { return (await writeNewWorkspaceFileNoFollowTracked( workspaceRoot, workspaceDir, requestedPath, source, maxBytes, )).path; } /** Create one inbound file and return its identity for race-safe compensation. */ export async function writeNewWorkspaceFileNoFollowTracked( workspaceRoot: string, workspaceDir: string, requestedPath: string, source: Readable, maxBytes?: number, ): Promise { const workspace = await canonicalWorkspace(workspaceRoot, workspaceDir); const components = fileComponents(workspace, requestedPath); const name = components.at(-1)!; const directories = components.slice(0, -1); const parent = await openDirectoryTree(workspace, directories, true, requestedPath); const openedPath = childPath(parent, name); let file: FileHandle | undefined; let device: number | undefined; let inode: number | undefined; let result: WorkspaceFileWriteResult | undefined; let failure: unknown; const cleanupFailures: unknown[] = []; try { file = await open( openedPath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600, ); const metadata = await file.stat(); device = metadata.dev; inode = metadata.ino; let bytesWritten = 0; for await (const chunk of source) { const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk as Uint8Array); bytesWritten += buffer.length; if (maxBytes !== undefined && bytesWritten > maxBytes) { throw new WorkspaceFileBoundaryError( `inbound file exceeds ${maxBytes} bytes: ${requestedPath}`, requestedPath, "limit", ); } let offset = 0; while (offset < buffer.length) { const { bytesWritten } = await file.write(buffer, offset, buffer.length - offset, null); if (bytesWritten === 0) throw new Error("inbound workspace write made no progress"); offset += bytesWritten; } } await file.sync(); await assertNameStillReferences(parent, name, metadata.dev, metadata.ino, requestedPath); result = { path: join(workspace, ...components), device: metadata.dev, inode: metadata.ino, }; } catch (error) { failure = boundaryError(error, requestedPath, "cannot create inbound workspace file safely"); if (device !== undefined && inode !== undefined) { try { await unlinkIfSameFile(parent, name, device, inode); } catch (cleanupError) { cleanupFailures.push(cleanupError); } } } await finishWithCleanup( failure, [file, parent.handle], `inbound workspace file cleanup failed: ${requestedPath}`, cleanupFailures, ); return result!; } /** Remove only the exact file created by a tracked no-follow write. */ export async function removeWorkspaceFileIfUnchangedNoFollow( workspaceRoot: string, workspaceDir: string, requestedPath: string, expectedDevice: number, expectedInode: number, ): Promise { const workspace = await canonicalWorkspace(workspaceRoot, workspaceDir); const components = fileComponents(workspace, requestedPath); const name = components.at(-1)!; const parent = await openDirectoryTree(workspace, components.slice(0, -1), false, requestedPath); let failure: unknown; try { await unlinkIfSameFile(parent, name, expectedDevice, expectedInode); } catch (error) { failure = boundaryError(error, requestedPath, "cannot compensate inbound workspace file safely"); } await finishWithCleanup( failure, [parent.handle], `inbound workspace compensation cleanup failed: ${requestedPath}`, ); } /** * Publish a protected same-filesystem staging file with one exclusive hard-link * operation. The target path is traversed without following workspace symlinks. */ export async function linkWorkspaceFileNoFollow( workspaceRoot: string, workspaceDir: string, requestedPath: string, stagedPath: string, ): Promise { const workspace = await canonicalWorkspace(workspaceRoot, workspaceDir); const components = fileComponents(workspace, requestedPath); const name = components.at(-1)!; const parent = await openDirectoryTree(workspace, components.slice(0, -1), true, requestedPath); let source: FileHandle | undefined; let result: WorkspaceFileWriteResult | undefined; let failure: unknown; const cleanupFailures: unknown[] = []; try { source = await open(stagedPath, constants.O_RDONLY | constants.O_NOFOLLOW); const metadata = await source.stat(); if (!metadata.isFile()) { throw new WorkspaceFileBoundaryError(`staged resource is not a regular file: ${stagedPath}`, stagedPath); } const targetPath = childPath(parent, name); await link(stagedPath, targetPath); try { await assertNameStillReferences(parent, name, metadata.dev, metadata.ino, requestedPath); } catch (error) { await unlinkIfSameFile(parent, name, metadata.dev, metadata.ino).catch((cleanupError) => { cleanupFailures.push(cleanupError); }); throw error; } result = { path: join(workspace, ...components), device: metadata.dev, inode: metadata.ino, }; } catch (error) { failure = boundaryError(error, requestedPath, "cannot publish staged workspace file safely"); } try { await finishWithCleanup( failure, [source, parent.handle], `staged workspace publication cleanup failed: ${requestedPath}`, cleanupFailures, ); } catch (error) { if (result !== undefined) { try { await removeWorkspaceFileIfUnchangedNoFollow( workspaceRoot, workspaceDir, requestedPath, result.device, result.inode, ); } catch (compensationError) { throw new AggregateError( [error, compensationError], `staged workspace publication and compensation both failed: ${requestedPath}`, { cause: error }, ); } } throw error; } return result!; } async function canonicalWorkspace(workspaceRoot: string, workspaceDir: string): Promise { if (process.platform !== "linux") { throw new WorkspaceFileBoundaryError( "race-safe descriptor-relative workspace access requires Linux", workspaceDir, ); } if (!isAbsolute(workspaceRoot) || !isAbsolute(workspaceDir)) { throw new WorkspaceFileBoundaryError("workspace root and project path must be absolute", workspaceDir); } const configuredRoot = resolve(workspaceRoot); const configuredWorkspace = resolve(workspaceDir); const rel = relative(configuredRoot, configuredWorkspace); if (rel === "" || rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) { throw new WorkspaceFileBoundaryError("project workspace escapes configured workspace root", workspaceDir); } const rootMetadata = await lstat(configuredRoot); if (rootMetadata.isSymbolicLink() || !rootMetadata.isDirectory()) { throw new WorkspaceFileBoundaryError("configured workspace root is not a real directory", workspaceRoot); } const root = await realpath(configuredRoot); const components = rel.split(sep); const openedWorkspace = await openDirectoryTree(root, components, false, workspaceDir); let result: string | undefined; let failure: unknown; try { const openedTarget = await realpath(`/proc/self/fd/${openedWorkspace.handle.fd}`); const expected = join(root, ...components); if (openedTarget !== expected) { throw new WorkspaceFileBoundaryError("project workspace contains a symlink", workspaceDir); } result = expected; } catch (error) { failure = boundaryError(error, workspaceDir, "project workspace validation failed"); } await finishWithCleanup( failure, [openedWorkspace.handle], `project workspace validation cleanup failed: ${workspaceDir}`, ); return result!; } function fileComponents(root: string, requestedPath: string): string[] { const token = requestedPath.trim(); if (token === "" || token.includes("\0")) { throw new WorkspaceFileBoundaryError("workspace file path is empty or invalid", requestedPath); } const absolute = isAbsolute(token) ? resolve(token) : resolve(root, token); const rel = relative(root, absolute); if (rel === "" || rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) { throw new WorkspaceFileBoundaryError(`workspace file path escapes current project: ${requestedPath}`, requestedPath); } const components = rel.split(/[\\/]/); if (components.some((component) => component === "" || component === "." || component === "..")) { throw new WorkspaceFileBoundaryError(`workspace file path is not normalized: ${requestedPath}`, requestedPath); } return components; } async function openDirectoryTree( root: string, components: readonly string[], create: boolean, requestedPath: string, ): Promise { let current: OpenDirectory = { handle: await open(root, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW), displayPath: root, }; try { for (const component of components) { const path = childPath(current, component); let next: FileHandle; try { next = await open(path, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW); } catch (error) { if (!create || !isErrno(error, "ENOENT")) throw error; try { await mkdir(path, { mode: 0o700 }); } catch (mkdirError) { if (!isErrno(mkdirError, "EEXIST")) throw mkdirError; } next = await open(path, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW); } const previous = current; current = { handle: next, displayPath: join(previous.displayPath, component) }; await previous.handle.close(); } return current; } catch (error) { try { await current.handle.close(); } catch (closeError) { throw new AggregateError( [error, closeError], `workspace path validation and directory close both failed: ${requestedPath}`, { cause: error }, ); } throw boundaryError(error, requestedPath, "workspace path contains a symlink or non-directory component"); } } function childPath(parent: OpenDirectory, name: string): string { return `/proc/self/fd/${parent.handle.fd}/${name}`; } async function assertNameStillReferences( parent: OpenDirectory, name: string, device: number, inode: number, requestedPath: string, ): Promise { const metadata = await lstat(childPath(parent, name)); if (metadata.isSymbolicLink() || metadata.dev !== device || metadata.ino !== inode) { throw new WorkspaceFileBoundaryError( `workspace file changed during no-follow access: ${requestedPath}`, requestedPath, ); } } async function unlinkIfSameFile(parent: OpenDirectory, name: string, device: number, inode: number): Promise { const path = childPath(parent, name); try { const metadata = await lstat(path); if (!metadata.isSymbolicLink() && metadata.dev === device && metadata.ino === inode) { await unlink(path); } } catch (error) { if (!isErrno(error, "ENOENT")) throw error; } } function boundaryError(error: unknown, requestedPath: string, prefix: string): WorkspaceFileBoundaryError { if (error instanceof WorkspaceFileBoundaryError) return error; const detail = error instanceof Error ? error.message : String(error); const reason = isErrno(error, "ENOENT") ? "not_found" : isErrno(error, "ELOOP") || isErrno(error, "ENOTDIR") ? "boundary" : "io"; return new WorkspaceFileBoundaryError( `${prefix}: ${requestedPath} (${detail})`, requestedPath, reason, { cause: error }, ); } async function finishWithCleanup( primaryFailure: unknown, handles: ReadonlyArray, message: string, additionalFailures: readonly unknown[] = [], ): Promise { const settled = await Promise.allSettled( handles.filter((handle): handle is FileHandle => handle !== undefined).map((handle) => handle.close()), ); const cleanupFailures = [ ...additionalFailures, ...settled.flatMap((result) => result.status === "rejected" ? [result.reason] : []), ]; if (primaryFailure !== undefined && cleanupFailures.length === 0) throw primaryFailure; if (primaryFailure !== undefined || cleanupFailures.length > 0) { const failures = primaryFailure === undefined ? cleanupFailures : [primaryFailure, ...cleanupFailures]; throw new AggregateError(failures, message, { cause: primaryFailure ?? cleanupFailures[0] }); } } function isErrno(error: unknown, code: string): error is NodeJS.ErrnoException { return error instanceof Error && "code" in error && error.code === code; }