diff --git a/Cargo.lock b/Cargo.lock index 0dfa500604..4bf625552e 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 ba693aaadc..64fd279817 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/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 7a71059a86..d1232d748f 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-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 0000000000..9b8bf50ffc --- /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/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 fc3b7ee1c8..67bdc775b6 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 0000000000..04b0e81b4c --- /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 50155440e3..0000000000 --- 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 c26330c17d..a614807125 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 0000000000..e955882008 --- /dev/null +++ b/issues/an-fsync-of-a-served-file-syncs-its-whole-volume.md @@ -0,0 +1,22 @@ +--- +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. + +**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/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 d715d23534..0000000000 --- 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/create-new-on-a-kernel-path-is-not-exclusive.md b/issues/create-new-on-a-kernel-path-is-not-exclusive.md index 161885cde1..0bf73d7d1e 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 236c6973e4..0000000000 --- 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/libc-refuses-what-toyos-cannot-yet-answer.md b/issues/libc-refuses-what-toyos-cannot-yet-answer.md index 934a963cdf..a214f0aa3e 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 7098b4a2b7..573a4c9a11 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/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 6af35d3d63..0b569211a1 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 ef3c77f29c..9b4a7e6892 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/rust b/rust index a56b4057c5..9d8bb4c347 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 e67ed48617..2f80369e36 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_path, Kernel, Name, Served}; #[derive(Clone, Copy)] enum Node { @@ -197,3 +197,113 @@ 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); } + +/// 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 { + 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 { + 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 == self.1.as_bytes() => Ok(Name::Other), + None => Err(ENOENT), + } + } +} + +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/f", Node::File), + ("/home/u/rel", Node::Link("a/file")), + ("/home/u/abs", Node::Link("/home/u/a/file")), + ("/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")), + ]), "/home") +} + +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())); +} + +/// 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/include/errno.h b/sdk/libc/include/errno.h index 6662185d19..227096367e 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 4b5215b031..f7f42bbe90 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 { @@ -55,8 +62,54 @@ 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, _ => 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 => ENAMETOOLONG, + Error::InvalidName => EINVAL, + 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 a644f23759..767af06b59 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,43 +366,72 @@ 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 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), + } } } @@ -404,9 +451,10 @@ 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` (`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; @@ -417,7 +465,8 @@ 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 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) => { crate::errno::set(code); @@ -444,10 +493,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 +514,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 +535,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 +556,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 +616,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 6ce01eb58a..50571ed05b 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 782d17a551..38dd011637 100644 --- a/sdk/libc/src/resolve.rs +++ b/sdk/libc/src/resolve.rs @@ -4,7 +4,10 @@ //! 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 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 //! 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 +124,93 @@ fn absent(at: &[u8], kernel: &mut impl Kernel) -> Result { } Ok(ENOENT) } + +/// 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: 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. + 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. 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); + } + 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); + } + 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) { + at = next; + continue; + } + 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'/'); + } + Ok(at) +} diff --git a/sdk/libc/src/spawn.rs b/sdk/libc/src/spawn.rs index 36bdca02f1..296af6c5f6 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 d5858cc6ee..0000000000 --- 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 dcaf0ebea4..0000000000 --- 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 2a11846ab5..ea2d6e7638 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 b545cdeef5..90690c4d82 100644 --- a/sdk/std/sys/pal/mod.rs +++ b/sdk/std/sys/pal/mod.rs @@ -45,22 +45,80 @@ 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. +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. diff --git a/sdk/std/sys/stdio.rs b/sdk/std/sys/stdio.rs index 797fefcfa9..9e19c25405 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::{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), } } @@ -167,7 +166,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 +195,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 d6033d9be1..0000000000 --- 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 0353ec3063..a8469c9de7 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 b909ac71da..130f8642f5 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)] @@ -42,12 +44,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 +68,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 @@ -173,7 +188,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)) } @@ -214,6 +229,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 +237,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 +253,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 0000000000..c40ada2279 --- /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 0000000000..718a7b51e7 --- /dev/null +++ b/sdk/toyos/src/files/owner.rs @@ -0,0 +1,477 @@ +//! `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. `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, + 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), + } +} + +/// 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) { + 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) +} + +/// 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; } + syscall::open(abs.as_bytes(), flags).map(Opened::Kernel) + }, + |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; } + dir.open(rel, flags).map(|opened| Opened::Served(Arc::clone(cap), opened)) + }, + ); + // 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] +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)?) { + // 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), + // 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 86c7f3f0a0..dc0f7f86e3 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 d89a8795a8..18e5a2a5cf 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 fe88d9fb8a..25b3db8186 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 76ec06d232..da95b4dd49 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,94 @@ impl Drop for Held<'_, T> { } } +/// 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) }; +} + +/// 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) }; +} + +// 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 { + /// The next ticket handed out. + next: AtomicU32, + /// The ticket whose turn it is, which waiters sleep on. + serving: AtomicU32, + value: UnsafeCell, +} + +// 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>(&'a Turnstile); + +impl Turnstile { + pub const fn new(value: T) -> Self { + Self { + next: AtomicU32::new(0), + serving: AtomicU32::new(0), + value: UnsafeCell::new(value), + } + } + + /// The value, once every ticket taken before this one has had its turn. + 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); + } + wait(&self.serving, serving); + } + } +} + +impl core::ops::Deref for Turn<'_, T> { + 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> { + 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> { + 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. + 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 +163,42 @@ 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 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>> = 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/206_libc_refusals.expect b/tests/corpus/tinycc/206_libc_refusals.expect index 97d52e8bb8..efa3ce6eb2 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 diff --git a/tests/corpus/tinycc/211_home_files.c b/tests/corpus/tinycc/211_home_files.c new file mode 100644 index 0000000000..4c1cea621e --- /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 0000000000..31fbc2cd93 --- /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 diff --git a/tests/corpus/tinycc/212_full_table.c b/tests/corpus/tinycc/212_full_table.c new file mode 100644 index 0000000000..2f423bb8fa --- /dev/null +++ b/tests/corpus/tinycc/212_full_table.c @@ -0,0 +1,55 @@ +/* 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. 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 +#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; +} + +/* 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; + } + int served = open(SERVED, O_RDONLY); + while ((fd = dup(served)) >= 0) + last = fd; + 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; + 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); + 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 new file mode 100644 index 0000000000..7eda59fe18 --- /dev/null +++ b/tests/corpus/tinycc/212_full_table.expect @@ -0,0 +1,6 @@ +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 diff --git a/tests/corpus/tinycc/LICENSE b/tests/corpus/tinycc/LICENSE index 4191155a23..53185b8572 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.