/** * Libc FFI boundary for anchored watcher directory handles. * * Loads system libc with platform-correct sonames, parses dirent records * using d_reclen/d_namlen bounds, and clears errno around readdir. * * @module src/serve/watch-snapshot-libc */ // bun:ffi — no Bun high-level openat/fdopendir; libc is required for fd-relative ops import { type Pointer, dlopen, FFIType, ptr, toArrayBuffer } from "bun:ffi"; // node:fs — flag constants for openat; no Bun equivalent import { constants as fsConstants } from "node:fs"; export type LibcSymbols = { openat: (dirfd: number, path: Pointer, flags: number) => number; fstatat: ( dirfd: number, path: Pointer, statBuffer: Pointer, flags: number ) => number; dup: (fd: number) => number; close: (fd: number) => number; fdopendir: (fd: number) => Pointer | null; readdir: (dirp: Pointer) => Pointer | null; closedir: (dirp: Pointer) => number; errnoPtr: () => Pointer; }; /** Platform dirent field layout (little-endian). */ export type DirentLayout = { /** Offset of d_reclen (uint16). Both Darwin and Linux: 16. */ dReclenOffset: number; /** * Offset of d_namlen (uint16) when present. * Darwin: 18. Linux glibc has no d_namlen (null). */ dNamlenOffset: number | null; /** Offset of d_name char array. Darwin: 21. Linux glibc: 19. */ dNameOffset: number; }; export type LoadedLibc = { /** Strong reference so FFI symbols stay live for the process lifetime. */ library: ReturnType; symbols: LibcSymbols; dirent: DirentLayout; openChildFlags: number; openDirFlags: number; atSymlinkNoFollow: number; statLayout: NativeStatLayout; }; type NativeStatLayout = { size: number; devOffset: number; devBytes: 4 | 8; modeOffset: number; modeBytes: 2 | 4; inoOffset: number; sizeOffset: number; mtimeSecOffset: number; mtimeNsecOffset: number; ctimeSecOffset: number; ctimeNsecOffset: number; }; export type FdRelativeStat = { isFile(): boolean; isDirectory(): boolean; isSymbolicLink(): boolean; dev: bigint | number; ino: bigint; size: bigint; mtimeNs: bigint; ctimeNs: bigint; }; let cachedLibc: LoadedLibc | null | undefined; /** * Deterministic libc soname candidates. * Never rely solely on `libc.` (Linux libc.so is often a linker script). */ export function libcLoadCandidates( platform: string = process.platform ): string[] { if (platform === "darwin") { return ["libSystem.B.dylib", "libc.dylib"]; } if (platform === "linux") { return ["libc.so.6", "libc.so"]; } return []; } function direntLayoutFor(platform: string): DirentLayout { if (platform === "darwin") { // struct dirent: d_ino(8) d_seekoff(8) d_reclen(2) d_namlen(2) d_type(1) d_name[] return { dReclenOffset: 16, dNamlenOffset: 18, dNameOffset: 21 }; } // glibc struct dirent: d_ino(8) d_off(8) d_reclen(2) d_type(1) d_name[] return { dReclenOffset: 16, dNamlenOffset: null, dNameOffset: 19 }; } function openLibcSymbols( path: string, errnoName: string ): ReturnType | null { try { return dlopen(path, { openat: { args: [FFIType.i32, FFIType.cstring, FFIType.i32], returns: FFIType.i32, }, fstatat: { args: [FFIType.i32, FFIType.cstring, FFIType.ptr, FFIType.i32], returns: FFIType.i32, }, dup: { args: [FFIType.i32], returns: FFIType.i32 }, close: { args: [FFIType.i32], returns: FFIType.i32 }, fdopendir: { args: [FFIType.i32], returns: FFIType.ptr }, readdir: { args: [FFIType.ptr], returns: FFIType.ptr }, closedir: { args: [FFIType.ptr], returns: FFIType.i32 }, [errnoName]: { args: [], returns: FFIType.ptr }, }); } catch { return null; } } export function loadLibc(): LoadedLibc | null { if (cachedLibc !== undefined) { return cachedLibc; } if (process.platform === "win32") { cachedLibc = null; return null; } const platform = process.platform; const errnoName = platform === "darwin" ? "__error" : "__errno_location"; const candidates = libcLoadCandidates(platform); let library: ReturnType | null = null; for (const candidate of candidates) { library = openLibcSymbols(candidate, errnoName); if (library) { break; } } if (!library) { cachedLibc = null; return null; } const raw = library.symbols as Record; const openat = asLibcFn(raw.openat); const fstatat = asLibcFn(raw.fstatat); const dup = asLibcFn(raw.dup); const close = asLibcFn(raw.close); const fdopendir = asLibcFn(raw.fdopendir); const readdir = asLibcFn(raw.readdir); const closedir = asLibcFn(raw.closedir); const errnoFn = asLibcFn(raw[errnoName]); if ( !openat || !fstatat || !dup || !close || !fdopendir || !readdir || !closedir || !errnoFn ) { cachedLibc = null; return null; } const O_RDONLY = fsConstants.O_RDONLY; const O_DIRECTORY = fsConstants.O_DIRECTORY; const O_NOFOLLOW = fsConstants.O_NOFOLLOW; const openDirFlags = O_RDONLY | O_DIRECTORY | O_NOFOLLOW; const atSymlinkNoFollow = platform === "darwin" ? 0x20 : 0x100; const statLayout = nativeStatLayout(platform, process.arch); cachedLibc = { library, symbols: { openat, fstatat, dup, close, fdopendir, readdir, closedir, errnoPtr: errnoFn, }, dirent: direntLayoutFor(platform), openChildFlags: openDirFlags, openDirFlags, atSymlinkNoFollow, statLayout, }; return cachedLibc; } function nativeStatLayout(platform: string, arch: string): NativeStatLayout { if (platform === "darwin") { return { size: 144, devOffset: 0, devBytes: 4, modeOffset: 4, modeBytes: 2, inoOffset: 8, sizeOffset: 96, mtimeSecOffset: 48, mtimeNsecOffset: 56, ctimeSecOffset: 64, ctimeNsecOffset: 72, }; } return { size: 144, devOffset: 0, devBytes: 8, modeOffset: arch === "x64" ? 24 : 16, modeBytes: 4, inoOffset: 8, sizeOffset: 48, mtimeSecOffset: 88, mtimeNsecOffset: 96, ctimeSecOffset: 104, ctimeNsecOffset: 112, }; } function asLibcFn unknown>( value: unknown ): T | null { if (typeof value !== "function") { return null; } return value as T; } export function errnoError( errno: number, syscall: string, path: string ): NodeJS.ErrnoException { const error = new Error( `${syscall} failed (errno ${errno}): ${path}` ) as NodeJS.ErrnoException; error.errno = errno; error.syscall = syscall; error.path = path; // Map common POSIX errno values used on darwin/linux. if (errno === 2) { error.code = "ENOENT"; } else if (errno === 13) { error.code = "EACCES"; } else if (errno === 20) { error.code = "ENOTDIR"; } else if (errno === 40 || errno === 62) { // Linux ELOOP=40, Darwin ELOOP=62 error.code = "ELOOP"; } else if (errno === 17) { error.code = "EEXIST"; } else { error.code = "EIO"; } return error; } export function readErrno(libc: LoadedLibc): number { const p = libc.symbols.errnoPtr(); if (!p) { return 0; } const view = new Int32Array(toArrayBuffer(p, 0, 4)); return view[0] ?? 0; } /** Clear thread errno so readdir EOF (null + errno 0) is distinguishable from error. */ export function clearErrno(libc: LoadedLibc): void { const p = libc.symbols.errnoPtr(); if (!p) { return; } const view = new Int32Array(toArrayBuffer(p, 0, 4)); view[0] = 0; } /** * Parse d_name from a dirent pointer using platform reclen/namlen bounds. * Returns null for malformed records (caller treats as scan failure). */ export function parseDirentName( ent: Pointer, layout: DirentLayout ): string | null { // Header must cover through d_name start (includes reclen and optional namlen). if (layout.dNameOffset < 18) { return null; } let header: DataView; try { header = new DataView(toArrayBuffer(ent, 0, layout.dNameOffset)); } catch { return null; } const reclen = header.getUint16(layout.dReclenOffset, true); // Record must at least hold the fixed header + one byte of d_name room. if (reclen < layout.dNameOffset + 1) { return null; } const maxNameBytes = reclen - layout.dNameOffset; // POSIX NAME_MAX is typically 255; reject absurd reclen-derived lengths. if (maxNameBytes > 1024) { return null; } let nameLen: number; if (layout.dNamlenOffset !== null) { const namlen = header.getUint16(layout.dNamlenOffset, true); if (namlen > maxNameBytes) { return null; } nameLen = namlen; } else { let bytes: Uint8Array; try { bytes = new Uint8Array( toArrayBuffer(ent, layout.dNameOffset, maxNameBytes) ); } catch { return null; } let end = 0; while (end < bytes.length && bytes[end] !== 0) { end += 1; } // Linux dirents are NUL-terminated within d_reclen; missing NUL → malformed. if (end === bytes.length) { return null; } return Buffer.from(bytes.subarray(0, end)).toString(); } if (nameLen === 0) { return ""; } try { const nameBytes = new Uint8Array( toArrayBuffer(ent, layout.dNameOffset, nameLen) ); return Buffer.from(nameBytes).toString(); } catch { return null; } } /** * Enumerate child names via fdopendir/readdir, stopping after `maxNames + 1` * observed entries so overflow is proven without materializing an unbounded list. */ export function readDirNames( libc: LoadedLibc, dirfd: number, maxNames: number ): { status: "ok"; names: string[] } | { status: "overflow" } { if (!Number.isInteger(maxNames) || maxNames < 0) { throw Object.assign(new Error("maxNames must be a non-negative integer"), { code: "EINVAL", }); } // fdopendir consumes the fd on success — operate on a dup so the handle stays live. const dupFd = libc.symbols.dup(dirfd); if (dupFd < 0) { throw errnoError(readErrno(libc), "dup", ""); } const dirp = libc.symbols.fdopendir(dupFd); if (!dirp) { libc.symbols.close(dupFd); throw errnoError(readErrno(libc), "fdopendir", ""); } const names: string[] = []; try { while (true) { clearErrno(libc); const ent = libc.symbols.readdir(dirp); if (!ent) { const err = readErrno(libc); if (err !== 0) { throw errnoError(err, "readdir", ""); } break; } const name = parseDirentName(ent, libc.dirent); if (name === null) { throw Object.assign(new Error("Malformed dirent record"), { code: "EIO", syscall: "readdir", }); } if (name === "" || name === "." || name === "..") { continue; } // maxNames+1th name proves overflow; do not store it or continue. if (names.length >= maxNames) { return { status: "overflow" }; } names.push(name); } } finally { libc.symbols.closedir(dirp); } return { status: "ok", names }; } export function openatOrThrow( libc: LoadedLibc, dirfd: number, name: string, flags: number, syscall: string ): number { if (name.includes("\0") || name.includes("/") || name.includes("\\")) { throw Object.assign(new Error(`Invalid directory entry name: ${name}`), { code: "EINVAL", }); } const nameBuf = Buffer.from(`${name}\0`); const fd = libc.symbols.openat(dirfd, ptr(nameBuf), flags); if (fd < 0) { throw errnoError(readErrno(libc), syscall, name); } return fd; } /** * Read no-follow metadata for one direct child without opening its content. * This avoids materializing dataless File Provider files while retaining the * parent-fd anchoring and symlink identity required by watcher snapshots. */ export function statatNoFollowOrThrow( libc: LoadedLibc, dirfd: number, name: string ): FdRelativeStat { if (name.includes("\0") || name.includes("/") || name.includes("\\")) { throw Object.assign(new Error(`Invalid directory entry name: ${name}`), { code: "EINVAL", }); } const nameBuffer = Buffer.from(`${name}\0`); const statBuffer = Buffer.alloc(libc.statLayout.size); for (let attempt = 0; attempt < 2; attempt += 1) { const result = libc.symbols.fstatat( dirfd, ptr(nameBuffer), ptr(statBuffer), libc.atSymlinkNoFollow ); if (result >= 0) { break; } const errno = readErrno(libc); // Darwin/Bun can transiently surface ENOENT during large fd-relative stat // batches. Retry the same anchored lookup once. A child that truly // disappeared still fails on the second probe, so callers retain the // scan-failed authority boundary instead of accepting a partial image. if (errno !== 2 || attempt > 0) { throw errnoError(errno, "fstatat", name); } } const layout = libc.statLayout; const mode = layout.modeBytes === 2 ? statBuffer.readUInt16LE(layout.modeOffset) : statBuffer.readUInt32LE(layout.modeOffset); const fileType = mode & 0o170_000; const dev = layout.devBytes === 4 ? statBuffer.readUInt32LE(layout.devOffset) : statBuffer.readBigUInt64LE(layout.devOffset); const mtimeNs = statBuffer.readBigInt64LE(layout.mtimeSecOffset) * 1_000_000_000n + statBuffer.readBigInt64LE(layout.mtimeNsecOffset); const ctimeNs = statBuffer.readBigInt64LE(layout.ctimeSecOffset) * 1_000_000_000n + statBuffer.readBigInt64LE(layout.ctimeNsecOffset); return { isFile: () => fileType === 0o100_000, isDirectory: () => fileType === 0o040_000, isSymbolicLink: () => fileType === 0o120_000, dev, ino: statBuffer.readBigUInt64LE(layout.inoOffset), size: statBuffer.readBigInt64LE(layout.sizeOffset), mtimeNs, ctimeNs, }; }