// node:wasi: our own WASI preview1 host, matching uvwasi's (and so Node's) behaviour. // Regular files (path_open) are read/written through fs.ts's sync entry points // (openSync/readSync/writeSync/...): that machinery already does raw, unbuffered byte // I/O over the sync mailbox and maps VFS errors onto Node codes, so routing through it // (rather than a lower-level syscall) avoids a second error-mapping table and gets the // "no text decode, no line buffering" fix for free. Preopened directories are *not* // opened as host fds -- path_* ops run fs.*Sync(hostPath, ...) by string, since nothing // here ever reads/writes a directory as a byte stream. // // Stdio is different: this kernel's fs fd table only ever holds VFS-opened fds (see // src/kernel/fs.ts's own fdTable) -- fd 0/1/2 were never registered there, so // fs.readSync(0, ...)/fs.writeSync(1, ...) answer EBADF (verified against this // worktree's build: both throw EBADF today, WASI-independent). process.stdin/stdout/ // stderr are the only route to the real stdio streams, so guest fd 0/1/2 (or // options.stdin/stdout/stderr, which this kernel can only sensibly mean as 0/1/2 too -- // there is no other host fd namespace a guest could be handed) go straight through // those objects, raw and unbuffered, instead of through fs. A write via StdioWriter // already accepts a Uint8Array untouched (see stdout.ts); a sync stdin read drains // whatever's already buffered (stdin.ts's readSyncRaw) -- genuinely blocking until more // arrives would need a kernel-level sync stdin syscall (stdin is delivered by // postMessage, not the Atomics mailbox fs reads use), which is out of scope here; see // the worktree report's "stdin" row. // // Everything a WASI instance owns (its fd table, memory, per-fd cursor) lives in one // constructor closure, built fresh per `new WASI(...)`, rather than split across class // fields and free functions -- WASI threads give each worker thread its own instance // (see tests/napi-wasm.spec.ts), so there is no shared state to manage beyond what the // guest's own WebAssembly.Memory may share. // // WASM memory can be shared (the same test): every access re-reads `memory.buffer` into // a fresh view, never caches one across calls. import { Buffer } from "./buffer"; import { invalidArgType, invalidArgValue, outOfRange } from "./errors"; import type { StdinStream } from "./stdin"; type FsLike = ReturnType; type Dirent = { name: string; isDirectory(): boolean; isSymbolicLink(): boolean }; interface Stdio { stdin: StdinStream | { readSyncRaw?(max: number): Uint8Array | null | undefined }; stdout: { write(b: Uint8Array): unknown }; stderr: { write(b: Uint8Array): unknown }; } // ---- preview1 constants (WASI's own numbering, not POSIX's) ---- const ERRNO = { SUCCESS: 0, E2BIG: 1, EACCES: 2, EADDRINUSE: 3, EADDRNOTAVAIL: 4, EAFNOSUPPORT: 5, EAGAIN: 6, EALREADY: 7, EBADF: 8, EBADMSG: 9, EBUSY: 10, ECANCELED: 11, ECHILD: 12, ECONNABORTED: 13, ECONNREFUSED: 14, ECONNRESET: 15, EDEADLK: 16, EDESTADDRREQ: 17, EDOM: 18, EDQUOT: 19, EEXIST: 20, EFAULT: 21, EFBIG: 22, EHOSTUNREACH: 23, EIDRM: 24, EILSEQ: 25, EINPROGRESS: 26, EINTR: 27, EINVAL: 28, EIO: 29, EISCONN: 30, EISDIR: 31, ELOOP: 32, EMFILE: 33, EMLINK: 34, EMSGSIZE: 35, EMULTIHOP: 36, ENAMETOOLONG: 37, ENETDOWN: 38, ENETRESET: 39, ENETUNREACH: 40, ENFILE: 41, ENOBUFS: 42, ENODEV: 43, ENOENT: 44, ENOEXEC: 45, ENOLCK: 46, ENOLINK: 47, ENOMEM: 48, ENOMSG: 49, ENOPROTOOPT: 50, ENOSPC: 51, ENOSYS: 52, ENOTCONN: 53, ENOTDIR: 54, ENOTEMPTY: 55, ENOTRECOVERABLE: 56, ENOTSOCK: 57, ENOTSUP: 58, ENOTTY: 59, ENXIO: 60, EOVERFLOW: 61, EOWNERDEAD: 62, EPERM: 63, EPIPE: 64, EPROTO: 65, EPROTONOSUPPORT: 66, EPROTOTYPE: 67, ERANGE: 68, EROFS: 69, ESPIPE: 70, ESRCH: 71, ESTALE: 72, ETIMEDOUT: 73, ETXTBSY: 74, EXDEV: 75, ENOTCAPABLE: 76, } as const; // fs.ts's thrown FsErrno codes -> preview1 errno. A code the VFS never throws still has // a slot here for when it grows one; anything else maps to EIO, uvwasi's own fallback. const NODE_CODE_TO_ERRNO: Record = { ENOENT: ERRNO.ENOENT, EEXIST: ERRNO.EEXIST, ENOTDIR: ERRNO.ENOTDIR, EISDIR: ERRNO.EISDIR, EBADF: ERRNO.EBADF, EINVAL: ERRNO.EINVAL, ENOTEMPTY: ERRNO.ENOTEMPTY, EMFILE: ERRNO.EMFILE, EIO: ERRNO.EIO, EAGAIN: ERRNO.EAGAIN, EADDRINUSE: ERRNO.EADDRINUSE, ESRCH: ERRNO.ESRCH, ENOTTY: ERRNO.ENOTTY, ELOOP: ERRNO.ELOOP, EACCES: ERRNO.EACCES, EPERM: ERRNO.EPERM, ENAMETOOLONG: ERRNO.ENAMETOOLONG, EXDEV: ERRNO.EXDEV, ENOSPC: ERRNO.ENOSPC, EROFS: ERRNO.EROFS, EFBIG: ERRNO.EFBIG, ENOSYS: ERRNO.ENOSYS, ESPIPE: ERRNO.ESPIPE, }; function errnoOf(e: unknown): number { const code = (e as { code?: string } | undefined)?.code; return (code ? NODE_CODE_TO_ERRNO[code] : undefined) ?? ERRNO.EIO; } /** The "CODE, uvwasi_init" shape real Node's native uvwasi_init failures use -- not the * usual ERR_* convention, since these come straight out of libuv/uvwasi, not * internal/errors (verified against real Node 24.16: `new WASI({preopens:{'/x': * '/missing'}})` throws `{code: 'UVWASI_ENOENT', message: 'UVWASI_ENOENT, uvwasi_init'}`). */ function initError(name: "ENOENT" | "ENOTDIR"): Error { return Object.assign(new Error(`UVWASI_${name}, uvwasi_init`), { code: `UVWASI_${name}` }); } function wasiAlreadyStarted(): Error { return Object.assign(new Error("WASI instance has already started"), { code: "ERR_WASI_ALREADY_STARTED" }); } const FILETYPE = { UNKNOWN: 0, BLOCK_DEVICE: 1, CHARACTER_DEVICE: 2, DIRECTORY: 3, REGULAR_FILE: 4, SOCKET_DGRAM: 5, SOCKET_STREAM: 6, SYMBOLIC_LINK: 7 } as const; const FDFLAGS = { APPEND: 1, DSYNC: 2, NONBLOCK: 4, RSYNC: 8, SYNC: 16 } as const; const OFLAGS = { CREAT: 1, DIRECTORY: 2, EXCL: 4, TRUNC: 8 } as const; const WHENCE = { SET: 0, CUR: 1, END: 2 } as const; const CLOCKID = { REALTIME: 0, MONOTONIC: 1, PROCESS_CPUTIME_ID: 2, THREAD_CPUTIME_ID: 3 } as const; const EVENTTYPE = { CLOCK: 0, FD_READ: 1, FD_WRITE: 2 } as const; const SUBSCRIPTION_CLOCK_ABSTIME = 1; // Rights aren't a real capability system here (every fd is sandboxed to its preopen // instead) -- preopened dirs and the files under them get every right a non-socket fd // can hold, as uvwasi's own default grants effectively do for a preopen's whole subtree. const ALL_RIGHTS = (1n << 29n) - 1n; /** A WasiExit never becomes a JS-visible exception past `start`: it carries the exit * code from proc_exit up to the one place (`start`) that knows whether returnOnExit * means "give the code back" or "actually call process.exit". */ class WasiExit { constructor(readonly code: number) {} } interface Preopen { guestName: string; hostRoot: string } type FdEntry = | { kind: "stdio"; hostFd: number; fdFlags: number } | { kind: "file"; hostFd: number; fdFlags: number } | { kind: "dir"; hostPath: string; preopen: Preopen; fdFlags: number; isPreopen: boolean }; /** Resolve `requestedPath` against `base`'s position within its preopen, lexically -- * uvwasi never consults the filesystem to normalize `..`, so neither do we. Walking * above the preopen root is rejected outright (ENOTCAPABLE), not clamped to the root: * verified against real Node 24.16 (preopens: {".": dir}), where "../x" from the * preopen root itself raises raw_os_error 76 even though sibling-of-root paths would * otherwise exist. */ // BigIntStats's numeric fields aren't declared on its TS class (Object.assign fills // them at runtime; see fs.ts), so `.size` doesn't type-check against the Stats| // BigIntStats union even though every fstatSync/statSync call here omits {bigint:true} // and always gets a plain Stats back. function fileSize(st: unknown): number { return Number((st as { size: number | bigint }).size); } function resolvePath(base: { hostPath: string; preopen: Preopen }, requestedPath: string): { hostPath: string } | { error: number } { const rel = base.hostPath === base.preopen.hostRoot ? [] : base.hostPath.slice(base.preopen.hostRoot.length + 1).split("/"); const parts = rel.filter(Boolean); for (const seg of requestedPath.split("/")) { if (seg === "" || seg === ".") continue; if (seg === "..") { if (parts.length === 0) return { error: ERRNO.ENOTCAPABLE }; parts.pop(); } else parts.push(seg); } return { hostPath: parts.length ? `${base.preopen.hostRoot}/${parts.join("/")}` : base.preopen.hostRoot }; } export function makeWasi(fsFactory: () => FsLike, proc: Stdio): { WASI: new (options?: unknown) => unknown } { class WASI { wasiImport: Record unknown>; #namespace: string; #started = false; #returnOnExit: boolean; #finalize: (instance: WebAssembly.Instance) => void; constructor(options?: unknown) { if (options === null || typeof options !== "object") throw invalidArgType("options", "of type object", options); const o = options as Record; const { version } = o; if (typeof version !== "string") throw invalidArgType("options.version", "of type string", version); if (version !== "unstable" && version !== "preview1") throw invalidArgValue("options.version", version, "unsupported WASI version"); this.#namespace = version === "unstable" ? "wasi_unstable" : "wasi_snapshot_preview1"; const argsOpt = o.args === undefined ? [] : o.args; if (!Array.isArray(argsOpt)) throw invalidArgType("options.args", "an instance of Array", argsOpt); const argStrings = argsOpt.map((a) => String(a)); const envOpt = o.env === undefined ? {} : o.env; if (envOpt === null || typeof envOpt !== "object" || Array.isArray(envOpt)) throw invalidArgType("options.env", "of type object", envOpt); const envStrings = Object.entries(envOpt as Record).map(([k, v]) => `${k}=${String(v)}`); const preopensOpt = o.preopens === undefined ? {} : o.preopens; if (preopensOpt === null || typeof preopensOpt !== "object" || Array.isArray(preopensOpt)) { throw invalidArgType("options.preopens", "of type object", preopensOpt); } if (o.returnOnExit !== undefined && typeof o.returnOnExit !== "boolean") throw invalidArgType("options.returnOnExit", "of type boolean", o.returnOnExit); this.#returnOnExit = o.returnOnExit === undefined ? true : (o.returnOnExit as boolean); const stdioFd = (name: "stdin" | "stdout" | "stderr", dflt: number): number => { const v = o[name]; if (v === undefined) return dflt; if (typeof v !== "number") throw invalidArgType(`options.${name}`, "of type number", v); if (!Number.isInteger(v)) throw outOfRange(`options.${name}`, "an integer", v); if (v < 0 || v > 2147483647) throw outOfRange(`options.${name}`, ">= 0 && <= 2147483647", v); return v; }; const hostStdin = stdioFd("stdin", 0); const hostStdout = stdioFd("stdout", 1); const hostStderr = stdioFd("stderr", 2); const fs = fsFactory(); const fds = new Map(); fds.set(0, { kind: "stdio", hostFd: hostStdin, fdFlags: 0 }); fds.set(1, { kind: "stdio", hostFd: hostStdout, fdFlags: 0 }); fds.set(2, { kind: "stdio", hostFd: hostStderr, fdFlags: 0 }); let nextFd = 3; const allocFd = (): number => { while (fds.has(nextFd)) nextFd++; return nextFd++; }; for (const [guestName, target] of Object.entries(preopensOpt as Record)) { const hostPath = String(target); let isDir: boolean; try { isDir = fs.statSync(hostPath)!.isDirectory(); } catch { throw initError("ENOENT"); } if (!isDir) throw initError("ENOTDIR"); const hostRoot = fs.realpathSync(hostPath) as string; fds.set(allocFd(), { kind: "dir", hostPath: hostRoot, preopen: { guestName, hostRoot }, fdFlags: 0, isPreopen: true }); } // Per-fd logical cursor (fd_seek/fd_tell), keyed by host fd. Only files use this; // without a prior fd_seek, read/write use `position: null` and let the real fd's // own cursor (as the kernel's VFS tracks it) do the advancing, matching uvwasi, // which only tracks an explicit offset once a program has asked to move one. const cursors = new Map(); const tellOf = (hostFd: number): bigint => cursors.get(hostFd) ?? 0n; let memory: WebAssembly.Memory | undefined; const view = (): DataView => new DataView(memory!.buffer); const bytes = (): Uint8Array => new Uint8Array(memory!.buffer); const dec = new TextDecoder(); const enc = new TextEncoder(); // .slice (not .subarray): TextDecoder refuses a SharedArrayBuffer-backed view // (memory may be shared under WASI threads), and slice always copies into a // fresh, non-shared ArrayBuffer. const readString = (ptr: number, len: number): string => dec.decode(bytes().slice(ptr, ptr + len)); const writeCString = (ptr: number, s: string): number => { const b = enc.encode(s); bytes().set(b, ptr); view().setUint8(ptr + b.length, 0); return b.length + 1; }; const entryOf = (fd: number): FdEntry | undefined => fds.get(fd); const dirBaseOf = (fd: number): { hostPath: string; preopen: Preopen } | undefined => { const e = entryOf(fd); return e?.kind === "dir" ? e : undefined; }; // statSync/fstatSync's return type is a number/bigint union (the `{bigint:true}` // option this file never passes); Number() normalizes either branch for the one // size/time precision filestat_t needs. function filestatBytes(input: unknown): Uint8Array { const st = input as { isDirectory(): boolean; isSymbolicLink?(): boolean; size: number | bigint; mtimeMs: number | bigint; ino: number | bigint }; const buf = new ArrayBuffer(64); const v = new DataView(buf); const filetype = st.isDirectory() ? FILETYPE.DIRECTORY : st.isSymbolicLink?.() ? FILETYPE.SYMBOLIC_LINK : FILETYPE.REGULAR_FILE; v.setBigUint64(8, BigInt(Math.trunc(Number(st.ino))), true); v.setUint8(16, filetype); v.setBigUint64(24, 1n, true); v.setBigUint64(32, BigInt(Math.trunc(Number(st.size))), true); const mtimeNs = BigInt(Math.trunc(Number(st.mtimeMs))) * 1_000_000n; v.setBigUint64(40, mtimeNs, true); v.setBigUint64(48, mtimeNs, true); v.setBigUint64(56, mtimeNs, true); return new Uint8Array(buf); } function fdstatBytes(filetype: number, fdFlags: number, rightsBase: bigint, rightsInheriting: bigint): Uint8Array { const buf = new ArrayBuffer(24); const v = new DataView(buf); v.setUint8(0, filetype); v.setUint16(2, fdFlags, true); v.setBigUint64(8, rightsBase, true); v.setBigUint64(16, rightsInheriting, true); return new Uint8Array(buf); } function readIovecs(ptr: number, len: number): { base: number; len: number }[] { const out: { base: number; len: number }[] = []; for (let i = 0; i < len; i++) out.push({ base: view().getUint32(ptr + i * 8, true), len: view().getUint32(ptr + i * 8 + 4, true) }); return out; } const wasiImport: Record unknown> = { args_sizes_get: (argcPtr: number, bufSizePtr: number): number => { view().setUint32(argcPtr, argStrings.length, true); view().setUint32(bufSizePtr, argStrings.reduce((n, a) => n + enc.encode(a).length + 1, 0), true); return ERRNO.SUCCESS; }, args_get: (argvPtr: number, argvBufPtr: number): number => { let at = argvBufPtr; for (let i = 0; i < argStrings.length; i++) { view().setUint32(argvPtr + i * 4, at, true); at += writeCString(at, argStrings[i]!); } return ERRNO.SUCCESS; }, environ_sizes_get: (countPtr: number, bufSizePtr: number): number => { view().setUint32(countPtr, envStrings.length, true); view().setUint32(bufSizePtr, envStrings.reduce((n, e) => n + enc.encode(e).length + 1, 0), true); return ERRNO.SUCCESS; }, environ_get: (envPtr: number, envBufPtr: number): number => { let at = envBufPtr; for (let i = 0; i < envStrings.length; i++) { view().setUint32(envPtr + i * 4, at, true); at += writeCString(at, envStrings[i]!); } return ERRNO.SUCCESS; }, clock_res_get: (id: number, resPtr: number): number => { if (id > CLOCKID.THREAD_CPUTIME_ID) return ERRNO.EINVAL; view().setBigUint64(resPtr, 1n, true); return ERRNO.SUCCESS; }, clock_time_get: (id: number, _precision: bigint, timePtr: number): number => { let ns: bigint; if (id === CLOCKID.REALTIME) ns = BigInt(Date.now()) * 1_000_000n; else if (id === CLOCKID.MONOTONIC) ns = BigInt(Math.trunc(performance.now() * 1_000_000)); else if (id === CLOCKID.PROCESS_CPUTIME_ID || id === CLOCKID.THREAD_CPUTIME_ID) { const u = globalThis.process?.cpuUsage?.() ?? { user: 0, system: 0 }; ns = BigInt(Math.trunc((u.user + u.system) * 1000)); } else return ERRNO.EINVAL; view().setBigUint64(timePtr, ns, true); return ERRNO.SUCCESS; }, fd_close: (fd: number): number => { const e = entryOf(fd); if (!e) return ERRNO.EBADF; if (e.kind === "file") { try { fs.closeSync(e.hostFd); } catch { /* already gone */ } cursors.delete(e.hostFd); } fds.delete(fd); return ERRNO.SUCCESS; }, fd_fdstat_get: (fd: number, outPtr: number): number => { const e = entryOf(fd); if (!e) return ERRNO.EBADF; if (e.kind === "dir") { bytes().set(fdstatBytes(FILETYPE.DIRECTORY, e.fdFlags, ALL_RIGHTS, ALL_RIGHTS), outPtr); return ERRNO.SUCCESS; } const filetype = e.kind === "stdio" ? FILETYPE.CHARACTER_DEVICE : FILETYPE.REGULAR_FILE; bytes().set(fdstatBytes(filetype, e.fdFlags, ALL_RIGHTS, 0n), outPtr); return ERRNO.SUCCESS; }, fd_fdstat_set_flags: (fd: number, flags: number): number => { const e = entryOf(fd); if (!e || e.kind === "dir") return ERRNO.EBADF; e.fdFlags = flags; return ERRNO.SUCCESS; }, fd_fdstat_set_rights: (fd: number, base: bigint, inheriting: bigint): number => { // ALL_RIGHTS is every right an fd here can hold (see its own comment); // asking to add a right beyond that is ENOTCAPABLE, same as real Node 24.16 // (verified: fd_fdstat_set_rights(1, -1n, -1n) exits 76). if (!entryOf(fd)) return ERRNO.EBADF; if ((base | inheriting) & ~ALL_RIGHTS) return ERRNO.ENOTCAPABLE; return ERRNO.SUCCESS; }, fd_prestat_get: (fd: number, outPtr: number): number => { const e = entryOf(fd); if (!e || e.kind !== "dir" || !e.isPreopen) return ERRNO.EBADF; view().setUint8(outPtr, 0); view().setUint32(outPtr + 4, enc.encode(e.preopen.guestName).length, true); return ERRNO.SUCCESS; }, fd_prestat_dir_name: (fd: number, pathPtr: number, pathLen: number): number => { const e = entryOf(fd); if (!e || e.kind !== "dir" || !e.isPreopen) return ERRNO.EBADF; const nameBytes = enc.encode(e.preopen.guestName); if (nameBytes.length > pathLen) return ERRNO.ENOBUFS; bytes().set(nameBytes, pathPtr); return ERRNO.SUCCESS; }, fd_filestat_get: (fd: number, outPtr: number): number => { const e = entryOf(fd); if (!e) return ERRNO.EBADF; if (e.kind === "stdio") { // No fs.fstatSync(0/1/2): process stdio isn't in the VFS fd table. A // character device has no meaningful size/mtime, so answer zeros, as // uvwasi does for a tty/pipe it can't stat any more precisely either. bytes().set(filestatBytes({ isDirectory: () => false, size: 0, mtimeMs: 0, ino: 0 }), outPtr); return ERRNO.SUCCESS; } try { const st = e.kind === "dir" ? fs.statSync(e.hostPath) : fs.fstatSync(e.hostFd); bytes().set(filestatBytes(st), outPtr); return ERRNO.SUCCESS; } catch (err) { return errnoOf(err); } }, fd_filestat_set_size: (fd: number, size: bigint): number => { const e = entryOf(fd); if (!e || e.kind !== "file") return ERRNO.EBADF; try { fs.ftruncateSync(e.hostFd, Number(size)); return ERRNO.SUCCESS; } catch (err) { return errnoOf(err); } }, fd_filestat_set_times: (fd: number, _atim: bigint, mtim: bigint, fstFlags: number): number => { const e = entryOf(fd); if (!e) return ERRNO.EBADF; if ((fstFlags & 0b1010) === 0 || e.kind === "stdio") return ERRNO.SUCCESS; try { const mtimeMs = (fstFlags & 0b1000) ? Number(mtim / 1_000_000n) : Date.now(); if (e.kind === "dir") fs.utimesSync(e.hostPath, new Date(mtimeMs), new Date(mtimeMs)); else fs.futimesSync(e.hostFd, new Date(mtimeMs), new Date(mtimeMs)); return ERRNO.SUCCESS; } catch (err) { return errnoOf(err); } }, fd_advise: (fd: number): number => (entryOf(fd) ? ERRNO.SUCCESS : ERRNO.EBADF), fd_allocate: (fd: number, offset: bigint, len: bigint): number => { const e = entryOf(fd); if (!e || e.kind !== "file") return ERRNO.EBADF; try { const want = Number(offset + len); if (want > fileSize(fs.fstatSync(e.hostFd))) fs.ftruncateSync(e.hostFd, want); return ERRNO.SUCCESS; } catch (err) { return errnoOf(err); } }, fd_datasync: (fd: number): number => { const e = entryOf(fd); if (e?.kind === "stdio") return ERRNO.SUCCESS; if (e?.kind !== "file") return ERRNO.EBADF; try { fs.fdatasyncSync(e.hostFd); return ERRNO.SUCCESS; } catch (err) { return errnoOf(err); } }, fd_sync: (fd: number): number => { const e = entryOf(fd); if (e?.kind === "stdio") return ERRNO.SUCCESS; if (e?.kind !== "file") return ERRNO.EBADF; try { fs.fsyncSync(e.hostFd); return ERRNO.SUCCESS; } catch (err) { return errnoOf(err); } }, fd_renumber: (fd: number, to: number): number => { const e = entryOf(fd); if (!e) return ERRNO.EBADF; const existing = entryOf(to); if (existing?.kind === "file") { try { fs.closeSync(existing.hostFd); } catch { /* ignore */ } cursors.delete(existing.hostFd); } fds.set(to, e); fds.delete(fd); return ERRNO.SUCCESS; }, fd_read: (fd: number, iovsPtr: number, iovsLen: number, nreadPtr: number): number => { const e = entryOf(fd); if (!e || e.kind === "dir") return ERRNO.EBADF; if (e.kind === "stdio" && e.hostFd === 0) { const iovecs = readIovecs(iovsPtr, iovsLen).filter((v) => v.len > 0); const want = iovecs.reduce((n, v) => n + v.len, 0); const chunk = (proc.stdin as { readSyncRaw?(max: number): Uint8Array | null | undefined }).readSyncRaw?.(want); let total = 0; if (chunk) { for (const { base, len } of iovecs) { const take = Math.min(len, chunk.length - total); if (take <= 0) break; bytes().set(chunk.subarray(total, total + take), base); total += take; } } view().setUint32(nreadPtr, total, true); return ERRNO.SUCCESS; } if (e.kind === "stdio") return ERRNO.EBADF; const hostFd = e.hostFd; const positioned = cursors.has(hostFd); let total = 0; let pos = positioned ? tellOf(hostFd) : 0n; try { for (const { base, len } of readIovecs(iovsPtr, iovsLen)) { if (len === 0) continue; const buf = Buffer.alloc(len); const n = fs.readSync(hostFd, buf, 0, len, positioned ? Number(pos) : null); bytes().set(buf.subarray(0, n), base); total += n; pos += BigInt(n); if (n < len) break; } } catch (err) { return errnoOf(err); } if (positioned) cursors.set(hostFd, pos); view().setUint32(nreadPtr, total, true); return ERRNO.SUCCESS; }, fd_pread: (fd: number, iovsPtr: number, iovsLen: number, offset: bigint, nreadPtr: number): number => { const e = entryOf(fd); if (e?.kind !== "file") return ERRNO.EBADF; let total = 0, pos = Number(offset); try { for (const { base, len } of readIovecs(iovsPtr, iovsLen)) { if (len === 0) continue; const buf = Buffer.alloc(len); const n = fs.readSync(e.hostFd, buf, 0, len, pos); bytes().set(buf.subarray(0, n), base); total += n; pos += n; if (n < len) break; } } catch (err) { return errnoOf(err); } view().setUint32(nreadPtr, total, true); return ERRNO.SUCCESS; }, fd_write: (fd: number, iovsPtr: number, iovsLen: number, nwrittenPtr: number): number => { const e = entryOf(fd); if (!e || e.kind === "dir") return ERRNO.EBADF; const positioned = e.kind === "file" && cursors.has(e.hostFd); let total = 0; let pos = positioned ? tellOf((e as { hostFd: number }).hostFd) : 0n; try { for (const { base, len } of readIovecs(iovsPtr, iovsLen)) { if (len === 0) continue; const chunk = bytes().slice(base, base + len); // copy out before writing can race a later guest mutation if (e.kind === "stdio" && e.hostFd === 1) proc.stdout.write(chunk); else if (e.kind === "stdio" && e.hostFd === 2) proc.stderr.write(chunk); else if (e.kind === "stdio") return ERRNO.EBADF; else fs.writeSync(e.hostFd, chunk, 0, chunk.length, (e.fdFlags & FDFLAGS.APPEND) || !positioned ? null : Number(pos)); total += len; pos += BigInt(len); } } catch (err) { return errnoOf(err); } if (positioned) cursors.set((e as { hostFd: number }).hostFd, pos); view().setUint32(nwrittenPtr, total, true); return ERRNO.SUCCESS; }, fd_pwrite: (fd: number, iovsPtr: number, iovsLen: number, offset: bigint, nwrittenPtr: number): number => { const e = entryOf(fd); if (e?.kind !== "file") return ERRNO.EBADF; let total = 0, pos = Number(offset); try { for (const { base, len } of readIovecs(iovsPtr, iovsLen)) { if (len === 0) continue; const chunk = bytes().slice(base, base + len); fs.writeSync(e.hostFd, chunk, 0, chunk.length, pos); total += len; pos += len; } } catch (err) { return errnoOf(err); } view().setUint32(nwrittenPtr, total, true); return ERRNO.SUCCESS; }, fd_seek: (fd: number, offset: bigint, whence: number, outPtr: number): number => { const e = entryOf(fd); if (e?.kind !== "file") return ERRNO.ESPIPE; try { const size = BigInt(fileSize(fs.fstatSync(e.hostFd))); const anchor = whence === WHENCE.SET ? 0n : whence === WHENCE.END ? size : tellOf(e.hostFd); const next = anchor + offset; if (next < 0n) return ERRNO.EINVAL; cursors.set(e.hostFd, next); view().setBigUint64(outPtr, next, true); return ERRNO.SUCCESS; } catch (err) { return errnoOf(err); } }, fd_tell: (fd: number, outPtr: number): number => { const e = entryOf(fd); if (e?.kind !== "file") return ERRNO.ESPIPE; view().setBigUint64(outPtr, tellOf(e.hostFd), true); return ERRNO.SUCCESS; }, fd_readdir: (fd: number, bufPtr: number, bufLen: number, cookie: bigint, outPtr: number): number => { const e = entryOf(fd); if (e?.kind !== "dir") return ERRNO.EBADF; let entries: Dirent[]; try { entries = fs.readdirSync(e.hostPath, { withFileTypes: true }) as unknown as Dirent[]; } catch (err) { return errnoOf(err); } let at = bufPtr, used = 0; for (let i = Number(cookie); i < entries.length; i++) { if (used + 24 > bufLen) break; const ent = entries[i]!; const nameBytes = enc.encode(ent.name); const direntBuf = new ArrayBuffer(24); const dv = new DataView(direntBuf); dv.setBigUint64(0, BigInt(i + 1), true); dv.setUint32(16, nameBytes.length, true); dv.setUint8(20, ent.isDirectory() ? FILETYPE.DIRECTORY : ent.isSymbolicLink() ? FILETYPE.SYMBOLIC_LINK : FILETYPE.REGULAR_FILE); bytes().set(new Uint8Array(direntBuf), at); at += 24; used += 24; const nameTake = Math.min(nameBytes.length, bufLen - used); bytes().set(nameBytes.subarray(0, nameTake), at); at += nameTake; used += nameTake; if (nameTake < nameBytes.length) break; } view().setUint32(outPtr, used, true); return ERRNO.SUCCESS; }, path_open: (fd: number, _dirflags: number, pathPtr: number, pathLen: number, oflags: number, _rightsBase: bigint, _rightsInheriting: bigint, fdFlags: number, outPtr: number): number => { const base = dirBaseOf(fd); if (!base) return ERRNO.EBADF; const resolved = resolvePath(base, readString(pathPtr, pathLen)); if ("error" in resolved) return resolved.error; try { let st; try { st = fs.statSync(resolved.hostPath); } catch { st = undefined; } if (st?.isDirectory()) { if (oflags & OFLAGS.EXCL) return ERRNO.EEXIST; const newFd = allocFd(); fds.set(newFd, { kind: "dir", hostPath: resolved.hostPath, preopen: base.preopen, fdFlags, isPreopen: false }); view().setUint32(outPtr, newFd, true); return ERRNO.SUCCESS; } if (!st && (oflags & OFLAGS.DIRECTORY)) return ERRNO.ENOENT; if (st && (oflags & OFLAGS.DIRECTORY)) return ERRNO.ENOTDIR; const flagsStr = (oflags & OFLAGS.CREAT) && (oflags & OFLAGS.EXCL) ? "wx+" : (oflags & OFLAGS.CREAT) && (oflags & OFLAGS.TRUNC) ? "w+" : (oflags & OFLAGS.CREAT) ? "a+" : (oflags & OFLAGS.TRUNC) ? "w+" : "r+"; // uvwasi always opens O_RDWR unless the base rights plainly forbid one side; // we don't model rights finely, so every path_open gets read+write, matching // the common case (wasi-libc's fopen always asks for both via path_open). const hostFd = fs.openSync(resolved.hostPath, flagsStr, 0o666); const newFd = allocFd(); fds.set(newFd, { kind: "file", hostFd, fdFlags }); view().setUint32(outPtr, newFd, true); return ERRNO.SUCCESS; } catch (err) { return errnoOf(err); } }, path_filestat_get: (fd: number, _flags: number, pathPtr: number, pathLen: number, outPtr: number): number => { const base = dirBaseOf(fd); if (!base) return ERRNO.EBADF; const resolved = resolvePath(base, readString(pathPtr, pathLen)); if ("error" in resolved) return resolved.error; try { bytes().set(filestatBytes(fs.statSync(resolved.hostPath)!), outPtr); return ERRNO.SUCCESS; } catch (err) { return errnoOf(err); } }, path_filestat_set_times: (fd: number, _flags: number, pathPtr: number, pathLen: number, _atim: bigint, mtim: bigint, fstFlags: number): number => { const base = dirBaseOf(fd); if (!base) return ERRNO.EBADF; const resolved = resolvePath(base, readString(pathPtr, pathLen)); if ("error" in resolved) return resolved.error; try { const mtimeMs = (fstFlags & 0b1000) ? Number(mtim / 1_000_000n) : Date.now(); fs.utimesSync(resolved.hostPath, new Date(mtimeMs), new Date(mtimeMs)); return ERRNO.SUCCESS; } catch (err) { return errnoOf(err); } }, path_create_directory: (fd: number, pathPtr: number, pathLen: number): number => { const base = dirBaseOf(fd); if (!base) return ERRNO.EBADF; const resolved = resolvePath(base, readString(pathPtr, pathLen)); if ("error" in resolved) return resolved.error; try { fs.mkdirSync(resolved.hostPath); return ERRNO.SUCCESS; } catch (err) { return errnoOf(err); } }, path_remove_directory: (fd: number, pathPtr: number, pathLen: number): number => { const base = dirBaseOf(fd); if (!base) return ERRNO.EBADF; const resolved = resolvePath(base, readString(pathPtr, pathLen)); if ("error" in resolved) return resolved.error; try { fs.rmdirSync(resolved.hostPath); return ERRNO.SUCCESS; } catch (err) { return errnoOf(err); } }, path_unlink_file: (fd: number, pathPtr: number, pathLen: number): number => { const base = dirBaseOf(fd); if (!base) return ERRNO.EBADF; const resolved = resolvePath(base, readString(pathPtr, pathLen)); if ("error" in resolved) return resolved.error; try { fs.unlinkSync(resolved.hostPath); return ERRNO.SUCCESS; } catch (err) { return errnoOf(err); } }, path_rename: (fd: number, oldPathPtr: number, oldPathLen: number, newFd: number, newPathPtr: number, newPathLen: number): number => { const oldBase = dirBaseOf(fd), newBase = dirBaseOf(newFd); if (!oldBase || !newBase) return ERRNO.EBADF; const from = resolvePath(oldBase, readString(oldPathPtr, oldPathLen)); const to = resolvePath(newBase, readString(newPathPtr, newPathLen)); if ("error" in from) return from.error; if ("error" in to) return to.error; try { fs.renameSync(from.hostPath, to.hostPath); return ERRNO.SUCCESS; } catch (err) { return errnoOf(err); } }, path_link: (fd: number, oldPathPtr: number, oldPathLen: number, newFd: number, newPathPtr: number, newPathLen: number): number => { const oldBase = dirBaseOf(fd), newBase = dirBaseOf(newFd); if (!oldBase || !newBase) return ERRNO.EBADF; const from = resolvePath(oldBase, readString(oldPathPtr, oldPathLen)); const to = resolvePath(newBase, readString(newPathPtr, newPathLen)); if ("error" in from) return from.error; if ("error" in to) return to.error; try { fs.linkSync(from.hostPath, to.hostPath); return ERRNO.SUCCESS; } catch (err) { return errnoOf(err); } }, path_symlink: (oldPathPtr: number, oldPathLen: number, fd: number, newPathPtr: number, newPathLen: number): number => { const base = dirBaseOf(fd); if (!base) return ERRNO.EBADF; const to = resolvePath(base, readString(newPathPtr, newPathLen)); if ("error" in to) return to.error; try { fs.symlinkSync(readString(oldPathPtr, oldPathLen), to.hostPath); return ERRNO.SUCCESS; } catch (err) { return errnoOf(err); } }, path_readlink: (fd: number, pathPtr: number, pathLen: number, bufPtr: number, bufLen: number, outPtr: number): number => { const base = dirBaseOf(fd); if (!base) return ERRNO.EBADF; const resolved = resolvePath(base, readString(pathPtr, pathLen)); if ("error" in resolved) return resolved.error; try { const target = fs.readlinkSync(resolved.hostPath) as string; const tb = enc.encode(target); const n = Math.min(tb.length, bufLen); bytes().set(tb.subarray(0, n), bufPtr); view().setUint32(outPtr, n, true); return ERRNO.SUCCESS; } catch (err) { return errnoOf(err); } }, random_get: (ptr: number, len: number): number => { // getRandomValues refuses a SharedArrayBuffer-backed view (memory may be // shared under WASI threads, see tests/napi-wasm.spec.ts's oxide binding), so // fill a plain buffer and copy it in instead of handing it the memory view. const tmp = new Uint8Array(len); globalThis.crypto.getRandomValues(tmp); bytes().set(tmp, ptr); return ERRNO.SUCCESS; }, sched_yield: (): number => ERRNO.SUCCESS, proc_exit: (code: number): never => { throw new WasiExit(code); }, // uvwasi's proc_raise maps the signal and calls uv_kill on the current process; // we have no real signal delivery to a sandboxed guest to match that with, so // ENOSYS (uvwasi's own answer on platforms raise() isn't wired for, e.g. Windows). proc_raise: (): number => ERRNO.ENOSYS, // No fd here is ever a real socket, so every sock_* call answers as uvwasi does // for a non-socket fd: ENOTSOCK. sock_accept: (): number => ERRNO.ENOTSOCK, sock_recv: (): number => ERRNO.ENOTSOCK, sock_send: (): number => ERRNO.ENOTSOCK, sock_shutdown: (): number => ERRNO.ENOTSOCK, poll_oneoff: (inPtr: number, outPtr: number, nsubscriptions: number, outNumPtr: number): number => { if (nsubscriptions === 0) return ERRNO.EINVAL; let bestTimeoutNs: bigint | undefined, bestUserdata = 0n; const events: { userdata: bigint; type: number }[] = []; for (let i = 0; i < nsubscriptions; i++) { const base = inPtr + i * 48; const userdata = view().getBigUint64(base, true); const tag = view().getUint8(base + 8); if (tag === EVENTTYPE.CLOCK) { const clockId = view().getUint32(base + 16, true); const timeout = view().getBigUint64(base + 24, true); const flags = view().getUint16(base + 40, true); let timeoutNs = timeout; if (flags & SUBSCRIPTION_CLOCK_ABSTIME) { const nowNs = clockId === CLOCKID.MONOTONIC ? BigInt(Math.trunc(performance.now() * 1_000_000)) : BigInt(Date.now()) * 1_000_000n; timeoutNs = timeout > nowNs ? timeout - nowNs : 0n; } if (bestTimeoutNs === undefined || timeoutNs < bestTimeoutNs) { bestTimeoutNs = timeoutNs; bestUserdata = userdata; } } else { // Our fds never block (sync fs has no "not ready yet" state), so every // fd_read/fd_write subscription is ready immediately, as uvwasi reports // for a regular file. events.push({ userdata, type: tag }); } } if (events.length === 0 && bestTimeoutNs !== undefined) { const ms = Number(bestTimeoutNs / 1_000_000n); // The guest runs in a Worker, so a blocking Atomics.wait here is safe: it // parks this thread without the browser's "don't block the main thread" bar. if (ms > 0) Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms); events.push({ userdata: bestUserdata, type: EVENTTYPE.CLOCK }); } let at = outPtr; for (const ev of events) { const buf = new ArrayBuffer(32); const v = new DataView(buf); v.setBigUint64(0, ev.userdata, true); v.setUint8(10, ev.type); bytes().set(new Uint8Array(buf), at); at += 32; } view().setUint32(outNumPtr, events.length, true); return ERRNO.SUCCESS; }, }; this.wasiImport = wasiImport; this.#finalize = (instance: WebAssembly.Instance): void => { const exportsObj = instance?.exports as Record | undefined; if (exportsObj === undefined || exportsObj === null || typeof exportsObj !== "object") throw invalidArgType("instance.exports", "of type object", exportsObj); const mem = exportsObj.memory; if (!(mem instanceof WebAssembly.Memory)) throw invalidArgType("instance.exports.memory", "a WebAssembly.Memory object", mem); memory = mem; }; } getImportObject(): Record { return { [this.#namespace]: this.wasiImport }; } finalizeBindings(instance: WebAssembly.Instance): void { this.#finalize(instance); } start(instance: WebAssembly.Instance): number { if (this.#started) throw wasiAlreadyStarted(); this.#finalize(instance); const start = (instance.exports as Record)._start; if (typeof start !== "function") throw invalidArgType("instance.exports._start", "of type function", start); this.#started = true; try { (start as () => void)(); return 0; } catch (e) { if (e instanceof WasiExit) { if (this.#returnOnExit) return e.code; (globalThis.process as unknown as { exit(code: number): never }).exit(e.code); } throw e; } } initialize(instance: WebAssembly.Instance): void { if (this.#started) throw wasiAlreadyStarted(); this.#finalize(instance); const exportsObj = instance.exports as Record; if (exportsObj._start !== undefined) throw invalidArgType("instance.exports._start", "undefined", exportsObj._start); const init = exportsObj._initialize; if (init !== undefined && typeof init !== "function") throw invalidArgType("instance.exports._initialize", "of type function", init); this.#started = true; if (typeof init === "function") (init as () => void)(); } } return { WASI }; }