From 60b9bb690b3573b170b660f6aee339eadcdb22bb Mon Sep 17 00:00:00 2001 From: japabu Date: Wed, 21 Oct 2026 17:06:55 +0200 Subject: [PATCH 1/2] fd: one descriptor table numbers every raw fd, and a child's descriptors are its fd: endowments A raw fd on ToyOS was a kernel handle, so C's small descriptor numbers, std's raw-fd interop and POSIX semantics (lowest-free numbering, dup sharing an object, dup2 answering its target, close-on-exec per descriptor) had no home, and libc kept close-on-exec marks, a socket table numbered from 1024 and a dup2 that could not answer POSIX's number. toyos::fd is the process's one descriptor table, owned by the copy of `toyos` whose `fd-table` feature is on: std's (rustc-dep-of-std) in a Rust program, libc's own (`fd-owner`, its C-only build) in a C program, every copy reaching it through `toyos_fd_*`. A descriptor names an object, a kernel handle or an object a library keeps itself, numbered lowest-free with an inheritable flag of its own; a call holds its object, so a racing close makes the next lookup BadFd and never a call on a closed handle. std's File, pipes, stdin and network types are table descriptors, as on unix: as_raw_fd, into_raw_fd and from_raw_fd move descriptor numbers, and OwnedFd closes and duplicates through the table (the rust fork's arms). `std::os::toyos::io::AsRawHandle` answers the kernel handle behind a descriptor, for ToyOS-only code and the mio fork's selector; a portable program needs none. A closed stdin reads its end and a closed stdout drops its bytes, as std on unix does. libc's every descriptor call is on the table: open, pipe, dup, dup2 (answering its target), fcntl's close-on-exec and F_DUPFD_CLOEXEC, and sockets, which are objects of its own (a close now releases one; the close-socket issue keeps its last exit, a C case). The socket2 fork's sockets converge the same way, and its try_clone is a dup. A child's descriptors, stdio included, travel as endowments labelled `fd:`, which its table installs at their numbers at its first use. The kernel's spawn slot map, SYS_HANDLE_DUP_SLOT and SYS_HANDLE_DUP_AT go, and with them HandleTable's slot-placing paths; an endowed console is minted for the child as the slot map's was. The launch protocol's stdio slots are descriptors 0 to 2, which the supervisor endows. The supervisor's own stdio is the kernel's three fd: endowments, its log ring put at 1 and 2 by the table's dup2; relocation's refusal line finds fd:2 in the endowment table. Guest tests moved off slot numbers and RawHandle(0..2); a C child asking about a descriptor it lacks is answered EBADF, as POSIX has it, rather than ended by a handle fault, so spawn_signaled loses its handle arm (the status mapping stays host-tested). The capability record, the children's issue and the stream issue say what the spawn now carries. Co-Authored-By: Claude Opus 5.5 --- Cargo.lock | 5 +- build/build.rs | 5 +- build/libc.rs | 2 +- ...nt-and-a-parent-takes-its-children-down.md | 16 +- .../a-stream-cannot-move-between-processes.md | 4 +- ...libc-close-of-a-socket-ends-the-process.md | 20 - ...ocket-is-unproven-on-a-connected-socket.md | 17 + ...-capability-end-state-is-twelve-answers.md | 21 +- issues/toyos-builds-itself.md | 2 +- kernel/src/loader/mod.rs | 30 +- kernel/src/loader/start.rs | 95 +--- kernel/src/object/handle.rs | 58 +- kernel/src/syscall/dispatch.rs | 16 +- kernel/src/syscall/handles.rs | 49 -- rust | 2 +- sdk/abi/src/syscall.rs | 58 +- sdk/libc/Cargo.toml | 3 + sdk/libc/arch/src/descriptor_requests.rs | 132 +---- sdk/libc/arch/src/prototypes.rs | 1 - sdk/libc/arch/src/spawn_requests.rs | 55 +- sdk/libc/src/errcode.rs | 2 + sdk/libc/src/fdreq.rs | 95 +--- sdk/libc/src/posix_io.rs | 202 ++++--- sdk/libc/src/socket.rs | 104 ++-- sdk/libc/src/spawn.rs | 47 +- sdk/libc/src/spawnreq.rs | 48 +- sdk/libc/src/stdio.rs | 13 +- sdk/std/os/io.rs | 145 +++-- sdk/std/os/net.rs | 10 +- sdk/std/os/process.rs | 16 +- sdk/std/sys/fs.rs | 72 ++- sdk/std/sys/net/connection.rs | 55 +- sdk/std/sys/pal/mod.rs | 35 ++ sdk/std/sys/pipe.rs | 26 +- sdk/std/sys/process.rs | 115 ++-- sdk/std/sys/stdio.rs | 22 +- sdk/toyos/Cargo.toml | 5 +- sdk/toyos/src/endow.rs | 5 +- sdk/toyos/src/fd.rs | 532 ++++++++++++++++++ sdk/toyos/src/launch.rs | 81 ++- sdk/toyos/src/lib.rs | 1 + sdk/toyos/src/log/stdio.rs | 91 +-- sdk/toyos/src/net/stream.rs | 51 +- sdk/toyos/src/relocate/mod.rs | 30 +- sdk/toyos/src/spawn.rs | 159 +++--- sdk/toyos/src/surface.rs | 6 - system/console/src/main.rs | 6 +- system/supervisor/src/main.rs | 67 ++- system/terminal/src/main.rs | 6 +- tests/guest/libc/spawn_cloexec.c | 25 +- tests/guest/libc/spawn_signaled.c | 5 - .../rust-tests/src/bin/abuse_elf_loader.rs | 22 +- .../rust-tests/src/bin/abuse_elf_segments.rs | 2 - .../rust-tests/src/bin/abuse_handle_table.rs | 89 +-- .../rust-tests/src/bin/abuse_page_straddle.rs | 2 - .../rust-tests/src/bin/abuse_pipe_owner.rs | 18 +- .../rust-tests/src/bin/abuse_spawn_argv.rs | 2 - .../guest/rust-tests/src/bin/bar_map_again.rs | 7 +- .../guest/rust-tests/src/bin/counters_read.rs | 7 +- .../rust-tests/src/bin/endowment_denied.rs | 13 +- .../rust-tests/src/bin/futex_wake_counts.rs | 15 +- .../rust-tests/src/bin/launch_unsent_stdio.rs | 27 +- .../guest/rust-tests/src/bin/process_tree.rs | 6 +- .../src/bin/spawn_child_ends_first.rs | 28 +- .../rust-tests/src/bin/spawn_image_object.rs | 2 - .../rust-tests/src/bin/spawn_lands_claimed.rs | 2 - .../rust-tests/src/bin/stream_handles.rs | 18 +- 67 files changed, 1613 insertions(+), 1315 deletions(-) delete mode 100644 issues/libc-close-of-a-socket-ends-the-process.md create mode 100644 issues/libc-close-of-a-socket-is-unproven-on-a-connected-socket.md create mode 100644 sdk/toyos/src/fd.rs diff --git a/Cargo.lock b/Cargo.lock index 6ebbb9bd15..cc527cf19c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2161,7 +2161,7 @@ dependencies = [ [[package]] name = "mio" version = "1.2.0" -source = "git+https://github.com/ToyOSOrg/mio?branch=toyos#836d14f1cca5d3597135216430d296aaa712300e" +source = "git+https://github.com/ToyOSOrg/mio?branch=toyos#9f34d36c2eb0f747bbe79be613bf5ee943a5c50d" dependencies = [ "libc", "toyos-abi", @@ -3897,9 +3897,10 @@ dependencies = [ [[package]] name = "socket2" version = "0.6.3" -source = "git+https://github.com/ToyOSOrg/socket2?branch=toyos#12abd8f732277436da2718073cec90f9d2d67f66" +source = "git+https://github.com/ToyOSOrg/socket2?branch=toyos#d38850198a97a55eeccedb5fb8d91a22efc89ba3" dependencies = [ "libc", + "toyos", "windows-sys 0.61.2", ] diff --git a/build/build.rs b/build/build.rs index cfc73bf78d..d8934a1d0e 100644 --- a/build/build.rs +++ b/build/build.rs @@ -2733,7 +2733,8 @@ mod tests { /// Features of a host-tested member that choose a build's world rather /// than revert a decision: loom's atomics, a crate's `std` and its default /// set, the toolchain's `core` and `rustc-dep-of-std`, libc's `std-runtime`, - /// the signer's `sign`, `flaws`, which compiles what a simulator's + /// libc's `fd-owner` and `toyos`'s `fd-table`, which pick the copy that + /// owns the descriptor table, the signer's `sign`, `flaws`, which compiles what a simulator's /// negative gates select at run time, `toyos-crypto`'s `portable`, `wpa2` /// and `rustls-provider`, which `--ci host` tests as forms of its own, and /// of the guest libraries, `toyos-font`'s `ttf`, its TrueType rendering, @@ -2741,6 +2742,8 @@ mod tests { const NOT_A_CONTROL: &[&str] = &[ "core", "default", + "fd-owner", + "fd-table", "flaws", "host-grant", "loom", diff --git a/build/libc.rs b/build/libc.rs index 0ff63f1c8e..3fee47e026 100644 --- a/build/libc.rs +++ b/build/libc.rs @@ -16,7 +16,7 @@ pub const FEATURES: &str = "std-runtime"; pub(crate) const BUILD: [&str; 5] = ["build", "--release", "--features", FEATURES, "--message-format=json"]; /// What [`build_c`] asks cargo for, as [`BUILD`] is [`build`]'s. -pub(crate) const BUILD_C: [&str; 4] = ["rustc", "--release", "--crate-type", "staticlib"]; +pub(crate) const BUILD_C: [&str; 6] = ["rustc", "--release", "--features", "fd-owner", "--crate-type", "staticlib"]; /// Build toyos-libc against the toolchain at `toolchain`, in `target_dir`, and /// install it there as `libtoyos_c.a`. Part of making a sysroot diff --git a/issues/a-childs-end-is-an-event-and-a-parent-takes-its-children-down.md b/issues/a-childs-end-is-an-event-and-a-parent-takes-its-children-down.md index 73fac3aab2..484f5119fd 100644 --- a/issues/a-childs-end-is-an-event-and-a-parent-takes-its-children-down.md +++ b/issues/a-childs-end-is-an-event-and-a-parent-takes-its-children-down.md @@ -64,13 +64,13 @@ directory is `issues/every-program-sees-only-the-files-it-was-given.md`'s. `posix_spawnp` with file actions and attributes start a child by `toyos::spawn`, std's launch-or-spawn rule, which std's `Command` now uses too. A C child starts with descriptors 0–2 and exactly what its file - actions name, a stated departure from POSIX, and a spawned child's table - keeps slots 0–2 for them at its spawn (`HandleTable::for_child`): an - endowment that filled an empty one read as the child's stdio. A stdio - slot a process was started without is no descriptor of its children - (`toyos::spawn::inherited`), and std's stdio answers its errors rather - than reading every one as a closed stream (`is_ebadf`). libc - keeps close-on-exec marks (`fdreq`); `waitpid`, + actions name, a stated departure from POSIX, each endowed as + `fd:`, which the child's descriptor table installs at its number + (`toyos::fd`). A descriptor a process was started without is no + descriptor of its children (`toyos::fd::copy`), and std's stdio answers + its errors rather than reading every one as a closed stream + (`is_ebadf`). libc's close-on-exec flag is the table's per-descriptor + inheritable flag; `waitpid`, `wait` and `wait4` answer each child once, by pid, group or any, with ``'s status for its stage 2 end (`spawnreq`); `environ`, `setenv` and `unsetenv`; `kill` with `SIGKILL` for a child or a group of @@ -95,7 +95,7 @@ directory is `issues/every-program-sees-only-the-files-it-was-given.md`'s. 0); the parent's stdout marked `FD_CLOEXEC`, a 0 that `addopen` opened `O_CLOEXEC`, and a descriptor that `adddup2` made and `addclose` closed, each absent in the child (`F_SETFD`, `O_CLOEXEC` or `addclose` ignored, - and the child's table without its stdio slots kept); the 256th unwaited + and the child's stdio not taken from its endowments); the 256th unwaited child refused `EAGAIN` beside a `poll` of 255 descriptors answering (no capacity check); a negative descriptor beside a pipe, the pipe answering (passed to the kernel, which ends the caller). diff --git a/issues/a-stream-cannot-move-between-processes.md b/issues/a-stream-cannot-move-between-processes.md index 481690bb5e..8cf92ff64d 100644 --- a/issues/a-stream-cannot-move-between-processes.md +++ b/issues/a-stream-cannot-move-between-processes.md @@ -28,8 +28,8 @@ netstack would drain what the child writes as doorbells, with no error on either side. Stage 3 makes std refuse both loudly instead. `File::from_raw_fd` of a stream's handle panics with `toyos::net`'s refusal. Every path std offers to move a raw handle into another process answers that -refusal before anything moves: a spawn's stdio slots, -`CommandExt::inherit_handle` into any slot, `CommandExt::endow` and +refusal before anything moves: a spawn's stdio descriptors, +`CommandExt::inherit_handle` as any descriptor, `CommandExt::endow` and `Command::provide`, each starting no child. `toyos::ipc`'s `send_with` takes only `OwnedHandle`s, and a stream's handle becomes one only through `OwnedHandle::from_raw`, whose contract stage 3 says excludes a handle diff --git a/issues/libc-close-of-a-socket-ends-the-process.md b/issues/libc-close-of-a-socket-ends-the-process.md deleted file mode 100644 index 12becf4a04..0000000000 --- a/issues/libc-close-of-a-socket-ends-the-process.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-10-01 ---- - -# libc's close of a socket ends the process - -Read from the code, not run. A socket's descriptor is `1024` plus its index in -libc's socket table (`sdk/libc/src/socket.rs`, `SOCKET_FD_BASE`), and no -handle. `close` (`sdk/libc/src/posix_io.rs`) passes every descriptor to -`SYS_CLOSE`, and the kernel answers a handle its caller does not hold by -ending the caller (`kernel/src/syscall/handles.rs`, `sys_close`, through -`HandleError::refuse`). `close_socket`, which closes the socket's pipes and -tells netd, is exported under `no_mangle` and called by nothing; the header -gate excuses it by this file (`sdk/libc/arch/src/prototypes.rs`). - -**Exit**: `close` of a socket's descriptor releases the socket and its pipes -and answers 0, `close_socket` is no export, and a guest C case closes a -connected socket and goes on. diff --git a/issues/libc-close-of-a-socket-is-unproven-on-a-connected-socket.md b/issues/libc-close-of-a-socket-is-unproven-on-a-connected-socket.md new file mode 100644 index 0000000000..db2c93d65a --- /dev/null +++ b/issues/libc-close-of-a-socket-is-unproven-on-a-connected-socket.md @@ -0,0 +1,17 @@ +--- +status: open +kind: defect +opened: 2026-10-01 +--- + +# libc's close of a socket is unproven on a connected socket + +A socket's descriptor names an object of libc's own in the process's one +descriptor table (`toyos::fd::Object::opaque`, `sdk/libc/src/socket.rs`), so +`close` (`sdk/libc/src/posix_io.rs`) releases it like any descriptor, and its +release closes the socket's pipes and stream and tells netstack. Before the +table, a socket's number was `1024` plus its index, `close` passed it to +`SYS_CLOSE`, and the kernel ended the caller for naming a handle it did not +hold. `close_socket` is gone. Read from the code; no guest case has run it. + +**Exit**: a guest C case closes a connected socket and goes on. diff --git a/issues/the-capability-end-state-is-twelve-answers.md b/issues/the-capability-end-state-is-twelve-answers.md index e1de26d386..34e61f7f9c 100644 --- a/issues/the-capability-end-state-is-twelve-answers.md +++ b/issues/the-capability-end-state-is-twelve-answers.md @@ -192,18 +192,19 @@ delegation and not a widening. ## 7. What authority is implicit in process creation? — COMMITTED -A child starts with an empty `HandleTable` and gets exactly two vectors: -`slot_map` *duplicates* (the parent keeps its copy) and `endow` *moves* -(`sdk/abi/src/syscall.rs:283`). Both are all-or-nothing and refuse by name — a -slot-map pair naming a handle the parent does not hold ends the parent -(`kernel/src/loader/start.rs:308`), an endowed handle without `TRANSFER` refuses -the spawn before anything leaves the table (`:206`), and the move is the last -thing a spawn does (`kernel/src/loader/mod.rs:649`). **Two things are implicit +A child starts with an empty `HandleTable` and gets exactly one vector, +`endow`, which *moves* (`sdk/abi/src/syscall.rs`, `SpawnArgs`): its stdio +and every other descriptor are endowments labelled `fd:`, which its +userland table numbers, and a console among them is minted for the child. It +is all-or-nothing and refuses by name — an endowed handle without `TRANSFER` +refuses the spawn before anything leaves the table +(`kernel/src/loader/start.rs`, `PendingHandles::commit`), and the move is the +last thing a spawn does (`kernel/src/loader/mod.rs`). **Two things are implicit and nothing else is:** -1. a `Console` slot is *minted fresh* rather than duplicated, at the parent's - rights, because the object is the line buffer - (`kernel/src/loader/start.rs:325`); +1. an endowed `Console` is *minted fresh* rather than moved, at the moved + handle's rights, because the object is the line buffer + (`kernel/src/loader/start.rs`, `PendingHandles::commit`); 2. the spawner is handed a `Process` handle carrying `DUP|TRANSFER|WAIT|READ|MANAGE` by type (`kernel/src/object/ops.rs:74`), and must dup-narrow and close if it wants to hold less. diff --git a/issues/toyos-builds-itself.md b/issues/toyos-builds-itself.md index a8dd7ccf40..68e6b74351 100644 --- a/issues/toyos-builds-itself.md +++ b/issues/toyos-builds-itself.md @@ -64,7 +64,7 @@ libc's state it is `userland/libc` at `15625e0cb`. (`issues/the-kernel-resolves-a-path-by-rules-of-its-own.md`); and socket descriptors that are pollable and can be non-blocking - (`issues/libc-close-of-a-socket-ends-the-process.md`). Read from each + (`issues/libc-close-of-a-socket-is-unproven-on-a-connected-socket.md`). Read from each library's source and not run: SQLite's default VFS takes `fcntl` record locks and keys them on `st_dev` and `st_ino`; libgit2 opens every repository through `realpath` and reads packs through a file `mmap`; curl and libssh2 diff --git a/kernel/src/loader/mod.rs b/kernel/src/loader/mod.rs index a0cd78a5d6..f271687603 100644 --- a/kernel/src/loader/mod.rs +++ b/kernel/src/loader/mod.rs @@ -19,7 +19,7 @@ mod start; mod tls; -pub use start::{build_child_handles, PendingHandles, SLOT_PAIR_LEN}; +pub use start::{build_child_handles, PendingHandles}; pub(crate) use start::{alloc_kernel_stack, KernelStack, Start}; pub use tls::{TlsBlock, DTV_INITIAL_CAPACITY, VARIANT as TLS_VARIANT}; @@ -439,16 +439,18 @@ pub const SUPERVISOR_PATH: &str = "/system/bin/supervisor"; /// Panics on failure: a boot that cannot start the supervisor has nowhere to report to. pub fn spawn_supervisor() -> Pid { let mut handles = HandleTable::new(); + let mut entries = Vec::new(); + let mut labels = Vec::new(); + // One console, the supervisor's descriptors 0 to 2. let console = KObjectRef::Console(crate::object::device::ConsoleObject::new()); - for slot in 0..3 { - let entry = crate::object::HandleEntry::new( - console.clone(), - ops::initial_rights(&console), - ); - let (_, displaced) = handles - .install_at(slot, entry) - .expect("spawn_supervisor: three slots cannot exhaust an empty table"); - assert!(displaced.is_none(), "an empty table had something at slot {slot}"); + for fd in ["0", "1", "2"] { + let entry = crate::object::HandleEntry::new(console.clone(), ops::initial_rights(&console)); + let handle = handles.install(entry).expect("spawn_supervisor: three handles cannot exhaust an empty table"); + let label_off = labels.len() as u32; + labels.extend_from_slice(toyos_abi::syscall::FD_PREFIX.as_bytes()); + labels.extend_from_slice(fd.as_bytes()); + let label_len = labels.len() as u32 - label_off; + entries.push(toyos_abi::syscall::EndowEntry { label_off, label_len, handle, _pad: 0 }); } let cap = KObjectRef::SysCap(crate::object::syscap::SysCap::new()); // Every machine-wide authority the system has: rights only shrink from @@ -470,13 +472,13 @@ pub fn spawn_supervisor() -> Pid { .install(crate::object::HandleEntry::new(cap, rights)) .expect("spawn_supervisor: an empty table refused the system capability"); let label = toyos_abi::syscall::SYSCAP_LABEL; - let mut entries = alloc::vec![toyos_abi::syscall::EndowEntry { - label_off: 0, + entries.push(toyos_abi::syscall::EndowEntry { + label_off: labels.len() as u32, label_len: label.len() as u32, handle: cap_handle, _pad: 0, - }]; - let mut labels = label.as_bytes().to_vec(); + }); + labels.extend_from_slice(label.as_bytes()); // Built by the kernel and owing nobody anything: no table but the supervisor's own holds it. let commit = |own| { start::endow_self(&mut handles, &mut entries, &mut labels, own); diff --git a/kernel/src/loader/start.rs b/kernel/src/loader/start.rs index 4b770306e3..32dc8d8f12 100644 --- a/kernel/src/loader/start.rs +++ b/kernel/src/loader/start.rs @@ -17,12 +17,9 @@ use crate::scheduler; use crate::user_ptr::UserBytes; use toyos_abi::handle::{RawHandle, Rights}; use toyos_abi::syscall::{ - EndowEntry, SyscallError, MAX_SLOT_MAP, MAX_SPAWN_ENDOWMENTS, MAX_SPAWN_LABELS_LEN, SELF_LABEL, + EndowEntry, SyscallError, MAX_SPAWN_ENDOWMENTS, MAX_SPAWN_LABELS_LEN, SELF_LABEL, }; -/// One `[child_slot, parent_handle]` pair of `SpawnArgs::slot_map_ptr`, in bytes. -pub const SLOT_PAIR_LEN: usize = 8; - /// Where a new context first runs. pub(crate) enum Start { /// A program's first instruction, on the stack its loader wrote. @@ -148,11 +145,35 @@ impl PendingHandles { } let mut entries = Vec::with_capacity(moving.len()); + // The consoles this spawn endowed, by the parent's object, each with the child's own. + let mut minted: Vec<(usize, KObjectRef)> = Vec::new(); + // Moved-out console handles, dropped once the parent's lock is let go. + let mut displaced = Vec::new(); for (mut entry, parent_handle) in moving { let moved = data .handles .remove(parent_handle) .expect("an endowed handle verified under this lock stopped resolving"); + // A console is minted, not moved: it is its own line buffer, and one shared + // would splice two processes' half-lines. Two endowments of one parent console + // are one child console, or aliased stdout and stderr would split in two. + let moved = match moved.object() { + KObjectRef::Console(parent_console) => { + let key = Arc::as_ptr(parent_console) as usize; + let object = match minted.iter().find(|(seen, _)| *seen == key) { + Some((_, object)) => object.clone(), + None => { + let object = KObjectRef::Console(crate::object::device::ConsoleObject::new()); + minted.push((key, object.clone())); + object + } + }; + let rights = moved.rights(); + displaced.push(moved); + HandleEntry::new(object, rights) + } + _ => moved, + }; entry.handle = table .install(moved) .expect("a child table with verified room refused an endowment"); @@ -162,6 +183,7 @@ impl PendingHandles { let held = ops::install(&mut data.handles, own.object().clone()) .expect("a caller's table with verified room refused its child"); drop(data); + drop(displaced); endow_self(&mut table, &mut entries, &mut labels, own); Ok((table, Endowments::new(entries, labels), held)) } @@ -187,75 +209,16 @@ pub(super) fn endow_self(table: &mut HandleTable, entries: &mut Vec, labels.extend_from_slice(SELF_LABEL.as_bytes()); } -/// Reads `SpawnArgs`'s two handle vectors into the child's pending handle state. +/// Reads `SpawnArgs`'s endowments into the child's pending handle state. // Every refusal here precedes any child state, so nothing is enqueued or orphaned. -pub fn build_child_handles( - slot_map: &UserBytes, - endow: &UserBytes, - labels: &[u8], -) -> Result { +pub fn build_child_handles(endow: &UserBytes, labels: &[u8]) -> Result { if endow.len() / ENDOW_ENTRY_LEN > MAX_SPAWN_ENDOWMENTS { return Err(SyscallError::InvalidArgument.into()); } - // Checked before the loop: `install_at`'s cap misses a repeated slot, which would duplicate - // handles under the parent's lock without bound. - if slot_map.len() / SLOT_PAIR_LEN > MAX_SLOT_MAP { - return Err(SyscallError::InvalidArgument.into()); - } if labels.len() > MAX_SPAWN_LABELS_LEN { return Err(SyscallError::InvalidArgument.into()); } - let data_arc = process_data(); - let data = data_arc.lock(); - let mut handles = HandleTable::for_child(); - // Dropped after the lock releases, not inside it: `install_at` leaves *where* to the caller. - let mut displaced = Vec::new(); - // Parent console objects already minted a replacement for, keyed by address. - let mut minted: Vec<(usize, crate::object::KObjectRef)> = Vec::new(); - for i in 0..slot_map.len() / SLOT_PAIR_LEN { - let mut pair = [0u8; SLOT_PAIR_LEN]; - slot_map.read_at(i * SLOT_PAIR_LEN, &mut pair); - let child_slot = u32::from_ne_bytes([pair[0], pair[1], pair[2], pair[3]]); - let parent = RawHandle(u32::from_ne_bytes([pair[4], pair[5], pair[6], pair[7]])); - // An unheld handle ends the parent (`object::HandleError`'s rule) rather than silently skipping the slot. - let rights = data.handles.rights_of(parent)?; - // A device claim carries no `DUP` and is refused by name. - let entry = data.handles.duplicate_entry(parent, rights)?; - // A console is minted, not duplicated: it is its own line buffer, and sharing one would splice - // two half-lines together. - let entry = match entry.object() { - crate::object::KObjectRef::Console(parent_console) => { - // Two slots naming the same parent console land on one child console — what this - // address-keyed lookup preserves — or aliased stdout/stderr would split into two buffers. - let key = alloc::sync::Arc::as_ptr(parent_console) as usize; - let object = match minted.iter().find(|(seen, _)| *seen == key) { - Some((_, object)) => object.clone(), - None => { - let object = crate::object::KObjectRef::Console( - crate::object::device::ConsoleObject::new(), - ); - minted.push((key, object.clone())); - object - } - }; - // The duplicate is displaced too, dropped outside the parent's lock. - displaced.push(Some(entry)); - crate::object::HandleEntry::new(object, rights) - } - _ => entry, - }; - let slot = u16::try_from(child_slot) - .map_err(|_| SyscallError::ResourceExhausted)?; - let (_, replaced) = handles - .install_at(slot, entry) - .map_err(|_| SyscallError::ResourceExhausted)?; - displaced.push(replaced); - } - - drop(data); - drop(displaced); - let mut raw = alloc::vec![0u8; endow.len()]; endow.read_at(0, &mut raw); - Ok(PendingHandles { table: handles, endow: raw, labels: labels.to_vec() }) + Ok(PendingHandles { table: HandleTable::new(), endow: raw, labels: labels.to_vec() }) } diff --git a/kernel/src/object/handle.rs b/kernel/src/object/handle.rs index a3ca66818a..06f3f59c31 100644 --- a/kernel/src/object/handle.rs +++ b/kernel/src/object/handle.rs @@ -119,6 +119,10 @@ impl HandleEntry { &self.object } + pub fn rights(&self) -> Rights { + self.rights + } + /// A second handle to the same object, carrying no more than this one. pub fn duplicate(&self, rights: Rights) -> Result { if !self.rights.contains(Rights::DUP) { @@ -172,25 +176,6 @@ impl HandleTable { Self { slots: Vec::new(), free: Vec::new() } } - /// A spawned child's table, whose slots 0–2 are its stdin, stdout and - /// stderr: at its spawn they hold what the slot map names and nothing - /// else, so no endowment installed into a gap among them reads as the - /// child's stdio. Once the child closes one, its next handle may land - /// there. - pub fn for_child() -> Self { - let empty = || Slot::Serving { generation: 0, entry: None }; - Self { slots: alloc::vec![empty(), empty(), empty()], free: Vec::new() } - } - - /// The handle `slot` holds, at its generation, or `None` for an empty or - /// retired slot. - pub fn held_at(&self, slot: u16) -> Option { - match self.slots.get(slot as usize)? { - Slot::Serving { generation, entry: Some(_) } => Some(RawHandle::new(slot, *generation)), - _ => None, - } - } - /// Whether `n` more `install`s can all succeed. // A retired slot counts in neither term, so it permanently costs one slot of room. pub fn has_room(&self, n: usize) -> bool { @@ -216,32 +201,6 @@ impl HandleTable { Ok(RawHandle::new(slot, 0)) } - /// Install at a caller-chosen slot, replacing whatever is there. - #[must_use = "the displaced entry must be dropped by the caller"] - pub fn install_at( - &mut self, - slot: u16, - entry: HandleEntry, - ) -> Result<(RawHandle, Option), TableFull> { - let slot_index = slot as usize; - // A slot past MAX_HANDLES is the table's cap, not a malformed argument. - if slot_index >= MAX_HANDLES { - return Err(TableFull); - } - while self.slots.len() <= slot_index { - self.free.push(self.slots.len() as u16); - self.slots.push(Slot::Serving { generation: 0, entry: None }); - } - self.free.retain(|&s| s != slot); - // A retired slot answers `TableFull` too: the table no longer has that slot at all. - let Slot::Serving { generation, entry: at } = &mut self.slots[slot_index] else { - return Err(TableFull); - }; - // Generation stays put: dup2 keeps the number the caller already holds valid. - let displaced = at.replace(entry); - Ok((RawHandle::new(slot, *generation), displaced)) - } - /// The entry a handle names, or why it names none; a retired slot is `Stale`, not `BadHandle`, since the slot is in range. fn entry_of(&self, h: RawHandle) -> Result<&HandleEntry, HandleError> { match self.slots.get(h.slot() as usize).ok_or(HandleError::BadHandle)? { @@ -303,15 +262,6 @@ impl HandleTable { Ok(self.entry_of(h)?.rights) } - /// A duplicate for *another* table — a child's, built at spawn. - pub fn duplicate_entry( - &self, - h: RawHandle, - rights: Rights, - ) -> Result { - self.entry_of(h)?.duplicate(rights) - } - /// Take a handle out of the table and retire its slot. #[must_use = "the removed entry must be dropped by the caller"] pub fn remove(&mut self, h: RawHandle) -> Result { diff --git a/kernel/src/syscall/dispatch.rs b/kernel/src/syscall/dispatch.rs index 2f71597651..3dbd04988b 100644 --- a/kernel/src/syscall/dispatch.rs +++ b/kernel/src/syscall/dispatch.rs @@ -30,7 +30,7 @@ use super::fs::{ spawn_cwd, sys_chdir, sys_delete, sys_getcwd, sys_mkdir, sys_open, sys_readdir, sys_readlink, sys_rename, sys_rmdir, sys_stat, sys_symlink, }; -use super::handles::{sys_close, sys_dup2, sys_handle_dup, sys_handle_dup_slot, with_object, with_object_ref}; +use super::handles::{sys_close, sys_handle_dup, with_object, with_object_ref}; use super::io::{ sys_null, sys_random, sys_read, sys_read_nonblock, sys_write, sys_write_nonblock, }; @@ -192,14 +192,8 @@ pub(crate) fn syscall_dispatch(num: u64, a1: u64, a2: u64, a3: u64, a4: u64) -> { return SyscallError::InvalidArgument.to_u64(); } - // slot_map and endow stay windows, not copies: both counts are userland's - // and each entry is read once. labels is copied because the child keeps it. - let Some(slot_map) = (args.slot_map_count as usize) - .checked_mul(crate::loader::SLOT_PAIR_LEN) - .and_then(|len| ctx.user_bytes(UserAddr::new(args.slot_map_ptr), len as u64)) - else { - return bad_addr; - }; + // endow stays a window, not a copy: its count is userland's and each entry + // is read once. labels is copied because the child keeps it. let Some(endow) = (args.endow_count as usize) .checked_mul(core::mem::size_of::()) .and_then(|len| ctx.user_bytes(UserAddr::new(args.endow_ptr), len as u64)) @@ -210,7 +204,7 @@ pub(crate) fn syscall_dispatch(num: u64, a1: u64, a2: u64, a3: u64, a4: u64) -> Ok(bytes) => bytes, Err(e) => return e.to_u64(), }; - let pending = match process::build_child_handles(&slot_map, &endow, &labels) { + let pending = match process::build_child_handles(&endow, &labels) { Ok(built) => built, Err(e) => return e.refuse(), }; @@ -280,8 +274,6 @@ pub(crate) fn syscall_dispatch(num: u64, a1: u64, a2: u64, a3: u64, a4: u64) -> } SYS_NANOSLEEP => sys_nanosleep(a1), SYS_HANDLE_DUP => sys_handle_dup(RawHandle(a1 as u32), a2), - SYS_HANDLE_DUP_AT => sys_dup2(RawHandle(a1 as u32), a2), - SYS_HANDLE_DUP_SLOT => sys_handle_dup_slot(a1), SYS_NULL => sys_null(a1), SYS_GETPID => process::current_process().raw() as u64, SYS_RENAME => { diff --git a/kernel/src/syscall/handles.rs b/kernel/src/syscall/handles.rs index d7e9ee53b6..9b76e6f4bf 100644 --- a/kernel/src/syscall/handles.rs +++ b/kernel/src/syscall/handles.rs @@ -74,52 +74,3 @@ pub(super) fn sys_handle_dup(h: RawHandle, want: u64) -> u64 { } } -/// A second handle to whatever the caller's `slot` holds, `NotFound` for one -/// that holds nothing. -pub(super) fn sys_handle_dup_slot(slot: u64) -> u64 { - let Ok(slot) = u16::try_from(slot) else { - return SyscallError::NotFound.to_u64(); - }; - let duplicated = process::with_process_data(|data| { - let held = data.handles.held_at(slot).ok_or(SyscallError::NotFound)?; - let rights = data.handles.rights_of(held)?; - Ok::<_, crate::object::Refusal>(data.handles.duplicate(held, rights)?) - }); - match duplicated { - Ok(new_h) => new_h.0 as u64, - Err(e) => e.refuse(), - } -} - -/// A second handle to `old`, installed at `slot`. -/// -/// `slot` is a slot, not a handle: a handle carries a generation this call has no business being told. -/// -/// Displacing a handle wakes nobody: dropping the entry is what gives back the reference a wake is owed for. -pub(super) fn sys_dup2(old: RawHandle, slot: u64) -> u64 { - let Ok(slot) = u16::try_from(slot) else { - return SyscallError::ResourceExhausted.to_u64(); - }; - let result = process::with_process_data(|data| { - let rights = data.handles.rights_of(old)?; - let entry = data.handles.duplicate_entry(old, rights)?; - data.handles - .install_at(slot, entry) - .map_err(|_| SyscallError::ResourceExhausted.into()) - .map(|(new_h, displaced)| (new_h, Displaced(displaced))) - }); - match result { - // Dropped here, outside the process lock: `HandleEntry`'s Drop can take - // `vfs::lock()`, which every sibling thread's page-fault handler also nests under. - Ok((new_h, displaced)) => { - drop(displaced); - new_h.0 as u64 - } - Err(e) => crate::object::Refusal::refuse(e), - } -} - -/// A displaced handle's drop obligation, carried out of a closure by [`sys_dup2`]. -// Never read: dropping it is the effect. -#[expect(dead_code)] -struct Displaced(Option); diff --git a/rust b/rust index 11e31a5277..a56b4057c5 160000 --- a/rust +++ b/rust @@ -1 +1 @@ -Subproject commit 11e31a52778f875f3e021db96d35a45fb0f22466 +Subproject commit a56b4057c58e624aedc895643f4f5e83f32c1912 diff --git a/sdk/abi/src/syscall.rs b/sdk/abi/src/syscall.rs index 51a1961b1c..fff9a5c515 100644 --- a/sdk/abi/src/syscall.rs +++ b/sdk/abi/src/syscall.rs @@ -100,9 +100,6 @@ pub const SYS_READ_NONBLOCK: u64 = 66; pub const SYS_WRITE_NONBLOCK: u64 = 67; pub const SYS_EXIT: u64 = 72; pub const SYS_GET_ENV: u64 = 73; -/// A second handle to the same object, at a slot the caller picks. See -/// [`dup2`]. -pub const SYS_HANDLE_DUP_AT: u64 = 74; pub const SYS_CLOCK_EPOCH: u64 = 75; pub const SYS_PIPE_MAP: u64 = 77; pub const SYS_SYMLINK: u64 = 81; @@ -274,10 +271,6 @@ pub const SYS_TRACE_READ: u64 = 127; /// kernel mediates, the firmware's Global Lock, or the power-off's sleep type. /// See [`acpi_access`], [`acpi_lock_take`] and [`acpi_s5`]. pub const SYS_ACPI: u64 = 128; -/// A second handle to whatever one of the caller's slots holds, or `NotFound` -/// for an empty slot: how a spawn hands on a stdio slot it may have been -/// started without. See [`dup_slot`]. -pub const SYS_HANDLE_DUP_SLOT: u64 = 129; /// A fresh null handle: every read answers end of file and every write takes /// all it is given. See [`null`]. pub const SYS_NULL: u64 = 130; @@ -309,11 +302,13 @@ pub const WNOHANG: u64 = 1; crate::user_safe! { /// Arguments for the `SYS_SPAWN` syscall, passed as a single pointer. /// - /// **Two vectors, two verbs.** `slot_map` *duplicates* — the parent keeps its - /// stdout — and `endow` *moves*, so a parent that wants to keep what it endows - /// duplicates first. That is what makes endowing a device claim work with no - /// special case: a claim carries no [`Rights::DUP`], so the move is the only - /// expressible form and the parent provably no longer holds it. + /// **Every handle a child starts with is endowed, and an endowment moves**, + /// so a parent that wants to keep what it endows duplicates first. That is + /// what makes endowing a device claim work with no special case: a claim + /// carries no [`Rights::DUP`], so the move is the only expressible form and + /// the parent provably no longer holds it. A child's descriptors are + /// endowments labelled [`FD_PREFIX`] and their number; a console among + /// them is the child's own, minted for it. /// /// [`Rights::DUP`]: crate::handle::Rights::DUP #[derive(Clone, Copy)] @@ -326,10 +321,6 @@ crate::user_safe! { pub path_len: u64, pub argv_ptr: u64, pub argv_len: u64, - /// `[[child_slot: u32, parent_handle: RawHandle]]`, duplicated into the - /// child. Stdio and nothing else, in practice. - pub slot_map_ptr: u64, - pub slot_map_count: u64, pub env_ptr: u64, pub env_len: u64, /// `[EndowEntry]`, moved out of the parent's table. @@ -404,6 +395,10 @@ pub const SELF_LABEL: &str = "self"; pub const SERVE_PREFIX: &str = "serve:"; /// `dev:`: the claim for a device class this program was given. pub const DEV_PREFIX: &str = "dev:"; +/// `fd:`: the child's descriptor `n`, which its descriptor table installs +/// at that number (`toyos::fd`). Numbering is userland's: the kernel only +/// moves the handle. +pub const FD_PREFIX: &str = "fd:"; /// `provide:`: a connector a *launching client* transferred, beside the /// namespace entry the launcher made from it. /// @@ -421,15 +416,6 @@ pub const PROVIDE_PREFIX: &str = "provide:"; pub const MAX_ENDOWMENTS: usize = 32; /// `(label, handle)` pairs one spawn may carry: the kernel adds [`SELF_LABEL`]. pub const MAX_SPAWN_ENDOWMENTS: usize = MAX_ENDOWMENTS - 1; -/// `(child slot, parent handle)` pairs one spawn may carry. -/// -/// **Derived rather than chosen.** A slot map installs into the child's table, -/// which has [`RawHandle::MAX_SLOTS`] slots, so a longer one necessarily names -/// a slot twice and the second pair says everything the first did. Without it -/// the only bound is the 2 MiB window the arguments are read through, and every -/// pair is a `duplicate_entry` under the parent's own lock — enough of them to -/// pass `MAX_HEAP_ALLOC`, where the allocator's refusal is a kernel panic. -pub const MAX_SLOT_MAP: usize = RawHandle::MAX_SLOTS; /// Bytes of label blob one endowment table holds, [`SELF_LABEL`]'s among them. pub const MAX_LABELS_LEN: usize = 4096; /// Bytes of label blob one spawn may carry: the kernel adds [`SELF_LABEL`]'s. @@ -2058,28 +2044,6 @@ pub fn dup_narrowed(handle: RawHandle, rights: Rights) -> Result Result { - check(syscall(SYS_HANDLE_DUP_AT, handle.0 as u64, slot as u64, 0, 0)) - .map(|v| RawHandle(v as u32)) -} - -/// A second handle to whatever this process's `slot` holds, with its rights; -/// `Err(NotFound)` for a slot that holds nothing. Asked of the process's own -/// table by slot, so an empty one is an answer and not a handle it does not -/// hold. -pub fn dup_slot(slot: u16) -> Result { - check(syscall(SYS_HANDLE_DUP_SLOT, slot as u64, 0, 0, 0)).map(|v| RawHandle(v as u32)) -} - /// A fresh null handle carrying `rights`: a read answers 0 bytes, a write of /// n bytes answers n, and a poll is ready at once in either direction. It /// names nothing and so carries no authority; `rights` may be any subset of diff --git a/sdk/libc/Cargo.toml b/sdk/libc/Cargo.toml index 4ed0122f42..067604a345 100644 --- a/sdk/libc/Cargo.toml +++ b/sdk/libc/Cargo.toml @@ -8,6 +8,9 @@ license = "MIT OR Apache-2.0" [features] # Enable when linking with Rust std (skips panic_handler and global_allocator) std-runtime = [] +# The C-only build's: this copy of `toyos` owns the process's descriptor +# table, which in a Rust program std's owns (`toyos::fd`). +fd-owner = ["toyos/fd-table"] [dependencies] toyos-abi = { path = "../abi" } diff --git a/sdk/libc/arch/src/descriptor_requests.rs b/sdk/libc/arch/src/descriptor_requests.rs index c17c4185eb..9c437450be 100644 --- a/sdk/libc/arch/src/descriptor_requests.rs +++ b/sdk/libc/arch/src/descriptor_requests.rs @@ -1,6 +1,4 @@ -//! What `fcntl` decides before the kernel is asked, `F_DUPFD`'s walk to its -//! floor over a handle table numbered as the kernel numbers one -//! (`kernel/src/object/handle.rs`), and each descriptor's close-on-exec mark. +//! What `fcntl` decides before the descriptor table is asked. use crate::fdreq::{self, Command}; use crate::header::{self, FCNTL_H}; @@ -16,10 +14,10 @@ fn fcntl_reads_an_int_argument_from_the_lower_half_of_its_register() { for (cmd, arg, want) in [ ("F_DUPFD", MINUS_ONE_AS_CLANG_PASSES_IT, Command::Invalid), ("F_DUPFD", u64::MAX, Command::Invalid), - ("F_DUPFD", 0xdead_beef_0000_000a, Command::DupAtLeast(10)), - ("F_DUPFD", 0, Command::DupAtLeast(0)), - ("F_DUPFD", 4095, Command::DupAtLeast(4095)), - // `{OPEN_MAX}`, the slots a table has. + ("F_DUPFD", 0xdead_beef_0000_000a, Command::DupAtLeast { floor: 10, cloexec: false }), + ("F_DUPFD", 0, Command::DupAtLeast { floor: 0, cloexec: false }), + ("F_DUPFD", 4095, Command::DupAtLeast { floor: 4095, cloexec: false }), + // `{OPEN_MAX}`, the descriptors a table has. ("F_DUPFD", 4096, Command::Invalid), ("F_DUPFD", 0x7fff_ffff, Command::Invalid), ("F_GETFD", u64::MAX, Command::GetFd), @@ -33,7 +31,8 @@ fn fcntl_reads_an_int_argument_from_the_lower_half_of_its_register() { ("F_SETLKW", 0, Command::Invalid), ("F_SETOWN", 0, Command::Unsupported), ("F_GETOWN", 0, Command::Unsupported), - ("F_DUPFD_CLOEXEC", 10, Command::Unsupported), + ("F_DUPFD_CLOEXEC", 10, Command::DupAtLeast { floor: 10, cloexec: true }), + ("F_DUPFD_CLOEXEC", 4096, Command::Invalid), ] { assert_eq!(fdreq::command(header::int(FCNTL_H, cmd), arg), want, "fcntl(_, {cmd}, {arg:#x})"); } @@ -41,120 +40,3 @@ fn fcntl_reads_an_int_argument_from_the_lower_half_of_its_register() { assert_eq!(fdreq::command(cmd, 0), Command::Invalid, "fcntl(_, {cmd}, 0)"); } } - -/// A handle table as the kernel keeps one: a fresh slot is the next index at -/// generation 0, numbered as the bare index; a closed slot goes on a free list -/// one generation on, and a `dup` takes the free list's last first. -struct Table { - generations: Vec, - held: Vec, - free: Vec, -} - -impl Table { - fn with(open: usize) -> Table { - Table { generations: vec![0; open], held: vec![true; open], free: Vec::new() } - } - - fn dup(&mut self) -> Result { - let slot = match self.free.pop() { - Some(slot) => slot, - None if self.generations.len() < 4096 => { - self.generations.push(0); - self.held.push(false); - self.generations.len() - 1 - } - None => return Err(()), - }; - self.held[slot] = true; - Ok((self.generations[slot] << 12) | slot as u32) - } - - fn close(&mut self, number: u32) { - let slot = (number & 0xfff) as usize; - assert!(self.held[slot] && self.generations[slot] == number >> 12, "closed {number:#x}, which is not held"); - self.held[slot] = false; - self.generations[slot] += 1; - self.free.push(slot); - } - - fn holding(&self) -> usize { - self.held.iter().filter(|&&h| h).count() - } -} - -/// Every floor under the slot count, over a table nothing has churned and over -/// one whose every slot has been closed once: the answer is at or above the -/// floor, and the walk never holds more than one duplicate besides it. -#[test] -fn f_dupfd_meets_its_floor_holding_one_duplicate_at_most() { - for churned in [false, true] { - for floor in [0u32, 1, 3, 4, 10, 255, 4094, 4095] { - let mut table = Table::with(4); - if churned { - let all: Vec = (0..4092).map(|_| table.dup().unwrap()).collect(); - all.into_iter().for_each(|n| table.close(n)); - } - let before = table.holding(); - let table = core::cell::RefCell::new(table); - let mut most = 0; - let answer = fdreq::dup_at_least( - floor, - || { - let mut t = table.borrow_mut(); - most = most.max(t.holding() + 1 - before); - t.dup() - }, - |below| table.borrow_mut().close(below), - ) - .unwrap(); - assert!(answer >= floor, "floor {floor} answered {answer:#x} (churned: {churned})"); - if !churned && floor <= 4 { - // The first duplicate, slot 4, meets the floor and is answered. - assert_eq!(answer, 4, "floor {floor}"); - } - assert_eq!(most, 1, "floor {floor} held {most} duplicates at once (churned: {churned})"); - assert_eq!(table.borrow().holding(), before + 1, "floor {floor} left duplicates open (churned: {churned})"); - } - } -} - -/// A descriptor's close-on-exec mark through `open` with and without -/// `O_CLOEXEC`, `F_SETFD` either way, and `F_GETFD`. -#[test] -fn each_descriptor_keeps_its_own_close_on_exec_mark() { - let (cloexec, read_write) = (header::int(FCNTL_H, "O_CLOEXEC"), header::int(FCNTL_H, "O_RDWR")); - let fd_cloexec = header::int(FCNTL_H, "FD_CLOEXEC"); - let mut marks = fdreq::CloseOnExec::new(); - marks.opened(3, read_write | cloexec); - marks.opened(4, read_write); - assert_eq!((marks.flags(3), marks.flags(4)), (fd_cloexec, 0), "open"); - - marks.set(3, false); - marks.set(4, true); - assert_eq!((marks.flags(3), marks.flags(4)), (0, fd_cloexec), "F_SETFD"); - - // A later handle in slot 4 (one generation on), which is a number of its own. - assert_eq!(marks.flags(4 | 1 << 12), 0, "the next generation of slot 4"); - - // `open` without `O_CLOEXEC` of a number marked. - marks.set(5, true); - marks.opened(5, read_write); - assert_eq!(marks.flags(5), 0, "open over a mark"); -} - -/// A slot is marked close-on-exec while the descriptor marked there is open: -/// a mark goes with its `close`, and a later descriptor at the slot is not -/// marked by an earlier one's. -#[test] -fn a_slot_is_marked_by_its_live_descriptor() { - use toyos_abi::RawHandle; - let mut marks = fdreq::CloseOnExec::new(); - let first = RawHandle::new(1, 0).0 as i32; - marks.set(first, true); - assert!(marks.slot_marked(1) && !marks.slot_marked(0)); - marks.set(first, false); - assert!(!marks.slot_marked(1), "closed"); - marks.set(RawHandle::new(5, 2).0 as i32, true); - assert!(marks.slot_marked(5), "a descriptor at a later generation"); -} diff --git a/sdk/libc/arch/src/prototypes.rs b/sdk/libc/arch/src/prototypes.rs index bff58a7563..3dfcec7988 100644 --- a/sdk/libc/arch/src/prototypes.rs +++ b/sdk/libc/arch/src/prototypes.rs @@ -21,7 +21,6 @@ const UNDECLARED: &[(&str, &str)] = &[ ("_Unwind_Resume", "what the precompiled `alloc`'s landing pads name"), ("__cxa_atexit", "the C++ ABI's, which `cxxabi.h` declares"), ("__cxa_thread_atexit_impl", "what libc++abi's `__cxa_thread_atexit` calls, which it declares"), - ("close_socket", "`close`'s arm for a socket, which `close` never calls: issues/libc-close-of-a-socket-ends-the-process.md"), ]; fn libc() -> PathBuf { diff --git a/sdk/libc/arch/src/spawn_requests.rs b/sdk/libc/arch/src/spawn_requests.rs index 2535137c40..4175d2c0a7 100644 --- a/sdk/libc/arch/src/spawn_requests.rs +++ b/sdk/libc/arch/src/spawn_requests.rs @@ -17,13 +17,22 @@ const PIPE: u32 = 0x2003; /// Every stdio descriptor held and none marked. const HELD: [Option<(u32, bool)>; 3] = [Some((STDIN, false)), Some((STDOUT, false)), Some((STDERR, false))]; -/// The plan, with `open` handing out handles from 0x5000 and recording each -/// close of a handle it opened. +/// This process's descriptor beyond 2 the tests name, holding [`PIPE`]. +const PIPE_FD: u32 = 9; + +/// The plan, for this process's `stdio` and its descriptor [`PIPE_FD`], with +/// `open` handing out handles from 0x5000 and recording each close of a +/// handle it opened. fn planned(stdio: [Option<(u32, bool)>; 3], actions: &[Action<&str>]) -> (Result, Vec) { let next = RefCell::new(0x5000); let closed = RefCell::new(Vec::new()); + let held = |fd: u32| match fd { + 0..=2 => stdio[fd as usize], + PIPE_FD => Some((PIPE, false)), + _ => None, + }; let plan = plan( - stdio, + held, actions, |path, _, _| match *path { "missing" => Err(crate::errcode::ENOENT), @@ -43,9 +52,9 @@ fn planned(stdio: [Option<(u32, bool)>; 3], actions: &[Action<&str>]) -> (Result #[test] fn a_child_starts_with_stdio_alone() { let (plan, _) = planned(HELD, &[]); - assert_eq!(plan, Ok(Plan { slots: vec![[0, STDIN], [1, STDOUT], [2, STDERR]], opened: vec![] })); + assert_eq!(plan, Ok(Plan { fds: vec![(0, STDIN), (1, STDOUT), (2, STDERR)], opened: vec![] })); let (plan, _) = planned([Some((STDIN, false)), None, Some((STDERR, false))], &[]); - assert_eq!(plan.unwrap().slots, [[0, STDIN], [2, STDERR]], "a stdout this process closed"); + assert_eq!(plan.unwrap().fds, [(0, STDIN), (2, STDERR)], "a stdout this process closed"); } /// The stage's three: this process's stdout marked `FD_CLOEXEC`, a 0 that @@ -55,16 +64,16 @@ fn a_child_starts_with_stdio_alone() { fn what_close_on_exec_or_an_addclose_names_is_absent() { let cloexec = header::int(FCNTL_H, "O_CLOEXEC"); let (plan, _) = planned([Some((STDIN, false)), Some((STDOUT, true)), Some((STDERR, false))], &[]); - assert_eq!(plan.unwrap().slots, [[0, STDIN], [2, STDERR]], "stdout marked FD_CLOEXEC"); + assert_eq!(plan.unwrap().fds, [(0, STDIN), (2, STDERR)], "stdout marked FD_CLOEXEC"); let open = Action::Open { fd: 0, path: "/tmp/in", oflag: cloexec, mode: 0 }; let (plan, _) = planned(HELD, &[open]); let plan = plan.unwrap(); - assert_eq!(plan.slots, [[1, STDOUT], [2, STDERR]], "a 0 opened O_CLOEXEC"); + assert_eq!(plan.fds, [(1, STDOUT), (2, STDERR)], "a 0 opened O_CLOEXEC"); assert_eq!(plan.opened, [0x5000], "the file opened here is this process's to close"); - let (plan, _) = planned(HELD, &[Action::Dup2 { fd: PIPE, newfd: 1 }, Action::Close { fd: 1 }]); - assert_eq!(plan.unwrap().slots, [[0, STDIN], [2, STDERR]], "a 1 adddup2 made and addclose closed"); + let (plan, _) = planned(HELD, &[Action::Dup2 { fd: PIPE_FD, newfd: 1 }, Action::Close { fd: 1 }]); + assert_eq!(plan.unwrap().fds, [(0, STDIN), (2, STDERR)], "a 1 adddup2 made and addclose closed"); } /// `adddup2` of a descriptor of this process's beyond 2 is the one way the @@ -72,11 +81,11 @@ fn what_close_on_exec_or_an_addclose_names_is_absent() { /// closed is `EBADF`, as is a stdio descriptor this process does not hold. #[test] fn an_adddup2_names_a_descriptor_for_the_child() { - let (plan, _) = planned(HELD, &[Action::Dup2 { fd: PIPE, newfd: 1 }]); - assert_eq!(plan.unwrap().slots, [[0, STDIN], [1, PIPE], [2, STDERR]]); - let (plan, _) = planned(HELD, &[Action::Dup2 { fd: PIPE, newfd: 7 }]); - assert_eq!(plan.unwrap().slots, [[0, STDIN], [1, STDOUT], [2, STDERR], [7, PIPE]]); - let (plan, _) = planned(HELD, &[Action::Close { fd: PIPE }, Action::Dup2 { fd: PIPE, newfd: 1 }]); + let (plan, _) = planned(HELD, &[Action::Dup2 { fd: PIPE_FD, newfd: 1 }]); + assert_eq!(plan.unwrap().fds, [(0, STDIN), (1, PIPE), (2, STDERR)]); + let (plan, _) = planned(HELD, &[Action::Dup2 { fd: PIPE_FD, newfd: 7 }]); + assert_eq!(plan.unwrap().fds, [(0, STDIN), (1, STDOUT), (2, STDERR), (7, PIPE)]); + let (plan, _) = planned(HELD, &[Action::Close { fd: PIPE_FD }, Action::Dup2 { fd: PIPE_FD, newfd: 1 }]); assert_eq!(plan, Err(EBADF), "a descriptor closed in the child"); let (plan, _) = planned([None, Some((STDOUT, false)), None], &[Action::Dup2 { fd: 2, newfd: 1 }]); assert_eq!(plan, Err(EBADF), "a stderr this process does not hold"); @@ -93,9 +102,9 @@ fn the_actions_run_in_order() { Action::Dup2 { fd: 2, newfd: 1 }, ]; let (plan, _) = planned(HELD, &actions); - assert_eq!(plan.unwrap().slots, [[0, STDIN], [1, 0x5000], [2, 0x5000]]); + assert_eq!(plan.unwrap().fds, [(0, STDIN), (1, 0x5000), (2, 0x5000)]); let (plan, _) = planned([Some((STDIN, false)), Some((STDOUT, true)), None], &[Action::Dup2 { fd: 1, newfd: 1 }]); - assert_eq!(plan.unwrap().slots, [[0, STDIN], [1, STDOUT]], "dup2 of a descriptor onto itself"); + assert_eq!(plan.unwrap().fds, [(0, STDIN), (1, STDOUT)], "dup2 of a descriptor onto itself"); } /// An open that fails ends the plan, and every file the plan opened before it @@ -111,20 +120,18 @@ fn a_failed_open_closes_what_the_plan_opened() { assert_eq!(closed, [0x5000]); } -/// A descriptor an action makes is a slot of the child's table; one it reads -/// may be this process's at a slot's later generation, past every slot. +/// A descriptor an action makes or reads is one a table holds, and one +/// this process does not hold is `EBADF`. #[test] -fn a_descriptor_made_past_the_childs_table_is_ebadf_and_one_read_is_not() { +fn a_descriptor_past_a_table_or_not_held_is_ebadf() { assert_eq!(child_fd(0), Ok(0)); assert_eq!(child_fd(4095), Ok(4095)); for fd in [-1, 4096, i32::MAX] { assert_eq!(child_fd(fd), Err(EBADF), "{fd}"); + assert_eq!(any_fd(fd), Err(EBADF), "{fd}"); } - let reused = toyos_abi::RawHandle::new(3, 1).0; - assert_eq!(any_fd(reused as i32), Ok(reused)); - assert_eq!(any_fd(-1), Err(EBADF)); - let (plan, _) = planned(HELD, &[Action::Dup2 { fd: reused, newfd: 0 }]); - assert_eq!(plan.unwrap().slots, [[0, reused], [1, STDOUT], [2, STDERR]]); + let (plan, _) = planned(HELD, &[Action::Dup2 { fd: 8, newfd: 0 }]); + assert_eq!(plan, Err(EBADF), "a descriptor this process does not hold"); } /// ``'s macros, as the header spells them. diff --git a/sdk/libc/src/errcode.rs b/sdk/libc/src/errcode.rs index 63909fbbac..4b5215b031 100644 --- a/sdk/libc/src/errcode.rs +++ b/sdk/libc/src/errcode.rs @@ -20,6 +20,8 @@ pub(crate) const EEXIST: i32 = 17; pub(crate) const ENODEV: i32 = 19; pub(crate) const ENOTDIR: i32 = 20; pub(crate) const EINVAL: i32 = 22; +pub(crate) const EMFILE: i32 = 24; +pub(crate) const ENOTTY: i32 = 25; pub(crate) const ENOSPC: i32 = 28; pub(crate) const EPIPE: i32 = 32; pub(crate) const EDOM: i32 = 33; diff --git a/sdk/libc/src/fdreq.rs b/sdk/libc/src/fdreq.rs index d641fbdbe2..7eb32e9a5c 100644 --- a/sdk/libc/src/fdreq.rs +++ b/sdk/libc/src/fdreq.rs @@ -1,12 +1,5 @@ -//! What `fcntl` decides before the kernel is asked, and the close-on-exec -//! marks `open`, `fcntl`, `close` and `dup2` keep. It reads nothing but what -//! it is handed, so the host tests it (`toyos-libc-copies`). - -use alloc::collections::BTreeSet; - -use toyos_abi::RawHandle; - -const O_CLOEXEC: i32 = 0x80000; +//! What `fcntl` decides before the descriptor table is asked. It reads +//! nothing but what it is handed, so the host tests it (`toyos-libc-copies`). const F_DUPFD: i32 = 0; const F_GETFD: i32 = 1; @@ -19,13 +12,17 @@ const F_SETLKW: i32 = 7; const F_SETOWN: i32 = 8; const F_GETOWN: i32 = 9; const F_DUPFD_CLOEXEC: i32 = 1030; -const FD_CLOEXEC: i32 = 1; +pub(crate) const FD_CLOEXEC: i32 = 1; + +/// `{OPEN_MAX}`: the descriptors one table holds (`toyos::fd::MAX_FDS`). +pub(crate) const OPEN_MAX: usize = 4096; /// What `fcntl` does for one command. #[derive(Debug, PartialEq, Eq)] pub(crate) enum Command { - /// A duplicate numbered `floor` or above ([`dup_at_least`]). - DupAtLeast(u32), + /// The lowest free descriptor at or above `floor` on the same object, + /// marked close-on-exec or not. + DupAtLeast { floor: i32, cloexec: bool }, /// The descriptor's close-on-exec flag, read. GetFd, /// The descriptor's close-on-exec flag, set to this. @@ -33,8 +30,8 @@ pub(crate) enum Command { /// A record lock, which no file here supports, or a command or `F_DUPFD` /// floor POSIX refuses: `EINVAL`. Invalid, - /// A descriptor's status flags or owner, or a duplicate closed on `exec`, - /// none of which this library keeps: `ENOSYS`. + /// A descriptor's status flags or owner, none of which this library + /// keeps: `ENOSYS`. Unsupported, } @@ -43,72 +40,18 @@ pub(crate) enum Command { /// register's upper half unspecified, so only the lower is read. pub(crate) fn command(cmd: i32, arg: u64) -> Command { let int = arg as u32 as i32; + let dup = |cloexec| match usize::try_from(int) { + // POSIX's `EINVAL` for a floor at or above `{OPEN_MAX}`. + Ok(floor) if floor < OPEN_MAX => Command::DupAtLeast { floor: int, cloexec }, + _ => Command::Invalid, + }; match cmd { - // POSIX's `EINVAL` for a floor at or above `{OPEN_MAX}`, the slots a - // table has: past them only a slot's generation reaches the floor. - F_DUPFD => match u32::try_from(int) { - Ok(floor) if (floor as usize) < RawHandle::MAX_SLOTS => Command::DupAtLeast(floor), - _ => Command::Invalid, - }, + F_DUPFD => dup(false), + F_DUPFD_CLOEXEC => dup(true), F_GETFD => Command::GetFd, F_SETFD => Command::SetFd(int & FD_CLOEXEC != 0), F_GETLK | F_SETLK | F_SETLKW => Command::Invalid, - F_DUPFD_CLOEXEC | F_GETFL | F_SETFL | F_GETOWN | F_SETOWN => Command::Unsupported, + F_GETFL | F_SETFL | F_GETOWN | F_SETOWN => Command::Unsupported, _ => Command::Invalid, } } - -/// A duplicate numbered `floor` or above, from `dup`, holding at most one -/// other duplicate at a time. The kernel picks each number, so this is not the -/// lowest free one: a duplicate below a floor under the slot count is a slot at -/// its first generation, and closed again it is the next `dup`'s, one -/// generation on and numbered past every slot. -pub(crate) fn dup_at_least( - floor: u32, - mut dup: impl FnMut() -> Result, - mut close: impl FnMut(u32), -) -> Result { - loop { - let duplicate = dup()?; - if duplicate >= floor { - return Ok(duplicate); - } - close(duplicate); - } -} - -/// The descriptors marked close-on-exec, by number, which a spawn reads by -/// slot (`posix_io::stdio_for_spawn`). A mark goes with its descriptor's -/// `close`; a descriptor closed some other way leaves its mark, which then -/// reads as the slot's next descriptor's. -pub(crate) struct CloseOnExec(BTreeSet); - -impl CloseOnExec { - pub(crate) const fn new() -> CloseOnExec { - CloseOnExec(BTreeSet::new()) - } - - /// `fd` as `open` answered it for `flags`: marked if they hold `O_CLOEXEC`. - pub(crate) fn opened(&mut self, fd: i32, flags: i32) { - self.set(fd, flags & O_CLOEXEC != 0); - } - - pub(crate) fn set(&mut self, fd: i32, cloexec: bool) { - if cloexec { - self.0.insert(fd); - } else { - self.0.remove(&fd); - } - } - - /// Whether the descriptor in table slot `slot` is marked: a mark goes with - /// its descriptor's `close`, so one at that slot is its live one's. - pub(crate) fn slot_marked(&self, slot: u16) -> bool { - self.0.iter().any(|&fd| RawHandle(fd as u32).slot() == slot) - } - - /// `F_GETFD`'s answer. - pub(crate) fn flags(&self, fd: i32) -> i32 { - if self.0.contains(&fd) { FD_CLOEXEC } else { 0 } - } -} diff --git a/sdk/libc/src/posix_io.rs b/sdk/libc/src/posix_io.rs index bca2b93e4d..bd41ddff5e 100644 --- a/sdk/libc/src/posix_io.rs +++ b/sdk/libc/src/posix_io.rs @@ -9,9 +9,7 @@ use core::ptr; use toyos_abi::{RawHandle, Rights}; use toyos_abi::syscall::{self, OpenFlags, SeekFrom}; -use crate::errcode::EINVAL; -use crate::fdreq::CloseOnExec; -use crate::pthread::Lock; +use crate::errcode::{EBADF, EINVAL}; use crate::time::Timespec; // The values `` declares. @@ -22,6 +20,7 @@ pub(crate) const O_RDWR: i32 = 2; pub(crate) const O_CREAT: i32 = 0x40; pub(crate) const O_TRUNC: i32 = 0x200; pub(crate) const O_APPEND: i32 = 0x400; +pub(crate) const O_CLOEXEC: i32 = 0x80000; const SEEK_SET: i32 = 0; const SEEK_CUR: i32 = 1; @@ -41,17 +40,50 @@ pub(crate) fn set_errno(e: toyos_abi::syscall::SyscallError) -> i32 { -1 } -fn fd(raw: i32) -> RawHandle { RawHandle(raw as u32) } +/// Why a call on a descriptor was not made: an `errno`. +pub(crate) type Errno = i32; + +/// `-1`, with `errno` set to `e`. +pub(crate) fn refuse(e: Errno) -> i32 { + crate::errno::set(e); + -1 +} + +/// `f` on the kernel handle descriptor `raw` names, held for the call +/// (`toyos::fd`): `EBADF` for a descriptor not open, and for one naming no +/// kernel handle, as a socket not yet connected names none. +pub(crate) fn on_fd(raw: i32, f: impl FnOnce(RawHandle) -> Result) -> Result { + match toyos::fd::with(raw, |o| o.release.is_none().then(|| f(o.handle))) { + Ok(Some(answer)) => answer.map_err(crate::errcode::of), + Ok(None) | Err(_) => Err(EBADF), + } +} + +/// The table's refusal as `errno`. +pub(crate) fn fd_errno(e: toyos::fd::Error) -> Errno { + match e { + toyos::fd::Error::BadFd => EBADF, + toyos::fd::Error::Full => crate::errcode::EMFILE, + } +} + +/// A kernel handle this process just got, as the lowest free descriptor, +/// inherited unless `cloexec`. +pub(crate) fn install(handle: RawHandle, cloexec: bool) -> Result { + toyos::fd::install(toyos::fd::Object::handle(handle), !cloexec, 0).map_err(fd_errno) +} /// A `write` to a descriptor. Stdout and stderr go through the SDK's stream -/// sinks, which are a program's log ring when its slot holds one +/// sinks, which are a program's log ring when its descriptor names one /// (`toyos::log::stdio`); every other descriptor is its handle. -pub fn write_fd(raw: i32, buf: &[u8]) -> Result { +pub fn write_fd(raw: i32, buf: &[u8]) -> Result { use toyos::log::stdio::{self, Stream}; + // 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), - 2 => stdio::write(Stream::Err, buf), - other => syscall::write(fd(other), buf), + 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)), } } @@ -67,10 +99,7 @@ pub fn c_str_to_bytes(s: *const u8) -> &'static [u8] { #[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(f) => { - CLOEXEC.lock().opened(f.0 as i32, flags); - f.0 as i32 - } + Ok(handle) => install(handle, flags & O_CLOEXEC != 0).unwrap_or_else(refuse), Err(e) => set_errno(e), } } @@ -114,18 +143,19 @@ pub unsafe extern "C" fn creat(path: *const u8, mode: u32) -> i32 { #[no_mangle] pub unsafe extern "C" fn close(raw_fd: i32) -> i32 { - CLOEXEC.lock().set(raw_fd, false); - syscall::close(fd(raw_fd)); - 0 + match toyos::fd::close(raw_fd) { + Ok(()) => 0, + Err(e) => refuse(fd_errno(e)), + } } #[no_mangle] 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 syscall::read(fd(raw_fd), slice) { + match on_fd(raw_fd, |h| syscall::read(h, slice)) { Ok(n) => n as isize, - Err(e) => { set_errno(e); -1 } + Err(e) => refuse(e) as isize, } } @@ -135,7 +165,7 @@ pub unsafe extern "C" fn write(raw_fd: i32, buf: *const u8, count: usize) -> isi let slice = core::slice::from_raw_parts(buf, count); match write_fd(raw_fd, slice) { Ok(n) => n as isize, - Err(e) => { set_errno(e); -1 } + Err(e) => refuse(e) as isize, } } @@ -147,15 +177,18 @@ 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 syscall::seek(fd(raw_fd), pos) { + match on_fd(raw_fd, |h| syscall::seek(h, pos)) { Ok(n) => n as i64, - Err(e) => { set_errno(e); -1 } + Err(e) => refuse(e) as i64, } } #[no_mangle] pub unsafe extern "C" fn fstat(raw_fd: i32, buf: *mut Stat) -> i32 { - unsafe { answered(syscall::fstat(fd(raw_fd)), buf) } + match on_fd(raw_fd, syscall::fstat) { + Ok(st) => unsafe { answered(Ok(st), buf) }, + Err(e) => refuse(e), + } } /// `buf` filled from the kernel's `answer`, or `errno` from its refusal. @@ -191,9 +224,9 @@ unsafe fn answered(answer: Result, buf: *m #[no_mangle] pub unsafe extern "C" fn ftruncate(raw_fd: i32, length: i64) -> i32 { - match syscall::ftruncate(fd(raw_fd), length as u64) { + match on_fd(raw_fd, |h| syscall::ftruncate(h, length as u64)) { Ok(()) => 0, - Err(e) => set_errno(e), + Err(e) => refuse(e), } } @@ -227,55 +260,65 @@ 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 syscall::fsync(fd(raw_fd)) { + match on_fd(raw_fd, syscall::fsync) { Ok(()) => 0, - Err(e) => set_errno(e), + Err(e) => refuse(e), } } +/// POSIX's `dup`: the lowest free descriptor on the same object, not +/// close-on-exec. #[no_mangle] pub unsafe extern "C" fn dup(raw_fd: i32) -> i32 { - match syscall::dup(fd(raw_fd)) { - Ok(f) => f.0 as i32, - Err(e) => set_errno(e), - } + toyos::fd::dup(raw_fd, 0, true).unwrap_or_else(|e| refuse(fd_errno(e))) } -/// **This does not honour POSIX's "returns `newfd`".** `newfd` is a slot, and -/// the handle the kernel hands back carries that slot's generation, so -/// `dup2(x, 1)` answers `1` only while slot 1 has never been closed. Nothing in -/// the tree redirects by closing a slot first, and this layer is where the -/// bookkeeping to fake it would go if something ever does. +/// POSIX's `dup2`: `new_fd` on `old_fd`'s object, not close-on-exec, after +/// closing what it named. #[no_mangle] pub unsafe extern "C" fn dup2(old_fd: i32, new_fd: i32) -> i32 { - if old_fd == new_fd { return new_fd; } - let Ok(slot) = u16::try_from(new_fd) else { - return set_errno(syscall::SyscallError::InvalidArgument); - }; - match syscall::dup2(fd(old_fd), slot) { - Ok(f) => { - // POSIX: the descriptor `dup2` answers is not closed on `exec`. - CLOEXEC.lock().set(f.0 as i32, false); - // The slot holds something else now, so the stream is asked again. - match slot { + if old_fd == new_fd { + return match toyos::fd::inheritable(old_fd) { + Ok(_) => new_fd, + Err(e) => refuse(fd_errno(e)), + }; + } + match toyos::fd::dup2(old_fd, new_fd, true) { + Ok(fd) => { + // The descriptor names something else now, so the stream is asked again. + match fd { 1 => toyos::log::stdio::forget(toyos::log::stdio::Stream::Out), 2 => toyos::log::stdio::forget(toyos::log::stdio::Stream::Err), _ => {} } - f.0 as i32 + fd } - Err(e) => set_errno(e), + Err(e) => refuse(fd_errno(e)), } } #[no_mangle] pub unsafe extern "C" fn pipe(pipefd: *mut i32) -> i32 { - let fds = match syscall::pipe() { - Ok(fds) => fds, + let ends = match syscall::pipe() { + Ok(ends) => ends, Err(e) => return set_errno(e), }; - *pipefd = fds.read.0 as i32; - *pipefd.add(1) = fds.write.0 as i32; + let read = match install(ends.read, false) { + Ok(fd) => fd, + Err(e) => { + syscall::close(ends.write); + return refuse(e); + } + }; + let write = match install(ends.write, false) { + Ok(fd) => fd, + Err(e) => { + let _ = toyos::fd::close(read); + return refuse(e); + } + }; + *pipefd = read; + *pipefd.add(1) = write; 0 } @@ -407,9 +450,12 @@ 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 syscall::fstat(fd(raw_fd)) { + match on_fd(raw_fd, syscall::fstat) { Ok(st) => (st.file_type == syscall::FileType::Tty) as i32, - Err(_) => 0, + Err(e) => { + crate::errno::set(if e == EBADF { EBADF } else { crate::errcode::ENOTTY }); + 0 + } } } @@ -422,21 +468,6 @@ pub unsafe extern "C" fn umask(mask: u32) -> u32 { old } -static CLOEXEC: Lock = Lock::new(CloseOnExec::new()); -/// What this process holds in slots 0–2, as copies for a child, each with -/// whether the descriptor is marked close-on-exec, as `spawnreq::plan` takes -/// them: `None` for a slot it was started without or closed. The copies close -/// when the caller drops them, once the child has its own. -pub(crate) fn stdio_for_spawn() -> Result<[Option<(toyos::OwnedHandle, bool)>; 3], toyos::spawn::Refused> { - let mut held = [None, None, None]; - for (slot, at) in (0u16..).zip(&mut held) { - if let Some(copy) = toyos::spawn::inherited(slot)? { - *at = Some((copy, CLOEXEC.lock().slot_marked(slot))); - } - } - Ok(held) -} - /// 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) @@ -445,29 +476,15 @@ pub(crate) fn is_file(path: &[u8]) -> bool { /// `arg` is the register C's variadic third argument arrives in (`fdreq`). #[no_mangle] pub unsafe extern "C" fn fcntl(raw_fd: i32, cmd: i32, arg: u64) -> i32 { - use crate::fdreq::Command; - let refused = match crate::fdreq::command(cmd, arg) { - Command::DupAtLeast(floor) => { - let duplicate = crate::fdreq::dup_at_least( - floor, - || syscall::dup(fd(raw_fd)).map(|h| h.0), - |below| syscall::close(RawHandle(below)), - ); - return match duplicate { - Ok(n) => n as i32, - Err(e) => set_errno(e), - }; - } - Command::GetFd => return CLOEXEC.lock().flags(raw_fd), - Command::SetFd(cloexec) => { - CLOEXEC.lock().set(raw_fd, cloexec); - return 0; - } - Command::Invalid => EINVAL, - Command::Unsupported => crate::errcode::ENOSYS, + use crate::fdreq::{Command, FD_CLOEXEC}; + let answer = match crate::fdreq::command(cmd, arg) { + Command::DupAtLeast { floor, cloexec } => toyos::fd::dup(raw_fd, floor, !cloexec).map_err(fd_errno), + Command::GetFd => toyos::fd::inheritable(raw_fd).map(|inherited| if inherited { 0 } else { FD_CLOEXEC }).map_err(fd_errno), + Command::SetFd(cloexec) => toyos::fd::set_inheritable(raw_fd, !cloexec).map(|()| 0).map_err(fd_errno), + Command::Invalid => Err(EINVAL), + Command::Unsupported => Err(crate::errcode::ENOSYS), }; - crate::errno::set(refused); - -1 + answer.unwrap_or_else(refuse) } // Directory operations @@ -677,7 +694,10 @@ pub unsafe extern "C" fn poll(fds: *mut pollfd, nfds: u32, timeout: i32) -> i32 .iter() .map(|&(fd, events)| match crate::socket::stream_of(fd) { Some(_) => (-1, 0), - None => (crate::socket::watched_of(fd).map_or(fd, |handle| handle.0 as i32), events), + None => { + let handle = crate::socket::watched_of(fd).or_else(|| on_fd(fd, Ok).ok()); + (handle.map_or(-1, |handle| handle.0 as i32), events) + } }) .collect(); loop { diff --git a/sdk/libc/src/socket.rs b/sdk/libc/src/socket.rs index 5c618b626e..8e3c630e08 100644 --- a/sdk/libc/src/socket.rs +++ b/sdk/libc/src/socket.rs @@ -79,29 +79,67 @@ struct SocketEntry { } const MAX_SOCKETS: usize = 128; -// Socket FDs start at 1024 to avoid collisions with file FDs -const SOCKET_FD_BASE: i32 = 1024; + +/// A socket's descriptor names an object of this library's own +/// (`toyos::fd::Object::opaque`), whose word is this tag beside the socket's +/// index here. +const SOCKET_TAG: u64 = 0x736f_636b << 32; static mut SOCKETS: [Option; MAX_SOCKETS] = [None; MAX_SOCKETS]; +/// The socket descriptor `fd` names, or `None` for a descriptor that is no +/// socket of this library's. fn sock_from_fd(fd: i32) -> Option<&'static mut Option> { - let idx = (fd - SOCKET_FD_BASE) as usize; - if idx >= MAX_SOCKETS { + let word = toyos::fd::with(fd, |o| o.release.map(|_| o.word)).ok().flatten()?; + if word & !0xffff_ffff != SOCKET_TAG { return None; } + let idx = usize::try_from(word & 0xffff_ffff).ok().filter(|&i| i < MAX_SOCKETS)?; unsafe { Some(&mut SOCKETS[idx]) } } +/// `entry` as a socket descriptor, the lowest free and inherited, or `-1` +/// with `errno` set and everything it held let go. fn alloc_socket(entry: SocketEntry) -> i32 { - unsafe { - for i in 0..MAX_SOCKETS { - if SOCKETS[i].is_none() { - SOCKETS[i] = Some(entry); - return SOCKET_FD_BASE + i as i32; - } + let Some(idx) = (0..MAX_SOCKETS).find(|&i| unsafe { SOCKETS[i].is_none() }) else { + let_go(entry); + set_errno(ENOMEM); + return -1; + }; + unsafe { SOCKETS[idx] = Some(entry) }; + match toyos::fd::install(toyos::fd::Object::opaque(SOCKET_TAG | idx as u64, release_socket), true, 0) { + Ok(fd) => fd, + // The table let the object go, and with it the socket. + Err(_) => { + set_errno(crate::errcode::EMFILE); + -1 + } + } +} + +/// A socket no descriptor names any more: its pipes, its stream and +/// netstack's socket go. +extern "C" fn release_socket(word: u64) { + let idx = (word & 0xffff_ffff) as usize; + if idx < MAX_SOCKETS { + if let Some(entry) = unsafe { SOCKETS[idx].take() } { + let_go(entry); + } + } +} + +fn let_go(entry: SocketEntry) { + // A stream's leaving is its connection's hang-up. + if entry.stream != 0 { stream::let_go(RawHandle(entry.stream as u32)); } + if entry.rx_fd != 0 { syscall::close(RawHandle(entry.rx_fd as u32)); } + if entry.tx_fd != 0 { syscall::close(RawHandle(entry.tx_fd as u32)); } + if entry.notify_fd != 0 { syscall::close(RawHandle(entry.notify_fd as u32)); } + if entry.netstack_id != 0 { + match entry.kind { + SocketKind::Tcp => { let _ = toyos::net::tcp_close(TcpSocketId(entry.netstack_id)); } + SocketKind::Udp => { let _ = toyos::net::udp_close(UdpSocketId(entry.netstack_id)); } } } - -1 } use crate::errno::set as set_errno; @@ -204,11 +242,7 @@ pub unsafe extern "C" fn socket(domain: i32, sock_type: i32, _protocol: i32) -> nodelay: false, broadcast: false, }; - let fd = alloc_socket(entry); - if fd < 0 { - set_errno(ENOMEM); - } - fd + alloc_socket(entry) } #[no_mangle] @@ -337,13 +371,7 @@ pub unsafe extern "C" fn accept( nodelay: accepted.nodelay, broadcast: false, }; - let new_fd = alloc_socket(new_entry); - if new_fd < 0 { - // The client's leaving: netstack closes the connection. - stream::let_go(accepted.handle); - set_errno(ENOMEM); - } - new_fd + alloc_socket(new_entry) } #[no_mangle] @@ -563,36 +591,6 @@ pub(crate) fn watched_of(fd: i32) -> Option { } -// close (for socket fds) - -#[no_mangle] -pub unsafe extern "C" fn close_socket(fd: i32) -> bool { - let slot = match sock_from_fd(fd) { - Some(s) => s, - None => return false, - }; - let entry = match slot.take() { - Some(e) => e, - None => return false, - }; - - // A stream's leaving is its connection's hang-up. - if entry.stream != 0 { stream::let_go(RawHandle(entry.stream as u32)); } - // Close pipe fds - if entry.rx_fd != 0 { syscall::close(RawHandle(entry.rx_fd as u32)); } - if entry.tx_fd != 0 { syscall::close(RawHandle(entry.tx_fd as u32)); } - if entry.notify_fd != 0 { syscall::close(RawHandle(entry.notify_fd as u32)); } - - // Tell netstack to close the socket - if entry.netstack_id != 0 { - match entry.kind { - SocketKind::Tcp => { let _ = toyos::net::tcp_close(TcpSocketId(entry.netstack_id)); } - SocketKind::Udp => { let _ = toyos::net::udp_close(UdpSocketId(entry.netstack_id)); } - } - } - true -} - // setsockopt / getsockopt fn option_errno(refusal: sockopt::Refusal) -> i32 { diff --git a/sdk/libc/src/spawn.rs b/sdk/libc/src/spawn.rs index 568523a3c1..3962f0027f 100644 --- a/sdk/libc/src/spawn.rs +++ b/sdk/libc/src/spawn.rs @@ -272,16 +272,22 @@ fn spawn_child(path: &str, argv: &[u8], env: &[u8], actions: &[Action> /// [`spawn_child`], its place in [`UNWAITED`] taken. fn spawn_counted(path: &str, argv: &[u8], env: &[u8], actions: &[Action>], group: Option) -> Result { - // Copies of this process's stdio, closed once the child holds its own. - let held = crate::posix_io::stdio_for_spawn().map_err(errno_of)?; - let stdio = held.each_ref().map(|h| h.as_ref().map(|(copy, marked)| (toyos::AsHandle::as_handle(copy).0, *marked))); + // Copies of what this process's descriptors name, closed once the child + // holds its own. + let mut held: Vec = Vec::new(); let plan = spawnreq::plan( - stdio, + |fd| { + let copy = toyos::fd::copy(fd as i32).ok().flatten()?; + let cloexec = !toyos::fd::inheritable(fd as i32).ok()?; + let handle = toyos::AsHandle::as_handle(©).0; + held.push(copy); + Some((handle, cloexec)) + }, actions, |path, oflag, mode| crate::posix_io::open_handle(path, oflag, mode).map(|h| h.0).map_err(crate::errcode::of), |handle| syscall::close(RawHandle(handle)), )?; - let started = start(path, argv, env, &plan.slots); + let started = start(path, argv, env, &plan.fds); for handle in plan.opened { syscall::close(RawHandle(handle)); } @@ -301,20 +307,18 @@ fn spawn_counted(path: &str, argv: &[u8], env: &[u8], actions: &[Action Result { +fn start(path: &str, argv: &[u8], env: &[u8], fds: &[(u32, u32)]) -> Result { let mut cwd = alloc::vec![0u8; syscall::MAX_PATH]; let n = syscall::getcwd(&mut cwd); let cwd = core::str::from_utf8(&cwd[..n]).map_err(|_| EINVAL)?; - let split = slots.partition_point(|&[child, _]| child <= 2); + let fds: Vec<(u32, RawHandle)> = fds.iter().map(|&(fd, handle)| (fd, RawHandle(handle))).collect(); let request = Spawn { program: path, image: None, argv, env, cwd, - // `plan` answers them by descriptor, so stdio comes first. - slots, - stdio: split, + fds: &fds, endow: &[], provided: &[], parent: Parent::Caller, @@ -559,24 +563,35 @@ pub unsafe extern "C" fn popen(command: *const u8, mode: *const u8) -> *mut crat return ptr::null_mut(); } }; - let (theirs, ours, slot) = match reading { + let (theirs, ours, child) = match reading { true => (ends.write, ends.read, 1), false => (ends.read, ends.write, 0), }; - let started = shell(command, &[Action::Dup2 { fd: theirs.0, newfd: slot }]); - syscall::close(theirs); + // Both ends as descriptors, this process's own closed on exec. + let (theirs, ours) = match (crate::posix_io::install(theirs, true), crate::posix_io::install(ours, true)) { + (Ok(theirs), Ok(ours)) => (theirs, ours), + (theirs, ours) => { + for fd in [theirs, ours].into_iter().flatten() { + let _ = toyos::fd::close(fd); + } + crate::errno::set(crate::errcode::EMFILE); + return ptr::null_mut(); + } + }; + let started = shell(command, &[Action::Dup2 { fd: theirs as u32, newfd: child }]); + let _ = toyos::fd::close(theirs); let pid = match started { Ok(pid) => pid, Err(errno) => { - syscall::close(ours); + let _ = toyos::fd::close(ours); crate::errno::set(errno); return ptr::null_mut(); } }; - let stream = unsafe { crate::stdio::fdopen(ours.0 as i32, mode) }; + let stream = unsafe { crate::stdio::fdopen(ours, mode) }; if stream.is_null() { // No stream to read the child by: its end goes, and the child is waited for. - syscall::close(ours); + let _ = toyos::fd::close(ours); unsafe { waitpid(pid, ptr::null_mut(), 0) }; crate::errno::set(crate::errcode::ENOMEM); return ptr::null_mut(); diff --git a/sdk/libc/src/spawnreq.rs b/sdk/libc/src/spawnreq.rs index c26ec31f9b..479828fc4b 100644 --- a/sdk/libc/src/spawnreq.rs +++ b/sdk/libc/src/spawnreq.rs @@ -12,14 +12,13 @@ use alloc::collections::{BTreeMap, BTreeSet}; use alloc::vec::Vec; use toyos_abi::end::End; -use toyos_abi::RawHandle; use crate::errcode::EBADF; const O_CLOEXEC: i32 = 0x80000; -/// The child's descriptors are its slots, each at its first generation. -pub(crate) const CHILD_FDS: usize = RawHandle::MAX_SLOTS; +/// The descriptors a child's table holds (`toyos::fd::MAX_FDS`). +pub(crate) const CHILD_FDS: usize = 4096; /// One `posix_spawn_file_actions_add*`, in the child's descriptor numbers. #[derive(Clone, Debug, PartialEq, Eq)] @@ -36,18 +35,17 @@ pub(crate) fn child_fd(fd: i32) -> Result { } /// A descriptor an action reads or closes, `adddup2`'s `fd` and -/// `addclose`'s: one of the child's, or of this process's, whose numbers run -/// past the child's slots once a slot is reused (`RawHandle`'s generation). +/// `addclose`'s: one of the child's, or of this process's. pub(crate) fn any_fd(fd: i32) -> Result { - u32::try_from(fd).map_err(|_| EBADF) + u32::try_from(fd).ok().filter(|&fd| (fd as usize) < CHILD_FDS).ok_or(EBADF) } -/// What the spawn hands the kernel: `[child slot, this process's handle]` -/// for every descriptor the child starts with, and the handles the actions +/// What the spawn gives the child: `(child descriptor, this process's +/// handle)` for every descriptor it starts with, and the handles the actions /// opened here, which are this process's to close once the spawn returns. #[derive(Debug, Default, PartialEq, Eq)] pub(crate) struct Plan { - pub(crate) slots: Vec<[u32; 2]>, + pub(crate) fds: Vec<(u32, u32)>, pub(crate) opened: Vec, } @@ -59,22 +57,23 @@ struct Entry { cloexec: bool, } -/// The child's table: `stdio[n]` is this process's handle for descriptor `n` -/// and whether it is marked close-on-exec, `None` for one it does not hold; -/// then each action in order, as POSIX runs them in the child; then the exec, -/// which closes what is marked. A descriptor of this process's beyond 2 that -/// no action closed is one an action may name, and is not the child's -/// otherwise. `open` opens a path here, answering the handle or an `errno`; -/// a refusal ends the plan with the handles it opened closed by `close`. +/// The child's table: `held(n)` is this process's handle for its descriptor +/// `n` and whether it is marked close-on-exec, `None` for one it does not +/// hold; 0 to 2 first, then each action in order, as POSIX runs them in the +/// child; then the exec, which closes what is marked. A descriptor of this +/// process's beyond 2 that no action closed is one an action may name, and is +/// not the child's otherwise. `open` opens a path here, answering the handle +/// or an `errno`; a refusal ends the plan with the handles it opened closed +/// by `close`. pub(crate) fn plan

( - stdio: [Option<(u32, bool)>; 3], + mut held: impl FnMut(u32) -> Option<(u32, bool)>, actions: &[Action

], mut open: impl FnMut(&P, i32, u32) -> Result, mut close: impl FnMut(u32), ) -> Result { let mut table: BTreeMap = BTreeMap::new(); - for (fd, held) in (0..).zip(stdio) { - if let Some((handle, cloexec)) = held { + for fd in 0..3 { + if let Some((handle, cloexec)) = held(fd) { table.insert(fd, Entry { handle, cloexec }); } } @@ -102,17 +101,18 @@ pub(crate) fn plan

( } Action::Dup2 { fd, newfd } => { let source = match table.get(fd) { - Some(entry) => entry.handle, - None if *fd > 2 && !closed.contains(fd) => *fd, - None => return refuse(&mut opened, &mut close, EBADF), + Some(entry) => Some(entry.handle), + None if *fd > 2 && !closed.contains(fd) => held(*fd).map(|(handle, _)| handle), + None => None, }; + let Some(source) = source else { return refuse(&mut opened, &mut close, EBADF) }; table.insert(*newfd, Entry { handle: source, cloexec: false }); closed.remove(newfd); } } } - let slots = table.into_iter().filter(|(_, e)| !e.cloexec).map(|(fd, e)| [fd, e.handle]).collect(); - Ok(Plan { slots, opened }) + let fds = table.into_iter().filter(|(_, e)| !e.cloexec).map(|(fd, e)| (fd, e.handle)).collect(); + Ok(Plan { fds, opened }) } /// The status `waitpid` writes for an end, as ``'s macros read diff --git a/sdk/libc/src/stdio.rs b/sdk/libc/src/stdio.rs index a4e1e14f60..9d51b15d10 100644 --- a/sdk/libc/src/stdio.rs +++ b/sdk/libc/src/stdio.rs @@ -1,13 +1,14 @@ use core::ptr; -use toyos_abi::RawHandle; use toyos_abi::syscall::{self, SeekFrom}; +use crate::posix_io::on_fd; + // Platform fd operations -fn sys_close(fd: i32) { syscall::close(RawHandle(fd as u32)); } +fn sys_close(fd: i32) { let _ = toyos::fd::close(fd); } fn sys_read(fd: i32, buf: &mut [u8]) -> isize { - match syscall::read(RawHandle(fd as u32), buf) { + match on_fd(fd, |h| syscall::read(h, buf)) { Ok(n) => n as isize, Err(_) => -1, } @@ -27,13 +28,13 @@ fn sys_seek(fd: i32, offset: i64, whence: i32) -> i64 { 2 => SeekFrom::End(offset), _ => return -1, }; - match syscall::seek(RawHandle(fd as u32), pos) { + match on_fd(fd, |h| syscall::seek(h, pos)) { Ok(n) => n as i64, Err(_) => -1, } } -fn sys_fsync(fd: i32) { let _ = syscall::fsync(RawHandle(fd as u32)); } +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 } @@ -164,7 +165,7 @@ 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 syscall::fstat(RawHandle(fd as u32)) { + match on_fd(fd, syscall::fstat) { Ok(st) => matches!( st.file_type, syscall::FileType::Tty | syscall::FileType::Serial diff --git a/sdk/std/os/io.rs b/sdk/std/os/io.rs index b2589594f9..d1dbdf7971 100644 --- a/sdk/std/os/io.rs +++ b/sdk/std/os/io.rs @@ -1,3 +1,11 @@ +//! Descriptors, as unix has them, and the kernel handles behind them. +//! +//! **A [`RawFd`](fd::RawFd) is a number in the process's one descriptor +//! table** (`toyos::fd`), which std and libc share: a `File` is one, as on +//! unix, and C's `read(fd)` reaches the same object. A [`RawHandle`] is the +//! kernel handle a descriptor names, which ToyOS-only code watches or hands to +//! the kernel directly ([`AsRawHandle`]); a portable program needs none. + // Re-export standard fd traits #[stable(feature = "toyos_ext", since = "1.0.0")] pub use fd::*; @@ -5,25 +13,75 @@ pub use fd::*; use crate::os::fd; use crate::sys::{AsInner, FromInner, IntoInner}; +/// A kernel handle, as `toyos_abi::RawHandle` numbers it. #[stable(feature = "toyos_ext", since = "1.0.0")] -impl AsRawFd for crate::fs::File { - #[inline] - fn as_raw_fd(&self) -> RawFd { - self.as_inner().as_raw_fd() +pub type RawHandle = u32; + +/// The kernel handle behind a descriptor. +#[stable(feature = "toyos_ext", since = "1.0.0")] +pub trait AsRawHandle { + /// The handle, good for as long as `self` holds its descriptor: closing + /// the descriptor closes it. A descriptor a library keeps as no kernel + /// handle answers `toyos_abi::HANDLE_INVALID`. + #[stable(feature = "toyos_ext", since = "1.0.0")] + fn as_raw_handle(&self) -> RawHandle; +} + +#[stable(feature = "toyos_ext", since = "1.0.0")] +impl AsRawHandle for T { + fn as_raw_handle(&self) -> RawHandle { + toyos::fd::with(self.as_fd().as_raw_fd(), |object| object.handle.0) + .expect("std::os::toyos: a borrowed descriptor is not open") } } -/// A stream's handle is read and written through its region, never as a +/// The descriptor `self` holds; with `borrowed`, as `AsFd` lends it too +/// (the network types' `AsFd` is `std::os::fd`'s own). +macro_rules! as_raw_fd { + ($($ty:ty => |$this:ident| $fd:expr $(, $borrowed:ident)?);+ $(;)?) => {$( + #[stable(feature = "toyos_ext", since = "1.0.0")] + impl AsRawFd for $ty { + #[inline] + fn as_raw_fd(&self) -> RawFd { + let $this = self; + $fd + } + } + $(as_raw_fd!(@$borrowed $ty);)? + )+}; + (@borrowed $ty:ty) => { + #[stable(feature = "toyos_ext", since = "1.0.0")] + impl AsFd for $ty { + #[inline] + fn as_fd(&self) -> BorrowedFd<'_> { + // SAFETY: the descriptor is `self`'s, open for as long as the borrow. + unsafe { BorrowedFd::borrow_raw(self.as_raw_fd()) } + } + } + }; +} + +as_raw_fd! { + crate::fs::File => |f| f.as_inner().as_raw_fd(), borrowed; + crate::process::ChildStdin => |p| p.as_inner().raw_fd(), borrowed; + crate::process::ChildStdout => |p| p.as_inner().raw_fd(), borrowed; + crate::process::ChildStderr => |p| p.as_inner().raw_fd(), borrowed; + crate::net::TcpStream => |s| s.as_inner().as_raw_fd(); + crate::net::TcpListener => |l| l.as_inner().as_raw_fd(); +} + +/// A stream's descriptor is read and written through its region, never as a /// file: taken as one, it panics with `toyos::net`'s refusal. #[stable(feature = "toyos_ext", since = "1.0.0")] impl FromRawFd for crate::fs::File { #[inline] unsafe fn from_raw_fd(fd: RawFd) -> crate::fs::File { - let handle = toyos_abi::RawHandle(fd as u32); - if toyos::net::stream::is_stream(handle) { - panic!("{}", toyos::net::stream::Misuse::AsFile(handle)); + if let Ok(handle) = toyos::fd::with(fd, |o| o.handle) { + if toyos::net::stream::is_stream(handle) { + panic!("{}", toyos::net::stream::Misuse::AsFile(handle)); + } } - crate::fs::File::from_inner(crate::sys::fs::File::from_fd(handle)) + crate::fs::File::from_inner(crate::sys::fs::File::from_fd(fd)) } } @@ -31,36 +89,42 @@ impl FromRawFd for crate::fs::File { impl IntoRawFd for crate::fs::File { #[inline] fn into_raw_fd(self) -> RawFd { - self.as_inner().raw_fd() + self.into_inner().into_raw_fd() + } +} + +#[stable(feature = "toyos_ext", since = "1.0.0")] +impl From for OwnedFd { + #[inline] + fn from(file: crate::fs::File) -> OwnedFd { + // SAFETY: the file's descriptor, given up to its new owner. + unsafe { OwnedFd::from_raw_fd(file.into_raw_fd()) } } } #[stable(feature = "toyos_ext", since = "1.0.0")] -impl AsRawFd for crate::net::TcpStream { +impl From for crate::fs::File { #[inline] - fn as_raw_fd(&self) -> RawFd { - self.as_inner().as_raw_fd() + fn from(owned: OwnedFd) -> crate::fs::File { + // SAFETY: the owned descriptor, given up to the file. + unsafe { crate::fs::File::from_raw_fd(owned.into_raw_fd()) } } } -/// A stream's handle taken back: one this process holds the stream's end for -/// shares it, and any other panics with `toyos::net`'s refusal, a stream -/// belonging to the process that connected or accepted it. +/// A stream's descriptor taken back: one whose stream this process holds an +/// end of shares it, and any other panics with `toyos::net`'s refusal, a +/// stream belonging to the process that connected or accepted it. #[stable(feature = "toyos_ext", since = "1.0.0")] impl FromRawFd for crate::net::TcpStream { #[inline] unsafe fn from_raw_fd(fd: RawFd) -> crate::net::TcpStream { - if let Err(misuse) = toyos::net::stream::adopt(toyos_abi::RawHandle(fd as u32)) { - panic!("{misuse}"); - } // SAFETY: the caller's, as `FromRawFd` states it. - let owned = unsafe { fd::OwnedFd::from_raw_fd(fd) }; - crate::net::TcpStream::from_inner(crate::sys::net::TcpStream::from_fd(owned)) + crate::net::TcpStream::from(unsafe { fd::OwnedFd::from_raw_fd(fd) }) } } -/// The handle given out keeps reaching the stream's end, for whoever takes it -/// back. +/// The descriptor given out keeps naming the stream's end, for whoever takes +/// it back. #[stable(feature = "toyos_ext", since = "1.0.0")] impl IntoRawFd for crate::net::TcpStream { #[inline] @@ -73,7 +137,8 @@ impl IntoRawFd for crate::net::TcpStream { impl From for crate::net::TcpStream { #[inline] fn from(owned: fd::OwnedFd) -> Self { - if let Err(misuse) = toyos::net::stream::adopt(toyos_abi::RawHandle(owned.as_raw_fd() as u32)) { + let handle = owned.as_raw_handle(); + if let Err(misuse) = toyos::net::stream::adopt(toyos_abi::RawHandle(handle)) { panic!("{misuse}"); } crate::net::TcpStream::from_inner(crate::sys::net::TcpStream::from_fd(owned)) @@ -88,38 +153,6 @@ impl From for fd::OwnedFd { } } -#[stable(feature = "toyos_ext", since = "1.0.0")] -impl AsRawFd for crate::net::TcpListener { - #[inline] - fn as_raw_fd(&self) -> RawFd { - self.as_inner().as_raw_fd() - } -} - -#[stable(feature = "toyos_ext", since = "1.0.0")] -impl AsRawFd for crate::process::ChildStdin { - #[inline] - fn as_raw_fd(&self) -> RawFd { - self.as_inner().raw_fd() - } -} - -#[stable(feature = "toyos_ext", since = "1.0.0")] -impl AsRawFd for crate::process::ChildStdout { - #[inline] - fn as_raw_fd(&self) -> RawFd { - self.as_inner().raw_fd() - } -} - -#[stable(feature = "toyos_ext", since = "1.0.0")] -impl AsRawFd for crate::process::ChildStderr { - #[inline] - fn as_raw_fd(&self) -> RawFd { - self.as_inner().raw_fd() - } -} - /// Switch stdin between canonical and raw mode. #[stable(feature = "toyos_ext", since = "1.0.0")] pub fn set_stdin_raw(raw: bool) { diff --git a/sdk/std/os/net.rs b/sdk/std/os/net.rs index f56df24cd2..1c5516f244 100644 --- a/sdk/std/os/net.rs +++ b/sdk/std/os/net.rs @@ -9,15 +9,15 @@ //! `WouldBlock`, which arms the want the next ring answers. use crate::io; -use crate::os::fd::RawFd; +use crate::os::toyos::io::RawHandle; /// ToyOS's extensions of [`crate::net::TcpStream`]. #[stable(feature = "toyos_ext", since = "1.0.0")] pub trait TcpStreamExt { - /// The stream's bell, watched for reading where the stream is watched for - /// writing. + /// The stream's bell, a kernel handle watched for reading where the stream + /// is watched for writing. #[stable(feature = "toyos_ext", since = "1.0.0")] - fn bell(&self) -> io::Result; + fn bell(&self) -> io::Result; /// Registers the stream for its readable and writable readiness: each /// named way's want armed and looked at, and netstack rung where one is @@ -28,7 +28,7 @@ pub trait TcpStreamExt { #[stable(feature = "toyos_ext", since = "1.0.0")] impl TcpStreamExt for crate::net::TcpStream { - fn bell(&self) -> io::Result { + fn bell(&self) -> io::Result { crate::sys::AsInner::as_inner(self).bell() } diff --git a/sdk/std/os/process.rs b/sdk/std/os/process.rs index e36237e193..285421723e 100644 --- a/sdk/std/os/process.rs +++ b/sdk/std/os/process.rs @@ -12,13 +12,11 @@ pub fn tty_piped() -> crate::process::Stdio { /// ToyOS-specific extensions to [`process::Command`]. #[stable(feature = "toyos_ext", since = "1.0.0")] pub trait CommandExt { - /// Pass an additional handle to the child process. - /// - /// The child process will inherit `parent_handle` at slot `child_slot`. - /// This is useful for passing pipe handles (e.g., for jobserver - /// protocols) to child processes. + /// Give the child descriptor `child_fd`, naming the kernel handle + /// `parent_handle`, which this process keeps: the spawn duplicates it. + /// One of 0 to 2 replaces what the command's stdio would have given. #[stable(feature = "toyos_ext", since = "1.0.0")] - fn inherit_handle(&mut self, child_slot: u32, parent_handle: u32) -> &mut Self; + fn inherit_handle(&mut self, child_fd: u32, parent_handle: u32) -> &mut Self; /// Give the child a handle under a name it can look itself up by. /// @@ -89,7 +87,7 @@ pub trait CommandExt { /// **A launch or nothing.** The supervisor starts a program its manifest declares, /// holding what its row says, so with no `launcher` connector, for a /// program no row declares, or for a command that endows a handle or names - /// a slot beyond stdio, the spawn answers `PermissionDenied` and starts + /// a descriptor beyond stdio, the spawn answers `PermissionDenied` and starts /// nothing. #[stable(feature = "toyos_ext", since = "1.0.0")] fn under_supervisor(&mut self) -> &mut Self; @@ -97,8 +95,8 @@ pub trait CommandExt { #[stable(feature = "toyos_ext", since = "1.0.0")] impl CommandExt for crate::process::Command { - fn inherit_handle(&mut self, child_slot: u32, parent_handle: u32) -> &mut Self { - self.as_inner_mut().inherit_handle(child_slot, parent_handle); + fn inherit_handle(&mut self, child_fd: u32, parent_handle: u32) -> &mut Self { + self.as_inner_mut().inherit_handle(child_fd, parent_handle); self } diff --git a/sdk/std/sys/fs.rs b/sdk/std/sys/fs.rs index 45a2e2a285..f9035b00ba 100644 --- a/sdk/std/sys/fs.rs +++ b/sdk/std/sys/fs.rs @@ -11,7 +11,6 @@ //! A file server that restarts is survived: its capability is connected //! again. -use toyos_abi::RawHandle; use toyos_abi::syscall::{self, OpenFlags}; use crate::collections::BTreeMap; @@ -35,7 +34,8 @@ const MAX_LINKS: usize = 40; pub struct File(Inner); enum Inner { - Kernel(RawHandle), + /// A descriptor naming a file the kernel serves. + Kernel(i32), Served(Arc), } @@ -489,16 +489,17 @@ impl Drop for Served { } impl File { - pub fn from_fd(fd: RawHandle) -> Self { + /// The file descriptor `fd` names, owned from here on. + pub fn from_fd(fd: i32) -> Self { File(Inner::Kernel(fd)) } - /// The kernel handle behind this file. A file a file server holds has - /// none: `as_child_stdio` is how one reaches a child. + /// 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(h) => h.0 as i32, - Inner::Served(_) => panic!("a file on a file server has no kernel handle"), + Inner::Kernel(fd) => *fd, + Inner::Served(_) => panic!("a file on a file server has no descriptor"), } } @@ -506,16 +507,29 @@ impl File { self.raw_fd() } - /// A kernel handle a child may write this file through: the file's own, or - /// a pipe its server writes to the file at this file's offset. The pipe - /// lives as long as the answer and the child's copy of it. - pub fn as_child_stdio(&self) -> io::Result<(RawHandle, Option)> { + /// This file's descriptor, given up: closing it is the caller's. + pub fn into_raw_fd(self) -> i32 { + let fd = self.raw_fd(); + crate::mem::forget(self); + fd + } + + /// The kernel handle behind this file, held for `f`. + pub fn with_handle(&self, f: impl FnOnce(toyos_abi::RawHandle) -> T) -> io::Result { match &self.0 { - Inner::Kernel(h) => Ok((*h, None)), - Inner::Served(s) => { - let pipe = s.with(|dir, fid, generation| dir.stream(fid, generation))?; - Ok((toyos::AsHandle::as_handle(&pipe), Some(pipe))) - } + 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")), + } + } + + /// A kernel handle a child may use this file through: a copy of the + /// 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 { + // SAFETY: a fresh duplicate, which nothing else answers for. + Inner::Kernel(fd) => crate::sys::on_fd(*fd, syscall::dup).map(|h| unsafe { toyos::OwnedHandle::from_raw(h) }), + Inner::Served(s) => s.with(|dir, fid, generation| dir.stream(fid, generation)).map(toyos::OwnedHandle::from), } } @@ -523,8 +537,8 @@ impl File { on_path( path, |abs| { - let fd = syscall::open(abs.as_bytes(), opts.to_flags()).map_err(to_io_error)?; - Ok(File(Inner::Kernel(fd))) + 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(); @@ -541,14 +555,14 @@ impl File { pub fn file_attr(&self) -> io::Result { match &self.0 { - Inner::Kernel(h) => syscall::fstat(*h).map(FileAttr::of_kernel).map_err(to_io_error), + 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), } } pub fn fsync(&self) -> io::Result<()> { match &self.0 { - Inner::Kernel(h) => syscall::fsync(*h).map_err(to_io_error), + Inner::Kernel(fd) => crate::sys::on_fd(*fd, syscall::fsync), Inner::Served(s) => s.with(|dir, fid, generation| dir.fsync(fid, generation)), } } @@ -575,14 +589,14 @@ impl File { pub fn truncate(&self, size: u64) -> io::Result<()> { match &self.0 { - Inner::Kernel(h) => syscall::ftruncate(*h, size).map_err(to_io_error), + 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)), } } pub fn read(&self, buf: &mut [u8]) -> io::Result { match &self.0 { - Inner::Kernel(h) => syscall::read(*h, buf).map_err(to_io_error), + 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)), } } @@ -617,7 +631,7 @@ impl File { pub fn write(&self, buf: &[u8]) -> io::Result { match &self.0 { - Inner::Kernel(h) => syscall::write(*h, buf).map_err(to_io_error), + 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)), } } @@ -650,13 +664,13 @@ impl File { pub fn seek(&self, pos: SeekFrom) -> io::Result { match &self.0 { - Inner::Kernel(h) => { + 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), }; - syscall::seek(*h, abi_pos).map_err(to_io_error) + crate::sys::on_fd(*fd, |h| syscall::seek(h, abi_pos)) } Inner::Served(s) => { let (whence, delta) = match pos { @@ -683,7 +697,7 @@ impl File { pub fn duplicate(&self) -> io::Result { match &self.0 { - Inner::Kernel(h) => Ok(File(Inner::Kernel(syscall::dup(*h).map_err(to_io_error)?))), + 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)))), } } @@ -699,8 +713,8 @@ impl File { impl Drop for File { fn drop(&mut self) { - if let Inner::Kernel(h) = self.0 { - syscall::close(h); + if let Inner::Kernel(fd) = self.0 { + let _ = toyos::fd::close(fd); } } } @@ -722,7 +736,7 @@ impl DirBuilder { impl fmt::Debug for File { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match &self.0 { - Inner::Kernel(h) => write!(f, "File({})", h.0), + Inner::Kernel(fd) => write!(f, "File({fd})"), Inner::Served(s) => write!(f, "File({}/{})", s.dir.prefix, s.rel), } } diff --git a/sdk/std/sys/net/connection.rs b/sdk/std/sys/net/connection.rs index 426dfec8ec..991057e4bc 100644 --- a/sdk/std/sys/net/connection.rs +++ b/sdk/std/sys/net/connection.rs @@ -14,6 +14,19 @@ use crate::time::Duration; // --- Helpers --- +/// A kernel handle this process just got, as an owned descriptor. +fn owned(handle: RawHandle) -> io::Result { + let fd = crate::sys::new_fd(handle)?; + // SAFETY: a descriptor just installed for this handle alone. + Ok(unsafe { OwnedFd::from_raw_fd(fd) }) +} + +/// The kernel handle an owned descriptor names. Its owner alone closes it, +/// so one gone is another owner having closed it, against I/O safety. +fn kernel(fd: &OwnedFd) -> RawHandle { + toyos::fd::with(fd.as_raw_fd(), |o| o.handle).expect("std::net: an owned descriptor was closed by another owner") +} + fn net_err_to_io(e: NetError) -> io::Error { let kind = match e { NetError::ConnectionRefused => io::ErrorKind::ConnectionRefused, @@ -138,12 +151,11 @@ impl TcpStream { pub fn connect_timeout(addr: &SocketAddr, timeout: Duration) -> io::Result { let (ip, port) = addr_to_v4(addr)?; let connected = stream::connect(ip, port, duration_to_ms(Some(timeout)), false).map_err(net_err_to_io)?; - // SAFETY: the connect made this handle for this stream and nothing else answers for it. - Ok(TcpStream::from_fd(unsafe { OwnedFd::from_raw_fd(connected.handle.0 as i32) })) + Ok(TcpStream::from_fd(owned(connected.handle)?)) } fn handle(&self) -> RawHandle { - RawHandle(self.fd.as_raw_fd() as u32) + kernel(&self.fd) } pub fn set_read_timeout(&self, dur: Option) -> io::Result<()> { @@ -222,11 +234,10 @@ impl TcpStream { Ok(()) } + /// A second descriptor on the same stream's end. pub fn duplicate(&self) -> io::Result { - let copy = stream::duplicate(self.handle()).map_err(syscall_err)?; let duplicate = TcpStream { - // SAFETY: `duplicate` made this handle of the same stream, entered against its end. - fd: unsafe { OwnedFd::from_raw_fd(copy.0 as i32) }, + fd: self.fd.try_clone()?, read_timeout_ms: AtomicU32::new(self.read_timeout_ms.load(Relaxed)), write_timeout_ms: AtomicU32::new(self.write_timeout_ms.load(Relaxed)), nonblocking: AtomicBool::new(self.nonblocking.load(Relaxed)), @@ -286,9 +297,9 @@ impl TcpStream { } /// The stream's bell: the handle a watch for room is made on, for reading. - pub fn bell(&self) -> io::Result { + pub fn bell(&self) -> io::Result { let bell = stream::doorbell(self.handle(), stream::Way::Send).map_err(stream_refused)?; - Ok(bell.0 as i32) + Ok(bell.0) } /// Registered for readiness: each named way's want armed, and netstack rung @@ -329,7 +340,7 @@ impl TcpListener { let (ip, port) = addr_to_v4(&addr)?; let bound = toyos::net::tcp_bind(ip, port).map_err(net_err_to_io)?; Ok(TcpListener { - notify_fd: unsafe { OwnedFd::from_raw_fd(bound.notify.into_raw().0 as i32) }, + notify_fd: owned(bound.notify.into_raw())?, socket: Arc::new(NetstackSocket::Tcp(bound.socket_id)), local: SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::from(ip), bound.bound_port)), nonblocking: AtomicBool::new(false), @@ -342,7 +353,7 @@ impl TcpListener { pub fn accept(&self) -> io::Result<(TcpStream, SocketAddr)> { let mut byte = [0u8; 1]; - let notify_fd = RawHandle(self.notify_fd.as_raw_fd() as u32); + let notify_fd = kernel(&self.notify_fd); if self.nonblocking.load(Relaxed) { syscall::read_nonblock(notify_fd, &mut byte).map_err(syscall_err)?; } else { @@ -354,16 +365,12 @@ impl TcpListener { Ipv4Addr::from(accepted.remote_addr), accepted.remote_port, )); - // SAFETY: the accept made this handle for this stream and nothing else answers for it. - let fd = unsafe { OwnedFd::from_raw_fd(accepted.handle.0 as i32) }; - Ok((TcpStream::from_fd(fd), peer)) + Ok((TcpStream::from_fd(owned(accepted.handle)?), peer)) } pub fn duplicate(&self) -> io::Result { - let new_fd = - syscall::dup(RawHandle(self.notify_fd.as_raw_fd() as u32)).map_err(syscall_err)?; Ok(TcpListener { - notify_fd: unsafe { OwnedFd::from_raw_fd(new_fd.0 as i32) }, + notify_fd: self.notify_fd.try_clone()?, socket: Arc::clone(&self.socket), local: self.local, nonblocking: AtomicBool::new(self.nonblocking.load(Relaxed)), @@ -444,8 +451,8 @@ impl UdpSocket { let bound = toyos::net::udp_bind(ip, port).map_err(net_err_to_io)?; Ok(UdpSocket { socket: Arc::new(NetstackSocket::Udp(bound.socket_id)), - tx_fd: unsafe { OwnedFd::from_raw_fd(bound.tx.into_raw().0 as i32) }, - rx_fd: unsafe { OwnedFd::from_raw_fd(bound.rx.into_raw().0 as i32) }, + tx_fd: owned(bound.tx.into_raw())?, + rx_fd: owned(bound.rx.into_raw())?, local: SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::from(ip), bound.bound_port)), peer: crate::sync::Mutex::new(None), broadcast: Arc::new(crate::sync::Mutex::new(false)), @@ -471,7 +478,7 @@ impl UdpSocket { let n = (resp.len as usize).min(buf.len()); if n > 0 { - let rx_fd = RawHandle(self.rx_fd.as_raw_fd() as u32); + let rx_fd = kernel(&self.rx_fd); syscall::read(rx_fd, &mut buf[..n]).map_err(syscall_err)?; } @@ -487,7 +494,7 @@ impl UdpSocket { let (ip, port) = addr_to_v4(addr)?; // Write data to TX pipe first, then send control message - let tx_fd = RawHandle(self.tx_fd.as_raw_fd() as u32); + let tx_fd = kernel(&self.tx_fd); if !buf.is_empty() { syscall::write(tx_fd, buf).map_err(syscall_err)?; } @@ -498,14 +505,10 @@ impl UdpSocket { } pub fn duplicate(&self) -> io::Result { - let new_tx_fd = - syscall::dup(RawHandle(self.tx_fd.as_raw_fd() as u32)).map_err(syscall_err)?; - let new_rx_fd = - syscall::dup(RawHandle(self.rx_fd.as_raw_fd() as u32)).map_err(syscall_err)?; Ok(UdpSocket { socket: Arc::clone(&self.socket), - tx_fd: unsafe { OwnedFd::from_raw_fd(new_tx_fd.0 as i32) }, - rx_fd: unsafe { OwnedFd::from_raw_fd(new_rx_fd.0 as i32) }, + tx_fd: self.tx_fd.try_clone()?, + rx_fd: self.rx_fd.try_clone()?, local: self.local, peer: crate::sync::Mutex::new(*self.peer.lock().unwrap()), broadcast: Arc::clone(&self.broadcast), diff --git a/sdk/std/sys/pal/mod.rs b/sdk/std/sys/pal/mod.rs index 09df6d5866..f0e9d35119 100644 --- a/sdk/std/sys/pal/mod.rs +++ b/sdk/std/sys/pal/mod.rs @@ -38,6 +38,41 @@ pub fn to_io_error(e: toyos_abi::syscall::SyscallError) -> crate::io::Error { crate::io::Error::from(kind) } +/// `f` on the kernel handle descriptor `fd` names, held for the call +/// (`toyos::fd::with`): a descriptor that is not open answers EBADF's error, +/// and one a library keeps as no kernel handle is no file to a kernel call. +pub fn on_fd( + fd: i32, + f: impl FnOnce(toyos_abi::RawHandle) -> Result, +) -> crate::io::Result { + 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 kernel handle this process just got, as a descriptor of its own: std +/// creates descriptors not inherited, as std is close-on-exec on unix. +pub fn new_fd(handle: toyos_abi::RawHandle) -> crate::io::Result { + toyos::fd::install(toyos::fd::Object::handle(handle), false, 0).map_err(fd_error) +} + +/// The descriptor table's refusal as std's error. +pub fn fd_error(e: toyos::fd::Error) -> crate::io::Error { + match e { + toyos::fd::Error::BadFd => { + crate::io::const_error!(crate::io::ErrorKind::Uncategorized, "the descriptor is not open") + } + toyos::fd::Error::Full => { + crate::io::const_error!(crate::io::ErrorKind::QuotaExceeded, "the descriptor table is full") + } + } +} + // argc/argv stored by _start for std::env::args() pub(crate) static ARGC: AtomicUsize = AtomicUsize::new(0); pub(crate) static ARGV: AtomicUsize = AtomicUsize::new(0); // *const *const u8 as usize diff --git a/sdk/std/sys/pipe.rs b/sdk/std/sys/pipe.rs index d8ea9c2f8c..2b1123c24a 100644 --- a/sdk/std/sys/pipe.rs +++ b/sdk/std/sys/pipe.rs @@ -1,30 +1,36 @@ use toyos_abi::{RawHandle, syscall}; use crate::io::{self, BorrowedCursor, IoSlice, IoSliceMut}; -use crate::sys::to_io_error; +use crate::sys::{fd_error, new_fd, on_fd}; +/// One end of a pipe: a descriptor naming it. #[derive(Debug)] pub struct Pipe { - fd: RawHandle, + fd: i32, } pub fn pipe() -> io::Result<(Pipe, Pipe)> { - let fds = syscall::pipe().map_err(to_io_error)?; - Ok((Pipe { fd: fds.read }, Pipe { fd: fds.write })) + let ends = syscall::pipe().map_err(crate::sys::to_io_error)?; + let read = new_fd(ends.read).inspect_err(|_| syscall::close(ends.write))?; + Ok((Pipe { fd: read }, Pipe { fd: new_fd(ends.write)? })) } impl Pipe { pub fn raw_fd(&self) -> i32 { - self.fd.0 as i32 + self.fd + } + + /// The kernel handle behind this end, held for `f`. + pub fn with_handle(&self, f: impl FnOnce(RawHandle) -> T) -> io::Result { + on_fd(self.fd, |h| Ok(f(h))) } pub fn try_clone(&self) -> io::Result { - let new_fd = syscall::dup(self.fd).map_err(to_io_error)?; - Ok(Pipe { fd: new_fd }) + toyos::fd::dup(self.fd, 0, false).map(|fd| Pipe { fd }).map_err(fd_error) } pub fn read(&self, buf: &mut [u8]) -> io::Result { - syscall::read(self.fd, buf).map_err(to_io_error) + on_fd(self.fd, |h| syscall::read(h, buf)) } pub fn read_buf(&self, mut buf: BorrowedCursor<'_, u8>) -> io::Result<()> { @@ -60,7 +66,7 @@ impl Pipe { } pub fn write(&self, buf: &[u8]) -> io::Result { - syscall::write(self.fd, buf).map_err(to_io_error) + on_fd(self.fd, |h| syscall::write(h, buf)) } pub fn write_vectored(&self, bufs: &[IoSlice<'_>]) -> io::Result { @@ -77,6 +83,6 @@ impl Pipe { impl Drop for Pipe { fn drop(&mut self) { - syscall::close(self.fd); + let _ = toyos::fd::close(self.fd); } } diff --git a/sdk/std/sys/process.rs b/sdk/std/sys/process.rs index c8da510faf..6318e14d4c 100644 --- a/sdk/std/sys/process.rs +++ b/sdk/std/sys/process.rs @@ -22,7 +22,7 @@ pub struct Command { stdin: Option, stdout: Option, stderr: Option, - extra_slots: Vec<[u32; 2]>, + extra_fds: Vec<(u32, u32)>, endowments: Vec<(String, u32)>, provided: Vec<(String, u32)>, parent: toyos::spawn::Parent, @@ -75,7 +75,7 @@ impl Command { stdin: None, stdout: None, stderr: None, - extra_slots: Vec::new(), + extra_fds: Vec::new(), endowments: Vec::new(), provided: Vec::new(), parent: toyos::spawn::Parent::Caller, @@ -171,10 +171,11 @@ impl Command { self.cwd.as_ref().map(|cs| Path::new(cs)) } - /// Add an extra handle mapping for the child process. - /// The child will see slot `child_slot` holding the parent's `parent_handle`. - pub fn inherit_handle(&mut self, child_slot: u32, parent_handle: u32) { - self.extra_slots.push([child_slot, parent_handle]); + /// Give the child descriptor `child_fd`, naming this process's + /// `parent_handle`, which this process keeps: the spawn duplicates it. + pub fn inherit_handle(&mut self, child_fd: u32, parent_handle: u32) { + self.extra_fds.retain(|&(fd, _)| fd != child_fd); + self.extra_fds.push((child_fd, parent_handle)); } /// Give the child `handle` under the name `label`. @@ -267,28 +268,26 @@ impl Command { let stdout = self.stdout.as_ref().unwrap_or(&default); let stderr = self.stderr.as_ref().unwrap_or(&default); - let mut slot_map: Vec<[u32; 2]> = Vec::new(); - let mut child_pipes: Vec = Vec::new(); - // The pipes a file server appends a served file from; held until the - // child has its own copies. - let mut streams: Vec = Vec::new(); - // Copies of this process's own stdio and fresh null handles, held - // until the child has its own. - let mut inherited: Vec = Vec::new(); + // The handles the child's descriptors are, each held here until the + // spawn has duplicated it into the child. + let mut held: Vec<(u32, toyos::OwnedHandle)> = Vec::new(); let mut stdin_pipe: Option = None; let mut stdout_pipe: Option = None; let mut stderr_pipe: Option = None; - for (pipe, stdio, slot, is_input) in + for (pipe, stdio, fd, is_input) in [(&mut stdin_pipe, stdin, 0, true), (&mut stdout_pipe, stdout, 1, false), (&mut stderr_pipe, stderr, 2, false)] { - // A slot `inherit_handle` names is that handle's. - if self.extra_slots.iter().any(|&[child, _]| child == slot) { + // A descriptor `inherit_handle` names is that handle's. + if self.extra_fds.iter().any(|&(child, _)| child == fd) { continue; } - Self::setup_slot(&mut slot_map, &mut child_pipes, &mut streams, &mut inherited, pipe, stdio, slot, is_input)?; + if let Some(handle) = Self::child_stdio(pipe, stdio, fd, is_input)? { + held.push((fd, handle)); + } } - let stdio = slot_map.len(); - slot_map.extend_from_slice(&self.extra_slots); + let mut fds: Vec<(u32, toyos_abi::RawHandle)> = + held.iter().map(|(fd, h)| (*fd, toyos::AsHandle::as_handle(h))).collect(); + fds.extend(self.extra_fds.iter().map(|&(fd, h)| (fd, toyos_abi::RawHandle(h)))); let capture = self.env.capture(); let env_buf = env_blob(&capture); @@ -302,8 +301,7 @@ impl Command { argv: &argv_buf, env: &env_buf, cwd: &cwd, - slots: &slot_map, - stdio, + fds: &fds, endow: &endow, provided: &provided, parent: self.parent, @@ -333,68 +331,57 @@ impl Command { // The child keeps the object alive; this process's handle, and with it // its own mapping, goes. drop(image); - drop(child_pipes); - drop(streams); - drop(inherited); + drop(held); let handle = spawned.map_err(refused)?; Ok((Process { handle }, pipes(stdin_pipe, stdout_pipe, stderr_pipe))) } - fn setup_slot( - slot_map: &mut Vec<[u32; 2]>, - child_pipes: &mut Vec, - streams: &mut Vec, - inherited: &mut Vec, + /// The handle the child's descriptor `fd` is, as `stdio` configures it, + /// owned until the spawn has its own: `None` for one the child starts + /// without. A pipe's other end is put in `parent_pipe`. + fn child_stdio( parent_pipe: &mut Option, stdio: &Stdio, - child_slot: u32, + fd: u32, is_input: bool, - ) -> io::Result<()> { - // What this process's own slot holds, if it holds anything: a slot its - // spawn left empty is empty in the child too. - let inherit = |from: u16, slot_map: &mut Vec<[u32; 2]>, inherited: &mut Vec| { - if let Some(copy) = toyos::spawn::inherited(from).map_err(refused)? { - slot_map.push([child_slot, toyos::AsHandle::as_handle(©).0]); - inherited.push(copy); + ) -> io::Result> { + // What this process's own descriptor names, if it is open: one it + // closed is closed in the child too. + let inherit = |from: i32| -> io::Result> { + let copy = toyos::fd::copy(from).map_err(crate::sys::to_io_error)?; + match copy { + Some(copy) if toyos::net::stream::is_stream(toyos::AsHandle::as_handle(©)) => { + Err(refused(toyos::spawn::Refused::Stream(toyos::AsHandle::as_handle(©)))) + } + copy => Ok(copy), } - io::Result::Ok(()) }; - match stdio { - Stdio::Inherit => inherit(child_slot as u16, slot_map, inherited)?, + // SAFETY: each `dup` answers a fresh handle nothing else answers for. + let copy = |pipe: &Pipe| pipe.with_handle(toyos_abi::syscall::dup)?.map(|h| unsafe { toyos::OwnedHandle::from_raw(h) }).map_err(crate::sys::to_io_error); + Ok(match stdio { + Stdio::Inherit => inherit(fd as i32)?, Stdio::MakePipe | Stdio::MakeTtyPipe => { let (r, w) = crate::sys::pipe::pipe()?; if matches!(stdio, Stdio::MakeTtyPipe) { - toyos_abi::syscall::mark_tty(toyos_abi::RawHandle(r.raw_fd() as u32)); - toyos_abi::syscall::mark_tty(toyos_abi::RawHandle(w.raw_fd() as u32)); + r.with_handle(toyos_abi::syscall::mark_tty)?; + w.with_handle(toyos_abi::syscall::mark_tty)?; } - if is_input { - slot_map.push([child_slot, r.raw_fd() as u32]); - child_pipes.push(r); - *parent_pipe = Some(w); - } else { - slot_map.push([child_slot, w.raw_fd() as u32]); - child_pipes.push(w); - *parent_pipe = Some(r); - } - } - Stdio::InheritFile(file) => { - let (handle, stream) = file.as_child_stdio()?; - slot_map.push([child_slot, handle.0]); - streams.extend(stream); + let (theirs, ours) = if is_input { (r, w) } else { (w, r) }; + *parent_pipe = Some(ours); + Some(copy(&theirs)?) } - Stdio::InheritPipe(pipe) => slot_map.push([child_slot, pipe.raw_fd() as u32]), - Stdio::ParentStdout => inherit(1, slot_map, inherited)?, - Stdio::ParentStderr => inherit(2, slot_map, inherited)?, + Stdio::InheritFile(file) => Some(file.as_child_stdio()?), + Stdio::InheritPipe(pipe) => Some(copy(pipe)?), + Stdio::ParentStdout => inherit(1)?, + Stdio::ParentStderr => inherit(2)?, Stdio::Null => { use toyos_abi::handle::Rights; let direction = if is_input { Rights::READ } else { Rights::WRITE }; let null = toyos::null(direction.union(Rights::DUP).union(Rights::TRANSFER).union(Rights::WAIT)) .map_err(crate::sys::to_io_error)?; - slot_map.push([child_slot, toyos::AsHandle::as_handle(&null).0]); - inherited.push(null); + Some(null) } - } - Ok(()) + }) } } diff --git a/sdk/std/sys/stdio.rs b/sdk/std/sys/stdio.rs index 3274c9600c..715b447bc5 100644 --- a/sdk/std/sys/stdio.rs +++ b/sdk/std/sys/stdio.rs @@ -1,13 +1,13 @@ use core::sync::atomic::{AtomicBool, Ordering}; use toyos::log::stdio::{self as log_stdio, Stream}; -use toyos_abi::RawHandle; use toyos_abi::syscall::{self, FileType}; use crate::io::{self, IoSlice, IoSliceMut}; -use crate::sys::to_io_error; +use crate::sys::{on_fd, to_io_error}; -const STDIN: RawHandle = RawHandle(0); +/// Standard input's descriptor. +const STDIN: i32 = 0; // --------------------------------------------------------------------------- // Stdin mode flag (canonical by default, raw when explicitly switched) @@ -37,7 +37,7 @@ static mut LINE_BUF: LineBuf = LineBuf { buf: [0; LINE_BUF_CAP], len: 0, pos: 0 fn read_one() -> io::Result { let mut byte = [0u8; 1]; - let n = syscall::read(STDIN, &mut byte).map_err(to_io_error)?; + let n = on_fd(STDIN, |h| syscall::read(h, &mut byte))?; if n == 0 { Err(io::Error::new(io::ErrorKind::UnexpectedEof, "eof")) } else { Ok(byte[0]) } } @@ -127,10 +127,14 @@ impl Stdin { impl io::Read for Stdin { fn read(&mut self, buf: &mut [u8]) -> io::Result { - let stat = syscall::fstat(STDIN).ok(); + // A closed stdin is at its end, as std's on unix reads one. + if toyos::fd::inheritable(STDIN).is_err() { + return Ok(0); + } + let stat = on_fd(STDIN, syscall::fstat).ok(); let interactive = stat.is_some_and(|s| s.file_type == FileType::Tty); if STDIN_RAW.load(Ordering::Relaxed) || !interactive { - syscall::read(STDIN, buf).map_err(to_io_error) + on_fd(STDIN, |h| syscall::read(h, buf)) } else { canonical_read(buf) } @@ -209,9 +213,9 @@ impl io::Write for Stderr { pub const STDIN_BUF_SIZE: usize = 64; -/// No error means a stdio stream is closed: a slot a process does not hold -/// ends it at its first use rather than answering one, and every error a -/// held stream answers is the caller's to see. +/// No error means a stdio stream is closed: a closed stdin reads its end and +/// a closed stdout or stderr drops its bytes without one, and every error an +/// open stream answers is the caller's to see. pub fn is_ebadf(_err: &io::Error) -> bool { false } diff --git a/sdk/toyos/Cargo.toml b/sdk/toyos/Cargo.toml index 54256318da..0353ec3063 100644 --- a/sdk/toyos/Cargo.toml +++ b/sdk/toyos/Cargo.toml @@ -7,7 +7,10 @@ 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"] +rustc-dep-of-std = ["core", "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 = [] # The negative control for the log ring's publication. It makes the store that # publishes a record `Relaxed`, and `kernel-loom`'s `log_ring` model must red: # diff --git a/sdk/toyos/src/endow.rs b/sdk/toyos/src/endow.rs index a5fce33ee0..494ce7cabd 100644 --- a/sdk/toyos/src/endow.rs +++ b/sdk/toyos/src/endow.rs @@ -64,6 +64,7 @@ macro_rules! from_handle { } from_handle! { + OwnedHandle => |h| h, Acceptor => Acceptor, Connector => Connector, Namespace => Namespace, @@ -113,8 +114,8 @@ impl Endowments { .is_some_and(|i| self.handles[i].load(Ordering::Acquire) != HANDLE_INVALID.0) } - /// Every label in the table, taken or not. For a diagnostic; nothing - /// resolves through it. + /// Every label in the table, taken or not: for a diagnostic, and for a + /// family of labels such as `toyos::fd`'s, each then taken by name. pub fn labels(&self) -> impl Iterator { (0..self.count).map(|i| self.label_at(i)) } diff --git a/sdk/toyos/src/fd.rs b/sdk/toyos/src/fd.rs new file mode 100644 index 0000000000..6355ef038d --- /dev/null +++ b/sdk/toyos/src/fd.rs @@ -0,0 +1,532 @@ +//! The process's one descriptor table: what a raw fd number means, for std, +//! libc and every crate between them. +//! +//! **A descriptor names an object**: a kernel handle, or an object of a +//! userland library that owns it ([`Object::opaque`]). Descriptors are +//! numbered lowest-free as POSIX has them, and [`dup`] and [`dup2`] make +//! further descriptors that share one object. Each descriptor carries its own +//! **inheritable** flag; whether a child is given it is the launchers' rule. +//! +//! **A call on a descriptor holds its object for the call** ([`with`]): a +//! descriptor closed meanwhile goes at once, and its object is released when +//! the last descriptor and the last call are done. So a racing close makes +//! the next lookup `BadFd`, and never a call on a handle that was closed. +//! +//! **One copy of this crate owns the table**, the one the `fd-table` feature +//! is on in: std's (`rustc-dep-of-std`) in a Rust program, libc's own in a C +//! program. It exports `toyos_fd_*`, and every copy, the owner's included, +//! reaches the table only through them, so all copies linked into one image +//! share one table. Nothing catches a second owner at link time: the feature +//! is set in those two builds and nowhere else. +//! +//! **A process's descriptors arrive as endowments**, `fd:` (`FD_PREFIX`), +//! each installed at its number at the table's first use; its stdio is +//! descriptors 0 to 2 among them. + +use toyos_abi::RawHandle; + +/// A descriptor number. +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; + +/// Why a table call was refused. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Error { + /// No descriptor by that number: `EBADF`. + BadFd, + /// No free descriptor at or above the floor asked, or no free object: + /// `EMFILE`. + Full, +} + +/// 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, + /// An opaque object's word, its owner's to interpret. + pub word: u64, + /// How an opaque object is let go once nothing names it. + pub release: Option, +} + +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 } + } + + /// 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) } + } + + /// Let the object go: an opaque one to its library, a handle closed (a + /// stream's end with its last handle, `net::stream::let_go`). + pub fn release(self) { + match self.release { + Some(release) => release(self.word), + None => crate::net::stream::let_go(self.handle), + } + } +} + +/// One descriptor's entry: its object's index and its own flag. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +struct Entry { + object: u16, + inheritable: bool, +} + +#[derive(Clone, Copy)] +struct Held { + object: Object, + /// The descriptors naming it and the calls holding it. + refs: u32, +} + +/// The table's state, without its lock: every decision the table makes, +/// which a host tests. A removal answers the object it released, for the +/// caller to let go outside the lock. +pub struct Table { + fds: [Option; N], + objects: [Option; N], +} + +impl Default for Table { + fn default() -> Self { + Self::new() + } +} + +impl Table { + pub const fn new() -> Self { + Self { fds: [None; N], objects: [None; N] } + } + + fn entry(&self, fd: Fd) -> Result { + usize::try_from(fd).ok().and_then(|i| self.fds.get(i).copied().flatten()).ok_or(Error::BadFd) + } + + fn lowest_free(&self, floor: Fd) -> Result { + let floor = usize::try_from(floor).map_err(|_| Error::Full)?; + (floor..N).find(|&i| self.fds[i].is_none()).ok_or(Error::Full) + } + + /// A reference fewer on `object`: the object, once none is left. + fn unref(&mut self, object: u16) -> Option { + let held = self.objects[object as usize].as_mut().expect("a referenced object"); + held.refs -= 1; + match held.refs { + 0 => self.objects[object as usize].take().map(|h| h.object), + _ => None, + } + } + + /// Name `object` by the lowest free descriptor at or above `floor`. + pub fn install(&mut self, object: Object, inheritable: bool, floor: Fd) -> Result { + let fd = self.lowest_free(floor)?; + let slot = self.objects.iter().position(Option::is_none).ok_or(Error::Full)?; + self.objects[slot] = Some(Held { object, refs: 1 }); + self.fds[fd] = Some(Entry { object: slot as u16, inheritable }); + Ok(fd as Fd) + } + + /// A second descriptor on `fd`'s object, the lowest free at or above + /// `floor`. + pub fn dup(&mut self, fd: Fd, floor: Fd, inheritable: bool) -> Result { + let entry = self.entry(fd)?; + let new = self.lowest_free(floor)?; + self.objects[entry.object as usize].as_mut().expect("named").refs += 1; + self.fds[new] = Some(Entry { object: entry.object, inheritable }); + Ok(new as Fd) + } + + /// `new` on `old`'s object, whatever `new` named before: that object, + /// if this was its last reference. `new == old` changes nothing. + pub fn dup2(&mut self, old: Fd, new: Fd, inheritable: bool) -> Result, Error> { + let entry = self.entry(old)?; + let slot = usize::try_from(new).ok().filter(|&i| i < N).ok_or(Error::BadFd)?; + if new == old { + return Ok(None); + } + self.objects[entry.object as usize].as_mut().expect("named").refs += 1; + let displaced = self.fds[slot].replace(Entry { object: entry.object, inheritable }); + Ok(displaced.and_then(|d| self.unref(d.object))) + } + + /// Forget `fd`: its object, if this was its last reference. + pub fn close(&mut self, fd: Fd) -> Result, Error> { + let entry = self.entry(fd)?; + self.fds[fd as usize] = None; + Ok(self.unref(entry.object)) + } + + /// `fd`'s object, held until [`Self::unuse`] of the token answered. + pub fn acquire(&mut self, fd: Fd) -> Result<(u16, Object), Error> { + let entry = self.entry(fd)?; + let held = self.objects[entry.object as usize].as_mut().expect("named"); + held.refs += 1; + Ok((entry.object, held.object)) + } + + /// A call done with the object `token` names: the object, if nothing + /// names it any more. + pub fn unuse(&mut self, token: u16) -> Option { + self.unref(token) + } + + /// Make `fd` name `object` from here on, in place: the object it named, + /// if this was its last reference. Its flag stays. + pub fn replace(&mut self, fd: Fd, object: Object) -> Result, Error> { + let entry = self.entry(fd)?; + let slot = self.objects.iter().position(Option::is_none).ok_or(Error::Full)?; + self.objects[slot] = Some(Held { object, refs: 1 }); + self.fds[fd as usize] = Some(Entry { object: slot as u16, inheritable: entry.inheritable }); + Ok(self.unref(entry.object)) + } + + pub fn inheritable(&self, fd: Fd) -> Result { + self.entry(fd).map(|e| e.inheritable) + } + + pub fn set_inheritable(&mut self, fd: Fd, inheritable: bool) -> Result<(), Error> { + self.entry(fd)?; + self.fds[fd as usize].as_mut().expect("present").inheritable = inheritable; + Ok(()) + } +} + +// The C interface: what every copy calls and the owner defines. A negative +// answer is an [`Error`]: -1 `BadFd`, -2 `Full`. + +const BAD_FD: i32 = -1; +const FULL: i32 = -2; + +#[cfg(any(feature = "fd-table", test))] +fn encode(e: Error) -> i32 { + match e { + Error::BadFd => BAD_FD, + Error::Full => FULL, + } +} + +fn decode(word: i32) -> Result { + match word { + BAD_FD => Err(Error::BadFd), + FULL => Err(Error::Full), + n => Ok(n), + } +} + +extern "C" { + fn toyos_fd_install(object: Object, inheritable: bool, floor: Fd) -> i32; + fn toyos_fd_acquire(fd: Fd, out: *mut Object) -> i32; + fn toyos_fd_unuse(token: i32); + fn toyos_fd_close(fd: Fd) -> i32; + fn toyos_fd_dup(fd: Fd, floor: Fd, inheritable: bool) -> i32; + fn toyos_fd_dup2(old: Fd, new: Fd, inheritable: bool) -> i32; + fn toyos_fd_replace(fd: Fd, object: Object) -> i32; + fn toyos_fd_inheritable(fd: Fd) -> i32; + fn toyos_fd_set_inheritable(fd: Fd, inheritable: bool) -> i32; +} + +/// Name `object` by the lowest free descriptor at or above `floor`. On +/// `Full` the object is let go, so nothing it held is left behind. +pub fn install(object: Object, inheritable: bool, floor: Fd) -> Result { + // SAFETY: the owner's definition below, which takes the object by value. + decode(unsafe { toyos_fd_install(object, inheritable, floor) }) +} + +/// `f` on `fd`'s object, held for the call: `BadFd` for a descriptor not +/// open. +pub fn with(fd: Fd, f: impl FnOnce(Object) -> R) -> Result { + let mut object = Object::handle(toyos_abi::HANDLE_INVALID); + // SAFETY: `object` is a live place for the answer. + let token = decode(unsafe { toyos_fd_acquire(fd, &mut object) })?; + struct Unuse(i32); + impl Drop for Unuse { + fn drop(&mut self) { + // SAFETY: a token `toyos_fd_acquire` answered, given back once. + unsafe { toyos_fd_unuse(self.0) } + } + } + let _unuse = Unuse(token); + Ok(f(object)) +} + +pub fn close(fd: Fd) -> Result<(), Error> { + // SAFETY: plain values. + decode(unsafe { toyos_fd_close(fd) }).map(drop) +} + +pub fn dup(fd: Fd, floor: Fd, inheritable: bool) -> Result { + // SAFETY: plain values. + decode(unsafe { toyos_fd_dup(fd, floor, inheritable) }) +} + +/// POSIX's `dup2`: `new` on `old`'s object, after closing what `new` named. +pub fn dup2(old: Fd, new: Fd, inheritable: bool) -> Result { + // SAFETY: plain values. + decode(unsafe { toyos_fd_dup2(old, new, inheritable) }) +} + +/// Make `fd` name `object` in place, letting go what it named: a socket +/// that connects becomes its stream at the same number. On `BadFd` or +/// `Full` the object is let go. +pub fn replace(fd: Fd, object: Object) -> Result<(), Error> { + // SAFETY: the owner's definition below, which takes the object by value. + decode(unsafe { toyos_fd_replace(fd, object) }).map(drop) +} + +/// A copy of the kernel handle `fd` names, for a child: `None` for a +/// descriptor that is not open, as a child of POSIX's holds no closed one; +/// `Err` for one that names no kernel handle, or that the kernel will not +/// duplicate. +pub fn copy(fd: Fd) -> Result, toyos_abi::syscall::SyscallError> { + match with(fd, |o| match o.release { + None => toyos_abi::syscall::dup(o.handle).map(crate::OwnedHandle), + Some(_) => Err(toyos_abi::syscall::SyscallError::InvalidArgument), + }) { + Ok(copied) => copied.map(Some), + Err(_) => Ok(None), + } +} + +pub fn inheritable(fd: Fd) -> Result { + // SAFETY: plain values. + decode(unsafe { toyos_fd_inheritable(fd) }).map(|flag| flag != 0) +} + +pub fn set_inheritable(fd: Fd, inheritable: bool) -> Result<(), Error> { + // SAFETY: plain values. + decode(unsafe { toyos_fd_set_inheritable(fd, inheritable) }).map(drop) +} + +/// The descriptor an `fd:` endowment's label names: `None` for any other +/// label, and for a number past the table. +pub fn endowed_fd(label: &str) -> Option { + let n: Fd = label.strip_prefix(toyos_abi::syscall::FD_PREFIX)?.parse().ok()?; + (0..MAX_FDS as Fd).contains(&n).then_some(n) +} + +/// The table itself, in the copy that owns it. +#[cfg(all(feature = "fd-table", not(test)))] +mod owner { + use super::*; + use crate::endow::Endowments; + use crate::sync::Lock; + use crate::OwnedHandle; + + /// The table, and whether its endowed descriptors were installed yet. + static TABLE: Lock<(bool, Table)> = Lock::new((false, Table::new())); + + /// `f` on the table, whose first use installs every descriptor the + /// process was endowed at its number. + fn table(f: impl FnOnce(&mut Table) -> R) -> R { + let mut held = TABLE.lock(); + let (ready, table) = &mut *held; + if !*ready { + *ready = true; + let endowed = Endowments::get(); + for label in endowed.labels() { + let Some(fd) = endowed_fd(label) else { continue }; + let Some(handle) = endowed.take::(label) else { continue }; + let installed = table.install(Object::handle(handle.into_raw()), true, fd); + assert_eq!(installed, Ok(fd), "toyos::fd: endowed descriptor {fd} is not free at its number"); + } + } + f(table) + } + + fn released(object: Option) { + if let Some(object) = object { + object.release(); + } + } + + #[no_mangle] + extern "C" fn toyos_fd_install(object: Object, inheritable: bool, floor: Fd) -> i32 { + table(|t| t.install(object, inheritable, floor)).unwrap_or_else(|e| { + object.release(); + encode(e) + }) + } + + #[no_mangle] + unsafe extern "C" fn toyos_fd_acquire(fd: Fd, out: *mut Object) -> i32 { + match table(|t| t.acquire(fd)) { + Ok((token, object)) => { + // SAFETY: the caller's live place. + unsafe { out.write(object) }; + i32::from(token) + } + Err(e) => encode(e), + } + } + + #[no_mangle] + extern "C" fn toyos_fd_unuse(token: i32) { + let token = u16::try_from(token).expect("toyos::fd: a token no acquire answered"); + released(table(|t| t.unuse(token))); + } + + #[no_mangle] + extern "C" fn toyos_fd_close(fd: Fd) -> i32 { + match table(|t| t.close(fd)) { + Ok(object) => { + released(object); + 0 + } + Err(e) => encode(e), + } + } + + #[no_mangle] + extern "C" fn toyos_fd_dup(fd: Fd, floor: Fd, inheritable: bool) -> i32 { + table(|t| t.dup(fd, floor, inheritable)).unwrap_or_else(encode) + } + + #[no_mangle] + extern "C" fn toyos_fd_dup2(old: Fd, new: Fd, inheritable: bool) -> i32 { + match table(|t| t.dup2(old, new, inheritable)) { + Ok(displaced) => { + released(displaced); + new + } + Err(e) => encode(e), + } + } + + #[no_mangle] + extern "C" fn toyos_fd_replace(fd: Fd, object: Object) -> i32 { + match table(|t| t.replace(fd, object)) { + Ok(old) => { + released(old); + 0 + } + Err(e) => { + object.release(); + encode(e) + } + } + } + + #[no_mangle] + extern "C" fn toyos_fd_inheritable(fd: Fd) -> i32 { + table(|t| t.inheritable(fd)).map_or_else(encode, i32::from) + } + + #[no_mangle] + extern "C" fn toyos_fd_set_inheritable(fd: Fd, inheritable: bool) -> i32 { + table(|t| t.set_inheritable(fd, inheritable)).map_or_else(encode, |()| 0) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + extern "C" fn nothing(_: u64) {} + + fn o(word: u64) -> Object { + Object::opaque(word, nothing) + } + + type T = Table<8>; + + /// The word of what a removal released, which names it in these tests. + fn w(released: Result, Error>) -> Result, Error> { + released.map(|o| o.map(|o| o.word)) + } + + #[test] + fn descriptors_are_numbered_lowest_free_from_the_floor() { + let mut t = T::new(); + assert_eq!(t.install(o(1), false, 0), Ok(0)); + assert_eq!(t.install(o(2), false, 0), Ok(1)); + assert_eq!(t.install(o(3), false, 5), Ok(5)); + assert_eq!(w(t.close(0)), Ok(Some(1))); + assert_eq!(t.install(o(4), false, 0), Ok(0), "a closed number is the lowest free again"); + assert_eq!(t.install(o(5), false, 8), Err(Error::Full)); + assert_eq!(t.install(o(5), false, -1), Err(Error::Full)); + } + + /// Descriptors on one object release it with the last of them, and each + /// keeps a flag of its own. + #[test] + fn an_object_goes_with_its_last_descriptor_and_flags_are_per_descriptor() { + let mut t = T::new(); + let a = t.install(o(7), true, 0).unwrap(); + let b = t.dup(a, 0, false).unwrap(); + assert_eq!((t.inheritable(a), t.inheritable(b)), (Ok(true), Ok(false))); + assert_eq!(w(t.close(a)), Ok(None), "released while a dup named it"); + assert_eq!(w(t.close(b)), Ok(Some(7))); + assert_eq!(w(t.close(b)), Err(Error::BadFd)); + } + + /// A call holds its object: a close meanwhile makes the descriptor gone + /// at once, and the object goes when the call is done. + #[test] + fn a_close_during_a_call_releases_the_object_when_the_call_is_done() { + let mut t = T::new(); + let fd = t.install(o(9), false, 0).unwrap(); + let (token, object) = t.acquire(fd).unwrap(); + assert_eq!(object.word, 9); + assert_eq!(w(t.close(fd)), Ok(None), "the object went under a call"); + assert!(matches!(t.acquire(fd), Err(Error::BadFd)), "a closed descriptor still answered"); + assert_eq!(t.unuse(token).map(|o| o.word), Some(9)); + } + + #[test] + fn dup2_displaces_what_the_target_named_and_onto_itself_changes_nothing() { + let mut t = T::new(); + let a = t.install(o(1), false, 0).unwrap(); + let b = t.install(o(2), false, 0).unwrap(); + assert_eq!(w(t.dup2(a, b, true)), Ok(Some(2))); + assert_eq!(t.inheritable(b), Ok(true)); + assert_eq!(w(t.dup2(a, a, true)), Ok(None)); + assert_eq!(t.inheritable(a), Ok(false), "dup2 onto itself set a flag"); + assert_eq!(w(t.dup2(a, 6, false)), Ok(None), "an unused target"); + assert_eq!(w(t.dup2(a, 8, false)), Err(Error::BadFd), "a target past the table"); + assert_eq!(w(t.dup2(7, 1, false)), Err(Error::BadFd)); + for fd in [a, b, 6] { + let last = t.close(fd).unwrap(); + assert_eq!(last.is_some(), fd == 6, "descriptor {fd}"); + } + } + + #[test] + fn replace_keeps_the_number_and_the_flag_and_releases_the_old_object() { + let mut t = T::new(); + let fd = t.install(o(1), true, 0).unwrap(); + assert_eq!(w(t.replace(fd, o(2))), Ok(Some(1))); + assert_eq!(t.inheritable(fd), Ok(true)); + assert_eq!(w(t.close(fd)), Ok(Some(2))); + assert_eq!(w(t.replace(fd, o(3))), Err(Error::BadFd)); + } + + #[test] + fn an_fd_label_names_its_number_and_no_other_label_names_one() { + assert_eq!(endowed_fd("fd:0"), Some(0)); + assert_eq!(endowed_fd("fd:4095"), Some(4095)); + for other in ["fd:4096", "fd:-1", "fd:", "fd:1x", "svc", "fd1", "dev:fd:1"] { + assert_eq!(endowed_fd(other), None, "{other}"); + } + } + + #[test] + fn the_c_words_round_trip_every_refusal() { + for e in [Error::BadFd, Error::Full] { + assert_eq!(decode(encode(e)), Err(e)); + } + assert_eq!(decode(0), Ok(0)); + assert_eq!(decode(4095), Ok(4095)); + } +} diff --git a/sdk/toyos/src/launch.rs b/sdk/toyos/src/launch.rs index 5eaaa0b9b2..f11e3790f0 100644 --- a/sdk/toyos/src/launch.rs +++ b/sdk/toyos/src/launch.rs @@ -61,9 +61,9 @@ pub const MSG_GONE: u32 = 5; /// authority in two batches that can arrive apart. pub const MAX_LAUNCH_EXTRAS: usize = 5; -/// Slots a launch may install in the child: stdin, stdout and stderr, and a +/// Descriptors a launch may give the child: stdin, stdout and stderr, and a /// caller that wants anything else in a child's table spawns it directly. -pub const MAX_LAUNCH_SLOTS: usize = 3; +pub const MAX_LAUNCH_FDS: usize = 3; const HEADER: usize = 36; @@ -87,7 +87,7 @@ pub enum Parent { /// `argv` and `env` are exactly the blobs `SYS_SPAWN` takes, so the launcher /// path and the direct path build one thing rather than two. /// -/// **Every handle in `slots`, `extras` and `parent` is the caller's to give, +/// **Every handle in `fds`, `extras` and `parent` is the caller's to give, /// and is named once.** [`launch`] consumes each one that it sends: it is moved /// to the launcher, or closed when the kernel refuses the move. A launch that /// names one twice, or the connection it is sent on, is a move the kernel @@ -113,11 +113,10 @@ pub struct Launch<'a> { /// `surface` port it made — a name the supervisor cannot know, because there is one /// per terminal. pub extras: &'a [(&'a str, RawHandle)], - /// `(child slot, handle)`, at most [`MAX_LAUNCH_SLOTS`] of them. The - /// handles are **moved**, so a caller passing on its own stdout duplicates - /// first — which is the same rule `SpawnArgs`'s two vectors state, with the - /// duplication moved to the caller because a launch has only the one verb. - pub slots: &'a [(u32, RawHandle)], + /// `(child descriptor, handle)`, at most [`MAX_LAUNCH_FDS`] of them, which + /// the launcher endows as `fd:`. The handles are **moved**, so a caller + /// passing on its own stdout duplicates first. + pub fds: &'a [(u32, RawHandle)], /// Whom the program is placed under. A place is **moved** with the batch, /// and counts against [`MAX_LAUNCH_EXTRAS`]. pub parent: Parent, @@ -126,17 +125,17 @@ pub struct Launch<'a> { /// Why a request would not go on the wire. #[derive(Debug, PartialEq, Eq)] pub enum EncodeError { - /// More than [`MAX_LAUNCH_EXTRAS`], or more than [`MAX_LAUNCH_SLOTS`]. + /// More than [`MAX_LAUNCH_EXTRAS`], or more than [`MAX_LAUNCH_FDS`]. TooMany, /// The blobs do not fit one frame. TooLarge, } /// The handles of one launch, and how many of them. -type Batch = ([RawHandle; MAX_LAUNCH_EXTRAS + MAX_LAUNCH_SLOTS], usize); +type Batch = ([RawHandle; MAX_LAUNCH_EXTRAS + MAX_LAUNCH_FDS], usize); impl Launch<'_> { - /// The numbers this launch names, each once and never `conn`: slots, then + /// The numbers this launch names, each once and never `conn`: descriptors, then /// extras, then the place. [`launch`] takes each as its own, and a number /// with two owners is closed twice. /// @@ -144,16 +143,16 @@ impl Launch<'_> { /// the kernel refuses `InvalidArgument`: what it holds is to close, not to /// send. fn batch(&self, conn: RawHandle) -> Result { - let mut out = [RawHandle(0); MAX_LAUNCH_EXTRAS + MAX_LAUNCH_SLOTS]; + let mut out = [RawHandle(0); MAX_LAUNCH_EXTRAS + MAX_LAUNCH_FDS]; let mut n = 0; let mut sendable = true; let place = match self.parent { Parent::Place(place) => Some(place), Parent::Supervisor => None, }; - let slots = self.slots.iter().map(|(_, h)| h); + let fds = self.fds.iter().map(|(_, h)| h); let extras = self.extras.iter().map(|(_, h)| h); - for handle in slots.chain(extras).chain(&place) { + for handle in fds.chain(extras).chain(&place) { if *handle == conn || out[..n].contains(handle) { sendable = false; continue; @@ -170,13 +169,13 @@ impl Launch<'_> { Parent::Place(_) => (1, PARENT_PLACE), Parent::Supervisor => (0, PARENT_SUPERVISOR), }; - if self.extras.len() + place > MAX_LAUNCH_EXTRAS || self.slots.len() > MAX_LAUNCH_SLOTS { + if self.extras.len() + place > MAX_LAUNCH_EXTRAS || self.fds.len() > MAX_LAUNCH_FDS { return Err(EncodeError::TooMany); } let names_len: usize = self.extras.iter().map(|(n, _)| n.len() + 1).sum(); - let slots_len = self.slots.len() * 4; + let fds_len = self.fds.len() * 4; let total = HEADER - + slots_len + + fds_len + self.program.len() + self.argv.len() + self.env.len() @@ -186,9 +185,9 @@ impl Launch<'_> { return Err(EncodeError::TooLarge); } let lens = [ - self.slots.len() as u32, + self.fds.len() as u32, self.extras.len() as u32, - slots_len as u32, + fds_len as u32, self.program.len() as u32, self.argv.len() as u32, self.env.len() as u32, @@ -204,8 +203,8 @@ impl Launch<'_> { buf[*at..*at + bytes.len()].copy_from_slice(bytes); *at += bytes.len(); }; - for (slot, _) in self.slots { - put(&slot.to_le_bytes(), &mut at); + for (fd, _) in self.fds { + put(&fd.to_le_bytes(), &mut at); } put(self.program.as_bytes(), &mut at); put(self.argv, &mut at); @@ -230,7 +229,7 @@ pub struct Request<'a> { pub env: &'a [u8], pub cwd: &'a str, pub extra_count: usize, - slots: &'a [u8], + fds: &'a [u8], names: &'a [u8], parent: u32, } @@ -244,18 +243,18 @@ impl<'a> Request<'a> { u32::from_le_bytes([buf[i * 4], buf[i * 4 + 1], buf[i * 4 + 2], buf[i * 4 + 3]]) as usize }; - let (slot_count, extra_count) = (field(0), field(1)); - let (slots_len, program_len, argv_len, env_len, cwd_len, names_len) = + let (fd_count, extra_count) = (field(0), field(1)); + let (fds_len, program_len, argv_len, env_len, cwd_len, names_len) = (field(2), field(3), field(4), field(5), field(6), field(7)); let parent = field(8) as u32; if extra_count + usize::from(parent == PARENT_PLACE) > MAX_LAUNCH_EXTRAS - || slot_count > MAX_LAUNCH_SLOTS - || slots_len != slot_count * 4 + || fd_count > MAX_LAUNCH_FDS + || fds_len != fd_count * 4 { return None; } let total = HEADER - .checked_add(slots_len)? + .checked_add(fds_len)? .checked_add(program_len)? .checked_add(argv_len)? .checked_add(env_len)? @@ -270,7 +269,7 @@ impl<'a> Request<'a> { *at += len; slice }; - let slots = take(slots_len, &mut at); + let fds = take(fds_len, &mut at); let program = core::str::from_utf8(take(program_len, &mut at)).ok()?; let argv = take(argv_len, &mut at); let env = take(env_len, &mut at); @@ -279,7 +278,7 @@ impl<'a> Request<'a> { if names.iter().filter(|&&b| b == 0).count() != extra_count { return None; } - Some(Self { program, argv, env, cwd, extra_count, slots, names, parent }) + Some(Self { program, argv, env, cwd, extra_count, fds, names, parent }) } /// Whom the request names as the parent: `None` for neither word, which @@ -292,18 +291,18 @@ impl<'a> Request<'a> { } } - /// The handles the batch carries: the slots, the extras, then the place. + /// The handles the batch carries: the descriptors, the extras, then the place. pub fn handle_count(&self) -> usize { - self.slot_count() + self.extra_count + usize::from(self.parent == PARENT_PLACE) + self.fd_count() + self.extra_count + usize::from(self.parent == PARENT_PLACE) } - /// The child slots the first handles of the batch are for, in order. - pub fn slot_numbers(&self) -> impl Iterator + '_ { - self.slots.as_chunks::<4>().0.iter().map(|c| u32::from_le_bytes(*c)) + /// The child descriptors the first handles of the batch are for, in order. + pub fn fd_numbers(&self) -> impl Iterator + '_ { + self.fds.as_chunks::<4>().0.iter().map(|c| u32::from_le_bytes(*c)) } - pub fn slot_count(&self) -> usize { - self.slots.len() / 4 + pub fn fd_count(&self) -> usize { + self.fds.len() / 4 } /// The extra connectors' names, in the order their handles arrive. @@ -405,7 +404,7 @@ mod tests { env: b"", cwd: "/", extras: &[], - slots: &[], + fds: &[], parent, }; for (parent, named) in [(Parent::Place(RawHandle(9)), Parent::Place(())), (Parent::Supervisor, Parent::Supervisor)] { @@ -426,9 +425,9 @@ mod tests { fn a_batch_names_each_handle_once_and_never_its_connection() { const CONN: RawHandle = RawHandle(3); let h = RawHandle; - let batch = |slots: &[(u32, RawHandle)], extras: &[(&str, RawHandle)], parent| { + let batch = |fds: &[(u32, RawHandle)], extras: &[(&str, RawHandle)], parent| { let request = - Launch { program: "/system/bin/cat", argv: b"", env: b"", cwd: "/", extras, slots, parent }; + Launch { program: "/system/bin/cat", argv: b"", env: b"", cwd: "/", extras, fds, parent }; let named = |(handles, count): Batch| handles[..count].to_vec(); request.batch(CONN).map(named).map_err(named) }; @@ -437,11 +436,11 @@ mod tests { Ok(vec![h(5), h(6), h(7), h(8), h(9)]), ); assert_eq!(batch(&[], &[("a", h(7)), ("b", h(7))], Parent::Supervisor), Err(vec![h(7)]), "two extras"); - assert_eq!(batch(&[(0, h(5)), (1, h(5))], &[], Parent::Supervisor), Err(vec![h(5)]), "two slots"); + assert_eq!(batch(&[(0, h(5)), (1, h(5))], &[], Parent::Supervisor), Err(vec![h(5)]), "two descriptors"); assert_eq!( batch(&[(0, h(7)), (1, h(6))], &[("a", h(7))], Parent::Supervisor), Err(vec![h(7), h(6)]), - "a slot and an extra", + "a descriptor and an extra", ); assert_eq!( batch(&[(0, h(5))], &[("a", h(9))], Parent::Place(h(9))), diff --git a/sdk/toyos/src/lib.rs b/sdk/toyos/src/lib.rs index 50cc7d984e..da8a6f3a11 100644 --- a/sdk/toyos/src/lib.rs +++ b/sdk/toyos/src/lib.rs @@ -16,6 +16,7 @@ pub mod audio; pub mod census; pub mod device; pub mod endow; +pub mod fd; 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 a71f5863e3..fe88d9fb8a 100644 --- a/sdk/toyos/src/log/stdio.rs +++ b/sdk/toyos/src/log/stdio.rs @@ -1,15 +1,15 @@ //! A program's stdout and stderr, and its own lines: records into its log ring -//! when the stream is one, bytes into the handle when it is anything else. +//! when the stream is one, bytes into its descriptor when it is anything else. //! -//! **Where a stream goes is what its slot holds.** `/system/bin/supervisor` puts a -//! program's log ring in slots 1 and 2, so a daemon's output is records; a -//! program a terminal started holds that terminal's pipes there, so its output -//! is bytes on the terminal. The first write to a stream asks the slot once -//! ([`FileType::SharedMemory`] is a ring) and every later one takes the answer, -//! so a write to a ring makes no syscall. A program a daemon spawns directly -//! inherits the daemon's slots and so writes into the daemon's ring: its lines -//! are the daemon's, which is the daemon's decision to have spawned it that -//! way. +//! **Where a stream goes is what its descriptor names** (`crate::fd`). +//! `/system/bin/supervisor` gives a program its log ring as descriptors 1 and +//! 2, so a daemon's output is records; a program a terminal started holds that +//! terminal's pipes there, so its output is bytes on the terminal. The first +//! write to a stream asks the descriptor once ([`FileType::SharedMemory`] is a +//! ring) and every later one takes the answer, so a write to a ring makes no +//! syscall. A program a daemon spawns directly inherits the daemon's stdio and +//! so writes into the daemon's ring: its lines are the daemon's, which is the +//! daemon's decision to have spawned it that way. //! //! A write to a ring **never waits, allocates or makes a syscall**: it stamps //! the record — time off the clock page, thread off the thread's control @@ -25,7 +25,7 @@ use toyos_abi::log::Severity; use super::region::{Body, FLAG_CLOSES, FLAG_UNENDED, TEXT_BYTES}; -/// A standard stream, by the slot it is. +/// A standard stream, by the descriptor it is. #[derive(Clone, Copy, Debug, PartialEq, Eq)] #[repr(u32)] pub enum Stream { @@ -160,7 +160,7 @@ impl Held { #[cfg(all(target_os = "toyos", not(test)))] pub use target::*; -/// The guest's half: slots, rings and syscalls. +/// The guest's half: descriptors, rings and syscalls. #[cfg(all(target_os = "toyos", not(test)))] mod target { use core::ptr::NonNull; @@ -168,15 +168,14 @@ mod target { use toyos_abi::log::Severity; use toyos_abi::syscall::{self, FileType, SyscallError}; - use toyos_abi::RawHandle; use super::{compose, Held, Stream}; use crate::log::region::{Body, Lane, Ring, RING_BYTES}; - /// Unasked, being asked, a handle — and past those, a ring's address. + /// Unasked, being asked, a descriptor — and past those, a ring's address. const UNASKED: usize = 0; const ASKING: usize = 1; - const HANDLE: usize = 2; + const DESCRIPTOR: usize = 2; static SINKS: [AtomicUsize; 2] = [AtomicUsize::new(UNASKED), AtomicUsize::new(UNASKED)]; static PENDING: [Held; 2] = [Held::new(Stream::Out), Held::new(Stream::Err)]; @@ -187,14 +186,15 @@ mod target { /// Where a stream's bytes go. pub enum Sink { Ring(Ring), - Handle(RawHandle), + /// The stream's descriptor (`crate::fd`), 1 or 2. + Descriptor(i32), } fn index(stream: Stream) -> usize { stream as usize - 1 } - /// The stream's sink, asking its slot the first time. + /// The stream's sink, asking its descriptor the first time. pub fn sink(stream: Stream) -> Sink { let word = &SINKS[index(stream)]; loop { @@ -203,17 +203,17 @@ mod target { if word.compare_exchange(UNASKED, ASKING, Ordering::Acquire, Ordering::Acquire).is_ok() { match ask(stream) { Ok(answer) => word.store(answer, Ordering::Release), - // The handle first, so the panic's own message has a + // The descriptor first, so the panic's own message has a // stream to go to rather than a probe to wait on. Err(why) => { - word.store(HANDLE, Ordering::Release); - panic!("stdio: slot {} {why}", stream as u32); + word.store(DESCRIPTOR, Ordering::Release); + panic!("stdio: descriptor {} {why}", stream as u32); } } } } ASKING => core::hint::spin_loop(), - HANDLE => return Sink::Handle(RawHandle(stream as u32)), + DESCRIPTOR => return Sink::Descriptor(stream as i32), // SAFETY: only `ask` stores an address here, and it is the // base of a ring mapping this process keeps for its life. at => return Sink::Ring(unsafe { Ring::at(NonNull::new_unchecked(at as *mut u8)) }), @@ -221,20 +221,24 @@ mod target { } } - /// Ask a stream's slot what it holds, once. A ring is mapped through a - /// handle of this process's own, so the mapping outlives whatever later - /// happens to the slot. `Err` finishes the sentence the refusal panics with. + /// Ask a stream's descriptor what it names, once. A ring is mapped + /// through a handle of this process's own, so the mapping outlives + /// whatever later happens to the descriptor; a descriptor that is not open + /// is written as one, and its bytes go nowhere. `Err` finishes the + /// sentence the refusal panics with. fn ask(stream: Stream) -> Result { use why::Why; - let slot = RawHandle(stream as u32); - let stat = syscall::fstat(slot).map_err(Why::Fstat)?; + let Ok(stat) = crate::fd::with(stream as i32, |o| syscall::fstat(o.handle)) else { return Ok(DESCRIPTOR) }; + let stat = stat.map_err(Why::Fstat)?; if stat.file_type != FileType::SharedMemory { - return Ok(HANDLE); + return Ok(DESCRIPTOR); } if (stat.size as usize) < RING_BYTES { return Err(Why::Small(stat.size)); } - let own = syscall::dup(slot).map_err(Why::Dup)?; + let own = crate::fd::with(stream as i32, |o| syscall::dup(o.handle)) + .unwrap_or(Err(SyscallError::NotFound)) + .map_err(Why::Dup)?; // SAFETY: `own` is a live shared-memory handle this process holds and // never closes, so the mapping stays for the process's life. let at = unsafe { syscall::shm_map(own) }.map_err(Why::Map)?; @@ -247,7 +251,7 @@ mod target { Ok(at.as_ptr() as usize) } - /// Why a slot could not be taken as a stream, in words that need no + /// Why a descriptor could not be taken as a stream, in words that need no /// allocation to say. mod why { use core::fmt; @@ -288,7 +292,7 @@ mod target { let _ = sink(Stream::Err); } - /// Forget what a stream's slot held: the slot was just replaced. + /// Forget what a stream's descriptor named: it was just replaced. pub fn forget(stream: Stream) { SINKS[index(stream)].store(UNASKED, Ordering::Release); } @@ -308,10 +312,11 @@ mod target { } /// `buf` onto a stream: whole lines to its ring, or every byte to its - /// handle. + /// 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 { match sink(stream) { - Sink::Handle(handle) => syscall::write(handle, buf), + Sink::Descriptor(fd) => crate::fd::with(fd, |o| syscall::write(o.handle, buf)).unwrap_or(Ok(buf.len())), Sink::Ring(ring) => { PENDING[index(stream)].with(|composer| composer.feed(buf, &mut push(&ring))); Ok(buf.len()) @@ -320,9 +325,8 @@ mod target { } /// Whatever part of a line a stream holds, as a record the line goes on - /// from. Never asks the slot: a stream nothing was written to holds - /// nothing, and a process spawned with an empty slot is ended for naming - /// it. + /// from. Never asks the descriptor: a stream nothing was written to holds + /// nothing. pub fn flush(stream: Stream) { finish(stream, false); } @@ -335,8 +339,8 @@ mod target { fn finish(stream: Stream, ended: bool) { let at = SINKS[index(stream)].load(Ordering::Acquire); - if at > HANDLE { - // SAFETY: as in `sink`, only a ring's base is stored above `HANDLE`. + if at > DESCRIPTOR { + // SAFETY: as in `sink`, only a ring's base is stored above `DESCRIPTOR`. let ring = unsafe { Ring::at(NonNull::new_unchecked(at as *mut u8)) }; PENDING[index(stream)].with(|composer| composer.finish(ended, &mut push(&ring))); } @@ -349,7 +353,7 @@ mod target { pub fn claim_lane() -> Option { match sink(Stream::Err) { Sink::Ring(ring) => ring.claim_lane(PID.load(Ordering::Relaxed), toyos_abi::current_tid().0), - Sink::Handle(_) => None, + Sink::Descriptor(_) => None, } } @@ -366,16 +370,17 @@ mod target { pub fn say(severity: Severity, args: core::fmt::Arguments<'_>) { match sink(Stream::Err) { Sink::Ring(ring) => compose(severity, args, &mut push(&ring)), - Sink::Handle(handle) => compose(severity, args, &mut |body: &mut Body| { + Sink::Descriptor(fd) => compose(severity, args, &mut |body: &mut Body| { + let write = |bytes: &[u8]| crate::fd::with(fd, |o| syscall::write(o.handle, bytes)); let mut rest = body.text(); while !rest.is_empty() { - match syscall::write(handle, rest) { - Ok(n @ 1..) => rest = &rest[n..], + match write(rest) { + Ok(Ok(n @ 1..)) => rest = &rest[n..], _ => return, } } if !body.unended() { - let _ = syscall::write(handle, b"\n"); + let _ = write(b"\n"); } }), } @@ -497,7 +502,7 @@ mod tests { } } -/// A host's build is a guest program's unit tests: there are no slots and no +/// A host's build is a guest program's unit tests: there are no descriptors and no /// ring, and a line said there is composed and goes nowhere. #[cfg(not(all(target_os = "toyos", not(test))))] pub use host::*; diff --git a/sdk/toyos/src/net/stream.rs b/sdk/toyos/src/net/stream.rs index be1129cf54..f5bd9b4311 100644 --- a/sdk/toyos/src/net/stream.rs +++ b/sdk/toyos/src/net/stream.rs @@ -13,9 +13,10 @@ //! connection and a writer on the stream's bell, so neither takes the other's //! wake. A doorbell says "look", never "there are bytes". //! -//! **A stream belongs to the process that connected or accepted it.** Every -//! handle of its connection in this process shares one end; a handle with no -//! end here is refused by name and nothing is written on it +//! **A stream belongs to the process that connected or accepted it.** Its +//! connection's one handle reaches its end, and descriptors on it share that +//! one handle (`crate::fd`); a handle with no end here is refused by name and +//! nothing is written on it //! (`issues/a-stream-cannot-move-between-processes.md`). An end goes once no //! handle and no call is left. The table is the instance std links: a program //! that also links `toyos` itself and drives these functions holds ends std @@ -199,15 +200,6 @@ impl Table { }) } - /// `copy`, a new handle of `handle`'s connection, reaches its end too; - /// `false` with no room for it. - fn add(&mut self, slot: usize, copy: RawHandle) -> bool { - let Some(room) = self.handles.iter().position(Option::is_none) else { return false }; - self.handles[room] = Some((copy, slot)); - self.end(slot).handles += 1; - true - } - /// `handle` reaches its end no more: `None` for a handle that reached /// none, else its end's slot and the end where it was its last use. fn forget(&mut self, handle: RawHandle) -> Option> { @@ -577,32 +569,12 @@ pub fn is_stream(handle: RawHandle) -> bool { TABLE.lock().find(handle).is_some() } -/// The stream handle this process holds in table slot `slot`, if one is -/// there. -pub fn stream_at(slot: u16) -> Option { - TABLE.lock().handles.iter().flatten().map(|&(held, _)| held).find(|held| held.slot() == slot) -} - /// A raw handle taken back as a stream: one this process holds an end for /// shares it, and any other is refused by name, with nothing written on it. pub fn adopt(handle: RawHandle) -> Result<(), Misuse> { if is_stream(handle) { Ok(()) } else { Err(Misuse::NotThisProcess(handle)) } } -/// A new handle of `handle`'s object: of a stream's connection, one that -/// reaches the same end; of anything else, the bare `dup`. -pub fn duplicate(handle: RawHandle) -> Result { - let copy = syscall::dup(handle)?; - let mut table = TABLE.lock(); - let Some(slot) = table.find(handle) else { return Ok(copy) }; - if table.add(slot, copy) { - return Ok(copy); - } - drop(table); - syscall::close(copy); - Err(SyscallError::ResourceExhausted) -} - /// Closes `handle`: of a stream's connection, its end goes with the last /// handle and the last call; of anything else, the bare `close`. pub fn let_go(handle: RawHandle) { @@ -747,19 +719,16 @@ mod tests { End { region: h(1), base: 0x20_0000, bell: h(2), peer: ([192, 0, 2, 2], 80), local_port: 49_152, read_shut: false, handles: 0, calls: 0 } } - /// A handle given out and taken back reaches the end it was given out of, - /// and the end goes only with its last handle and its last call. + /// The end goes only with its handle and its last call. #[test] - fn an_end_goes_with_its_last_handle_and_its_last_call() { + fn an_end_goes_with_its_handle_and_its_last_call() { let mut table = Table::new(); let slot = table.reserve().unwrap(); table.enter(slot, end(), h(10)); - assert!(table.add(slot, h(11))); - assert_eq!(table.find(h(11)), Some(slot), "a duplicate reaches the end"); - let (called, _) = table.call(h(11)).unwrap(); - assert_eq!(table.forget(h(10)), Some(None), "a handle is left"); - assert_eq!(table.forget(h(11)), Some(None), "a call is left"); - assert_eq!(table.find(h(11)), None, "the handle reaches nothing"); + assert_eq!(table.find(h(10)), Some(slot), "the handle reaches the end"); + let (called, _) = table.call(h(10)).unwrap(); + assert_eq!(table.forget(h(10)), Some(None), "a call is left"); + assert_eq!(table.find(h(10)), None, "the handle reaches nothing"); assert_eq!(table.returned(called), Some(End { handles: 0, calls: 0, ..end() }), "the last call frees it"); assert_eq!(table.reserve(), Some(slot + 1), "a freed slot is not taken before its halves are cleared"); table.unreserve(slot); diff --git a/sdk/toyos/src/relocate/mod.rs b/sdk/toyos/src/relocate/mod.rs index 608cef1b3d..0626148249 100644 --- a/sdk/toyos/src/relocate/mod.rs +++ b/sdk/toyos/src/relocate/mod.rs @@ -92,11 +92,39 @@ pub unsafe extern "C" fn relocate_self(header: *const u8) { // SAFETY: the caller's contract. if unsafe { apply_to_self(header) }.is_err() { // A literal in place: there is no table of names, whose entries would be relocated words. - let _ = toyos_abi::syscall::write_nonblock(RawHandle(2), REFUSED_LINE); + if let Some(stderr) = stderr() { + let _ = toyos_abi::syscall::write_nonblock(stderr, REFUSED_LINE); + } toyos_abi::syscall::exit(REFUSED_STATUS); } } +/// The handle this process was endowed as its stderr, `fd:2`, read off the +/// kernel's endowment table with nothing but the stack and the code's own +/// addresses: no word of the image is relocated yet, so no table, `str` +/// pointer or formatting is reached. +fn stderr() -> Option { + use toyos_abi::syscall::{endowments, MAX_ENDOWMENTS, MAX_LABELS_LEN}; + const ENTRY: usize = 16; + let word = |b: &[u8], at: usize| -> Option { + let bytes = b.get(at..at.checked_add(4)?)?; + Some(u32::from_ne_bytes([*bytes.first()?, *bytes.get(1)?, *bytes.get(2)?, *bytes.get(3)?]) as usize) + }; + let mut buf = [0u8; 8 + MAX_ENDOWMENTS * ENTRY + MAX_LABELS_LEN]; + let n = endowments(&mut buf); + let table = buf.get(..n)?; + let count = word(table, 0)?.min(MAX_ENDOWMENTS); + let blob = count.checked_mul(ENTRY)?.checked_add(8)?; + for i in 0..count { + let at = i.checked_mul(ENTRY)?.checked_add(8)?; + let (off, len) = (blob.checked_add(word(table, at)?)?, word(table, at.checked_add(4)?)?); + if table.get(off..off.checked_add(len)?) == Some(b"fd:2".as_slice()) { + return Some(RawHandle(word(table, at.checked_add(8)?)? as u32)); + } + } + None +} + /// # Safety /// [`relocate_self`]'s. unsafe fn apply_to_self(header: *const u8) -> Result<(), Refusal> { diff --git a/sdk/toyos/src/spawn.rs b/sdk/toyos/src/spawn.rs index 1451adc557..6e76c45db1 100644 --- a/sdk/toyos/src/spawn.rs +++ b/sdk/toyos/src/spawn.rs @@ -8,16 +8,18 @@ //! **The supervisor starts a child only by a launch**, and a launch runs its //! row's own program with no authority but its row's and the connectors //! provided beside it. So a spawn asked of the supervisor that carries an -//! endowment or an extra slot, or names its image, is refused, as is one that +//! endowment or a descriptor past stdio, or names its image, is refused, as is one that //! names its image and provides a connector; one that decided what its child //! holds or runs is spawned directly; and one whose launch was not made is //! spawned directly only where it asked nothing a launch alone gives. -use toyos_abi::syscall::{self, EndowEntry, SpawnArgs, SyscallError, MAX_SPAWN_ENDOWMENTS, MAX_SPAWN_LABELS_LEN, SVC_LABEL}; +use toyos_abi::syscall::{ + self, EndowEntry, SpawnArgs, SyscallError, FD_PREFIX, MAX_SPAWN_ENDOWMENTS, MAX_SPAWN_LABELS_LEN, SVC_LABEL, +}; -use crate::launch::{self, Launch, LaunchError, Outcome, MAX_LAUNCH_EXTRAS, MAX_LAUNCH_SLOTS}; +use crate::launch::{self, Launch, LaunchError, Outcome, MAX_LAUNCH_EXTRAS, MAX_LAUNCH_FDS}; use crate::process::{Image, Process}; -use crate::{AsHandle, OwnedHandle, RawHandle}; +use crate::{AsHandle, RawHandle}; /// Whom a child is placed under, whose end takes it down. #[derive(Clone, Copy, PartialEq, Eq, Debug)] @@ -46,12 +48,11 @@ pub struct Spawn<'a> { pub env: &'a [u8], /// Absolute: the kernel starts a child only where its spawn says. pub cwd: &'a str, - /// `[child slot, caller's handle]`, installed in order, so a later one for - /// a slot wins. - pub slots: &'a [[u32; 2]], - /// How many of `slots`, first, are the child's stdin, stdout and stderr, - /// which a launch carries; any after them only a direct spawn installs. - pub stdio: usize, + /// `(child descriptor, caller's handle)`, each a number once: the child + /// starts with these and no other descriptors. The caller keeps its + /// handles, which a spawn duplicates. A launch carries 0 to 2; a + /// descriptor past them only a direct spawn gives. + pub fds: &'a [(u32, RawHandle)], /// `(label, handle)` the child is endowed, each moved. pub endow: &'a [(&'a str, RawHandle)], /// `(name, connector)` put in a launched child's namespace beside its @@ -94,7 +95,7 @@ impl Spawn<'_> { let asked = Asked { supervisor: self.parent == Parent::Supervisor, endowed: !self.endow.is_empty(), - extra_slots: self.slots.len() > self.stdio, + extra_fds: self.fds.iter().any(|&(fd, _)| fd > 2), image_named: self.image.is_some(), provided: !self.provided.is_empty(), }; @@ -111,35 +112,57 @@ impl Spawn<'_> { pub fn direct(&self, env: &[u8], image: Option<&Image>) -> Result { let path = image_and_name(self.program, self.image).0; let names_svc = self.endow.iter().any(|(label, _)| *label == SVC_LABEL); - if self.endow.len() + usize::from(!names_svc) > MAX_SPAWN_ENDOWMENTS { - return Err(Refused::Invalid("more endowments than a spawn carries")); + if self.endow.len() + self.fds.len() + usize::from(!names_svc) > MAX_SPAWN_ENDOWMENTS { + return Err(Refused::Invalid("more endowments and descriptors than a spawn carries")); } let mut entries = [EndowEntry { label_off: 0, label_len: 0, handle: RawHandle(0), _pad: 0 }; MAX_SPAWN_ENDOWMENTS]; let mut labels = [0u8; MAX_SPAWN_LABELS_LEN]; let (mut count, mut used) = (0, 0); - // A duplicate rather than the handle itself: an endowment is a move, - // and a parent that gave its namespace away could not spawn a second - // child. - let inherited = match names_svc { - true => None, - false => match crate::endow::namespace() { - Some(ns) => Some(syscall::dup(ns.as_handle()).map_err(Refused::Syscall)?), - None => None, - }, + // Duplicates rather than the handles themselves: an endowment is a + // move, and a parent that gave its namespace or its stdout away could + // not spawn a second child. + let mut copies = [toyos_abi::HANDLE_INVALID; MAX_SPAWN_ENDOWMENTS]; + let mut copied = 0; + let release = |copies: &[RawHandle], e| { + copies.iter().for_each(|&h| syscall::close(h)); + e + }; + let svc = match (names_svc, crate::endow::namespace()) { + (false, Some(ns)) => Some(syscall::dup(ns.as_handle()).map_err(Refused::Syscall)?), + _ => None, }; - let release = |e| { - if let Some(handle) = inherited { - syscall::close(handle); + if let Some(svc) = svc { + copies[copied] = svc; + copied += 1; + } + let mut number = [0u8; 10]; + let mut put = |label: &[&[u8]], handle: RawHandle, count: &mut usize, used: &mut usize| { + let len: usize = label.iter().map(|part| part.len()).sum(); + let room = labels.get_mut(*used..*used + len).ok_or(Refused::Invalid("endowment labels past what a spawn carries"))?; + let mut at = 0; + for part in label { + room[at..at + part.len()].copy_from_slice(part); + at += part.len(); } - e + entries[*count] = EndowEntry { label_off: *used as u32, label_len: len as u32, handle, _pad: 0 }; + (*count, *used) = (*count + 1, *used + len); + Ok(()) }; - for (label, handle) in self.endow.iter().copied().chain(inherited.map(|h| (SVC_LABEL, h))) { - let Some(room) = labels.get_mut(used..used + label.len()) else { - return Err(release(Refused::Invalid("endowment labels past what a spawn carries"))); + for &(label, handle) in self.endow { + put(&[label.as_bytes()], handle, &mut count, &mut used).map_err(|e| release(&copies[..copied], e))?; + } + if let Some(svc) = svc { + put(&[SVC_LABEL.as_bytes()], svc, &mut count, &mut used).map_err(|e| release(&copies[..copied], e))?; + } + for &(fd, handle) in self.fds { + let copy = match syscall::dup(handle) { + Ok(copy) => copy, + Err(e) => return Err(release(&copies[..copied], Refused::Syscall(e))), }; - room.copy_from_slice(label.as_bytes()); - entries[count] = EndowEntry { label_off: used as u32, label_len: label.len() as u32, handle, _pad: 0 }; - (count, used) = (count + 1, used + label.len()); + copies[copied] = copy; + copied += 1; + let digits = decimal(fd, &mut number); + put(&[FD_PREFIX.as_bytes(), digits], copy, &mut count, &mut used).map_err(|e| release(&copies[..copied], e))?; } let (image, image_len) = image.map_or((0, 0), Image::spawn_words); let args = SpawnArgs { @@ -147,8 +170,6 @@ impl Spawn<'_> { path_len: path.len() as u64, argv_ptr: self.argv.as_ptr() as u64, argv_len: self.argv.len() as u64, - slot_map_ptr: self.slots.as_ptr() as u64, - slot_map_count: self.slots.len() as u64, env_ptr: env.as_ptr() as u64, env_len: env.len() as u64, endow_ptr: entries.as_ptr() as u64, @@ -170,8 +191,8 @@ impl Spawn<'_> { // SAFETY: the kernel installed this handle for this call and no other. Ok(handle) => Ok(unsafe { Process::from_raw(handle) }), // An endowment moves only on a spawn that happened, so the - // duplicate is still this process's to close. - Err(e) => Err(release(Refused::Syscall(e))), + // duplicates are still this process's to close. + Err(e) => Err(release(&copies[..copied], Refused::Syscall(e))), } } @@ -179,9 +200,9 @@ impl Spawn<'_> { /// handle in another process: a stream belongs to the process that /// connected or accepted it. fn refuse_streams(&self) -> Result<(), Refused> { - let slots = self.slots.iter().map(|&[_, handle]| RawHandle(handle)); + let fds = self.fds.iter().map(|&(_, handle)| handle); let given = self.endow.iter().chain(self.provided).map(|&(_, handle)| handle); - match slots.chain(given).find(|h| crate::net::stream::is_stream(*h)) { + match fds.chain(given).find(|h| crate::net::stream::is_stream(*h)) { Some(stream) => Err(Refused::Stream(stream)), None => Ok(()), } @@ -209,9 +230,9 @@ impl Spawn<'_> { if self.provided.len() + usize::from(!for_supervisor) > MAX_LAUNCH_EXTRAS { return Err(Refused::Invalid("more connectors provided than a launch carries beside its place")); } - let stdio = &self.slots[..self.stdio]; - if stdio.len() > MAX_LAUNCH_SLOTS { - return Err(Refused::Invalid("more than three stdio slots")); + let stdio = self.fds; + if stdio.len() > MAX_LAUNCH_FDS || stdio.iter().any(|&(fd, _)| fd > 2) { + return Err(Refused::Invalid("a launch carries descriptors 0 to 2 alone")); } let place = match self.parent { Parent::Supervisor => None, @@ -225,23 +246,23 @@ impl Spawn<'_> { Err(e) => return unmade(Unmade::PlaceNotCopied(e), None), }, }; - let mut slots = [(0u32, RawHandle(0)); MAX_LAUNCH_SLOTS]; - let release = |slots: &[(u32, RawHandle)]| { - slots.iter().for_each(|(_, h)| syscall::close(*h)); + let mut fds = [(0u32, RawHandle(0)); MAX_LAUNCH_FDS]; + let release = |fds: &[(u32, RawHandle)]| { + fds.iter().for_each(|(_, h)| syscall::close(*h)); if let launch::Parent::Place(copy) = parent { syscall::close(copy); } }; - for (i, &[child, handle]) in stdio.iter().enumerate() { - match syscall::dup(RawHandle(handle)) { - Ok(copy) => slots[i] = (child, copy), + for (i, &(child, handle)) in stdio.iter().enumerate() { + match syscall::dup(handle) { + Ok(copy) => fds[i] = (child, copy), Err(e) => { - release(&slots[..i]); + release(&fds[..i]); return Err(Refused::Syscall(e)); } } } - let slots = &slots[..stdio.len()]; + let fds = &fds[..stdio.len()]; let mut extras = [("", RawHandle(0)); MAX_LAUNCH_EXTRAS]; extras[..self.provided.len()].copy_from_slice(self.provided); let request = Launch { @@ -250,7 +271,7 @@ impl Spawn<'_> { env: self.env, cwd: self.cwd, extras: &extras[..self.provided.len()], - slots, + fds, parent, }; // **The send moved them.** Every arm but `NotSent` is past the point @@ -264,25 +285,23 @@ impl Spawn<'_> { Ok(Outcome::Refused) | Err(LaunchError::Sent(_)) => Err(Refused::NotStarted), Ok(Outcome::Gone) => Err(Refused::Syscall(SyscallError::Gone)), Err(LaunchError::NotSent(_)) => { - release(slots); + release(fds); unmade(Unmade::NotSent, None) } } } } -/// A copy of what this process's `slot` holds, for a child to inherit there, -/// or `None` for a slot it was started without: a stdio slot a spawn left -/// empty is empty in the child too, as POSIX has a closed descriptor. A -/// stream's slot is refused as [`Spawn::launch`] refuses its handle. -pub fn inherited(slot: u16) -> Result, Refused> { - if let Some(stream) = crate::net::stream::stream_at(slot) { - return Err(Refused::Stream(stream)); - } - match syscall::dup_slot(slot) { - Ok(copy) => Ok(Some(OwnedHandle(copy))), - Err(SyscallError::NotFound) => Ok(None), - Err(e) => Err(Refused::Syscall(e)), +/// `n` in decimal, in `buf`. +fn decimal(mut n: u32, buf: &mut [u8; 10]) -> &[u8] { + let mut at = buf.len(); + loop { + at -= 1; + buf[at] = b'0' + (n % 10) as u8; + n /= 10; + if n == 0 { + return &buf[at..]; + } } } @@ -309,7 +328,7 @@ pub fn direct_env(env: &[u8], named: bool, from_supervisor: Option<&str>, out: & struct Asked { supervisor: bool, endowed: bool, - extra_slots: bool, + extra_fds: bool, image_named: bool, provided: bool, } @@ -324,13 +343,13 @@ enum Route { } fn route(asked: Asked) -> Route { - if asked.supervisor && (asked.endowed || asked.extra_slots) { - return Route::Refused("the supervisor starts only a launch, which carries no endowment and no extra slot"); + if asked.supervisor && (asked.endowed || asked.extra_fds) { + return Route::Refused("the supervisor starts only a launch, which carries no endowment and no descriptor past stdio"); } if asked.image_named && (asked.supervisor || asked.provided) { return Route::Refused("a launch runs its row's own program, so it takes no image the caller names"); } - match asked.endowed || asked.extra_slots || asked.image_named { + match asked.endowed || asked.extra_fds || asked.image_named { true => Route::Direct, false => Route::Launch, } @@ -385,7 +404,7 @@ pub fn image_and_name<'a>(program: &'a str, image: Option<&'a str>) -> (&'a str, mod tests { use super::*; - const SUPERVISOR: Asked = Asked { supervisor: true, endowed: false, extra_slots: false, image_named: false, provided: false }; + const SUPERVISOR: Asked = Asked { supervisor: true, endowed: false, extra_fds: false, image_named: false, provided: false }; /// What the supervisor would not start is refused here, and nothing is /// spawned in its place. @@ -393,7 +412,7 @@ mod tests { fn the_supervisor_is_asked_only_by_a_launch_of_its_row_s_own_program() { for asked in [ Asked { endowed: true, ..SUPERVISOR }, - Asked { extra_slots: true, ..SUPERVISOR }, + Asked { extra_fds: true, ..SUPERVISOR }, Asked { image_named: true, ..SUPERVISOR }, Asked { image_named: true, provided: true, ..Asked::default() }, ] { @@ -412,7 +431,7 @@ mod tests { fn a_spawn_that_decided_its_child_is_spawned_directly() { for asked in [ Asked { endowed: true, ..Asked::default() }, - Asked { extra_slots: true, ..Asked::default() }, + Asked { extra_fds: true, ..Asked::default() }, Asked { image_named: true, ..Asked::default() }, ] { assert_eq!(route(asked), Route::Direct); diff --git a/sdk/toyos/src/surface.rs b/sdk/toyos/src/surface.rs index 758730ab7a..ba8b76a013 100644 --- a/sdk/toyos/src/surface.rs +++ b/sdk/toyos/src/surface.rs @@ -86,12 +86,6 @@ pub const MAX_CLIENTS: usize = 4; /// names one connection for as long as its holder does not itself point the /// slot somewhere else — and it designates nothing anywhere else, because a /// handle is a slot in one table. -/// -/// That qualification is `SYS_HANDLE_DUP_AT`, which replaces a live slot -/// *without* advancing its generation because the bare number is what a POSIX -/// `dup2` caller goes on using. A surface owner that `dup2`'d over one of its -/// own client connections would hand two clients one name; none does, and the -/// number is a log line rather than an authority. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct ClientId(RawHandle); diff --git a/system/console/src/main.rs b/system/console/src/main.rs index 6f25d906a6..0a897822d3 100644 --- a/system/console/src/main.rs +++ b/system/console/src/main.rs @@ -24,7 +24,7 @@ //! whose mapping is the scanout, so it is the one that pays for a read. use std::io::{Read, Write}; -use std::os::fd::AsRawFd; +use std::os::toyos::io::AsRawHandle; use std::os::toyos::process; use std::os::toyos::process::CommandExt; use std::process::{Child, ChildStderr, ChildStdin, ChildStdout, Command}; @@ -234,8 +234,8 @@ fn main() { // as it wrote it, for a line of the log to be drawn above. let mut partial: Vec = Vec::new(); loop { - poller.watch_raw(toyos::RawHandle(shell.stdout.as_raw_fd() as u32), READABLE, TOKEN_STDOUT); - poller.watch_raw(toyos::RawHandle(shell.stderr.as_raw_fd() as u32), READABLE, TOKEN_STDERR); + poller.watch_raw(toyos::RawHandle(shell.stdout.as_raw_handle()), READABLE, TOKEN_STDOUT); + poller.watch_raw(toyos::RawHandle(shell.stderr.as_raw_handle()), READABLE, TOKEN_STDERR); poller.watch_raw(host.acceptor_handle(), READABLE, TOKEN_LISTEN); for client in host.client_handles() { poller.watch_raw(client, READABLE, TOKEN_CLIENT); diff --git a/system/supervisor/src/main.rs b/system/supervisor/src/main.rs index fb9bb10ea9..f48ecb60ad 100644 --- a/system/supervisor/src/main.rs +++ b/system/supervisor/src/main.rs @@ -281,7 +281,8 @@ impl Log { .finish() .expect("supervisor: no namespace for the log's origins"); let conn = names.open(ORIGINS).expect("supervisor: the log's origins port refused the supervisor"); - let stdin = toyos_abi::syscall::dup_narrowed(toyos::RawHandle(0), STDIN_RIGHTS) + let stdin = toyos::fd::with(0, |o| toyos_abi::syscall::dup_narrowed(o.handle, STDIN_RIGHTS)) + .expect("supervisor: the kernel gave it no console as descriptor 0") .expect("supervisor: its console would not narrow"); let log = Self { conn, @@ -290,12 +291,17 @@ impl Log { rx: RefCell::new(ipc::FrameRx::new()), unanswered: Cell::new(0), }; - // The supervisor's own lines: a ring like every program's, in the two slots the - // kernel filled with its console. + // The supervisor's own lines: a ring like every program's, as the two + // descriptors the kernel gave its console. let ring = new_ring(); - for slot in [1, 2] { - toyos_abi::syscall::dup2(ring.as_handle(), slot).expect("supervisor: its ring would not take its own slot"); - } + let copy = toyos_abi::syscall::dup(ring.as_handle()).expect("supervisor: its ring would not duplicate"); + let held = toyos::fd::install(toyos::fd::Object::handle(copy), false, 0) + .expect("supervisor: no descriptor for its own ring"); + for (fd, stream) in [(1, toyos::log::stdio::Stream::Out), (2, toyos::log::stdio::Stream::Err)] { + toyos::fd::dup2(held, fd, false).expect("supervisor: its ring would not take its own descriptor"); + toyos::log::stdio::forget(stream); + } + toyos::fd::close(held).expect("supervisor: the ring's descriptor it just made"); let (alive, keep) = toyos::pipe_pair().expect("supervisor: no pipe to say it lives"); // Held for the machine's life: the supervisor's end is the machine's. core::mem::forget(keep); @@ -1954,9 +1960,9 @@ impl Supervisor<'_> { // Past every refusal that does not know which handle is which, so ownership // can be split. Every part still releases on every path below. let all = held.take(); - let (slot_handles, rest) = all.split_at(request.slot_count()); + let (fd_handles, rest) = all.split_at(request.fd_count()); let (extra_handles, place_handle) = rest.split_at(request.extra_count); - let slots = Moved(slot_handles.to_vec()); + let fds = Moved(fd_handles.to_vec()); let place = Moved(place_handle.to_vec()); // Owned, so they close when the launch is done with: `SYS_NAMESPACE_BUILD` // copies a connector into the namespace and leaves the caller's handle, @@ -2098,15 +2104,15 @@ impl Supervisor<'_> { } }; let session = starts.session(&mut self.sessions); - let caller_slots: Vec<(u32, toyos::RawHandle)> = - request.slot_numbers().zip(slots.0.iter().copied()).collect(); + let caller_fds: Vec<(u32, toyos::RawHandle)> = + request.fd_numbers().zip(fds.0.iter().copied()).collect(); let launch = Launch { conn, program, command, session, - slots, - caller_slots, + fds, + caller_fds, _place: place, placed: under.is_some(), extras, @@ -2211,7 +2217,7 @@ impl Supervisor<'_> { /// Start `launch`, holding `folder` and in it ([`grants::cwd`]) where it /// holds one, and answer its caller. fn start_launch(&mut self, launch: Launch, folder: Option) { - let Launch { conn, mut program, mut command, session, slots, caller_slots, _place, placed, extras, cwd } = launch; + let Launch { conn, mut program, mut command, session, fds, caller_fds, _place, placed, extras, cwd } = launch; if let Some(folder) = folder { // The image `prepare` read is kept: only the working directory is // judged again. @@ -2234,7 +2240,7 @@ impl Supervisor<'_> { let program = &program; // `inherit_handle` duplicates into the child, so the supervisor's own copies go with - // `slots` when this returns. + // `fds` when this returns. self.make_home(program); if program.package().is_some() { if let Err(why) = self.make_app_home(program) { @@ -2255,9 +2261,9 @@ impl Supervisor<'_> { &extras, storage, (&self.minted, session), - Output::Launch { log: &mut self.log, slots: &caller_slots }, + Output::Launch { log: &mut self.log, fds: &caller_fds }, ); - drop(slots); + drop(fds); match started { Ok((child, _)) => { // SAFETY: `into_raw_handle` gave up the child's one handle in this table. @@ -2292,9 +2298,9 @@ struct Launch { /// Prepared: its image read and hashed, its working directory judged. command: Command, session: Session, - /// The caller's stdio and extra slots, owned until the spawn duplicates them. - slots: Moved, - caller_slots: Vec<(u32, toyos::RawHandle)>, + /// The caller's stdio, owned until the spawn duplicates it. + fds: Moved, + caller_fds: Vec<(u32, toyos::RawHandle)>, /// The place the child is spawned under, owned until then. _place: Moved, /// Whether it names a place, or the supervisor. @@ -2775,9 +2781,9 @@ fn start<'a>( // A service the supervisor starts gets a ring of its own as its stdout and stderr // and the supervisor's console to read as its stdin; `logkeeper` also gets the acceptor // the rings reach it on and the one console handle that may write. A - // launch keeps the slots its caller sent, but for a ring among them — - // the caller's own log — which is replaced by the program's, and a stdio - // slot it sent nothing for is a null, never this process's own. + // launch keeps the descriptors its caller sent, but for a ring among them + // — the caller's own log — which is replaced by the program's, and a stdio + // descriptor it sent nothing for is a null, never this process's own. let mut ring: Option = None; let mut origins: Option = None; let log: &mut Log = match output { @@ -2792,24 +2798,25 @@ fn start<'a>( }; command.endow(ORIGINS, acceptor.as_handle().0); origins = Some(acceptor); - let console = toyos_abi::syscall::dup(toyos::RawHandle(0)) + let console = toyos::fd::with(0, |o| toyos_abi::syscall::dup(o.handle)) + .expect("supervisor: its console is descriptor 0") .expect("supervisor: its console would not duplicate for logkeeper"); command.endow(CONSOLE, console.0); held.0.push(console); } log } - Output::Launch { log, slots } => { + Output::Launch { log, fds } => { command.stdin(Stdio::null()).stdout(Stdio::null()).stderr(Stdio::null()); - for &(slot, handle) in slots { - let caller_ring = matches!(slot, 1 | 2) + for &(fd, handle) in fds { + let caller_ring = matches!(fd, 1 | 2) && toyos_abi::syscall::fstat(handle) .is_ok_and(|stat| stat.file_type == FileType::SharedMemory); if caller_ring { let own = ring.get_or_insert_with(new_ring).as_handle(); - command.inherit_handle(slot, own.0); + command.inherit_handle(fd, own.0); } else { - command.inherit_handle(slot, handle.0); + command.inherit_handle(fd, handle.0); } } log @@ -2864,10 +2871,10 @@ fn start<'a>( } /// Where a started program's output goes: a service's own ring, or a -/// launch's caller's slots with any ring among them replaced. +/// launch's caller's descriptors with any ring among them replaced. enum Output<'l> { Boot(&'l mut Log), - Launch { log: &'l mut Log, slots: &'l [(u32, toyos::RawHandle)] }, + Launch { log: &'l mut Log, fds: &'l [(u32, toyos::RawHandle)] }, } /// The namespace this program's `receives` names, [`toyos_swap::PORT`] and diff --git a/system/terminal/src/main.rs b/system/terminal/src/main.rs index d9ba9163d6..a7e11d50a6 100644 --- a/system/terminal/src/main.rs +++ b/system/terminal/src/main.rs @@ -7,7 +7,7 @@ //! terminal writing only translated bytes into a pipe could never give it. use std::io::{Read, Write}; -use std::os::fd::AsRawFd; +use std::os::toyos::io::AsRawHandle; use std::os::toyos::process; use std::os::toyos::process::CommandExt; use std::process::Command; @@ -88,8 +88,8 @@ fn main() { println!("terminal: ready"); loop { - poller.watch_raw(RawHandle(shell_stdout.as_raw_fd() as u32), READABLE, TOKEN_STDOUT); - poller.watch_raw(RawHandle(shell_stderr.as_raw_fd() as u32), READABLE, TOKEN_STDERR); + poller.watch_raw(RawHandle(shell_stdout.as_raw_handle()), READABLE, TOKEN_STDOUT); + poller.watch_raw(RawHandle(shell_stderr.as_raw_handle()), READABLE, TOKEN_STDERR); poller.watch_raw(window.handle(), READABLE, TOKEN_WINDOW); poller.watch_raw(host.acceptor_handle(), READABLE, TOKEN_LISTEN); for client in host.client_handles() { diff --git a/tests/guest/libc/spawn_cloexec.c b/tests/guest/libc/spawn_cloexec.c index 800f01727a..f0fce06014 100644 --- a/tests/guest/libc/spawn_cloexec.c +++ b/tests/guest/libc/spawn_cloexec.c @@ -1,34 +1,35 @@ /* A descriptor the parent marked FD_CLOEXEC, a 0 that addopen opened O_CLOEXEC, and a 1 that adddup2 made and addclose closed are each absent in - the child. A child cannot ask about a slot it does not hold and live - (tests/CLAUDE.md), so each child asks about one: a child that holds it - exits 0, and one that does not ends with SIGSEGV's handle fault. */ + the child. Each child asks about one: a child that holds it exits 0, and + one that does not is answered EBADF and exits 2. */ +#include #include -#include #include #include "check.h" #define SELF "/system/bin/test_c_spawn_cloexec" -/* The child's verdict on descriptor `fd`. */ +/* The child's verdict on descriptor `fd`: 0 held, 2 not, and 3 for any + other answer. */ static int holds(int fd) { struct stat st; - return fstat(fd, &st) == 0 ? 0 : 2; + if (fstat(fd, &st) == 0) + return 0; + return errno == EBADF ? 2 : 3; } /* How a child asking about `fd`, its descriptors as `actions` leave them, ended: 1 for held, 0 for absent. */ static int child_holds(const char *fd, const posix_spawn_file_actions_t *actions) { int status = waited(spawn_role(SELF, "holds", fd, actions, NULL)); - if (WIFEXITED(status) && WEXITSTATUS(status) == 0) - return 1; - CHECK(WIFSIGNALED(status) && WTERMSIG(status) == SIGSEGV, "the child asking about %s ended %#x", fd, status); - return 0; + CHECK(WIFEXITED(status) && (WEXITSTATUS(status) == 0 || WEXITSTATUS(status) == 2), + "the child asking about %s ended %#x", fd, status); + return WEXITSTATUS(status) == 0; } -/* A process started without a stdout starts children of its own: the slot - it lacks is no descriptor its children are given, and nothing ends it. */ +/* A process started without a stdout starts children of its own: the + descriptor it lacks is none its children are given, and nothing ends it. */ static int spawns_without_stdout(void) { char *argv[] = {SELF, "holds", "0", NULL}; pid_t pid; diff --git a/tests/guest/libc/spawn_signaled.c b/tests/guest/libc/spawn_signaled.c index 21c0e2e473..5f20c09786 100644 --- a/tests/guest/libc/spawn_signaled.c +++ b/tests/guest/libc/spawn_signaled.c @@ -26,10 +26,6 @@ static void fault(const char *kind) { #else __asm__ volatile("brk #0"); #endif - } else if (strcmp(kind, "handle") == 0) { - /* Slot 4000 at its first generation, which nothing gave this process. */ - char byte; - read(4000, &byte, 1); } } @@ -44,7 +40,6 @@ static const struct { {"arithmetic", SIGFPE}, #endif {"trap", SIGTRAP}, - {"handle", SIGSEGV}, }; int main(int argc, char **argv) { diff --git a/tests/guest/rust-tests/src/bin/abuse_elf_loader.rs b/tests/guest/rust-tests/src/bin/abuse_elf_loader.rs index fde0477195..1ef1b5deb5 100644 --- a/tests/guest/rust-tests/src/bin/abuse_elf_loader.rs +++ b/tests/guest/rust-tests/src/bin/abuse_elf_loader.rs @@ -19,7 +19,7 @@ use std::process::Command; use toyos_abi::syscall::{self, SpawnArgs, SyscallError}; use toyos_abi::RawHandle; -use toyos_abi::end::{End, Fault}; +use toyos_abi::end::End; /// Where the child starts: `SpawnArgs` names a working directory or the spawn is refused. const CWD: &str = "/"; @@ -238,7 +238,7 @@ fn spawn_path(path: &str) -> Result { spawn_argv(path, &format!("{path}\0")) } -/// `path` spawned with `argv`, NUL-separated, and no slot of this process's. +/// `path` spawned with `argv`, NUL-separated, and no descriptor of this process's. fn spawn_argv(path: &str, argv: &str) -> Result { unsafe { syscall::spawn(&SpawnArgs { @@ -246,8 +246,6 @@ fn spawn_argv(path: &str, argv: &str) -> Result { path_len: path.len() as u64, argv_ptr: argv.as_ptr() as u64, argv_len: argv.len() as u64, - slot_map_ptr: 0, - slot_map_count: 0, env_ptr: 0, env_len: 0, endow_ptr: 0, @@ -300,8 +298,6 @@ fn spawn_image(path: &str, bytes: &[u8]) -> Result { path_len: path.len() as u64, argv_ptr: argv.as_ptr() as u64, argv_len: argv.len() as u64, - slot_map_ptr: 0, - slot_map_count: 0, env_ptr: 0, env_len: 0, endow_ptr: 0, @@ -1042,12 +1038,10 @@ fn tls_refs_so(name: &[u8; 4], addend: i64) -> Vec { .build() } -/// What `toyos::relocate` writes to slot 2 when it refuses its own program. +/// What `toyos::relocate` writes to its stderr, `fd:2`, when it refuses its own program. const REFUSED_LINE: &str = "this program's relocations are refused: it cannot start\n"; /// The status it exits with then. const REFUSED_STATUS: i32 = 127; -/// How the kernel ends a process that names a handle it does not hold. -const HANDLE_FAULT: End = End::Fault(Fault::Handle); /// The argument a refused copy is handed, which its `main` panics on. const REFUSED_COPY: &str = "--refused-copy"; @@ -1074,8 +1068,8 @@ fn own_image_tagged(tag: u64) -> Vec { } /// A copy of this program tagged `tag` exits [`REFUSED_STATUS`] and says -/// [`REFUSED_LINE`] on the slot 2 it is handed. Handed no slot 2, it names a -/// handle it does not hold, and the kernel ends it. +/// [`REFUSED_LINE`] on the stderr it is endowed. Endowed none, it says +/// nothing and exits the same. fn refuses_itself(name: &str, tag: u64) { let path = write_file(name, &own_image_tagged(tag)); let out = Command::new(&path) @@ -1087,9 +1081,9 @@ fn refuses_itself(name: &str, tag: u64) { assert_eq!(said, REFUSED_LINE, "{name}"); let child = spawn_argv(&path, &format!("{path}\0{REFUSED_COPY}\0")) - .unwrap_or_else(|e| panic!("{name} without slot 2: spawn gave {e:?}")); + .unwrap_or_else(|e| panic!("{name} without a stderr: spawn gave {e:?}")); // SAFETY: the handle `SYS_SPAWN` just answered, held nowhere else. let child = unsafe { toyos::process::Process::from_raw(child) }; - let end = child.wait().unwrap_or_else(|e| panic!("{name} without slot 2: wait: {e:?}")); - assert_eq!(end, HANDLE_FAULT, "{name} without slot 2"); + let end = child.wait().unwrap_or_else(|e| panic!("{name} without a stderr: wait: {e:?}")); + assert_eq!(end, End::Exited(REFUSED_STATUS), "{name} without a stderr"); } diff --git a/tests/guest/rust-tests/src/bin/abuse_elf_segments.rs b/tests/guest/rust-tests/src/bin/abuse_elf_segments.rs index 7eb8f006d7..ce5088bd53 100644 --- a/tests/guest/rust-tests/src/bin/abuse_elf_segments.rs +++ b/tests/guest/rust-tests/src/bin/abuse_elf_segments.rs @@ -148,8 +148,6 @@ fn spawn_as(path: &str, image: &[u8]) -> SyscallError { path_len: path.len() as u64, argv_ptr: argv.as_ptr() as u64, argv_len: argv.len() as u64, - slot_map_ptr: 0, - slot_map_count: 0, env_ptr: 0, env_len: 0, endow_ptr: 0, diff --git a/tests/guest/rust-tests/src/bin/abuse_handle_table.rs b/tests/guest/rust-tests/src/bin/abuse_handle_table.rs index f74bea6afd..1add129be0 100644 --- a/tests/guest/rust-tests/src/bin/abuse_handle_table.rs +++ b/tests/guest/rust-tests/src/bin/abuse_handle_table.rs @@ -1,21 +1,20 @@ //! The handle-table cap must hold on every insertion path. //! -//! There are three of them — plain open, `dup2` and `SYS_SPAWN`'s slot map — and -//! a table that grows past the cap on any one reaches a hashbrown doubling -//! above the kernel's 2 MiB single-allocation ceiling. +//! There are two of them — a plain install, as an open or a `dup` makes, and +//! `SYS_SPAWN`'s endowments — and a table that grows past the cap on either +//! reaches a hashbrown doubling above the kernel's 2 MiB single-allocation +//! ceiling. //! //! The cap **is** the slot range now: a `RawHandle` carries twelve bits of //! slot, so "the table is full" and "that slot does not exist" are one event //! and one error word rather than two that could disagree. //! -//! **`SYS_SPAWN`'s two vectors are bounded by their own counts as well, and -//! the cap cannot stand in for either.** A slot map that names one slot over -//! and over never trips the cap — the child's table stays one entry wide — and -//! an endowment vector that names one handle twice passes a preflight run -//! against a table nothing has been taken out of yet. Both were reachable with -//! one argument and both ended in the kernel rather than in an error word, so -//! each has an arm here and the last arm is what says the machine survived -//! them. +//! **`SYS_SPAWN`'s endowments are bounded by their own count as well, and +//! the cap cannot stand in for it.** An endowment vector that names one handle +//! twice passes a preflight run against a table nothing has been taken out of +//! yet. It was reachable with one argument and ended in the kernel rather than +//! in an error word, so it has an arm here and the last arm is what says the +//! machine survived it. //! //! **A spawn's answer is an insert into its caller's table**, whose room is //! checked before the spawn's endowments move: at the cap the spawn is @@ -29,9 +28,7 @@ //! **A pipe and a port are two inserts answered as one**: at the cap, and one //! slot short of fitting, each is refused with the table as it was. -use toyos_abi::syscall::{ - self, EndowEntry, MmapFlags, MmapProt, SpawnArgs, SyscallError, MAX_ENDOWMENTS, MAX_SLOT_MAP, SELF_LABEL, -}; +use toyos_abi::syscall::{self, EndowEntry, MmapFlags, MmapProt, SpawnArgs, SyscallError, MAX_ENDOWMENTS, SELF_LABEL}; use toyos_abi::RawHandle; use toyos_abi::end::End; @@ -47,7 +44,6 @@ const CWD: &str = "/"; const MAX_HANDLES: u32 = 4096; const REGION: usize = 4 * 1024 * 1024; -const PAIRS: usize = 100_000; /// The label blob the endowment arms index into. Its content is never read — /// the vector is refused before anything is installed — but the offsets have to @@ -68,23 +64,16 @@ fn main() { }; assert!(!region.is_null(), "mmap failed"); - // The child's table is built by a different insert path than dup2's. - let pairs = region as *mut [u32; 2]; - for i in 0..PAIRS { - unsafe { pairs.add(i).write([i as u32, 1]) }; - } let argv = unsafe { region.add(REGION / 2) }; const ARGV0: &[u8] = b"/system/bin/echo\0"; unsafe { core::ptr::copy_nonoverlapping(ARGV0.as_ptr(), argv, ARGV0.len()) }; - let spawn_with = |slot_map_count: u64, endow_count: u64| { + let spawn_with = |endow_count: u64| { let args = SpawnArgs { path_ptr: argv as u64, path_len: (ARGV0.len() - 1) as u64, argv_ptr: argv as u64, argv_len: ARGV0.len() as u64, - slot_map_ptr: region as u64, - slot_map_count, env_ptr: 0, env_len: 0, endow_ptr: (region as u64) + (REGION / 4) as u64, @@ -100,35 +89,12 @@ fn main() { unsafe { syscall::spawn(&args) } }; - // A slot map longer than the child's table has slots. Past this the count - // is bounded only by the 2 MiB window the arguments are read through. - let err = spawn_with(PAIRS as u64, 0) - .expect_err("a slot_map past MAX_SLOT_MAP must be rejected"); - assert_eq!(err, SyscallError::InvalidArgument, "wrong error for an oversized slot_map"); - - // **The same length, every pair naming slot 0.** The cap never fires: the - // child's table stays one entry wide while the parent duplicates a handle - // per pair under its own lock and carries every displaced entry out of it. - for i in 0..PAIRS { - unsafe { pairs.add(i).write([0, 1]) }; - } - let err = spawn_with(PAIRS as u64, 0) - .expect_err("a slot_map repeating one slot must be rejected"); - assert_eq!(err, SyscallError::InvalidArgument, "wrong error for a repeated slot"); - - // And the cap itself, which is a different refusal and is what says the two - // above are the *count* rather than a blanket no. One pair, naming a slot - // no table has. - unsafe { pairs.write([MAX_SLOT_MAP as u32 + 1, 1]) }; - let err = spawn_with(1, 0).expect_err("a slot past the table must be rejected"); - assert_eq!(err, SyscallError::ResourceExhausted, "wrong error at the child's table cap"); - // **An endowment vector naming one handle twice.** Every check in the // preflight runs against a table nothing has been removed from, so a repeat // passed both times; the first removal retired the slot and the second // answered `Stale` into an `expect`. let endow = unsafe { region.add(REGION / 4) } as *mut EndowEntry; - let entry = EndowEntry { label_off: 0, label_len: LABELS.len() as u32, handle: RawHandle(1), _pad: 0 }; + let entry = EndowEntry { label_off: 0, label_len: LABELS.len() as u32, handle: stdout(), _pad: 0 }; unsafe { endow.write(entry); endow.add(1).write(entry); @@ -138,18 +104,16 @@ fn main() { LABELS.len(), ); } - let err = spawn_with(0, 2).expect_err("an endowment vector naming one handle twice must be rejected"); + let err = spawn_with(2).expect_err("an endowment vector naming one handle twice must be rejected"); assert_eq!(err, SyscallError::InvalidArgument, "wrong error for a repeated endowment"); a_caller_endows_no_self(); a_spawn_carries_one_entry_fewer_than_a_table(); - // dup2 picks the slot, so it never went through the allocating path that - // carried the cap. let mut filled = Vec::new(); let mut refused = None; - for n in 3..40_000u16 { - match syscall::dup2(RawHandle(1), n) { + for n in 0..40_000u32 { + match syscall::dup(stdout()) { Ok(handle) => filled.push(handle), Err(e) => { refused = Some((n, e)); @@ -157,12 +121,9 @@ fn main() { } } } - let (n, e) = refused.expect("dup2 must eventually refuse to grow the handle table"); + let (n, e) = refused.expect("dup must eventually refuse to grow the handle table"); assert_eq!(e, SyscallError::ResourceExhausted, "wrong error at the handle cap"); - assert!( - u32::from(n) <= MAX_HANDLES + 16, - "handle table reached {n} slots, past the {MAX_HANDLES} cap" - ); + assert!(n <= MAX_HANDLES, "the handle table took {n} more handles, past the {MAX_HANDLES} cap"); a_pair_is_refused_whole(&mut filled); @@ -178,8 +139,7 @@ fn main() { assert_eq!(spawned, Err(SyscallError::ResourceExhausted), "a spawn from a full table was not refused"); // Still this process's: the close ends it if the refused spawn moved it. syscall::close(last); - let reused = syscall::dup2(RawHandle(1), 3) - .expect("dup2 must work again after closing handles"); + let reused = syscall::dup(stdout()).expect("dup must work again after closing handles"); syscall::close(reused); unsafe { syscall::munmap(region, REGION) }.expect("munmap"); @@ -205,9 +165,9 @@ fn a_pair_is_refused_whole(filled: &mut Vec) { refused("no slot free"); syscall::close(filled.pop().expect("the fill installed a handle")); refused("one slot free"); - filled.push(syscall::dup(RawHandle(1)).expect("the one free slot survived two refused pairs")); + filled.push(syscall::dup(stdout()).expect("the one free slot survived two refused pairs")); assert_eq!( - syscall::dup(RawHandle(1)), + syscall::dup(stdout()), Err(SyscallError::ResourceExhausted), "a refused pair left a second slot free" ); @@ -224,8 +184,6 @@ fn spawn_endowed(entries: &[EndowEntry], labels: &[u8]) -> Result RawHandle { + toyos::fd::with(1, |o| o.handle).expect("the test is given a standard output") +} diff --git a/tests/guest/rust-tests/src/bin/abuse_page_straddle.rs b/tests/guest/rust-tests/src/bin/abuse_page_straddle.rs index 434790dd17..881e49c51d 100644 --- a/tests/guest/rust-tests/src/bin/abuse_page_straddle.rs +++ b/tests/guest/rust-tests/src/bin/abuse_page_straddle.rs @@ -119,8 +119,6 @@ fn main() { path_len: "/system/bin/echo".len() as u64, argv_ptr: base, argv_len: argv.len() as u64, - slot_map_ptr: 0, - slot_map_count: 0, env_ptr: 0, env_len: 0, endow_ptr: 0, diff --git a/tests/guest/rust-tests/src/bin/abuse_pipe_owner.rs b/tests/guest/rust-tests/src/bin/abuse_pipe_owner.rs index 6374d9b55d..147cf10ee2 100644 --- a/tests/guest/rust-tests/src/bin/abuse_pipe_owner.rs +++ b/tests/guest/rust-tests/src/bin/abuse_pipe_owner.rs @@ -39,8 +39,8 @@ const SELF_PATH: &str = "/system/bin/test_rs_abuse_pipe_owner"; /// How the kernel ends a process that names a handle it does not hold. const HANDLE_FAULT: End = End::Fault(Fault::Handle); -/// Where the victim parks its ends. High, and nothing in this tree grows a -/// table that far: a process holding 900 handles would be a different bug. +/// Where the victim's ends are. High, and nothing in this tree grows a table +/// that far: a process holding 900 handles would be a different bug. const VICTIM_READ_SLOT: u16 = 900; const VICTIM_WRITE_SLOT: u16 = 901; @@ -153,11 +153,17 @@ fn attacker(role: &str) -> ! { /// Holds a live pipe at two numbers it publishes, and afterwards says what /// crossed it. fn victim() -> ! { + // A pipe made once every slot below the victim's is taken lands at the + // next two fresh ones, and stays there when the fillers go. + let stdout = toyos::fd::with(1, |o| o.handle).expect("the victim is given a standard output"); + let mut fillers = Vec::new(); + while fillers.last().is_none_or(|h: &RawHandle| h.slot() + 1 < VICTIM_READ_SLOT) { + fillers.push(syscall::dup(stdout).expect("fill the victim's table below its slots")); + } let (read, write) = toyos::pipe_pair().expect("the pipe the attack is aimed at"); - let parked_read = - syscall::dup2(read.as_handle(), VICTIM_READ_SLOT).expect("park the read end"); - let parked_write = - syscall::dup2(write.as_handle(), VICTIM_WRITE_SLOT).expect("park the write end"); + let (parked_read, parked_write) = (read.as_handle(), write.as_handle()); + assert_eq!((parked_read.slot(), parked_write.slot()), (VICTIM_READ_SLOT, VICTIM_WRITE_SLOT), "the pipe is not where it is published"); + fillers.into_iter().for_each(syscall::close); println!("{} {}", parked_read.0, parked_write.0); std::io::stdout().flush().expect("flush"); diff --git a/tests/guest/rust-tests/src/bin/abuse_spawn_argv.rs b/tests/guest/rust-tests/src/bin/abuse_spawn_argv.rs index c024e634b2..bfcef6cd8d 100644 --- a/tests/guest/rust-tests/src/bin/abuse_spawn_argv.rs +++ b/tests/guest/rust-tests/src/bin/abuse_spawn_argv.rs @@ -40,8 +40,6 @@ fn main() { path_len: 1, argv_ptr: region as u64, argv_len: 0, - slot_map_ptr: 0, - slot_map_count: 0, env_ptr: 0, env_len: 0, endow_ptr: 0, diff --git a/tests/guest/rust-tests/src/bin/bar_map_again.rs b/tests/guest/rust-tests/src/bin/bar_map_again.rs index 93e1c55d2b..2dda57fc90 100644 --- a/tests/guest/rust-tests/src/bin/bar_map_again.rs +++ b/tests/guest/rust-tests/src/bin/bar_map_again.rs @@ -85,7 +85,7 @@ fn main() { // Standard output fills the table: a claim's own handle carries no `DUP`. let mut filled = Vec::new(); let full = loop { - match syscall::dup(RawHandle(1)) { + match syscall::dup(stdout()) { Ok(handle) => filled.push(handle), Err(why) => break why, } @@ -106,3 +106,8 @@ fn main() { assert_eq!(after, first, "bar_map_again: the ask after the refusal maps another window"); println!("bar_map_again: answered after the refusal with a handle that maps"); } + +/// The kernel handle this process's standard output is, descriptor 1's. +fn stdout() -> RawHandle { + toyos::fd::with(1, |o| o.handle).expect("the test is given a standard output") +} diff --git a/tests/guest/rust-tests/src/bin/counters_read.rs b/tests/guest/rust-tests/src/bin/counters_read.rs index 618f958860..d757594abd 100644 --- a/tests/guest/rust-tests/src/bin/counters_read.rs +++ b/tests/guest/rust-tests/src/bin/counters_read.rs @@ -127,7 +127,7 @@ fn rights(full: &[Record]) { assert_eq!(read(&none).err(), Some(SyscallError::PermissionDenied), "a cap without COUNTERS read the counters"); let mut buf = vec![RawRecord::EMPTY; full.len()]; assert_eq!( - syscall::counters(RawHandle(1), &mut buf), + syscall::counters(stdout(), &mut buf), Err(SyscallError::PermissionDenied), "a console was taken as a capability", ); @@ -168,3 +168,8 @@ fn unheld() { assert_eq!(String::from_utf8_lossy(&out.stdout).trim(), "reached unheld", "the child never reached its call"); assert_eq!(End::from_word(out.status.into_raw()), Some(HANDLE_FAULT), "SYS_COUNTERS took a handle nobody holds"); } + +/// The kernel handle this process's standard output is, descriptor 1's. +fn stdout() -> RawHandle { + toyos::fd::with(1, |o| o.handle).expect("the test is given a standard output") +} diff --git a/tests/guest/rust-tests/src/bin/endowment_denied.rs b/tests/guest/rust-tests/src/bin/endowment_denied.rs index e8bd84c471..1340c67893 100644 --- a/tests/guest/rust-tests/src/bin/endowment_denied.rs +++ b/tests/guest/rust-tests/src/bin/endowment_denied.rs @@ -318,17 +318,17 @@ fn a_right_the_capability_lacks_is_a_word() { // check: `DEVICE` and `RT` are bits nothing else carries, so this is the // same refusal the narrowed capability got. assert_eq!( - syscall::device_claim(RawHandle(1), DeviceRequest::Class(DeviceType::Framebuffer)).err(), + syscall::device_claim(stdout(), DeviceRequest::Class(DeviceType::Framebuffer)).err(), Some(SyscallError::PermissionDenied), "a pipe was taken as a capability by SYS_DEVICE_CLAIM", ); assert_eq!( - syscall::rt_enter(RawHandle(1)), + syscall::rt_enter(stdout()), Err(SyscallError::PermissionDenied), "a pipe was taken as a capability by SYS_RT_ENTER", ); assert_eq!( - syscall::shutdown(RawHandle(1)), + syscall::shutdown(stdout()), SyscallError::PermissionDenied, "a pipe was taken as a capability by SYS_SHUTDOWN", ); @@ -411,7 +411,7 @@ fn the_roster_is_a_right_and_the_header_is_not() { // arms above assert for `DEVICE`, `RT` and `POWER`. let mut by_a_console = [0u8; ONE_ENTRY]; assert_eq!( - syscall::sysinfo(RawHandle(1), &mut by_a_console), + syscall::sysinfo(stdout(), &mut by_a_console), 0, "a console was taken as a capability by SYS_SYSINFO", ); @@ -592,3 +592,8 @@ fn not_a_handle(role: &str) -> ! { }; panic!("{role} was answered {answered} instead of ending the caller"); } + +/// The kernel handle this process's standard output is, descriptor 1's. +fn stdout() -> RawHandle { + toyos::fd::with(1, |o| o.handle).expect("the test is given a standard output") +} diff --git a/tests/guest/rust-tests/src/bin/futex_wake_counts.rs b/tests/guest/rust-tests/src/bin/futex_wake_counts.rs index 457b6f6c53..9e19eb5007 100644 --- a/tests/guest/rust-tests/src/bin/futex_wake_counts.rs +++ b/tests/guest/rust-tests/src/bin/futex_wake_counts.rs @@ -54,7 +54,6 @@ use std::time::Duration; use toyos::endow::{Endowments, SYSCAP_LABEL}; use toyos::syscap::SysCap; use toyos_abi::syscall::{self, MmapFlags, MmapProt}; -use toyos_abi::RawHandle; #[path = "../roster.rs"] mod roster; @@ -86,10 +85,10 @@ static COUNTS_ARMED: AtomicU32 = AtomicU32::new(0); const SELF: &str = "/system/bin/test_rs_futex_wake_counts"; /// `argv[1]` the sweeper is spawned with. const SWEEP: &str = "sweep"; -/// `argv[1]` of [`revoked_while_still_mapped`]'s child, and the slot it finds -/// the pipe in. +/// `argv[1]` of [`revoked_while_still_mapped`]'s child, and the descriptor +/// it finds the pipe at. const LET_GO: &str = "let-go"; -const INHERITED_PIPE: RawHandle = RawHandle(3); +const INHERITED_PIPE: i32 = 3; /// How many 2 MiB frames the third arm parks a waiter on and then unmaps. /// @@ -502,7 +501,7 @@ fn revoked_while_still_mapped() { let child = Command::new(SELF) .arg(LET_GO) - .inherit_handle(INHERITED_PIPE.0, ends.read.0) + .inherit_handle(INHERITED_PIPE as u32, ends.read.0) .stdout(Stdio::piped()) .spawn() .unwrap_or_else(|e| panic!("spawn the child that lets go: {e}")); @@ -527,8 +526,10 @@ fn revoked_while_still_mapped() { /// The other process of [`revoked_while_still_mapped`]: map the pipe it /// inherited, then close its only handle to it, which unmaps its window. fn let_go() { - syscall::pipe_map(INHERITED_PIPE).expect("map the inherited pipe"); - syscall::close(INHERITED_PIPE); + toyos::fd::with(INHERITED_PIPE, |o| syscall::pipe_map(o.handle)) + .expect("the pipe was inherited as its descriptor") + .expect("map the inherited pipe"); + toyos::fd::close(INHERITED_PIPE).expect("close the inherited pipe's one descriptor"); println!("mapped and let go"); } diff --git a/tests/guest/rust-tests/src/bin/launch_unsent_stdio.rs b/tests/guest/rust-tests/src/bin/launch_unsent_stdio.rs index c52fd50532..d347dd2f3a 100644 --- a/tests/guest/rust-tests/src/bin/launch_unsent_stdio.rs +++ b/tests/guest/rust-tests/src/bin/launch_unsent_stdio.rs @@ -1,33 +1,32 @@ -//! A program the supervisor launches holds a null in each stdio slot its -//! caller sent nothing for, never the supervisor's own console or log ring: +//! A program the supervisor launches holds a null in each stdio descriptor +//! its caller sent nothing for, never the supervisor's own console or log ring: //! a launched `cat` sent only a stdout reads end of file at once and writes -//! nothing, and a launched `echo` sent no slot at all says [`UNSENT`] +//! nothing, and a launched `echo` sent no descriptor at all says [`UNSENT`] //! nowhere. [`SENT`], said by one sent this job's stdout, is what the host //! waits for before it asserts that. //! -//! The launches are made through `toyos::spawn` with the slots named -//! outright, so which ones a launch carries is this test's, not what a -//! table's free slot happens to hold. +//! The launches are made through `toyos::spawn` with the descriptors named +//! outright, so which ones a launch carries is this test's. //! //! `a_launch_gives_each_unsent_stdio_slot_a_null` runs it on QEMU x86-64 //! (`tests/images/launchcase.toml`), whose test-runner holds a launcher. use toyos::AsHandle; use toyos::spawn::{Launched, Parent, Spawn}; +use toyos_abi::RawHandle; const UNSENT: &str = "nothing-sent-said-this"; const SENT: &str = "slot-sent-said-this"; -/// `program` with `argv` launched with exactly `slots`, and waited for. -fn launched(program: &str, argv: &[u8], slots: &[[u32; 2]]) { +/// `program` with `argv` launched with exactly `fds`, and waited for. +fn launched(program: &str, argv: &[u8], fds: &[(u32, RawHandle)]) { let request = Spawn { program, image: None, argv, env: b"", cwd: "/", - slots, - stdio: slots.len(), + fds, endow: &[], provided: &[], parent: Parent::Caller, @@ -39,19 +38,19 @@ fn launched(program: &str, argv: &[u8], slots: &[[u32; 2]]) { Err(refused) => panic!("the launch of {program} was refused: {refused:?}"), }; let end = child.wait().unwrap_or_else(|e| panic!("wait for {program}: {e:?}")); - assert!(end.success(), "{program} launched with slots {slots:?} ended {end:?}"); + assert!(end.success(), "{program} launched with descriptors {fds:?} ended {end:?}"); } fn main() { let (read, write) = toyos::pipe_pair().expect("a pipe"); - launched("/system/bin/cat", b"/system/bin/cat", &[[1, write.as_handle().0]]); + launched("/system/bin/cat", b"/system/bin/cat", &[(1, write.as_handle())]); drop(write); let mut buf = [0u8; 64]; let n = read.read(&mut buf).expect("read what cat wrote"); assert_eq!(n, 0, "a launched cat sent no stdin wrote {:?}", String::from_utf8_lossy(&buf[..n])); launched("/system/bin/echo", format!("/system/bin/echo\0{UNSENT}").as_bytes(), &[]); - let out = toyos::spawn::inherited(1).expect("this job's stdout").expect("this job holds a stdout"); - launched("/system/bin/echo", format!("/system/bin/echo\0{SENT}").as_bytes(), &[[1, out.as_handle().0]]); + let out = toyos::fd::copy(1).expect("this job's stdout").expect("this job holds a stdout"); + launched("/system/bin/echo", format!("/system/bin/echo\0{SENT}").as_bytes(), &[(1, out.as_handle())]); eprintln!("launch_unsent_stdio: a launched cat sent no stdin read end of file"); } diff --git a/tests/guest/rust-tests/src/bin/process_tree.rs b/tests/guest/rust-tests/src/bin/process_tree.rs index 4e47ec6a49..a4472c3dde 100644 --- a/tests/guest/rust-tests/src/bin/process_tree.rs +++ b/tests/guest/rust-tests/src/bin/process_tree.rs @@ -194,7 +194,7 @@ fn under_b_is_gone(grown: &Grown) { env: b"", cwd: "/", extras: &[], - slots: &[], + fds: &[], parent: Parent::Place(place), }; let mut home = [0u8; 256]; @@ -228,8 +228,6 @@ fn spawn_under(place: RawHandle) -> Result { path_len: SELF_PATH.len() as u64, argv_ptr: argv.as_ptr() as u64, argv_len: argv.len() as u64, - slot_map_ptr: 0, - slot_map_count: 0, env_ptr: 0, env_len: 0, endow_ptr: 0, @@ -278,7 +276,7 @@ fn a_pipe_is_no_place() { env: b"", cwd: "/", extras: &[], - slots: &[], + fds: &[], parent: Parent::Place(place), }; let mut home = [0u8; 256]; diff --git a/tests/guest/rust-tests/src/bin/spawn_child_ends_first.rs b/tests/guest/rust-tests/src/bin/spawn_child_ends_first.rs index 2bbef09662..da9aaf1fdf 100644 --- a/tests/guest/rust-tests/src/bin/spawn_child_ends_first.rs +++ b/tests/guest/rust-tests/src/bin/spawn_child_ends_first.rs @@ -14,7 +14,7 @@ //! //! Every wait is unbounded: the runner's deadline is the only clock. -use toyos_abi::syscall::{self, debug_action, SpawnArgs, SyscallError}; +use toyos_abi::syscall::{self, debug_action, EndowEntry, SpawnArgs, SyscallError}; use toyos_abi::RawHandle; use toyos_abi::end::End; @@ -27,7 +27,8 @@ fn main() { match std::env::args().nth(1).as_deref() { Some("exit") => std::process::exit(CODE), Some("speak") => { - assert_eq!(syscall::write(RawHandle(1), b"x"), Ok(1), "the child's one byte"); + let wrote = toyos::fd::with(1, |o| syscall::write(o.handle, b"x")).expect("the child's descriptor 1"); + assert_eq!(wrote, Ok(1), "the child's one byte"); } Some(other) => panic!("unknown role {other:?}"), None => test(), @@ -36,7 +37,7 @@ fn main() { fn test() { hold_the_next_spawn(); - let child = spawn("exit", &[]).expect("a spawn whose child ended inside it"); + let child = spawn("exit", None).expect("a spawn whose child ended inside it"); assert_eq!( syscall::process_wait_nonblock(child), Ok(End::Exited(CODE)), @@ -61,7 +62,7 @@ fn a_spawn_from_a_full_table_starts_no_child() { }; assert_eq!(full, SyscallError::ResourceExhausted, "the table did not fill"); hold_the_next_spawn(); - let spawned = spawn("speak", &[[1, ends.write.0]]); + let spawned = spawn("speak", Some(ends.write)); for handle in filled { syscall::close(handle); } @@ -81,23 +82,24 @@ fn hold_the_next_spawn() { ); } -/// `SYS_SPAWN` of this binary in `role`, duplicating each `[child_slot, -/// parent_handle]` pair of `slot_map` into the child. -fn spawn(role: &str, slot_map: &[[u32; 2]]) -> Result { +/// `SYS_SPAWN` of this binary in `role`, endowed `stdout` as its descriptor 1, +/// which moves only on a spawn that happens. +fn spawn(role: &str, stdout: Option) -> Result { let argv = format!("{SELF_PATH}\0{role}"); + const LABEL: &str = "fd:1"; + let endow: Vec = + stdout.into_iter().map(|handle| EndowEntry { label_off: 0, label_len: LABEL.len() as u32, handle, _pad: 0 }).collect(); let args = SpawnArgs { path_ptr: SELF_PATH.as_ptr() as u64, path_len: SELF_PATH.len() as u64, argv_ptr: argv.as_ptr() as u64, argv_len: argv.len() as u64, - slot_map_ptr: slot_map.as_ptr() as u64, - slot_map_count: slot_map.len() as u64, env_ptr: 0, env_len: 0, - endow_ptr: 0, - endow_count: 0, - labels_ptr: 0, - labels_len: 0, + endow_ptr: endow.as_ptr() as u64, + endow_count: endow.len() as u64, + labels_ptr: LABEL.as_ptr() as u64, + labels_len: LABEL.len() as u64, cwd_ptr: "/".as_ptr() as u64, cwd_len: 1, image: 0, diff --git a/tests/guest/rust-tests/src/bin/spawn_image_object.rs b/tests/guest/rust-tests/src/bin/spawn_image_object.rs index 05d96ca121..255426fc25 100644 --- a/tests/guest/rust-tests/src/bin/spawn_image_object.rs +++ b/tests/guest/rust-tests/src/bin/spawn_image_object.rs @@ -38,8 +38,6 @@ fn spawn(image: RawHandle, len: u64) -> Result { path_len: SELF.len() as u64, argv_ptr: argv.as_ptr() as u64, argv_len: argv.len() as u64, - slot_map_ptr: 0, - slot_map_count: 0, env_ptr: 0, env_len: 0, endow_ptr: 0, diff --git a/tests/guest/rust-tests/src/bin/spawn_lands_claimed.rs b/tests/guest/rust-tests/src/bin/spawn_lands_claimed.rs index e2dd02be44..53a51e6717 100644 --- a/tests/guest/rust-tests/src/bin/spawn_lands_claimed.rs +++ b/tests/guest/rust-tests/src/bin/spawn_lands_claimed.rs @@ -81,8 +81,6 @@ fn spawn_under(place: RawHandle) -> Result { path_len: SELF_PATH.len() as u64, argv_ptr: argv.as_ptr() as u64, argv_len: argv.len() as u64, - slot_map_ptr: 0, - slot_map_count: 0, env_ptr: 0, env_len: 0, endow_ptr: 0, diff --git a/tests/guest/rust-tests/src/bin/stream_handles.rs b/tests/guest/rust-tests/src/bin/stream_handles.rs index ca663771fe..6d504cb446 100644 --- a/tests/guest/rust-tests/src/bin/stream_handles.rs +++ b/tests/guest/rust-tests/src/bin/stream_handles.rs @@ -10,7 +10,7 @@ //! panics with `toyos::net`'s refusal. //! - **A stream's handle is no file**: `File::from_raw_fd` of one panics with //! that refusal. -//! - **No spawn moves one**: a stdio slot and a slot past them by +//! - **No spawn moves one**: a stdio descriptor and one past them by //! `inherit_handle`, `endow` and `provide` each answer the refusal, start no //! child, and leave the stream reading and writing through the handle. //! @@ -28,6 +28,7 @@ use std::fs::File; use std::io::{BufRead, BufReader, Read, Write}; use std::net::{SocketAddr, TcpStream}; use std::os::fd::{AsFd, AsRawFd, FromRawFd, IntoRawFd, OwnedFd}; +use std::os::toyos::io::AsRawHandle; use std::os::toyos::process::CommandExt; use std::process::{Child, Command, Stdio}; @@ -79,7 +80,7 @@ fn shares_and_refuses_moves() { let status = child.wait().expect("wait the client"); assert_eq!( said, - ["shared", "stdio slot refused", "slot past stdio refused", "endow refused", "provide refused"], + ["shared", "stdio descriptor refused", "descriptor past stdio refused", "endow refused", "provide refused"], "the client said otherwise", ); assert!(status.success(), "the client exited {status:?}"); @@ -211,12 +212,12 @@ fn client() -> ! { reads(&mut taken, 12, 4); say("shared"); - let handle = taken.as_raw_fd() as u32; + let handle = taken.as_raw_handle(); let paths: [(&str, fn(&mut Command, u32)); 4] = [ - ("stdio slot", |command, handle| { + ("stdio descriptor", |command, handle| { command.inherit_handle(1, handle); }), - ("slot past stdio", |command, handle| { + ("descriptor past stdio", |command, handle| { command.inherit_handle(5, handle); }), ("endow", |command, handle| { @@ -254,12 +255,13 @@ fn client() -> ! { /// A client that takes a bare `dup` of its stream's handle as a stream. fn foreign() -> ! { let stream = connect(); - let copy = syscall::dup(RawHandle(stream.as_raw_fd() as u32)).expect("a bare duplicate"); + let copy = syscall::dup(RawHandle(stream.as_raw_handle())).expect("a bare duplicate"); + let fd = toyos::fd::install(toyos::fd::Object::handle(copy), false, 0).expect("a descriptor for the duplicate"); say("duplicated"); let mut go = String::new(); std::io::stdin().read_line(&mut go).expect("the test's go"); - // SAFETY: a handle this process holds and nothing else answers for. - let _adopted = unsafe { TcpStream::from_raw_fd(copy.0 as i32) }; + // SAFETY: a descriptor this process holds and nothing else answers for. + let _adopted = unsafe { TcpStream::from_raw_fd(fd) }; say("adopted"); std::process::exit(0) } From 249293c2093355400f69d840d917d85cd2c10587 Mon Sep 17 00:00:00 2001 From: japabu Date: Wed, 21 Oct 2026 18:08:06 +0200 Subject: [PATCH 2/2] fd: the table refuses a bad endowment, holds no panic under its lock, and costs no data Review r1 of #978. An fd: label is read as canonical decimal only, so "fd:+3" and "fd:03" name nothing. An endowment whose number is already open is refused at the boundary and its handle closed, where an assert used to panic under the table's lock: the panic's message goes to stderr, a descriptor, and the same thread waited on the lock forever. The table's own operations no longer panic either. They answer a broken table as a fault, which the owner panics on once the lock is released. The table's empty state is all zeros, so it lands in .bss: a descriptor's word is its object's index plus one with a spare bit for the flag, and an object with no references is free. net::stream's tables, which every program now links through the table's release of a handle, are made all-zero the same way. calc's .data is back to the base's 0x16a8 bytes from 0x356c0. toyos::fd::copy refuses a stream's end by the handle the descriptor names. The check ran on the fresh duplicate, which is never a stream, so a stream dup2'd onto stdio was passed to a child. std's File child copy goes through the same check. libc's posix_spawn refuses a descriptor it holds but cannot give, where it started the child without it. Also: - a closed stdin reads its end without a probe that raced a close; - libc's poll answers POLLNVAL for a descriptor that is not open; - libc sockets carry a tag disjoint from socket2's; - libc's table-size constants are toyos::fd::MAX_FDS; - clippy lints the fd-table owner. socket2 now makes sockets not inherited and reuses keys (2aa6b0049e70). Co-Authored-By: Claude Opus 5.5 --- Cargo.lock | 2 +- build/clippy.rs | 6 + ...one-copy-outlives-a-redirect-in-another.md | 21 ++ sdk/libc/arch/src/spawn_requests.rs | 24 +- sdk/libc/src/fdreq.rs | 4 +- sdk/libc/src/posix_io.rs | 12 +- sdk/libc/src/socket.rs | 6 +- sdk/libc/src/spawn.rs | 6 +- sdk/libc/src/spawnreq.rs | 25 +- sdk/std/sys/fs.rs | 5 +- sdk/std/sys/pal/mod.rs | 30 +- sdk/std/sys/process.rs | 24 +- sdk/std/sys/stdio.rs | 20 +- sdk/toyos/src/fd.rs | 294 ++++------------- sdk/toyos/src/fd/table.rs | 303 ++++++++++++++++++ sdk/toyos/src/net/stream.rs | 32 +- tests/guest/libc/poll_closed.c | 17 + .../rust-tests/src/bin/stream_handles.rs | 52 +-- 18 files changed, 554 insertions(+), 329 deletions(-) create mode 100644 issues/a-stdio-sink-cached-in-one-copy-outlives-a-redirect-in-another.md create mode 100644 sdk/toyos/src/fd/table.rs create mode 100644 tests/guest/libc/poll_closed.c diff --git a/Cargo.lock b/Cargo.lock index cc527cf19c..0dfa500604 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3897,7 +3897,7 @@ dependencies = [ [[package]] name = "socket2" version = "0.6.3" -source = "git+https://github.com/ToyOSOrg/socket2?branch=toyos#d38850198a97a55eeccedb5fb8d91a22efc89ba3" +source = "git+https://github.com/ToyOSOrg/socket2?branch=toyos#2aa6b0049e70b4f1ff7ce01d83222c87ea72a05e" dependencies = [ "libc", "toyos", diff --git a/build/clippy.rs b/build/clippy.rs index a7fdffc3a9..5d58443fb9 100644 --- a/build/clippy.rs +++ b/build/clippy.rs @@ -209,6 +209,12 @@ const SHAPES: &[Shape] = &[ before: &["-p", "toyos", "--all-targets"], after: &["$ADOPTED", "-D", "warnings"], }, + // The copy that owns the descriptor table, as std's and C-only libc's + // build it: `fd-table`, which no other shape turns on. + Shape { + before: &["-p", "toyos", "--lib", "--features", "fd-table"], + after: &["$ADOPTED", "-D", "warnings"], + }, // Its own target directory: cargo keeps one check of a unit, so a unit // linted under two sets of lints is checked again by each, every run, and // so is every crate depending on it. diff --git a/issues/a-stdio-sink-cached-in-one-copy-outlives-a-redirect-in-another.md b/issues/a-stdio-sink-cached-in-one-copy-outlives-a-redirect-in-another.md new file mode 100644 index 0000000000..03abe0aa39 --- /dev/null +++ b/issues/a-stdio-sink-cached-in-one-copy-outlives-a-redirect-in-another.md @@ -0,0 +1,21 @@ +--- +status: open +kind: defect +opened: 2026-10-21 +--- + +# A stdio sink cached in one copy outlives a redirect in another + +`toyos::log::stdio` asks once what descriptors 1 and 2 name and caches the +answer, a log ring's mapping or the descriptor itself, in a static of each +copy of `toyos` linked into a program (`SINKS`, `sdk/toyos/src/log/stdio.rs`). +The descriptor table is one per process (`toyos::fd`), but the cache is not: +libc's `dup2` onto 1 or 2 calls `forget` only in libc's own copy, so in a +program that mixes Rust and C, std's copy keeps writing to the ring it mapped +before the redirect. Std's own `toyos::fd::dup2` forgets nothing. A +descriptor sink is asked on every write and is unaffected; only a cached ring +goes stale. Read from the code; no guest case has shown it. + +**Exit**: the sink cache lives with the table's owner, or every change of +descriptor 1 or 2 is seen by every copy, and a guest case redirects a ring +stdout from C and sees std's next line land at the new target. diff --git a/sdk/libc/arch/src/spawn_requests.rs b/sdk/libc/arch/src/spawn_requests.rs index 4175d2c0a7..bb5d37decf 100644 --- a/sdk/libc/arch/src/spawn_requests.rs +++ b/sdk/libc/arch/src/spawn_requests.rs @@ -5,7 +5,7 @@ use std::cell::RefCell; use toyos_abi::end::{End, Fault}; -use crate::errcode::EBADF; +use crate::errcode::{EBADF, EINVAL}; use crate::header::{self, FCNTL_H, SIGNAL_H}; use crate::spawnreq::{any_fd, child_fd, plan, wait_status, Action, Plan}; @@ -20,6 +20,10 @@ const HELD: [Option<(u32, bool)>; 3] = [Some((STDIN, false)), Some((STDOUT, fals /// This process's descriptor beyond 2 the tests name, holding [`PIPE`]. const PIPE_FD: u32 = 9; +/// A descriptor this process holds but cannot give a child: a stream's, or +/// a socket a library keeps as no kernel handle. +const UNCOPYABLE_FD: u32 = 10; + /// The plan, for this process's `stdio` and its descriptor [`PIPE_FD`], with /// `open` handing out handles from 0x5000 and recording each close of a /// handle it opened. @@ -27,9 +31,10 @@ fn planned(stdio: [Option<(u32, bool)>; 3], actions: &[Action<&str>]) -> (Result let next = RefCell::new(0x5000); let closed = RefCell::new(Vec::new()); let held = |fd: u32| match fd { - 0..=2 => stdio[fd as usize], - PIPE_FD => Some((PIPE, false)), - _ => None, + 0..=2 => Ok(stdio[fd as usize]), + PIPE_FD => Ok(Some((PIPE, false))), + UNCOPYABLE_FD => Err(EINVAL), + _ => Ok(None), }; let plan = plan( held, @@ -47,6 +52,17 @@ fn planned(stdio: [Option<(u32, bool)>; 3], actions: &[Action<&str>]) -> (Result (plan, closed.into_inner()) } +/// A descriptor this process holds but cannot give refuses the spawn with +/// its `errno`, and what the plan opened is closed, rather than the child +/// starting without it. +#[test] +fn a_descriptor_that_cannot_be_given_refuses_the_spawn() { + let open = Action::Open { fd: 3, path: "/tmp/in", oflag: 0, mode: 0 }; + let (plan, closed) = planned(HELD, &[open, Action::Dup2 { fd: UNCOPYABLE_FD, newfd: 1 }]); + assert_eq!(plan, Err(EINVAL)); + assert_eq!(closed, [0x5000], "the file the plan opened was left open"); +} + /// With no actions, the child holds what this process holds of 0–2 and /// nothing else. #[test] diff --git a/sdk/libc/src/fdreq.rs b/sdk/libc/src/fdreq.rs index 7eb32e9a5c..83d5c4bd27 100644 --- a/sdk/libc/src/fdreq.rs +++ b/sdk/libc/src/fdreq.rs @@ -14,8 +14,8 @@ const F_GETOWN: i32 = 9; const F_DUPFD_CLOEXEC: i32 = 1030; pub(crate) const FD_CLOEXEC: i32 = 1; -/// `{OPEN_MAX}`: the descriptors one table holds (`toyos::fd::MAX_FDS`). -pub(crate) const OPEN_MAX: usize = 4096; +/// `{OPEN_MAX}`: the descriptors one table holds. +pub(crate) const OPEN_MAX: usize = toyos::fd::MAX_FDS; /// What `fcntl` does for one command. #[derive(Debug, PartialEq, Eq)] diff --git a/sdk/libc/src/posix_io.rs b/sdk/libc/src/posix_io.rs index bd41ddff5e..a644f23759 100644 --- a/sdk/libc/src/posix_io.rs +++ b/sdk/libc/src/posix_io.rs @@ -277,12 +277,6 @@ pub unsafe extern "C" fn dup(raw_fd: i32) -> i32 { /// closing what it named. #[no_mangle] pub unsafe extern "C" fn dup2(old_fd: i32, new_fd: i32) -> i32 { - if old_fd == new_fd { - return match toyos::fd::inheritable(old_fd) { - Ok(_) => new_fd, - Err(e) => refuse(fd_errno(e)), - }; - } match toyos::fd::dup2(old_fd, new_fd, true) { Ok(fd) => { // The descriptor names something else now, so the stream is asked again. @@ -667,6 +661,7 @@ pub unsafe extern "C" fn poll(fds: *mut pollfd, nfds: u32, timeout: i32) -> i32 const POLLIN: i16 = 1; const POLLOUT: i16 = 4; + const POLLNVAL: i16 = 0x20; if nfds == 0 { if timeout > 0 { @@ -700,8 +695,11 @@ pub unsafe extern "C" fn poll(fds: *mut pollfd, nfds: u32, timeout: i32) -> i32 } }) .collect(); + // A descriptor that is not open answers `POLLNVAL`; a negative one is + // ignored, as POSIX has it. + let closed: alloc::vec::Vec = entries.iter().map(|&(fd, _)| fd >= 0 && toyos::fd::with(fd, drop).is_err()).collect(); loop { - let mut revents = alloc::vec![0i16; n]; + let mut revents: alloc::vec::Vec = closed.iter().map(|&closed| if closed { POLLNVAL } else { 0 }).collect(); let mut watching = alloc::vec::Vec::new(); for (i, &(fd, events)) in entries.iter().enumerate() { let Some(handle) = crate::socket::stream_of(fd) else { continue }; diff --git a/sdk/libc/src/socket.rs b/sdk/libc/src/socket.rs index 8e3c630e08..979325b812 100644 --- a/sdk/libc/src/socket.rs +++ b/sdk/libc/src/socket.rs @@ -81,9 +81,9 @@ struct SocketEntry { const MAX_SOCKETS: usize = 128; /// A socket's descriptor names an object of this library's own -/// (`toyos::fd::Object::opaque`), whose word is this tag beside the socket's -/// index here. -const SOCKET_TAG: u64 = 0x736f_636b << 32; +/// (`toyos::fd::Object::opaque`), whose word is this tag, `libc`, beside the +/// socket's index here, its upper half unlike socket2's. +const SOCKET_TAG: u64 = 0x6c69_6263 << 32; static mut SOCKETS: [Option; MAX_SOCKETS] = [None; MAX_SOCKETS]; diff --git a/sdk/libc/src/spawn.rs b/sdk/libc/src/spawn.rs index 3962f0027f..36bdca02f1 100644 --- a/sdk/libc/src/spawn.rs +++ b/sdk/libc/src/spawn.rs @@ -277,11 +277,11 @@ fn spawn_counted(path: &str, argv: &[u8], env: &[u8], actions: &[Action = Vec::new(); let plan = spawnreq::plan( |fd| { - let copy = toyos::fd::copy(fd as i32).ok().flatten()?; - let cloexec = !toyos::fd::inheritable(fd as i32).ok()?; + let Some(copy) = toyos::fd::copy(fd as i32).map_err(errno_of)? else { return Ok(None) }; + let Ok(inheritable) = toyos::fd::inheritable(fd as i32) else { return Ok(None) }; let handle = toyos::AsHandle::as_handle(©).0; held.push(copy); - Some((handle, cloexec)) + Ok(Some((handle, !inheritable))) }, actions, |path, oflag, mode| crate::posix_io::open_handle(path, oflag, mode).map(|h| h.0).map_err(crate::errcode::of), diff --git a/sdk/libc/src/spawnreq.rs b/sdk/libc/src/spawnreq.rs index 479828fc4b..d6c83c3f7d 100644 --- a/sdk/libc/src/spawnreq.rs +++ b/sdk/libc/src/spawnreq.rs @@ -17,9 +17,6 @@ use crate::errcode::EBADF; const O_CLOEXEC: i32 = 0x80000; -/// The descriptors a child's table holds (`toyos::fd::MAX_FDS`). -pub(crate) const CHILD_FDS: usize = 4096; - /// One `posix_spawn_file_actions_add*`, in the child's descriptor numbers. #[derive(Clone, Debug, PartialEq, Eq)] pub(crate) enum Action

{ @@ -31,13 +28,13 @@ pub(crate) enum Action

{ /// A descriptor of the child's an action makes, `addopen`'s and `adddup2`'s /// `newfd`, or `EBADF` for one no child's table has. pub(crate) fn child_fd(fd: i32) -> Result { - u32::try_from(fd).ok().filter(|&fd| (fd as usize) < CHILD_FDS).ok_or(EBADF) + u32::try_from(fd).ok().filter(|&fd| (fd as usize) < toyos::fd::MAX_FDS).ok_or(EBADF) } /// A descriptor an action reads or closes, `adddup2`'s `fd` and /// `addclose`'s: one of the child's, or of this process's. pub(crate) fn any_fd(fd: i32) -> Result { - u32::try_from(fd).ok().filter(|&fd| (fd as usize) < CHILD_FDS).ok_or(EBADF) + u32::try_from(fd).ok().filter(|&fd| (fd as usize) < toyos::fd::MAX_FDS).ok_or(EBADF) } /// What the spawn gives the child: `(child descriptor, this process's @@ -59,21 +56,21 @@ struct Entry { /// The child's table: `held(n)` is this process's handle for its descriptor /// `n` and whether it is marked close-on-exec, `None` for one it does not -/// hold; 0 to 2 first, then each action in order, as POSIX runs them in the +/// hold, or the `errno` of one it holds but cannot give; 0 to 2 first, then each action in order, as POSIX runs them in the /// child; then the exec, which closes what is marked. A descriptor of this /// process's beyond 2 that no action closed is one an action may name, and is /// not the child's otherwise. `open` opens a path here, answering the handle /// or an `errno`; a refusal ends the plan with the handles it opened closed /// by `close`. pub(crate) fn plan

( - mut held: impl FnMut(u32) -> Option<(u32, bool)>, + mut held: impl FnMut(u32) -> Result, i32>, actions: &[Action

], mut open: impl FnMut(&P, i32, u32) -> Result, mut close: impl FnMut(u32), ) -> Result { let mut table: BTreeMap = BTreeMap::new(); for fd in 0..3 { - if let Some((handle, cloexec)) = held(fd) { + if let Some((handle, cloexec)) = held(fd)? { table.insert(fd, Entry { handle, cloexec }); } } @@ -101,11 +98,15 @@ pub(crate) fn plan

( } Action::Dup2 { fd, newfd } => { let source = match table.get(fd) { - Some(entry) => Some(entry.handle), - None if *fd > 2 && !closed.contains(fd) => held(*fd).map(|(handle, _)| handle), - None => None, + Some(entry) => Ok(Some(entry.handle)), + None if *fd > 2 && !closed.contains(fd) => held(*fd).map(|held| held.map(|(handle, _)| handle)), + None => Ok(None), + }; + let source = match source { + Ok(Some(source)) => source, + Ok(None) => return refuse(&mut opened, &mut close, EBADF), + Err(errno) => return refuse(&mut opened, &mut close, errno), }; - let Some(source) = source else { return refuse(&mut opened, &mut close, EBADF) }; table.insert(*newfd, Entry { handle: source, cloexec: false }); closed.remove(newfd); } diff --git a/sdk/std/sys/fs.rs b/sdk/std/sys/fs.rs index f9035b00ba..2a11846ab5 100644 --- a/sdk/std/sys/fs.rs +++ b/sdk/std/sys/fs.rs @@ -527,8 +527,9 @@ impl File { /// offset. The caller holds it until the child has its own. pub fn as_child_stdio(&self) -> io::Result { match &self.0 { - // SAFETY: a fresh duplicate, which nothing else answers for. - Inner::Kernel(fd) => crate::sys::on_fd(*fd, syscall::dup).map(|h| unsafe { toyos::OwnedHandle::from_raw(h) }), + 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), } } diff --git a/sdk/std/sys/pal/mod.rs b/sdk/std/sys/pal/mod.rs index f0e9d35119..b545cdeef5 100644 --- a/sdk/std/sys/pal/mod.rs +++ b/sdk/std/sys/pal/mod.rs @@ -45,14 +45,38 @@ pub fn on_fd( fd: i32, f: impl FnOnce(toyos_abi::RawHandle) -> Result, ) -> crate::io::Result { - let answer = toyos::fd::with(fd, |object| match object.release { + 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 { 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)? + }) +} + +/// Why nothing was spawned, as std's error. +pub fn refused(refused: toyos::spawn::Refused) -> crate::io::Error { + use toyos::spawn::Refused; + + use crate::io::{Error, ErrorKind}; + match refused { + Refused::Permission(why) => Error::new(ErrorKind::PermissionDenied, why), + Refused::Invalid(why) => Error::new(ErrorKind::InvalidInput, why), + Refused::Stream(handle) => { + Error::new(ErrorKind::InvalidInput, toyos::net::stream::Misuse::Moved(handle).to_string()) + } + Refused::NotStarted => Error::from(ErrorKind::Other), + Refused::Syscall(e) => to_io_error(e), + } } /// A kernel handle this process just got, as a descriptor of its own: std diff --git a/sdk/std/sys/process.rs b/sdk/std/sys/process.rs index 6318e14d4c..a60be02f03 100644 --- a/sdk/std/sys/process.rs +++ b/sdk/std/sys/process.rs @@ -7,6 +7,7 @@ use crate::path::Path; use crate::process::StdioPipes; use crate::sys::fs::File; use crate::sys::pipe::Pipe; +use crate::sys::refused; use crate::{fmt, io}; //////////////////////////////////////////////////////////////////////////////// @@ -347,15 +348,7 @@ impl Command { ) -> io::Result> { // What this process's own descriptor names, if it is open: one it // closed is closed in the child too. - let inherit = |from: i32| -> io::Result> { - let copy = toyos::fd::copy(from).map_err(crate::sys::to_io_error)?; - match copy { - Some(copy) if toyos::net::stream::is_stream(toyos::AsHandle::as_handle(©)) => { - Err(refused(toyos::spawn::Refused::Stream(toyos::AsHandle::as_handle(©)))) - } - copy => Ok(copy), - } - }; + let inherit = |from: i32| toyos::fd::copy(from).map_err(refused); // SAFETY: each `dup` answers a fresh handle nothing else answers for. let copy = |pipe: &Pipe| pipe.with_handle(toyos_abi::syscall::dup)?.map(|h| unsafe { toyos::OwnedHandle::from_raw(h) }).map_err(crate::sys::to_io_error); Ok(match stdio { @@ -390,19 +383,6 @@ fn utf8<'a>(path: &'a OsStr, what: &'static str) -> io::Result<&'a str> { path.to_str().ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, what)) } -fn refused(refused: toyos::spawn::Refused) -> io::Error { - use toyos::spawn::Refused; - match refused { - Refused::Permission(why) => io::Error::new(io::ErrorKind::PermissionDenied, why), - Refused::Invalid(why) => io::Error::new(io::ErrorKind::InvalidInput, why), - Refused::Stream(handle) => { - io::Error::new(io::ErrorKind::InvalidInput, toyos::net::stream::Misuse::Moved(handle).to_string()) - } - Refused::NotStarted => io::Error::from(io::ErrorKind::Other), - Refused::Syscall(e) => crate::sys::to_io_error(e), - } -} - impl From for Stdio { fn from(pipe: ChildPipe) -> Stdio { Stdio::InheritPipe(pipe) diff --git a/sdk/std/sys/stdio.rs b/sdk/std/sys/stdio.rs index 715b447bc5..797fefcfa9 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::{on_fd, to_io_error}; +use crate::sys::{fd_error, on_fd, on_open_fd, to_io_error}; /// Standard input's descriptor. const STDIN: i32 = 0; @@ -35,9 +35,19 @@ 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. +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)), + } +} + fn read_one() -> io::Result { let mut byte = [0u8; 1]; - let n = on_fd(STDIN, |h| syscall::read(h, &mut byte))?; + let n = read_stdin(&mut byte)?; if n == 0 { Err(io::Error::new(io::ErrorKind::UnexpectedEof, "eof")) } else { Ok(byte[0]) } } @@ -127,14 +137,10 @@ impl Stdin { impl io::Read for Stdin { fn read(&mut self, buf: &mut [u8]) -> io::Result { - // A closed stdin is at its end, as std's on unix reads one. - if toyos::fd::inheritable(STDIN).is_err() { - return Ok(0); - } let stat = on_fd(STDIN, syscall::fstat).ok(); let interactive = stat.is_some_and(|s| s.file_type == FileType::Tty); if STDIN_RAW.load(Ordering::Relaxed) || !interactive { - on_fd(STDIN, |h| syscall::read(h, buf)) + read_stdin(buf) } else { canonical_read(buf) } diff --git a/sdk/toyos/src/fd.rs b/sdk/toyos/src/fd.rs index 6355ef038d..b909ac71da 100644 --- a/sdk/toyos/src/fd.rs +++ b/sdk/toyos/src/fd.rs @@ -77,131 +77,8 @@ impl Object { } } -/// One descriptor's entry: its object's index and its own flag. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -struct Entry { - object: u16, - inheritable: bool, -} - -#[derive(Clone, Copy)] -struct Held { - object: Object, - /// The descriptors naming it and the calls holding it. - refs: u32, -} - -/// The table's state, without its lock: every decision the table makes, -/// which a host tests. A removal answers the object it released, for the -/// caller to let go outside the lock. -pub struct Table { - fds: [Option; N], - objects: [Option; N], -} - -impl Default for Table { - fn default() -> Self { - Self::new() - } -} - -impl Table { - pub const fn new() -> Self { - Self { fds: [None; N], objects: [None; N] } - } - - fn entry(&self, fd: Fd) -> Result { - usize::try_from(fd).ok().and_then(|i| self.fds.get(i).copied().flatten()).ok_or(Error::BadFd) - } - - fn lowest_free(&self, floor: Fd) -> Result { - let floor = usize::try_from(floor).map_err(|_| Error::Full)?; - (floor..N).find(|&i| self.fds[i].is_none()).ok_or(Error::Full) - } - - /// A reference fewer on `object`: the object, once none is left. - fn unref(&mut self, object: u16) -> Option { - let held = self.objects[object as usize].as_mut().expect("a referenced object"); - held.refs -= 1; - match held.refs { - 0 => self.objects[object as usize].take().map(|h| h.object), - _ => None, - } - } - - /// Name `object` by the lowest free descriptor at or above `floor`. - pub fn install(&mut self, object: Object, inheritable: bool, floor: Fd) -> Result { - let fd = self.lowest_free(floor)?; - let slot = self.objects.iter().position(Option::is_none).ok_or(Error::Full)?; - self.objects[slot] = Some(Held { object, refs: 1 }); - self.fds[fd] = Some(Entry { object: slot as u16, inheritable }); - Ok(fd as Fd) - } - - /// A second descriptor on `fd`'s object, the lowest free at or above - /// `floor`. - pub fn dup(&mut self, fd: Fd, floor: Fd, inheritable: bool) -> Result { - let entry = self.entry(fd)?; - let new = self.lowest_free(floor)?; - self.objects[entry.object as usize].as_mut().expect("named").refs += 1; - self.fds[new] = Some(Entry { object: entry.object, inheritable }); - Ok(new as Fd) - } - - /// `new` on `old`'s object, whatever `new` named before: that object, - /// if this was its last reference. `new == old` changes nothing. - pub fn dup2(&mut self, old: Fd, new: Fd, inheritable: bool) -> Result, Error> { - let entry = self.entry(old)?; - let slot = usize::try_from(new).ok().filter(|&i| i < N).ok_or(Error::BadFd)?; - if new == old { - return Ok(None); - } - self.objects[entry.object as usize].as_mut().expect("named").refs += 1; - let displaced = self.fds[slot].replace(Entry { object: entry.object, inheritable }); - Ok(displaced.and_then(|d| self.unref(d.object))) - } - - /// Forget `fd`: its object, if this was its last reference. - pub fn close(&mut self, fd: Fd) -> Result, Error> { - let entry = self.entry(fd)?; - self.fds[fd as usize] = None; - Ok(self.unref(entry.object)) - } - - /// `fd`'s object, held until [`Self::unuse`] of the token answered. - pub fn acquire(&mut self, fd: Fd) -> Result<(u16, Object), Error> { - let entry = self.entry(fd)?; - let held = self.objects[entry.object as usize].as_mut().expect("named"); - held.refs += 1; - Ok((entry.object, held.object)) - } - - /// A call done with the object `token` names: the object, if nothing - /// names it any more. - pub fn unuse(&mut self, token: u16) -> Option { - self.unref(token) - } - - /// Make `fd` name `object` from here on, in place: the object it named, - /// if this was its last reference. Its flag stays. - pub fn replace(&mut self, fd: Fd, object: Object) -> Result, Error> { - let entry = self.entry(fd)?; - let slot = self.objects.iter().position(Option::is_none).ok_or(Error::Full)?; - self.objects[slot] = Some(Held { object, refs: 1 }); - self.fds[fd as usize] = Some(Entry { object: slot as u16, inheritable: entry.inheritable }); - Ok(self.unref(entry.object)) - } - - pub fn inheritable(&self, fd: Fd) -> Result { - self.entry(fd).map(|e| e.inheritable) - } - - pub fn set_inheritable(&mut self, fd: Fd, inheritable: bool) -> Result<(), Error> { - self.entry(fd)?; - self.fds[fd as usize].as_mut().expect("present").inheritable = inheritable; - Ok(()) - } -} +#[cfg(any(feature = "fd-table", test))] +mod table; // The C interface: what every copy calls and the owner defines. A negative // answer is an [`Error`]: -1 `BadFd`, -2 `Full`. @@ -271,7 +148,8 @@ pub fn dup(fd: Fd, floor: Fd, inheritable: bool) -> Result { decode(unsafe { toyos_fd_dup(fd, floor, inheritable) }) } -/// POSIX's `dup2`: `new` on `old`'s object, after closing what `new` named. +/// POSIX's `dup2`: `new` on `old`'s object, after closing what `new` named; +/// onto itself, nothing but `old` checked open. pub fn dup2(old: Fd, new: Fd, inheritable: bool) -> Result { // SAFETY: plain values. decode(unsafe { toyos_fd_dup2(old, new, inheritable) }) @@ -286,17 +164,18 @@ pub fn replace(fd: Fd, object: Object) -> Result<(), Error> { } /// A copy of the kernel handle `fd` names, for a child: `None` for a -/// descriptor that is not open, as a child of POSIX's holds no closed one; -/// `Err` for one that names no kernel handle, or that the kernel will not +/// descriptor that is not open, as a child of POSIX's holds no closed one. +/// Refused for a stream's end, which `net::stream` answers for here alone, +/// for an object that is no kernel handle, and for one the kernel will not /// duplicate. -pub fn copy(fd: Fd) -> Result, toyos_abi::syscall::SyscallError> { - match with(fd, |o| match o.release { - None => toyos_abi::syscall::dup(o.handle).map(crate::OwnedHandle), - Some(_) => Err(toyos_abi::syscall::SyscallError::InvalidArgument), - }) { - Ok(copied) => copied.map(Some), - Err(_) => Ok(None), - } +pub fn copy(fd: Fd) -> Result, crate::spawn::Refused> { + use 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)), + }); + copied.map_or(Ok(None), |copied| copied.map(Some)) } pub fn inheritable(fd: Fd) -> Result { @@ -309,16 +188,19 @@ pub fn set_inheritable(fd: Fd, inheritable: bool) -> Result<(), Error> { decode(unsafe { toyos_fd_set_inheritable(fd, inheritable) }).map(drop) } -/// The descriptor an `fd:` endowment's label names: `None` for any other -/// label, and for a number past the table. +/// The descriptor an `fd:` endowment's label names, `n` in canonical +/// decimal: `None` for any other label, and for a number past the table. pub fn endowed_fd(label: &str) -> Option { - let n: Fd = label.strip_prefix(toyos_abi::syscall::FD_PREFIX)?.parse().ok()?; + let digits = label.strip_prefix(toyos_abi::syscall::FD_PREFIX)?; + let canonical = digits.bytes().all(|b| b.is_ascii_digit()) && (digits == "0" || !digits.starts_with('0')); + let n: Fd = digits.parse().ok().filter(|_| canonical)?; (0..MAX_FDS as Fd).contains(&n).then_some(n) } /// The table itself, in the copy that owns it. #[cfg(all(feature = "fd-table", not(test)))] mod owner { + use super::table::{Fault, Table}; use super::*; use crate::endow::Endowments; use crate::sync::Lock; @@ -328,21 +210,42 @@ mod owner { static TABLE: Lock<(bool, Table)> = Lock::new((false, Table::new())); /// `f` on the table, whose first use installs every descriptor the - /// process was endowed at its number. - fn table(f: impl FnOnce(&mut Table) -> R) -> R { - let mut held = TABLE.lock(); - let (ready, table) = &mut *held; - if !*ready { - *ready = true; - let endowed = Endowments::get(); - for label in endowed.labels() { - let Some(fd) = endowed_fd(label) else { continue }; - let Some(handle) = endowed.take::(label) else { continue }; - let installed = table.install(Object::handle(handle.into_raw()), true, fd); - assert_eq!(installed, Ok(fd), "toyos::fd: endowed descriptor {fd} is not free at its number"); + /// process was endowed at its number. Nothing panics under the lock: a + /// 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 answer = { + let mut held = TABLE.lock(); + let (ready, table) = &mut *held; + let installed = match *ready { + true => Ok(()), + false => { + *ready = true; + endow(table, endowed) + } + }; + installed.and_then(|()| f(table)) + }; + answer.map_err(|fault| match fault { + Fault::Refused(e) => e, + Fault::Broken => panic!("toyos::fd: the descriptor table is inconsistent"), + }) + } + + /// 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> { + 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::Broken) => return Err(Fault::Broken), } } - f(table) + Ok(()) } fn released(object: Option) { @@ -365,7 +268,7 @@ mod owner { Ok((token, object)) => { // SAFETY: the caller's live place. unsafe { out.write(object) }; - i32::from(token) + token as i32 } Err(e) => encode(e), } @@ -373,8 +276,8 @@ mod owner { #[no_mangle] extern "C" fn toyos_fd_unuse(token: i32) { - let token = u16::try_from(token).expect("toyos::fd: a token no acquire answered"); - released(table(|t| t.unuse(token))); + let token = usize::try_from(token).expect("toyos::fd: a token no acquire answered"); + released(table(|t| t.unuse(token)).expect("toyos::fd: a token no acquire answered")); } #[no_mangle] @@ -433,90 +336,11 @@ mod owner { mod tests { use super::*; - extern "C" fn nothing(_: u64) {} - - fn o(word: u64) -> Object { - Object::opaque(word, nothing) - } - - type T = Table<8>; - - /// The word of what a removal released, which names it in these tests. - fn w(released: Result, Error>) -> Result, Error> { - released.map(|o| o.map(|o| o.word)) - } - - #[test] - fn descriptors_are_numbered_lowest_free_from_the_floor() { - let mut t = T::new(); - assert_eq!(t.install(o(1), false, 0), Ok(0)); - assert_eq!(t.install(o(2), false, 0), Ok(1)); - assert_eq!(t.install(o(3), false, 5), Ok(5)); - assert_eq!(w(t.close(0)), Ok(Some(1))); - assert_eq!(t.install(o(4), false, 0), Ok(0), "a closed number is the lowest free again"); - assert_eq!(t.install(o(5), false, 8), Err(Error::Full)); - assert_eq!(t.install(o(5), false, -1), Err(Error::Full)); - } - - /// Descriptors on one object release it with the last of them, and each - /// keeps a flag of its own. - #[test] - fn an_object_goes_with_its_last_descriptor_and_flags_are_per_descriptor() { - let mut t = T::new(); - let a = t.install(o(7), true, 0).unwrap(); - let b = t.dup(a, 0, false).unwrap(); - assert_eq!((t.inheritable(a), t.inheritable(b)), (Ok(true), Ok(false))); - assert_eq!(w(t.close(a)), Ok(None), "released while a dup named it"); - assert_eq!(w(t.close(b)), Ok(Some(7))); - assert_eq!(w(t.close(b)), Err(Error::BadFd)); - } - - /// A call holds its object: a close meanwhile makes the descriptor gone - /// at once, and the object goes when the call is done. - #[test] - fn a_close_during_a_call_releases_the_object_when_the_call_is_done() { - let mut t = T::new(); - let fd = t.install(o(9), false, 0).unwrap(); - let (token, object) = t.acquire(fd).unwrap(); - assert_eq!(object.word, 9); - assert_eq!(w(t.close(fd)), Ok(None), "the object went under a call"); - assert!(matches!(t.acquire(fd), Err(Error::BadFd)), "a closed descriptor still answered"); - assert_eq!(t.unuse(token).map(|o| o.word), Some(9)); - } - - #[test] - fn dup2_displaces_what_the_target_named_and_onto_itself_changes_nothing() { - let mut t = T::new(); - let a = t.install(o(1), false, 0).unwrap(); - let b = t.install(o(2), false, 0).unwrap(); - assert_eq!(w(t.dup2(a, b, true)), Ok(Some(2))); - assert_eq!(t.inheritable(b), Ok(true)); - assert_eq!(w(t.dup2(a, a, true)), Ok(None)); - assert_eq!(t.inheritable(a), Ok(false), "dup2 onto itself set a flag"); - assert_eq!(w(t.dup2(a, 6, false)), Ok(None), "an unused target"); - assert_eq!(w(t.dup2(a, 8, false)), Err(Error::BadFd), "a target past the table"); - assert_eq!(w(t.dup2(7, 1, false)), Err(Error::BadFd)); - for fd in [a, b, 6] { - let last = t.close(fd).unwrap(); - assert_eq!(last.is_some(), fd == 6, "descriptor {fd}"); - } - } - - #[test] - fn replace_keeps_the_number_and_the_flag_and_releases_the_old_object() { - let mut t = T::new(); - let fd = t.install(o(1), true, 0).unwrap(); - assert_eq!(w(t.replace(fd, o(2))), Ok(Some(1))); - assert_eq!(t.inheritable(fd), Ok(true)); - assert_eq!(w(t.close(fd)), Ok(Some(2))); - assert_eq!(w(t.replace(fd, o(3))), Err(Error::BadFd)); - } - #[test] fn an_fd_label_names_its_number_and_no_other_label_names_one() { assert_eq!(endowed_fd("fd:0"), Some(0)); assert_eq!(endowed_fd("fd:4095"), Some(4095)); - for other in ["fd:4096", "fd:-1", "fd:", "fd:1x", "svc", "fd1", "dev:fd:1"] { + for other in ["fd:4096", "fd:-1", "fd:", "fd:1x", "svc", "fd1", "dev:fd:1", "fd:+3", "fd:03", "fd:00", "fd: 3"] { assert_eq!(endowed_fd(other), None, "{other}"); } } diff --git a/sdk/toyos/src/fd/table.rs b/sdk/toyos/src/fd/table.rs new file mode 100644 index 0000000000..57ed8e71da --- /dev/null +++ b/sdk/toyos/src/fd/table.rs @@ -0,0 +1,303 @@ +//! The table's state, without its lock: every decision the table makes, +//! which a host tests. Nothing in it panics. + +use super::{Error, Fd, Object, MAX_FDS}; +use toyos_abi::RawHandle; + +/// A descriptor's word: 0 for a free number, else its object's index plus +/// one, with [`INHERITABLE`] set for an inheritable one. The empty table is +/// all zeros, so it costs a program no initialised data. +type Word = u16; +const INHERITABLE: Word = 0x8000; +const _: () = assert!(MAX_FDS < INHERITABLE as usize); + +#[derive(Clone, Copy)] +struct Held { + object: Object, + /// The descriptors naming it and the calls holding it; 0 for a free one. + refs: u32, +} + +const FREE: Held = Held { object: Object::handle(RawHandle(0)), refs: 0 }; + +/// Why a table operation failed: refused, or the table found inconsistent, +/// which the owner panics on only once its lock is released, since the panic +/// writes to a descriptor. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(super) enum Fault { + Refused(Error), + Broken, +} + +impl From for Fault { + fn from(e: Error) -> Self { + Self::Refused(e) + } +} + +/// A removal answers the object it released, for the caller to let go +/// outside the lock. +pub(super) struct Table { + fds: [Word; N], + objects: [Held; N], +} + +impl Table { + pub(super) const fn new() -> Self { + Self { fds: [0; N], objects: [FREE; N] } + } + + /// `fd`'s index, object and flag. + fn entry(&self, fd: Fd) -> Result<(usize, usize, bool), Fault> { + let i = usize::try_from(fd).map_err(|_| Error::BadFd)?; + let word = *self.fds.get(i).ok_or(Error::BadFd)?; + let object = usize::from(word & !INHERITABLE).checked_sub(1).ok_or(match word { + 0 => Fault::Refused(Error::BadFd), + _ => Fault::Broken, + })?; + Ok((i, object, word & INHERITABLE != 0)) + } + + fn set(&mut self, i: usize, object: usize, inheritable: bool) -> Result<(), Fault> { + let index = Word::try_from(object).ok().filter(|&o| o < INHERITABLE - 1).ok_or(Fault::Broken)?; + let flag = if inheritable { INHERITABLE } else { 0 }; + *self.fds.get_mut(i).ok_or(Fault::Broken)? = (index + 1) | flag; + Ok(()) + } + + fn lowest_free(&self, floor: Fd) -> Result { + let floor = usize::try_from(floor).map_err(|_| Error::Full)?; + let above = self.fds.get(floor..).ok_or(Error::Full)?; + above.iter().position(|&w| w == 0).map(|i| floor + i).ok_or(Error::Full) + } + + /// A free object holding `object`, referenced once. + fn hold(&mut self, object: Object) -> Result { + let slot = self.objects.iter().position(|h| h.refs == 0).ok_or(Error::Full)?; + self.objects[slot] = Held { object, refs: 1 }; + Ok(slot) + } + + fn held(&mut self, object: usize) -> Result<&mut Held, Fault> { + self.objects.get_mut(object).filter(|h| h.refs != 0).ok_or(Fault::Broken) + } + + fn reference(&mut self, object: usize) -> Result { + let held = self.held(object)?; + held.refs = held.refs.checked_add(1).ok_or(Fault::Broken)?; + Ok(held.object) + } + + /// A reference fewer on `object`: the object, once none is left. + fn unref(&mut self, object: usize) -> Result, Fault> { + let held = self.held(object)?; + held.refs -= 1; + Ok((held.refs == 0).then(|| core::mem::replace(held, FREE).object)) + } + + /// Name `object` by the lowest free descriptor at or above `floor`. + pub(super) fn install(&mut self, object: Object, inheritable: bool, floor: Fd) -> Result { + let fd = self.lowest_free(floor)?; + let slot = self.hold(object)?; + self.set(fd, slot, inheritable)?; + Ok(fd as Fd) + } + + /// Name `object` by `fd` exactly: `BadFd` for a number past the table + /// or already open. + pub(super) fn install_at(&mut self, fd: Fd, object: Object, inheritable: bool) -> Result<(), Fault> { + let i = usize::try_from(fd).map_err(|_| Error::BadFd)?; + if self.fds.get(i) != Some(&0) { + return Err(Error::BadFd.into()); + } + let slot = self.hold(object)?; + self.set(i, slot, inheritable) + } + + /// A second descriptor on `fd`'s object, the lowest free at or above + /// `floor`. + pub(super) fn dup(&mut self, fd: Fd, floor: Fd, inheritable: bool) -> Result { + let (_, object, _) = self.entry(fd)?; + let new = self.lowest_free(floor)?; + self.reference(object)?; + self.set(new, object, inheritable)?; + Ok(new as Fd) + } + + /// `new` on `old`'s object, whatever `new` named before: that object, + /// if this was its last reference. `new == old` changes nothing. + pub(super) fn dup2(&mut self, old: Fd, new: Fd, inheritable: bool) -> Result, Fault> { + let (_, object, _) = self.entry(old)?; + let slot = usize::try_from(new).ok().filter(|&i| i < N).ok_or(Error::BadFd)?; + if new == old { + return Ok(None); + } + let displaced = self.entry(new).ok(); + self.reference(object)?; + self.set(slot, object, inheritable)?; + match displaced { + Some((_, displaced, _)) => self.unref(displaced), + None => Ok(None), + } + } + + /// Forget `fd`: its object, if this was its last reference. + pub(super) fn close(&mut self, fd: Fd) -> Result, Fault> { + let (i, object, _) = self.entry(fd)?; + *self.fds.get_mut(i).ok_or(Fault::Broken)? = 0; + self.unref(object) + } + + /// `fd`'s object, held until [`Self::unuse`] of the token answered. + pub(super) fn acquire(&mut self, fd: Fd) -> Result<(usize, Object), Fault> { + let (_, object, _) = self.entry(fd)?; + Ok((object, self.reference(object)?)) + } + + /// A call done with the object `token` names: the object, if nothing + /// names it any more. + pub(super) fn unuse(&mut self, token: usize) -> Result, Fault> { + self.unref(token) + } + + /// Make `fd` name `object` from here on, in place: the object it named, + /// if this was its last reference. Its flag stays. + pub(super) fn replace(&mut self, fd: Fd, object: Object) -> Result, Fault> { + let (i, old, inheritable) = self.entry(fd)?; + let slot = self.hold(object)?; + self.set(i, slot, inheritable)?; + self.unref(old) + } + + pub(super) fn inheritable(&self, fd: Fd) -> Result { + self.entry(fd).map(|(_, _, inheritable)| inheritable) + } + + pub(super) fn set_inheritable(&mut self, fd: Fd, inheritable: bool) -> Result<(), Fault> { + let (i, object, _) = self.entry(fd)?; + self.set(i, object, inheritable) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + extern "C" fn nothing(_: u64) {} + + fn o(word: u64) -> Object { + Object::opaque(word, nothing) + } + + type T = Table<8>; + + /// The word of what a removal released, which names it in these tests. + fn w(released: Result, Fault>) -> Result, Fault> { + released.map(|o| o.map(|o| o.word)) + } + + /// The objects something still references. + fn held(t: &T) -> usize { + t.objects.iter().filter(|h| h.refs != 0).count() + } + + #[test] + fn descriptors_are_numbered_lowest_free_from_the_floor() { + let mut t = T::new(); + assert_eq!(t.install(o(1), false, 0), Ok(0)); + assert_eq!(t.install(o(2), false, 0), Ok(1)); + assert_eq!(t.install(o(3), false, 5), Ok(5)); + assert_eq!(w(t.close(0)), Ok(Some(1))); + assert_eq!(t.install(o(4), false, 0), Ok(0), "a closed number is the lowest free again"); + assert_eq!(t.install(o(5), false, 8), Err(Error::Full.into())); + assert_eq!(t.install(o(5), false, -1), Err(Error::Full.into())); + } + + /// Descriptors on one object release it with the last of them, and each + /// keeps a flag of its own. + #[test] + fn an_object_goes_with_its_last_descriptor_and_flags_are_per_descriptor() { + let mut t = T::new(); + let a = t.install(o(7), true, 0).unwrap(); + let b = t.dup(a, 0, false).unwrap(); + assert_eq!((t.inheritable(a), t.inheritable(b)), (Ok(true), Ok(false))); + assert_eq!(t.set_inheritable(b, true), Ok(())); + assert_eq!((t.inheritable(a), t.inheritable(b)), (Ok(true), Ok(true))); + assert_eq!(t.set_inheritable(a, false), Ok(())); + assert_eq!((t.inheritable(a), t.inheritable(b)), (Ok(false), Ok(true)), "a flag is the descriptor's, not its object's"); + assert_eq!(w(t.close(a)), Ok(None), "released while a dup named it"); + assert_eq!(w(t.close(b)), Ok(Some(7))); + assert_eq!(w(t.close(b)), Err(Error::BadFd.into())); + assert_eq!(held(&t), 0, "an object outlived its descriptors"); + } + + /// A call holds its object: a close meanwhile makes the descriptor gone + /// at once, and the object goes when the call is done. + #[test] + fn a_close_during_a_call_releases_the_object_when_the_call_is_done() { + let mut t = T::new(); + let fd = t.install(o(9), false, 0).unwrap(); + let (token, object) = t.acquire(fd).unwrap(); + assert_eq!(object.word, 9); + assert_eq!(w(t.close(fd)), Ok(None), "the object went under a call"); + assert!(matches!(t.acquire(fd), Err(Fault::Refused(Error::BadFd))), "a closed descriptor still answered"); + assert_eq!(w(t.unuse(token)), Ok(Some(9))); + assert_eq!(held(&t), 0, "an object outlived its call"); + } + + #[test] + fn dup2_displaces_what_the_target_named_and_onto_itself_changes_nothing() { + let mut t = T::new(); + let a = t.install(o(1), false, 0).unwrap(); + let b = t.install(o(2), false, 0).unwrap(); + assert_eq!(w(t.dup2(a, b, true)), Ok(Some(2))); + assert_eq!(t.inheritable(b), Ok(true)); + assert_eq!(w(t.dup2(a, a, true)), Ok(None)); + assert_eq!(t.inheritable(a), Ok(false), "dup2 onto itself set a flag"); + assert_eq!(w(t.dup2(a, 6, false)), Ok(None), "an unused target"); + assert_eq!(w(t.dup2(a, 8, false)), Err(Error::BadFd.into()), "a target past the table"); + assert_eq!(w(t.dup2(7, 1, false)), Err(Error::BadFd.into())); + for fd in [a, b, 6] { + let last = t.close(fd).unwrap(); + assert_eq!(last.is_some(), fd == 6, "descriptor {fd}"); + } + assert_eq!(held(&t), 0, "an object outlived its descriptors"); + } + + #[test] + fn replace_keeps_the_number_and_the_flag_and_releases_the_old_object() { + let mut t = T::new(); + let fd = t.install(o(1), true, 0).unwrap(); + assert_eq!(w(t.replace(fd, o(2))), Ok(Some(1))); + assert_eq!(t.inheritable(fd), Ok(true)); + assert_eq!(w(t.close(fd)), Ok(Some(2))); + assert_eq!(w(t.replace(fd, o(3))), Err(Error::BadFd.into())); + } + + /// An endowment goes at its own number or nowhere: one whose number is + /// open already is refused, and the table is unchanged. + #[test] + fn an_endowment_is_installed_at_its_number_or_refused() { + let mut t = T::new(); + assert_eq!(t.install_at(3, o(1), true), Ok(())); + assert_eq!(t.inheritable(3), Ok(true)); + assert_eq!(t.install_at(3, o(2), true), Err(Error::BadFd.into())); + assert_eq!(t.install_at(8, o(2), true), Err(Error::BadFd.into())); + assert_eq!(t.install_at(-1, o(2), true), Err(Error::BadFd.into())); + assert_eq!(w(t.close(3)), Ok(Some(1))); + assert_eq!(t.install(o(3), false, 0), Ok(0), "a refusal took a number or an object"); + } + + /// A descriptor naming no object is a broken table, which the table + /// answers rather than panics on: the owner panics once its lock is let + /// go. + #[test] + fn an_inconsistent_table_is_answered_not_panicked_on() { + let mut t = T::new(); + t.fds[2] = 1; + assert_eq!(t.acquire(2).map(|_| ()), Err(Fault::Broken)); + assert_eq!(w(t.close(2)), Err(Fault::Broken)); + t.fds[2] = INHERITABLE; + assert_eq!(t.inheritable(2), Err(Fault::Broken)); + } +} diff --git a/sdk/toyos/src/net/stream.rs b/sdk/toyos/src/net/stream.rs index f5bd9b4311..340cdde9f9 100644 --- a/sdk/toyos/src/net/stream.rs +++ b/sdk/toyos/src/net/stream.rs @@ -101,11 +101,21 @@ type Tx = Sending; /// An end's two halves, each behind its own lock. struct Halves { - rx: Lock>, - tx: Lock>, + rx: Lock>, + tx: Lock>, } -static HALVES: [Halves; MAX_STREAMS] = [const { Halves { rx: Lock::new(None), tx: Lock::new(None) } }; MAX_STREAMS]; +/// A half, or none. `repr(u8)` puts `Empty` at zero, as it does +/// [`Slot::Free`], so the tables cost a program that makes no stream no +/// initialised data. +#[repr(u8)] +enum Kept { + Empty, + Held(T), +} + +static HALVES: [Halves; MAX_STREAMS] = + [const { Halves { rx: Lock::new(Kept::Empty), tx: Lock::new(Kept::Empty) } }; MAX_STREAMS]; static TABLE: Lock = Lock::new(Table::new()); @@ -125,6 +135,7 @@ struct End { } #[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[repr(u8)] enum Slot { Free, /// Reserved: by a connect or an accept that has not answered, or by an end @@ -213,8 +224,8 @@ impl Table { /// Lets go of an end no handle or call reaches: its halves, its bell and its /// region, whose mapping goes with its handle. fn free(slot: usize, end: End) { - *HALVES[slot].rx.lock() = None; - *HALVES[slot].tx.lock() = None; + *HALVES[slot].rx.lock() = Kept::Empty; + *HALVES[slot].tx.lock() = Kept::Empty; syscall::close(end.bell); syscall::close(end.region); TABLE.lock().unreserve(slot); @@ -322,11 +333,14 @@ fn call(handle: RawHandle, f: impl FnOnce(&Call) -> R) -> Result } /// A half reached through its slot's lock. -struct Half(&'static Lock>); +struct Half(&'static Lock>); impl session::Lock for Half { fn with(&self, f: impl FnOnce(&mut T) -> R) -> R { - f(self.0.lock().as_mut().expect("a live end has both halves")) + match &mut *self.0.lock() { + Kept::Held(half) => f(half), + Kept::Empty => panic!("a live end has both halves"), + } } } @@ -687,8 +701,8 @@ fn open( let (rx, tx) = session::client(words, LAYOUT); let lent = region.share().map_err(|_| NetError::Io)?; let (answer, bell, connection) = ask(NetstackConn::connect()?, lent)?; - *HALVES[slot].rx.lock() = Some(rx); - *HALVES[slot].tx.lock() = Some(tx); + *HALVES[slot].rx.lock() = Kept::Held(rx); + *HALVES[slot].tx.lock() = Kept::Held(tx); let (region, base) = region.into_raw(); let peer = answer.peer().unwrap_or(peer); let end = End { region, base: base as usize, bell, peer, local_port: answer.local_port(), read_shut: false, handles: 0, calls: 0 }; diff --git a/tests/guest/libc/poll_closed.c b/tests/guest/libc/poll_closed.c new file mode 100644 index 0000000000..020173bc57 --- /dev/null +++ b/tests/guest/libc/poll_closed.c @@ -0,0 +1,17 @@ +/* A descriptor that is not open answers POLLNVAL at once, as POSIX has it, + even with no timeout and nothing else ready. */ +#include + +#include "check.h" + +int main(void) { + int ends[2]; + CHECK(pipe(ends) == 0, "pipe"); + CHECK(close(ends[1]) == 0 && close(ends[0]) == 0, "close"); + struct pollfd fds[1] = {{.fd = ends[0], .events = POLLIN}}; + int ready = poll(fds, 1, -1); + CHECK(ready == 1, "poll answered %d", ready); + CHECK(fds[0].revents == POLLNVAL, "revents %#x", fds[0].revents); + printf("poll_closed: a closed descriptor answered POLLNVAL\n"); + return 0; +} diff --git a/tests/guest/rust-tests/src/bin/stream_handles.rs b/tests/guest/rust-tests/src/bin/stream_handles.rs index 6d504cb446..b07c83e4d6 100644 --- a/tests/guest/rust-tests/src/bin/stream_handles.rs +++ b/tests/guest/rust-tests/src/bin/stream_handles.rs @@ -11,8 +11,9 @@ //! - **A stream's handle is no file**: `File::from_raw_fd` of one panics with //! that refusal. //! - **No spawn moves one**: a stdio descriptor and one past them by -//! `inherit_handle`, `endow` and `provide` each answer the refusal, start no -//! child, and leave the stream reading and writing through the handle. +//! `inherit_handle`, `endow`, `provide` and a stdin the child inherits each +//! answer the refusal, start no child, and leave the stream reading and +//! writing through the handle. //! //! - **netstack's watch of a stream's connection** (`SERVER_WATCH`) is one //! the kernel takes, and is answered by the client's hang-up. @@ -80,7 +81,14 @@ fn shares_and_refuses_moves() { let status = child.wait().expect("wait the client"); assert_eq!( said, - ["shared", "stdio descriptor refused", "descriptor past stdio refused", "endow refused", "provide refused"], + [ + "shared", + "stdio descriptor refused", + "descriptor past stdio refused", + "endow refused", + "provide refused", + "inherited stdio refused", + ], "the client said otherwise", ); assert!(status.success(), "the client exited {status:?}"); @@ -227,28 +235,34 @@ fn client() -> ! { command.provide("stream", handle); }), ]; + let fd = taken.as_raw_fd(); let mut at = 16; + let mut answered = |path: &str, spawned: std::io::Result| match spawned { + Err(refused) + if refused.kind() == std::io::ErrorKind::InvalidInput + && refused.to_string() == Misuse::Moved(RawHandle(handle)).to_string() => + { + taken.write_all(b"x").unwrap_or_else(|e| panic!("{path}: the stream writes no more: {e}")); + reads(&mut taken, at, 1); + at += 1; + say(&format!("{path} refused")); + } + Err(other) => say(&format!("{path} answered {other}")), + Ok(mut child) => { + let _ = child.wait(); + say(&format!("{path} took the stream's handle")); + } + }; for (path, moving) in paths { let mut command = Command::new(SELF_PATH); command.arg("child").stdout(Stdio::null()); moving(&mut command, handle); - match command.spawn() { - Err(refused) - if refused.kind() == std::io::ErrorKind::InvalidInput - && refused.to_string() == Misuse::Moved(RawHandle(handle)).to_string() => - { - taken.write_all(b"x").unwrap_or_else(|e| panic!("{path}: the stream writes no more: {e}")); - reads(&mut taken, at, 1); - at += 1; - say(&format!("{path} refused")); - } - Err(other) => say(&format!("{path} answered {other}")), - Ok(mut child) => { - let _ = child.wait(); - say(&format!("{path} took the stream's handle")); - } - } + answered(path, command.spawn()); } + // Last, as every spawn inherits stdin from here: the stream as this + // process's own stdin, which the child would inherit. + toyos::fd::dup2(fd, 0, false).expect("the stream onto stdin"); + answered("inherited stdio", Command::new(SELF_PATH).arg("child").stdin(Stdio::inherit()).stdout(Stdio::null()).spawn()); std::process::exit(0) }