From ed1c177ab7100799e21df12ea1cab6c02e88b9f6 Mon Sep 17 00:00:00 2001 From: japabu Date: Wed, 21 Oct 2026 17:47:33 +0200 Subject: [PATCH 1/4] files: one client of the file servers, in toyos, for std and libc alike std kept the process's directory capabilities, its routing of a path to the kernel or a file server, its link following and its open served files in std's own fs module, so a C program reached none of them: every path under /home, /apps, /config, /state, /log and /boot answered ENOENT. toyos::files now keeps that state, in the copy of toyos that owns the descriptor table, behind toyos_fs_*. An open served file is a descriptor naming an object of toyos's own (tag SERVED_FILE), held in a registry its object word indexes, so read, write, seek, fstat, truncate, fsync and close dispatch on the descriptor whoever serves it. A capability's connection is shared by threads through a futex turnstile, which replaces std's copied Mutex and Condvar one (sdk/std/copies, sys/turns.rs go). std's fs is a thin client of it. libc's open, read, write, lseek, fstat, ftruncate, fsync, stat, lstat, unlink, rmdir, mkdir, rename, chdir, readlink, realpath, opendir and readdir route through it, with each refusal as POSIX's errno; symlink works where a file server holds the name, which refuses one that exists, and stays ENOSYS on the kernel's. stdout and stderr write a descriptor through it, so a served file at 1 or 2 is written as one. A corpus case writes, reads, links, resolves, renames, lists and removes files under /home. A posix_spawn addopen of a served path answers EOPNOTSUPP until a served file can be given to a child. realpath of a served path follows its last name's links, POSIX-relative to the link's directory; the server resolves the names before it. A refused fd: endowment is now said on stderr, once the table's lock is let go. rust: library/Cargo.lock names toyos's alloc edge (9d8bb4c347f). Co-Authored-By: Claude Opus 5.5 --- Cargo.lock | 11 +- Cargo.toml | 1 - ...ograms-name-no-file-a-file-server-holds.md | 19 - ...bc-refuses-what-toyos-cannot-yet-answer.md | 6 +- ...-small-interrupts-post-and-threads-wait.md | 9 +- rust | 2 +- sdk/libc/arch/src/path_resolution.rs | 44 +- sdk/libc/include/errno.h | 1 + sdk/libc/src/errcode.rs | 51 +- sdk/libc/src/posix_io.rs | 229 ++++--- sdk/libc/src/refused.rs | 7 - sdk/libc/src/resolve.rs | 26 +- sdk/libc/src/spawn.rs | 2 +- sdk/libc/src/stdio.rs | 38 +- sdk/std/copies/Cargo.toml | 14 - sdk/std/copies/src/lib.rs | 11 - sdk/std/sys/fs.rs | 596 +++--------------- sdk/std/sys/pal/mod.rs | 66 ++ sdk/std/sys/stdio.rs | 6 +- sdk/std/sys/turns.rs | 103 --- sdk/toyos/Cargo.toml | 3 +- sdk/toyos/src/fd.rs | 36 +- sdk/toyos/src/files.rs | 313 +++++++++ sdk/toyos/src/files/owner.rs | 454 +++++++++++++ sdk/toyos/src/fs.rs | 15 + sdk/toyos/src/lib.rs | 3 + sdk/toyos/src/log/stdio.rs | 12 +- sdk/toyos/src/sync.rs | 151 ++++- tests/corpus/tinycc/211_home_files.c | 89 +++ tests/corpus/tinycc/211_home_files.expect | 30 + 30 files changed, 1540 insertions(+), 808 deletions(-) delete mode 100644 issues/c-programs-name-no-file-a-file-server-holds.md delete mode 100644 sdk/std/copies/Cargo.toml delete mode 100644 sdk/std/copies/src/lib.rs delete mode 100644 sdk/std/sys/turns.rs create mode 100644 sdk/toyos/src/files.rs create mode 100644 sdk/toyos/src/files/owner.rs create mode 100644 tests/corpus/tinycc/211_home_files.c create mode 100644 tests/corpus/tinycc/211_home_files.expect diff --git a/Cargo.lock b/Cargo.lock index 0dfa500604b..4bf625552eb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3476,6 +3476,12 @@ version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" +[[package]] +name = "rustc-std-workspace-alloc" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d441c3b2ebf55cebf796bfdc265d67fa09db17b7bb6bd4be75c509e1e8fec3" + [[package]] name = "rustc-std-workspace-core" version = "1.0.1" @@ -4329,6 +4335,7 @@ name = "toyos" version = "0.18.0" dependencies = [ "loom", + "rustc-std-workspace-alloc", "rustc-std-workspace-core", "toyos-abi", ] @@ -4826,10 +4833,6 @@ dependencies = [ "toyos-update", ] -[[package]] -name = "toyos-std-copies" -version = "0.1.0" - [[package]] name = "toyos-supervisor" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index ba693aaadc5..64fd2798174 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -85,7 +85,6 @@ members = [ "sdk/icon", "sdk/keymap", "sdk/libc/arch", - "sdk/std/copies", "sdk/osrelease", "sdk/toyos", "sdk/window", diff --git a/issues/c-programs-name-no-file-a-file-server-holds.md b/issues/c-programs-name-no-file-a-file-server-holds.md deleted file mode 100644 index d715d235343..00000000000 --- a/issues/c-programs-name-no-file-a-file-server-holds.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-09-27 ---- - -# A C program names no file a file server holds - -`/apps`, `/config`, `/home`, `/state`, `/log` and `/boot` are served by -`/system/bin/fileserver` through directory capabilities in each program's namespace, -and std reaches them through `toyos::fs` (`sdk/std/sys/fs.rs`). -`sdk/libc` does not: `open`, `stat`, `opendir` and every other path call -in `sdk/libc/src/posix_io.rs` and `sdk/libc/src/stdio.rs` go to the -kernel's `SYS_OPEN` family, and the kernel serves ROOT and `/tmp` only. So a C -program — tinycc, doomgeneric, anything built with `toyos-cc` — is refused -every path under those six directories with `ENOENT`, and a file it writes -where a user's files live cannot be written at all. - -**Exit**: one file-server client crate in the SDK, the protocol's only implementation, which std uses; libc on top of it, with a C test that writes a file under `/home`, reads it back and lists it; then `/system` over the same protocol and the kernel's file syscalls deleted — the owner accepted the gap until then. diff --git a/issues/libc-refuses-what-toyos-cannot-yet-answer.md b/issues/libc-refuses-what-toyos-cannot-yet-answer.md index 934a963cdfd..a214f0aa3e8 100644 --- a/issues/libc-refuses-what-toyos-cannot-yet-answer.md +++ b/issues/libc-refuses-what-toyos-cannot-yet-answer.md @@ -11,8 +11,10 @@ These answer failure in their POSIX form and do nothing `tests/corpus/tinycc/206_libc_refusals.c`. Each waits on what it names: - `link`, `ENOSYS`: no call gives a file a second name. -- `symlink`, `ENOSYS`: `SYS_SYMLINK` displaces what its name held - (`issues/a-symbolic-link-on-tmp-displaces-its-name-and-lists-nowhere.md`). +- `symlink` of a link the kernel would hold, `ENOSYS`: `SYS_SYMLINK` + displaces what its name held + (`issues/a-symbolic-link-on-tmp-displaces-its-name-and-lists-nowhere.md`). A + file server makes one and refuses a name that exists, as POSIX has it. - `chmod` and `fchmod`, `ENOSYS`: a file has no mode bits to set. - `utimes`, `ENOSYS`: no call sets a file's times. - `statvfs` and `fstatvfs`, `ENOSYS`: no call answers a filesystem's size or diff --git a/issues/the-kernel-is-small-interrupts-post-and-threads-wait.md b/issues/the-kernel-is-small-interrupts-post-and-threads-wait.md index 7098b4a2b71..573a4c9a11a 100644 --- a/issues/the-kernel-is-small-interrupts-post-and-threads-wait.md +++ b/issues/the-kernel-is-small-interrupts-post-and-threads-wait.md @@ -16,8 +16,8 @@ kernel's part of a restart, and named module tests hold each program's ## What is left, in order -1. **Step 9's file half** (stage 4), once libc is a file server's client - (`issues/c-programs-name-no-file-a-file-server-holds.md`). +1. **Step 9's file half** (stage 4). libc is a file server's client + through `toyos::files`, as std is. 2. **Step 8's exit** (stage 4). Its empty view needs step 9's file half: until then the kernel serves `/system` and `/tmp` to every process. 3. **P1 of the Wi-Fi track** (`issues/there-is-no-wifi.md`): a BAR stays @@ -157,9 +157,8 @@ Beside that order, each waiting on something outside it: `fs_rename.rs`), the page cache (`file_cache.rs`) and the file syscalls. `/tmp` leaves the kernel per session, as the owner ruled ("Per session", 2026-10-03, in the isolation track), and `/system` is - read over the file protocol, which - `issues/c-programs-name-no-file-a-file-server-holds.md`'s exit puts - after libc is a file server's client. **Exit**: `tmpfs.rs`, + read over the file protocol, which std and libc both reach through + `toyos::files`. **Exit**: `tmpfs.rs`, `fs_rename.rs` and `file_cache.rs` are gone, `vfs.rs` resolves ROOT for exec and `SYS_DLOPEN` alone, and `sdk/abi` has no `SYS_OPEN` (9), `SYS_STAT` (16), `SYS_READDIR` (17), `SYS_DELETE` (18), `SYS_CHDIR` diff --git a/rust b/rust index a56b4057c58..9d8bb4c347f 160000 --- a/rust +++ b/rust @@ -1 +1 @@ -Subproject commit a56b4057c58e624aedc895643f4f5e83f32c1912 +Subproject commit 9d8bb4c347f442c296dd3e9e307c3b7ca1abcd38 diff --git a/sdk/libc/arch/src/path_resolution.rs b/sdk/libc/arch/src/path_resolution.rs index e67ed486172..6b7b4e19a87 100644 --- a/sdk/libc/arch/src/path_resolution.rs +++ b/sdk/libc/arch/src/path_resolution.rs @@ -9,7 +9,7 @@ use toyos_abi::syscall::{SyscallError, MAX_LINKS, MAX_PATH}; use crate::errcode::{EIO, ELOOP, ENAMETOOLONG, ENOENT, ENOTDIR}; use crate::header::{self, LIMITS_H}; -use crate::resolve::{resolve, Kernel}; +use crate::resolve::{resolve, resolve_served, Kernel}; #[derive(Clone, Copy)] enum Node { @@ -197,3 +197,45 @@ fn a_kernel_refusal_is_its_errno() { fn path_max_is_limits_h_s() { assert_eq!(MAX_PATH, header::int(LIMITS_H, "PATH_MAX") as usize); } + +/// [`resolve_served`] over `tree`: a link's target, or `None`, and `ENOENT` +/// for a name that is not there. +fn served(tree: &Tree, abs: &str) -> Result { + let read_link = |at: &[u8]| match tree.at(at) { + Some(Node::Link(target)) => Ok(Some(target.as_bytes().to_vec())), + Some(_) => Ok(None), + None => Err(ENOENT), + }; + let exists = |at: &[u8]| tree.at(at).map(drop).ok_or(ENOENT); + resolve_served(abs.as_bytes(), read_link, exists).map(|at| String::from_utf8(at).expect("UTF-8")) +} + +/// A served path's last link is read against the link's own directory, as +/// POSIX has it, and not against a mount as the kernel's is; an absolute one +/// from the root. +#[test] +fn a_served_link_is_read_against_its_own_directory() { + let tree = Tree::new(&[ + ("/home/u/a", Node::Dir), + ("/home/u/a/file", Node::File), + ("/home/u/a/rel", Node::Link("file")), + ("/home/u/up", Node::Link("a/../a/rel")), + ("/home/u/abs", Node::Link("/home/u/a/file")), + ]); + assert_eq!(served(&tree, "/home/u/a/rel"), Ok(String::from("/home/u/a/file"))); + assert_eq!(served(&tree, "/home/u/up"), Ok(String::from("/home/u/a/file")), "a chain of two"); + assert_eq!(served(&tree, "/home/u/abs"), Ok(String::from("/home/u/a/file"))); + assert_eq!(served(&tree, "/home/u/./a//file"), Ok(String::from("/home/u/a/file")), "lexically clean"); +} + +#[test] +fn a_served_loop_is_eloop_and_a_dangling_link_enoent() { + let tree = Tree::new(&[ + ("/home/u", Node::Dir), + ("/home/u/x", Node::Link("y")), + ("/home/u/y", Node::Link("x")), + ("/home/u/dangling", Node::Link("gone")), + ]); + assert_eq!(served(&tree, "/home/u/x"), Err(ELOOP)); + assert_eq!(served(&tree, "/home/u/dangling"), Err(ENOENT)); +} diff --git a/sdk/libc/include/errno.h b/sdk/libc/include/errno.h index 6662185d19f..227096367e6 100644 --- a/sdk/libc/include/errno.h +++ b/sdk/libc/include/errno.h @@ -83,6 +83,7 @@ int *__errno_location(void); #define ETIMEDOUT 110 #define ECONNREFUSED 111 #define EHOSTUNREACH 113 +#define ESTALE 116 #define ECANCELED 125 #define EOWNERDEAD 130 #define ENOTRECOVERABLE 131 diff --git a/sdk/libc/src/errcode.rs b/sdk/libc/src/errcode.rs index 4b5215b031d..429c0b1dd01 100644 --- a/sdk/libc/src/errcode.rs +++ b/sdk/libc/src/errcode.rs @@ -1,5 +1,5 @@ //! libc's errno codes, `include/errno.h`'s by name and value, and the one -//! reading of a kernel refusal as one. Nothing here reads or sets `errno` +//! reading of a kernel's or a file server's refusal as one. Nothing here reads or sets `errno` //! (`errno.rs` does), so the host tests it (`toyos-libc-copies`). use toyos_abi::syscall::SyscallError; @@ -17,18 +17,24 @@ pub(crate) const EACCES: i32 = 13; pub(crate) const EFAULT: i32 = 14; pub(crate) const EBUSY: i32 = 16; pub(crate) const EEXIST: i32 = 17; +pub(crate) const EXDEV: i32 = 18; pub(crate) const ENODEV: i32 = 19; pub(crate) const ENOTDIR: i32 = 20; +pub(crate) const EISDIR: i32 = 21; pub(crate) const EINVAL: i32 = 22; +pub(crate) const ENFILE: i32 = 23; pub(crate) const EMFILE: i32 = 24; pub(crate) const ENOTTY: i32 = 25; +pub(crate) const EFBIG: i32 = 27; pub(crate) const ENOSPC: i32 = 28; +pub(crate) const EROFS: i32 = 30; pub(crate) const EPIPE: i32 = 32; pub(crate) const EDOM: i32 = 33; pub(crate) const ERANGE: i32 = 34; pub(crate) const EDEADLK: i32 = 35; pub(crate) const ENAMETOOLONG: i32 = 36; pub(crate) const ENOSYS: i32 = 38; +pub(crate) const ENOTEMPTY: i32 = 39; pub(crate) const ELOOP: i32 = 40; pub(crate) const EOVERFLOW: i32 = 75; pub(crate) const EILSEQ: i32 = 84; @@ -43,6 +49,7 @@ pub(crate) const ENOTCONN: i32 = 107; pub(crate) const ETIMEDOUT: i32 = 110; pub(crate) const ECONNREFUSED: i32 = 111; pub(crate) const EHOSTUNREACH: i32 = 113; +pub(crate) const ESTALE: i32 = 116; /// The errno a kernel refusal is answered with. pub(crate) fn of(e: SyscallError) -> i32 { @@ -60,3 +67,45 @@ pub(crate) fn of(e: SyscallError) -> i32 { _ => EINVAL, } } + +/// The errno a refusal of `toyos::files` is answered with. +pub(crate) fn of_files(e: toyos::files::Error) -> i32 { + use toyos::files::Error; + match e { + Error::Kernel(e) => of(e), + Error::Served(e) => of_served(e), + Error::Fd(toyos::fd::Error::BadFd) | Error::NotAFile => EBADF, + Error::Fd(toyos::fd::Error::Full) => EMFILE, + Error::NotUtf8 => EILSEQ, + Error::Loop => ELOOP, + Error::CrossesServers => EXDEV, + } +} + +/// The errno a file server's refusal, or its client end's, is answered with. +fn of_served(e: toyos::fs::Error) -> i32 { + use toyos::fs::Error; + match e { + Error::NotFound | Error::NotHeld => ENOENT, + Error::AlreadyExists => EEXIST, + Error::NotADirectory | Error::NoDirectory => ENOTDIR, + Error::IsADirectory => EISDIR, + Error::NotEmpty => ENOTEMPTY, + Error::NotALink | Error::IntoItself | Error::UnknownFile | Error::InvalidPath | Error::InvalidRequest => EINVAL, + Error::NameTooLong | Error::InvalidName => ENAMETOOLONG, + Error::ReadOnly => EROFS, + Error::NotOpenForWriting => EBADF, + Error::Root => EBUSY, + Error::Escapes => EACCES, + Error::CrossesDirectories => EXDEV, + Error::Loop => ELOOP, + Error::NoSpace => ENOSPC, + Error::FileTooLarge => EFBIG, + Error::LimitExceeded | Error::TooManyConnections | Error::TooManyStreams => EAGAIN, + Error::TooManyOpen => ENFILE, + Error::Io | Error::Malformed => EIO, + Error::Stale | Error::Gone => ESTALE, + Error::Unsupported => EOPNOTSUPP, + Error::OutOfResources => ENOMEM, + } +} diff --git a/sdk/libc/src/posix_io.rs b/sdk/libc/src/posix_io.rs index a644f23759c..60d36c3a468 100644 --- a/sdk/libc/src/posix_io.rs +++ b/sdk/libc/src/posix_io.rs @@ -1,4 +1,6 @@ -// POSIX file I/O — thin wrappers around toyos-abi syscalls. +// POSIX file I/O: a path or a descriptor is `toyos::files`', which routes it to +// the kernel or to the file server behind one of the process's directory +// capabilities. #![allow(non_camel_case_types)] @@ -9,6 +11,8 @@ use core::ptr; use toyos_abi::{RawHandle, Rights}; use toyos_abi::syscall::{self, OpenFlags, SeekFrom}; +use toyos::files::{self, Open}; + use crate::errcode::{EBADF, EINVAL}; use crate::time::Timespec; @@ -18,6 +22,7 @@ pub(crate) const O_RDONLY: i32 = 0; pub(crate) const O_WRONLY: i32 = 1; pub(crate) const O_RDWR: i32 = 2; pub(crate) const O_CREAT: i32 = 0x40; +pub(crate) const O_EXCL: i32 = 0x80; pub(crate) const O_TRUNC: i32 = 0x200; pub(crate) const O_APPEND: i32 = 0x400; pub(crate) const O_CLOEXEC: i32 = 0x80000; @@ -81,12 +86,17 @@ pub fn write_fd(raw: i32, buf: &[u8]) -> Result { // A closed stdout is `EBADF`'s, where the stream sink drops its bytes. toyos::fd::inheritable(raw).map_err(fd_errno)?; match raw { - 1 => stdio::write(Stream::Out, buf).map_err(crate::errcode::of), - 2 => stdio::write(Stream::Err, buf).map_err(crate::errcode::of), - other => on_fd(other, |h| syscall::write(h, buf)), + 1 => stdio::write(Stream::Out, buf).map_err(crate::errcode::of_files), + 2 => stdio::write(Stream::Err, buf).map_err(crate::errcode::of_files), + other => files::write(other, buf).map_err(crate::errcode::of_files), } } +/// `-1`, with `errno` set for `toyos::files`' refusal `e`. +pub(crate) fn refuse_files(e: files::Error) -> i32 { + refuse(crate::errcode::of_files(e)) +} + pub fn c_str_to_bytes(s: *const u8) -> &'static [u8] { unsafe { let len = super::string::strlen(s); @@ -97,21 +107,43 @@ pub fn c_str_to_bytes(s: *const u8) -> &'static [u8] { // File descriptor operations #[no_mangle] -pub unsafe extern "C" fn open(path: *const u8, flags: i32, mode: u32) -> i32 { - match open_handle(c_str_to_bytes(path), flags, mode) { - Ok(handle) => install(handle, flags & O_CLOEXEC != 0).unwrap_or_else(refuse), - Err(e) => set_errno(e), +pub unsafe extern "C" fn open(path: *const u8, flags: i32, _mode: u32) -> i32 { + let path = c_str_to_bytes(path); + let cloexec = flags & O_CLOEXEC != 0; + let (readable, writable) = read_write(flags); + if path == DEV_NULL { + return match syscall::null(null_rights(readable, writable)) { + Ok(handle) => install(handle, cloexec).unwrap_or_else(refuse), + Err(e) => set_errno(e), + }; } + let how = Open { + read: readable, + write: writable, + append: flags & O_APPEND != 0, + create: flags & O_CREAT != 0 && flags & O_EXCL == 0, + create_new: flags & O_CREAT != 0 && flags & O_EXCL != 0, + truncate: flags & O_TRUNC != 0, + cloexec, + }; + files::open(path, how).unwrap_or_else(refuse_files) } -/// `open`'s file, as a handle of this process's: what a spawn's `addopen` -/// opens too. -pub(crate) fn open_handle(path: &[u8], flags: i32, _mode: u32) -> Result { +/// Whether `open`'s `flags` ask to read and to write. +fn read_write(flags: i32) -> (bool, bool) { let access = flags & 3; - let readable = access == O_RDONLY || access == O_RDWR; - let writable = access == O_WRONLY || access == O_RDWR; + (access == O_RDONLY || access == O_RDWR, access == O_WRONLY || access == O_RDWR) +} + +/// A spawn's `addopen` file, as a handle of this process's for the child. +/// A file a file server holds is refused: no kernel handle reaches it. +pub(crate) fn open_handle(path: &[u8], flags: i32, _mode: u32) -> Result { + let (readable, writable) = read_write(flags); if path == DEV_NULL { - return syscall::null(null_rights(readable, writable)); + return syscall::null(null_rights(readable, writable)).map_err(crate::errcode::of); + } + if files::is_served(path) { + return Err(crate::errcode::EOPNOTSUPP); } let mut oflags = OpenFlags(0); @@ -120,7 +152,7 @@ pub(crate) fn open_handle(path: &[u8], flags: i32, _mode: u32) -> Result i32 { pub unsafe extern "C" fn read(raw_fd: i32, buf: *mut u8, count: usize) -> isize { if buf.is_null() || count == 0 { return 0; } let slice = core::slice::from_raw_parts_mut(buf, count); - match on_fd(raw_fd, |h| syscall::read(h, slice)) { + match files::read(raw_fd, slice) { Ok(n) => n as isize, - Err(e) => refuse(e) as isize, + Err(e) => refuse_files(e) as isize, } } @@ -177,25 +209,22 @@ pub unsafe extern "C" fn lseek(raw_fd: i32, offset: i64, whence: i32) -> i64 { SEEK_END => SeekFrom::End(offset), _ => { crate::errno::set(EINVAL); return -1; } }; - match on_fd(raw_fd, |h| syscall::seek(h, pos)) { + match files::seek(raw_fd, pos) { Ok(n) => n as i64, - Err(e) => refuse(e) as i64, + Err(e) => refuse_files(e) as i64, } } #[no_mangle] pub unsafe extern "C" fn fstat(raw_fd: i32, buf: *mut Stat) -> i32 { - match on_fd(raw_fd, syscall::fstat) { - Ok(st) => unsafe { answered(Ok(st), buf) }, - Err(e) => refuse(e), - } + unsafe { answered(files::fstat(raw_fd), buf) } } -/// `buf` filled from the kernel's `answer`, or `errno` from its refusal. -unsafe fn answered(answer: Result, buf: *mut Stat) -> i32 { +/// `buf` filled from `answer`, or `errno` from its refusal. +unsafe fn answered(answer: Result, buf: *mut Stat) -> i32 { let st = match answer { Ok(st) => st, - Err(e) => return set_errno(e), + Err(e) => return refuse_files(e), }; if !buf.is_null() { // SAFETY: C's caller hands one `struct stat` at `buf`. @@ -224,10 +253,8 @@ unsafe fn answered(answer: Result, buf: *m #[no_mangle] pub unsafe extern "C" fn ftruncate(raw_fd: i32, length: i64) -> i32 { - match on_fd(raw_fd, |h| syscall::ftruncate(h, length as u64)) { - Ok(()) => 0, - Err(e) => refuse(e), - } + let Ok(length) = u64::try_from(length) else { return refuse(EINVAL) }; + files::truncate(raw_fd, length).map_or_else(refuse_files, |()| 0) } /// [`ftruncate`] of `path` opened for writing. @@ -260,10 +287,7 @@ pub unsafe extern "C" fn pathconf(_path: *const u8, name: i32) -> i64 { #[no_mangle] pub unsafe extern "C" fn fsync(raw_fd: i32) -> i32 { - match on_fd(raw_fd, syscall::fsync) { - Ok(()) => 0, - Err(e) => refuse(e), - } + files::fsync(raw_fd).map_or_else(refuse_files, |()| 0) } /// POSIX's `dup`: the lowest free descriptor on the same object, not @@ -320,18 +344,12 @@ pub unsafe extern "C" fn pipe(pipefd: *mut i32) -> i32 { #[no_mangle] pub unsafe extern "C" fn unlink(path: *const u8) -> i32 { - match syscall::delete(c_str_to_bytes(path)) { - Ok(()) => 0, - Err(e) => set_errno(e), - } + files::unlink(c_str_to_bytes(path)).map_or_else(refuse_files, |()| 0) } #[no_mangle] pub unsafe extern "C" fn rmdir(path: *const u8) -> i32 { - match syscall::rmdir(c_str_to_bytes(path)) { - Ok(()) => 0, - Err(e) => set_errno(e), - } + files::rmdir(c_str_to_bytes(path)).map_or_else(refuse_files, |()| 0) } #[no_mangle] @@ -348,44 +366,57 @@ pub unsafe extern "C" fn getcwd(buf: *mut u8, size: usize) -> *mut u8 { #[no_mangle] pub unsafe extern "C" fn chdir(path: *const u8) -> i32 { - match syscall::chdir(c_str_to_bytes(path)) { - Ok(()) => 0, - Err(e) => set_errno(e), - } + files::chdir(c_str_to_bytes(path)).map_or_else(refuse_files, |()| 0) } #[no_mangle] pub unsafe extern "C" fn stat(path: *const u8, buf: *mut Stat) -> i32 { - unsafe { answered(syscall::stat(c_str_to_bytes(path), true), buf) } + unsafe { answered(files::stat(c_str_to_bytes(path), true), buf) } } /// `stat` that does not follow a final symbolic link. #[no_mangle] pub unsafe extern "C" fn lstat(path: *const u8, buf: *mut Stat) -> i32 { - unsafe { answered(syscall::stat(c_str_to_bytes(path), false), buf) } + unsafe { answered(files::stat(c_str_to_bytes(path), false), buf) } } /// `SYS_READLINK` answers `NotFound` for a path that is no link as for one -/// that names nothing, and POSIX tells them apart: `EINVAL` for the first. +/// that names nothing, and POSIX tells them apart: `EINVAL` for the first. A +/// file server tells them apart itself. #[no_mangle] pub unsafe extern "C" fn readlink(path: *const u8, buf: *mut u8, size: usize) -> isize { let Some(len) = crate::linkreq::target_len(size) else { crate::errno::set(EINVAL); return -1; }; - // SAFETY: C's caller hands `size` bytes at `buf`. - let target = unsafe { core::slice::from_raw_parts_mut(buf, len) }; - match syscall::readlink(c_str_to_bytes(path), target) { - Ok(n) => n as isize, - Err(syscall::SyscallError::NotFound) => { + match files::read_link(c_str_to_bytes(path)) { + Ok(target) => { + let n = target.len().min(len); + // SAFETY: C's caller hands `size` bytes at `buf`, and `len` is at most it. + unsafe { ptr::copy_nonoverlapping(target.as_ptr(), buf, n) }; + n as isize + } + Err(files::Error::Kernel(syscall::SyscallError::NotFound)) => { // `stat` sets `errno` for a path that names nothing. if unsafe { stat(path, ptr::null_mut()) } == 0 { crate::errno::set(EINVAL); } -1 } - Err(e) => set_errno(e) as isize, + Err(e) => refuse_files(e) as isize, + } +} + +/// A file server refuses a name that exists, as POSIX has it. The kernel's +/// `SYS_SYMLINK` displaces whatever holds the name, and a question asked first +/// races its answer, so a link the kernel would hold is refused `ENOSYS`. +#[no_mangle] +pub unsafe extern "C" fn symlink(target: *const u8, link: *const u8) -> i32 { + let link = c_str_to_bytes(link); + if !files::is_served(link) { + return refuse(crate::errcode::ENOSYS); } + files::symlink(c_str_to_bytes(target), link).map_or_else(refuse_files, |()| 0) } struct TheKernel; @@ -404,9 +435,9 @@ impl crate::resolve::Kernel for TheKernel { } } -/// POSIX's `realpath`, by the kernel's rules of resolution (`resolve`): the -/// file `open` of `path` reaches, in a block of `malloc`'s when `resolved` is -/// null. +/// POSIX's `realpath`, by the kernel's rules of resolution (`resolve`) or a +/// file server's: the file `open` of `path` reaches, in a block of `malloc`'s +/// when `resolved` is null. #[no_mangle] pub unsafe extern "C" fn realpath(path: *const u8, resolved: *mut u8) -> *mut u8 { use syscall::MAX_PATH; @@ -417,7 +448,21 @@ pub unsafe extern "C" fn realpath(path: *const u8, resolved: *mut u8) -> *mut u8 let mut cwd = [0u8; MAX_PATH]; let n = syscall::getcwd(&mut cwd); assert!(n != 0 && n <= MAX_PATH, "SYS_GETCWD: a working directory past MAX_PATH"); - let at = match crate::resolve::resolve(&cwd[..n], c_str_to_bytes(path), &mut TheKernel) { + let path = c_str_to_bytes(path); + let resolved_at = match core::str::from_utf8(&cwd[..n]).ok().zip(core::str::from_utf8(path).ok()) { + Some((cwd, text)) if files::is_served(path) => { + let abs = files::absolute(cwd, text); + let read_link = |at: &[u8]| match files::read_link(at) { + Ok(target) => Ok(Some(target)), + Err(files::Error::Served(toyos::fs::Error::NotALink)) => Ok(None), + Err(e) => Err(crate::errcode::of_files(e)), + }; + let exists = |at: &[u8]| files::stat(at, true).map(drop).map_err(crate::errcode::of_files); + crate::resolve::resolve_served(abs.as_bytes(), read_link, exists) + } + _ => crate::resolve::resolve(&cwd[..n], path, &mut TheKernel), + }; + let at = match resolved_at { Ok(at) => at, Err(code) => { crate::errno::set(code); @@ -444,10 +489,11 @@ pub unsafe extern "C" fn access(path: *const u8, _mode: i32) -> i32 { #[no_mangle] pub unsafe extern "C" fn isatty(raw_fd: i32) -> i32 { - match on_fd(raw_fd, syscall::fstat) { - Ok(st) => (st.file_type == syscall::FileType::Tty) as i32, - Err(e) => { - crate::errno::set(if e == EBADF { EBADF } else { crate::errcode::ENOTTY }); + match files::fstat(raw_fd) { + Ok(st) if st.file_type == syscall::FileType::Tty => 1, + answer => { + let bad = matches!(answer, Err(files::Error::Fd(toyos::fd::Error::BadFd))); + crate::errno::set(if bad { EBADF } else { crate::errcode::ENOTTY }); 0 } } @@ -464,7 +510,7 @@ pub unsafe extern "C" fn umask(mask: u32) -> u32 { /// Whether `path` names a file, and not a directory. pub(crate) fn is_file(path: &[u8]) -> bool { - syscall::stat(path, true).is_ok_and(|st| st.file_type == syscall::FileType::File) + files::stat(path, true).is_ok_and(|st| st.file_type == syscall::FileType::File) } /// `arg` is the register C's variadic third argument arrives in (`fdreq`). @@ -485,10 +531,17 @@ pub unsafe extern "C" fn fcntl(raw_fd: i32, cmd: i32, arg: u64) -> i32 { /// An open directory: its whole listing, and where the next `readdir` starts. pub struct DIR { - listing: Vec, + listing: Listing, at: usize, } +/// A listing as its server answered it: the kernel's, or a file server's +/// entries, `d_type` and name. +enum Listing { + Kernel(Vec), + Served(Vec<(u8, Box<[u8]>)>), +} + #[repr(C)] pub struct dirent { pub d_ino: u64, @@ -499,25 +552,59 @@ pub struct dirent { /// `d_type`s: the listing tells a directory from everything else, and no more. const DT_UNKNOWN: u8 = 0; const DT_DIR: u8 = 4; +const DT_REG: u8 = 8; +const DT_LNK: u8 = 10; #[no_mangle] pub unsafe extern "C" fn opendir(path: *const u8) -> *mut DIR { - let path_bytes = c_str_to_bytes(path); - match crate::listing::whole(|buf| syscall::readdir(path_bytes, buf)) { + let path = c_str_to_bytes(path); + let listing = match files::is_served(path) { + false => crate::listing::whole(|buf| syscall::readdir(path, buf)).map(Listing::Kernel).map_err(crate::errcode::of), + true => served_listing(path), + }; + match listing { Ok(listing) => Box::into_raw(Box::new(DIR { listing, at: 0 })), Err(e) => { - set_errno(e); + crate::errno::set(e); ptr::null_mut() } } } +/// A file server's listing of `path`, each entry's `d_type` beside its name. +fn served_listing(path: &[u8]) -> Result { + let listing = files::read_dir(path).map_err(crate::errcode::of_files)?; + let mut entries = Vec::new(); + toyos::fs::for_each_entry(&listing, |kind, _, name| { + let d_type = match kind { + toyos::fs::KIND_DIR => DT_DIR, + toyos::fs::KIND_FILE => DT_REG, + toyos::fs::KIND_SYMLINK => DT_LNK, + _ => DT_UNKNOWN, + }; + entries.push((d_type, Box::from(name.as_bytes()))); + }) + .map_err(|e| crate::errcode::of_files(files::Error::Served(e)))?; + Ok(Listing::Served(entries)) +} + #[no_mangle] pub unsafe extern "C" fn readdir(dir: *mut DIR) -> *mut dirent { if dir.is_null() { return ptr::null_mut(); } let d = &mut *dir; - let Some(entry) = syscall::dirent(&d.listing, &mut d.at) else { return ptr::null_mut() }; - let Some(d_name) = crate::listing::d_name(entry.name) else { + let (d_ino, d_type, name) = match &d.listing { + Listing::Kernel(listing) => { + let Some(entry) = syscall::dirent(listing, &mut d.at) else { return ptr::null_mut() }; + (entry.node, if entry.is_dir { DT_DIR } else { DT_UNKNOWN }, entry.name) + } + Listing::Served(entries) => { + let Some((d_type, name)) = entries.get(d.at) else { return ptr::null_mut() }; + d.at += 1; + // A file server answers no node in a listing. + (0, *d_type, &name[..]) + } + }; + let Some(d_name) = crate::listing::d_name(name) else { crate::errno::set(crate::errcode::EOVERFLOW); return ptr::null_mut(); }; @@ -525,7 +612,7 @@ pub unsafe extern "C" fn readdir(dir: *mut DIR) -> *mut dirent { // Use a static buffer for the dirent (not thread-safe, matching POSIX convention) static mut DIRENT_BUF: dirent = dirent { d_ino: 0, d_type: 0, d_name: [0; crate::listing::D_NAME] }; let ent = &raw mut DIRENT_BUF; - ent.write(dirent { d_ino: entry.node, d_type: if entry.is_dir { DT_DIR } else { DT_UNKNOWN }, d_name }); + ent.write(dirent { d_ino, d_type, d_name }); ent } diff --git a/sdk/libc/src/refused.rs b/sdk/libc/src/refused.rs index 6ce01eb58a9..50571ed05bb 100644 --- a/sdk/libc/src/refused.rs +++ b/sdk/libc/src/refused.rs @@ -47,13 +47,6 @@ pub unsafe extern "C" fn link(_existing: *const u8, _new: *const u8) -> i32 { refuse() } -/// `SYS_SYMLINK` displaces whatever holds the name, where POSIX refuses one -/// that exists, and a question asked first races its answer. -#[no_mangle] -pub unsafe extern "C" fn symlink(_target: *const u8, _link: *const u8) -> i32 { - refuse() -} - /// A file has no owner. #[no_mangle] pub extern "C" fn fchown(_fd: i32, _owner: u32, _group: u32) -> i32 { diff --git a/sdk/libc/src/resolve.rs b/sdk/libc/src/resolve.rs index 782d17a5515..6774f14ac83 100644 --- a/sdk/libc/src/resolve.rs +++ b/sdk/libc/src/resolve.rs @@ -4,7 +4,9 @@ //! asks the kernel only through [`Kernel`], so the host tests it //! (`toyos-libc-copies`). //! -//! The rules are not POSIX's (`issues/the-kernel-resolves-a-path-by-rules-of-its-own.md`): +//! A path a file server holds is resolved by [`resolve_served`] instead. +//! +//! The kernel's rules are not POSIX's (`issues/the-kernel-resolves-a-path-by-rules-of-its-own.md`): //! `..` is read off the text before any name is looked up, only the last //! name's link is followed, at most [`MAX_LINKS`] of them, and a relative link //! is read against the mount it is on, the first name of its path. The kernel @@ -121,3 +123,25 @@ fn absent(at: &[u8], kernel: &mut impl Kernel) -> Result { } Ok(ENOENT) } + +/// `abs`, a path a file server holds, its last name's links followed as +/// POSIX reads a relative one, against the link's own directory, or the errno +/// it is refused with. `read_link` answers a link's target, `None` for a name +/// that is no link; `exists` refuses a path that names nothing. The server +/// resolves the names before the last itself, and they stay as written. +pub(crate) fn resolve_served( + abs: &[u8], + mut read_link: impl FnMut(&[u8]) -> Result>, i32>, + mut exists: impl FnMut(&[u8]) -> Result<(), i32>, +) -> Result, i32> { + let mut at = joined(b"/", abs)?; + for _ in 0..=MAX_LINKS { + let Some(target) = read_link(&at)? else { + exists(&at)?; + return Ok(at); + }; + let dir = at.rsplitn(2, |&b| b == b'/').nth(1).filter(|d| !d.is_empty()).unwrap_or(b"/"); + at = joined(dir, &target)?; + } + Err(ELOOP) +} diff --git a/sdk/libc/src/spawn.rs b/sdk/libc/src/spawn.rs index 36bdca02f14..296af6c5f65 100644 --- a/sdk/libc/src/spawn.rs +++ b/sdk/libc/src/spawn.rs @@ -284,7 +284,7 @@ fn spawn_counted(path: &str, argv: &[u8], env: &[u8], actions: &[Action isize { - match on_fd(fd, |h| syscall::read(h, buf)) { + match files::read(fd, buf) { Ok(n) => n as isize, Err(_) => -1, } @@ -28,24 +27,17 @@ fn sys_seek(fd: i32, offset: i64, whence: i32) -> i64 { 2 => SeekFrom::End(offset), _ => return -1, }; - match on_fd(fd, |h| syscall::seek(h, pos)) { + match files::seek(fd, pos) { Ok(n) => n as i64, Err(_) => -1, } } -fn sys_fsync(fd: i32) { let _ = on_fd(fd, syscall::fsync); } - -fn sys_delete(path: &[u8]) -> i32 { - match syscall::delete(path) { Ok(()) => 0, Err(_) => -1 } -} - -fn sys_rename(old: &[u8], new: &[u8]) -> i32 { - match syscall::rename(old, new) { Ok(()) => 0, Err(_) => -1 } -} +fn sys_fsync(fd: i32) { let _ = files::fsync(fd); } -fn sys_mkdir(path: &[u8]) -> i32 { - match syscall::mkdir(path) { Ok(()) => 0, Err(_) => -1 } +/// `0`, or `-1` with `errno` set for the refusal. +fn answered(done: Result<(), files::Error>) -> i32 { + done.map_or_else(super::posix_io::refuse_files, |()| 0) } // FILE struct @@ -165,11 +157,9 @@ unsafe fn unregister_stream(f: *mut FILE) { /// failure mode that is hardest to tell apart from the program never having run. /// C's rule is "interactive device", not "terminal", and a serial console is one. unsafe fn fd_is_interactive(fd: i32) -> bool { - match on_fd(fd, syscall::fstat) { - Ok(st) => matches!( - st.file_type, - syscall::FileType::Tty | syscall::FileType::Serial - ), + use toyos_abi::syscall::FileType; + match files::fstat(fd) { + Ok(st) => matches!(st.file_type, FileType::Tty | FileType::Serial), Err(_) => false, } } @@ -626,12 +616,12 @@ pub(crate) unsafe fn unget(f: *mut FILE, bytes: &[u8]) -> bool { #[no_mangle] pub unsafe extern "C" fn remove(path: *const u8) -> i32 { - sys_delete(unsafe { c_str_bytes(path) }) + answered(files::unlink(unsafe { c_str_bytes(path) })) } #[no_mangle] pub unsafe extern "C" fn rename(old: *const u8, new: *const u8) -> i32 { - sys_rename(unsafe { c_str_bytes(old) }, unsafe { c_str_bytes(new) }) + answered(files::rename(unsafe { c_str_bytes(old) }, unsafe { c_str_bytes(new) })) } #[no_mangle] @@ -655,7 +645,7 @@ pub unsafe extern "C" fn atof(s: *const u8) -> f64 { #[no_mangle] pub unsafe extern "C" fn mkdir(path: *const u8, _mode: u32) -> i32 { - sys_mkdir(unsafe { c_str_bytes(path) }) + answered(files::mkdir(unsafe { c_str_bytes(path) })) } #[no_mangle] diff --git a/sdk/std/copies/Cargo.toml b/sdk/std/copies/Cargo.toml deleted file mode 100644 index d5858cc6eef..00000000000 --- a/sdk/std/copies/Cargo.toml +++ /dev/null @@ -1,14 +0,0 @@ -[package] -name = "toyos-std-copies" -description = "A host test of std's ToyOS platform: its modules that name nothing but core and std's own sync, compiled as they are." -version = "0.1.0" -edition = "2024" -license = "MIT OR Apache-2.0" -publish = false - -# A package of its own because `sdk/std` is std's, built into the sysroot by -# a `#[path]` out of the fork: this compiles only the files that name nothing -# std alone has, the way `toyos-libc-copies` compiles libc's. - -[lib] -doctest = false diff --git a/sdk/std/copies/src/lib.rs b/sdk/std/copies/src/lib.rs deleted file mode 100644 index dcaf0ebea49..00000000000 --- a/sdk/std/copies/src/lib.rs +++ /dev/null @@ -1,11 +0,0 @@ -//! std's ToyOS modules that name nothing but `core` and std's own `sync` and -//! `ops`, on the host: the turnstile threads sharing one directory's -//! connection are served through in turn. - -// What the modules below name as std's own, which a host's std is. -#[cfg(test)] -use std::{ops, sync}; - -#[cfg(test)] -#[path = "../../sys/turns.rs"] -mod turns; diff --git a/sdk/std/sys/fs.rs b/sdk/std/sys/fs.rs index 2a11846ab5d..ea2d6e76381 100644 --- a/sdk/std/sys/fs.rs +++ b/sdk/std/sys/fs.rs @@ -1,71 +1,23 @@ -//! Files on ToyOS. -//! -//! A path names a file on one of two kinds of server. The kernel serves -//! `/system` (the signed image) and `/tmp`; every other directory a process -//! may reach is a capability in its namespace, `fs:`, served by a -//! file server process (`toyos::fs`). A path is made absolute against the -//! working directory, normalized, and sent to the capability with the longest -//! matching prefix, relative to it; a path under no capability the process -//! holds is the kernel's. -//! -//! A file server that restarts is survived: its capability is connected -//! again. - -use toyos_abi::syscall::{self, OpenFlags}; - -use crate::collections::BTreeMap; +//! Files on ToyOS: a path is served by the kernel (`/system`, `/tmp`) or by a +//! file server behind one of the process's directory capabilities, and +//! `toyos::files` routes it, follows its links and keeps what it opened. A +//! `File` is one descriptor of the process's table either way. + +use toyos::files::{self, Open}; +use toyos_abi::syscall::{self, SeekFrom as AbiSeek}; + use crate::ffi::OsString; use crate::fmt; use crate::fs::TryLockError; use crate::hash::Hash; use crate::io::{self, BorrowedCursor, IoSlice, IoSliceMut, SeekFrom}; use crate::path::{Path, PathBuf}; -use crate::sync::{Arc, Mutex}; pub use crate::sys::fs::common::Dir; use crate::sys::time::SystemTime; -use crate::sys::to_io_error; - -/// The deepest directory a capability names, in components. -const MAX_CAPABILITY_DEPTH: usize = 4; +use crate::sys::{fd_error, files_error, served_error, to_io_error}; -/// The most absolute symlinks one path resolution follows. -const MAX_LINKS: usize = 40; - -pub struct File(Inner); - -enum Inner { - /// A descriptor naming a file the kernel serves. - Kernel(i32), - Served(Arc), -} - -/// A file open on a file server, which keeps its offset. -struct Served { - dir: Arc, - rel: String, - fid: u64, - generation: u64, -} - -#[path = "turns.rs"] -mod turns; - -/// One directory capability this process holds, connected: threads sharing it -/// are served in turn. -struct Capability { - prefix: String, - dir: turns::Turnstile, -} - -impl Capability { - fn new(prefix: &str, dir: toyos::fs::Dir) -> Self { - Self { prefix: String::from(prefix), dir: turns::Turnstile::new(dir) } - } - - fn lock(&self) -> turns::Turn<'_, toyos::fs::Dir> { - self.dir.lock() - } -} +/// A descriptor naming a file, which the `File` owns. +pub struct File(i32); #[derive(Clone)] pub struct FileAttr { @@ -139,16 +91,6 @@ impl FileType { } impl FileAttr { - fn of(stat: toyos::fs::Stat) -> FileAttr { - FileAttr { - size: stat.size, - file_type: FileType::of_kind(stat.kind), - mtime: stat.mtime, - volume: stat.volume, - node: stat.node, - } - } - fn of_kernel(stat: syscall::Stat) -> FileAttr { FileAttr { size: stat.size, @@ -276,296 +218,75 @@ impl OpenOptions { self.create_new = create_new; } - fn to_flags(&self) -> OpenFlags { - let mut flags = OpenFlags(0); - if self.read { - flags |= OpenFlags::READ; - } - if self.write || self.append { - flags |= OpenFlags::WRITE; - } - if self.append { - flags |= OpenFlags::APPEND; - } - if self.create || self.create_new { - flags |= OpenFlags::CREATE; + /// What `toyos::files` opens: std's descriptors are not inherited, as + /// std's are close-on-exec on unix. + fn how(&self) -> Open { + Open { + read: self.read, + write: self.write, + append: self.append, + create: self.create, + create_new: self.create_new, + truncate: self.truncate, + cloexec: true, } - if self.truncate { - flags |= OpenFlags::TRUNCATE; - } - flags } +} - fn served_flags(&self) -> u64 { - use toyos::fs::*; - let mut flags = 0; - if self.read { - flags |= O_READ; - } - if self.write { - flags |= O_WRITE; - } - if self.append { - flags |= O_APPEND; - } - if self.create { - flags |= O_CREATE; - } - if self.create_new { - flags |= O_CREATE_NEW; - } - if self.truncate { - flags |= O_TRUNCATE; - } - flags - } -} - -/// Where a path is served. -enum Route { - Kernel(String), - Served(Arc, String), -} - -/// Every capability this process has looked for, found or not. -static CAPABILITIES: Mutex>>> = Mutex::new(BTreeMap::new()); - -/// A file server's refusal, or the client end's own, as std says it. -fn served_error(e: toyos::fs::Error) -> io::Error { - use io::ErrorKind as Kind; - use toyos::fs::Error; - match e { - Error::NotFound => io::const_error!(Kind::NotFound, "no such file or directory"), - Error::AlreadyExists => io::const_error!(Kind::AlreadyExists, "it already exists"), - Error::NotADirectory => io::const_error!(Kind::NotADirectory, "not a directory"), - Error::IsADirectory => io::const_error!(Kind::IsADirectory, "is a directory"), - Error::NotEmpty => io::const_error!(Kind::DirectoryNotEmpty, "the directory is not empty"), - Error::NotALink => io::const_error!(Kind::InvalidInput, "not a symbolic link"), - Error::NameTooLong => io::const_error!(Kind::InvalidFilename, "the name or path is too long"), - Error::InvalidName => io::const_error!(Kind::InvalidFilename, "the volume cannot store that name"), - Error::ReadOnly => io::const_error!(Kind::PermissionDenied, "the directory is read-only to this process"), - Error::NotOpenForWriting => io::const_error!(Kind::PermissionDenied, "the file is not open for writing"), - Error::Root => io::const_error!(Kind::ResourceBusy, "a directory the file server serves stays where it is"), - Error::Escapes => io::const_error!(Kind::PermissionDenied, "a link leads out of the directory"), - Error::CrossesDirectories => { - io::const_error!(Kind::CrossesDevices, "a link leads to another directory capability") - } - Error::Loop => io::const_error!(Kind::FilesystemLoop, "too many symbolic links"), - Error::IntoItself => io::const_error!(Kind::InvalidInput, "a directory cannot move beneath itself"), - Error::NoSpace => io::const_error!(Kind::StorageFull, "the volume is full"), - Error::FileTooLarge => io::const_error!(Kind::FileTooLarge, "the file would be too large"), - Error::LimitExceeded => io::const_error!(Kind::QuotaExceeded, "past a bound the file server keeps"), - Error::TooManyOpen => io::const_error!(Kind::QuotaExceeded, "too many files open on the file server"), - Error::TooManyConnections => { - io::const_error!(Kind::QuotaExceeded, "the file server serves as many connections as it takes") - } - Error::TooManyStreams => { - io::const_error!(Kind::QuotaExceeded, "the file server serves as many streams as it takes") - } - Error::Io => io::const_error!(Kind::Other, "the volume's device failed, or its structures do not decode"), - Error::Stale => io::const_error!( - Kind::StaleNetworkFileHandle, - "the file was removed, or its file server restarted, since it was opened", - ), - Error::UnknownFile => io::const_error!(Kind::InvalidInput, "the file server holds no such open file"), - Error::InvalidPath => io::const_error!(Kind::InvalidInput, "the path is not one the file server takes"), - Error::InvalidRequest => io::const_error!(Kind::InvalidInput, "the file server takes no such request"), - Error::Unsupported => io::const_error!(Kind::Unsupported, "the volume's format has no such thing"), - Error::OutOfResources => io::const_error!(Kind::OutOfMemory, "the kernel had no room for the request"), - Error::NoDirectory => io::const_error!(Kind::NotADirectory, "the connection was given files and no directory"), - Error::Gone => io::const_error!(Kind::StaleNetworkFileHandle, "the file server is gone"), - Error::NotHeld => io::const_error!(Kind::NotFound, "this process holds no such directory"), - Error::Malformed => io::const_error!(Kind::InvalidData, "the file server answered outside its protocol"), - } -} - -/// `path`, absolute against the working directory and without `.` or `..`. -fn absolute(path: &Path) -> io::Result { - let text = path - .to_str() - .ok_or_else(|| io::const_error!(io::ErrorKind::InvalidInput, "path is not UTF-8"))?; - let joined = if text.starts_with('/') { - String::from(text) - } else { - let cwd = crate::env::current_dir()?; - let cwd = cwd.to_str().expect("getcwd answers UTF-8"); - format!("{cwd}/{text}") - }; - let mut parts: Vec<&str> = Vec::new(); - for part in joined.split('/') { - match part { - "" | "." => {} - ".." => { - parts.pop(); - } - other => parts.push(other), - } - } - Ok(format!("/{}", parts.join("/"))) -} - -/// The capability named `fs:`, remembering the answer either way. -/// -/// The connect waits for the server's hello, so it is made outside the lock: a -/// server that has not answered holds up only the threads that asked it, and -/// of two threads that connect at once the first answer kept is the one used. -fn capability(prefix: &str) -> io::Result>> { - if let Some(found) = CAPABILITIES.lock().unwrap().get(prefix) { - return Ok(found.clone()); - } - let Some(names) = toyos::endow::namespace() else { - return Ok(None); - }; - let name = format!("{}{prefix}", toyos::fs::CAPABILITY_PREFIX); - let found = match toyos::fs::Dir::connect(names, &name) { - Ok(dir) => Some(Arc::new(Capability::new(prefix, dir))), - Err(toyos::fs::Error::NotHeld) => None, - Err(e) => return Err(served_error(e)), - }; - Ok(CAPABILITIES.lock().unwrap().entry(String::from(prefix)).or_insert(found).clone()) -} - -/// Which server `abs` is on, and the path there. -fn route(abs: &str) -> io::Result { - let parts: Vec<&str> = abs.split('/').filter(|p| !p.is_empty()).collect(); - for depth in (1..=parts.len().min(MAX_CAPABILITY_DEPTH)).rev() { - let prefix = format!("/{}", parts[..depth].join("/")); - if let Some(cap) = capability(&prefix)? { - return Ok(Route::Served(cap, parts[depth..].join("/"))); - } - } - Ok(Route::Kernel(String::from(abs))) -} - -/// Ask the server behind `path`, following an absolute link it meets to -/// wherever that lands in this process's own table. -fn on_path( - path: &Path, - mut kernel: impl FnMut(&str) -> io::Result, - mut served: impl FnMut(&Arc, &mut toyos::fs::Dir, &str) -> Result, -) -> io::Result { - let mut abs = absolute(path)?; - for _ in 0..MAX_LINKS { - let (cap, rel) = match route(&abs)? { - Route::Kernel(abs) => return kernel(&abs), - Route::Served(cap, rel) => (cap, rel), - }; - let mut dir = cap.lock(); - match served(&cap, &mut dir, &rel) { - Ok(answer) => return Ok(answer), - Err(toyos::fs::Refused::Error(e)) => return Err(served_error(e)), - Err(toyos::fs::Refused::Link(len)) => { - let mut buf = [0u8; toyos::fs::MAX_PATH]; - let target = dir.link_target(len, &mut buf); - let target = crate::str::from_utf8(target).map_err(|_| { - io::const_error!(io::ErrorKind::InvalidData, "a link target is not UTF-8") - })?; - abs = absolute(Path::new(target))?; - } - } - } - Err(io::const_error!(io::ErrorKind::FilesystemLoop, "too many symbolic links")) +/// `path`'s bytes, as `toyos::files` takes a path. +fn bytes(path: &Path) -> &[u8] { + path.as_os_str().as_encoded_bytes() } /// Whether `path` is on a file server rather than the kernel. pub fn is_served(path: &Path) -> bool { - absolute(path).and_then(|abs| route(&abs)).is_ok_and(|r| matches!(r, Route::Served(..))) -} - -impl Served { - fn with( - &self, - op: impl FnOnce(&mut toyos::fs::Dir, u64, u64) -> Result, - ) -> io::Result { - let mut dir = self.dir.lock(); - op(&mut dir, self.fid, self.generation).map_err(served_error) - } -} - -impl Drop for Served { - fn drop(&mut self) { - self.dir.lock().close(self.fid, self.generation); - } + files::is_served(bytes(path)) } impl File { /// The file descriptor `fd` names, owned from here on. pub fn from_fd(fd: i32) -> Self { - File(Inner::Kernel(fd)) + File(fd) } - /// This file's descriptor. A file a file server holds has none yet: - /// `as_child_stdio` is how one reaches a child. pub fn raw_fd(&self) -> i32 { - match &self.0 { - Inner::Kernel(fd) => *fd, - Inner::Served(_) => panic!("a file on a file server has no descriptor"), - } + self.0 } pub fn as_raw_fd(&self) -> i32 { - self.raw_fd() + self.0 } /// This file's descriptor, given up: closing it is the caller's. pub fn into_raw_fd(self) -> i32 { - let fd = self.raw_fd(); + let fd = self.0; crate::mem::forget(self); fd } - /// The kernel handle behind this file, held for `f`. + /// The kernel handle behind this file, held for `f`: none for a file a + /// file server holds. pub fn with_handle(&self, f: impl FnOnce(toyos_abi::RawHandle) -> T) -> io::Result { - match &self.0 { - Inner::Kernel(fd) => crate::sys::on_fd(*fd, |h| Ok(f(h))), - Inner::Served(_) => Err(io::const_error!(io::ErrorKind::Unsupported, "a file on a file server has no kernel handle")), - } + crate::sys::on_fd(self.0, |h| Ok(f(h))) } - /// A kernel handle a child may use this file through: a copy of the + /// A kernel handle a child may use this file through: a copy of a kernel /// file's own, or a pipe its server writes to the file at this file's /// offset. The caller holds it until the child has its own. pub fn as_child_stdio(&self) -> io::Result { - match &self.0 { - Inner::Kernel(fd) => toyos::fd::copy(*fd) - .map_err(crate::sys::refused)? - .ok_or_else(|| crate::sys::fd_error(toyos::fd::Error::BadFd)), - Inner::Served(s) => s.with(|dir, fid, generation| dir.stream(fid, generation)).map(toyos::OwnedHandle::from), - } + files::stream(self.0).map_err(files_error) } pub fn open(path: &Path, opts: &OpenOptions) -> io::Result { - on_path( - path, - |abs| { - let handle = syscall::open(abs.as_bytes(), opts.to_flags()).map_err(to_io_error)?; - Ok(File(Inner::Kernel(crate::sys::new_fd(handle)?))) - }, - |cap, dir, rel| { - let flags = opts.served_flags(); - let opened = dir.open(rel, flags)?; - Ok(File(Inner::Served(Arc::new(Served { - dir: Arc::clone(cap), - rel: String::from(rel), - fid: opened.fid, - generation: opened.generation, - })))) - }, - ) + files::open(bytes(path), opts.how()).map(File).map_err(files_error) } pub fn file_attr(&self) -> io::Result { - match &self.0 { - Inner::Kernel(fd) => crate::sys::on_fd(*fd, syscall::fstat).map(FileAttr::of_kernel), - Inner::Served(s) => s.with(|dir, fid, generation| dir.fstat(fid, generation)).map(FileAttr::of), - } + files::fstat(self.0).map(FileAttr::of_kernel).map_err(files_error) } pub fn fsync(&self) -> io::Result<()> { - match &self.0 { - Inner::Kernel(fd) => crate::sys::on_fd(*fd, syscall::fsync), - Inner::Served(s) => s.with(|dir, fid, generation| dir.fsync(fid, generation)), - } + files::fsync(self.0).map_err(files_error) } pub fn datasync(&self) -> io::Result<()> { @@ -589,35 +310,15 @@ impl File { } pub fn truncate(&self, size: u64) -> io::Result<()> { - match &self.0 { - Inner::Kernel(fd) => crate::sys::on_fd(*fd, |h| syscall::ftruncate(h, size)), - Inner::Served(s) => s.with(|dir, fid, generation| dir.truncate(fid, generation, size)), - } + files::truncate(self.0, size).map_err(files_error) } pub fn read(&self, buf: &mut [u8]) -> io::Result { - match &self.0 { - Inner::Kernel(fd) => crate::sys::on_fd(*fd, |h| syscall::read(h, buf)), - Inner::Served(s) => s.with(|dir, fid, generation| dir.read(fid, generation, buf)), - } + files::read(self.0, buf).map_err(files_error) } pub fn read_vectored(&self, bufs: &mut [IoSliceMut<'_>]) -> io::Result { - let mut total = 0; - for buf in bufs { - match self.read(buf) { - Ok(0) => break, - Ok(n) => total += n, - Err(e) => { - if total == 0 { - return Err(e); - } else { - break; - } - } - } - } - Ok(total) + crate::io::default_read_vectored(|b| self.read(b), bufs) } pub fn is_read_vectored(&self) -> bool { @@ -631,28 +332,11 @@ impl File { } pub fn write(&self, buf: &[u8]) -> io::Result { - match &self.0 { - Inner::Kernel(fd) => crate::sys::on_fd(*fd, |h| syscall::write(h, buf)), - Inner::Served(s) => s.with(|dir, fid, generation| dir.write(fid, generation, buf).map(|w| w.len)), - } + files::write(self.0, buf).map_err(files_error) } pub fn write_vectored(&self, bufs: &[IoSlice<'_>]) -> io::Result { - let mut total = 0; - for buf in bufs { - match self.write(buf) { - Ok(0) => break, - Ok(n) => total += n, - Err(e) => { - if total == 0 { - return Err(e); - } else { - break; - } - } - } - } - Ok(total) + crate::io::default_write_vectored(|b| self.write(b), bufs) } pub fn is_write_vectored(&self) -> bool { @@ -660,32 +344,16 @@ impl File { } pub fn flush(&self) -> io::Result<()> { - self.fsync() + Ok(()) } pub fn seek(&self, pos: SeekFrom) -> io::Result { - match &self.0 { - Inner::Kernel(fd) => { - let abi_pos = match pos { - SeekFrom::Start(n) => syscall::SeekFrom::Start(n), - SeekFrom::Current(n) => syscall::SeekFrom::Current(n), - SeekFrom::End(n) => syscall::SeekFrom::End(n), - }; - crate::sys::on_fd(*fd, |h| syscall::seek(h, abi_pos)) - } - Inner::Served(s) => { - let (whence, delta) = match pos { - // Past the largest offset a file has is the request's fault, as the kernel's route says. - SeekFrom::Start(n) => { - let n = i64::try_from(n).map_err(|_| io::Error::from(io::ErrorKind::InvalidInput))?; - (toyos::fs::SEEK_START, n) - } - SeekFrom::Current(d) => (toyos::fs::SEEK_CURRENT, d), - SeekFrom::End(d) => (toyos::fs::SEEK_END, d), - }; - s.with(|dir, fid, generation| dir.seek(fid, generation, whence, delta)) - } - } + let pos = match pos { + SeekFrom::Start(n) => AbiSeek::Start(n), + SeekFrom::Current(n) => AbiSeek::Current(n), + SeekFrom::End(n) => AbiSeek::End(n), + }; + files::seek(self.0, pos).map_err(files_error) } pub fn size(&self) -> Option> { @@ -696,11 +364,9 @@ impl File { self.seek(SeekFrom::Current(0)) } + /// A second descriptor on the same open file, sharing its offset. pub fn duplicate(&self) -> io::Result { - match &self.0 { - Inner::Kernel(fd) => Ok(File(Inner::Kernel(toyos::fd::dup(*fd, 0, false).map_err(crate::sys::fd_error)?))), - Inner::Served(s) => Ok(File(Inner::Served(Arc::clone(s)))), - } + toyos::fd::dup(self.0, 0, false).map(File).map_err(fd_error) } pub fn set_permissions(&self, _perm: FilePermissions) -> io::Result<()> { @@ -714,9 +380,7 @@ impl File { impl Drop for File { fn drop(&mut self) { - if let Inner::Kernel(fd) = self.0 { - let _ = toyos::fd::close(fd); - } + let _ = toyos::fd::close(self.0); } } @@ -726,24 +390,17 @@ impl DirBuilder { } pub fn mkdir(&self, p: &Path) -> io::Result<()> { - on_path( - p, - |abs| syscall::mkdir(abs.as_bytes()).map_err(to_io_error), - |_, dir, rel| dir.mkdir(rel), - ) + files::mkdir(bytes(p)).map_err(files_error) } } impl fmt::Debug for File { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match &self.0 { - Inner::Kernel(fd) => write!(f, "File({fd})"), - Inner::Served(s) => write!(f, "File({}/{})", s.dir.prefix, s.rel), - } + write!(f, "File({})", self.0) } } -/// The type of something the kernel answered a `stat` or `fstat` of. +/// The type of something a `stat` or `fstat` answered. fn kernel_file_type(ty: syscall::FileType) -> FileType { FileType { is_file: ty == syscall::FileType::File, @@ -752,96 +409,22 @@ fn kernel_file_type(ty: syscall::FileType) -> FileType { } } -/// A listing too large for a first buffer is asked again at the size it -/// named; a directory that outgrows its own listing this many times is -/// refused rather than waited on. -const LIST_ATTEMPTS: usize = 4; - pub fn readdir(p: &Path) -> io::Result { - let dir_path = p.to_path_buf(); - let entries = on_path( - p, - |abs| { - let mut buf = vec![0u8; 65536]; - let mut n = 0; - let mut fits = false; - for _ in 0..LIST_ATTEMPTS { - n = syscall::readdir(abs.as_bytes(), &mut buf).map_err(to_io_error)?; - if n <= buf.len() { - fits = true; - break; - } - buf.clear(); - buf.resize(n, 0); - } - Ok(fits.then(|| kernel_entries(&dir_path, &buf[..n]))) - }, - |_, dir, rel| { - let mut buf = vec![0u8; 65536]; - for _ in 0..LIST_ATTEMPTS { - let n = dir.read_dir(rel, &mut buf)?; - if n <= buf.len() { - let mut entries = Vec::new(); - toyos::fs::for_each_entry(&buf[..n], |kind, _, name| { - entries.push(DirEntry { - dir_path: dir_path.clone(), - name: OsString::from(name), - file_type: FileType::of_kind(kind), - }) - })?; - return Ok(Some(entries)); - } - buf.resize(n, 0); - } - Ok(None) - }, - )? - .ok_or(io::const_error!(io::ErrorKind::Interrupted, "directory kept growing while it was being listed"))?; - Ok(ReadDir { entries, index: 0 }) -} - -/// The entries of a whole `readdir` listing, read by `syscall::dirent`. -fn kernel_entries(dir_path: &Path, data: &[u8]) -> Vec { + let listing = files::read_dir(bytes(p)).map_err(files_error)?; let mut entries = Vec::new(); - let mut at = 0; - while let Some(entry) = syscall::dirent(data, &mut at) { - entries.push(DirEntry { - dir_path: dir_path.to_path_buf(), - name: OsString::from(crate::str::from_utf8(entry.name).unwrap_or("")), - file_type: FileType { is_file: !entry.is_dir, is_dir: entry.is_dir, is_symlink: false }, - }); - } - entries + toyos::fs::for_each_entry(&listing, |kind, _, name| { + entries.push(DirEntry { dir_path: p.to_path_buf(), name: OsString::from(name), file_type: FileType::of_kind(kind) }) + }) + .map_err(served_error)?; + Ok(ReadDir { entries, index: 0 }) } pub fn unlink(p: &Path) -> io::Result<()> { - on_path( - p, - |abs| syscall::delete(abs.as_bytes()).map_err(to_io_error), - |_, dir, rel| dir.unlink(rel), - ) + files::unlink(bytes(p)).map_err(files_error) } pub fn rename(old: &Path, new: &Path) -> io::Result<()> { - let (from, to) = (route(&absolute(old)?)?, route(&absolute(new)?)?); - match (from, to) { - (Route::Kernel(a), Route::Kernel(b)) => { - syscall::rename(a.as_bytes(), b.as_bytes()).map_err(to_io_error) - } - (Route::Served(ca, a), Route::Served(cb, b)) if Arc::ptr_eq(&ca, &cb) => { - ca.lock().rename(&a, &b).map_err(|e| match e { - toyos::fs::Refused::Error(e) => served_error(e), - toyos::fs::Refused::Link(_) => io::const_error!( - io::ErrorKind::CrossesDevices, - "the rename crosses a link out of its directory", - ), - }) - } - _ => Err(io::const_error!( - io::ErrorKind::CrossesDevices, - "the two paths are on different servers", - )), - } + files::rename(bytes(old), bytes(new)).map_err(files_error) } pub fn set_perm(_p: &Path, _perm: FilePermissions) -> io::Result<()> { @@ -853,11 +436,7 @@ pub fn set_perm_nofollow(_p: &Path, _perm: FilePermissions) -> io::Result<()> { } pub fn rmdir(p: &Path) -> io::Result<()> { - on_path( - p, - |abs| syscall::rmdir(abs.as_bytes()).map_err(to_io_error), - |_, dir, rel| dir.rmdir(rel), - ) + files::rmdir(bytes(p)).map_err(files_error) } pub fn remove_dir_all(path: &Path) -> io::Result<()> { @@ -882,48 +461,25 @@ pub fn exists(path: &Path) -> io::Result { } pub fn readlink(p: &Path) -> io::Result { - on_path( - p, - |abs| { - let mut buf = [0u8; 4096]; - let n = syscall::readlink(abs.as_bytes(), &mut buf).map_err(to_io_error)?; - // SAFETY: The kernel returns valid UTF-8 paths as raw bytes. - Ok(PathBuf::from(unsafe { OsString::from_encoded_bytes_unchecked(buf[..n].to_vec()) })) - }, - |_, dir, rel| { - let mut buf = [0u8; toyos::fs::MAX_PATH]; - let n = dir.read_link(rel, &mut buf)?; - let text = crate::str::from_utf8(&buf[..n]).map_err(|_| toyos::fs::Error::Malformed)?; - Ok(PathBuf::from(text)) - }, - ) + let target = files::read_link(bytes(p)).map_err(files_error)?; + // SAFETY: a link's target is the bytes a path was written as. + Ok(PathBuf::from(unsafe { OsString::from_encoded_bytes_unchecked(target) })) } pub fn symlink(original: &Path, link: &Path) -> io::Result<()> { - let target = original - .to_str() - .ok_or_else(|| io::const_error!(io::ErrorKind::InvalidInput, "path is not UTF-8"))?; - on_path( - link, - |abs| syscall::symlink(target.as_bytes(), abs.as_bytes()).map_err(to_io_error), - |_, dir, rel| dir.symlink(target, rel).map_err(toyos::fs::Refused::Error), - ) + files::symlink(bytes(original), bytes(link)).map_err(files_error) } pub fn link(_src: &Path, _dst: &Path) -> io::Result<()> { Err(io::Error::new(io::ErrorKind::Unsupported, "no hard links on ToyOS")) } -fn kernel_stat(abs: &str, follow: bool) -> io::Result { - syscall::stat(abs.as_bytes(), follow).map(FileAttr::of_kernel).map_err(to_io_error) -} - pub fn stat(path: &Path) -> io::Result { - on_path(path, |abs| kernel_stat(abs, true), |_, dir, rel| dir.stat(rel, true).map(FileAttr::of)) + files::stat(bytes(path), true).map(FileAttr::of_kernel).map_err(files_error) } pub fn lstat(path: &Path) -> io::Result { - on_path(path, |abs| kernel_stat(abs, false), |_, dir, rel| dir.stat(rel, false).map(FileAttr::of)) + files::stat(bytes(path), false).map(FileAttr::of_kernel).map_err(files_error) } pub fn canonicalize(p: &Path) -> io::Result { @@ -985,11 +541,5 @@ pub fn read_image(path: &Path) -> io::Result { /// Make the working directory `p`, which a file server judges when it serves /// it and the kernel judges when it does. pub fn chdir(p: &Path) -> io::Result<()> { - let abs = absolute(p)?; - if let Route::Served(..) = route(&abs)? { - if !stat(Path::new(&abs))?.file_type.is_dir { - return Err(io::const_error!(io::ErrorKind::NotADirectory, "not a directory")); - } - } - syscall::chdir(abs.as_bytes()).map_err(to_io_error) + files::chdir(bytes(p)).map_err(files_error) } diff --git a/sdk/std/sys/pal/mod.rs b/sdk/std/sys/pal/mod.rs index b545cdeef50..244b8733b70 100644 --- a/sdk/std/sys/pal/mod.rs +++ b/sdk/std/sys/pal/mod.rs @@ -63,6 +63,72 @@ pub fn on_open_fd( }) } +/// A file server's refusal, or the client end's own, as std says it. +pub fn served_error(e: toyos::fs::Error) -> crate::io::Error { + use crate::io::{self, ErrorKind as Kind}; + use toyos::fs::Error; + match e { + Error::NotFound => io::const_error!(Kind::NotFound, "no such file or directory"), + Error::AlreadyExists => io::const_error!(Kind::AlreadyExists, "it already exists"), + Error::NotADirectory => io::const_error!(Kind::NotADirectory, "not a directory"), + Error::IsADirectory => io::const_error!(Kind::IsADirectory, "is a directory"), + Error::NotEmpty => io::const_error!(Kind::DirectoryNotEmpty, "the directory is not empty"), + Error::NotALink => io::const_error!(Kind::InvalidInput, "not a symbolic link"), + Error::NameTooLong => io::const_error!(Kind::InvalidFilename, "the name or path is too long"), + Error::InvalidName => io::const_error!(Kind::InvalidFilename, "the volume cannot store that name"), + Error::ReadOnly => io::const_error!(Kind::PermissionDenied, "the directory is read-only to this process"), + Error::NotOpenForWriting => io::const_error!(Kind::PermissionDenied, "the file is not open for writing"), + Error::Root => io::const_error!(Kind::ResourceBusy, "a directory the file server serves stays where it is"), + Error::Escapes => io::const_error!(Kind::PermissionDenied, "a link leads out of the directory"), + Error::CrossesDirectories => { + io::const_error!(Kind::CrossesDevices, "a link leads to another directory capability") + } + Error::Loop => io::const_error!(Kind::FilesystemLoop, "too many symbolic links"), + Error::IntoItself => io::const_error!(Kind::InvalidInput, "a directory cannot move beneath itself"), + Error::NoSpace => io::const_error!(Kind::StorageFull, "the volume is full"), + Error::FileTooLarge => io::const_error!(Kind::FileTooLarge, "the file would be too large"), + Error::LimitExceeded => io::const_error!(Kind::QuotaExceeded, "past a bound the file server keeps"), + Error::TooManyOpen => io::const_error!(Kind::QuotaExceeded, "too many files open on the file server"), + Error::TooManyConnections => { + io::const_error!(Kind::QuotaExceeded, "the file server serves as many connections as it takes") + } + Error::TooManyStreams => { + io::const_error!(Kind::QuotaExceeded, "the file server serves as many streams as it takes") + } + Error::Io => io::const_error!(Kind::Other, "the volume's device failed, or its structures do not decode"), + Error::Stale => io::const_error!( + Kind::StaleNetworkFileHandle, + "the file was removed, or its file server restarted, since it was opened", + ), + Error::UnknownFile => io::const_error!(Kind::InvalidInput, "the file server holds no such open file"), + Error::InvalidPath => io::const_error!(Kind::InvalidInput, "the path is not one the file server takes"), + Error::InvalidRequest => io::const_error!(Kind::InvalidInput, "the file server takes no such request"), + Error::Unsupported => io::const_error!(Kind::Unsupported, "the volume's format has no such thing"), + Error::OutOfResources => io::const_error!(Kind::OutOfMemory, "the kernel had no room for the request"), + Error::NoDirectory => io::const_error!(Kind::NotADirectory, "the connection was given files and no directory"), + Error::Gone => io::const_error!(Kind::StaleNetworkFileHandle, "the file server is gone"), + Error::NotHeld => io::const_error!(Kind::NotFound, "this process holds no such directory"), + Error::Malformed => io::const_error!(Kind::InvalidData, "the file server answered outside its protocol"), + } +} + +/// `toyos::files`' refusal, as std says it. +pub fn files_error(e: toyos::files::Error) -> crate::io::Error { + use crate::io; + use toyos::files; + match e { + files::Error::Kernel(e) => to_io_error(e), + files::Error::Served(e) => served_error(e), + files::Error::Fd(e) => fd_error(e), + files::Error::NotUtf8 => io::const_error!(io::ErrorKind::InvalidInput, "path is not UTF-8"), + files::Error::Loop => io::const_error!(io::ErrorKind::FilesystemLoop, "too many symbolic links"), + files::Error::CrossesServers => { + io::const_error!(io::ErrorKind::CrossesDevices, "the two paths are on different servers") + } + files::Error::NotAFile => io::const_error!(io::ErrorKind::InvalidInput, "the descriptor names no file"), + } +} + /// Why nothing was spawned, as std's error. pub fn refused(refused: toyos::spawn::Refused) -> crate::io::Error { use toyos::spawn::Refused; diff --git a/sdk/std/sys/stdio.rs b/sdk/std/sys/stdio.rs index 797fefcfa96..f59d0d15634 100644 --- a/sdk/std/sys/stdio.rs +++ b/sdk/std/sys/stdio.rs @@ -4,7 +4,7 @@ use toyos::log::stdio::{self as log_stdio, Stream}; use toyos_abi::syscall::{self, FileType}; use crate::io::{self, IoSlice, IoSliceMut}; -use crate::sys::{fd_error, on_fd, on_open_fd, to_io_error}; +use crate::sys::{fd_error, files_error, on_fd, on_open_fd}; /// Standard input's descriptor. const STDIN: i32 = 0; @@ -167,7 +167,7 @@ impl Stdout { impl io::Write for Stdout { fn write(&mut self, buf: &[u8]) -> io::Result { - log_stdio::write(Stream::Out, buf).map_err(to_io_error) + log_stdio::write(Stream::Out, buf).map_err(files_error) } fn write_vectored(&mut self, bufs: &[IoSlice<'_>]) -> io::Result { @@ -196,7 +196,7 @@ impl Stderr { impl io::Write for Stderr { fn write(&mut self, buf: &[u8]) -> io::Result { - log_stdio::write(Stream::Err, buf).map_err(to_io_error) + log_stdio::write(Stream::Err, buf).map_err(files_error) } fn write_vectored(&mut self, bufs: &[IoSlice<'_>]) -> io::Result { diff --git a/sdk/std/sys/turns.rs b/sdk/std/sys/turns.rs deleted file mode 100644 index d6033d9be1e..00000000000 --- a/sdk/std/sys/turns.rs +++ /dev/null @@ -1,103 +0,0 @@ -//! A value held in turn: each `lock` takes a ticket, and the value goes to the -//! tickets in the order they were taken. std's mutex alone lets the thread -//! that let go take it straight back ahead of the one it woke, so threads -//! sharing one directory's connection would not be served in turn. -//! -//! It names nothing but `crate::sync`, so a host compiles it as it is -//! (`sdk/std/copies`). - -use crate::sync::{Condvar, Mutex, MutexGuard}; - -pub struct Turnstile { - value: Mutex, - turns: Mutex, - served: Condvar, -} - -struct Turns { - next: u64, - serving: u64, -} - -/// The value, held for one turn. -pub struct Turn<'a, T> { - // Dropped before `_ticket`, so the next ticket finds the value free. - value: MutexGuard<'a, T>, - _ticket: Ticket<'a, T>, -} - -struct Ticket<'a, T>(&'a Turnstile); - -impl Turnstile { - pub const fn new(value: T) -> Self { - Self { value: Mutex::new(value), turns: Mutex::new(Turns { next: 0, serving: 0 }), served: Condvar::new() } - } - - /// The value, once every ticket taken before this one has had its turn. - pub fn lock(&self) -> Turn<'_, T> { - let mut turns = self.turns.lock().unwrap(); - let ticket = turns.next; - turns.next += 1; - drop(self.served.wait_while(turns, |t| t.serving != ticket).unwrap()); - let ticket = Ticket(self); - Turn { value: self.value.lock().unwrap(), _ticket: ticket } - } -} - -impl Drop for Ticket<'_, T> { - fn drop(&mut self) { - self.0.turns.lock().unwrap().serving += 1; - self.0.served.notify_all(); - } -} - -impl crate::ops::Deref for Turn<'_, T> { - type Target = T; - fn deref(&self) -> &T { - &self.value - } -} - -impl crate::ops::DerefMut for Turn<'_, T> { - fn deref_mut(&mut self) -> &mut T { - &mut self.value - } -} - -#[cfg(test)] -mod tests { - use std::sync::Arc; - use std::time::{Duration, Instant}; - - use super::*; - - /// A thread that lets the value go and asks for it again waits behind - /// the threads that asked first, in the order they asked. - #[test] - fn the_value_goes_to_the_tickets_in_the_order_they_were_taken() { - let stile = Arc::new(Turnstile::new(Vec::new())); - let mut held = stile.lock(); - held.push("first"); - let queued: Vec<_> = ["second", "third"] - .into_iter() - .enumerate() - .map(|(i, name)| { - let theirs = Arc::clone(&stile); - let thread = std::thread::spawn(move || theirs.lock().push(name)); - // The next waits for this one's ticket, so the two are taken in order. - let asked = Instant::now(); - while stile.turns.lock().unwrap().next < i as u64 + 2 { - assert!(asked.elapsed() < Duration::from_secs(10), "{name} never took a ticket"); - std::thread::yield_now(); - } - thread - }) - .collect(); - drop(held); - stile.lock().push("first again"); - for thread in queued { - thread.join().unwrap(); - } - assert_eq!(*stile.lock(), ["first", "second", "third", "first again"]); - } -} diff --git a/sdk/toyos/Cargo.toml b/sdk/toyos/Cargo.toml index 0353ec3063d..a8469c9de79 100644 --- a/sdk/toyos/Cargo.toml +++ b/sdk/toyos/Cargo.toml @@ -7,7 +7,7 @@ description = "The ToyOS userland SDK: typed handles, IPC, ports, namespaces, su repository = "https://github.com/ToyOSOrg/ToyOS" [features] -rustc-dep-of-std = ["core", "toyos-abi/rustc-dep-of-std", "fd-table"] +rustc-dep-of-std = ["core", "alloc", "toyos-abi/rustc-dep-of-std", "fd-table"] # This copy owns the process's descriptor table (`src/fd.rs`): on in std's # build and in libc's C-only build, and in no other. fd-table = [] @@ -74,6 +74,7 @@ unexpected_cfgs = { level = "warn", check-cfg = ['cfg(target_os, values("toyos") [dependencies] core = { version = "1.0.0", optional = true, package = "rustc-std-workspace-core" } +alloc = { version = "1.0.0", optional = true, package = "rustc-std-workspace-alloc" } toyos-abi = { path = "../abi", default-features = false } [dev-dependencies] diff --git a/sdk/toyos/src/fd.rs b/sdk/toyos/src/fd.rs index b909ac71dad..c1a99ed9161 100644 --- a/sdk/toyos/src/fd.rs +++ b/sdk/toyos/src/fd.rs @@ -42,12 +42,20 @@ pub enum Error { Full, } +/// The tag of a handle and of an object another library keeps. +pub const UNTAGGED: u32 = 0; +/// The tag of a file a file server holds open (`crate::files`). +pub const SERVED_FILE: u32 = 1; + /// What a descriptor names. #[repr(C)] #[derive(Clone, Copy, Debug)] pub struct Object { /// The kernel handle, or [`toyos_abi::HANDLE_INVALID`] for an opaque object. pub handle: RawHandle, + /// Which kind of opaque object this crate keeps it is, [`UNTAGGED`] for a + /// handle and for another library's. + pub tag: u32, /// An opaque object's word, its owner's to interpret. pub word: u64, /// How an opaque object is let go once nothing names it. @@ -58,13 +66,18 @@ impl Object { /// A kernel handle the table owns from here on: it is closed when the /// object is released. pub const fn handle(handle: RawHandle) -> Self { - Self { handle, word: 0, release: None } + Self { handle, tag: UNTAGGED, word: 0, release: None } } /// An object a library keeps itself, `word`, which `release` is called /// with once no descriptor and no call names it. pub const fn opaque(word: u64, release: extern "C" fn(u64)) -> Self { - Self { handle: toyos_abi::HANDLE_INVALID, word, release: Some(release) } + Self::tagged(UNTAGGED, word, release) + } + + /// An opaque object of a kind this crate keeps, which [`Object::tag`] names. + pub(crate) const fn tagged(tag: u32, word: u64, release: extern "C" fn(u64)) -> Self { + Self { handle: toyos_abi::HANDLE_INVALID, tag, word, release: Some(release) } } /// Let the object go: an opaque one to its library, a handle closed (a @@ -214,6 +227,7 @@ mod owner { /// panic writes to stderr, a descriptor, which would wait on it forever. fn table(f: impl FnOnce(&mut Table) -> Result) -> Result { let endowed = Endowments::get(); + let mut refused = 0; let answer = { let mut held = TABLE.lock(); let (ready, table) = &mut *held; @@ -221,11 +235,15 @@ mod owner { true => Ok(()), false => { *ready = true; - endow(table, endowed) + endow(table, endowed).map(|n| refused = n) } }; installed.and_then(|()| f(table)) }; + // Said once the lock is let go, since stderr is a descriptor. + for _ in 0..refused { + let _ = crate::files::write(2, b"toyos::fd: an fd: endowment whose number was taken was refused\n"); + } answer.map_err(|fault| match fault { Fault::Refused(e) => e, Fault::Broken => panic!("toyos::fd: the descriptor table is inconsistent"), @@ -233,19 +251,23 @@ mod owner { } /// Install each `fd:` endowment at `n`. One whose number another - /// already took is refused, its handle closed. - fn endow(table: &mut Table, endowed: &Endowments) -> Result<(), Fault> { + /// already took is refused, its handle closed: how many were. + fn endow(table: &mut Table, endowed: &Endowments) -> Result { + let mut refused = 0; for label in endowed.labels() { let Some(fd) = endowed_fd(label) else { continue }; let Some(handle) = endowed.take::(label) else { continue }; match table.install_at(fd, Object::handle(handle.0), true) { // The table owns the handle from here on. Ok(()) => drop(handle.into_raw()), - Err(Fault::Refused(_)) => drop(handle), + Err(Fault::Refused(_)) => { + drop(handle); + refused += 1; + } Err(Fault::Broken) => return Err(Fault::Broken), } } - Ok(()) + Ok(refused) } fn released(object: Option) { diff --git a/sdk/toyos/src/files.rs b/sdk/toyos/src/files.rs new file mode 100644 index 00000000000..c40ada2279b --- /dev/null +++ b/sdk/toyos/src/files.rs @@ -0,0 +1,313 @@ +//! Files by path, and the descriptors that name open ones, wherever they are +//! served: the kernel serves `/system` and `/tmp`, and every other directory a +//! process may reach is a capability in its namespace, `fs:`, +//! served by a file server ([`crate::fs`]). +//! +//! **A path goes to the capability with the longest matching prefix**, at +//! most [`MAX_CAPABILITY_DEPTH`] components deep, relative to it; a path under +//! no capability the process holds is the kernel's. An absolute link a server +//! meets is followed here, in this process's own table, at most [`MAX_LINKS`] +//! times. A file server that restarts is survived: its capability is connected +//! again ([`crate::fs::Dir`]). +//! +//! **One copy of this crate keeps this state**, the copy that owns the +//! descriptor table ([`crate::fd`]), and every copy reaches it through +//! `toyos_fs_*`: std and libc see one set of connections and one table. A +//! file a server holds open is a descriptor naming an object of this module's +//! ([`crate::fd::SERVED_FILE`]); a kernel file is one naming its handle. + +use alloc::string::String; +use alloc::vec::Vec; + +use toyos_abi::syscall::{SeekFrom, Stat, SyscallError}; + +use crate::fd::{self, Fd}; +use crate::fs; +use crate::OwnedHandle; + +/// The deepest directory a capability names, in components. +pub const MAX_CAPABILITY_DEPTH: usize = 4; + +/// The most absolute links one path's resolution follows. +pub const MAX_LINKS: usize = 40; + +/// What an open asks for. +#[repr(C)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct Open { + pub read: bool, + pub write: bool, + pub append: bool, + pub create: bool, + pub create_new: bool, + pub truncate: bool, + /// The descriptor is not inherited (`toyos::fd`'s flag). + pub cloexec: bool, +} + +/// Why a call was not answered. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Error { + /// The kernel's refusal, of a path or a handle it serves. + Kernel(SyscallError), + /// A file server's refusal, or its client end's ([`crate::fs::Error`]). + Served(fs::Error), + /// The descriptor table's. + Fd(fd::Error), + /// A path that is not UTF-8, which neither server takes. + NotUtf8, + /// More absolute links on the way than are followed. + Loop, + /// A rename between two servers, or between a server and the kernel. + CrossesServers, + /// A descriptor that names no file, but an object of a library's own. + NotAFile, +} + +// The C interface's words: a negative answer is an `Error`. +const BAD_FD: i64 = -1; +const FULL: i64 = -2; +const NOT_UTF8: i64 = -3; +const LOOP: i64 = -4; +const CROSSES: i64 = -5; +const NOT_A_FILE: i64 = -6; +const KERNEL: i64 = 0x100; +const SERVED: i64 = 0x200; + +#[cfg(any(feature = "fd-table", test))] +fn encode(e: Error) -> i64 { + match e { + Error::Fd(fd::Error::BadFd) => BAD_FD, + Error::Fd(fd::Error::Full) => FULL, + Error::NotUtf8 => NOT_UTF8, + Error::Loop => LOOP, + Error::CrossesServers => CROSSES, + Error::NotAFile => NOT_A_FILE, + Error::Kernel(e) => -(KERNEL + (u64::MAX - e.to_u64()) as i64), + Error::Served(e) => -(SERVED + i64::from(e.byte())), + } +} + +fn decode(word: i64) -> Result { + match word { + 0.. => Ok(word as u64), + BAD_FD => Err(Error::Fd(fd::Error::BadFd)), + FULL => Err(Error::Fd(fd::Error::Full)), + NOT_UTF8 => Err(Error::NotUtf8), + LOOP => Err(Error::Loop), + CROSSES => Err(Error::CrossesServers), + NOT_A_FILE => Err(Error::NotAFile), + _ => { + let code = -word; + let kernel = (KERNEL..SERVED).contains(&code).then(|| SyscallError::from_u64(u64::MAX - (code - KERNEL) as u64)).flatten(); + let served = (SERVED..SERVED + 256).contains(&code).then(|| fs::Error::from_byte((code - SERVED) as u8)).flatten(); + match (kernel, served) { + (Some(e), _) => Err(Error::Kernel(e)), + (_, Some(e)) => Err(Error::Served(e)), + _ => panic!("toyos::files: an answer {word} no error encodes"), + } + } + } +} + +const SEEK_START: u32 = 0; +const SEEK_CURRENT: u32 = 1; +const SEEK_END: u32 = 2; + +extern "C" { + fn toyos_fs_open(path: *const u8, len: usize, how: Open) -> i64; + fn toyos_fs_stat(path: *const u8, len: usize, follow: bool, out: *mut Stat) -> i64; + fn toyos_fs_fstat(fd: Fd, out: *mut Stat) -> i64; + fn toyos_fs_read(fd: Fd, buf: *mut u8, len: usize) -> i64; + fn toyos_fs_write(fd: Fd, buf: *const u8, len: usize) -> i64; + fn toyos_fs_seek(fd: Fd, whence: u32, delta: i64) -> i64; + fn toyos_fs_truncate(fd: Fd, size: u64) -> i64; + fn toyos_fs_fsync(fd: Fd) -> i64; + fn toyos_fs_stream(fd: Fd) -> i64; + fn toyos_fs_mkdir(path: *const u8, len: usize) -> i64; + fn toyos_fs_rmdir(path: *const u8, len: usize) -> i64; + fn toyos_fs_unlink(path: *const u8, len: usize) -> i64; + fn toyos_fs_rename(from: *const u8, from_len: usize, to: *const u8, to_len: usize) -> i64; + fn toyos_fs_symlink(target: *const u8, target_len: usize, link: *const u8, link_len: usize) -> i64; + fn toyos_fs_readlink(path: *const u8, len: usize, out: *mut u8, cap: usize) -> i64; + fn toyos_fs_readdir(path: *const u8, len: usize, out: *mut u8, cap: usize) -> i64; + fn toyos_fs_is_served(path: *const u8, len: usize) -> i64; + fn toyos_fs_chdir(path: *const u8, len: usize) -> i64; +} + +/// Open `path`: a descriptor, the lowest free. +pub fn open(path: &[u8], how: Open) -> Result { + // SAFETY: `path` is live for the call, and `how` is a plain value. + decode(unsafe { toyos_fs_open(path.as_ptr(), path.len(), how) }).map(|fd| fd as Fd) +} + +/// What `path` names: the link itself where `follow` is false. +pub fn stat(path: &[u8], follow: bool) -> Result { + let mut out = Stat::default(); + // SAFETY: both live for the call. + decode(unsafe { toyos_fs_stat(path.as_ptr(), path.len(), follow, &mut out) }).map(|_| out) +} + +pub fn fstat(fd: Fd) -> Result { + let mut out = Stat::default(); + // SAFETY: `out` is live for the call. + decode(unsafe { toyos_fs_fstat(fd, &mut out) }).map(|_| out) +} + +pub fn read(fd: Fd, buf: &mut [u8]) -> Result { + // SAFETY: `buf` is live and writable for the call. + decode(unsafe { toyos_fs_read(fd, buf.as_mut_ptr(), buf.len()) }).map(|n| n as usize) +} + +pub fn write(fd: Fd, buf: &[u8]) -> Result { + // SAFETY: `buf` is live for the call. + decode(unsafe { toyos_fs_write(fd, buf.as_ptr(), buf.len()) }).map(|n| n as usize) +} + +/// Move the descriptor's offset: where it landed. +pub fn seek(fd: Fd, pos: SeekFrom) -> Result { + let (whence, delta) = match pos { + SeekFrom::Start(n) => (SEEK_START, i64::try_from(n).map_err(|_| Error::Kernel(SyscallError::InvalidArgument))?), + SeekFrom::Current(d) => (SEEK_CURRENT, d), + SeekFrom::End(d) => (SEEK_END, d), + }; + // SAFETY: plain values. + decode(unsafe { toyos_fs_seek(fd, whence, delta) }) +} + +pub fn truncate(fd: Fd, size: u64) -> Result<(), Error> { + // SAFETY: plain values. + decode(unsafe { toyos_fs_truncate(fd, size) }).map(drop) +} + +pub fn fsync(fd: Fd) -> Result<(), Error> { + // SAFETY: plain values. + decode(unsafe { toyos_fs_fsync(fd) }).map(drop) +} + +/// A kernel handle a child may write the file through as its stdout: a copy +/// of a kernel file's own, or a pipe the file's server writes to it at its +/// offset. The caller holds it until the child has its own. +pub fn stream(fd: Fd) -> Result { + // SAFETY: plain values; a handle answered is this process's alone. + decode(unsafe { toyos_fs_stream(fd) }).map(|h| OwnedHandle(toyos_abi::RawHandle(h as u32))) +} + +macro_rules! path_call { + ($($(#[$doc:meta])* $name:ident => $sys:ident;)+) => {$( + $(#[$doc])* + pub fn $name(path: &[u8]) -> Result<(), Error> { + // SAFETY: `path` is live for the call. + decode(unsafe { $sys(path.as_ptr(), path.len()) }).map(drop) + } + )+}; +} + +path_call! { + mkdir => toyos_fs_mkdir; + rmdir => toyos_fs_rmdir; + unlink => toyos_fs_unlink; + /// Make `path` the working directory, which a file server judges when it + /// serves it and the kernel when it does. + chdir => toyos_fs_chdir; +} + +/// `from` renamed `to`, both on one server. +pub fn rename(from: &[u8], to: &[u8]) -> Result<(), Error> { + // SAFETY: both live for the call. + decode(unsafe { toyos_fs_rename(from.as_ptr(), from.len(), to.as_ptr(), to.len()) }).map(drop) +} + +/// Make `link` a symlink holding `target`, stored as written. +pub fn symlink(target: &[u8], link: &[u8]) -> Result<(), Error> { + // SAFETY: both live for the call. + decode(unsafe { toyos_fs_symlink(target.as_ptr(), target.len(), link.as_ptr(), link.len()) }).map(drop) +} + +/// What the link at `path` holds. +pub fn read_link(path: &[u8]) -> Result, Error> { + let mut out = alloc::vec![0u8; toyos_abi::syscall::MAX_PATH]; + // SAFETY: both live for the call; `out` is writable. + let n = decode(unsafe { toyos_fs_readlink(path.as_ptr(), path.len(), out.as_mut_ptr(), out.len()) })? as usize; + out.truncate(n); + Ok(out) +} + +/// The listing of the directory at `path`, in [`crate::fs::for_each_entry`]'s +/// encoding whoever serves it. +pub fn read_dir(path: &[u8]) -> Result, Error> { + let mut out = alloc::vec![0u8; 64 * 1024]; + loop { + // SAFETY: both live for the call; `out` is writable. + let n = decode(unsafe { toyos_fs_readdir(path.as_ptr(), path.len(), out.as_mut_ptr(), out.len()) })? as usize; + if n <= out.len() { + out.truncate(n); + return Ok(out); + } + out.resize(n, 0); + } +} + +/// Whether `path` is on a file server rather than the kernel. +pub fn is_served(path: &[u8]) -> bool { + // SAFETY: `path` is live for the call. + decode(unsafe { toyos_fs_is_served(path.as_ptr(), path.len()) }) == Ok(1) +} + +/// `path`, absolute against `cwd` and without `.` or `..`. +pub fn absolute(cwd: &str, path: &str) -> String { + let joined = match path.starts_with('/') { + true => String::from(path), + false => alloc::format!("{cwd}/{path}"), + }; + let mut parts: Vec<&str> = Vec::new(); + for part in joined.split('/') { + match part { + "" | "." => {} + ".." => { + parts.pop(); + } + other => parts.push(other), + } + } + alloc::format!("/{}", parts.join("/")) +} + +/// The state, in the copy that owns it. +#[cfg(all(feature = "fd-table", not(test)))] +mod owner; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn a_path_is_made_absolute_and_lexically_clean() { + assert_eq!(absolute("/home/a", "b/./c"), "/home/a/b/c"); + assert_eq!(absolute("/home/a", "../b"), "/home/b"); + assert_eq!(absolute("/home/a", "/tmp//x/"), "/tmp/x"); + assert_eq!(absolute("/", "../../.."), "/"); + } + + #[test] + fn every_error_crosses_the_c_interface_and_comes_back() { + let mut every = alloc::vec![ + Error::Fd(fd::Error::BadFd), + Error::Fd(fd::Error::Full), + Error::NotUtf8, + Error::Loop, + Error::CrossesServers, + Error::NotAFile, + ]; + every.extend((0..=255).filter_map(|code| SyscallError::from_u64(u64::MAX - code)).map(Error::Kernel)); + every.extend((0..=255u8).filter_map(fs::Error::from_byte).map(Error::Served)); + assert!(every.len() > 40, "only {} errors", every.len()); + for e in every { + let word = encode(e); + assert!(word < 0, "{e:?} encodes as an answer"); + assert_eq!(decode(word), Err(e), "{e:?}"); + } + assert_eq!(decode(0), Ok(0)); + assert_eq!(decode(i64::MAX), Ok(i64::MAX as u64)); + } +} diff --git a/sdk/toyos/src/files/owner.rs b/sdk/toyos/src/files/owner.rs new file mode 100644 index 00000000000..019219b7308 --- /dev/null +++ b/sdk/toyos/src/files/owner.rs @@ -0,0 +1,454 @@ +//! `crate::files`' state and its C interface, in the copy that owns the +//! descriptor table. + +use alloc::collections::BTreeMap; +use alloc::string::String; +use alloc::sync::Arc; +use alloc::vec::Vec; + +use toyos_abi::syscall::{self, FileType, OpenFlags, SeekFrom, Stat, SyscallError}; +use toyos_abi::RawHandle; + +use super::{encode, Error, Open, MAX_CAPABILITY_DEPTH, MAX_LINKS, SEEK_CURRENT, SEEK_END, SEEK_START}; +use crate::fd::{self, Fd, Object, SERVED_FILE}; +use crate::fs::{self, Dir, Refused}; +use crate::sync::{Lock, Turnstile}; + +/// One directory capability this process holds, connected: threads sharing +/// it are served in turn. +struct Capability { + dir: Turnstile, +} + +/// Every capability this process has looked for, found or not. +static CAPABILITIES: Lock>>> = Lock::new(BTreeMap::new()); + +/// Where a path is served. +enum Route { + Kernel(String), + Served(Arc, String), +} + +/// A file a file server holds open, which a descriptor names. +struct OpenFile { + cap: Arc, + fid: u64, + generation: u64, +} + +impl Drop for OpenFile { + fn drop(&mut self) { + self.cap.dir.lock().close(self.fid, self.generation); + } +} + +/// The open files descriptors name: a served file's object word is its +/// index here, and the slot is emptied when the table lets the object go. +static OPEN: Lock>>> = Lock::new(Vec::new()); + +/// A descriptor's object let go: the open file goes with it, closed outside +/// the lock, as a close asks the server. +extern "C" fn release_file(word: u64) { + let file = usize::try_from(word).ok().and_then(|i| OPEN.lock().get_mut(i).and_then(Option::take)); + assert!(file.is_some(), "toyos::files: a released open file {word} that was not held"); + drop(file); +} + +/// The open file `word` names, while a descriptor names it. +fn open_file(word: u64) -> Option> { + OPEN.lock().get(usize::try_from(word).ok()?)?.clone() +} + +/// The working directory, as the kernel keeps it. +fn cwd() -> String { + let mut buf = alloc::vec![0u8; syscall::MAX_PATH]; + let n = syscall::getcwd(&mut buf); + String::from_utf8_lossy(&buf[..n.min(buf.len())]).into_owned() +} + +fn utf8(path: &[u8]) -> Result<&str, Error> { + core::str::from_utf8(path).map_err(|_| Error::NotUtf8) +} + +/// The capability named `fs:`, remembering the answer either way. +/// The connect waits for the server's hello, so it is made outside the lock: +/// of two threads that connect at once the first answer kept is the one used. +fn capability(prefix: &str) -> Result>, Error> { + if let Some(found) = CAPABILITIES.lock().get(prefix) { + return Ok(found.clone()); + } + let Some(names) = crate::endow::namespace() else { return Ok(None) }; + let name = alloc::format!("{}{prefix}", fs::CAPABILITY_PREFIX); + let found = match Dir::connect(names, &name) { + Ok(dir) => Some(Arc::new(Capability { dir: Turnstile::new(dir) })), + Err(fs::Error::NotHeld) => None, + Err(e) => return Err(Error::Served(e)), + }; + Ok(CAPABILITIES.lock().entry(String::from(prefix)).or_insert(found).clone()) +} + +/// Which server `abs` is on, and the path there. +fn route(abs: &str) -> Result { + let parts: Vec<&str> = abs.split('/').filter(|p| !p.is_empty()).collect(); + for depth in (1..=parts.len().min(MAX_CAPABILITY_DEPTH)).rev() { + let prefix = alloc::format!("/{}", parts[..depth].join("/")); + if let Some(cap) = capability(&prefix)? { + return Ok(Route::Served(cap, parts[depth..].join("/"))); + } + } + Ok(Route::Kernel(String::from(abs))) +} + +/// Ask whoever serves `path`, following an absolute link a server meets to +/// wherever it lands in this process's own table. +fn on_path( + path: &[u8], + mut kernel: impl FnMut(&str) -> Result, + mut served: impl FnMut(&Arc, &mut Dir, &str) -> Result, +) -> Result { + let mut abs = super::absolute(&cwd(), utf8(path)?); + for _ in 0..MAX_LINKS { + let (cap, rel) = match route(&abs)? { + Route::Kernel(abs) => return kernel(&abs).map_err(Error::Kernel), + Route::Served(cap, rel) => (cap, rel), + }; + let mut dir = cap.dir.lock(); + match served(&cap, &mut dir, &rel) { + Ok(answer) => return Ok(answer), + Err(Refused::Error(e)) => return Err(Error::Served(e)), + Err(Refused::Link(len)) => { + let mut buf = [0u8; fs::MAX_PATH]; + let target = dir.link_target(len, &mut buf); + abs = super::absolute("/", utf8(target)?); + } + } + } + Err(Error::Loop) +} + +/// `kernel` on `fd`'s handle, held for the call, or `served` on its open file. +fn on_fd( + fd: Fd, + kernel: impl FnOnce(RawHandle) -> Result, + served: impl FnOnce(&mut Dir, u64, u64) -> Result, +) -> Result { + // A kernel call is made while the descriptor's object is held; an open + // file is taken out of it, counted, and asked after. + let named = fd::with(fd, |o| match (o.release, o.tag) { + (None, _) => Ok(Err(kernel(o.handle).map_err(Error::Kernel))), + (Some(_), SERVED_FILE) => { + Ok(Ok(open_file(o.word).expect("toyos::files: a held descriptor names an open file"))) + } + (Some(_), _) => Err(Error::NotAFile), + }); + match named.map_err(Error::Fd)?? { + Err(answer) => answer, + Ok(file) => served(&mut file.cap.dir.lock(), file.fid, file.generation).map_err(Error::Served), + } +} + +/// A descriptor for an open file a server answered. +fn served_fd(cap: &Arc, opened: fs::Opened, cloexec: bool) -> Result { + let file = Arc::new(OpenFile { cap: Arc::clone(cap), fid: opened.fid, generation: opened.generation }); + let word = { + let mut open = OPEN.lock(); + match open.iter().position(Option::is_none) { + Some(free) => { + open[free] = Some(file); + free + } + None => { + open.push(Some(file)); + open.len() - 1 + } + } + }; + let object = Object::tagged(SERVED_FILE, word as u64, release_file); + fd::install(object, !cloexec, 0).map_err(|_| Refused::Error(fs::Error::TooManyOpen)) +} + +/// What a server answered, as the kernel's `Stat` says it. +fn stat_of(stat: fs::Stat) -> Stat { + let file_type = match stat.kind { + fs::KIND_FILE => FileType::File, + fs::KIND_DIR => FileType::Directory, + fs::KIND_SYMLINK => FileType::Symlink, + _ => FileType::Unknown, + }; + Stat { file_type, size: stat.size, mtime: stat.mtime, volume: stat.volume, node: stat.node } +} + +fn answer(result: Result) -> i64 { + match result { + Ok(n) => n as i64, + Err(e) => encode(e), + } +} + +/// `len` bytes at `ptr`, as the caller lends them for the call. +/// +/// # Safety +/// `ptr` names `len` readable bytes for the call. +unsafe fn bytes<'a>(ptr: *const u8, len: usize) -> &'a [u8] { + // SAFETY: the caller's. + unsafe { core::slice::from_raw_parts(ptr, len) } +} + +#[no_mangle] +unsafe extern "C" fn toyos_fs_open(path: *const u8, len: usize, how: Open) -> i64 { + // SAFETY: the caller's. + let path = unsafe { bytes(path, len) }; + let opened = on_path( + path, + |abs| { + let mut flags = OpenFlags(0); + if how.read { flags |= OpenFlags::READ; } + if how.write || how.append { flags |= OpenFlags::WRITE; } + if how.append { flags |= OpenFlags::APPEND; } + if how.create || how.create_new { flags |= OpenFlags::CREATE; } + if how.truncate { flags |= OpenFlags::TRUNCATE; } + let handle = syscall::open(abs.as_bytes(), flags)?; + fd::install(Object::handle(handle), !how.cloexec, 0).map_err(|_| SyscallError::ResourceExhausted) + }, + |cap, dir, rel| { + let mut flags = 0; + if how.read { flags |= fs::O_READ; } + if how.write { flags |= fs::O_WRITE; } + if how.append { flags |= fs::O_APPEND; } + if how.create { flags |= fs::O_CREATE; } + if how.create_new { flags |= fs::O_CREATE_NEW; } + if how.truncate { flags |= fs::O_TRUNCATE; } + served_fd(cap, dir.open(rel, flags)?, how.cloexec) + }, + ); + answer(opened.map(|fd| fd as u64)) +} + +#[no_mangle] +unsafe extern "C" fn toyos_fs_stat(path: *const u8, len: usize, follow: bool, out: *mut Stat) -> i64 { + // SAFETY: the caller's. + let path = unsafe { bytes(path, len) }; + let stat = on_path(path, |abs| syscall::stat(abs.as_bytes(), follow), |_, dir, rel| dir.stat(rel, follow).map(stat_of)); + answer(stat.map(|stat| { + // SAFETY: the caller's live place. + unsafe { out.write(stat) }; + 0 + })) +} + +#[no_mangle] +unsafe extern "C" fn toyos_fs_fstat(fd: Fd, out: *mut Stat) -> i64 { + let stat = on_fd(fd, syscall::fstat, |dir, fid, generation| dir.fstat(fid, generation).map(stat_of)); + answer(stat.map(|stat| { + // SAFETY: the caller's live place. + unsafe { out.write(stat) }; + 0 + })) +} + +#[no_mangle] +unsafe extern "C" fn toyos_fs_read(fd: Fd, buf: *mut u8, len: usize) -> i64 { + // SAFETY: the caller lends `len` writable bytes for the call, and only + // one of the two closures runs. + let out = || unsafe { core::slice::from_raw_parts_mut(buf, len) }; + let n = on_fd(fd, |h| syscall::read(h, out()), |dir, fid, generation| dir.read(fid, generation, out())); + answer(n.map(|n| n as u64)) +} + +#[no_mangle] +unsafe extern "C" fn toyos_fs_write(fd: Fd, buf: *const u8, len: usize) -> i64 { + // SAFETY: the caller's. + let buf = unsafe { bytes(buf, len) }; + let n = on_fd(fd, |h| syscall::write(h, buf), |dir, fid, generation| dir.write(fid, generation, buf).map(|w| w.len)); + answer(n.map(|n| n as u64)) +} + +#[no_mangle] +extern "C" fn toyos_fs_seek(fd: Fd, whence: u32, delta: i64) -> i64 { + let (kernel, served) = match whence { + SEEK_START => (SeekFrom::Start(delta as u64), fs::SEEK_START), + SEEK_CURRENT => (SeekFrom::Current(delta), fs::SEEK_CURRENT), + SEEK_END => (SeekFrom::End(delta), fs::SEEK_END), + _ => return encode(Error::Kernel(SyscallError::InvalidArgument)), + }; + answer(on_fd(fd, |h| syscall::seek(h, kernel), |dir, fid, generation| dir.seek(fid, generation, served, delta))) +} + +#[no_mangle] +extern "C" fn toyos_fs_truncate(fd: Fd, size: u64) -> i64 { + answer(on_fd(fd, |h| syscall::ftruncate(h, size), |dir, fid, generation| dir.truncate(fid, generation, size)).map(|()| 0)) +} + +#[no_mangle] +extern "C" fn toyos_fs_fsync(fd: Fd) -> i64 { + answer(on_fd(fd, syscall::fsync, |dir, fid, generation| dir.fsync(fid, generation)).map(|()| 0)) +} + +#[no_mangle] +extern "C" fn toyos_fs_stream(fd: Fd) -> i64 { + let handle = on_fd(fd, syscall::dup, |dir, fid, generation| dir.stream(fid, generation).map(|pipe| pipe.into_raw())); + answer(handle.map(|h| u64::from(h.0))) +} + +/// A call on one path, with nothing to answer but whether it was made. +/// +/// # Safety +/// `path` names `len` readable bytes for the call. +unsafe fn path_call( + path: *const u8, + len: usize, + kernel: impl FnMut(&str) -> Result<(), SyscallError>, + served: impl FnMut(&mut Dir, &str) -> Result<(), Refused>, +) -> i64 { + let mut served = served; + // SAFETY: the caller's. + let path = unsafe { bytes(path, len) }; + answer(on_path(path, kernel, |_, dir, rel| served(dir, rel)).map(|()| 0)) +} + +#[no_mangle] +unsafe extern "C" fn toyos_fs_mkdir(path: *const u8, len: usize) -> i64 { + // SAFETY: the caller's. + unsafe { path_call(path, len, |abs| syscall::mkdir(abs.as_bytes()), |dir, rel| dir.mkdir(rel)) } +} + +#[no_mangle] +unsafe extern "C" fn toyos_fs_rmdir(path: *const u8, len: usize) -> i64 { + // SAFETY: the caller's. + unsafe { path_call(path, len, |abs| syscall::rmdir(abs.as_bytes()), |dir, rel| dir.rmdir(rel)) } +} + +#[no_mangle] +unsafe extern "C" fn toyos_fs_unlink(path: *const u8, len: usize) -> i64 { + // SAFETY: the caller's. + unsafe { path_call(path, len, |abs| syscall::delete(abs.as_bytes()), |dir, rel| dir.unlink(rel)) } +} + +#[no_mangle] +unsafe extern "C" fn toyos_fs_chdir(path: *const u8, len: usize) -> i64 { + // SAFETY: the caller's. + let path = unsafe { bytes(path, len) }; + let changed = utf8(path).and_then(|path| { + let abs = super::absolute(&cwd(), path); + if let Route::Served(..) = route(&abs)? { + let stat = on_path(abs.as_bytes(), |abs| syscall::stat(abs.as_bytes(), true), |_, dir, rel| dir.stat(rel, true).map(stat_of))?; + if stat.file_type != FileType::Directory { + return Err(Error::Served(fs::Error::NotADirectory)); + } + } + syscall::chdir(abs.as_bytes()).map_err(Error::Kernel) + }); + answer(changed.map(|()| 0)) +} + +#[no_mangle] +unsafe extern "C" fn toyos_fs_rename(from: *const u8, from_len: usize, to: *const u8, to_len: usize) -> i64 { + // SAFETY: the caller's. + let (from, to) = unsafe { (bytes(from, from_len), bytes(to, to_len)) }; + let renamed = (|| { + let cwd = cwd(); + let (from, to) = (super::absolute(&cwd, utf8(from)?), super::absolute(&cwd, utf8(to)?)); + match (route(&from)?, route(&to)?) { + (Route::Kernel(a), Route::Kernel(b)) => syscall::rename(a.as_bytes(), b.as_bytes()).map_err(Error::Kernel), + (Route::Served(ca, a), Route::Served(cb, b)) if Arc::ptr_eq(&ca, &cb) => { + ca.dir.lock().rename(&a, &b).map_err(|refused| match refused { + Refused::Error(e) => Error::Served(e), + // A link out of the directory leads to another server. + Refused::Link(_) => Error::CrossesServers, + }) + } + _ => Err(Error::CrossesServers), + } + })(); + answer(renamed.map(|()| 0)) +} + +#[no_mangle] +unsafe extern "C" fn toyos_fs_symlink(target: *const u8, target_len: usize, link: *const u8, link_len: usize) -> i64 { + // SAFETY: the caller's. + let (target, link) = unsafe { (bytes(target, target_len), bytes(link, link_len)) }; + let made = utf8(target).and_then(|target| { + on_path(link, |abs| syscall::symlink(target.as_bytes(), abs.as_bytes()), |_, dir, rel| { + dir.symlink(target, rel).map_err(Refused::Error) + }) + }); + answer(made.map(|()| 0)) +} + +#[no_mangle] +unsafe extern "C" fn toyos_fs_readlink(path: *const u8, len: usize, out: *mut u8, cap: usize) -> i64 { + // SAFETY: the caller's. + let path = unsafe { bytes(path, len) }; + // SAFETY: the caller lends `cap` writable bytes for the call, and each + // closure makes the slice only for its own run. + let target = || unsafe { core::slice::from_raw_parts_mut(out, cap) }; + let n = on_path(path, |abs| syscall::readlink(abs.as_bytes(), target()), |_, dir, rel| { + let mut buf = [0u8; fs::MAX_PATH]; + let n = dir.read_link(rel, &mut buf)?; + let out = target(); + let n = n.min(out.len()); + out[..n].copy_from_slice(&buf[..n]); + Ok(n) + }); + answer(n.map(|n| n as u64)) +} + +/// A listing too large for the first buffer is asked again at the size it +/// named; a directory that outgrows its own listing this many times is +/// refused rather than waited on. +const LIST_ATTEMPTS: usize = 4; + +#[no_mangle] +unsafe extern "C" fn toyos_fs_readdir(path: *const u8, len: usize, out: *mut u8, cap: usize) -> i64 { + // SAFETY: the caller's. + let path = unsafe { bytes(path, len) }; + let listing = on_path( + path, + |abs| { + let mut buf = alloc::vec![0u8; 64 * 1024]; + for _ in 0..LIST_ATTEMPTS { + let n = syscall::readdir(abs.as_bytes(), &mut buf)?; + if n <= buf.len() { + // The kernel's listing, in the served one's encoding. + let (mut out, mut at, mut entry) = (Vec::new(), 0, alloc::vec![0u8; 11 + fs::MAX_PATH]); + while let Some(d) = syscall::dirent(&buf[..n], &mut at) { + let kind = if d.is_dir { fs::KIND_DIR } else { fs::KIND_FILE }; + let name = core::str::from_utf8(d.name).unwrap_or(""); + let n = fs::encode_entry(&mut entry, kind, 0, name).ok_or(SyscallError::InvalidArgument)?; + out.extend_from_slice(&entry[..n]); + } + return Ok(out); + } + buf.resize(n, 0); + } + Err(SyscallError::ResourceExhausted) + }, + |_, dir, rel| { + let mut buf = alloc::vec![0u8; 64 * 1024]; + for _ in 0..LIST_ATTEMPTS { + let n = dir.read_dir(rel, &mut buf)?; + if n <= buf.len() { + buf.truncate(n); + return Ok(buf); + } + buf.resize(n, 0); + } + Err(Refused::Error(fs::Error::LimitExceeded)) + }, + ); + answer(listing.map(|listing| { + if listing.len() <= cap { + // SAFETY: the caller lends `cap` writable bytes for the call. + unsafe { core::ptr::copy_nonoverlapping(listing.as_ptr(), out, listing.len()) }; + } + listing.len() as u64 + })) +} + +#[no_mangle] +unsafe extern "C" fn toyos_fs_is_served(path: *const u8, len: usize) -> i64 { + // SAFETY: the caller's. + let path = unsafe { bytes(path, len) }; + let served = utf8(path).and_then(|path| route(&super::absolute(&cwd(), path))); + answer(served.map(|route| u64::from(matches!(route, Route::Served(..))))) +} diff --git a/sdk/toyos/src/fs.rs b/sdk/toyos/src/fs.rs index 86c7f3f0a0c..dc0f7f86e34 100644 --- a/sdk/toyos/src/fs.rs +++ b/sdk/toyos/src/fs.rs @@ -167,6 +167,21 @@ macro_rules! errors { } } + /// The word [`Self::byte`] answered, the client's own among them: + /// how one crosses `crate::files`' C interface. + pub const fn from_byte(byte: u8) -> Option { + match byte { + $($code => Some(Self::$name),)* + $($lcode => Some(Self::$lname),)* + _ => None, + } + } + + /// This word's discriminant, the client's own words' too. + pub const fn byte(self) -> u8 { + self as u8 + } + #[cfg(test)] const WIRE: &[Error] = &[$(Error::$name,)*]; #[cfg(test)] diff --git a/sdk/toyos/src/lib.rs b/sdk/toyos/src/lib.rs index d89a8795a82..18e5a2a5cfa 100644 --- a/sdk/toyos/src/lib.rs +++ b/sdk/toyos/src/lib.rs @@ -12,11 +12,14 @@ // directory is the only build that is not, and it links no syscall. #![cfg_attr(not(test), no_std)] +extern crate alloc; + pub mod audio; pub mod census; pub mod device; pub mod endow; pub mod fd; +pub mod files; pub mod fs; pub mod poller; pub mod power; diff --git a/sdk/toyos/src/log/stdio.rs b/sdk/toyos/src/log/stdio.rs index fe88d9fb8a5..25b3db81868 100644 --- a/sdk/toyos/src/log/stdio.rs +++ b/sdk/toyos/src/log/stdio.rs @@ -228,7 +228,10 @@ mod target { /// sentence the refusal panics with. fn ask(stream: Stream) -> Result { use why::Why; - let Ok(stat) = crate::fd::with(stream as i32, |o| syscall::fstat(o.handle)) else { return Ok(DESCRIPTOR) }; + // A descriptor that is not open, or names a file a server holds, is + // written as a descriptor. + let asked = crate::fd::with(stream as i32, |o| o.release.is_none().then(|| syscall::fstat(o.handle))); + let Ok(Some(stat)) = asked else { return Ok(DESCRIPTOR) }; let stat = stat.map_err(Why::Fstat)?; if stat.file_type != FileType::SharedMemory { return Ok(DESCRIPTOR); @@ -314,9 +317,12 @@ mod target { /// `buf` onto a stream: whole lines to its ring, or every byte to its /// descriptor, and none anywhere for a descriptor that is not open, as /// std on unix drops a closed stdout's bytes. - pub fn write(stream: Stream, buf: &[u8]) -> Result { + pub fn write(stream: Stream, buf: &[u8]) -> Result { match sink(stream) { - Sink::Descriptor(fd) => crate::fd::with(fd, |o| syscall::write(o.handle, buf)).unwrap_or(Ok(buf.len())), + Sink::Descriptor(fd) => match crate::files::write(fd, buf) { + Err(crate::files::Error::Fd(crate::fd::Error::BadFd)) => Ok(buf.len()), + written => written, + }, Sink::Ring(ring) => { PENDING[index(stream)].with(|composer| composer.feed(buf, &mut push(&ring))); Ok(buf.len()) diff --git a/sdk/toyos/src/sync.rs b/sdk/toyos/src/sync.rs index 76ec06d2328..fc3c106bb1d 100644 --- a/sdk/toyos/src/sync.rs +++ b/sdk/toyos/src/sync.rs @@ -1,8 +1,11 @@ -//! A lock over the kernel's futex: one `u32`, 0 free, 1 held, 2 held with -//! waiters, as Drepper's "Futexes Are Tricky" (2011) builds its mutex in -//! §"Mutex, Take 2". A holder that finds 2 at its release wakes one waiter, -//! and a waiter sleeps only while the word still says 2. It allocates nothing, -//! so a `static` can hold one. +//! Locks over the kernel's futex, which allocate nothing, so a `static` can +//! hold one. +//! +//! [`Lock`] is one `u32`, 0 free, 1 held, 2 held with waiters, as Drepper's +//! "Futexes Are Tricky" (2011) builds its mutex in §"Mutex, Take 2". A holder +//! that finds 2 at its release wakes one waiter, and a waiter sleeps only while +//! the word still says 2. [`Turnstile`] serves its holders in the order they +//! asked. use core::cell::UnsafeCell; use core::sync::atomic::{AtomicU32, Ordering}; @@ -54,6 +57,95 @@ impl Drop for Held<'_, T> { } } +/// How a [`Turnstile`]'s waiters sleep on and wake from its word. +pub trait Sleep { + /// Sleep while `word` still says `expected`; a spurious return is allowed. + fn wait(word: &AtomicU32, expected: u32); + /// Wake every sleeper on `word`. + fn wake_all(word: &AtomicU32); +} + +/// The kernel's futex. +pub struct Futex; + +impl Sleep for Futex { + fn wait(word: &AtomicU32, expected: u32) { + // SAFETY: `word` is a live, aligned u32. + unsafe { syscall::futex_wait(word.as_ptr(), expected, None) }; + } + fn wake_all(word: &AtomicU32) { + // SAFETY: `word` is a live, aligned u32. + unsafe { syscall::futex_wake(word.as_ptr(), u32::MAX) }; + } +} + +/// `T` held in turn: each [`Turnstile::lock`] takes a ticket, and the value +/// goes to the tickets in the order they were taken. A plain lock lets the +/// thread that let go take it straight back ahead of the one it woke, so +/// threads sharing one connection would not be served in turn. +pub struct Turnstile { + /// The next ticket handed out. + next: AtomicU32, + /// The ticket whose turn it is, which waiters sleep on. + serving: AtomicU32, + value: UnsafeCell, + sleep: core::marker::PhantomData, +} + +// SAFETY: the value is reached only through a `Turn`, which only the ticket +// being served holds. +unsafe impl Sync for Turnstile {} + +/// The value, held for one turn. +pub struct Turn<'a, T, S: Sleep = Futex>(&'a Turnstile); + +impl Turnstile { + pub const fn new(value: T) -> Self { + Self { + next: AtomicU32::new(0), + serving: AtomicU32::new(0), + value: UnsafeCell::new(value), + sleep: core::marker::PhantomData, + } + } + + /// The value, once every ticket taken before this one has had its turn. + pub fn lock(&self) -> Turn<'_, T, S> { + let ticket = self.next.fetch_add(1, Ordering::Relaxed); + loop { + let serving = self.serving.load(Ordering::Acquire); + if serving == ticket { + return Turn(self); + } + S::wait(&self.serving, serving); + } + } +} + +impl core::ops::Deref for Turn<'_, T, S> { + type Target = T; + fn deref(&self) -> &T { + // SAFETY: this ticket is the one being served. + unsafe { &*self.0.value.get() } + } +} + +impl core::ops::DerefMut for Turn<'_, T, S> { + fn deref_mut(&mut self) -> &mut T { + // SAFETY: this ticket is the one being served. + unsafe { &mut *self.0.value.get() } + } +} + +impl Drop for Turn<'_, T, S> { + fn drop(&mut self) { + self.0.serving.fetch_add(1, Ordering::Release); + // Every waiter, since only the next ticket's may go on and only it + // knows it is the next. + S::wake_all(&self.0.serving); + } +} + /// Takes the lock `state` is the word of, sleeping while another holds it. pub fn lock_word(state: &AtomicU32) { if state.compare_exchange(0, 1, Ordering::Acquire, Ordering::Relaxed).is_ok() { @@ -72,3 +164,52 @@ pub fn unlock_word(state: &AtomicU32) { unsafe { syscall::futex_wake(state.as_ptr(), 1) }; } } + +#[cfg(test)] +mod tests { + use std::sync::Arc; + use std::time::{Duration, Instant}; + use std::vec::Vec; + + use super::*; + + /// A host's stand-in for the futex: a wait yields once. + struct Yield; + + impl Sleep for Yield { + fn wait(_: &AtomicU32, _: u32) { + std::thread::yield_now(); + } + fn wake_all(_: &AtomicU32) {} + } + + /// A thread that lets the value go and asks for it again waits behind + /// the threads that asked first, in the order they asked. + #[test] + fn the_value_goes_to_the_tickets_in_the_order_they_were_taken() { + let stile: Arc, Yield>> = Arc::new(Turnstile::new(Vec::new())); + let mut held = stile.lock(); + held.push("first"); + let queued: Vec<_> = ["second", "third"] + .into_iter() + .enumerate() + .map(|(i, name)| { + let theirs = Arc::clone(&stile); + let thread = std::thread::spawn(move || theirs.lock().push(name)); + // The next waits for this one's ticket, so the two are taken in order. + let asked = Instant::now(); + while stile.next.load(Ordering::Relaxed) < i as u32 + 2 { + assert!(asked.elapsed() < Duration::from_secs(10), "{name} never took a ticket"); + std::thread::yield_now(); + } + thread + }) + .collect(); + drop(held); + stile.lock().push("first again"); + for thread in queued { + thread.join().unwrap(); + } + assert_eq!(*stile.lock(), ["first", "second", "third", "first again"]); + } +} diff --git a/tests/corpus/tinycc/211_home_files.c b/tests/corpus/tinycc/211_home_files.c new file mode 100644 index 00000000000..4c1cea621e5 --- /dev/null +++ b/tests/corpus/tinycc/211_home_files.c @@ -0,0 +1,89 @@ +/* A directory a file server holds, `/home`, through the C library as a C + program reaches it: a file is made, written, read back at an offset, + appended to through stdio, linked, resolved, renamed, listed and removed, + and each refusal is POSIX's errno. */ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define DIR_PATH "/home/211_files" + +static void judge(const char *what, int is) { + printf("%s: %s\n", what, is ? "yes" : "NO"); +} + +/* The `d_type` `DIR_PATH`'s listing answers for `name`, -1 when it lists none. */ +static int listed(const char *name) { + DIR *d = opendir(DIR_PATH); + struct dirent *e; + int type = -1; + while (d && (e = readdir(d))) + if (strcmp(e->d_name, name) == 0) + type = e->d_type; + if (d) + closedir(d); + return type; +} + +int main(void) { + char buf[32], real[PATH_MAX]; + struct stat st; + int fd, n; + + /* What a run before this one left. */ + unlink(DIR_PATH "/f"); + unlink(DIR_PATH "/g"); + unlink(DIR_PATH "/l"); + rmdir(DIR_PATH); + + judge("mkdir makes the directory", mkdir(DIR_PATH, 0755) == 0); + fd = open(DIR_PATH "/f", O_WRONLY | O_CREAT | O_EXCL, 0644); + judge("open makes the file", fd >= 0); + judge("write writes it", write(fd, "served\n", 7) == 7); + judge("close closes it", close(fd) == 0); + judge("O_EXCL refuses it again with EEXIST", + open(DIR_PATH "/f", O_WRONLY | O_CREAT | O_EXCL, 0644) == -1 && errno == EEXIST); + + fd = open(DIR_PATH "/f", O_RDONLY); + n = read(fd, buf, sizeof buf); + judge("read reads it back", n == 7 && memcmp(buf, "served\n", 7) == 0); + judge("lseek moves the offset", lseek(fd, 1, SEEK_SET) == 1); + n = read(fd, buf, sizeof buf); + judge("read reads from the offset", n == 6 && memcmp(buf, "erved\n", 6) == 0); + judge("fstat answers a regular file and its size", fstat(fd, &st) == 0 && S_ISREG(st.st_mode) && st.st_size == 7); + judge("isatty answers no, with ENOTTY", isatty(fd) == 0 && errno == ENOTTY); + judge("write to a file open to read is EBADF", write(fd, "x", 1) == -1 && errno == EBADF); + close(fd); + + FILE *f = fopen(DIR_PATH "/f", "a"); + judge("fopen appends", f && fputs("more\n", f) >= 0 && fclose(f) == 0); + judge("stat answers the appended size", stat(DIR_PATH "/f", &st) == 0 && st.st_size == 12); + + judge("symlink makes a link", symlink("f", DIR_PATH "/l") == 0); + judge("symlink refuses a name that exists with EEXIST", symlink("f", DIR_PATH "/l") == -1 && errno == EEXIST); + n = readlink(DIR_PATH "/l", buf, sizeof buf); + judge("readlink answers its target", n == 1 && buf[0] == 'f'); + judge("readlink of a file is EINVAL", readlink(DIR_PATH "/f", buf, sizeof buf) == -1 && errno == EINVAL); + judge("lstat answers the link", lstat(DIR_PATH "/l", &st) == 0 && S_ISLNK(st.st_mode)); + judge("stat follows it", stat(DIR_PATH "/l", &st) == 0 && S_ISREG(st.st_mode) && st.st_size == 12); + judge("realpath resolves it", realpath(DIR_PATH "/./l", real) && strcmp(real, DIR_PATH "/f") == 0); + + judge("chdir enters the directory", chdir(DIR_PATH) == 0); + judge("rename renames by a relative path", rename("f", "g") == 0); + judge("the old name is gone with ENOENT", stat(DIR_PATH "/f", &st) == -1 && errno == ENOENT); + judge("the listing has the file", listed("g") == DT_REG); + judge("the listing has the link", listed("l") == DT_LNK); + judge("rmdir of a full directory is ENOTEMPTY", rmdir(DIR_PATH) == -1 && errno == ENOTEMPTY); + judge("chdir leaves it", chdir("/tmp") == 0); + + judge("unlink removes the file and the link", unlink(DIR_PATH "/g") == 0 && unlink(DIR_PATH "/l") == 0); + judge("rmdir removes the directory", rmdir(DIR_PATH) == 0); + judge("it is gone with ENOENT", stat(DIR_PATH, &st) == -1 && errno == ENOENT); + return 0; +} diff --git a/tests/corpus/tinycc/211_home_files.expect b/tests/corpus/tinycc/211_home_files.expect new file mode 100644 index 00000000000..31fbc2cd939 --- /dev/null +++ b/tests/corpus/tinycc/211_home_files.expect @@ -0,0 +1,30 @@ +mkdir makes the directory: yes +open makes the file: yes +write writes it: yes +close closes it: yes +O_EXCL refuses it again with EEXIST: yes +read reads it back: yes +lseek moves the offset: yes +read reads from the offset: yes +fstat answers a regular file and its size: yes +isatty answers no, with ENOTTY: yes +write to a file open to read is EBADF: yes +fopen appends: yes +stat answers the appended size: yes +symlink makes a link: yes +symlink refuses a name that exists with EEXIST: yes +readlink answers its target: yes +readlink of a file is EINVAL: yes +lstat answers the link: yes +stat follows it: yes +realpath resolves it: yes +chdir enters the directory: yes +rename renames by a relative path: yes +the old name is gone with ENOENT: yes +the listing has the file: yes +the listing has the link: yes +rmdir of a full directory is ENOTEMPTY: yes +chdir leaves it: yes +unlink removes the file and the link: yes +rmdir removes the directory: yes +it is gone with ENOENT: yes From 82070a7a7d5e4c90d79aea3c81b54b2060f45f9a Mon Sep 17 00:00:00 2001 From: japabu Date: Wed, 21 Oct 2026 21:53:39 +0200 Subject: [PATCH 2/4] corpus: 206 expects F_DUPFD_CLOEXEC to work, as #978 made it lowest free descriptor at or above the argument, marked close-on-exec, as POSIX has it. 206_libc_refusals still expected the old refusal (-1, ENOSYS), and as a metal-only corpus case nothing ran it before the T14 boot of #988, which answered 10. The record of what libc refuses loses the same stale lines, and F_DUPFD and F_GETFD's departures with them: both are POSIX's now. Co-Authored-By: Claude Opus 5.5 --- tests/corpus/tinycc/206_libc_refusals.expect | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/corpus/tinycc/206_libc_refusals.expect b/tests/corpus/tinycc/206_libc_refusals.expect index 97d52e8bb88..efa3ce6eb24 100644 --- a/tests/corpus/tinycc/206_libc_refusals.expect +++ b/tests/corpus/tinycc/206_libc_refusals.expect @@ -29,7 +29,7 @@ open O_CLOEXEC; F_GETFD: 1 dup2 onto it: answered it; F_GETFD: 0 fcntl F_DUPFD -1: -1, EINVAL fcntl F_DUPFD 4096: -1, EINVAL -fcntl F_DUPFD_CLOEXEC: -1, ENOSYS +fcntl F_DUPFD_CLOEXEC: 10, no errno fcntl F_GETFL: -1, ENOSYS fcntl F_SETFL: -1, ENOSYS fcntl 12345: -1, EINVAL From d66423c7d20f815b20fd38e73d1937cbaddfb97d Mon Sep 17 00:00:00 2001 From: japabu Date: Wed, 21 Oct 2026 22:12:33 +0200 Subject: [PATCH 3/4] files: a full table refuses an open, realpath resolves a name at a time Review r1 of #988. An open of a served file installed its descriptor on the capability's turn. On a full table the table let the new open file go, whose close asks the server on that same turn, so the thread waited on itself and every later user of the directory behind it. The open now hands back what it opened and installs it once the turn is let go, and a full table answers the table's own refusal, EMFILE in C and QuotaExceeded in std, on /tmp and on a served path alike; the kernel arm answered EINVAL and the served one ENFILE. A corpus case fills the table and opens both. libc's realpath resolves the names a file server holds as POSIX does, one at a time: a link before the last name is followed, `..` after one is the parent of where it led, and a relative one is read against its own directory. At the first name the kernel holds it hands the rest to the kernel's resolver, so a served link to /tmp or /system resolves there, where it answered ENOENT for a file that exists. The turnstile's sleep is private, a futex, and a yield in a host test, rather than a public trait and type parameter that existed for the test. A descriptor that names no kernel handle is refused a child with EOPNOTSUPP (std's Unsupported), as an addopen of a served path is; a kernel NotSupported is EOPNOTSUPP in C. A served refusal of a name the volume cannot store is EINVAL. std's stdin reads whoever serves it. Issue records this made stale are brought up to date: - a served file's raw fd is retired; - the served flush issue is now about fsync, since flush is a no-op as everywhere else; - the kernel's non-exclusive create names C's O_EXCL too; - /tmp's symlink is left with the remaining exit only; - two records and grants.rs point at toyos::files; - a served file at stdio stopping every spawn is recorded; - the corpus licence counts the four new files. Co-Authored-By: Claude Opus 5.5 --- ...oss-a-file-servers-restart-answers-gone.md | 6 +- ...el-mount-to-a-served-path-leads-nowhere.md | 2 +- ...-served-file-at-stdio-stops-every-spawn.md | 21 ++++ ...erved-file-panics-when-asked-its-raw-fd.md | 20 ---- ...mp-displaces-its-name-and-lists-nowhere.md | 20 ++-- ...of-a-served-file-syncs-its-whole-volume.md | 23 ++++ ...e-new-on-a-kernel-path-is-not-exclusive.md | 16 +-- ...of-a-served-file-syncs-its-whole-volume.md | 21 ---- ...nel-resolves-a-path-by-rules-of-its-own.md | 8 +- lib/manifest/src/grants.rs | 4 +- sdk/libc/arch/src/path_resolution.rs | 107 ++++++++++++------ sdk/libc/src/errcode.rs | 4 +- sdk/libc/src/posix_io.rs | 38 ++++--- sdk/libc/src/resolve.rs | 99 ++++++++++++---- sdk/std/sys/pal/mod.rs | 14 +-- sdk/std/sys/stdio.rs | 13 +-- sdk/toyos/src/fd.rs | 2 +- sdk/toyos/src/files/owner.rs | 36 ++++-- sdk/toyos/src/sync.rs | 69 +++++------ tests/corpus/tinycc/212_full_table.c | 39 +++++++ tests/corpus/tinycc/212_full_table.expect | 5 + tests/corpus/tinycc/LICENSE | 4 +- 22 files changed, 364 insertions(+), 207 deletions(-) create mode 100644 issues/a-served-file-at-stdio-stops-every-spawn.md delete mode 100644 issues/a-served-file-panics-when-asked-its-raw-fd.md create mode 100644 issues/an-fsync-of-a-served-file-syncs-its-whole-volume.md delete mode 100644 issues/every-flush-of-a-served-file-syncs-its-whole-volume.md create mode 100644 tests/corpus/tinycc/212_full_table.c create mode 100644 tests/corpus/tinycc/212_full_table.expect diff --git a/issues/a-file-held-across-a-file-servers-restart-answers-gone.md b/issues/a-file-held-across-a-file-servers-restart-answers-gone.md index 7a71059a866..d1232d748f9 100644 --- a/issues/a-file-held-across-a-file-servers-restart-answers-gone.md +++ b/issues/a-file-held-across-a-file-servers-restart-answers-gone.md @@ -7,9 +7,9 @@ opened: 2026-09-27 # A file held across a file server's restart answers Gone No volume records an object id, so nothing tells the held file from another -put at its path since, and a handle held across a restart answers `Gone` -(`std`'s `sdk/std/sys/fs.rs`); `logd` loses `/log` for the boot if LOG's server -restarts. +put at its path since, and a file held across a restart answers `Gone` +(`toyos::files`, so std's `File` and a C descriptor alike); `logd` loses +`/log` for the boot if LOG's server restarts. **Exit**: DATA's entry carries an object id and a generation that nothing reuses, a held handle reopens only when both match, and a host test that diff --git a/issues/a-link-on-a-kernel-mount-to-a-served-path-leads-nowhere.md b/issues/a-link-on-a-kernel-mount-to-a-served-path-leads-nowhere.md index fc3b7ee1c81..67bdc775b6c 100644 --- a/issues/a-link-on-a-kernel-mount-to-a-served-path-leads-nowhere.md +++ b/issues/a-link-on-a-kernel-mount-to-a-served-path-leads-nowhere.md @@ -10,7 +10,7 @@ The kernel resolves a symlink on its own mounts — ROOT and `/tmp` — in its o tree (`kernel/src/vfs.rs`), and its tree holds nothing under `/apps`, `/config`, `/home`, `/state`, `/log` or `/boot` but ROOT's empty directories: those are file servers', reached through a directory capability -(`sdk/std/sys/fs.rs`). So a link in `/tmp` whose target is +(`toyos::files`, which std and libc share). So a link in `/tmp` whose target is `/home/toy/notes` opens nothing (`NotFound`), where the same link on a served directory is followed — a file server hands an absolute target back to the client, which resolves it in its own table. diff --git a/issues/a-served-file-at-stdio-stops-every-spawn.md b/issues/a-served-file-at-stdio-stops-every-spawn.md new file mode 100644 index 00000000000..04b0e81b4cd --- /dev/null +++ b/issues/a-served-file-at-stdio-stops-every-spawn.md @@ -0,0 +1,21 @@ +--- +status: open +kind: defect +opened: 2026-10-21 +--- + +# A served file at stdio stops every spawn + +A file a file server holds is a descriptor naming an object of +`toyos::files`', not a kernel handle, and `toyos::fd::copy` refuses one for a +child (`EOPNOTSUPP`, std's `Unsupported`). libc's `posix_spawn` asks for +descriptors 0 to 2 on every spawn (`spawnreq::plan`), so once a C program has +put a served file there, by `dup2` or `freopen`, every `posix_spawn`, +`system` and `popen` it makes is refused. So is an `adddup2` of a served +descriptor and an `addopen` of a served path, and std's `Command` with +inherited stdio in the same case. Before served files were descriptors, no +served file could sit at 0 to 2. Read from the code; no guest case has run it. + +**Exit**: a served file can be given to a child, as PR 4 of the descriptor +work gives served kinds (`EXPORT`), and a guest C case redirects stdout to a +file under `/home`, runs `system`, and reads the child's output back from it. diff --git a/issues/a-served-file-panics-when-asked-its-raw-fd.md b/issues/a-served-file-panics-when-asked-its-raw-fd.md deleted file mode 100644 index 50155440e3f..00000000000 --- a/issues/a-served-file-panics-when-asked-its-raw-fd.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-09-27 ---- - -# A served file panics when asked its raw fd - -`std::os::toyos::io::AsRawFd` for `std::fs::File` calls the backend's -`File::as_raw_fd` (`sdk/std/sys/fs.rs`), which panics with -"a file on a file server has no descriptor" for every file under `/apps`, -`/config`, `/home`, `/state`, `/log` and `/boot`. A crate that takes a file's -fd — to lock it, map it or hand it to a C library — builds for ToyOS and -panics there, which is what "existing Rust just works" rules out. - -Owner: std's ToyOS file layer. - -**Exit**: `as_raw_fd` on a served file answers without a panic — a -descriptor whose object reaches the file's server, as `as_child_stdio`'s pipe does for a -child's writes — with a guest test that asks it of a file under `/home`. diff --git a/issues/a-symbolic-link-on-tmp-displaces-its-name-and-lists-nowhere.md b/issues/a-symbolic-link-on-tmp-displaces-its-name-and-lists-nowhere.md index c26330c17da..a614807125b 100644 --- a/issues/a-symbolic-link-on-tmp-displaces-its-name-and-lists-nowhere.md +++ b/issues/a-symbolic-link-on-tmp-displaces-its-name-and-lists-nowhere.md @@ -8,16 +8,18 @@ opened: 2026-10-01 Read from the code, not run. The kernel's `/tmp` (`kernel/src/tmpfs.rs`) makes a link over whatever file its name held, where POSIX's `symlink` refuses -a name that exists `EEXIST`, so libc's `symlink` refuses `ENOSYS` -(`sdk/libc/src/refused.rs`). Its `list` answers files alone, so no -`readdir` of a directory shows a link it holds. +a name that exists `EEXIST`, so libc's `symlink` refuses `ENOSYS` for a link +the kernel would hold (`sdk/libc/src/posix_io.rs`). Its `list` answers files +alone, so no `readdir` of a directory shows a link it holds. -**Exit**: `symlink` is the file server's, not the kernel's. libc's `symlink` -sends the request to the server of the directory that is to hold the link, as -std's does (`on_path` in `sdk/std/sys/fs.rs`), with nothing -asked first, and answers its `AlreadyExists` `EEXIST`. The file server makes the link in -the one request that refuses a name that exists, and libc's `readdir` of that -directory names the link, each asserted by a test. The kernel's `SYS_SYMLINK` +libc's `symlink` already sends a link a file server is to hold to that +server, through `toyos::files` as std's does, which refuses a name that +exists, answered `EEXIST`, and `readdir` there names the link; corpus case +`211_home_files` asserts both under `/home`. What is left is `/tmp`. + +**Exit**: `/tmp` is a file server's, so every `symlink` is one, sent with +nothing asked first and answered `EEXIST` for a name that exists, and libc's +`readdir` of `/tmp` names the link, each asserted by a test. The kernel's `SYS_SYMLINK` and its symlink code (`kernel/src/vfs.rs`, `kernel/src/tmpfs.rs`) go wherever nothing calls them. That `EEXIST` is how LLVM's `LockFileManager` takes its lock (`create_link`, `::symlink` in `llvm/lib/Support/Unix/Path.inc`). An LLVM diff --git a/issues/an-fsync-of-a-served-file-syncs-its-whole-volume.md b/issues/an-fsync-of-a-served-file-syncs-its-whole-volume.md new file mode 100644 index 00000000000..c0258be03b1 --- /dev/null +++ b/issues/an-fsync-of-a-served-file-syncs-its-whole-volume.md @@ -0,0 +1,23 @@ +--- +status: open +kind: defect +opened: 2026-09-27 +--- + +# An fsync of a served file syncs its whole volume + +The file server answers `FSYNC` by syncing the volume, every open file's +entry and every dirty block of the cache (`system/fileserver/src/main.rs`, +`FSYNC`, which calls `sync` with the file's node and syncs the whole volume). So an +`fsync` or `File::sync_all` of one file on `/home` syncs all of DATA, and a +4 KiB create and fsync cost 3.5–3.6 ms p50 on QEMU TCG against the kernel's +1.0–1.3 ms, measured on the branch that moved DATA to the file server. + +std's ToyOS `File::flush` is a no-op, as it is on every other platform: a +`File` buffers nothing, since each `write` is the server's when it answers +(`toyos::files`), so a flush has nothing to hand on and `Write::flush` asks +for no durability. It used to be `fsync`, which made every `BufWriter` flush +sync the volume. + +**Exit**: `FSYNC` makes durable the file it names and what that file's entry +depends on, with a guest measurement of one file's fsync beside the volume's. diff --git a/issues/create-new-on-a-kernel-path-is-not-exclusive.md b/issues/create-new-on-a-kernel-path-is-not-exclusive.md index 161885cde16..0bf73d7d1ed 100644 --- a/issues/create-new-on-a-kernel-path-is-not-exclusive.md +++ b/issues/create-new-on-a-kernel-path-is-not-exclusive.md @@ -6,15 +6,17 @@ opened: 2026-09-27 # `create_new` on a kernel path is not exclusive -std's `OpenOptions::create_new(true)` promises an open that fails with -`AlreadyExists` when the file is there. std's ToyOS `to_flags` -(`sdk/std/sys/fs.rs`) turns `create_new` into the kernel's +std's `OpenOptions::create_new(true)` and C's `open` with +`O_CREAT|O_EXCL` promise an open that fails when the file is there +(`AlreadyExists`, `EEXIST`). Both reach `toyos_fs_open`'s kernel arm +(`sdk/toyos/src/files/owner.rs`), which turns `create_new` into the kernel's plain `OpenFlags::CREATE`, and the kernel has no exclusive flag to turn it -into, so on `/tmp` a second `create_new` of one path opens the file the first -made and says nothing. A served path does not share it: the file protocol -carries `O_CREATE_NEW` and the file server refuses. Found when fsd's test actuator took two +into. So on `/tmp` a second exclusive create of one path opens the file the +first made and says nothing, and a C lock file made that way locks nothing. A +served path does not share it: the file protocol carries `O_CREATE_NEW` and +the file server refuses. Found when the file server's test actuator took two `create_new`s of one `/tmp` mark from two processes as two first times. **Exit**: an exclusive create reaches the kernel's `/tmp` and is refused `AlreadyExists` there, with a guest test that makes one path twice with -`create_new` and has the second refused. +`create_new` and with `O_CREAT|O_EXCL` and has each second one refused. diff --git a/issues/every-flush-of-a-served-file-syncs-its-whole-volume.md b/issues/every-flush-of-a-served-file-syncs-its-whole-volume.md deleted file mode 100644 index 236c6973e4e..00000000000 --- a/issues/every-flush-of-a-served-file-syncs-its-whole-volume.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-09-27 ---- - -# Every flush of a served file syncs its whole volume - -std's ToyOS `File::flush` is `File::fsync` -(`sdk/std/sys/fs.rs`), where every other platform's is a -no-op, and the file server answers `FSYNC` by syncing the volume, every open -file's entry and every dirty block of the cache (`system/fileserver/src/main.rs`, -`FSYNC`, which calls `sync` with the file's node and syncs the whole volume). So a -`BufWriter` over a file on `/home` syncs all of DATA each time it flushes, and a -4 KiB create and fsync cost 3.5–3.6 ms p50 on QEMU TCG against the kernel's -1.0–1.3 ms, measured on the branch that moved DATA to fsd. - -**Exit**: `File::flush` hands the server what is buffered and makes nothing -durable, as std's contract for `Write::flush` is, and `FSYNC` makes durable the -file it names and what that file's entry depends on, with a guest measurement -of a `BufWriter` flush beside an fsync. diff --git a/issues/the-kernel-resolves-a-path-by-rules-of-its-own.md b/issues/the-kernel-resolves-a-path-by-rules-of-its-own.md index 6af35d3d637..0b569211a12 100644 --- a/issues/the-kernel-resolves-a-path-by-rules-of-its-own.md +++ b/issues/the-kernel-resolves-a-path-by-rules-of-its-own.md @@ -26,9 +26,11 @@ A path on the kernel's own mounts, ROOT and `/tmp`, is resolved by A file server resolves its own paths POSIX's way, with a bound of its own (`system/fileserver/src/resolve.rs`, `MAX_LINKS`), so the same link reads differently on `/tmp` and on a served directory. libc's `realpath` -(`sdk/libc/src/resolve.rs`) answers by the kernel's rules, so that a -program that canonicalises a path and then opens it opens the same file; -`sdk/libc/arch/src/path_resolution.rs` pins those rules. +(`sdk/libc/src/resolve.rs`, `resolve_path`) resolves the names a file server +holds POSIX's way and hands the kernel the rest at the first name it holds, +answering that part by the kernel's rules, so that a program that +canonicalises a path and then opens it opens the same file; +`sdk/libc/arch/src/path_resolution.rs` pins both. **Owner**: the kernel VFS, `kernel/src/vfs.rs`. diff --git a/lib/manifest/src/grants.rs b/lib/manifest/src/grants.rs index ef3c77f29c9..9b4a7e68923 100644 --- a/lib/manifest/src/grants.rs +++ b/lib/manifest/src/grants.rs @@ -56,8 +56,8 @@ pub const MAX_STORE_BYTES: usize = HEADER.len() + 1 + MAX_ENTRIES * MAX_LINE; /// `toyos::fs::MAX_GRANT_ROOT` leaves, which the supervisor asserts. pub const MAX_ROOT: usize = 54; -/// The deepest folder, in components: std finds a capability by probing at -/// most this many (`MAX_CAPABILITY_DEPTH` in `sdk/std/sys/fs.rs`). +/// The deepest folder, in components: std and libc find a capability by +/// probing at most this many (`toyos::files::MAX_CAPABILITY_DEPTH`). pub const MAX_DEPTH: usize = 4; const MAX_LINE: usize = "grant ".len() + crate::USER.len() + 1 + MAX_PROGRAM_NAME + 1 + DIGEST_LEN + 1 + 11 + 1 + MAX_ROOT + 2; diff --git a/sdk/libc/arch/src/path_resolution.rs b/sdk/libc/arch/src/path_resolution.rs index 6b7b4e19a87..33d3880d0ec 100644 --- a/sdk/libc/arch/src/path_resolution.rs +++ b/sdk/libc/arch/src/path_resolution.rs @@ -9,7 +9,7 @@ use toyos_abi::syscall::{SyscallError, MAX_LINKS, MAX_PATH}; use crate::errcode::{EIO, ELOOP, ENAMETOOLONG, ENOENT, ENOTDIR}; use crate::header::{self, LIMITS_H}; -use crate::resolve::{resolve, resolve_served, Kernel}; +use crate::resolve::{resolve, resolve_path, Kernel, Name, Served}; #[derive(Clone, Copy)] enum Node { @@ -198,44 +198,85 @@ fn path_max_is_limits_h_s() { assert_eq!(MAX_PATH, header::int(LIMITS_H, "PATH_MAX") as usize); } -/// [`resolve_served`] over `tree`: a link's target, or `None`, and `ENOENT` -/// for a name that is not there. -fn served(tree: &Tree, abs: &str) -> Result { - let read_link = |at: &[u8]| match tree.at(at) { - Some(Node::Link(target)) => Ok(Some(target.as_bytes().to_vec())), - Some(_) => Ok(None), - None => Err(ENOENT), - }; - let exists = |at: &[u8]| tree.at(at).map(drop).ok_or(ENOENT); - resolve_served(abs.as_bytes(), read_link, exists).map(|at| String::from_utf8(at).expect("UTF-8")) +/// A file server holding `/home`, its names in `home`, beside the kernel's +/// [`tree`]: as `toyos::files` answers, a name is its link's target or +/// nothing, one that is not there `ENOENT`, and one through a file `ENOTDIR`. +struct Home(Tree); + +impl Served for Home { + fn holds(&mut self, at: &[u8]) -> bool { + at == b"/home" || at.starts_with(b"/home/") + } + + fn name(&mut self, at: &[u8]) -> Result { + let parent = &at[..at.iter().rposition(|&b| b == b'/').unwrap()]; + if parent.len() > b"/home".len() && matches!(self.0.at(parent), Some(Node::File)) { + return Err(ENOTDIR); + } + match self.0.at(at) { + Some(Node::Link(target)) => Ok(Name::Link(target.as_bytes().to_vec())), + Some(_) => Ok(Name::Other), + None if at == b"/home" => Ok(Name::Other), + None => Err(ENOENT), + } + } } -/// A served path's last link is read against the link's own directory, as -/// POSIX has it, and not against a mount as the kernel's is; an absolute one -/// from the root. -#[test] -fn a_served_link_is_read_against_its_own_directory() { - let tree = Tree::new(&[ +fn home() -> Home { + Home(Tree::new(&[ + ("/home/u", Node::Dir), ("/home/u/a", Node::Dir), + ("/home/u/a/b", Node::Dir), ("/home/u/a/file", Node::File), - ("/home/u/a/rel", Node::Link("file")), - ("/home/u/up", Node::Link("a/../a/rel")), + ("/home/u/a/f", Node::File), + ("/home/u/rel", Node::Link("a/file")), ("/home/u/abs", Node::Link("/home/u/a/file")), - ]); - assert_eq!(served(&tree, "/home/u/a/rel"), Ok(String::from("/home/u/a/file"))); - assert_eq!(served(&tree, "/home/u/up"), Ok(String::from("/home/u/a/file")), "a chain of two"); - assert_eq!(served(&tree, "/home/u/abs"), Ok(String::from("/home/u/a/file"))); - assert_eq!(served(&tree, "/home/u/./a//file"), Ok(String::from("/home/u/a/file")), "lexically clean"); -} - -#[test] -fn a_served_loop_is_eloop_and_a_dangling_link_enoent() { - let tree = Tree::new(&[ - ("/home/u", Node::Dir), + ("/home/u/d", Node::Link("a")), + ("/home/u/deep", Node::Link("a/b")), + ("/home/u/out", Node::Link("/tmp/d/f")), + ("/home/u/outdir", Node::Link("/tmp/x")), ("/home/u/x", Node::Link("y")), ("/home/u/y", Node::Link("x")), ("/home/u/dangling", Node::Link("gone")), - ]); - assert_eq!(served(&tree, "/home/u/x"), Err(ELOOP)); - assert_eq!(served(&tree, "/home/u/dangling"), Err(ENOENT)); + ])) +} + +fn mixed(cwd: &str, path: &str) -> Result { + resolve_path(cwd.as_bytes(), path.as_bytes(), &mut home(), &mut tree()).map(|at| String::from_utf8(at).unwrap()) +} + +/// A served link is read against its own directory, as POSIX has it, and +/// not against a mount as the kernel's is; an absolute one from the root. +#[test] +fn a_served_link_is_read_against_its_own_directory() { + assert_eq!(mixed("/", "/home/u/rel"), Ok("/home/u/a/file".into())); + assert_eq!(mixed("/", "/home/u/abs"), Ok("/home/u/a/file".into())); + assert_eq!(mixed("/home/u", "./a//file"), Ok("/home/u/a/file".into()), "relative and lexically clean"); +} + +/// Every name's link is followed where it is met, so `..` after one is the +/// parent of where it led, and two spellings of one file answer one path. +#[test] +fn a_served_link_before_the_last_name_is_followed_and_dot_dot_after_it_is_its_parent() { + assert_eq!(mixed("/", "/home/u/d/file"), Ok("/home/u/a/file".into())); + assert_eq!(mixed("/", "/home/u/deep/../f"), Ok("/home/u/a/f".into()), "the parent of a/b, not of deep"); + assert_eq!(mixed("/", "/home/u/d/../d/file"), mixed("/", "/home/u/a/file")); +} + +/// A served link to a path the kernel holds is resolved there, by the +/// kernel's rules, whether it is the last name or one before it. +#[test] +fn a_served_link_out_to_the_kernel_is_resolved_by_the_kernel() { + assert_eq!(mixed("/", "/home/u/out"), Ok("/tmp/d/f".into())); + assert_eq!(mixed("/", "/home/u/outdir/f"), Ok("/tmp/x/f".into())); + assert_eq!(mixed("/", "/tmp/d/abs"), Ok("/tmp/x/f".into()), "a kernel path is the kernel's from its first name"); + assert_eq!(mixed("/home/u", "../../tmp/d/f"), Ok("/tmp/d/f".into())); +} + +#[test] +fn a_served_loop_is_eloop_and_a_missing_name_enoent_or_enotdir() { + assert_eq!(mixed("/", "/home/u/x"), Err(ELOOP)); + assert_eq!(mixed("/", "/home/u/dangling"), Err(ENOENT)); + assert_eq!(mixed("/", "/home/u/a/file/g"), Err(ENOTDIR)); + assert_eq!(mixed("/", "/"), Ok("/".into())); } diff --git a/sdk/libc/src/errcode.rs b/sdk/libc/src/errcode.rs index 429c0b1dd01..de3cc5093e8 100644 --- a/sdk/libc/src/errcode.rs +++ b/sdk/libc/src/errcode.rs @@ -62,6 +62,7 @@ pub(crate) fn of(e: SyscallError) -> i32 { // A write whose reader is gone, and not `ENOENT`, which says the file does not exist. SyscallError::Gone => EPIPE, SyscallError::Io => EIO, + SyscallError::NotSupported => EOPNOTSUPP, // A read of a connection met handles no frame announced: the peer broke its protocol. SyscallError::HandlesPending => EPROTO, _ => EINVAL, @@ -92,7 +93,8 @@ fn of_served(e: toyos::fs::Error) -> i32 { Error::IsADirectory => EISDIR, Error::NotEmpty => ENOTEMPTY, Error::NotALink | Error::IntoItself | Error::UnknownFile | Error::InvalidPath | Error::InvalidRequest => EINVAL, - Error::NameTooLong | Error::InvalidName => ENAMETOOLONG, + Error::NameTooLong => ENAMETOOLONG, + Error::InvalidName => EINVAL, Error::ReadOnly => EROFS, Error::NotOpenForWriting => EBADF, Error::Root => EBUSY, diff --git a/sdk/libc/src/posix_io.rs b/sdk/libc/src/posix_io.rs index 60d36c3a468..767af06b596 100644 --- a/sdk/libc/src/posix_io.rs +++ b/sdk/libc/src/posix_io.rs @@ -419,6 +419,22 @@ pub unsafe extern "C" fn symlink(target: *const u8, link: *const u8) -> i32 { files::symlink(c_str_to_bytes(target), link).map_or_else(refuse_files, |()| 0) } +struct TheServers; + +impl crate::resolve::Served for TheServers { + fn holds(&mut self, at: &[u8]) -> bool { + files::is_served(at) + } + + fn name(&mut self, at: &[u8]) -> Result { + let st = files::stat(at, false).map_err(crate::errcode::of_files)?; + match st.file_type { + syscall::FileType::Symlink => files::read_link(at).map(crate::resolve::Name::Link).map_err(crate::errcode::of_files), + _ => Ok(crate::resolve::Name::Other), + } + } +} + struct TheKernel; impl crate::resolve::Kernel for TheKernel { @@ -435,9 +451,10 @@ impl crate::resolve::Kernel for TheKernel { } } -/// POSIX's `realpath`, by the kernel's rules of resolution (`resolve`) or a -/// file server's: the file `open` of `path` reaches, in a block of `malloc`'s -/// when `resolved` is null. +/// POSIX's `realpath` (`resolve::resolve_path`): POSIX's resolution where a +/// file server holds the names, the kernel's rules where it does, so the +/// answer is the file `open` of `path` reaches, in a block of `malloc`'s when +/// `resolved` is null. #[no_mangle] pub unsafe extern "C" fn realpath(path: *const u8, resolved: *mut u8) -> *mut u8 { use syscall::MAX_PATH; @@ -448,20 +465,7 @@ pub unsafe extern "C" fn realpath(path: *const u8, resolved: *mut u8) -> *mut u8 let mut cwd = [0u8; MAX_PATH]; let n = syscall::getcwd(&mut cwd); assert!(n != 0 && n <= MAX_PATH, "SYS_GETCWD: a working directory past MAX_PATH"); - let path = c_str_to_bytes(path); - let resolved_at = match core::str::from_utf8(&cwd[..n]).ok().zip(core::str::from_utf8(path).ok()) { - Some((cwd, text)) if files::is_served(path) => { - let abs = files::absolute(cwd, text); - let read_link = |at: &[u8]| match files::read_link(at) { - Ok(target) => Ok(Some(target)), - Err(files::Error::Served(toyos::fs::Error::NotALink)) => Ok(None), - Err(e) => Err(crate::errcode::of_files(e)), - }; - let exists = |at: &[u8]| files::stat(at, true).map(drop).map_err(crate::errcode::of_files); - crate::resolve::resolve_served(abs.as_bytes(), read_link, exists) - } - _ => crate::resolve::resolve(&cwd[..n], path, &mut TheKernel), - }; + let resolved_at = crate::resolve::resolve_path(&cwd[..n], c_str_to_bytes(path), &mut TheServers, &mut TheKernel); let at = match resolved_at { Ok(at) => at, Err(code) => { diff --git a/sdk/libc/src/resolve.rs b/sdk/libc/src/resolve.rs index 6774f14ac83..16c12797dec 100644 --- a/sdk/libc/src/resolve.rs +++ b/sdk/libc/src/resolve.rs @@ -4,7 +4,8 @@ //! asks the kernel only through [`Kernel`], so the host tests it //! (`toyos-libc-copies`). //! -//! A path a file server holds is resolved by [`resolve_served`] instead. +//! A path a file server holds is resolved POSIX's way by [`resolve_path`], +//! which hands the kernel what it holds. //! //! The kernel's rules are not POSIX's (`issues/the-kernel-resolves-a-path-by-rules-of-its-own.md`): //! `..` is read off the text before any name is looked up, only the last @@ -124,24 +125,82 @@ fn absent(at: &[u8], kernel: &mut impl Kernel) -> Result { Ok(ENOENT) } -/// `abs`, a path a file server holds, its last name's links followed as -/// POSIX reads a relative one, against the link's own directory, or the errno -/// it is refused with. `read_link` answers a link's target, `None` for a name -/// that is no link; `exists` refuses a path that names nothing. The server -/// resolves the names before the last itself, and they stay as written. -pub(crate) fn resolve_served( - abs: &[u8], - mut read_link: impl FnMut(&[u8]) -> Result>, i32>, - mut exists: impl FnMut(&[u8]) -> Result<(), i32>, -) -> Result, i32> { - let mut at = joined(b"/", abs)?; - for _ in 0..=MAX_LINKS { - let Some(target) = read_link(&at)? else { - exists(&at)?; - return Ok(at); - }; - let dir = at.rsplitn(2, |&b| b == b'/').nth(1).filter(|d| !d.is_empty()).unwrap_or(b"/"); - at = joined(dir, &target)?; +/// What a name a file server holds is, as resolution asks it, its own link +/// not followed. +pub(crate) enum Name { + Link(Vec), + Other, +} + +/// The two calls [`resolve_path`] asks of the file servers, each of an +/// absolute path without `.` or `..`. +pub(crate) trait Served { + /// Whether a file server holds `at`, and not the kernel. + fn holds(&mut self, at: &[u8]) -> bool; + /// `at` itself, or the errno it is refused with: one that names nothing, + /// or passes through a file, is refused. + fn name(&mut self, at: &[u8]) -> Result; +} + +/// `path` made absolute against `cwd` and resolved as POSIX resolves it, a +/// name at a time, while a file server holds the names: each link is +/// followed where it is met, a relative one against its own directory, and +/// `..` after it is the parent of where it led. At the first name the kernel +/// holds, the rest is [`resolve`]d by the kernel's rules, which its `open` +/// reads it by. The answer holds no link a file server keeps. +pub(crate) fn resolve_path(cwd: &[u8], path: &[u8], served: &mut impl Served, kernel: &mut impl Kernel) -> Result, i32> { + if path.is_empty() { + return Err(ENOENT); + } + let names = |text: &[u8]| -> Vec> { text.split(|&b| b == b'/').map(<[u8]>::to_vec).collect() }; + let mut pending: alloc::collections::VecDeque> = alloc::collections::VecDeque::new(); + if path.first() != Some(&b'/') { + pending.extend(names(cwd)); + } + pending.extend(names(path)); + let mut at: Vec = Vec::new(); + let mut followed = 0; + while let Some(name) = pending.pop_front() { + match &name[..] { + b"" | b"." => continue, + b".." => { + let parent = at.iter().rposition(|&b| b == b'/').unwrap_or(0); + at.truncate(parent); + continue; + } + _ => {} + } + let mut next = at.clone(); + next.push(b'/'); + next.extend_from_slice(&name); + if next.len() >= MAX_PATH { + return Err(ENAMETOOLONG); + } + if !served.holds(&next) { + for rest in pending { + next.push(b'/'); + next.extend_from_slice(&rest); + } + return resolve(b"/", &next, kernel); + } + match served.name(&next)? { + Name::Other => at = next, + Name::Link(target) => { + if followed == MAX_LINKS { + return Err(ELOOP); + } + followed += 1; + if target.first() == Some(&b'/') { + at.clear(); + } + for name in names(&target).into_iter().rev() { + pending.push_front(name); + } + } + } + } + if at.is_empty() { + at.push(b'/'); } - Err(ELOOP) + Ok(at) } diff --git a/sdk/std/sys/pal/mod.rs b/sdk/std/sys/pal/mod.rs index 244b8733b70..90690c4d827 100644 --- a/sdk/std/sys/pal/mod.rs +++ b/sdk/std/sys/pal/mod.rs @@ -45,22 +45,14 @@ pub fn on_fd( fd: i32, f: impl FnOnce(toyos_abi::RawHandle) -> Result, ) -> crate::io::Result { - on_open_fd(fd, f).map_err(fd_error)? -} - -/// [`on_fd`], the table's refusal answered as it is, for a caller to whom a -/// descriptor not open means something of its own. -pub fn on_open_fd( - fd: i32, - f: impl FnOnce(toyos_abi::RawHandle) -> Result, -) -> Result, toyos::fd::Error> { - toyos::fd::with(fd, |object| match object.release { + let answer = toyos::fd::with(fd, |object| match object.release { None => f(object.handle).map_err(to_io_error), Some(_) => Err(crate::io::const_error!( crate::io::ErrorKind::Unsupported, "the descriptor names an object of a library's own", )), - }) + }); + answer.map_err(fd_error)? } /// A file server's refusal, or the client end's own, as std says it. diff --git a/sdk/std/sys/stdio.rs b/sdk/std/sys/stdio.rs index f59d0d15634..9e19c254058 100644 --- a/sdk/std/sys/stdio.rs +++ b/sdk/std/sys/stdio.rs @@ -4,7 +4,7 @@ use toyos::log::stdio::{self as log_stdio, Stream}; use toyos_abi::syscall::{self, FileType}; use crate::io::{self, IoSlice, IoSliceMut}; -use crate::sys::{fd_error, files_error, on_fd, on_open_fd}; +use crate::sys::{files_error, on_fd}; /// Standard input's descriptor. const STDIN: i32 = 0; @@ -35,13 +35,12 @@ struct LineBuf { // is single-threaded, so there is no concurrent access. static mut LINE_BUF: LineBuf = LineBuf { buf: [0; LINE_BUF_CAP], len: 0, pos: 0 }; -/// A read of stdin, closed or not: a closed one is at its end, as std's on -/// unix reads one. +/// A read of stdin, whoever serves it, closed or not: a closed one is at its +/// end, as std's on unix reads one. fn read_stdin(buf: &mut [u8]) -> io::Result { - match on_open_fd(STDIN, |h| syscall::read(h, buf)) { - Ok(read) => read, - Err(toyos::fd::Error::BadFd) => Ok(0), - Err(e) => Err(fd_error(e)), + match toyos::files::read(STDIN, buf) { + Err(toyos::files::Error::Fd(toyos::fd::Error::BadFd)) => Ok(0), + read => read.map_err(files_error), } } diff --git a/sdk/toyos/src/fd.rs b/sdk/toyos/src/fd.rs index c1a99ed9161..b45c05d454a 100644 --- a/sdk/toyos/src/fd.rs +++ b/sdk/toyos/src/fd.rs @@ -186,7 +186,7 @@ pub fn copy(fd: Fd) -> Result, crate::spawn::Refused> let copied = with(fd, |o| match o.release { None if crate::net::stream::is_stream(o.handle) => Err(Refused::Stream(o.handle)), None => toyos_abi::syscall::dup(o.handle).map(crate::OwnedHandle).map_err(Refused::Syscall), - Some(_) => Err(Refused::Syscall(toyos_abi::syscall::SyscallError::InvalidArgument)), + Some(_) => Err(Refused::Syscall(toyos_abi::syscall::SyscallError::NotSupported)), }); copied.map_or(Ok(None), |copied| copied.map(Some)) } diff --git a/sdk/toyos/src/files/owner.rs b/sdk/toyos/src/files/owner.rs index 019219b7308..dcdb7cdad90 100644 --- a/sdk/toyos/src/files/owner.rs +++ b/sdk/toyos/src/files/owner.rs @@ -100,7 +100,9 @@ fn route(abs: &str) -> Result { } /// Ask whoever serves `path`, following an absolute link a server meets to -/// wherever it lands in this process's own table. +/// wherever it lands in this process's own table. `served` runs on the +/// capability's turn, so nothing it does may reach the descriptor table, +/// whose release of an open file takes that turn again. fn on_path( path: &[u8], mut kernel: impl FnMut(&str) -> Result, @@ -147,9 +149,17 @@ fn on_fd( } } -/// A descriptor for an open file a server answered. -fn served_fd(cap: &Arc, opened: fs::Opened, cloexec: bool) -> Result { - let file = Arc::new(OpenFile { cap: Arc::clone(cap), fid: opened.fid, generation: opened.generation }); +/// What an open opened, before a descriptor names it. +enum Opened { + Kernel(RawHandle), + Served(Arc, fs::Opened), +} + +/// A descriptor for an open file a server answered: on a full table the +/// file is closed again, which takes its capability's turn, so no turn may be +/// held here. +fn served_fd(cap: Arc, opened: fs::Opened, cloexec: bool) -> Result { + let file = Arc::new(OpenFile { cap, fid: opened.fid, generation: opened.generation }); let word = { let mut open = OPEN.lock(); match open.iter().position(Option::is_none) { @@ -164,7 +174,7 @@ fn served_fd(cap: &Arc, opened: fs::Opened, cloexec: bool) -> Result } }; let object = Object::tagged(SERVED_FILE, word as u64, release_file); - fd::install(object, !cloexec, 0).map_err(|_| Refused::Error(fs::Error::TooManyOpen)) + fd::install(object, !cloexec, 0) } /// What a server answered, as the kernel's `Stat` says it. @@ -207,8 +217,7 @@ unsafe extern "C" fn toyos_fs_open(path: *const u8, len: usize, how: Open) -> i6 if how.append { flags |= OpenFlags::APPEND; } if how.create || how.create_new { flags |= OpenFlags::CREATE; } if how.truncate { flags |= OpenFlags::TRUNCATE; } - let handle = syscall::open(abs.as_bytes(), flags)?; - fd::install(Object::handle(handle), !how.cloexec, 0).map_err(|_| SyscallError::ResourceExhausted) + syscall::open(abs.as_bytes(), flags).map(Opened::Kernel) }, |cap, dir, rel| { let mut flags = 0; @@ -218,10 +227,19 @@ unsafe extern "C" fn toyos_fs_open(path: *const u8, len: usize, how: Open) -> i6 if how.create { flags |= fs::O_CREATE; } if how.create_new { flags |= fs::O_CREATE_NEW; } if how.truncate { flags |= fs::O_TRUNCATE; } - served_fd(cap, dir.open(rel, flags)?, how.cloexec) + dir.open(rel, flags).map(|opened| Opened::Served(Arc::clone(cap), opened)) }, ); - answer(opened.map(|fd| fd as u64)) + // Installed once the turn is let go: on a full table the open file is + // closed again (`served_fd`), and a kernel one by the table. + let installed = opened.and_then(|opened| { + match opened { + Opened::Kernel(handle) => fd::install(Object::handle(handle), !how.cloexec, 0), + Opened::Served(cap, opened) => served_fd(cap, opened, how.cloexec), + } + .map_err(Error::Fd) + }); + answer(installed.map(|fd| fd as u64)) } #[no_mangle] diff --git a/sdk/toyos/src/sync.rs b/sdk/toyos/src/sync.rs index fc3c106bb1d..da95b4dd490 100644 --- a/sdk/toyos/src/sync.rs +++ b/sdk/toyos/src/sync.rs @@ -57,72 +57,71 @@ impl Drop for Held<'_, T> { } } -/// How a [`Turnstile`]'s waiters sleep on and wake from its word. -pub trait Sleep { - /// Sleep while `word` still says `expected`; a spurious return is allowed. - fn wait(word: &AtomicU32, expected: u32); - /// Wake every sleeper on `word`. - fn wake_all(word: &AtomicU32); +/// Sleep while `word` still says `expected`; a spurious return is allowed. +#[cfg(not(test))] +fn wait(word: &AtomicU32, expected: u32) { + // SAFETY: `word` is a live, aligned u32. + unsafe { syscall::futex_wait(word.as_ptr(), expected, None) }; } -/// The kernel's futex. -pub struct Futex; +/// Wake every sleeper on `word`. +#[cfg(not(test))] +fn wake_all(word: &AtomicU32) { + // SAFETY: `word` is a live, aligned u32. + unsafe { syscall::futex_wake(word.as_ptr(), u32::MAX) }; +} -impl Sleep for Futex { - fn wait(word: &AtomicU32, expected: u32) { - // SAFETY: `word` is a live, aligned u32. - unsafe { syscall::futex_wait(word.as_ptr(), expected, None) }; - } - fn wake_all(word: &AtomicU32) { - // SAFETY: `word` is a live, aligned u32. - unsafe { syscall::futex_wake(word.as_ptr(), u32::MAX) }; - } +// A host test has no futex: a waiter yields and spins. +#[cfg(test)] +fn wait(_: &AtomicU32, _: u32) { + std::thread::yield_now(); } +#[cfg(test)] +fn wake_all(_: &AtomicU32) {} + /// `T` held in turn: each [`Turnstile::lock`] takes a ticket, and the value /// goes to the tickets in the order they were taken. A plain lock lets the /// thread that let go take it straight back ahead of the one it woke, so /// threads sharing one connection would not be served in turn. -pub struct Turnstile { +pub struct Turnstile { /// The next ticket handed out. next: AtomicU32, /// The ticket whose turn it is, which waiters sleep on. serving: AtomicU32, value: UnsafeCell, - sleep: core::marker::PhantomData, } // SAFETY: the value is reached only through a `Turn`, which only the ticket // being served holds. -unsafe impl Sync for Turnstile {} +unsafe impl Sync for Turnstile {} /// The value, held for one turn. -pub struct Turn<'a, T, S: Sleep = Futex>(&'a Turnstile); +pub struct Turn<'a, T>(&'a Turnstile); -impl Turnstile { +impl Turnstile { pub const fn new(value: T) -> Self { Self { next: AtomicU32::new(0), serving: AtomicU32::new(0), value: UnsafeCell::new(value), - sleep: core::marker::PhantomData, } } /// The value, once every ticket taken before this one has had its turn. - pub fn lock(&self) -> Turn<'_, T, S> { + pub fn lock(&self) -> Turn<'_, T> { let ticket = self.next.fetch_add(1, Ordering::Relaxed); loop { let serving = self.serving.load(Ordering::Acquire); if serving == ticket { return Turn(self); } - S::wait(&self.serving, serving); + wait(&self.serving, serving); } } } -impl core::ops::Deref for Turn<'_, T, S> { +impl core::ops::Deref for Turn<'_, T> { type Target = T; fn deref(&self) -> &T { // SAFETY: this ticket is the one being served. @@ -130,19 +129,19 @@ impl core::ops::Deref for Turn<'_, T, S> { } } -impl core::ops::DerefMut for Turn<'_, T, S> { +impl core::ops::DerefMut for Turn<'_, T> { fn deref_mut(&mut self) -> &mut T { // SAFETY: this ticket is the one being served. unsafe { &mut *self.0.value.get() } } } -impl Drop for Turn<'_, T, S> { +impl Drop for Turn<'_, T> { fn drop(&mut self) { self.0.serving.fetch_add(1, Ordering::Release); // Every waiter, since only the next ticket's may go on and only it // knows it is the next. - S::wake_all(&self.0.serving); + wake_all(&self.0.serving); } } @@ -173,21 +172,11 @@ mod tests { use super::*; - /// A host's stand-in for the futex: a wait yields once. - struct Yield; - - impl Sleep for Yield { - fn wait(_: &AtomicU32, _: u32) { - std::thread::yield_now(); - } - fn wake_all(_: &AtomicU32) {} - } - /// A thread that lets the value go and asks for it again waits behind /// the threads that asked first, in the order they asked. #[test] fn the_value_goes_to_the_tickets_in_the_order_they_were_taken() { - let stile: Arc, Yield>> = Arc::new(Turnstile::new(Vec::new())); + let stile: Arc>> = Arc::new(Turnstile::new(Vec::new())); let mut held = stile.lock(); held.push("first"); let queued: Vec<_> = ["second", "third"] diff --git a/tests/corpus/tinycc/212_full_table.c b/tests/corpus/tinycc/212_full_table.c new file mode 100644 index 00000000000..ed0805280d2 --- /dev/null +++ b/tests/corpus/tinycc/212_full_table.c @@ -0,0 +1,39 @@ +/* A full descriptor table refuses an open with EMFILE, whoever serves the + path: the kernel's /tmp and a file server's /home alike, and the served + file the server opened is let go again rather than held. */ +#include +#include +#include +#include +#include + +#define SERVED "/home/212_full_table" +#define KERNEL "/tmp/212_full_table" + +static int made(const char *path) { + int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644); + return fd >= 0 && close(fd) == 0; +} + +int main(void) { + int last = -1, fd; + if (!made(SERVED) || !made(KERNEL)) { + printf("could not make the two files\n"); + return 1; + } + while ((fd = open("/dev/null", O_RDONLY)) >= 0) + last = fd; + printf("the table filled with EMFILE: %s\n", errno == EMFILE ? "yes" : "NO"); + errno = 0; + printf("an open on /tmp is EMFILE: %s\n", open(KERNEL, O_RDONLY) == -1 && errno == EMFILE ? "yes" : "NO"); + errno = 0; + printf("an open on /home is EMFILE: %s\n", open(SERVED, O_RDONLY) == -1 && errno == EMFILE ? "yes" : "NO"); + close(last); + fd = open(SERVED, O_RDONLY); + printf("with one free, /home opens: %s\n", fd >= 0 ? "yes" : "NO"); + close(fd); + for (fd = 3; fd < last; fd++) + close(fd); + printf("both are removed: %s\n", unlink(SERVED) == 0 && unlink(KERNEL) == 0 ? "yes" : "NO"); + return 0; +} diff --git a/tests/corpus/tinycc/212_full_table.expect b/tests/corpus/tinycc/212_full_table.expect new file mode 100644 index 00000000000..90445bfffed --- /dev/null +++ b/tests/corpus/tinycc/212_full_table.expect @@ -0,0 +1,5 @@ +the table filled with EMFILE: yes +an open on /tmp is EMFILE: yes +an open on /home is EMFILE: yes +with one free, /home opens: yes +both are removed: yes diff --git a/tests/corpus/tinycc/LICENSE b/tests/corpus/tinycc/LICENSE index 4191155a23d..53185b85727 100644 --- a/tests/corpus/tinycc/LICENSE +++ b/tests/corpus/tinycc/LICENSE @@ -21,9 +21,9 @@ against tinycc upstream at 64552b3faa39ee7948a9ea21bfcc11045b90c70d (repo.or.cz/tinycc.git, 2026-08-05), allowing for the `-` → `_` renames this project made to some filenames. - tinycc/ 330 files beside this one: 239 byte-identical to tinycc's `tests/tests2`, + tinycc/ 334 files beside this one: 239 byte-identical to tinycc's `tests/tests2`, 17 tinycc files this project modified, - 74 not upstream at all — 73 cases written here, plus + 78 not upstream at all — 77 cases written here, plus `fred.txt`, which is output `40_stdio.c` writes when it runs and which was committed by accident. From 2b122fe851d1457a195a7e40aeb547dc38528edf Mon Sep 17 00:00:00 2001 From: japabu Date: Wed, 21 Oct 2026 22:44:29 +0200 Subject: [PATCH 4/4] files: realpath under a deeper grant, a full table filled by duplicates Review r2 of #988. realpath handed a path to the kernel at the first name no file server held, so every path under a folder grant deeper than one name, such as fs:/home/toy/Games without fs:/home, went to the kernel at /home and answered ENOENT. The handoff is now asked of the whole path as it stands, and a name above the capability that holds it is passed through as a directory, as the client's routing passes it. A host case with a three-name capability pins it. 212_full_table filled the table with /dev/null opens, each a kernel handle, and on the T14 the kernel's handle table ran out first: its 4096 slots are MAX_FDS's, and the process's own handles take some of them. libc answered that EINVAL, and the served open, which needs no kernel handle, then succeeded. The case now fills the table with duplicates of one /home descriptor, which take no kernel handle, so the /home open runs the install-after-turn path that hung at 2d3c5d8a6, and then fills the kernel's table on its own. toyos::fd::MAX_FDS is the kernel's slot count by name. libc answers the kernel's ResourceExhausted ENOMEM, as std answers it OutOfMemory, where it fell to EINVAL. It is the kernel's one word for a full handle table and for no memory, so a full table cannot yet be EMFILE; issues/a-full-kernel-handle-table-answers-enomem-not-emfile.md records it. A rename between two of the kernel's mounts is EXDEV, as one between a server and the kernel is, rather than EOPNOTSUPP. Co-Authored-By: Claude Opus 5.5 --- ...-handle-table-answers-enomem-not-emfile.md | 24 +++++++++++ ...of-a-served-file-syncs-its-whole-volume.md | 3 +- sdk/libc/arch/src/path_resolution.rs | 41 +++++++++++++++---- sdk/libc/src/errcode.rs | 2 + sdk/libc/src/resolve.rs | 28 +++++++++---- sdk/toyos/src/fd.rs | 6 ++- sdk/toyos/src/files/owner.rs | 7 +++- tests/corpus/tinycc/212_full_table.c | 26 +++++++++--- tests/corpus/tinycc/212_full_table.expect | 3 +- 9 files changed, 113 insertions(+), 27 deletions(-) create mode 100644 issues/a-full-kernel-handle-table-answers-enomem-not-emfile.md diff --git a/issues/a-full-kernel-handle-table-answers-enomem-not-emfile.md b/issues/a-full-kernel-handle-table-answers-enomem-not-emfile.md new file mode 100644 index 00000000000..9b8bf50ffc3 --- /dev/null +++ b/issues/a-full-kernel-handle-table-answers-enomem-not-emfile.md @@ -0,0 +1,24 @@ +--- +status: open +kind: defect +opened: 2026-10-21 +--- + +# A full kernel handle table answers ENOMEM, not EMFILE + +The kernel answers a full handle table `ResourceExhausted` +(`kernel/src/object/handle.rs`, `HandleError::TableFull`), the same word it +answers for no memory. libc can only answer that `ENOMEM` +(`sdk/libc/src/errcode.rs`), and std `OutOfMemory`. So an `open`, `pipe` or +`/dev/null` that a full kernel table refuses says `ENOMEM`, where POSIX has +`EMFILE` for a process that holds as many descriptors as it may. The kernel's +table is the binding limit for descriptors that are kernel handles: it holds +as many slots as `toyos::fd::MAX_FDS`, and the process's own handles take some +of them. Corpus case `212_full_table` asserts the `ENOMEM`. + +A word of the kernel's own for a full table would let libc say `EMFILE`. The +owner ruled no kernel change for POSIX alone, so this waits on a reason the +kernel's own callers have to tell the two apart. + +**Exit**: a full handle table and no memory are two answers at the kernel +boundary, libc answers the first `EMFILE`, and `212_full_table` expects it. diff --git a/issues/an-fsync-of-a-served-file-syncs-its-whole-volume.md b/issues/an-fsync-of-a-served-file-syncs-its-whole-volume.md index c0258be03b1..e955882008a 100644 --- a/issues/an-fsync-of-a-served-file-syncs-its-whole-volume.md +++ b/issues/an-fsync-of-a-served-file-syncs-its-whole-volume.md @@ -16,8 +16,7 @@ entry and every dirty block of the cache (`system/fileserver/src/main.rs`, std's ToyOS `File::flush` is a no-op, as it is on every other platform: a `File` buffers nothing, since each `write` is the server's when it answers (`toyos::files`), so a flush has nothing to hand on and `Write::flush` asks -for no durability. It used to be `fsync`, which made every `BufWriter` flush -sync the volume. +for no durability. **Exit**: `FSYNC` makes durable the file it names and what that file's entry depends on, with a guest measurement of one file's fsync beside the volume's. diff --git a/sdk/libc/arch/src/path_resolution.rs b/sdk/libc/arch/src/path_resolution.rs index 33d3880d0ec..2f80369e363 100644 --- a/sdk/libc/arch/src/path_resolution.rs +++ b/sdk/libc/arch/src/path_resolution.rs @@ -198,14 +198,16 @@ fn path_max_is_limits_h_s() { assert_eq!(MAX_PATH, header::int(LIMITS_H, "PATH_MAX") as usize); } -/// A file server holding `/home`, its names in `home`, beside the kernel's -/// [`tree`]: as `toyos::files` answers, a name is its link's target or -/// nothing, one that is not there `ENOENT`, and one through a file `ENOTDIR`. -struct Home(Tree); +/// A file server holding the capability `grant`, its names in the tree, +/// beside the kernel's [`tree`]: as `toyos::files` answers, a name is its +/// link's target or nothing, one that is not there `ENOENT`, and one through +/// a file `ENOTDIR`. +struct Home(Tree, &'static str); impl Served for Home { fn holds(&mut self, at: &[u8]) -> bool { - at == b"/home" || at.starts_with(b"/home/") + let grant = self.1.as_bytes(); + at == grant || (at.starts_with(grant) && at.get(grant.len()) == Some(&b'/')) } fn name(&mut self, at: &[u8]) -> Result { @@ -216,7 +218,7 @@ impl Served for Home { match self.0.at(at) { Some(Node::Link(target)) => Ok(Name::Link(target.as_bytes().to_vec())), Some(_) => Ok(Name::Other), - None if at == b"/home" => Ok(Name::Other), + None if at == self.1.as_bytes() => Ok(Name::Other), None => Err(ENOENT), } } @@ -238,7 +240,7 @@ fn home() -> Home { ("/home/u/x", Node::Link("y")), ("/home/u/y", Node::Link("x")), ("/home/u/dangling", Node::Link("gone")), - ])) + ]), "/home") } fn mixed(cwd: &str, path: &str) -> Result { @@ -280,3 +282,28 @@ fn a_served_loop_is_eloop_and_a_missing_name_enoent_or_enotdir() { assert_eq!(mixed("/", "/home/u/a/file/g"), Err(ENOTDIR)); assert_eq!(mixed("/", "/"), Ok("/".into())); } + +/// A capability three names deep, `fs:/home/toy/Games` without `fs:/home`, as +/// a folder grant gives one: the names above it are passed through as the +/// client routes past them, and a path beside it is the kernel's. +#[test] +fn a_path_under_a_deeper_capability_is_walked_and_one_beside_it_is_the_kernels() { + let games = || { + Home( + Tree::new(&[ + ("/home/toy/Games/save", Node::File), + ("/home/toy/Games/rom", Node::Link("roms/a.gba")), + ("/home/toy/Games/roms", Node::Dir), + ("/home/toy/Games/roms/a.gba", Node::File), + ]), + "/home/toy/Games", + ) + }; + let answer = |path: &str| { + resolve_path(b"/", path.as_bytes(), &mut games(), &mut tree()).map(|at| String::from_utf8(at).unwrap()) + }; + assert_eq!(answer("/home/toy/Games/save"), Ok("/home/toy/Games/save".into())); + assert_eq!(answer("/home/toy/Games/rom"), Ok("/home/toy/Games/roms/a.gba".into())); + assert_eq!(answer("/home/toy/./Games/../Games/save"), Ok("/home/toy/Games/save".into())); + assert_eq!(answer("/home/toy/Other"), Err(ENOENT), "beside the capability: the kernel's, which holds nothing there"); +} diff --git a/sdk/libc/src/errcode.rs b/sdk/libc/src/errcode.rs index de3cc5093e8..f7f42bbe907 100644 --- a/sdk/libc/src/errcode.rs +++ b/sdk/libc/src/errcode.rs @@ -62,6 +62,8 @@ pub(crate) fn of(e: SyscallError) -> i32 { // A write whose reader is gone, and not `ENOENT`, which says the file does not exist. SyscallError::Gone => EPIPE, SyscallError::Io => EIO, + // The kernel's word for a full handle table and for no memory alike. + SyscallError::ResourceExhausted => ENOMEM, SyscallError::NotSupported => EOPNOTSUPP, // A read of a connection met handles no frame announced: the peer broke its protocol. SyscallError::HandlesPending => EPROTO, diff --git a/sdk/libc/src/resolve.rs b/sdk/libc/src/resolve.rs index 16c12797dec..38dd011637b 100644 --- a/sdk/libc/src/resolve.rs +++ b/sdk/libc/src/resolve.rs @@ -135,7 +135,8 @@ pub(crate) enum Name { /// The two calls [`resolve_path`] asks of the file servers, each of an /// absolute path without `.` or `..`. pub(crate) trait Served { - /// Whether a file server holds `at`, and not the kernel. + /// Whether a file server holds `at`, and not the kernel: by the deepest + /// capability `at` is under, which may be several names below its first. fn holds(&mut self, at: &[u8]) -> bool; /// `at` itself, or the errno it is refused with: one that names nothing, /// or passes through a file, is refused. @@ -145,9 +146,9 @@ pub(crate) trait Served { /// `path` made absolute against `cwd` and resolved as POSIX resolves it, a /// name at a time, while a file server holds the names: each link is /// followed where it is met, a relative one against its own directory, and -/// `..` after it is the parent of where it led. At the first name the kernel -/// holds, the rest is [`resolve`]d by the kernel's rules, which its `open` -/// reads it by. The answer holds no link a file server keeps. +/// `..` after it is the parent of where it led. Once the path as it stands +/// is the kernel's, it is [`resolve`]d by the kernel's rules, which its +/// `open` reads it by. The answer holds no link a file server keeps. pub(crate) fn resolve_path(cwd: &[u8], path: &[u8], served: &mut impl Served, kernel: &mut impl Kernel) -> Result, i32> { if path.is_empty() { return Err(ENOENT); @@ -176,12 +177,21 @@ pub(crate) fn resolve_path(cwd: &[u8], path: &[u8], served: &mut impl Served, ke if next.len() >= MAX_PATH { return Err(ENAMETOOLONG); } + let mut full = next.clone(); + for rest in &pending { + full.push(b'/'); + full.extend_from_slice(rest); + } + // Asked of the path as it reads, `.` and `..` taken off its text: a + // link met further on is asked again where it leads. + if !served.holds(&joined(b"/", &full)?) { + return resolve(b"/", &full, kernel); + } + // A name above the capability that holds the path, which the client + // routes past and no server holds: a directory, as its routing reads it. if !served.holds(&next) { - for rest in pending { - next.push(b'/'); - next.extend_from_slice(&rest); - } - return resolve(b"/", &next, kernel); + at = next; + continue; } match served.name(&next)? { Name::Other => at = next, diff --git a/sdk/toyos/src/fd.rs b/sdk/toyos/src/fd.rs index b45c05d454a..130f8642f5d 100644 --- a/sdk/toyos/src/fd.rs +++ b/sdk/toyos/src/fd.rs @@ -29,8 +29,10 @@ use toyos_abi::RawHandle; pub type Fd = i32; /// Descriptors and objects one table holds: as many as the kernel holds -/// handles for one process. -pub const MAX_FDS: usize = 4096; +/// handles for one process. The process's own handles take some of those, so +/// descriptors that are kernel handles meet the kernel's limit first, and +/// only descriptors that share an object or name none meet this one. +pub const MAX_FDS: usize = toyos_abi::RawHandle::MAX_SLOTS; /// Why a table call was refused. #[derive(Clone, Copy, Debug, PartialEq, Eq)] diff --git a/sdk/toyos/src/files/owner.rs b/sdk/toyos/src/files/owner.rs index dcdb7cdad90..718a7b51e76 100644 --- a/sdk/toyos/src/files/owner.rs +++ b/sdk/toyos/src/files/owner.rs @@ -367,7 +367,12 @@ unsafe extern "C" fn toyos_fs_rename(from: *const u8, from_len: usize, to: *cons let cwd = cwd(); let (from, to) = (super::absolute(&cwd, utf8(from)?), super::absolute(&cwd, utf8(to)?)); match (route(&from)?, route(&to)?) { - (Route::Kernel(a), Route::Kernel(b)) => syscall::rename(a.as_bytes(), b.as_bytes()).map_err(Error::Kernel), + // The kernel refuses a rename between its own mounts as + // unsupported, which is a rename across servers as POSIX reads it. + (Route::Kernel(a), Route::Kernel(b)) => syscall::rename(a.as_bytes(), b.as_bytes()).map_err(|e| match e { + SyscallError::NotSupported => Error::CrossesServers, + e => Error::Kernel(e), + }), (Route::Served(ca, a), Route::Served(cb, b)) if Arc::ptr_eq(&ca, &cb) => { ca.dir.lock().rename(&a, &b).map_err(|refused| match refused { Refused::Error(e) => Error::Served(e), diff --git a/tests/corpus/tinycc/212_full_table.c b/tests/corpus/tinycc/212_full_table.c index ed0805280d2..2f423bb8fa3 100644 --- a/tests/corpus/tinycc/212_full_table.c +++ b/tests/corpus/tinycc/212_full_table.c @@ -1,6 +1,10 @@ /* A full descriptor table refuses an open with EMFILE, whoever serves the path: the kernel's /tmp and a file server's /home alike, and the served - file the server opened is let go again rather than held. */ + file the server opened is let go again rather than held. Duplicates of + one /home descriptor fill the table, as they take no kernel handle; + descriptors that are each a kernel handle meet the kernel's handle table + first, which answers ENOMEM, the kernel's one word for a full table and + for no memory. */ #include #include #include @@ -15,15 +19,22 @@ static int made(const char *path) { return fd >= 0 && close(fd) == 0; } +/* Close every descriptor from 3 up to and including `last`. */ +static void close_to(int last) { + for (int fd = 3; fd <= last; fd++) + close(fd); +} + int main(void) { int last = -1, fd; if (!made(SERVED) || !made(KERNEL)) { printf("could not make the two files\n"); return 1; } - while ((fd = open("/dev/null", O_RDONLY)) >= 0) + int served = open(SERVED, O_RDONLY); + while ((fd = dup(served)) >= 0) last = fd; - printf("the table filled with EMFILE: %s\n", errno == EMFILE ? "yes" : "NO"); + printf("duplicates filled the table with EMFILE: %s\n", errno == EMFILE ? "yes" : "NO"); errno = 0; printf("an open on /tmp is EMFILE: %s\n", open(KERNEL, O_RDONLY) == -1 && errno == EMFILE ? "yes" : "NO"); errno = 0; @@ -32,8 +43,13 @@ int main(void) { fd = open(SERVED, O_RDONLY); printf("with one free, /home opens: %s\n", fd >= 0 ? "yes" : "NO"); close(fd); - for (fd = 3; fd < last; fd++) - close(fd); + close_to(last); + + errno = 0; + while ((fd = open("/dev/null", O_RDONLY)) >= 0) + last = fd; + printf("kernel handles ran out first with ENOMEM: %s\n", errno == ENOMEM ? "yes" : "NO"); + close_to(last); printf("both are removed: %s\n", unlink(SERVED) == 0 && unlink(KERNEL) == 0 ? "yes" : "NO"); return 0; } diff --git a/tests/corpus/tinycc/212_full_table.expect b/tests/corpus/tinycc/212_full_table.expect index 90445bfffed..7eda59fe180 100644 --- a/tests/corpus/tinycc/212_full_table.expect +++ b/tests/corpus/tinycc/212_full_table.expect @@ -1,5 +1,6 @@ -the table filled with EMFILE: yes +duplicates filled the table with EMFILE: yes an open on /tmp is EMFILE: yes an open on /home is EMFILE: yes with one free, /home opens: yes +kernel handles ran out first with ENOMEM: yes both are removed: yes