RustForge

rust-kernel

Rustbelt: a hobby x86_64 kernel written in Rust

authorTyler Fowler <tyler@localhost>2026-10-30 11:08:48 -0700
commita8e8b7433b554ac9d840770e29a15e927a1b97ac
parent7ad1388c3b1af493be4824bf58f1cf29159bd94e
treebrowse files at this commit
WIP 15.1: Linux C-string path syscalls, struct stat, *at() calls, rename, pathtest
Mbuild.rs+1 -1
Msrc/errno.rs+3 -2
Msrc/fs/file.rs+84 -8
Msrc/fs/mod.rs+66 -0
Msrc/syscall.rs+306 -79
Mtests/fat.rs+1 -1
Mtests/musl_prereq.rs+8 -0
Mtests/vfs.rs+4 -4
Muser/cp.rs+2 -2
Muser/demo.rs+4 -4
Muser/fetch.rs+1 -1
Muser/hello.rs+1 -1
Muser/httpd.rs+1 -1
Muser/ls.rs+5 -5
Auser/pathtest.rs+177 -0
Muser/sh.rs+3 -4
Muser/touch.rs+1 -1
Muser/ulib.rs+127 -29

18 files changed, 795 insertions, 143 deletions

@@ -77,7 +77,7 @@ const PROGRAMS: &[&str] = &[     "random", "getty", "tile", // stage 13: virtual drivers     "forktest", "cowtest", "mmaptest", "sigtest", // stage 14: processes     "threads", "echoserver", "ptytest", "nbtest", // stage 15: libc plumbing-    "fputest", // stage 15.1: musl prerequisites+    "fputest", "pathtest", // stage 15.1: musl prerequisites ];  const DISK_SECTORS: u64 = 64 * 1024 * 1024 / 512;
@@ -19,6 +19,7 @@ pub enum Errno {     EFAULT = 14,       // bad address (pointer not in user space / not mapped)     EBUSY = 16,        // busy (e.g. removing a mount point)     EEXIST = 17,       // file exists+    EXDEV = 18,        // can't do this across file systems (rename: copy instead)     ENODEV = 19,       // no such device (e.g. no GPU for a framebuffer ioctl)     ENOTDIR = 20,      // a path component isn't a directory     EISDIR = 21,       // is a directory@@ -62,12 +63,12 @@ impl Errno {     /// The errno for a negative syscall result like -2, if we know it.     pub fn from_ret(ret: i64) -> Option<Errno> {         use Errno::*;-        const ALL: [Errno; 48] = [+        const ALL: [Errno; 49] = [             EPERM, ESRCH, EINTR, ERESTARTSYS, ERESTARTNOHAND, ENOENT, EIO, E2BIG, ENOEXEC, EBADF, ECHILD, EAGAIN, ENOMEM, EFAULT, EBUSY, EEXIST, ENODEV,             ENOTDIR, EISDIR, EINVAL, EMFILE, ENOTTY, EFBIG, ENOSPC, ESPIPE, EROFS, EPIPE, ERANGE,             ENAMETOOLONG, ENOSYS, ENOTEMPTY, ELOOP, EOPNOTSUPP, ENOTSOCK, EDESTADDRREQ, EMSGSIZE,             EPROTONOSUPPORT, EAFNOSUPPORT, EADDRINUSE, ENETDOWN, ENETUNREACH, ECONNRESET, EISCONN,-            ENOTCONN, ETIMEDOUT, ECONNREFUSED, ENXIO, EINPROGRESS,+            ENOTCONN, ETIMEDOUT, ECONNREFUSED, ENXIO, EINPROGRESS, EXDEV,         ];         ALL.into_iter().find(|&e| -(e as i64) == ret)     }
@@ -8,12 +8,12 @@  use super::{Inode, InodeKind}; use crate::errno::Errno;-use alloc::sync::Arc;+use alloc::{string::String, sync::Arc}; use spin::Mutex; -/// What `stat`/`fstat` return. Our own small layout (Linux's `struct stat`-/// has ~15 fields we have no use for yet). `#[repr(C)]` because it's copied-/// to user space as is; user/ulib.rs has the same struct.+/// What a `File` or inode tells the kernel about itself. Our own small+/// layout; the syscalls turn it into Linux's `struct stat` (below), which+/// is what user programs get. #[repr(C)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub struct Stat {@@ -42,6 +42,70 @@ impl Stat {     } } +/// Linux's `struct stat` on x86_64 (144 bytes), what stat/fstat/lstat/+/// newfstatat write to user memory since stage 15.1: a C library reads+/// these exact offsets. We fill in what we know and make up the rest the+/// way a simple Linux file system would.+#[repr(C)]+#[derive(Debug, Clone, Copy, Default)]+pub struct LinuxStat {+    pub st_dev: u64,     // which file system (we say 1 for all)+    pub st_ino: u64,     // file number: we hash the path (see below)+    pub st_nlink: u64,   // hard links: 1 (directories 2)+    pub st_mode: u32,    // S_IF* type bits | permission bits+    pub st_uid: u32,     // owner: everything belongs to root (0)+    pub st_gid: u32,+    _pad0: u32,+    pub st_rdev: u64,    // device number for device files (0 here)+    pub st_size: i64,    // bytes+    pub st_blksize: i64, // good I/O size: 4096+    pub st_blocks: i64,  // 512-byte blocks used+    pub st_atime: [i64; 2], // last access (seconds, nanoseconds): we don't keep times yet+    pub st_mtime: [i64; 2], // last change of the contents+    pub st_ctime: [i64; 2], // last change of the inode+    _unused: [i64; 3],+}++/// File type bits of `st_mode` (octal, as in <sys/stat.h>).+pub const S_IFIFO: u32 = 0o010000;+pub const S_IFCHR: u32 = 0o020000;+pub const S_IFDIR: u32 = 0o040000;+pub const S_IFREG: u32 = 0o100000;+pub const S_IFLNK: u32 = 0o120000;+pub const S_IFSOCK: u32 = 0o140000;++impl LinuxStat {+    /// `ino` should tell files apart: callers pass a hash of the path (or of+    /// the open file's address for pipes and sockets). Programs use+    /// (st_dev, st_ino) to spot "same file" (`cp a a`), nothing more.+    pub fn new(stat: &Stat, ino: u64) -> LinuxStat {+        let (ty, default_perm) = match stat.kind {+            KIND_DIR => (S_IFDIR, 0o755),+            KIND_SYMLINK => (S_IFLNK, 0o777),+            KIND_CONSOLE | KIND_DEVICE => (S_IFCHR, 0o666),+            KIND_PIPE => (S_IFIFO, 0o600),+            KIND_SOCKET => (S_IFSOCK, 0o777),+            _ => (S_IFREG, 0o644),+        };+        let perm = if stat.mode == 0 { default_perm } else { stat.mode as u32 & 0o7777 };+        LinuxStat {+            st_dev: 1,+            st_ino: ino.max(1), // 0 means "no inode" to some programs+            st_nlink: if stat.kind == KIND_DIR { 2 } else { 1 },+            st_mode: ty | perm,+            st_size: stat.size as i64,+            st_blksize: 4096,+            st_blocks: stat.size.div_ceil(512) as i64,+            ..Default::default()+        }+    }+}++/// A 64-bit FNV-1a hash: our stand-in for inode numbers (from the path).+pub fn ino_hash(bytes: &[u8]) -> u64 {+    bytes.iter().fold(0xcbf2_9ce4_8422_2325, |h, &b| (h ^ b as u64).wrapping_mul(0x100_0000_01b3))+}+ /// `whence` values for `seek`, as in Linux's lseek. pub const SEEK_SET: u64 = 0; pub const SEEK_CUR: u64 = 1;@@ -57,6 +121,12 @@ pub trait File: Send + Sync {         Err(Errno::ESPIPE)     }     fn stat(&self) -> Result<Stat, Errno>;+    /// The absolute path this was opened by, if it was (files and+    /// directories): for openat & co. relative to a directory fd, fchdir,+    /// and fstat's inode number.+    fn path(&self) -> Option<String> {+        None+    }     /// Fill `buf` with Linux `dirent64` records for the next directory     /// entries; returns bytes used, 0 at the end.     /// Socket syscalls (connect, accept, ...) need the socket behind an fd;@@ -119,15 +189,17 @@ pub struct InodeFile {     offset: Mutex<usize>,     /// O_APPEND: every write goes to the current end of the file (`>>`).     append: bool,+    /// The absolute path it was opened by (see `File::path`).+    path: String, }  impl InodeFile {-    pub fn open(inode: Arc<dyn Inode>) -> Result<Self, Errno> {-        Ok(InodeFile { inode, offset: Mutex::new(0), append: false })+    pub fn open(inode: Arc<dyn Inode>, path: String) -> Result<Self, Errno> {+        Ok(InodeFile { inode, offset: Mutex::new(0), append: false, path })     } -    pub fn open_append(inode: Arc<dyn Inode>) -> Result<Self, Errno> {-        Ok(InodeFile { inode, offset: Mutex::new(0), append: true })+    pub fn open_append(inode: Arc<dyn Inode>, path: String) -> Result<Self, Errno> {+        Ok(InodeFile { inode, offset: Mutex::new(0), append: true, path })     } } @@ -169,6 +241,10 @@ impl File for InodeFile {         Ok(Stat::of(&*self.inode))     } +    fn path(&self) -> Option<String> {+        Some(self.path.clone())+    }+     fn read_at(&self, buf: &mut [u8], offset: u64) -> Result<usize, Errno> {         match self.inode.kind() {             InodeKind::Directory => Err(Errno::EISDIR),
@@ -275,6 +275,72 @@ pub fn remove(path: &str, want_dir: bool) -> Result<(), Errno> {     parent.remove(&name) } +/// The mount point `path` (absolute, normalized) lives under.+fn mount_of(path: &str) -> Option<String> {+    let mounts = MOUNTS.lock();+    mounts.iter().filter(|m| mount_contains(&m.path, path)).max_by_key(|m| m.path.len()).map(|m| m.path.clone())+}++/// rename(old, new). Our `Inode` trait has no "move this entry" operation+/// yet, so we do it the long way, which is enough for what programs use+/// rename for (save to a temp file, then rename it over the real one):+///   - a file: copy its bytes to `new` (replacing a file there), then+///     delete `old`;+///   - an empty directory: create `new` (or reuse an empty one), delete `old`;+///   - anything else (a non-empty directory, a symlink, a device), or+///     across file systems: EXDEV, which tells `mv` to copy and delete+///     itself, like Linux does across file systems.+/// Not atomic, unlike Linux's: a crash in the middle can leave both.+pub fn rename(old: &str, new: &str) -> Result<(), Errno> {+    let (old, new) = (normalize("/", old)?, normalize("/", new)?);+    let src = lookup_nofollow(&old)?;+    if old == new {+        return Ok(());+    }+    if new.starts_with(&old) && new.as_bytes().get(old.len()) == Some(&b'/') {+        return Err(Errno::EINVAL); // a directory into itself+    }+    if MOUNTS.lock().iter().any(|m| m.path == old || m.path == new) {+        return Err(Errno::EBUSY);+    }+    if mount_of(&old) != mount_of(&new) {+        return Err(Errno::EXDEV);+    }+    let existing = lookup_nofollow(&new).ok();+    match src.kind() {+        InodeKind::File => {+            if let Some(dst) = &existing {+                if dst.kind() == InodeKind::Directory {+                    return Err(Errno::EISDIR);+                }+                remove(&new, false)?;+            }+            let data = read_all(&old)?;+            let dst = create(&new, InodeKind::File)?;+            let mut done = 0;+            while done < data.len() {+                done += dst.write_at(done, &data[done..])?;+            }+            remove(&old, false)+        }+        InodeKind::Directory => {+            if !src.list()?.is_empty() {+                return Err(Errno::EXDEV);+            }+            match &existing {+                Some(dst) if dst.kind() != InodeKind::Directory => return Err(Errno::ENOTDIR),+                Some(dst) if !dst.list()?.is_empty() => return Err(Errno::ENOTEMPTY),+                Some(_) => {}+                None => {+                    create(&new, InodeKind::Directory)?;+                }+            }+            remove(&old, true)+        }+        _ => Err(Errno::EXDEV),+    }+}+ /// Read a whole file into memory (used to load programs). pub fn read_all(path: &str) -> Result<Vec<u8>, Errno> {     let inode = lookup(path)?;
@@ -141,7 +141,7 @@ //! OS use, and it's faster.  use crate::errno::Errno;-use crate::fs::file::{File, InodeFile, Stat};+use crate::fs::file::{File, InodeFile, LinuxStat, Stat}; use crate::fs::{self, InodeKind}; use crate::trap::TrapFrame; use crate::{memory, process, signal, thread};@@ -233,6 +233,25 @@ pub const SYS_PSELECT6: u64 = 270; pub const SYS_PPOLL: u64 = 271; pub const SYS_EPOLL_PWAIT: u64 = 281; pub const SYS_EPOLL_CREATE1: u64 = 291;+// Stage 15.1: the rest of Linux's path calls, and a few musl needs early.+pub const SYS_LSTAT: u64 = 6;+pub const SYS_ACCESS: u64 = 21;+pub const SYS_RENAME: u64 = 82;+pub const SYS_FCHDIR: u64 = 81;+pub const SYS_GETUID: u64 = 102;+pub const SYS_GETGID: u64 = 104;+pub const SYS_GETEUID: u64 = 107;+pub const SYS_GETEGID: u64 = 108;+pub const SYS_CLOCK_NANOSLEEP: u64 = 230;+pub const SYS_OPENAT: u64 = 257;+pub const SYS_MKDIRAT: u64 = 258;+pub const SYS_NEWFSTATAT: u64 = 262;+pub const SYS_UNLINKAT: u64 = 263;+pub const SYS_RENAMEAT: u64 = 264;+pub const SYS_READLINKAT: u64 = 267;+pub const SYS_FACCESSAT: u64 = 269;+pub const SYS_RENAMEAT2: u64 = 316;+pub const SYS_FACCESSAT2: u64 = 439;  // Socket constants (Linux's values). pub const AF_INET: u64 = 2;@@ -242,7 +261,18 @@ pub const IPPROTO_ICMP: u64 = 1; pub const SOL_SOCKET: u64 = 1; pub const SO_RCVTIMEO: u64 = 20; +pub const O_ACCMODE: u64 = 3; // O_RDONLY 0, O_WRONLY 1, O_RDWR 2 pub const O_CREAT: u64 = 0o100;+pub const O_EXCL: u64 = 0o200;+pub const O_DIRECTORY: u64 = 0o200000;+pub const O_NOFOLLOW: u64 = 0o400000;+/// The *at() calls: "relative to the cwd" instead of a directory fd, and flags.+pub const AT_FDCWD: i64 = -100;+pub const AT_SYMLINK_NOFOLLOW: u64 = 0x100;+pub const AT_REMOVEDIR: u64 = 0x200;+pub const AT_EMPTY_PATH: u64 = 0x1000;+/// Longest path, counting the NUL (Linux's PATH_MAX).+pub const PATH_MAX: usize = 4096; pub const O_NOCTTY: u64 = 0o400; pub const O_TRUNC: u64 = 0o1000; pub const O_APPEND: u64 = 0o2000;@@ -295,6 +325,24 @@ pub fn name(nr: u64) -> &'static str {         SYS_CLOSE => "close",         SYS_STAT => "stat",         SYS_FSTAT => "fstat",+        SYS_LSTAT => "lstat",+        SYS_ACCESS => "access",+        SYS_RENAME => "rename",+        SYS_FCHDIR => "fchdir",+        SYS_GETUID => "getuid",+        SYS_GETGID => "getgid",+        SYS_GETEUID => "geteuid",+        SYS_GETEGID => "getegid",+        SYS_CLOCK_NANOSLEEP => "clock_nanosleep",+        SYS_OPENAT => "openat",+        SYS_MKDIRAT => "mkdirat",+        SYS_NEWFSTATAT => "newfstatat",+        SYS_UNLINKAT => "unlinkat",+        SYS_RENAMEAT => "renameat",+        SYS_READLINKAT => "readlinkat",+        SYS_FACCESSAT => "faccessat",+        SYS_RENAMEAT2 => "renameat2",+        SYS_FACCESSAT2 => "faccessat2",         SYS_LSEEK => "lseek",         SYS_IOCTL => "ioctl",         SYS_PIPE => "pipe",@@ -492,10 +540,15 @@ extern "C" fn syscall_dispatch(frame: &mut TrapFrame) {     let result = match nr {         SYS_READ => sys_read(a1, a2, a3),         SYS_WRITE => sys_write(a1, a2, a3),-        SYS_OPEN => sys_open(a1, a2, a3),+        SYS_OPEN => sys_openat(AT_FDCWD as u64, a1, a2),+        SYS_OPENAT => sys_openat(a1, a2, a3),         SYS_CLOSE => current().and_then(|p| p.close(a1)).map(|_| 0),-        SYS_STAT => sys_stat(a1, a2, a3),+        SYS_STAT => sys_newfstatat(AT_FDCWD as u64, a1, a2, 0),+        SYS_LSTAT => sys_newfstatat(AT_FDCWD as u64, a1, a2, AT_SYMLINK_NOFOLLOW),         SYS_FSTAT => sys_fstat(a1, a2),+        SYS_NEWFSTATAT => sys_newfstatat(a1, a2, a3, a4),+        SYS_ACCESS => sys_faccessat(AT_FDCWD as u64, a1),+        SYS_FACCESSAT | SYS_FACCESSAT2 => sys_faccessat(a1, a2),         SYS_LSEEK => current().and_then(|p| p.file(a1)).and_then(|f| f.seek(a2 as i64, a3)),         SYS_MMAP => current().and_then(|p| sys_mmap(&p, a1, a2, a3, a4, a5, frame.r9)),         SYS_MPROTECT => current().and_then(|p| sys_mprotect(&p, a1, a2, a3)),@@ -524,22 +577,29 @@ extern "C" fn syscall_dispatch(frame: &mut TrapFrame) {         // last thread); exit_group ends them all. libc's exit() is the latter.         SYS_EXIT => process::exit_thread(process::ExitStatus::Exited(a1 as i32 as i64)),         SYS_EXIT_GROUP => process::exit_group(process::ExitStatus::Exited(a1 as i32 as i64)),-        SYS_WAIT4 => sys_wait4(a1 as i32 as i64, a2, a3),+        SYS_WAIT4 => sys_wait4(a1 as i32 as i64, a2, a3, a4),         SYS_KILL => current().and_then(|p| signal::sys_kill(&p, a1 as i32 as i64, a2)),         SYS_UNAME => sys_uname(a1),         SYS_FCNTL => sys_fcntl(a1, a2, a3),         SYS_GETCWD => sys_getcwd(a1, a2),-        SYS_CHDIR => sys_chdir(a1, a2),-        SYS_MKDIR => user_path(a1, a2).and_then(|p| fs::create(&p, InodeKind::Directory)).map(|_| 0),-        SYS_RMDIR => user_path(a1, a2).and_then(|p| fs::remove(&p, true)).map(|_| 0),-        SYS_UNLINK => user_path(a1, a2).and_then(|p| fs::remove(&p, false)).map(|_| 0),-        SYS_READLINK => sys_readlink(a1, a2, a3, a4),+        SYS_CHDIR => user_path(a1).and_then(|p| sys_chdir(p)),+        SYS_FCHDIR => sys_fchdir(a1),+        SYS_MKDIR => sys_mkdirat(AT_FDCWD as u64, a1),+        SYS_MKDIRAT => sys_mkdirat(a1, a2),+        SYS_RMDIR => sys_unlinkat(AT_FDCWD as u64, a1, AT_REMOVEDIR),+        SYS_UNLINK => sys_unlinkat(AT_FDCWD as u64, a1, 0),+        SYS_UNLINKAT => sys_unlinkat(a1, a2, a3),+        SYS_RENAME => sys_renameat(AT_FDCWD as u64, a1, AT_FDCWD as u64, a2, 0),+        SYS_RENAMEAT => sys_renameat(a1, a2, a3, a4, 0),+        SYS_RENAMEAT2 => sys_renameat(a1, a2, a3, a4, a5),+        SYS_READLINK => sys_readlinkat(AT_FDCWD as u64, a1, a2, a3),+        SYS_READLINKAT => sys_readlinkat(a1, a2, a3, a4),         SYS_SETPGID => current().and_then(|p| process::setpgid(&p, a1, a2)).map(|_| 0),         SYS_GETPPID => current().map(|p| if p.parent() == process::ORPHAN { 0 } else { p.parent() }),         SYS_GETPGRP => current().map(|p| p.pgid()),         SYS_GETPGID => current().and_then(|p| if a1 == 0 { Ok(p.pgid()) } else { process::get(a1).map(|t| t.pgid()).ok_or(Errno::ESRCH) }),         SYS_GETDENTS64 => sys_getdents64(a1, a2, a3),-        SYS_SPAWN => sys_spawn(a1, a2, a3, a4, a5),+        SYS_SPAWN => sys_spawn(a1, a2, a3, a4),         SYS_MOUNTS => sys_mounts(a1, a2),         SYS_SYSINFO => sys_sysinfo(a1),         SYS_SOCKET => sys_socket(a1, a2, a3),@@ -547,11 +607,14 @@ extern "C" fn syscall_dispatch(frame: &mut TrapFrame) {         SYS_ACCEPT => sys_accept(a1, a2, a3, 0),         SYS_ACCEPT4 => sys_accept(a1, a2, a3, a4),         SYS_SENDTO => sys_sendto(a1, a2, a3, a5),-        SYS_RECVFROM => sys_recvfrom(a1, a2, a3, a5),+        SYS_RECVFROM => sys_recvfrom(a1, a2, a3, a5, frame.r9),         SYS_BIND => with_socket(a1, |s| s.bind(read_sockaddr(a2)?.1)).map(|_| 0),         SYS_LISTEN => with_socket(a1, |s| s.listen(a2 as usize)).map(|_| 0),         SYS_SETSOCKOPT => sys_setsockopt(a1, a2, a3, a4, a5),         SYS_CLOCK_GETTIME => sys_clock_gettime(a1, a2),+        SYS_CLOCK_NANOSLEEP => sys_clock_nanosleep(a1, a2, a3, a4),+        // One user, root: musl asks at startup (and `id`, `ls -l` ask).+        SYS_GETUID | SYS_GETGID | SYS_GETEUID | SYS_GETEGID => Ok(0),         SYS_RESOLVE => sys_resolve(a1, a2, a3, a4),         SYS_NETINFO => copy_to_user(a1, &crate::net::info()).map(|_| 0),         SYS_GETRANDOM => sys_getrandom(a1, a2),@@ -635,11 +698,17 @@ fn trace_line(nr: u64, a: [u64; 4], ret: i64) -> String {     let args = match nr {         SYS_READ => format!("{}, {}, {}", a[0], if ret > 0 { text(a[1], ret as u64) } else { String::from("\"\"") }, a[2]),         SYS_WRITE => format!("{}, {}, {}", a[0], text(a[1], a[2]), a[2]),-        SYS_OPEN => format!("{}, {:#o}", text(a[0], a[1]), a[2]),-        SYS_STAT => format!("{}, {:#x}", text(a[0], a[1]), a[2]),-        SYS_READLINK => format!("{}, {:#x}, {}", text(a[0], a[1]), a[2], a[3]),-        SYS_CHDIR | SYS_MKDIR | SYS_RMDIR | SYS_UNLINK => text(a[0], a[1]),-        SYS_SPAWN => format!("{}, {}", text(a[0], a[1]), text(a[2], a[3])),+        // Paths are C strings (stage 15.1); the *at() calls show the dirfd+        // (-100 = AT_FDCWD) first, like strace.+        SYS_OPEN => format!("{}, {:#o}", cstr(a[0]), a[1]),+        SYS_OPENAT => format!("{}, {}, {:#o}", a[0] as i32, cstr(a[1]), a[2]),+        SYS_STAT | SYS_LSTAT => format!("{}, {:#x}", cstr(a[0]), a[1]),+        SYS_NEWFSTATAT => format!("{}, {}, {:#x}, {:#x}", a[0] as i32, cstr(a[1]), a[2], a[3]),+        SYS_READLINK => format!("{}, {:#x}, {}", cstr(a[0]), a[1], a[2]),+        SYS_CHDIR | SYS_MKDIR | SYS_RMDIR | SYS_UNLINK | SYS_ACCESS => cstr(a[0]),+        SYS_MKDIRAT | SYS_UNLINKAT | SYS_FACCESSAT | SYS_READLINKAT => format!("{}, {}", a[0] as i32, cstr(a[1])),+        SYS_RENAME => format!("{}, {}", cstr(a[0]), cstr(a[1])),+        SYS_SPAWN => format!("{}, {}", cstr(a[0]), text(a[1], a[2])),         SYS_SCHED_YIELD | SYS_GETPID => String::new(),         SYS_CLOSE | SYS_EXIT | SYS_EXIT_GROUP | SYS_DUP => format!("{}", a[0] as i64),         SYS_PIPE | SYS_UNAME => format!("{:#x}", a[0]),@@ -720,40 +789,74 @@ fn sys_write(fd: u64, buf: u64, len: u64) -> Result<u64, Errno> {     result } -/// Read a path argument and make it absolute (relative to the cwd).-fn user_path(ptr: u64, len: u64) -> Result<String, Errno> {-    if len > 4096 {-        return Err(Errno::ENAMETOOLONG);+// --- Paths (stage 15.1: Linux's C-string ABI) --------------------------------------+//+// A path argument is a pointer to a NUL-terminated string, like in C (up+// to stage 15 we took a pointer and a length, which no libc does). We copy+// it in page by page with `user_cstr`, so a bad pointer, or a string that+// runs into an unmapped page, is EFAULT rather than a kernel crash, and at+// most PATH_MAX bytes (with the NUL): longer is ENAMETOOLONG.+//+// The *at() variants take a directory fd first: a relative path starts+// there instead of at the cwd (AT_FDCWD = "the cwd", what the old calls+// mean). musl uses them for stat (newfstatat), opendir is open with+// O_DIRECTORY, and so on.++/// The directory a relative path in an *at() call starts from.+fn at_base(dirfd: u64) -> Result<String, Errno> {+    let p = current()?;+    if dirfd as i32 as i64 == AT_FDCWD {+        return Ok(p.cwd.lock().clone());+    }+    let file = p.file(dirfd)?; // EBADF+    match file.path() {+        Some(path) if file.stat()?.kind == crate::fs::file::KIND_DIR => Ok(path),+        _ => Err(Errno::ENOTDIR),     }-    let path = core::str::from_utf8(user_slice(ptr, len)?).map_err(|_| Errno::EINVAL)?;-    current()?.resolve(path) } -/// Copy a plain-data struct out to user memory.-pub fn copy_to_user<T: Copy>(ptr: u64, value: &T) -> Result<(), Errno> {-    let size = core::mem::size_of::<T>();-    let dst = user_slice_mut(ptr, size as u64)?;-    let src = unsafe { core::slice::from_raw_parts(value as *const T as *const u8, size) };-    dst.copy_from_slice(src);-    Ok(())+/// A path string (already copied in) made absolute, relative to `dirfd`.+fn path_at(dirfd: u64, raw: &[u8]) -> Result<String, Errno> {+    // Our file systems use UTF-8 names (Linux allows any bytes but '/' and NUL).+    let path = core::str::from_utf8(raw).map_err(|_| Errno::EINVAL)?;+    if path.is_empty() {+        return Err(Errno::ENOENT); // like Linux: "" is not a path+    }+    if path.starts_with('/') {+        return fs::normalize("/", path); // absolute: dirfd doesn't matter+    }+    fs::normalize(&at_base(dirfd)?, path) } -/// Copy `N` u32s in from user memory (ioctl arguments).-fn read_user_u32s<const N: usize>(ptr: u64) -> Result<[u32; N], Errno> {-    let src = user_slice(ptr, 4 * N as u64)?;-    let mut out = [0u32; N];-    for (i, word) in out.iter_mut().enumerate() {-        *word = u32::from_le_bytes(src[4 * i..4 * i + 4].try_into().unwrap());-    }-    Ok(out)+/// Copy in a path argument and make it absolute, relative to `dirfd`.+fn user_path_at(dirfd: u64, ptr: u64) -> Result<String, Errno> {+    path_at(dirfd, &user_cstr(ptr, PATH_MAX - 1)?)+}++/// Copy in a path argument, relative to the cwd.+fn user_path(ptr: u64) -> Result<String, Errno> {+    user_path_at(AT_FDCWD as u64, ptr) } -fn sys_open(path_ptr: u64, path_len: u64, flags: u64) -> Result<u64, Errno> {-    let path = user_path(path_ptr, path_len)?;-    let inode = match fs::lookup(&path) {+/// open / openat(dirfd, path, flags, mode). Everything is readable and+/// writable whatever O_RDONLY/O_WRONLY say, and `mode` is ignored (new+/// files get the file system's default permissions).+fn sys_openat(dirfd: u64, path_ptr: u64, flags: u64) -> Result<u64, Errno> {+    let path = user_path_at(dirfd, path_ptr)?;+    let found = if flags & O_NOFOLLOW != 0 { fs::lookup_nofollow(&path) } else { fs::lookup(&path) };+    let inode = match found {         Err(Errno::ENOENT) if flags & O_CREAT != 0 => fs::create(&path, InodeKind::File)?,+        Ok(_) if flags & O_CREAT != 0 && flags & O_EXCL != 0 => return Err(Errno::EEXIST),+        Ok(i) if flags & O_NOFOLLOW != 0 && i.kind() == InodeKind::Symlink => return Err(Errno::ELOOP),         other => other?,     };+    let is_dir = inode.kind() == InodeKind::Directory;+    if flags & O_DIRECTORY != 0 && !is_dir {+        return Err(Errno::ENOTDIR); // opendir() on a file+    }+    if is_dir && flags & O_ACCMODE != 0 {+        return Err(Errno::EISDIR); // directories open read-only+    }     // Device files (/dev/...) come with their own File.     let cloexec = flags & O_CLOEXEC != 0;     if let Some(device) = inode.open_device()? {@@ -769,22 +872,126 @@ fn sys_open(path_ptr: u64, path_len: u64, flags: u64) -> Result<u64, Errno> {     if flags & O_TRUNC != 0 && inode.kind() == InodeKind::File {         inode.truncate()?;     }-    let file = if flags & O_APPEND != 0 { InodeFile::open_append(inode)? } else { InodeFile::open(inode)? };+    let file = if flags & O_APPEND != 0 { InodeFile::open_append(inode, path)? } else { InodeFile::open(inode, path)? };     current()?.add_file_with(Arc::new(file), 0, cloexec) } -fn sys_stat(path_ptr: u64, path_len: u64, stat_ptr: u64) -> Result<u64, Errno> {-    let inode = fs::lookup(&user_path(path_ptr, path_len)?)?;-    copy_to_user(stat_ptr, &Stat::of(&*inode))?;+/// newfstatat(dirfd, path, *stat, flags): stat, lstat (AT_SYMLINK_NOFOLLOW:+/// a symlink itself, not what it points to) and, with AT_EMPTY_PATH and+/// path "", fstat(dirfd). Writes Linux's `struct stat`.+fn sys_newfstatat(dirfd: u64, path_ptr: u64, stat_ptr: u64, flags: u64) -> Result<u64, Errno> {+    let raw = user_cstr(path_ptr, PATH_MAX - 1)?;+    if raw.is_empty() && flags & AT_EMPTY_PATH != 0 && dirfd as i32 as i64 != AT_FDCWD {+        return sys_fstat(dirfd, stat_ptr);+    }+    let path = if raw.is_empty() && flags & AT_EMPTY_PATH != 0 { at_base(dirfd)? } else { path_at(dirfd, &raw)? };+    let inode = if flags & AT_SYMLINK_NOFOLLOW != 0 { fs::lookup_nofollow(&path)? } else { fs::lookup(&path)? };+    let stat = LinuxStat::new(&Stat::of(&*inode), fs::file::ino_hash(path.as_bytes()));+    copy_to_user(stat_ptr, &stat)?;     Ok(0) }  fn sys_fstat(fd: u64, stat_ptr: u64) -> Result<u64, Errno> {-    let stat = current()?.file(fd)?.stat()?;-    copy_to_user(stat_ptr, &stat)?;+    let file = current()?.file(fd)?;+    // Files opened by path: the same inode number stat() gives. Others+    // (pipes, sockets, consoles): the open file's address, unique while open.+    let ino = match file.path() {+        Some(path) => fs::file::ino_hash(path.as_bytes()),+        None => Arc::as_ptr(&file) as *const u8 as u64,+    };+    copy_to_user(stat_ptr, &LinuxStat::new(&file.stat()?, ino))?;     Ok(0) } +/// access / faccessat(dirfd, path, mode): "does it exist?" There are no+/// permissions to check yet (we're root), so existing is enough.+fn sys_faccessat(dirfd: u64, path_ptr: u64) -> Result<u64, Errno> {+    fs::lookup(&user_path_at(dirfd, path_ptr)?)?;+    Ok(0)+}++/// mkdir / mkdirat(dirfd, path, mode); `mode` is ignored.+fn sys_mkdirat(dirfd: u64, path_ptr: u64) -> Result<u64, Errno> {+    fs::create(&user_path_at(dirfd, path_ptr)?, InodeKind::Directory)?;+    Ok(0)+}++/// unlink / rmdir / unlinkat(dirfd, path, flags): AT_REMOVEDIR = rmdir.+fn sys_unlinkat(dirfd: u64, path_ptr: u64, flags: u64) -> Result<u64, Errno> {+    if flags & !AT_REMOVEDIR != 0 {+        return Err(Errno::EINVAL);+    }+    fs::remove(&user_path_at(dirfd, path_ptr)?, flags & AT_REMOVEDIR != 0)?;+    Ok(0)+}++/// rename / renameat / renameat2 (no flags: RENAME_NOREPLACE & co. are+/// EINVAL). See `fs::rename` for what we can and can't move.+fn sys_renameat(olddir: u64, old_ptr: u64, newdir: u64, new_ptr: u64, flags: u64) -> Result<u64, Errno> {+    let old = user_path_at(olddir, old_ptr)?;+    let new = user_path_at(newdir, new_ptr)?;+    if flags != 0 {+        return Err(Errno::EINVAL);+    }+    fs::rename(&old, &new)?;+    Ok(0)+}++fn sys_chdir(path: String) -> Result<u64, Errno> {+    if fs::lookup(&path)?.kind() != InodeKind::Directory {+        return Err(Errno::ENOTDIR);+    }+    *current()?.cwd.lock() = path;+    Ok(0)+}++/// fchdir(fd): cd to a directory we have open.+fn sys_fchdir(fd: u64) -> Result<u64, Errno> {+    sys_chdir(at_base(fd)?)+}++/// readlink / readlinkat(dirfd, path, buf, size): the link's target, *no*+/// NUL added (like Linux); returns its length (cut to `size`).+fn sys_readlinkat(dirfd: u64, path_ptr: u64, buf: u64, len: u64) -> Result<u64, Errno> {+    let path = user_path_at(dirfd, path_ptr)?;+    if len as i64 <= 0 {+        return Err(Errno::EINVAL);+    }+    // There's no /proc yet, but libcs and programs ask "where am I?"+    // with readlink("/proc/self/exe"): answer just that one.+    let target = if path == "/proc/self/exe" {+        current()?.exe.lock().clone()+    } else {+        let inode = fs::lookup_nofollow(&path)?;+        if inode.kind() != InodeKind::Symlink {+            return Err(Errno::EINVAL); // not a symlink+        }+        inode.readlink()?+    };+    let n = target.len().min(len as usize);+    user_slice_mut(buf, n as u64)?.copy_from_slice(&target.as_bytes()[..n]);+    Ok(n as u64)+}++/// Copy a plain-data struct out to user memory.+pub fn copy_to_user<T: Copy>(ptr: u64, value: &T) -> Result<(), Errno> {+    let size = core::mem::size_of::<T>();+    let dst = user_slice_mut(ptr, size as u64)?;+    let src = unsafe { core::slice::from_raw_parts(value as *const T as *const u8, size) };+    dst.copy_from_slice(src);+    Ok(())+}++/// Copy `N` u32s in from user memory (ioctl arguments).+fn read_user_u32s<const N: usize>(ptr: u64) -> Result<[u32; N], Errno> {+    let src = user_slice(ptr, 4 * N as u64)?;+    let mut out = [0u32; N];+    for (i, word) in out.iter_mut().enumerate() {+        *word = u32::from_le_bytes(src[4 * i..4 * i + 4].try_into().unwrap());+    }+    Ok(out)+}+ fn sys_pipe2(fds_ptr: u64, flags: u64) -> Result<u64, Errno> {     if flags & !(O_CLOEXEC | O_NONBLOCK) != 0 {         return Err(Errno::EINVAL); // O_DIRECT (packet mode): not supported@@ -848,33 +1055,15 @@ fn sys_getcwd(buf: u64, len: u64) -> Result<u64, Errno> {     Ok(cwd.len() as u64) } -fn sys_chdir(path_ptr: u64, path_len: u64) -> Result<u64, Errno> {-    let path = user_path(path_ptr, path_len)?;-    if fs::lookup(&path)?.kind() != InodeKind::Directory {-        return Err(Errno::ENOTDIR);-    }-    *current()?.cwd.lock() = path;-    Ok(0)-}--fn sys_readlink(path_ptr: u64, path_len: u64, buf: u64, len: u64) -> Result<u64, Errno> {-    let path = user_path(path_ptr, path_len)?;-    // There's no /proc yet, but libcs and programs ask "where am I?"-    // with readlink("/proc/self/exe"): answer just that one.-    let target = if path == "/proc/self/exe" { current()?.exe.lock().clone() } else { fs::readlink(&path)? };-    let n = target.len().min(len as usize);-    user_slice_mut(buf, n as u64)?.copy_from_slice(&target.as_bytes()[..n]);-    Ok(n as u64)-}- fn sys_getdents64(fd: u64, buf: u64, len: u64) -> Result<u64, Errno> {     let file = current()?.file(fd)?;     file.getdents(user_slice_mut(buf, len)?).map(|n| n as u64) } -fn sys_spawn(path_ptr: u64, path_len: u64, argv_ptr: u64, argv_len: u64, opts_ptr: u64) -> Result<u64, Errno> {+/// spawn(path, argv blob, blob length, *options): our own call (500).+fn sys_spawn(path_ptr: u64, argv_ptr: u64, argv_len: u64, opts_ptr: u64) -> Result<u64, Errno> {     let parent = current()?;-    let path = user_path(path_ptr, path_len)?;+    let path = user_path(path_ptr)?;     if argv_len as usize > process::MAX_ARGS_SIZE {         return Err(Errno::E2BIG);     }@@ -1049,11 +1238,25 @@ fn read_sockaddr(ptr: u64) -> Result<crate::net::socket::Addr, Errno> {     Ok(([bytes[4], bytes[5], bytes[6], bytes[7]], u16::from_be_bytes([bytes[2], bytes[3]]))) } -fn write_sockaddr(ptr: u64, (addr, port): crate::net::socket::Addr) -> Result<(), Errno> {+/// Write a peer's address like Linux: `*len_ptr` (a u32) says how much+/// room there is; we write at most that much and set it to the real size+/// (16). No `len_ptr` (only our own older programs do that): all 16 bytes.+fn write_sockaddr(ptr: u64, len_ptr: u64, (addr, port): crate::net::socket::Addr) -> Result<(), Errno> {     if ptr == 0 {         return Ok(()); // caller doesn't want it     }-    copy_to_user(ptr, &SockAddrIn { family: AF_INET as u16, port_be: port.to_be(), addr, zero: [0; 8] })+    let sa = SockAddrIn { family: AF_INET as u16, port_be: port.to_be(), addr, zero: [0; 8] };+    let bytes = unsafe { core::slice::from_raw_parts(&sa as *const SockAddrIn as *const u8, 16) };+    let room = if len_ptr == 0 {+        16+    } else {+        u32::from_le_bytes(user_slice(len_ptr, 4)?.try_into().unwrap()).min(16) as usize+    };+    user_slice_mut(ptr, room as u64)?.copy_from_slice(&bytes[..room]);+    if len_ptr != 0 {+        copy_to_user(len_ptr, &16u32)?;+    }+    Ok(()) }  /// Run `f` on the socket behind `fd` (ENOTSOCK if it's another kind of@@ -1085,10 +1288,7 @@ fn sys_accept(fd: u64, addr_ptr: u64, len_ptr: u64, flags: u64) -> Result<u64, E     let (socket, peer) = with_socket(fd, |s| s.accept())?;     socket.set_nonblocking(flags & O_NONBLOCK != 0);     let new_fd = current()?.add_file_with(Arc::new(socket), 0, flags & O_CLOEXEC != 0)?;-    write_sockaddr(addr_ptr, peer)?;-    if len_ptr != 0 {-        copy_to_user(len_ptr, &16u32)?;-    }+    write_sockaddr(addr_ptr, len_ptr, peer)?;     Ok(new_fd) } @@ -1097,10 +1297,10 @@ fn sys_sendto(fd: u64, buf: u64, len: u64, addr_ptr: u64) -> Result<u64, Errno>      with_socket(fd, |s| s.send(user_slice(buf, len)?, to)).map(|n| n as u64) } -fn sys_recvfrom(fd: u64, buf: u64, len: u64, addr_ptr: u64) -> Result<u64, Errno> {+fn sys_recvfrom(fd: u64, buf: u64, len: u64, addr_ptr: u64, len_ptr: u64) -> Result<u64, Errno> {     let (n, from) = with_socket(fd, |s| s.recv(user_slice_mut(buf, len)?))?;     if let Some(from) = from {-        write_sockaddr(addr_ptr, from)?;+        write_sockaddr(addr_ptr, len_ptr, from)?;     }     Ok(n as u64) }@@ -1192,8 +1392,7 @@ fn user_cstr_array(ptr: u64) -> Result<Vec<Vec<u8>>, Errno> {  fn sys_execve(frame: &mut TrapFrame, path: u64, argv: u64, envp: u64) -> Result<u64, Errno> {     let p = current()?;-    let path = String::from_utf8(user_cstr(path, 4096)?).map_err(|_| Errno::EINVAL)?;-    let path = p.resolve(&path)?;+    let path = user_path(path)?;     let argv = user_cstr_array(argv)?;     let envp = user_cstr_array(envp)?;     process::execve(&p, frame, &path, &argv, &envp)?;@@ -1283,8 +1482,28 @@ fn sys_tgkill(tgid: Option<u64>, tid: u64, sig: u64) -> Result<u64, Errno> {  /// nanosleep(*req, *rem). Our timer ticks every ~55 ms, so sleeps are /// rounded up to that. Cut short by a signal: the time left goes to *rem.+/// clock_nanosleep(clock, flags, *req, *rem): nanosleep on a chosen clock;+/// with TIMER_ABSTIME (1), `req` is a time on that clock to sleep until.+fn sys_clock_nanosleep(clock: u64, flags: u64, req: u64, rem: u64) -> Result<u64, Errno> {+    let now = match clock {+        0 => crate::time::realtime_us(),+        1 | 7 => crate::time::now_us(),+        _ => return Err(Errno::EINVAL), // CPU-time clocks: can't sleep on those+    };+    if flags & 1 == 0 {+        return sys_nanosleep(req, rem);+    }+    // Absolute: sleep the difference (and never report time left, like Linux).+    sleep_for(read_timespec_us(req)?.saturating_sub(now), 0)+}+ fn sys_nanosleep(req: u64, rem: u64) -> Result<u64, Errno> {-    let us = read_timespec_us(req)?;+    sleep_for(read_timespec_us(req)?, rem)+}++/// Sleep `us` microseconds; if a signal cuts it short, the time left goes+/// to `*rem` (a timespec) unless that's 0.+fn sleep_for(us: u64, rem: u64) -> Result<u64, Errno> {     if us == 0 {         thread::yield_now();         return Ok(0);@@ -1373,14 +1592,22 @@ fn sys_writev(fd: u64, iov: u64, count: u64) -> Result<u64, Errno> {     result } -fn sys_wait4(pid: i64, status_ptr: u64, options: u64) -> Result<u64, Errno> {+/// wait4(pid, *status, options, *rusage). We don't count CPU time per+/// process yet, so a `struct rusage` (144 bytes) comes back all zeros.+fn sys_wait4(pid: i64, status_ptr: u64, options: u64, rusage_ptr: u64) -> Result<u64, Errno> {     if status_ptr != 0 {         user_slice_mut(status_ptr, 4)?; // check before reaping anything     }+    if rusage_ptr != 0 {+        user_slice_mut(rusage_ptr, 144)?;+    }     let (pid, status) = process::wait4(&*current()?, pid, options)?;     if status_ptr != 0 && pid != 0 {         copy_to_user(status_ptr, &status)?;     }+    if rusage_ptr != 0 && pid != 0 {+        user_slice_mut(rusage_ptr, 144)?.fill(0);+    }     Ok(pid) } 
@@ -41,7 +41,7 @@ fn fat_volume_is_mounted() {  #[test_case] fn read_text_file() {-    let file = InodeFile::open(fs::lookup("/mnt/disk0/readme.txt").unwrap()).unwrap();+    let file = InodeFile::open(fs::lookup("/mnt/disk0/readme.txt").unwrap(), "/mnt/disk0/readme.txt".into()).unwrap();     let mut buf = [0u8; 256];     let n = file.read(&mut buf).unwrap();     assert!(buf[..n].starts_with(b"Hello from the FAT32 disk!"));
@@ -68,6 +68,14 @@ fn fpu_state_is_per_thread() {     run_without_leaks("fputest", &[]); } +/// C-string paths: EFAULT for bad pointers (NULL, kernel, unmapped, a+/// string running off its page), ENAMETOOLONG, Linux's `struct stat`, the+/// *at() calls, rename.+#[test_case]+fn linux_path_syscalls() {+    run_without_leaks("pathtest", &[]);+}+ #[panic_handler] fn panic(info: &PanicInfo) -> ! {     rustbelt::test_panic_handler(info)
@@ -28,7 +28,7 @@ fn main(boot_info: &'static BootInfo) -> ! {  #[test_case] fn read_boot_files() {-    let file = InodeFile::open(fs::lookup("/hello.txt").unwrap()).unwrap();+    let file = InodeFile::open(fs::lookup("/hello.txt").unwrap(), "/hello.txt".into()).unwrap();     let mut buf = [0u8; 64];     let n = file.read(&mut buf).unwrap();     assert!(buf[..n].starts_with(b"Hello from the ramfs!"));@@ -44,7 +44,7 @@ fn lookup_errors() {     assert_eq!(fs::lookup("/hello.txt/x").err(), Some(Errno::ENOTDIR));     assert_eq!(fs::lookup("relative").err(), Some(Errno::EINVAL));     // Directories can be opened (for getdents) but not read like files.-    let dir = InodeFile::open(fs::lookup("/etc").unwrap()).unwrap();+    let dir = InodeFile::open(fs::lookup("/etc").unwrap(), "/etc".into()).unwrap();     assert_eq!(dir.read(&mut [0u8; 8]), Err(Errno::EISDIR)); } @@ -103,7 +103,7 @@ fn remove_files_and_directories() { #[test_case] fn seek_stat_and_truncate() {     let inode = fs::create("/tmp/seek.txt", InodeKind::File).unwrap();-    let file = InodeFile::open(inode.clone()).unwrap();+    let file = InodeFile::open(inode.clone(), "/tmp/seek.txt".into()).unwrap();     file.write(b"0123456789").unwrap();     assert_eq!(file.seek(-3, SEEK_END), Ok(7));     let mut buf = [0u8; 8];@@ -119,7 +119,7 @@ fn seek_stat_and_truncate() {  #[test_case] fn getdents_returns_linux_dirent64_records() {-    let dir = InodeFile::open(fs::lookup("/etc").unwrap()).unwrap();+    let dir = InodeFile::open(fs::lookup("/etc").unwrap(), "/etc".into()).unwrap();     let mut buf = [0u8; 64];     let n = dir.getdents(&mut buf).unwrap();     // One entry, "motd": 19-byte header + "motd\0" = 24 bytes (already 8-aligned).
@@ -19,7 +19,7 @@ fn main() -> i64 {     };     let mut st = Stat::default();     let mut dst = String::from(dst);-    if stat(&dst, &mut st) == 0 && st.kind == KIND_DIR {+    if stat(&dst, &mut st) == 0 && st.is_dir() {         let name = src.rsplit('/').next().unwrap_or(src);         dst = format!("{}/{}", dst.trim_end_matches('/'), name);     }@@ -28,7 +28,7 @@ fn main() -> i64 {         eprintln!("cp: {}: {}", src, strerror(from));         return 1;     }-    let to = open(&dst, O_CREAT | O_TRUNC);+    let to = open(&dst, O_WRONLY | O_CREAT | O_TRUNC);     if to < 0 {         eprintln!("cp: {}: {}", dst, strerror(to));         close(from);
@@ -46,9 +46,9 @@ fn main() -> i64 {      // --- stat / fstat / lseek ------------------------------------------------     let mut st = Stat::default();-    check(stat("readme.txt", &mut st) == 0 && st.kind == KIND_FILE && st.size == n as u64, "stat readme.txt");-    check(fstat(1, &mut st) == 0 && st.kind == KIND_CONSOLE, "fstat stdout = console");-    check(stat("bin", &mut st) == 0 && st.kind == KIND_DIR, "stat bin is a directory");+    check(stat("readme.txt", &mut st) == 0 && st.is_file() && st.size == n as i64, "stat readme.txt");+    check(fstat(1, &mut st) == 0 && st.is_char_device() && isatty(1), "fstat stdout = a terminal");+    check(stat("bin", &mut st) == 0 && st.is_dir(), "stat bin is a directory");     check(lseek(fd, 6, SEEK_SET) == 6, "lseek SEEK_SET");     let mut word = [0u8; 4];     check(read(fd, &mut word) == 4 && &word == b"from", "read after lseek");@@ -87,7 +87,7 @@ fn main() -> i64 {         check(mkdir(dir) == -EEXIST, "mkdir again -> EEXIST");         let mut file_buf = [0u8; 64];         let file = join(&mut file_buf, dir, "/f.txt");-        let fd = open(file, O_CREAT);+        let fd = open(file, O_WRONLY | O_CREAT);         check(write(fd, b"bytes") == 5, "write new file");         close(fd);         check(rmdir(dir) == -ENOTEMPTY, "rmdir non-empty -> ENOTEMPTY");
@@ -97,7 +97,7 @@ fn main() -> i64 {      // ...then the body, to stdout or the file, until the server closes.     let out = match out_path {-        Some(p) => ulib::open(p, ulib::O_CREAT | ulib::O_TRUNC),+        Some(p) => ulib::open(p, ulib::O_WRONLY | ulib::O_CREAT | ulib::O_TRUNC),         None => 1,     };     if out < 0 {
@@ -32,7 +32,7 @@ fn main() -> i64 {     check(&mut failures, close(fd) == 0, "close");      // Create a file on the second mount (/tmp), then read it back.-    let fd = open("/tmp/note.txt", O_CREAT);+    let fd = open("/tmp/note.txt", O_WRONLY | O_CREAT);     check(&mut failures, write(fd, b"written from ring 3") == 19, "write /tmp/note.txt");     close(fd);     let fd = open("/tmp/note.txt", 0);
@@ -109,7 +109,7 @@ fn respond(conn: i64, status: &'static str, body: &[u8]) -> (&'static str, usize  fn send_file(conn: i64, path: &str) -> (&'static str, usize) {     let mut st = ulib::Stat::default();-    if ulib::stat(path, &mut st) < 0 || st.kind != ulib::KIND_FILE {+    if ulib::stat(path, &mut st) < 0 || !st.is_file() {         return respond(conn, "404 Not Found", b"not found\n");     }     let fd = ulib::open(path, 0);
@@ -41,7 +41,7 @@ fn main() -> i64 {         paths.push(".");     }     let mut st = Stat::default();-    let columns = !long && fstat(1, &mut st) == 0 && st.kind == KIND_CONSOLE;+    let columns = !long && isatty(1);      let mut status = 0;     for (i, path) in paths.iter().enumerate() {@@ -51,8 +51,8 @@ fn main() -> i64 {             status = 1;             continue;         }-        if st.kind != KIND_DIR {-            let entry = Entry { name: String::from(*path), dir: false, link: false, dev: st.kind == KIND_DEVICE, size: st.size, mode: st.mode, target: String::new() };+        if !st.is_dir() {+            let entry = Entry { name: String::from(*path), dir: false, link: false, dev: st.is_char_device(), size: st.size as u64, mode: st.perm() as u64, target: String::new() };             print_entries(&[entry], long, columns);             continue;         }@@ -104,8 +104,8 @@ fn read_dir(path: &str, long: bool) -> Result<Vec<Entry>, i64> {                     e.target = String::from(core::str::from_utf8(&buf[..n as usize]).unwrap_or("?"));                 }             } else if stat(&full, &mut st) == 0 {-                e.size = st.size;-                e.mode = st.mode;+                e.size = st.size as u64;+                e.mode = st.perm() as u64;             }         }     }
@@ -0,0 +1,177 @@+//! pathtest -- Linux's path syscalls: C strings, `struct stat`, *at() (stage 15.1).+//!+//! Since 15.1 every path argument is what C passes: a pointer to a+//! NUL-terminated string (before, our own programs passed pointer ++//! length, which no libc can). The kernel copies it in carefully, so:+//!   - a NULL, kernel-space, or unmapped pointer is EFAULT (not a crash),+//!     also when the string runs off the end of a mapped page;+//!   - more than PATH_MAX (4096) bytes without a NUL is ENAMETOOLONG;+//!   - "" is ENOENT.+//! Then: stat/fstat/lstat/newfstatat fill in Linux's 144-byte `struct+//! stat`, and the *at() calls take paths relative to a directory fd.+//! Exit code = number of failed checks.+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++extern crate alloc;++use alloc::string::String;+use alloc::vec;+use ulib::*;++/// A kernel address: user code may never pass one of these.+const KERNEL_PTR: u64 = 0xffff_8000_0000_0000;++fn sys(nr: u64, a: u64, b: u64, c: u64) -> i64 {+    syscall3(nr, a, b, c)+}++fn main() -> i64 {+    let mut c = Checks::new("pathtest");+    let mut st = Stat::default();+    let stp = &mut st as *mut Stat as u64;++    // --- bad pointers: EFAULT, from every path call ---------------------------+    for &(bad, what) in &[(0u64, "NULL"), (KERNEL_PTR, "a kernel address"), (0xf0_0000_0000, "an unmapped user address")] {+        let calls = [+            ("open", sys(SYS_OPEN, bad, 0, 0)),+            ("openat", syscall5(SYS_OPENAT, AT_FDCWD as u64, bad, 0, 0, 0)),+            ("stat", sys(SYS_STAT, bad, stp, 0)),+            ("lstat", sys(SYS_LSTAT, bad, stp, 0)),+            ("newfstatat", syscall5(SYS_NEWFSTATAT, AT_FDCWD as u64, bad, stp, 0, 0)),+            ("access", sys(SYS_ACCESS, bad, 0, 0)),+            ("mkdir", sys(SYS_MKDIR, bad, 0o755, 0)),+            ("rmdir", sys(SYS_RMDIR, bad, 0, 0)),+            ("unlink", sys(SYS_UNLINK, bad, 0, 0)),+            ("rename", sys(SYS_RENAME, bad, bad, 0)),+            ("chdir", sys(SYS_CHDIR, bad, 0, 0)),+            ("readlink", sys(SYS_READLINK, bad, stp, 64)),+            ("execve", sys(SYS_EXECVE, bad, 0, 0)),+        ];+        let failed: alloc::vec::Vec<&str> = calls.iter().filter(|(_, r)| *r != -EFAULT).map(|(n, _)| *n).collect();+        if !failed.is_empty() {+            println!("pathtest: not EFAULT for {}: {:?}", what, failed);+        }+        c.check(failed.is_empty(), &alloc::format!("13 path calls with {} as the path: EFAULT", what));+    }+    // A path that runs off the end of a mapped page into an unmapped one:+    // the kernel must notice at the page boundary.+    let two = mmap(0, 8192, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0) as u64;+    munmap(two + 4096, 4096);+    let tail = two + 4096 - 8;+    unsafe { core::ptr::copy_nonoverlapping(b"/tmp/abc".as_ptr(), tail as *mut u8, 8) }; // no NUL+    c.check(sys(SYS_STAT, tail, stp, 0) == -EFAULT, "a path running into an unmapped page: EFAULT");+    unsafe { *((two + 4095) as *mut u8) = 0 }; // now it ends just in time+    c.check(sys(SYS_STAT, tail, stp, 0) == -ENOENT, "the same path ending right at the page end: ENOENT");+    munmap(two, 4096);+    // Good path, bad `struct stat` pointer.+    c.check(sys(SYS_STAT, c"/".as_ptr() as u64, KERNEL_PTR, 0) == -EFAULT, "stat into a kernel address: EFAULT");+    c.check(sys(SYS_FSTAT, 0, 0, 0) == -EFAULT, "fstat into NULL: EFAULT");++    // --- lengths ------------------------------------------------------------------+    let mut long = vec![b'a'; 5000]; // no NUL in the first 4096 bytes+    long.push(0);+    c.check(sys(SYS_STAT, long.as_ptr() as u64, stp, 0) == -ENAMETOOLONG, "no NUL within PATH_MAX: ENAMETOOLONG");+    // 4095 bytes + NUL is allowed (short components, so it's just missing).+    let mut max = String::new();+    while max.len() < 4095 {+        max.push_str("/d");+    }+    max.truncate(4095);+    c.check(stat(&max, &mut st) == -ENOENT, "a 4095-byte path is accepted (ENOENT)");+    c.check(sys(SYS_STAT, c"".as_ptr() as u64, stp, 0) == -ENOENT, "an empty path: ENOENT");++    // --- Linux's struct stat ----------------------------------------------------+    c.check(core::mem::size_of::<Stat>() == 144, "struct stat is 144 bytes");+    c.check(stat("/mnt/disk0/bin", &mut st) == 0 && st.is_dir() && st.nlink == 2, "a directory: S_IFDIR, 2 links");+    let r = stat("/mnt/disk0/bin/pathtest", &mut st);+    let ino = st.ino;+    c.check(+        r == 0 && st.is_file() && st.size > 0 && st.blksize == 4096 && st.blocks == (st.size + 511) / 512 && st.ino != 0,+        "a file: S_IFREG, size, blksize 4096, 512-byte blocks, an inode number",+    );+    c.check(st.uid == 0 && st.gid == 0 && st.perm() != 0, "owned by root, with permission bits");+    let fd = open("/mnt/disk0/bin/pathtest", O_RDONLY);+    let mut st2 = Stat::default();+    c.check(fstat(fd, &mut st2) == 0 && st2.ino == ino && st2.size == st.size, "fstat on it: same inode and size");+    let mut st3 = Stat::default();+    c.check(fstatat(fd, "", &mut st3, AT_EMPTY_PATH) == 0 && st3.ino == ino, "newfstatat(fd, \"\", AT_EMPTY_PATH) = fstat");+    c.check(fstatat(fd, "x", &mut st3, 0) == -ENOTDIR, "a file as dirfd: ENOTDIR");+    close(fd);+    c.check(fstat(0, &mut st) == 0 && st.is_char_device() && isatty(0), "stdin: a character device and a tty");+    let (r, w) = pipe().unwrap();+    c.check(fstat(r, &mut st) == 0 && st.file_type() == S_IFIFO, "a pipe: S_IFIFO");+    close(r);+    close(w);++    // Symlinks: stat follows, lstat doesn't (on the ext2 disk, if it's there).+    let link = (0..4).map(|i| alloc::format!("/mnt/disk{}/hello-link", i)).find(|p| lstat(p, &mut st) == 0);+    if let Some(link) = link {+        c.check(lstat(&link, &mut st) == 0 && st.is_symlink() && st.size == 9, "lstat on a symlink: S_IFLNK, size = target length");+        c.check(stat(&link, &mut st) == 0 && st.is_file(), "stat on it: the file it points to");+        let mut buf = [0u8; 64];+        let n = readlink(&link, &mut buf);+        c.check(n == 9 && &buf[..9] == b"hello.txt", "readlink: the target, no NUL");+        let dir = &link[..link.rfind('/').unwrap()];+        let dfd = open(dir, O_RDONLY | O_DIRECTORY);+        let n = syscall5(SYS_READLINKAT, dfd as u64, c"hello-link".as_ptr() as u64, buf.as_mut_ptr() as u64, 64, 0);+        c.check(n == 9, "readlinkat(dirfd, \"hello-link\")");+        c.check(fstatat(dfd, "hello-link", &mut st, AT_SYMLINK_NOFOLLOW) == 0 && st.is_symlink(), "newfstatat AT_SYMLINK_NOFOLLOW = lstat");+        close(dfd);+    } else {+        println!("pathtest: no ext2 disk with hello-link: skipped symlink checks");+    }+    c.check(readlink("/mnt/disk0/bin", &mut [0u8; 8]) == -EINVAL, "readlink on a non-link: EINVAL");++    // --- open flags ----------------------------------------------------------------+    c.check(open("/mnt/disk0/bin/pathtest", O_RDONLY | O_DIRECTORY) == -ENOTDIR, "O_DIRECTORY on a file: ENOTDIR");+    c.check(open("/tmp", O_WRONLY) == -EISDIR, "a directory opened for writing: EISDIR");+    let fd = open("/tmp/pt-excl", O_WRONLY | O_CREAT | O_EXCL);+    c.check(fd >= 0 && open("/tmp/pt-excl", O_WRONLY | O_CREAT | O_EXCL) == -EEXIST, "O_CREAT|O_EXCL: creates, then EEXIST");+    close(fd);+    c.check(access("/tmp/pt-excl") == 0 && access("/tmp/nope") == -ENOENT, "access: 0 / ENOENT");++    // --- *at() relative to a directory fd ------------------------------------------+    c.check(mkdir("/tmp/ptdir") == 0, "mkdir /tmp/ptdir");+    let dfd = open("/tmp/ptdir", O_RDONLY | O_DIRECTORY);+    c.check(dfd >= 0, "open a directory with O_DIRECTORY");+    let fd = openat(dfd, "inner.txt", O_WRONLY | O_CREAT);+    c.check(fd >= 0 && write(fd, b"12345") == 5, "openat(dirfd, \"inner.txt\", O_CREAT) + write");+    close(fd);+    c.check(stat("/tmp/ptdir/inner.txt", &mut st) == 0 && st.size == 5, "...it's in that directory");+    c.check(openat(AT_FDCWD, "/tmp/ptdir/inner.txt", O_RDONLY) >= 0, "openat(AT_FDCWD, absolute)");+    c.check(openat(99, "inner.txt", O_RDONLY) == -EBADF, "openat with a bad dirfd: EBADF");+    c.check(openat(99, "/tmp/ptdir/inner.txt", O_RDONLY) >= 0, "...unless the path is absolute");+    c.check(syscall5(SYS_MKDIRAT, dfd as u64, c"sub".as_ptr() as u64, 0o755, 0, 0) == 0, "mkdirat(dirfd, \"sub\")");+    c.check(sys(SYS_FACCESSAT, dfd as u64, c"sub".as_ptr() as u64, 0) == 0, "faccessat(dirfd, \"sub\")");+    c.check(syscall5(SYS_UNLINKAT, dfd as u64, c"sub".as_ptr() as u64, 0, 0, 0) == -EISDIR, "unlinkat on a directory without AT_REMOVEDIR: EISDIR");+    c.check(syscall5(SYS_UNLINKAT, dfd as u64, c"sub".as_ptr() as u64, AT_REMOVEDIR, 0, 0) == 0, "unlinkat(AT_REMOVEDIR)");+    let r = syscall5(SYS_RENAMEAT, dfd as u64, c"inner.txt".as_ptr() as u64, AT_FDCWD as u64, c"/tmp/renamed.txt".as_ptr() as u64, 0);+    c.check(r == 0 && access("/tmp/ptdir/inner.txt") == -ENOENT, "renameat(dirfd, \"inner.txt\", AT_FDCWD, \"/tmp/renamed.txt\")");+    // fchdir: cd to the directory we have open.+    c.check(sys(SYS_FCHDIR, dfd as u64, 0, 0) == 0 && stat("../renamed.txt", &mut st) == 0 && st.size == 5, "fchdir(dirfd), then a relative path");+    chdir("/");+    close(dfd);++    // --- rename -----------------------------------------------------------------------+    let fd = open("/tmp/pt-new", O_WRONLY | O_CREAT);+    write(fd, b"new contents");+    close(fd);+    c.check(rename("/tmp/pt-new", "/tmp/renamed.txt") == 0, "rename a file over another");+    c.check(stat("/tmp/renamed.txt", &mut st) == 0 && st.size == 12 && access("/tmp/pt-new") == -ENOENT, "...it replaced it");+    c.check(rename("/tmp/renamed.txt", "/mnt/disk0/x.txt") == -EXDEV, "rename across file systems: EXDEV");+    c.check(rename("/tmp/ptdir", "/tmp/ptdir2") == 0 && stat("/tmp/ptdir2", &mut st) == 0 && st.is_dir(), "rename an empty directory");+    c.check(rename("/tmp/ptdir2", "/tmp/ptdir2/in") == -EINVAL, "rename a directory into itself: EINVAL");+    c.check(rename("/tmp/nope", "/tmp/x") == -ENOENT, "rename a missing file: ENOENT");+    unlink("/tmp/renamed.txt");+    unlink("/tmp/pt-excl");+    rmdir("/tmp/ptdir2");++    // --- small things musl asks at startup ------------------------------------------+    c.check(sys(SYS_GETUID, 0, 0, 0) == 0, "getuid: 0 (root)");+    c.done()+}
@@ -117,8 +117,7 @@ fn main() -> i64 {         }         None => 0,     };-    let mut st = Stat::default();-    let interactive = fd == 0 && fstat(0, &mut st) == 0 && st.kind == KIND_CONSOLE;+    let interactive = fd == 0 && isatty(0);     let mut input = LineReader { fd, buf: Vec::new(), eof: false };      sh.refresh_drives();@@ -386,7 +385,7 @@ impl Shell {                 prev_pipe = r;             }             if let Some((file, append)) = &stage.stdout {-                let flags = O_CREAT | O_CLOEXEC | if *append { O_APPEND } else { O_TRUNC };+                let flags = O_WRONLY | O_CREAT | O_CLOEXEC | if *append { O_APPEND } else { O_TRUNC };                 stdout = open(file, flags);                 if stdout < 0 {                     eprintln!("sh: {}: {}", file, strerror(stdout));@@ -512,7 +511,7 @@ impl Shell {             return Some(name.to_string());         }         let mut st = Stat::default();-        self.drives.iter().map(|d| format!("{}/bin/{}", d.path.trim_end_matches('/'), name)).find(|p| stat(p, &mut st) == 0 && st.kind == KIND_FILE)+        self.drives.iter().map(|d| format!("{}/bin/{}", d.path.trim_end_matches('/'), name)).find(|p| stat(p, &mut st) == 0 && st.is_file())     }      /// Start one program: fork, then in the child set up its process
@@ -13,7 +13,7 @@ fn main() -> i64 {     let mut status = 0;     for i in 1..argc() {         let path = arg(i).unwrap_or("");-        let fd = open(path, O_CREAT); // no O_TRUNC: keep the contents+        let fd = open(path, O_WRONLY | O_CREAT); // no O_TRUNC: keep the contents         if fd < 0 {             eprintln!("touch: {}: {}", path, strerror(fd));             status = 1;
@@ -63,6 +63,25 @@ pub const SYS_UNLINK: u64 = 87; pub const SYS_READLINK: u64 = 89; pub const SYS_GETDENTS64: u64 = 217; pub const SYS_SPAWN: u64 = 500;+// Stage 15.1: more of Linux's path calls (all take C strings).+pub const SYS_LSTAT: u64 = 6;+pub const SYS_ACCESS: u64 = 21;+pub const SYS_RENAME: u64 = 82;+pub const SYS_FCHDIR: u64 = 81;+pub const SYS_GETUID: u64 = 102;+pub const SYS_OPENAT: u64 = 257;+pub const SYS_MKDIRAT: u64 = 258;+pub const SYS_NEWFSTATAT: u64 = 262;+pub const SYS_UNLINKAT: u64 = 263;+pub const SYS_RENAMEAT: u64 = 264;+pub const SYS_READLINKAT: u64 = 267;+pub const SYS_FACCESSAT: u64 = 269;+pub const AT_FDCWD: i64 = -100;+pub const AT_SYMLINK_NOFOLLOW: u64 = 0x100;+pub const AT_REMOVEDIR: u64 = 0x200;+pub const AT_EMPTY_PATH: u64 = 0x1000;+/// Longest path the kernel takes, counting the NUL.+pub const PATH_MAX: usize = 4096; pub const SYS_MOUNTS: u64 = 501; pub const SYS_SYSINFO: u64 = 502; pub const SYS_SOCKET: u64 = 41;@@ -78,7 +97,11 @@ pub const SYS_RESOLVE: u64 = 503; pub const SYS_NETINFO: u64 = 504; pub const SYS_GETRANDOM: u64 = 318; +pub const O_RDONLY: u64 = 0;+pub const O_WRONLY: u64 = 1; pub const O_CREAT: u64 = 0o100;+pub const O_EXCL: u64 = 0o200;+pub const O_DIRECTORY: u64 = 0o200000; pub const O_TRUNC: u64 = 0o1000; pub const O_APPEND: u64 = 0o2000; pub const TTY_SET_RAW: u64 = 1;@@ -114,23 +137,63 @@ pub const EEXIST: i64 = 17; pub const ENOTDIR: i64 = 20; pub const EISDIR: i64 = 21; pub const ESPIPE: i64 = 29;+pub const EXDEV: i64 = 18;+pub const EINVAL: i64 = 22;+pub const ENAMETOOLONG: i64 = 36; pub const ENOTEMPTY: i64 = 39;+pub const ELOOP: i64 = 40; pub const ETIMEDOUT: i64 = 110; -/// Same layout as the kernel's `fs::file::Stat`.+/// Linux's `struct stat` on x86_64 (what stat/fstat/lstat fill in since+/// stage 15.1; the same 144 bytes a C program's <sys/stat.h> describes). #[repr(C)] #[derive(Default, Clone, Copy)] pub struct Stat {-    pub kind: u64, // KIND_*-    pub size: u64,-    pub mode: u64, // permission bits like 0o644; 0 = the file system has none+    pub dev: u64,+    pub ino: u64,+    pub nlink: u64,+    pub mode: u32, // file type (S_IF*) | permission bits like 0o644+    pub uid: u32,+    pub gid: u32,+    _pad0: u32,+    pub rdev: u64,+    pub size: i64,+    pub blksize: i64,+    pub blocks: i64,+    pub atime: [i64; 2], // seconds, nanoseconds+    pub mtime: [i64; 2],+    pub ctime: [i64; 2],+    _unused: [i64; 3],+}+pub const S_IFMT: u32 = 0o170000; // the type bits of `mode`+pub const S_IFIFO: u32 = 0o010000;+pub const S_IFCHR: u32 = 0o020000; // terminals, /dev/null, /dev/fb0, ...+pub const S_IFDIR: u32 = 0o040000;+pub const S_IFREG: u32 = 0o100000;+pub const S_IFLNK: u32 = 0o120000;+pub const S_IFSOCK: u32 = 0o140000;++impl Stat {+    pub fn file_type(&self) -> u32 {+        self.mode & S_IFMT+    }+    pub fn is_dir(&self) -> bool {+        self.file_type() == S_IFDIR+    }+    pub fn is_file(&self) -> bool {+        self.file_type() == S_IFREG+    }+    pub fn is_symlink(&self) -> bool {+        self.file_type() == S_IFLNK+    }+    pub fn is_char_device(&self) -> bool {+        self.file_type() == S_IFCHR+    }+    /// Permission bits (0o755 & co.).+    pub fn perm(&self) -> u32 {+        self.mode & 0o7777+    } }-pub const KIND_FILE: u64 = 1;-pub const KIND_DIR: u64 = 2;-pub const KIND_CONSOLE: u64 = 3;-pub const KIND_PIPE: u64 = 4;-pub const KIND_SYMLINK: u64 = 5;-pub const KIND_DEVICE: u64 = 7; // /dev/null, /dev/fb0, ...  /// Same layout as the kernel's `syscall::SpawnOptions`. #[repr(C)]@@ -196,18 +259,51 @@ pub fn read(fd: i64, buf: &mut [u8]) -> i64 { pub fn write(fd: i64, buf: &[u8]) -> i64 {     syscall3(SYS_WRITE, fd as u64, buf.as_ptr() as u64, buf.len() as u64) }+/// Call `f` with `path` as a C string (NUL-terminated), the way the+/// kernel's path calls want it (Linux's ABI). A Rust &str isn't one: it has+/// no NUL at the end, so we copy it into a buffer and add one. Too long+/// for the kernel: -ENAMETOOLONG; a NUL inside: -EINVAL (C can't say that).+pub fn with_cpath(path: &str, f: impl FnOnce(u64) -> i64) -> i64 {+    if path.len() >= PATH_MAX {+        return -ENAMETOOLONG;+    }+    if path.as_bytes().contains(&0) {+        return -EINVAL;+    }+    let mut c = alloc::vec::Vec::with_capacity(path.len() + 1);+    c.extend_from_slice(path.as_bytes());+    c.push(0);+    f(c.as_ptr() as u64)+}+/// open(path, flags, mode): new files get mode 0o644 (the kernel ignores it). pub fn open(path: &str, flags: u64) -> i64 {-    syscall3(SYS_OPEN, path.as_ptr() as u64, path.len() as u64, flags)+    with_cpath(path, |p| syscall3(SYS_OPEN, p, flags, 0o644))+}+/// openat(dirfd, path, flags): a relative path starts at directory `dirfd`.+pub fn openat(dirfd: i64, path: &str, flags: u64) -> i64 {+    with_cpath(path, |p| syscall5(SYS_OPENAT, dirfd as u64, p, flags, 0o644, 0)) } pub fn close(fd: i64) -> i64 {     syscall3(SYS_CLOSE, fd as u64, 0, 0) }+/// stat follows symlinks; lstat describes the link itself. pub fn stat(path: &str, st: &mut Stat) -> i64 {-    syscall3(SYS_STAT, path.as_ptr() as u64, path.len() as u64, st as *mut Stat as u64)+    with_cpath(path, |p| syscall3(SYS_STAT, p, st as *mut Stat as u64, 0))+}+pub fn lstat(path: &str, st: &mut Stat) -> i64 {+    with_cpath(path, |p| syscall3(SYS_LSTAT, p, st as *mut Stat as u64, 0)) } pub fn fstat(fd: i64, st: &mut Stat) -> i64 {     syscall3(SYS_FSTAT, fd as u64, st as *mut Stat as u64, 0) }+/// newfstatat(dirfd, path, st, flags): what musl's stat() really calls.+pub fn fstatat(dirfd: i64, path: &str, st: &mut Stat, flags: u64) -> i64 {+    with_cpath(path, |p| syscall5(SYS_NEWFSTATAT, dirfd as u64, p, st as *mut Stat as u64, flags, 0))+}+/// Is `fd` a terminal? (Like libc: does it answer the TCGETS ioctl?)+pub fn isatty(fd: i64) -> bool {+    tcgetattr(fd).is_ok()+} pub fn lseek(fd: i64, offset: i64, whence: u64) -> i64 {     syscall3(SYS_LSEEK, fd as u64, offset as u64, whence) }@@ -326,11 +422,9 @@ impl CStrings {  /// execve(path, argv, envp): only returns on failure (-errno). pub fn execve(path: &str, argv: &[&str], envp: &[&str]) -> i64 {-    let mut cpath = alloc::vec::Vec::from(path.as_bytes());-    cpath.push(0);     let mut args = CStrings::new(argv.iter().copied());     let mut env = CStrings::new(envp.iter().copied());-    syscall3(SYS_EXECVE, cpath.as_ptr() as u64, args.as_ptr() as u64, env.as_ptr() as u64)+    with_cpath(path, |p| syscall3(SYS_EXECVE, p, args.as_ptr() as u64, env.as_ptr() as u64)) }  /// The same environment I got: for passing on to execve.@@ -602,20 +696,29 @@ pub fn getcwd(buf: &mut [u8]) -> i64 {     if n > 0 { n - 1 } else { n } } pub fn chdir(path: &str) -> i64 {-    syscall3(SYS_CHDIR, path.as_ptr() as u64, path.len() as u64, 0)+    with_cpath(path, |p| syscall3(SYS_CHDIR, p, 0, 0)) }+/// mkdir(path, mode): 0o755 (the kernel ignores the mode for now). pub fn mkdir(path: &str) -> i64 {-    syscall3(SYS_MKDIR, path.as_ptr() as u64, path.len() as u64, 0)+    with_cpath(path, |p| syscall3(SYS_MKDIR, p, 0o755, 0)) } pub fn rmdir(path: &str) -> i64 {-    syscall3(SYS_RMDIR, path.as_ptr() as u64, path.len() as u64, 0)+    with_cpath(path, |p| syscall3(SYS_RMDIR, p, 0, 0)) }-/// The target of a symlink, into `buf`; returns its length or -errno.+/// The target of a symlink, into `buf` (no NUL added); its length or -errno. pub fn readlink(path: &str, buf: &mut [u8]) -> i64 {-    syscall5(SYS_READLINK, path.as_ptr() as u64, path.len() as u64, buf.as_mut_ptr() as u64, buf.len() as u64, 0)+    with_cpath(path, |p| syscall3(SYS_READLINK, p, buf.as_mut_ptr() as u64, buf.len() as u64)) } pub fn unlink(path: &str) -> i64 {-    syscall3(SYS_UNLINK, path.as_ptr() as u64, path.len() as u64, 0)+    with_cpath(path, |p| syscall3(SYS_UNLINK, p, 0, 0))+}+/// rename(old, new): replaces a file at `new`. -EXDEV: copy + delete instead.+pub fn rename(old: &str, new: &str) -> i64 {+    with_cpath(old, |o| with_cpath(new, |n| syscall3(SYS_RENAME, o, n, 0)))+}+/// access(path, mode): 0 if it exists (we don't check permissions yet).+pub fn access(path: &str) -> i64 {+    with_cpath(path, |p| syscall3(SYS_ACCESS, p, 0, 0)) } pub fn getdents64(fd: i64, buf: &mut [u8]) -> i64 {     syscall3(SYS_GETDENTS64, fd as u64, buf.as_mut_ptr() as u64, buf.len() as u64)@@ -623,14 +726,9 @@ pub fn getdents64(fd: i64, buf: &mut [u8]) -> i64 { /// Start program `path` with arguments `argv` (NUL-separated, e.g. /// "child\0hi"). Returns the child's pid (or -errno). pub fn spawn(path: &str, argv: &str, opts: &SpawnOptions) -> i64 {-    syscall5(-        SYS_SPAWN,-        path.as_ptr() as u64,-        path.len() as u64,-        argv.as_ptr() as u64,-        argv.len() as u64,-        opts as *const SpawnOptions as u64,-    )+    with_cpath(path, |p| {+        syscall5(SYS_SPAWN, p, argv.as_ptr() as u64, argv.len() as u64, opts as *const SpawnOptions as u64, 0)+    }) }  /// Walk the `linux_dirent64` records getdents64 put in `buf[..len]`,