RustForge

foundry

Foundry OS: the operating system built on the Rustbelt kernel

authorTyler Fowler <tyler@localhost>2026-10-30 11:08:48 -0700
commite8a1598981b9d4c220ff475716dda5dd7df785f3
parent618ed3f823d916066aae2a189d31e57ee4314adb
treebrowse files at this commit
WIP 15.1: Linux C-string path syscalls, struct stat, *at() calls, rename, pathtest
Mcp.rs+2 -2
Mdemo.rs+4 -4
Mfetch.rs+1 -1
Mhello.rs+1 -1
Mhttpd.rs+1 -1
Mls.rs+5 -5
Apathtest.rs+177 -0
Msh.rs+3 -4
Mtouch.rs+1 -1
Mulib.rs+127 -29

10 files changed, 322 insertions, 48 deletions

@@ -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]`,