diff --git a/abi/snapshot.json b/abi/snapshot.json index 1d3c08f31..bbdceee02 100644 --- a/abi/snapshot.json +++ b/abi/snapshot.json @@ -4497,6 +4497,320 @@ "number": 415 } ], + "vfs_metadata": { + "access_modes": [ + { + "name": "F_OK", + "value": 0 + }, + { + "name": "R_OK", + "value": 4 + }, + { + "name": "W_OK", + "value": 2 + }, + { + "name": "X_OK", + "value": 1 + } + ], + "at_flags": [ + { + "name": "AT_FDCWD", + "value": -100 + }, + { + "name": "AT_SYMLINK_NOFOLLOW", + "value": 256 + }, + { + "name": "AT_REMOVEDIR", + "value": 512 + }, + { + "name": "AT_EMPTY_PATH", + "value": 4096 + } + ], + "dirent_types": [ + { + "name": "DT_UNKNOWN", + "value": 0 + }, + { + "name": "DT_FIFO", + "value": 1 + }, + { + "name": "DT_CHR", + "value": 2 + }, + { + "name": "DT_DIR", + "value": 4 + }, + { + "name": "DT_BLK", + "value": 6 + }, + { + "name": "DT_REG", + "value": 8 + }, + { + "name": "DT_LNK", + "value": 10 + }, + { + "name": "DT_SOCK", + "value": 12 + } + ], + "fcntl_commands": [ + { + "name": "F_DUPFD", + "value": 0 + }, + { + "name": "F_GETFD", + "value": 1 + }, + { + "name": "F_SETFD", + "value": 2 + }, + { + "name": "F_GETFL", + "value": 3 + }, + { + "name": "F_SETFL", + "value": 4 + }, + { + "name": "F_GETLK", + "value": 12 + }, + { + "name": "F_SETLK", + "value": 13 + }, + { + "name": "F_SETLKW", + "value": 14 + }, + { + "name": "F_SETOWN", + "value": 8 + }, + { + "name": "F_GETOWN", + "value": 9 + }, + { + "name": "F_DUPFD_CLOEXEC", + "value": 1030 + }, + { + "name": "F_DUPFD_CLOFORK", + "value": 1028 + }, + { + "name": "F_OFD_GETLK", + "value": 36 + }, + { + "name": "F_OFD_SETLK", + "value": 37 + }, + { + "name": "F_OFD_SETLKW", + "value": 38 + } + ], + "fd_flags": [ + { + "name": "FD_CLOEXEC", + "value": 1 + }, + { + "name": "FD_CLOFORK", + "value": 2 + } + ], + "file_modes": [ + { + "name": "S_IFMT", + "value": 61440 + }, + { + "name": "S_IFSOCK", + "value": 49152 + }, + { + "name": "S_IFLNK", + "value": 40960 + }, + { + "name": "S_IFREG", + "value": 32768 + }, + { + "name": "S_IFBLK", + "value": 24576 + }, + { + "name": "S_IFDIR", + "value": 16384 + }, + { + "name": "S_IFCHR", + "value": 8192 + }, + { + "name": "S_IFIFO", + "value": 4096 + }, + { + "name": "S_ISUID", + "value": 2048 + }, + { + "name": "S_ISGID", + "value": 1024 + }, + { + "name": "S_ISVTX", + "value": 512 + }, + { + "name": "S_IRWXU", + "value": 448 + }, + { + "name": "S_IRUSR", + "value": 256 + }, + { + "name": "S_IWUSR", + "value": 128 + }, + { + "name": "S_IXUSR", + "value": 64 + }, + { + "name": "S_IRWXG", + "value": 56 + }, + { + "name": "S_IRGRP", + "value": 32 + }, + { + "name": "S_IWGRP", + "value": 16 + }, + { + "name": "S_IXGRP", + "value": 8 + }, + { + "name": "S_IRWXO", + "value": 7 + }, + { + "name": "S_IROTH", + "value": 4 + }, + { + "name": "S_IWOTH", + "value": 2 + }, + { + "name": "S_IXOTH", + "value": 1 + }, + { + "name": "S_MODE_BITS", + "value": 4095 + } + ], + "open_flags": [ + { + "name": "O_RDONLY", + "value": 0 + }, + { + "name": "O_WRONLY", + "value": 1 + }, + { + "name": "O_RDWR", + "value": 2 + }, + { + "name": "O_ACCMODE", + "value": 3 + }, + { + "name": "O_CREAT", + "value": 64 + }, + { + "name": "O_EXCL", + "value": 128 + }, + { + "name": "O_NOCTTY", + "value": 256 + }, + { + "name": "O_TRUNC", + "value": 512 + }, + { + "name": "O_APPEND", + "value": 1024 + }, + { + "name": "O_NONBLOCK", + "value": 2048 + }, + { + "name": "O_DIRECTORY", + "value": 65536 + }, + { + "name": "O_NOFOLLOW", + "value": 131072 + }, + { + "name": "O_CLOEXEC", + "value": 524288 + }, + { + "name": "O_CLOFORK", + "value": 8388608 + } + ], + "seek_whence": [ + { + "name": "SEEK_SET", + "value": 0 + }, + { + "name": "SEEK_CUR", + "value": 1 + }, + { + "name": "SEEK_END", + "value": 2 + } + ] + }, "wakeup_events": { "fields": [ { diff --git a/crates/kernel/src/syscalls.rs b/crates/kernel/src/syscalls.rs index 8228b2f7a..c1b1db081 100644 --- a/crates/kernel/src/syscalls.rs +++ b/crates/kernel/src/syscalls.rs @@ -4179,8 +4179,6 @@ pub fn sys_execveat( path: &[u8], flags: u32, ) -> Result<(), Errno> { - const AT_EMPTY_PATH: u32 = 0x1000; - if flags & AT_EMPTY_PATH != 0 && path.is_empty() { // fexecve path: exec the file referenced by dirfd let entry = proc.fd_table.get(dirfd)?; diff --git a/crates/shared/src/lib.rs b/crates/shared/src/lib.rs index fa8a38ccb..958297f8e 100644 --- a/crates/shared/src/lib.rs +++ b/crates/shared/src/lib.rs @@ -500,6 +500,7 @@ pub mod flags { pub const O_ACCMODE: u32 = 3; pub const O_CREAT: u32 = 0o100; pub const O_EXCL: u32 = 0o200; + pub const O_NOCTTY: u32 = 0o400; pub const O_TRUNC: u32 = 0o1000; pub const O_APPEND: u32 = 0o2000; pub const O_NONBLOCK: u32 = 0o4000; @@ -511,6 +512,7 @@ pub mod flags { pub const AT_FDCWD: i32 = -100; pub const AT_SYMLINK_NOFOLLOW: u32 = 0x100; pub const AT_REMOVEDIR: u32 = 0x200; + pub const AT_EMPTY_PATH: u32 = 0x1000; } /// File descriptor flags (FD_*). @@ -672,6 +674,11 @@ pub mod mode { pub const S_IFCHR: u32 = 0o020000; pub const S_IFIFO: u32 = 0o010000; + // Special permission bits + pub const S_ISUID: u32 = 0o4000; + pub const S_ISGID: u32 = 0o2000; + pub const S_ISVTX: u32 = 0o1000; + // Owner permissions pub const S_IRWXU: u32 = 0o700; pub const S_IRUSR: u32 = 0o400; @@ -689,6 +696,8 @@ pub mod mode { pub const S_IROTH: u32 = 0o004; pub const S_IWOTH: u32 = 0o002; pub const S_IXOTH: u32 = 0o001; + + pub const S_MODE_BITS: u32 = S_ISUID | S_ISGID | S_ISVTX | S_IRWXU | S_IRWXG | S_IRWXO; } /// Shared-memory channel layout offsets and sizes. diff --git a/docs/plans/2026-05-20-rust-owned-host-logic-plan.md b/docs/plans/2026-05-20-rust-owned-host-logic-plan.md index dbee0925a..caae38602 100644 --- a/docs/plans/2026-05-20-rust-owned-host-logic-plan.md +++ b/docs/plans/2026-05-20-rust-owned-host-logic-plan.md @@ -117,7 +117,7 @@ path. | In progress / stacked PR | Process lifecycle cleanup consolidation | Rust `ProcessTable` now owns parent lookup, wait-target matching, wait-status derivation, host-crash zombie marking, authorized child reaping, thread-exit clear-tid metadata, SysV shared-memory attachment metadata, host-bridged TCP listener target policy, and process-owned host timer cleanup metadata. TS keeps blocked waiter queues, Worker/memory cleanup, platform timer handles, process-memory writes/futex wakeups, and the actual TCP server objects because those are host primitives. Remaining audit: thread channel/Worker allocation and free-list lifecycle. | Kernel owns process lifecycle invariants that do not require Worker identity; JS owns Worker termination, memory objects, crash observation, and platform callbacks. | ProcessTable unit tests; fork/exec/spawn/clone/wait tests; crash/trap tests; browser parity smoke when worker entries change. | | In progress / stacked PR | IPC/resource cleanup in Rust | Rust `Process` now records `shmat` address -> segment metadata, inherits it through fork, clears it across exec setup, and detaches live mappings from `remove_process()`. TS still copies bytes between guest memory and kernel SysV segments because only the host can address guest `Memory`. | `remove_process()` owns IPC attachment cleanup; JS only handles guest-memory transfer and host primitive wake/schedule work. | SysV IPC and mqueue Rust tests plus host integration/e2e coverage for blocking and cleanup. | | In progress / stacked PR | Readiness metadata improvements | Rust/shared now owns the kernel wakeup event record layout, wake-type bits, poll/epoll event bits, and `fd_set` sizing consumed by the host. Kernel wakeup events now target matching poll retries by pipe index before falling back to the broad retry pass; select/pselect and signal-safe ppoll/pselect fallback behavior remains JS-owned. | JS still owns timers/retry queues/`Atomics.waitAsync`, but readiness decisions are less inferred from syscall numbers. No extra Wasm round trip per syscall. | Pipe/socket/poll/select/ppoll/pselect tests; browser bridge smoke for affected wake paths; performance comparison before removing broad fallback logic. | -| Planned | VFS policy split | Keep backend I/O, OPFS/IndexedDB/fetch, Node `fs`, and lazy archive materialization in JS. Move permission and policy decisions into Rust where process uid/gid/umask/fd context is authoritative. | Guest-visible policy is enforced in Rust; host adapters only perform platform operations requested through a checked contract. | VFS unit tests, uid/gid/permission tests, host-fs metadata tests, default mount tests, Node/browser parity tests. | +| In progress / stacked PR | VFS policy split | Rust/shared now owns VFS-visible open flags, `*at` flags, fd/fcntl flags, access modes, file mode bits, dirent types, and seek constants generated into `host/src/generated/abi.ts`; host VFS adapters and the WASI shim consume those generated values. Standalone OPFS worker and vendored SharedFS internals keep local copies because they are entry-point/vendor boundaries. Kernel already enforces uid/gid/umask permission checks from host stat metadata. Remaining design work: explicit mount/read-only policy contract and Node/browser backend parity for enforcing it. | Guest-visible constants and existing permission policy metadata are Rust-owned. Mount/read-only enforcement needs a checked contract before host adapters can become pure platform executors. | Generated ABI vitest; `check-abi-version.sh`; VFS unit tests, uid/gid/permission tests, host-fs metadata tests, default mount tests, Node/browser parity tests before changing enforcement behavior. | | Done / stacked PR | Procfs/process snapshot schema metadata | `crates/shared` owns the binary process snapshot layout, `dump-abi` publishes the schema in `abi/snapshot.json` and generated TS bindings, and `parseProcSnapshots` consumes those generated offsets and sizes. | TS no longer hand-decodes undocumented offsets for kernel process snapshot data. Procfs text formatting remains Rust-owned. | Rust procfs/process snapshot tests; generated ABI vitest; UI/kernel-host tests that consume snapshots. | Deferral rule: if a chunk would move browser/Node primitives, add runtime JS diff --git a/host/src/browser-kernel-worker-entry.ts b/host/src/browser-kernel-worker-entry.ts index 9369da054..c96c0073a 100644 --- a/host/src/browser-kernel-worker-entry.ts +++ b/host/src/browser-kernel-worker-entry.ts @@ -89,10 +89,15 @@ import type { MainToKernelMessage, KernelToMainMessage, } from "./browser-kernel-protocol"; +import { FILE_MODES, OPEN_FLAGS } from "./generated/abi"; const DEFAULT_MAX_PAGES = 16384; const PAGE_SIZE = 65536; const FORK_BUF_SIZE = 16384; +const O_WRONLY_CREAT_TRUNC = + OPEN_FLAGS.O_WRONLY | OPEN_FLAGS.O_CREAT | OPEN_FLAGS.O_TRUNC; +const FILE_PERMISSION_BITS = + FILE_MODES.S_IRWXU | FILE_MODES.S_IRWXG | FILE_MODES.S_IRWXO; // State let kernelWorker: CentralizedKernelWorker; @@ -247,12 +252,12 @@ function overlayEtcFromRootfs(target: MemoryFileSystem, rootfsImage: Uint8Array) // Only handle regular files for now; the canonical images/rootfs/etc/* // is flat (no subdirs, no symlinks). const st = source.stat(sourcePath); - const isRegular = (st.mode & 0xf000) === 0x8000; + const isRegular = (st.mode & FILE_MODES.S_IFMT) === FILE_MODES.S_IFREG; if (!isRegular) continue; // Read full content (sequential — pass null offset for read/write // semantics rather than pread/pwrite). - const fdR = source.open(sourcePath, 0, 0); // O_RDONLY + const fdR = source.open(sourcePath, OPEN_FLAGS.O_RDONLY, 0); const size = st.size; const buf = new Uint8Array(size); let read = 0; @@ -264,7 +269,11 @@ function overlayEtcFromRootfs(target: MemoryFileSystem, rootfsImage: Uint8Array) source.close(fdR); // Write into target. - const fdW = target.open(targetPath, 0o1101 /* O_WRONLY|O_CREAT|O_TRUNC */, st.mode & 0o777); + const fdW = target.open( + targetPath, + O_WRONLY_CREAT_TRUNC, + st.mode & FILE_PERMISSION_BITS, + ); if (read > 0) target.write(fdW, buf.subarray(0, read), null, read); target.close(fdW); } @@ -360,7 +369,11 @@ async function handleInit(msg: Extract) { try { memfs.mkdir(dir, 0o755); } catch { /* exists */ } } const certBytes = new TextEncoder().encode(caCertPem); - const certFd = memfs.open("/etc/ssl/certs/ca-certificates.crt", 0o1101, 0o644); + const certFd = memfs.open( + "/etc/ssl/certs/ca-certificates.crt", + O_WRONLY_CREAT_TRUNC, + 0o644, + ); memfs.write(certFd, certBytes, 0, certBytes.length); memfs.close(certFd); } catch (e) { diff --git a/host/src/generated/abi.ts b/host/src/generated/abi.ts index 1fb318649..df505c3ed 100644 --- a/host/src/generated/abi.ts +++ b/host/src/generated/abi.ts @@ -128,6 +128,104 @@ export const EPOLL_EVENTS = { export const SELECT_FD_SETSIZE = 1024 as const; export const SELECT_FD_SET_BYTES = 128 as const; +export const OPEN_FLAGS = { + O_RDONLY: 0, + O_WRONLY: 1, + O_RDWR: 2, + O_ACCMODE: 3, + O_CREAT: 64, + O_EXCL: 128, + O_NOCTTY: 256, + O_TRUNC: 512, + O_APPEND: 1024, + O_NONBLOCK: 2048, + O_DIRECTORY: 65536, + O_NOFOLLOW: 131072, + O_CLOEXEC: 524288, + O_CLOFORK: 8388608, +} as const; + +export const AT_FLAGS = { + AT_FDCWD: -100, + AT_SYMLINK_NOFOLLOW: 256, + AT_REMOVEDIR: 512, + AT_EMPTY_PATH: 4096, +} as const; + +export const FD_FLAGS = { + FD_CLOEXEC: 1, + FD_CLOFORK: 2, +} as const; + +export const FCNTL_COMMANDS = { + F_DUPFD: 0, + F_GETFD: 1, + F_SETFD: 2, + F_GETFL: 3, + F_SETFL: 4, + F_GETLK: 12, + F_SETLK: 13, + F_SETLKW: 14, + F_SETOWN: 8, + F_GETOWN: 9, + F_DUPFD_CLOEXEC: 1030, + F_DUPFD_CLOFORK: 1028, + F_OFD_GETLK: 36, + F_OFD_SETLK: 37, + F_OFD_SETLKW: 38, +} as const; + +export const ACCESS_MODES = { + F_OK: 0, + R_OK: 4, + W_OK: 2, + X_OK: 1, +} as const; + +export const FILE_MODES = { + S_IFMT: 61440, + S_IFSOCK: 49152, + S_IFLNK: 40960, + S_IFREG: 32768, + S_IFBLK: 24576, + S_IFDIR: 16384, + S_IFCHR: 8192, + S_IFIFO: 4096, + S_ISUID: 2048, + S_ISGID: 1024, + S_ISVTX: 512, + S_IRWXU: 448, + S_IRUSR: 256, + S_IWUSR: 128, + S_IXUSR: 64, + S_IRWXG: 56, + S_IRGRP: 32, + S_IWGRP: 16, + S_IXGRP: 8, + S_IRWXO: 7, + S_IROTH: 4, + S_IWOTH: 2, + S_IXOTH: 1, + S_MODE_BITS: 4095, +} as const; + +export const DIRENT_TYPES = { + DT_UNKNOWN: 0, + DT_FIFO: 1, + DT_CHR: 2, + DT_DIR: 4, + DT_BLK: 6, + DT_REG: 8, + DT_LNK: 10, + DT_SOCK: 12, +} as const; + +export const SEEK_WHENCE = { + SEEK_SET: 0, + SEEK_CUR: 1, + SEEK_END: 2, +} as const; + export const STRUCT_SIZE_WASM_STAT = 88 as const; export const STRUCT_SIZE_WASM_DIRENT = 16 as const; export const STRUCT_SIZE_WASM_TIMESPEC = 16 as const; diff --git a/host/src/kernel-worker.ts b/host/src/kernel-worker.ts index f09a508e3..f1496cfeb 100644 --- a/host/src/kernel-worker.ts +++ b/host/src/kernel-worker.ts @@ -34,6 +34,7 @@ import { ABI_KERNEL_EXPORT, ABI_SYSCALL_NAMES, ABI_SYSCALLS, + AT_FLAGS, CHANNEL_STATUS_COMPLETE, CHANNEL_STATUS_IDLE, CHANNEL_STATUS_PENDING, @@ -5748,7 +5749,6 @@ export class CentralizedKernelWorker { * Resolves the fd path via kernel_get_fd_path, then delegates to exec flow. */ private handleExecveat(channel: ChannelInfo, origArgs: number[]): void { - const AT_EMPTY_PATH = 0x1000; const dirfd = origArgs[0]; const flags = origArgs[4]; @@ -5762,7 +5762,7 @@ export class CentralizedKernelWorker { let execPath: string; - if ((flags & AT_EMPTY_PATH) !== 0 && pathStr === "") { + if ((flags & AT_FLAGS.AT_EMPTY_PATH) !== 0 && pathStr === "") { // fexecve path: resolve fd to file path via kernel const getFdPath = this.kernelInstance!.exports.kernel_get_fd_path as ((pid: number, fd: number, bufPtr: bigint, bufLen: number) => number) | undefined; diff --git a/host/src/platform/native-metadata.ts b/host/src/platform/native-metadata.ts index b90463a3a..f92b9bb07 100644 --- a/host/src/platform/native-metadata.ts +++ b/host/src/platform/native-metadata.ts @@ -1,11 +1,15 @@ import type { Stats } from "node:fs"; import type { StatResult } from "../types"; +import { ACCESS_MODES, FILE_MODES } from "../generated/abi"; -const MODE_CHANGE_MASK = 0o7777; const UID_GID_UNCHANGED = 0xffffffff; -const X_OK = 0o1; -const W_OK = 0o2; -const R_OK = 0o4; +const MODE_CHANGE_MASK = FILE_MODES.S_MODE_BITS; +const READABLE_BITS = + FILE_MODES.S_IRUSR | FILE_MODES.S_IRGRP | FILE_MODES.S_IROTH; +const WRITABLE_BITS = + FILE_MODES.S_IWUSR | FILE_MODES.S_IWGRP | FILE_MODES.S_IWOTH; +const EXECUTABLE_BITS = + FILE_MODES.S_IXUSR | FILE_MODES.S_IXGRP | FILE_MODES.S_IXOTH; interface VirtualMetadata { mode?: number; @@ -61,9 +65,15 @@ export class NativeMetadataOverlay { access(s: Stats, amode: number): void { const mode = this.toStatResult(s).mode; - if ((amode & R_OK) !== 0 && (mode & 0o444) === 0) throw new Error("EACCES"); - if ((amode & W_OK) !== 0 && (mode & 0o222) === 0) throw new Error("EACCES"); - if ((amode & X_OK) !== 0 && (mode & 0o111) === 0) throw new Error("EACCES"); + if ((amode & ACCESS_MODES.R_OK) !== 0 && (mode & READABLE_BITS) === 0) { + throw new Error("EACCES"); + } + if ((amode & ACCESS_MODES.W_OK) !== 0 && (mode & WRITABLE_BITS) === 0) { + throw new Error("EACCES"); + } + if ((amode & ACCESS_MODES.X_OK) !== 0 && (mode & EXECUTABLE_BITS) === 0) { + throw new Error("EACCES"); + } } private metadataFor(s: Stats): VirtualMetadata { diff --git a/host/src/platform/node.ts b/host/src/platform/node.ts index 8bf55dce2..896185dad 100644 --- a/host/src/platform/node.ts +++ b/host/src/platform/node.ts @@ -10,6 +10,7 @@ import * as fs from "node:fs"; import * as os from "node:os"; import * as path from "node:path"; import type { PlatformIO, StatResult, StatfsResult } from "../types"; +import { OPEN_FLAGS } from "../generated/abi"; import { nativeStatfs, translateOpenFlags } from "../vfs/host-fs"; import { NativeMetadataOverlay } from "./native-metadata"; @@ -62,7 +63,7 @@ export class NodePlatformIO implements PlatformIO { open(path: string, flags: number, mode: number): number { const nativePath = this.rewritePath(path); - const created = (flags & 0o100) !== 0 && !fs.existsSync(nativePath); + const created = (flags & OPEN_FLAGS.O_CREAT) !== 0 && !fs.existsSync(nativePath); const fd = fs.openSync(nativePath, translateOpenFlags(flags), mode); if (created) this.metadata.chmod(fs.fstatSync(fd), mode); this.fdPositions.set(fd, 0); diff --git a/host/src/vfs/default-mounts.ts b/host/src/vfs/default-mounts.ts index 99bfaa0c7..4256ab56e 100644 --- a/host/src/vfs/default-mounts.ts +++ b/host/src/vfs/default-mounts.ts @@ -12,8 +12,12 @@ */ import type { MountConfig } from "./types"; +import { OPEN_FLAGS } from "../generated/abi"; import { MemoryFileSystem } from "./memory-fs"; +const O_WRONLY_CREAT_TRUNC = + OPEN_FLAGS.O_WRONLY | OPEN_FLAGS.O_CREAT | OPEN_FLAGS.O_TRUNC; + export interface MountSpec { /** Absolute VFS mount point (e.g., "/etc"). No trailing slash except "/". */ path: string; @@ -94,7 +98,7 @@ function readTextFile(fs: MemoryFileSystem, path: string): string | null { function writeTextFile(fs: MemoryFileSystem, path: string, text: string): void { const bytes = new TextEncoder().encode(text); - const fd = fs.open(path, 0o1101, 0o644); // O_WRONLY | O_CREAT | O_TRUNC + const fd = fs.open(path, O_WRONLY_CREAT_TRUNC, 0o644); try { if (bytes.byteLength > 0) fs.write(fd, bytes, null, bytes.byteLength); } finally { diff --git a/host/src/vfs/device-fs.ts b/host/src/vfs/device-fs.ts index cc2c28217..a138bed89 100644 --- a/host/src/vfs/device-fs.ts +++ b/host/src/vfs/device-fs.ts @@ -1,9 +1,10 @@ import type { StatResult, StatfsResult } from "../types"; import type { FileSystemBackend, DirEntry } from "./types"; import { DEVFS_SUPER_MAGIC, zeroCapacityStatfs } from "../statfs"; +import { DIRENT_TYPES, FILE_MODES } from "../generated/abi"; -const S_IFCHR = 0o020000; -const S_IFDIR = 0o040000; +const { DT_CHR, DT_DIR, DT_LNK } = DIRENT_TYPES; +const { S_IFCHR, S_IFDIR } = FILE_MODES; type DeviceReader = (buffer: Uint8Array, length: number) => number; type DeviceWriter = (buffer: Uint8Array, length: number) => number; @@ -64,12 +65,12 @@ const SUBDIRS = ["pts", "shm", "mqueue"]; /** Extra entries to list in /dev readdir (kernel-managed, not in devices map). */ const EXTRA_ENTRIES: DirEntry[] = [ - { name: "ptmx", type: 2 /* DT_CHR */, ino: 0x100 }, - { name: "pts", type: 4 /* DT_DIR */, ino: 0x101 }, - { name: "fd", type: 10 /* DT_LNK */, ino: 0x102 }, - { name: "stdin", type: 10 /* DT_LNK */, ino: 0x103 }, - { name: "stdout", type: 10 /* DT_LNK */, ino: 0x104 }, - { name: "stderr", type: 10 /* DT_LNK */, ino: 0x105 }, + { name: "ptmx", type: DT_CHR, ino: 0x100 }, + { name: "pts", type: DT_DIR, ino: 0x101 }, + { name: "fd", type: DT_LNK, ino: 0x102 }, + { name: "stdin", type: DT_LNK, ino: 0x103 }, + { name: "stdout", type: DT_LNK, ino: 0x104 }, + { name: "stderr", type: DT_LNK, ino: 0x105 }, ]; function isRootPath(path: string): boolean { diff --git a/host/src/vfs/host-fs.ts b/host/src/vfs/host-fs.ts index 11d57d883..05f873165 100644 --- a/host/src/vfs/host-fs.ts +++ b/host/src/vfs/host-fs.ts @@ -11,6 +11,7 @@ import type { StatResult, StatfsResult } from "../types"; import { NativeMetadataOverlay } from "../platform/native-metadata"; import type { FileSystemBackend, DirEntry } from "./types"; import { DEFAULT_STATFS_BLOCK_SIZE, DEFAULT_STATFS_NAMELEN } from "../statfs"; +import { OPEN_FLAGS } from "../generated/abi"; /** * Translate Linux/POSIX open flags (as used by musl libc) to the @@ -18,35 +19,23 @@ import { DEFAULT_STATFS_BLOCK_SIZE, DEFAULT_STATFS_NAMELEN } from "../statfs"; * The numeric values differ between Linux and macOS/BSD. */ export function translateOpenFlags(linuxFlags: number): number { - // Linux flag constants (octal) - const L_O_WRONLY = 0o1; - const L_O_RDWR = 0o2; - const L_O_CREAT = 0o100; - const L_O_EXCL = 0o200; - const L_O_NOCTTY = 0o400; - const L_O_TRUNC = 0o1000; - const L_O_APPEND = 0o2000; - const L_O_NONBLOCK = 0o4000; - const L_O_DIRECTORY = 0o200000; - const L_O_NOFOLLOW = 0o400000; - let native = 0; // Access mode (bottom 2 bits) - if (linuxFlags & L_O_RDWR) native |= fs.constants.O_RDWR; - else if (linuxFlags & L_O_WRONLY) native |= fs.constants.O_WRONLY; + if (linuxFlags & OPEN_FLAGS.O_RDWR) native |= fs.constants.O_RDWR; + else if (linuxFlags & OPEN_FLAGS.O_WRONLY) native |= fs.constants.O_WRONLY; // else O_RDONLY = 0 - if (linuxFlags & L_O_CREAT) native |= fs.constants.O_CREAT; - if (linuxFlags & L_O_EXCL) native |= fs.constants.O_EXCL; - if (linuxFlags & L_O_TRUNC) native |= fs.constants.O_TRUNC; - if (linuxFlags & L_O_APPEND) native |= fs.constants.O_APPEND; - if (linuxFlags & L_O_NONBLOCK) native |= fs.constants.O_NONBLOCK; - if ((linuxFlags & L_O_DIRECTORY) && fs.constants.O_DIRECTORY) + if (linuxFlags & OPEN_FLAGS.O_CREAT) native |= fs.constants.O_CREAT; + if (linuxFlags & OPEN_FLAGS.O_EXCL) native |= fs.constants.O_EXCL; + if (linuxFlags & OPEN_FLAGS.O_TRUNC) native |= fs.constants.O_TRUNC; + if (linuxFlags & OPEN_FLAGS.O_APPEND) native |= fs.constants.O_APPEND; + if (linuxFlags & OPEN_FLAGS.O_NONBLOCK) native |= fs.constants.O_NONBLOCK; + if ((linuxFlags & OPEN_FLAGS.O_DIRECTORY) && fs.constants.O_DIRECTORY) native |= fs.constants.O_DIRECTORY; - if ((linuxFlags & L_O_NOFOLLOW) && fs.constants.O_NOFOLLOW) + if ((linuxFlags & OPEN_FLAGS.O_NOFOLLOW) && fs.constants.O_NOFOLLOW) native |= fs.constants.O_NOFOLLOW; - if ((linuxFlags & L_O_NOCTTY) && fs.constants.O_NOCTTY) + if ((linuxFlags & OPEN_FLAGS.O_NOCTTY) && fs.constants.O_NOCTTY) native |= fs.constants.O_NOCTTY; // O_LARGEFILE and O_CLOEXEC have no Node.js equivalent; ignored. @@ -125,7 +114,7 @@ export class HostFileSystem implements FileSystemBackend { open(path: string, flags: number, mode: number): number { const nativePath = this.safePath(path); - const created = (flags & 0o100) !== 0 && !fs.existsSync(nativePath); + const created = (flags & OPEN_FLAGS.O_CREAT) !== 0 && !fs.existsSync(nativePath); const fd = fs.openSync(nativePath, translateOpenFlags(flags), mode); if (created) this.metadata.chmod(fs.fstatSync(fd), mode); this.fdPositions.set(fd, 0); diff --git a/host/src/vfs/image-helpers.ts b/host/src/vfs/image-helpers.ts index aa1ba8230..9575db63c 100644 --- a/host/src/vfs/image-helpers.ts +++ b/host/src/vfs/image-helpers.ts @@ -6,9 +6,11 @@ * For host-disk-aware utilities (walking a directory, saving to a file), * see scripts-side helpers. */ +import { OPEN_FLAGS } from "../generated/abi"; import type { MemoryFileSystem } from "./memory-fs"; -const O_WRONLY_CREAT_TRUNC = 0o1101; +const O_WRONLY_CREAT_TRUNC = + OPEN_FLAGS.O_WRONLY | OPEN_FLAGS.O_CREAT | OPEN_FLAGS.O_TRUNC; /** Write text content to a path in the memfs. Creates parent dirs implicitly via writeVfsBinary. */ export function writeVfsFile( diff --git a/host/src/vfs/memory-fs.ts b/host/src/vfs/memory-fs.ts index 2f26f8686..668615628 100644 --- a/host/src/vfs/memory-fs.ts +++ b/host/src/vfs/memory-fs.ts @@ -1,6 +1,7 @@ import { decompress as zstdDecompress } from "fzstd"; import type { StatResult, StatfsResult } from "../types"; import { SFFS_SUPER_MAGIC } from "../statfs"; +import { DIRENT_TYPES, FILE_MODES, OPEN_FLAGS } from "../generated/abi"; import type { FileSystemBackend, DirEntry } from "./types"; import { SharedFS, @@ -8,6 +9,9 @@ import { } from "./sharedfs-vendor"; import type { ZipEntry } from "./zip"; +const O_WRONLY_CREAT_TRUNC = + OPEN_FLAGS.O_WRONLY | OPEN_FLAGS.O_CREAT | OPEN_FLAGS.O_TRUNC; + /** Serializable lazy file entry for transfer between instances. */ export interface LazyFileEntry { ino: number; @@ -271,7 +275,7 @@ export class MemoryFileSystem implements FileSystemBackend { try { this.fs.mkdir(current, 0o755); } catch { /* exists */ } } // Create empty stub file - const fd = this.fs.open(path, 0o1101, mode); // O_WRONLY | O_CREAT | O_TRUNC + const fd = this.fs.open(path, O_WRONLY_CREAT_TRUNC, mode); this.fs.close(fd); // Get inode const st = this.fs.stat(path); @@ -362,7 +366,7 @@ export class MemoryFileSystem implements FileSystemBackend { const target = symlinkTargets.get(ze.fileName)!; this.fs.symlink(target, vfsPath); } else { - const fd = this.fs.open(vfsPath, 0o1101, ze.mode); // O_WRONLY | O_CREAT | O_TRUNC + const fd = this.fs.open(vfsPath, O_WRONLY_CREAT_TRUNC, ze.mode); this.fs.close(fd); } @@ -454,7 +458,7 @@ export class MemoryFileSystem implements FileSystemBackend { throw new Error(`Failed to fetch lazy file ${entry.path}: HTTP ${resp.status}`); } const data = new Uint8Array(await resp.arrayBuffer()); - const fd = this.fs.open(entry.path, 0o1101, 0o755); // O_WRONLY | O_CREAT | O_TRUNC + const fd = this.fs.open(entry.path, O_WRONLY_CREAT_TRUNC, 0o755); this.fs.write(fd, data); this.fs.close(fd); this.lazyFiles.delete(st.ino); @@ -498,7 +502,7 @@ export class MemoryFileSystem implements FileSystemBackend { const ze = zipLookup.get(zipFileName); if (!ze) continue; const content = extractZipEntry(zipData, ze); - const fd = this.fs.open(vfsPath, 0o1101, 0o755); // O_WRONLY | O_CREAT | O_TRUNC + const fd = this.fs.open(vfsPath, O_WRONLY_CREAT_TRUNC, 0o755); if (content.length > 0) this.fs.write(fd, content); this.fs.close(fd); } @@ -930,7 +934,7 @@ export class MemoryFileSystem implements FileSystemBackend { gid: number, content: Uint8Array, ): void { - const fd = this.open(path, 0o1101, mode); // O_WRONLY | O_CREAT | O_TRUNC + const fd = this.open(path, O_WRONLY_CREAT_TRUNC, mode); if (content.length > 0) this.write(fd, content, null, content.length); this.close(fd); this.chown(path, uid, gid); @@ -966,10 +970,14 @@ export class MemoryFileSystem implements FileSystemBackend { if (!entry) return null; // Determine d_type from mode const mode = entry.stat.mode; - let dtype = 0; // DT_UNKNOWN - if ((mode & 0xf000) === 0x8000) dtype = 8; // DT_REG - else if ((mode & 0xf000) === 0x4000) dtype = 4; // DT_DIR - else if ((mode & 0xf000) === 0xa000) dtype = 10; // DT_LNK + let dtype = DIRENT_TYPES.DT_UNKNOWN; + if ((mode & FILE_MODES.S_IFMT) === FILE_MODES.S_IFREG) { + dtype = DIRENT_TYPES.DT_REG; + } else if ((mode & FILE_MODES.S_IFMT) === FILE_MODES.S_IFDIR) { + dtype = DIRENT_TYPES.DT_DIR; + } else if ((mode & FILE_MODES.S_IFMT) === FILE_MODES.S_IFLNK) { + dtype = DIRENT_TYPES.DT_LNK; + } return { name: entry.name, type: dtype, ino: entry.stat.ino }; } diff --git a/host/src/vfs/zip.ts b/host/src/vfs/zip.ts index c2ed001d4..ea739e09e 100644 --- a/host/src/vfs/zip.ts +++ b/host/src/vfs/zip.ts @@ -7,6 +7,7 @@ */ import { inflateSync } from "fflate"; +import { FILE_MODES } from "../generated/abi"; // --- Zip format signatures --- @@ -29,9 +30,7 @@ const COMPRESSION_DEFLATE = 8; // Unix creator OS code const CREATOR_UNIX = 3; -// Unix file type mask for symlinks -const S_IFLNK = 0xa000; -const S_IFMT = 0xf000; +const { S_IFLNK, S_IFMT } = FILE_MODES; export interface ZipEntry { fileName: string; diff --git a/host/src/wasi-shim.ts b/host/src/wasi-shim.ts index 5fdcf589b..d74b361e4 100644 --- a/host/src/wasi-shim.ts +++ b/host/src/wasi-shim.ts @@ -16,6 +16,7 @@ import { ABI_SYSCALLS, + AT_FLAGS, CHANNEL_STATUS_IDLE, CHANNEL_STATUS_PENDING, CH_ARG_SIZE, @@ -26,6 +27,10 @@ import { CH_RETURN, CH_STATUS, CH_SYSCALL, + FCNTL_COMMANDS, + FILE_MODES, + OPEN_FLAGS, + SEEK_WHENCE, STRUCT_SIZE_WASM_STAT, } from "./generated/abi"; @@ -74,38 +79,36 @@ const SYS_DUP2 = ABI_SYSCALLS.Dup2; const SYS_SHUTDOWN = ABI_SYSCALLS.Shutdown; // --- POSIX flags (from crates/shared/src/lib.rs) --- -const O_RDONLY = 0; -const O_WRONLY = 1; -const O_RDWR = 2; -const O_CREAT = 0o100; -const O_EXCL = 0o200; -const O_TRUNC = 0o1000; -const O_APPEND = 0o2000; -const O_NONBLOCK = 0o4000; -const O_DIRECTORY = 0o200000; -const O_NOFOLLOW = 0o400000; - -const AT_FDCWD = -100; -const AT_SYMLINK_NOFOLLOW = 0x100; -const AT_REMOVEDIR = 0x200; - -const F_GETFL = 3; -const F_SETFL = 4; - -// SEEK constants (POSIX) -const SEEK_SET = 0; -const SEEK_CUR = 1; -const SEEK_END = 2; - -// S_IFMT mode bits -const S_IFDIR = 0o040000; -const S_IFCHR = 0o020000; -const S_IFBLK = 0o060000; -const S_IFREG = 0o100000; -const S_IFIFO = 0o010000; -const S_IFLNK = 0o120000; -const S_IFSOCK = 0o140000; -const S_IFMT = 0o170000; +const O_RDONLY = OPEN_FLAGS.O_RDONLY; +const O_RDWR = OPEN_FLAGS.O_RDWR; +const O_ACCMODE = OPEN_FLAGS.O_ACCMODE; +const O_CREAT = OPEN_FLAGS.O_CREAT; +const O_EXCL = OPEN_FLAGS.O_EXCL; +const O_TRUNC = OPEN_FLAGS.O_TRUNC; +const O_APPEND = OPEN_FLAGS.O_APPEND; +const O_NONBLOCK = OPEN_FLAGS.O_NONBLOCK; +const O_DIRECTORY = OPEN_FLAGS.O_DIRECTORY; +const O_NOFOLLOW = OPEN_FLAGS.O_NOFOLLOW; + +const AT_FDCWD = AT_FLAGS.AT_FDCWD; +const AT_SYMLINK_NOFOLLOW = AT_FLAGS.AT_SYMLINK_NOFOLLOW; +const AT_REMOVEDIR = AT_FLAGS.AT_REMOVEDIR; + +const F_GETFL = FCNTL_COMMANDS.F_GETFL; +const F_SETFL = FCNTL_COMMANDS.F_SETFL; + +const SEEK_SET = SEEK_WHENCE.SEEK_SET; +const SEEK_CUR = SEEK_WHENCE.SEEK_CUR; +const SEEK_END = SEEK_WHENCE.SEEK_END; + +const S_IFDIR = FILE_MODES.S_IFDIR; +const S_IFCHR = FILE_MODES.S_IFCHR; +const S_IFBLK = FILE_MODES.S_IFBLK; +const S_IFREG = FILE_MODES.S_IFREG; +const S_IFIFO = FILE_MODES.S_IFIFO; +const S_IFLNK = FILE_MODES.S_IFLNK; +const S_IFSOCK = FILE_MODES.S_IFSOCK; +const S_IFMT = FILE_MODES.S_IFMT; // Stat struct size written by kernel const WASM_STAT_SIZE = STRUCT_SIZE_WASM_STAT; @@ -1124,8 +1127,10 @@ export class WasiShim { if (errno) { // If O_RDWR fails with EISDIR or EACCES, retry with O_RDONLY if ((errno === 21 || errno === 13) && !(posixFlags & O_CREAT)) { - posixFlags = (posixFlags & ~3) | O_RDONLY; - const retry = this.doSyscall(SYS_OPENAT, kernelDirfd, pathAddr, posixFlags, 0o666); + posixFlags = (posixFlags & ~O_ACCMODE) | O_RDONLY; + const retry = this.doSyscall( + SYS_OPENAT, kernelDirfd, pathAddr, posixFlags, 0o666, + ); if (retry.errno) return translateLinuxErrno(retry.errno); new DataView(this.memory.buffer).setUint32(fdOut, retry.result, true); return WASI_ESUCCESS; diff --git a/host/test/generated-abi.test.ts b/host/test/generated-abi.test.ts index 09478d973..acd54a2bc 100644 --- a/host/test/generated-abi.test.ts +++ b/host/test/generated-abi.test.ts @@ -6,6 +6,8 @@ import { ABI_SYSCALL_NAMES, ABI_SYSCALLS, ABI_VERSION, + ACCESS_MODES, + AT_FLAGS, CHANNEL_STATUS, CH_ARG_SIZE, CH_ARGS, @@ -23,7 +25,11 @@ import { CH_STATUS, CH_SYSCALL, CH_TOTAL_SIZE, + DIRENT_TYPES, EPOLL_EVENTS, + FCNTL_COMMANDS, + FD_FLAGS, + FILE_MODES, HOST_ADAPTER_MANIFEST_FIELDS, HOST_ADAPTER_MANIFEST_MAGIC, HOST_ADAPTER_MANIFEST_SIZE, @@ -35,6 +41,7 @@ import { HOST_ADAPTER_VERSION, HOST_ADAPTER_WORKER_FEATURES, HOST_INTERCEPTED_SYSCALLS, + OPEN_FLAGS, POLL_EVENTS, PROC_SNAPSHOT_COUNT_OFFSET, PROC_SNAPSHOT_COUNT_SIZE, @@ -42,6 +49,7 @@ import { PROC_SNAPSHOT_RECORD_FIXED_SIZE, SELECT_FD_SET_BYTES, SELECT_FD_SETSIZE, + SEEK_WHENCE, STRUCT_SIZE_WASM_DIRENT, STRUCT_SIZE_WASM_POLL_FD, STRUCT_SIZE_WASM_STAT, @@ -234,4 +242,15 @@ describe("generated host ABI bindings", () => { expect(SELECT_FD_SETSIZE).toBe(snapshot.io_multiplexing.select.fd_setsize); expect(SELECT_FD_SET_BYTES).toBe(snapshot.io_multiplexing.select.fd_set_bytes); }); + + it("match Rust-owned VFS metadata", () => { + expect(OPEN_FLAGS).toEqual(namedValueMap(snapshot.vfs_metadata.open_flags)); + expect(AT_FLAGS).toEqual(namedValueMap(snapshot.vfs_metadata.at_flags)); + expect(FD_FLAGS).toEqual(namedValueMap(snapshot.vfs_metadata.fd_flags)); + expect(FCNTL_COMMANDS).toEqual(namedValueMap(snapshot.vfs_metadata.fcntl_commands)); + expect(ACCESS_MODES).toEqual(namedValueMap(snapshot.vfs_metadata.access_modes)); + expect(FILE_MODES).toEqual(namedValueMap(snapshot.vfs_metadata.file_modes)); + expect(DIRENT_TYPES).toEqual(namedValueMap(snapshot.vfs_metadata.dirent_types)); + expect(SEEK_WHENCE).toEqual(namedValueMap(snapshot.vfs_metadata.seek_whence)); + }); }); diff --git a/tools/xtask/src/dump_abi.rs b/tools/xtask/src/dump_abi.rs index 5e80db8f8..db68bcb81 100644 --- a/tools/xtask/src/dump_abi.rs +++ b/tools/xtask/src/dump_abi.rs @@ -18,6 +18,10 @@ //! layout consumed by the host retry scheduler //! * [`wasm_posix_shared::poll`], [`wasm_posix_shared::epoll`], and //! [`wasm_posix_shared::select`] — I/O multiplexing event metadata +//! * [`wasm_posix_shared::flags`], [`wasm_posix_shared::access`], +//! [`wasm_posix_shared::mode`], [`wasm_posix_shared::dirent`], and +//! [`wasm_posix_shared::seek`] — VFS-visible constants consumed by host +//! adapters //! //! When `--kernel-wasm ` is provided, the snapshot also covers //! every export in the built kernel `.wasm` (after filtering through @@ -419,6 +423,54 @@ fn render_ts_module() -> String { shared::select::FD_SET_BYTES )); + out.push_str("export const OPEN_FLAGS = {\n"); + for (name, value) in open_flags() { + out.push_str(&format!(" {}: {},\n", name, value)); + } + out.push_str("} as const;\n\n"); + + out.push_str("export const AT_FLAGS = {\n"); + for (name, value) in at_flags() { + out.push_str(&format!(" {}: {},\n", name, value)); + } + out.push_str("} as const;\n\n"); + + out.push_str("export const FD_FLAGS = {\n"); + for (name, value) in fd_flags() { + out.push_str(&format!(" {}: {},\n", name, value)); + } + out.push_str("} as const;\n\n"); + + out.push_str("export const FCNTL_COMMANDS = {\n"); + for (name, value) in fcntl_commands() { + out.push_str(&format!(" {}: {},\n", name, value)); + } + out.push_str("} as const;\n\n"); + + out.push_str("export const ACCESS_MODES = {\n"); + for (name, value) in access_modes() { + out.push_str(&format!(" {}: {},\n", name, value)); + } + out.push_str("} as const;\n\n"); + + out.push_str("export const FILE_MODES = {\n"); + for (name, value) in file_modes() { + out.push_str(&format!(" {}: {},\n", name, value)); + } + out.push_str("} as const;\n\n"); + + out.push_str("export const DIRENT_TYPES = {\n"); + for (name, value) in dirent_types() { + out.push_str(&format!(" {}: {},\n", name, value)); + } + out.push_str("} as const;\n\n"); + + out.push_str("export const SEEK_WHENCE = {\n"); + for (name, value) in seek_whence() { + out.push_str(&format!(" {}: {},\n", name, value)); + } + out.push_str("} as const;\n\n"); + out.push_str(&format!( "export const STRUCT_SIZE_WASM_STAT = {} as const;\n", size_of::() @@ -657,6 +709,7 @@ fn build_snapshot(kernel_wasm: &std::path::Path) -> Result { root.insert("process_snapshot".into(), process_snapshot()); root.insert("wakeup_events".into(), wakeup_events()); root.insert("io_multiplexing".into(), io_multiplexing()); + root.insert("vfs_metadata".into(), vfs_metadata()); root.insert("marshalled_structs".into(), marshalled_structs()); root.insert("syscalls".into(), syscalls()); @@ -958,6 +1011,164 @@ fn io_multiplexing() -> Value { Value::Object(m.into_iter().collect()) } +fn open_flags() -> [(&'static str, u32); 14] { + use shared::flags::*; + [ + ("O_RDONLY", O_RDONLY), + ("O_WRONLY", O_WRONLY), + ("O_RDWR", O_RDWR), + ("O_ACCMODE", O_ACCMODE), + ("O_CREAT", O_CREAT), + ("O_EXCL", O_EXCL), + ("O_NOCTTY", O_NOCTTY), + ("O_TRUNC", O_TRUNC), + ("O_APPEND", O_APPEND), + ("O_NONBLOCK", O_NONBLOCK), + ("O_DIRECTORY", O_DIRECTORY), + ("O_NOFOLLOW", O_NOFOLLOW), + ("O_CLOEXEC", O_CLOEXEC), + ("O_CLOFORK", O_CLOFORK), + ] +} + +fn at_flags() -> [(&'static str, i32); 4] { + use shared::flags::*; + [ + ("AT_FDCWD", AT_FDCWD), + ("AT_SYMLINK_NOFOLLOW", AT_SYMLINK_NOFOLLOW as i32), + ("AT_REMOVEDIR", AT_REMOVEDIR as i32), + ("AT_EMPTY_PATH", AT_EMPTY_PATH as i32), + ] +} + +fn fd_flags() -> [(&'static str, u32); 2] { + use shared::fd_flags::*; + [("FD_CLOEXEC", FD_CLOEXEC), ("FD_CLOFORK", FD_CLOFORK)] +} + +fn fcntl_commands() -> [(&'static str, u32); 15] { + use shared::fcntl_cmd::*; + [ + ("F_DUPFD", F_DUPFD), + ("F_GETFD", F_GETFD), + ("F_SETFD", F_SETFD), + ("F_GETFL", F_GETFL), + ("F_SETFL", F_SETFL), + ("F_GETLK", F_GETLK), + ("F_SETLK", F_SETLK), + ("F_SETLKW", F_SETLKW), + ("F_SETOWN", F_SETOWN), + ("F_GETOWN", F_GETOWN), + ("F_DUPFD_CLOEXEC", F_DUPFD_CLOEXEC), + ("F_DUPFD_CLOFORK", F_DUPFD_CLOFORK), + ("F_OFD_GETLK", F_OFD_GETLK), + ("F_OFD_SETLK", F_OFD_SETLK), + ("F_OFD_SETLKW", F_OFD_SETLKW), + ] +} + +fn access_modes() -> [(&'static str, u32); 4] { + use shared::access::*; + [ + ("F_OK", F_OK), + ("R_OK", R_OK), + ("W_OK", W_OK), + ("X_OK", X_OK), + ] +} + +fn file_modes() -> [(&'static str, u32); 24] { + use shared::mode::*; + [ + ("S_IFMT", S_IFMT), + ("S_IFSOCK", S_IFSOCK), + ("S_IFLNK", S_IFLNK), + ("S_IFREG", S_IFREG), + ("S_IFBLK", S_IFBLK), + ("S_IFDIR", S_IFDIR), + ("S_IFCHR", S_IFCHR), + ("S_IFIFO", S_IFIFO), + ("S_ISUID", S_ISUID), + ("S_ISGID", S_ISGID), + ("S_ISVTX", S_ISVTX), + ("S_IRWXU", S_IRWXU), + ("S_IRUSR", S_IRUSR), + ("S_IWUSR", S_IWUSR), + ("S_IXUSR", S_IXUSR), + ("S_IRWXG", S_IRWXG), + ("S_IRGRP", S_IRGRP), + ("S_IWGRP", S_IWGRP), + ("S_IXGRP", S_IXGRP), + ("S_IRWXO", S_IRWXO), + ("S_IROTH", S_IROTH), + ("S_IWOTH", S_IWOTH), + ("S_IXOTH", S_IXOTH), + ("S_MODE_BITS", S_MODE_BITS), + ] +} + +fn dirent_types() -> [(&'static str, u32); 8] { + use shared::dirent::*; + [ + ("DT_UNKNOWN", DT_UNKNOWN), + ("DT_FIFO", DT_FIFO), + ("DT_CHR", DT_CHR), + ("DT_DIR", DT_DIR), + ("DT_BLK", DT_BLK), + ("DT_REG", DT_REG), + ("DT_LNK", DT_LNK), + ("DT_SOCK", DT_SOCK), + ] +} + +fn seek_whence() -> [(&'static str, u32); 3] { + use shared::seek::*; + [ + ("SEEK_SET", SEEK_SET), + ("SEEK_CUR", SEEK_CUR), + ("SEEK_END", SEEK_END), + ] +} + +fn named_values(entries: [(&'static str, u32); N]) -> Value { + let values = entries + .into_iter() + .map(|(name, value)| { + let mut m: JsonMap = BTreeMap::new(); + m.insert("name".into(), json!(name)); + m.insert("value".into(), json!(value)); + Value::Object(m.into_iter().collect()) + }) + .collect(); + Value::Array(values) +} + +fn named_signed_values(entries: [(&'static str, i32); N]) -> Value { + let values = entries + .into_iter() + .map(|(name, value)| { + let mut m: JsonMap = BTreeMap::new(); + m.insert("name".into(), json!(name)); + m.insert("value".into(), json!(value)); + Value::Object(m.into_iter().collect()) + }) + .collect(); + Value::Array(values) +} + +fn vfs_metadata() -> Value { + let mut m: JsonMap = BTreeMap::new(); + m.insert("open_flags".into(), named_values(open_flags())); + m.insert("at_flags".into(), named_signed_values(at_flags())); + m.insert("fd_flags".into(), named_values(fd_flags())); + m.insert("fcntl_commands".into(), named_values(fcntl_commands())); + m.insert("access_modes".into(), named_values(access_modes())); + m.insert("file_modes".into(), named_values(file_modes())); + m.insert("dirent_types".into(), named_values(dirent_types())); + m.insert("seek_whence".into(), named_values(seek_whence())); + Value::Object(m.into_iter().collect()) +} + fn marshalled_structs() -> Value { use shared::fbdev::{FbBitfield, FbFixScreenInfo, FbVarScreenInfo}; use shared::{WasmDirent, WasmFlock, WasmPollFd, WasmStat, WasmStatfs, WasmTimespec}; @@ -1693,6 +1904,7 @@ fn additive_top_level_section(section: &str) -> bool { | "io_multiplexing" | "process_snapshot" | "syscall_arg_descriptors" + | "vfs_metadata" | "wakeup_events" ) } @@ -1941,6 +2153,46 @@ mod tests { "fd_set_bytes": 128 } }, + "vfs_metadata": { + "open_flags": [ + {"name": "O_RDONLY", "value": 0}, + {"name": "O_WRONLY", "value": 1}, + {"name": "O_RDWR", "value": 2} + ], + "at_flags": [ + {"name": "AT_FDCWD", "value": -100}, + {"name": "AT_SYMLINK_NOFOLLOW", "value": 256}, + {"name": "AT_REMOVEDIR", "value": 512}, + {"name": "AT_EMPTY_PATH", "value": 4096} + ], + "fd_flags": [ + {"name": "FD_CLOEXEC", "value": 1}, + {"name": "FD_CLOFORK", "value": 2} + ], + "fcntl_commands": [ + {"name": "F_GETFL", "value": 3}, + {"name": "F_SETFL", "value": 4} + ], + "access_modes": [ + {"name": "F_OK", "value": 0}, + {"name": "R_OK", "value": 4}, + {"name": "W_OK", "value": 2}, + {"name": "X_OK", "value": 1} + ], + "file_modes": [ + {"name": "S_IFMT", "value": 61440}, + {"name": "S_IFREG", "value": 32768} + ], + "dirent_types": [ + {"name": "DT_UNKNOWN", "value": 0}, + {"name": "DT_REG", "value": 8} + ], + "seek_whence": [ + {"name": "SEEK_SET", "value": 0}, + {"name": "SEEK_CUR", "value": 1}, + {"name": "SEEK_END", "value": 2} + ] + }, "syscalls": [ {"number": 1, "name": "Open"}, {"number": 2, "name": "Close"} @@ -2063,6 +2315,20 @@ mod tests { ); } + #[test] + fn adding_vfs_metadata_section_is_compatible() { + let mut old = base_snapshot(); + old.as_object_mut().unwrap().remove("vfs_metadata"); + let new = base_snapshot(); + + let report = classify_compat_change(&old, &new).unwrap(); + assert!(report.breaking.is_empty(), "{report:?}"); + assert_eq!( + report.additive, + vec!["added top-level section \"vfs_metadata\""] + ); + } + #[test] fn changed_existing_export_is_breaking() { let old = base_snapshot();