// Raw SVC wrappers for the FlashOS kernel ABI — the lowest layer of // flibc. Each fn loads the syscall ID into x8 (per the EL0→EL1 contract // established in arch/aarch64/entry.S:el0_svc), then `svc #0` to trap. Argument / // return wiring follows AAPCS64: x0..x5 inputs, x0 return. // // Syscall IDs come from lib/syscall_defs.zig — the same constants the // kernel-side dispatch table in src/sys.zig uses to populate // sys_call_table. A renumbering there propagates here automatically. // // No `linksection` attributes: flibc consumers are ELF-loaded programs // (the sys_execve path), not the in-blob user_init.o that PID 1 still // uses. The kernel's loader places these wrappers wherever the ELF's // PT_LOAD segments dictate, not in the .text.user blob region. const defs = #import("syscall_defs") pub fn fork() i32 { return asm volatile ("svc #0" : [ret] "={x0}" (-> i32), : [nr] "{x8}" (defs.SYS_FORK), : .{ .memory = true }) } pub fn exit() noreturn { asm volatile ("svc #0" : : [nr] "{x8}" (defs.SYS_EXIT), : .{ .memory = true }) unreachable } // Reset the machine (SYS_REBOOT). The kernel performs a board-specific // reset and never returns control to userland, so this wrapper is // noreturn like exit(). pub fn reboot() noreturn { asm volatile ("svc #0" : : [nr] "{x8}" (defs.SYS_REBOOT), : .{ .memory = true }) unreachable } pub fn wait() i32 { return asm volatile ("svc #0" : [ret] "={x0}" (-> i32), : [nr] "{x8}" (defs.SYS_WAIT), : .{ .memory = true }) } pub fn dump_free() u64 { return asm volatile ("svc #0" : [ret] "={x0}" (-> u64), : [nr] "{x8}" (defs.SYS_DUMP_FREE), : .{ .memory = true }) } // Hardware monitors (slots 49..52), all argument-free `-> u64`. mem_total // is the frozen allocatable pool size in pages; uptime is seconds since // boot; cpu_temp is milli-degrees Celsius and cpu_freq is Hz, each 0 when // the board exposes no firmware to ask (virt) — callers render that `n/a`. pub fn mem_total() u64 { return asm volatile ("svc #0" : [ret] "={x0}" (-> u64), : [nr] "{x8}" (defs.SYS_MEMTOTAL), : .{ .memory = true }) } pub fn uptime() u64 { return asm volatile ("svc #0" : [ret] "={x0}" (-> u64), : [nr] "{x8}" (defs.SYS_UPTIME), : .{ .memory = true }) } pub fn cpu_temp() u64 { return asm volatile ("svc #0" : [ret] "={x0}" (-> u64), : [nr] "{x8}" (defs.SYS_CPU_TEMP), : .{ .memory = true }) } pub fn cpu_freq() u64 { return asm volatile ("svc #0" : [ret] "={x0}" (-> u64), : [nr] "{x8}" (defs.SYS_CPU_FREQ), : .{ .memory = true }) } /// exec_path(path, argv) — path-resolved ELF exec on slot 31. `path` is /// a NUL-terminated UVA; `argv` points at a NULL-terminated array of /// `[*:0]u8`. The kernel (src/execve.zig:execveKernel) streams PT_LOAD /// segments from the resolved VFS file and lays an argv block on the /// new user stack, then erets with `x0 = argc`, `x1 = argv`. Returns /// only on failure (-1); on success the caller's image is replaced. pub fn exec_path(path [*:0]u8, argv [*]?[*:0]u8) i32 { return asm volatile ("svc #0" : [ret] "={x0}" (-> i32), : [nr] "{x8}" (defs.SYS_EXECVE), [path] "{x0}" (path), [argv] "{x1}" (argv), : .{ .memory = true }) } pub fn kill(pid i32) i32 { return asm volatile ("svc #0" : [ret] "={x0}" (-> i32), : [nr] "{x8}" (defs.SYS_KILL), [pid] "{x0}" (pid), : .{ .memory = true }) } /// brk(addr) — set the heap break to `addr` (rounded up to PAGE_SIZE by /// the kernel). Returns the new break, or the current break if addr==0. /// Negative on out-of-range (below HEAP_BASE, or above /// STACK_TOP - STACK_BUDGET). i64 because the heap range covers UVAs /// that don't fit in i32. pub fn brk(addr u64) i64 { return asm volatile ("svc #0" : [ret] "={x0}" (-> i64), : [nr] "{x8}" (defs.SYS_BRK), [addr] "{x0}" (addr), : .{ .memory = true }) } /// sbrk(delta) — bump the break by `delta` bytes (kernel rounds the /// resulting target up to PAGE_SIZE). Returns the *previous* break (the /// start of the freshly-allocated region on grow) or -1 on /// overflow / out-of-range. Negative `delta` shrinks; the kernel frees /// released pages and flushes the TLB. pub fn sbrk(delta i64) i64 { return asm volatile ("svc #0" : [ret] "={x0}" (-> i64), : [nr] "{x8}" (defs.SYS_SBRK), [delta] "{x0}" (delta), : .{ .memory = true }) } // ---- Unified fd-table ABI (slots 32..35) ---- // // Slots 32..35 (read/write/close/dup2) dispatch on the fd's kind tag in // the unified `fds` table (console / pipe / file); slot 18 is the pipe // constructor; slot 36 is the working-directory store. The kernel-side // handlers live in src/sys.zig (sys_read/write/close/dup2/sys_chdir) // and src/fdtable.zig (the lookup + close/dup2 mechanics). These // wrappers are the userland surface; the harness keeps using the raw // `sys_*` wrappers in user_space/kernel_tests.zig so PID 1 stays // blob-loaded for now. /// read(fd, buf, len) — drain up to `len` bytes from `fd` into `buf`. /// Returns the byte count, 0 on clean EOF (no peer for a pipe), or -1 /// on an invalid fd / wild UVA. Backend-aware: console blocks on the /// RX ring, pipe blocks on the SPSC ring, file copies from the open /// VFS file. pub fn read(fd i32, buf [*]mut u8, len u64) i64 { return asm volatile ("svc #0" : [ret] "={x0}" (-> i64), : [nr] "{x8}" (defs.SYS_READ), [fd] "{x0}" (fd), [buf] "{x1}" (buf), [len] "{x2}" (len), : .{ .memory = true }) } /// write_fd(fd, buf, len) — emit `len` bytes from `buf` to `fd`. Returns /// the byte count or -1. Carries an explicit length and routes through /// the unified fd table by fd kind (console / pipe / file). pub fn write_fd(fd i32, buf [*]u8, len u64) i64 { return asm volatile ("svc #0" : [ret] "={x0}" (-> i64), : [nr] "{x8}" (defs.SYS_WRITE), [fd] "{x0}" (fd), [buf] "{x1}" (buf), [len] "{x2}" (len), : .{ .memory = true }) } /// close(fd) — release `fd` from the calling task's table. File fds run /// the backend's vfs_close flush before the slot clears; pipe fds drop /// the page-refcount (last close frees the pipe page); console is /// refcount-exempt. Returns 0 on success, -1 on bad fd. pub fn close(fd i32) i32 { return asm volatile ("svc #0" : [ret] "={x0}" (-> i32), : [nr] "{x8}" (defs.SYS_CLOSE), [fd] "{x0}" (fd), : .{ .memory = true }) } /// dup2(old, new) — redirect `new` to point at `old`'s backend, closing /// whatever `new` previously held. Returns `new` on success, -1 on bad /// old fd / out-of-range new. The mechanic that powers `fsh`'s pipe /// wiring (dup2 the pipe end onto fd 0/1 before `exec_path`). pub fn dup2(oldfd i32, newfd i32) i32 { return asm volatile ("svc #0" : [ret] "={x0}" (-> i32), : [nr] "{x8}" (defs.SYS_DUP2), [old] "{x0}" (oldfd), [new] "{x1}" (newfd), : .{ .memory = true }) } /// pipe() — allocate a pipe page and install two fds (read end + write /// end) into the calling task's table. Returns both fds packed into a /// single i64: low 32 bits = read fd, high 32 bits = write fd. Negative /// on failure (no free fd pair / out-of-pages). Single-register return /// matches src/sys.zig:sys_pipe — avoids any copy_to_user dance. pub fn pipe() i64 { return asm volatile ("svc #0" : [ret] "={x0}" (-> i64), : [nr] "{x8}" (defs.SYS_PIPE), : .{ .memory = true }) } /// chdir(path) — replace the calling task's `cwd` with the joined + /// `.`/`..`-collapsed version of `path`. Relative paths are joined /// against the current `cwd`; absolute paths are collapsed in place. /// No backend existence check this release; /// the open/execve boundary trusts the stored value. Returns 0 on /// success, -1 on wild user pointer / un-NUL-terminated input / /// oversize composition past CWD_SIZE (256). pub fn chdir(path [*:0]u8) i32 { return asm volatile ("svc #0" : [ret] "={x0}" (-> i32), : [nr] "{x8}" (defs.SYS_CHDIR), [path] "{x0}" (path), : .{ .memory = true }) } /// getcwd(buf, len) — copy the calling task's NUL-terminated working /// directory (slot 48, SYS_GETCWD) into `buf`, and return the path /// length excluding the NUL. The readback half of `chdir`; `pwd` is the /// sole consumer. Returns -1 on a wild buffer UVA or a `len` too small /// to hold the path plus its terminator. pub fn getcwd(buf [*]mut u8, len u64) i64 { return asm volatile ("svc #0" : [ret] "={x0}" (-> i64), : [nr] "{x8}" (defs.SYS_GETCWD), [buf] "{x0}" (buf), [len] "{x1}" (len), : .{ .memory = true }) } /// open(path) — resolve `path` through VFS and install a file fd in the /// calling task's unified table (slot 7, SYS_OPEN_FILE). `path` is a /// NUL-terminated UVA; relative paths are joined against the task's /// `cwd` at the syscall boundary (the same resolver `execve` / `chdir` /// use). Returns the new fd (>= 0), or -1 on resolve failure / no free /// fd / out-of-file-objects. /// /// This is the lone surviving member of the legacy file ABI (slots /// 7..11): the read / write / close shims at 8/9/11 are DEPRECATED in /// favour of the unified `read` (slot 32) / `write_fd` (33) / `close` /// (34) handlers, which dispatch the file-kind fd this returns. There is /// no unified "open" — slot 7 stays canonical. fsh uses it to slurp /// `/etc/fshrc` at startup (open → read → close). pub fn open(path [*:0]u8) i32 { return asm volatile ("svc #0" : [ret] "={x0}" (-> i32), : [nr] "{x8}" (defs.SYS_OPEN_FILE), [path] "{x0}" (path), : .{ .memory = true }) } /// create(path) — creat(): make a new empty file at `path` and return a /// writable fd (slot 53, SYS_CREATE). `path` is a NUL-terminated UVA; /// relative paths join against the task's `cwd` at the boundary, like /// `open`. Returns the new fd (>= 0), or -1 on a >8.3 name, an existing /// file, a full or read-only volume, or no free fd. The new file is /// caller-owned. /mnt only — a create elsewhere is EROFS. pub fn create(path [*:0]u8) i32 { return asm volatile ("svc #0" : [ret] "={x0}" (-> i32), : [nr] "{x8}" (defs.SYS_CREATE), [path] "{x0}" (path), : .{ .memory = true }) } /// unlink(path) — remove the file at `path` (slot 54, SYS_UNLINK). Same /// path-resolution rules as `open`. Returns 0 on success, -1 on a missing /// file, a directory, a read-only volume, or a fault. pub fn unlink(path [*:0]u8) i32 { return asm volatile ("svc #0" : [ret] "={x0}" (-> i32), : [nr] "{x8}" (defs.SYS_UNLINK), [path] "{x0}" (path), : .{ .memory = true }) } /// rename(old, new) — rename `old` to `new` within the same directory /// (slot 55, SYS_RENAME). Both are NUL-terminated UVAs, resolved against /// `cwd` like `open`. Returns 0 on success, -1 on a missing source, a /// cross-directory or cross-mount move, a >8.3 new name, an existing /// target, or a fault. Cross-directory moves are the caller's copy+unlink /// job (see /bin/mv). pub fn rename(old [*:0]u8, new [*:0]u8) i32 { return asm volatile ("svc #0" : [ret] "={x0}" (-> i32), : [nr] "{x8}" (defs.SYS_RENAME), [old] "{x0}" (old), [new] "{x1}" (new), : .{ .memory = true }) } /// readdir(path, index, out) — fill `out` with the `index`-th entry of /// the directory at `path` (slot 37, SYS_READDIR). `path` is a NUL- /// terminated UVA; relative paths join against the task's `cwd` at the /// boundary, like `open` / `chdir`. Stateless — pass a fresh `index` each /// call; there is no opendir handle. Returns 0 on a hit (`out` filled), -1 /// at end-of-directory / bad path / wild pointer. The kernel /// copy_to_user's the whole Dirent on a hit, so `out` must point at a /// writable 40-byte object. `ls` loops `index` 0.. until -1. pub fn readdir(path [*:0]u8, index u64, out *mut defs.Dirent) i32 { return asm volatile ("svc #0" : [ret] "={x0}" (-> i32), : [nr] "{x8}" (defs.SYS_READDIR), [path] "{x0}" (path), [index] "{x1}" (index), [out] "{x2}" (out), : .{ .memory = true }) } /// klog_read(buf, len) — snapshot the most-recent min(len, retained) bytes /// of the kernel log ring (slot 38, SYS_KLOG_READ) into `buf`, oldest /// first. Returns the byte count (0 when the ring is empty), or -1 on a /// wild buffer UVA. Consume-free — the ring is unchanged, so repeated /// reads re-see the live log. `/bin/dmesg` sizes `buf` to KLOG_SIZE to /// capture the whole retained log in one call. pub fn klog_read(buf [*]mut u8, len u64) i64 { return asm volatile ("svc #0" : [ret] "={x0}" (-> i64), : [nr] "{x8}" (defs.SYS_KLOG_READ), [buf] "{x0}" (buf), [len] "{x1}" (len), : .{ .memory = true }) } // ---- Process credentials (slots 39..44) ---- // // Identity for the login/auth flow. Getters report the // calling task's real / effective uid / gid; setuid / setgid mutate them // under a root-gated policy (euid 0 sets any id; a dropped process may // only reset to an id it already holds, else -1). `/bin/login` uses // setgid + setuid to drop privilege after authenticating, then execs the // user's shell. i64 returns mirror the kernel handlers' -1 sentinel. pub fn getuid() i64 { return asm volatile ("svc #0" : [ret] "={x0}" (-> i64), : [nr] "{x8}" (defs.SYS_GETUID), : .{ .memory = true }) } pub fn geteuid() i64 { return asm volatile ("svc #0" : [ret] "={x0}" (-> i64), : [nr] "{x8}" (defs.SYS_GETEUID), : .{ .memory = true }) } pub fn getgid() i64 { return asm volatile ("svc #0" : [ret] "={x0}" (-> i64), : [nr] "{x8}" (defs.SYS_GETGID), : .{ .memory = true }) } pub fn getegid() i64 { return asm volatile ("svc #0" : [ret] "={x0}" (-> i64), : [nr] "{x8}" (defs.SYS_GETEGID), : .{ .memory = true }) } pub fn setuid(uid u32) i64 { return asm volatile ("svc #0" : [ret] "={x0}" (-> i64), : [nr] "{x8}" (defs.SYS_SETUID), [uid] "{x0}" (uid), : .{ .memory = true }) } pub fn setgid(gid u32) i64 { return asm volatile ("svc #0" : [ret] "={x0}" (-> i64), : [nr] "{x8}" (defs.SYS_SETGID), [gid] "{x0}" (gid), : .{ .memory = true }) } // authenticate(user, user_len, pass, pass_len) — kernel-side credential // verify against the active shadow database (slot 45). Returns 0 on a // match, -1 otherwise. The KDF lives in the kernel; the caller passes a // plaintext password once and never sees a salt or hash. /bin/login is // the sole consumer. pub fn authenticate(user [*]u8, user_len u64, pass [*]u8, pass_len u64) i64 { return asm volatile ("svc #0" : [ret] "={x0}" (-> i64), : [nr] "{x8}" (defs.SYS_AUTHENTICATE), [u] "{x0}" (user), [ul] "{x1}" (user_len), [p] "{x2}" (pass), [pl] "{x3}" (pass_len), : .{ .memory = true }) } // passwd(user, user_len, old, old_len, new, new_len) — kernel-side // password change (slot 46). The kernel re-hashes with a fresh salt and // rewrites `user`'s record in the writable FAT32 shadow. Root may reset // any record without the old password; everyone else only their own // record with the correct old password (-EACCES otherwise). Returns 0 on // success, -1 when no writable shadow exists (QEMU virt / fresh card) or // the input is malformed. /bin/passwd is the interactive consumer. pub fn passwd(user [*]u8, user_len u64, old [*]u8, old_len u64, new [*]u8, new_len u64) i64 { return asm volatile ("svc #0" : [ret] "={x0}" (-> i64), : [nr] "{x8}" (defs.SYS_PASSWD), [u] "{x0}" (user), [ul] "{x1}" (user_len), [o] "{x2}" (old), [ol] "{x3}" (old_len), [n] "{x4}" (new), [nl] "{x5}" (new_len), : .{ .memory = true }) } // set_console_mode(mode) — toggle the kernel console echo flag (slot 25). // CONSOLE_MODE_ECHO on => the kernel echoes typed printable bytes; off // suppresses echo (used around /bin/login's password prompt). Returns 0. pub fn set_console_mode(mode u64) i64 { return asm volatile ("svc #0" : [ret] "={x0}" (-> i64), : [nr] "{x8}" (defs.SYS_SET_CONSOLE_MODE), [m] "{x0}" (mode), : .{ .memory = true }) }