rust-kernel
Rustbelt: a hobby x86_64 kernel written in Rust
| author | Tyler Fowler <tyler@localhost> | 2026-10-30 11:08:48 -0700 |
|---|---|---|
| commit | a8e8b7433b554ac9d840770e29a15e927a1b97ac | |
| parent | 7ad1388c3b1af493be4824bf58f1cf29159bd94e | |
| tree | browse files at this commit | |
WIP 15.1: Linux C-string path syscalls, struct stat, *at() calls, rename, pathtest
| M | build.rs | +1 -1 |
| M | src/errno.rs | +3 -2 |
| M | src/fs/file.rs | +84 -8 |
| M | src/fs/mod.rs | +66 -0 |
| M | src/syscall.rs | +306 -79 |
| M | tests/fat.rs | +1 -1 |
| M | tests/musl_prereq.rs | +8 -0 |
| M | tests/vfs.rs | +4 -4 |
| M | user/cp.rs | +2 -2 |
| M | user/demo.rs | +4 -4 |
| M | user/fetch.rs | +1 -1 |
| M | user/hello.rs | +1 -1 |
| M | user/httpd.rs | +1 -1 |
| M | user/ls.rs | +5 -5 |
| A | user/pathtest.rs | +177 -0 |
| M | user/sh.rs | +3 -4 |
| M | user/touch.rs | +1 -1 |
| M | user/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]`,