diff --git a/issues/a-childs-stdio-handle-is-not-the-one-command-named.md b/issues/a-childs-stdio-handle-is-not-the-one-command-named.md index 6345032b968..dc19015b336 100644 --- a/issues/a-childs-stdio-handle-is-not-the-one-command-named.md +++ b/issues/a-childs-stdio-handle-is-not-the-one-command-named.md @@ -7,7 +7,7 @@ opened: 2026-09-01 # A child's stdio slot is not the handle `Command` named for it Found while building a guest arm for -`sys/stdio/toyos.rs`'s error mapping, which needs a child whose own stdin or +`sdk/std/sys/stdio.rs`'s error mapping, which needs a child whose own stdin or stdout the parent has staged. It could not be staged, twice, and the second one is a capability statement rather than a plumbing one. diff --git a/issues/a-file-held-across-a-file-servers-restart-answers-gone.md b/issues/a-file-held-across-a-file-servers-restart-answers-gone.md index 3a281aff943..7a71059a866 100644 --- a/issues/a-file-held-across-a-file-servers-restart-answers-gone.md +++ b/issues/a-file-held-across-a-file-servers-restart-answers-gone.md @@ -8,7 +8,7 @@ opened: 2026-09-27 No volume records an object id, so nothing tells the held file from another put at its path since, and a handle held across a restart answers `Gone` -(`std`'s `sys/fs/toyos.rs`); `logd` loses `/log` for the boot if LOG's server +(`std`'s `sdk/std/sys/fs.rs`); `logd` loses `/log` for the boot if LOG's server restarts. **Exit**: DATA's entry carries an object id and a generation that nothing diff --git a/issues/a-held-launcher-that-will-not-open-is-a-direct-spawn.md b/issues/a-held-launcher-that-will-not-open-is-a-direct-spawn.md index 585e563bc5f..eecd2d6875e 100644 --- a/issues/a-held-launcher-that-will-not-open-is-a-direct-spawn.md +++ b/issues/a-held-launcher-that-will-not-open-is-a-direct-spawn.md @@ -6,8 +6,7 @@ opened: 2026-10-04 # A held launcher that will not open is a direct spawn -std's ToyOS `launch` (`library/std/src/sys/process/toyos.rs` in the `rust/` -fork) reads `toyos::endow::launcher().and_then(|held| held.open(LAUNCHER).ok())`: +std's ToyOS `launch` (`sdk/std/sys/process.rs`) reads `toyos::endow::launcher().and_then(|held| held.open(LAUNCHER).ok())`: a process that holds a launcher and whose open of it fails is treated as one that holds none, and its spawn of a declared program goes on as a direct spawn, without that program's row and with no error. The shape is the one `main` had diff --git a/issues/a-launch-too-large-for-one-frame-becomes-a-direct-spawn.md b/issues/a-launch-too-large-for-one-frame-becomes-a-direct-spawn.md index 69e65c618a0..b14f3ddd370 100644 --- a/issues/a-launch-too-large-for-one-frame-becomes-a-direct-spawn.md +++ b/issues/a-launch-too-large-for-one-frame-becomes-a-direct-spawn.md @@ -8,7 +8,7 @@ opened: 2026-10-02 `toyos::launch::launch` (`toyos/src/launch.rs`) answers `LaunchError::NotSent` when `Launch::encode` refuses the request, and std's `Command::launch` -(`rust/library/std/src/sys/process/toyos.rs`) answers `NotSent` with the direct +(`sdk/std/sys/process.rs`) answers `NotSent` with the direct spawn. `encode` refuses a request whose header, program, argv, environment, working directory and connector names do not fit `MAX_FRAME_LEN` (`toyos/src/ipc.rs`). Its other refusal, too many connectors or slots, std diff --git a/issues/a-link-on-a-kernel-mount-to-a-served-path-leads-nowhere.md b/issues/a-link-on-a-kernel-mount-to-a-served-path-leads-nowhere.md index 6b196019f93..fc3b7ee1c81 100644 --- a/issues/a-link-on-a-kernel-mount-to-a-served-path-leads-nowhere.md +++ b/issues/a-link-on-a-kernel-mount-to-a-served-path-leads-nowhere.md @@ -10,7 +10,7 @@ The kernel resolves a symlink on its own mounts — ROOT and `/tmp` — in its o tree (`kernel/src/vfs.rs`), and its tree holds nothing under `/apps`, `/config`, `/home`, `/state`, `/log` or `/boot` but ROOT's empty directories: those are file servers', reached through a directory capability -(`rust/library/std/src/sys/fs/toyos.rs`). So a link in `/tmp` whose target is +(`sdk/std/sys/fs.rs`). So a link in `/tmp` whose target is `/home/toy/notes` opens nothing (`NotFound`), where the same link on a served directory is followed — a file server hands an absolute target back to the client, which resolves it in its own table. diff --git a/issues/a-provided-name-cannot-reach-an-undeclared-child.md b/issues/a-provided-name-cannot-reach-an-undeclared-child.md index 4d020edc898..be85408ea49 100644 --- a/issues/a-provided-name-cannot-reach-an-undeclared-child.md +++ b/issues/a-provided-name-cannot-reach-an-undeclared-child.md @@ -20,8 +20,8 @@ base's whole entry set when the bit is set and refuses a bit it does not define (`kernel/src/syscall/ipc.rs`), and `endowment_denied`'s `the_base_plus_one_more_name` asserts both halves in a guest. -**What is left is the std fork, and only the std lane can do it.** -`rust/library/std/src/sys/process/toyos.rs`'s `spawn` endows the parent's +**What is left is std's ToyOS backend, and only the std lane can do it.** +`sdk/std/sys/process.rs`'s `spawn` endows the parent's namespace handle unchanged — `inherited_namespace` duplicates it and pushes it under `SVC_LABEL` — so a caller that transferred a connector to a program the manifest does **not** declare, the one case where the launcher answers diff --git a/issues/a-rust-std-executable-runs-no-init-array-so-a-linked-c-constructor-is-skipped.md b/issues/a-rust-std-executable-runs-no-init-array-so-a-linked-c-constructor-is-skipped.md index 704eea9cd81..000f1163854 100644 --- a/issues/a-rust-std-executable-runs-no-init-array-so-a-linked-c-constructor-is-skipped.md +++ b/issues/a-rust-std-executable-runs-no-init-array-so-a-linked-c-constructor-is-skipped.md @@ -6,7 +6,7 @@ opened: 2026-09-29 # A Rust std executable runs no `.init_array`, so a C constructor linked into it is skipped -`rust/library/std/src/sys/pal/toyos/mod.rs:93` starts a std binary at +`sdk/std/sys/pal/mod.rs:93` starts a std binary at `start_rust`, which calls `main` directly and never walks `.init_array` (the comment there: "exes don't run .init_array"). `userland/libc/src/lib.rs`'s `start_c` does walk it, but only `#[cfg(not(feature = "std-runtime"))]`: a diff --git a/issues/a-served-file-panics-when-asked-its-raw-fd.md b/issues/a-served-file-panics-when-asked-its-raw-fd.md index 65fb247e0b9..8708dc54268 100644 --- a/issues/a-served-file-panics-when-asked-its-raw-fd.md +++ b/issues/a-served-file-panics-when-asked-its-raw-fd.md @@ -6,14 +6,14 @@ opened: 2026-09-27 # A served file panics when asked its raw fd -`std::os::toyos::io::AsRawFd` for `std::fs::File` calls the fork's -`File::as_raw_fd` (`rust/library/std/src/sys/fs/toyos.rs`), which panics with +`std::os::toyos::io::AsRawFd` for `std::fs::File` calls the backend's +`File::as_raw_fd` (`sdk/std/sys/fs.rs`), which panics with "a file on a file server has no kernel handle" for every file under `/apps`, `/config`, `/home`, `/state`, `/log` and `/boot`. A crate that takes a file's fd — to lock it, map it or hand it to a C library — builds for ToyOS and panics there, which is what "existing Rust just works" rules out. -Owner: the std fork's ToyOS file layer. +Owner: std's ToyOS file layer. **Exit**: `as_raw_fd` on a served file answers without a panic — a kernel handle that reaches the file's server, as `as_child_stdio`'s pipe does for a diff --git a/issues/a-symbolic-link-on-tmp-displaces-its-name-and-lists-nowhere.md b/issues/a-symbolic-link-on-tmp-displaces-its-name-and-lists-nowhere.md index a1d363de19e..472c5241df0 100644 --- a/issues/a-symbolic-link-on-tmp-displaces-its-name-and-lists-nowhere.md +++ b/issues/a-symbolic-link-on-tmp-displaces-its-name-and-lists-nowhere.md @@ -14,7 +14,7 @@ a name that exists `EEXIST`, so libc's `symlink` refuses `ENOSYS` **Exit**: `symlink` is the file server's, not the kernel's. libc's `symlink` sends the request to the server of the directory that is to hold the link, as -std's does (`on_path` in `rust/library/std/src/sys/fs/toyos.rs`), with nothing +std's does (`on_path` in `sdk/std/sys/fs.rs`), with nothing asked first, and answers its `AlreadyExists` `EEXIST`. fsd makes the link in the one request that refuses a name that exists, and libc's `readdir` of that directory names the link, each asserted by a test. The kernel's `SYS_SYMLINK` diff --git a/issues/a-thread-std-detaches-holds-its-place-in-the-process-for-good.md b/issues/a-thread-std-detaches-holds-its-place-in-the-process-for-good.md index 7abe5d449f1..691f5e55e81 100644 --- a/issues/a-thread-std-detaches-holds-its-place-in-the-process-for-good.md +++ b/issues/a-thread-std-detaches-holds-its-place-in-the-process-for-good.md @@ -10,7 +10,7 @@ The kernel keeps an exited thread in its process's table until `SYS_THREAD_JOIN` collects it, and counts it against `toyos_abi::syscall::MAX_THREADS` until then (`kernel::proclife::spawn::Admit::Full`). std's `Thread` -(`rust/library/std/src/sys/thread/toyos.rs`) has no `Drop`, and a `JoinHandle` +(`sdk/std/sys/thread.rs`) has no `Drop`, and a `JoinHandle` dropped without `join` detaches, so nothing ever collects a thread std detached: a program that detaches threads over its life has `thread::spawn` refused once those that exited and those still running are diff --git a/issues/an-accept-that-never-reaches-netstack-strands-its-listener.md b/issues/an-accept-that-never-reaches-netstack-strands-its-listener.md index c4936fcc607..eeafbca55b2 100644 --- a/issues/an-accept-that-never-reaches-netstack-strands-its-listener.md +++ b/issues/an-accept-that-never-reaches-netstack-strands-its-listener.md @@ -6,7 +6,7 @@ opened: 2026-09-27 # An accept that never reaches netd strands its listener's owner -std's `TcpListener::accept` (`rust/library/std/src/sys/net/connection/toyos.rs`) +std's `TcpListener::accept` (`sdk/std/sys/net/connection.rs`) reads netd's wake byte first, and only then calls `toyos::net::tcp_accept`, which reaches netd (`NetdConn::connect`) and makes the data path (`DataPath::create`) before it sends the request. If either fails, or the send diff --git a/issues/c-programs-name-no-file-a-file-server-holds.md b/issues/c-programs-name-no-file-a-file-server-holds.md index 51a849f9a32..d23629fc81c 100644 --- a/issues/c-programs-name-no-file-a-file-server-holds.md +++ b/issues/c-programs-name-no-file-a-file-server-holds.md @@ -8,7 +8,7 @@ opened: 2026-09-27 `/apps`, `/config`, `/home`, `/state`, `/log` and `/boot` are served by `/system/bin/fsd` through directory capabilities in each program's namespace, -and std reaches them through `toyos::fs` (`rust/library/std/src/sys/fs/toyos.rs`). +and std reaches them through `toyos::fs` (`sdk/std/sys/fs.rs`). `userland/libc` does not: `open`, `stat`, `opendir` and every other path call in `userland/libc/src/posix_io.rs` and `userland/libc/src/stdio.rs` go to the kernel's `SYS_OPEN` family, and the kernel serves ROOT and `/tmp` only. So a C diff --git a/issues/create-new-on-a-kernel-path-is-not-exclusive.md b/issues/create-new-on-a-kernel-path-is-not-exclusive.md index ca2d3a76b13..faeaa6452c4 100644 --- a/issues/create-new-on-a-kernel-path-is-not-exclusive.md +++ b/issues/create-new-on-a-kernel-path-is-not-exclusive.md @@ -7,8 +7,8 @@ opened: 2026-09-27 # `create_new` on a kernel path is not exclusive std's `OpenOptions::create_new(true)` promises an open that fails with -`AlreadyExists` when the file is there. The std fork's `to_flags` -(`rust/library/std/src/sys/fs/toyos.rs`) turns `create_new` into the kernel's +`AlreadyExists` when the file is there. std's ToyOS `to_flags` +(`sdk/std/sys/fs.rs`) turns `create_new` into the kernel's plain `OpenFlags::CREATE`, and the kernel has no exclusive flag to turn it into, so on `/tmp` a second `create_new` of one path opens the file the first made and says nothing. A served path does not share it: the file protocol diff --git a/issues/dtv-capacity-is-a-workload-bound.md b/issues/dtv-capacity-is-a-workload-bound.md index e6faf69bcec..8869aac3bb8 100644 --- a/issues/dtv-capacity-is-a-workload-bound.md +++ b/issues/dtv-capacity-is-a-workload-bound.md @@ -18,7 +18,7 @@ scheduler designs establishing that a bound over a workload-set quantity is a defect rather than a policy, and this is one. **The refusal has no recoverable form.** The only caller is std's -`__tls_get_addr_slow` (`rust/library/std/src/sys/pal/toyos/tls.rs`), and its own +`__tls_get_addr_slow` (`sdk/std/sys/pal/tls.rs`), and its own comment says why it cannot pass the error on: *"`__tls_get_addr`'s ABI is an address and there is nobody to return an error to: a refusal added to `offset` is a pointer near the top of the address space that the caller would then diff --git a/issues/every-flush-of-a-served-file-syncs-its-whole-volume.md b/issues/every-flush-of-a-served-file-syncs-its-whole-volume.md index 0ba1081bd40..5a0625b8a6c 100644 --- a/issues/every-flush-of-a-served-file-syncs-its-whole-volume.md +++ b/issues/every-flush-of-a-served-file-syncs-its-whole-volume.md @@ -6,8 +6,8 @@ opened: 2026-09-27 # Every flush of a served file syncs its whole volume -The std fork's `File::flush` is `File::fsync` -(`rust/library/std/src/sys/fs/toyos.rs`), where every other platform's is a +std's ToyOS `File::flush` is `File::fsync` +(`sdk/std/sys/fs.rs`), where every other platform's is a no-op, and fsd answers `FSYNC` by syncing the volume, every open file's entry and every dirty block of the cache (`userland/fsd/src/main.rs`, `FSYNC`). So a `BufWriter` over a file on `/home` syncs all of DATA each time it flushes, and a diff --git a/issues/nothing-keys-the-hosted-rustc-on-stds-sources.md b/issues/nothing-keys-the-hosted-rustc-on-stds-sources.md new file mode 100644 index 00000000000..c14284e8b93 --- /dev/null +++ b/issues/nothing-keys-the-hosted-rustc-on-stds-sources.md @@ -0,0 +1,32 @@ +--- +status: open +kind: tooling +opened: 2026-10-07 +--- + +# Nothing keys the hosted rustc on std's sources + +Nothing keys the hosted rustc on std's sources. `src/toolchain.rs`'s +`hosted_rustc_owed` reads a stamp and whether `bin/rustc` is there, and no std +source: an edit to `sdk/std`, or to the fork's `library/`, leaves a built +hosted rustc standing, and `src/build.rs`'s `collect_hosted_rustc` ships its +`lib/*.so`, the `libstd-*.so` that rustc itself runs on among them, beside the +rlibs of the build's own sysroot. The compiler's key, which is what forgets a +hosted rustc, reads the fork's `compiler/`, `src/tools`, `src/stage0` and +`Cargo.lock` and nothing of `library/`. + +It is also unmeasured since std's ToyOS backend left the fork for `sdk/std`: +no hosted rustc has been built whose std reaches the backend through the +fork's `#[path]` arms. Only the primary checkout builds one, in its own +`rust/` (`issues/a-worktree-cannot-build-a-hosted-rustc-of-its-own.md`), so +the branch that moved the backend could not; no tracked config sets +`hosted-rustc = true`, and the primary held no hosted `stage2` when the move +landed. By reading, bootstrap compiles `library/std` in place and the arms +resolve to the primary's `sdk/std`. + +Owner: the build system (`src/toolchain.rs`). + +**Exit:** on the primary at a `main` that carries the move, a build whose +config asks for the hosted rustc exits 0, and the `libstd-*.so` under +`rust/build/x86_64-unknown-toyos/stage2/lib` carries `sdk/std/sys/` paths and +no `sys/pal/toyos`. diff --git a/issues/nothing-refuses-a-std-that-read-a-worktree-file-its-key-does-not-name.md b/issues/nothing-refuses-a-std-that-read-a-worktree-file-its-key-does-not-name.md new file mode 100644 index 00000000000..7a6083d33d2 --- /dev/null +++ b/issues/nothing-refuses-a-std-that-read-a-worktree-file-its-key-does-not-name.md @@ -0,0 +1,23 @@ +--- +status: open +kind: tooling +opened: 2026-10-07 +--- + +# Nothing refuses a std that read a worktree file its key does not name + +A sysroot's key reads the trees `src/sysroot.rs`'s `SYSROOT_SOURCES` lists, +and nothing holds that list against what std's build read. With `sdk/std` +taken off it, an edit to `sdk/std/sys/pal/mod.rs` that makes every program +exit one higher left the key at `46c572c6e5176094`: `cargo run -- +--build-only --arch aarch64` finished in 2 s having built no std, and +`virt_readonly_copyout` passed on the std built before the edit. + +`toolchain::assert_std_built_from` reads std's dep-info and decides only +whether its `toyos-abi` and `toyos` sources are this worktree's. +`assert_std_reads_no_worktree` holds the freestanding libraries to reading +nothing of the worktree; ToyOS's std has no check of what it may read there. + +**Exit:** a build of ToyOS's std whose dep-info names a file of the worktree +that is outside its fork and outside every tree the key reads is refused by +name, and a test builds that case. diff --git a/issues/os-toyos-io-traits-keep-a-posix-name.md b/issues/os-toyos-io-traits-keep-a-posix-name.md index df9885c522c..a4694fe06f0 100644 --- a/issues/os-toyos-io-traits-keep-a-posix-name.md +++ b/issues/os-toyos-io-traits-keep-a-posix-name.md @@ -7,12 +7,12 @@ opened: 2026-08-24 # `std::os::toyos::io` names its raw-handle traits `AsRawFd` and `FromRawFd` `os::toyos::*` is ToyOS's own extension API and speaks ToyOS, and "fds belong -only in libc jargon" (owner, 2026-08-19). The `rust/` fork's -`library/std/src/os/toyos/io.rs` re-exports `std::os::fd`, so +only in libc jargon" (owner, 2026-08-19). +`sdk/std/os/io.rs` re-exports `std::os::fd`, so `std::os::toyos::io::{AsRawFd, FromRawFd}` still speak POSIX. `tests/toyos-rust-tests/src/bin/std_fs.rs` is their one caller in this repository. -**Exit**: the next time the trait is touched in the fork, `AsRawFd` and +**Exit**: the next time the trait is touched, `AsRawFd` and `FromRawFd` in `std::os::toyos::io` are renamed to `os::toyos`'s own word for a raw handle, and `std_fs.rs` uses the new names (owner, 2026-09-30). diff --git a/issues/remove-dir-all-empties-a-directory-and-leaves-it.md b/issues/remove-dir-all-empties-a-directory-and-leaves-it.md index ab65c149dca..56a3ab7cdd4 100644 --- a/issues/remove-dir-all-empties-a-directory-and-leaves-it.md +++ b/issues/remove-dir-all-empties-a-directory-and-leaves-it.md @@ -6,7 +6,7 @@ opened: 2026-09-05 # `std::fs::remove_dir_all` empties a directory and never removes it -`rust/library/std/src/sys/fs/toyos.rs`'s `remove_dir_all` walks the directory, +`sdk/std/sys/fs.rs`'s `remove_dir_all` walks the directory, unlinks every file and recurses into every subdirectory, and returns `Ok(())` without ever calling `rmdir` on anything — its own path included. Every directory in the tree it walked survives, empty. @@ -35,9 +35,9 @@ the same hole. ## Exit condition `remove_dir_all` removes the directory it was given and every directory under -it. Closes when that lands in the fork and `userland/pkg`'s own `remove_tree` +it. Closes when that lands and `userland/pkg`'s own `remove_tree` is deleted with it — that walk exists only because this one does not finish. -The fix is one `rmdir(path)` after the loop, and it is in the sysroot's fourth -source (`rust/library`), so it lands on its own branch with the machine's +The fix is one `rmdir(path)` after the loop, and it is in a sysroot +source (`sdk/std`), so it lands on its own branch with the machine's sysroot claim. diff --git a/issues/std-and-libc-allocate-under-a-spinlock-with-no-futex.md b/issues/std-and-libc-allocate-under-a-spinlock-with-no-futex.md index 20566b76e3b..df44305cd57 100644 --- a/issues/std-and-libc-allocate-under-a-spinlock-with-no-futex.md +++ b/issues/std-and-libc-allocate-under-a-spinlock-with-no-futex.md @@ -10,8 +10,7 @@ Every allocation a ToyOS program makes goes through one `dlmalloc` behind a process-wide `AtomicI32` that a waiter takes with `swap(1, Acquire)` in a `spin_loop()` loop, never sleeping on a futex: -- std's: `library/std/src/sys/alloc/toyos.rs` in the `rust/` fork at - `a0d44493`, lines 57–72 (`LOCKED`, `lock`, `DropLock`), taken by `alloc`, +- std's: `sdk/std/sys/alloc.rs`, lines 57–72 (`LOCKED`, `lock`, `DropLock`), taken by `alloc`, `alloc_zeroed`, `dealloc` and `realloc`, lines 74–96. - libc's, in a program linked without std: `userland/libc/src/lib.rs`, lines 174–188, taken by `LibcAllocator`'s `alloc`, `dealloc` and `realloc`, lines @@ -33,7 +32,7 @@ than one thread, among them `userland/sshserver`, `userland/supervisor` and The futex both need is there: libc's `futex_lock` and `futex_unlock` (`userland/libc/src/pthread.rs`, lines 116–131), and std's `sync::Mutex`, which is the futex mutex on ToyOS (`library/std/src/sys/sync/mutex/mod.rs`, over -`library/std/src/sys/pal/toyos/futex.rs`). +`sdk/std/sys/pal/futex.rs`). Owner track: `issues/toyos-has-its-own-allocator.md`, whose std front replaces std's allocator; it does not rule whether this lock is fixed on diff --git a/issues/std-and-libc-drop-the-answer-thread-join-gives.md b/issues/std-and-libc-drop-the-answer-thread-join-gives.md index 2ab35f48043..a592ba0f790 100644 --- a/issues/std-and-libc-drop-the-answer-thread-join-gives.md +++ b/issues/std-and-libc-drop-the-answer-thread-join-gives.md @@ -6,7 +6,7 @@ opened: 2026-09-27 # std and libc drop the answer `thread_join` gives -`rust/library/std/src/sys/thread/toyos.rs`'s `Thread::join` and +`sdk/std/sys/thread.rs`'s `Thread::join` and `userland/libc/src/pthread.rs`'s `pthread_join` call `toyos_abi::syscall::thread_join` and discard what it returns. A join the kernel refuses — `NotFound` for a tid it never had or already collected, diff --git a/issues/std-answers-every-spawn-refusal-but-notfound-as-other.md b/issues/std-answers-every-spawn-refusal-but-notfound-as-other.md index d6ccb5f4afa..24176ba1264 100644 --- a/issues/std-answers-every-spawn-refusal-but-notfound-as-other.md +++ b/issues/std-answers-every-spawn-refusal-but-notfound-as-other.md @@ -6,8 +6,8 @@ opened: 2026-09-25 # std answers every spawn refusal but `NotFound` as `Other` -The std fork's direct spawn (`rust/library/std/src/sys/process/toyos.rs`, the -`spawned.map_err` in `Command::spawn`, at fork `3f0bda148507`) maps +std's direct spawn (`sdk/std/sys/process.rs`, the +`spawned.map_err` in `Command::spawn`) maps `SyscallError::NotFound` to `io::ErrorKind::NotFound` and every other `SyscallError` to `io::ErrorKind::Other`, and keeps no raw code. So a `Command::current_dir` the kernel refuses as not absolute — `InvalidArgument` diff --git a/issues/std-calloc-overflow-writes-past-a-fifteen-byte-block.md b/issues/std-calloc-overflow-writes-past-a-fifteen-byte-block.md index e32761fff33..42c2c262584 100644 --- a/issues/std-calloc-overflow-writes-past-a-fifteen-byte-block.md +++ b/issues/std-calloc-overflow-writes-past-a-fifteen-byte-block.md @@ -6,7 +6,7 @@ opened: 2026-09-26 # std's C `calloc` answers an overflowing request with a pointer past its block -`library/std/src/sys/pal/toyos/mod.rs`'s `c_allocator` is the `malloc`, +`sdk/std/sys/pal/mod.rs`'s `c_allocator` is the `malloc`, `calloc`, `free` and `realloc` of every userland program: std defines them for the Rust crates that call C's allocator, and `/system/bin/doom`'s C gets them too, because `userland/libc` defines none of the four when it is built diff --git a/issues/std-file-lock-answers-ok-and-locks-nothing.md b/issues/std-file-lock-answers-ok-and-locks-nothing.md index 2542a6aa16d..d5af2a8ebda 100644 --- a/issues/std-file-lock-answers-ok-and-locks-nothing.md +++ b/issues/std-file-lock-answers-ok-and-locks-nothing.md @@ -6,7 +6,7 @@ opened: 2026-10-03 # std's `File::lock` answers `Ok` and locks nothing -The ToyOS file pal in the std fork (`library/std/src/sys/fs/toyos.rs`) answers +std's ToyOS file pal (`sdk/std/sys/fs.rs`) answers `Ok(())` from `File::lock`, `lock_shared`, `try_lock`, `try_lock_shared` and `unlock` and takes no lock. Two processes that each ask for the exclusive lock on one file are both told they hold it, and nothing in the kernel ABI or the diff --git a/issues/std-leaks-a-thread-stack-per-spawn.md b/issues/std-leaks-a-thread-stack-per-spawn.md index 69748212156..a44bd39ee44 100644 --- a/issues/std-leaks-a-thread-stack-per-spawn.md +++ b/issues/std-leaks-a-thread-stack-per-spawn.md @@ -6,7 +6,7 @@ opened: 2026-07-30 # Std leaks a whole thread stack on every `thread::spawn` -`rust/library/std/src/sys/thread/toyos.rs` allocates the stack with +`sdk/std/sys/thread.rs` allocates the stack with `alloc::alloc` (2 MiB minimum), hands its base to `SYS_THREAD_SPAWN`, and never records the pointer. `Thread` holds only a tid and has no `Drop`, `join` does not free it, and the trampoline cannot — it is standing on it. So every spawned diff --git a/issues/std-lookup-host-answers-no-address-as-other.md b/issues/std-lookup-host-answers-no-address-as-other.md index 6879799fced..7cbebea3905 100644 --- a/issues/std-lookup-host-answers-no-address-as-other.md +++ b/issues/std-lookup-host-answers-no-address-as-other.md @@ -6,7 +6,7 @@ opened: 2026-09-26 # std's lookup_host answers a name with no address as `Other` -The std fork's `lookup_host` (`library/std/src/sys/net/connection/toyos.rs`) +std's `lookup_host` (`sdk/std/sys/net/connection.rs`) answers netd's empty answer with `io::ErrorKind::Other` and the message `DNS lookup failed: no results`, and every netd error that is not one of six kinds with `Other` and `netd error`. A program asking for a name therefore diff --git a/issues/std-maps-a-device-error-to-other-not-uncategorized.md b/issues/std-maps-a-device-error-to-other-not-uncategorized.md index 6464d1c5dc7..653a40240d4 100644 --- a/issues/std-maps-a-device-error-to-other-not-uncategorized.md +++ b/issues/std-maps-a-device-error-to-other-not-uncategorized.md @@ -4,9 +4,9 @@ kind: defect opened: 2026-09-01 --- -# The std fork answers `Other` for a device error, where upstream says `Uncategorized` +# std on ToyOS answers `Other` for a device error, where upstream says `Uncategorized` -`rust/library/std/src/sys/pal/toyos/mod.rs`'s one +`sdk/std/sys/pal/mod.rs`'s one `SyscallError -> ErrorKind` map sends `SyscallError::Io` to `io::ErrorKind::Other`. Every other platform in the fork spells that `Uncategorized` — `sys/io/error/unix.rs:189`, `hermit.rs:29`, `uefi.rs:53`, @@ -26,7 +26,7 @@ header and the code it describes is worse than a non-idiomatic arm. ## Exit condition One change that moves all four together: `Io => ErrorKind::Uncategorized` in -the fork's map, `userland/logd/src/policy.rs`'s header reworded to name the new +that map, `userland/logd/src/policy.rs`'s header reworded to name the new spelling, its test's constructed kind moved with it, and `boot_volume_metadata_error` in `tests/common/volumes.rs`, which requires the guest to print `kind=Other` for a boot volume that refused every read — a fourth diff --git a/issues/std-names-its-toyos-backend-by-a-path-out-of-the-fork.md b/issues/std-names-its-toyos-backend-by-a-path-out-of-the-fork.md new file mode 100644 index 00000000000..6b85c9d06b8 --- /dev/null +++ b/issues/std-names-its-toyos-backend-by-a-path-out-of-the-fork.md @@ -0,0 +1,27 @@ +--- +status: open +kind: defect +opened: 2026-10-07 +--- + +# std names its ToyOS backend by a path out of the fork + +Fourteen arms under `rust/library/std/src` select ToyOS's file with a +`#[path]` that climbs out of the fork into `sdk/std/`, and two of those files, +`sdk/std/os/ffi.rs` and `sdk/std/sys/pal/mod.rs`, name one back in it +(`os/unix/ffi/os_str.rs`, `sys/pal/unsupported/common.rs`). The owner chose +the mechanism when he ruled the backend out of the fork; what it leaves is +this: + +- the fork knows where this repository keeps the backend, and the backend + where the fork keeps two of its files, so moving `sdk/std`, `rust/` or + either of those two breaks every fork commit pinned before the move, as + `issues/std-names-the-sdk-crates-by-path.md` says of the two crates; +- the fourteen lines are not upstream-mergeable as written + (`.claude/agents/implementer.md`, "A fork"); +- a panic raised in the backend names its file through the arm that selected + it: `library/std/src/sys/time/../../../../../../sdk/std/sys/time.rs`, read + out of the `libstd` the move built. + +**Exit:** no `#[path]` under `rust/library` names a file outside the fork, and +none under `sdk/std` names one inside it. diff --git a/issues/std-reads-a-query-modules-byte-count-as-a-module-count.md b/issues/std-reads-a-query-modules-byte-count-as-a-module-count.md index 206991a2b55..cae37e3ea35 100644 --- a/issues/std-reads-a-query-modules-byte-count-as-a-module-count.md +++ b/issues/std-reads-a-query-modules-byte-count-as-a-module-count.md @@ -6,11 +6,12 @@ opened: 2026-09-29 # std reads a `SYS_QUERY_MODULES` byte count as a module count -Two readers in the std fork take `query_modules`'s `Ok(n)` as a record count +Two readers, one in std's ToyOS backend and one in the fork's backtrace crate, +take `query_modules`'s `Ok(n)` as a record count and read `size_of::()`-byte records from offset 0 until one would pass the end of their 4096-byte buffer: -- the unwinder, `rust/library/std/src/sys/pal/toyos/mod.rs`, +- the unwinder, `sdk/std/sys/pal/mod.rs`, `eh_frame::load_modules`, reads each as `base`/`text_end`/`eh_frame_hdr`; - the backtrace crate, `rust/library/backtrace/src/symbolize/gimli/libs_toyos.rs`, `native_libraries`, makes a `Library` of each whose one segment is diff --git a/issues/std-says-a-launch-moves-its-handles-to-the-launcher-even-when-the-move-is-refused.md b/issues/std-says-a-launch-moves-its-handles-to-the-launcher-even-when-the-move-is-refused.md index 86c96dde1ea..819cd57acbb 100644 --- a/issues/std-says-a-launch-moves-its-handles-to-the-launcher-even-when-the-move-is-refused.md +++ b/issues/std-says-a-launch-moves-its-handles-to-the-launcher-even-when-the-move-is-refused.md @@ -9,13 +9,12 @@ opened: 2026-10-05 `toyos::launch::launch` consumes every handle a `Launch` names: the kernel moves them to the launcher, or refuses the move and `Connection::send_handles` closes them in the caller (`toyos/src/ipc.rs`, `toyos/src/launch.rs`). The -std fork's prose about the same handles says something else, at the gitlink's -commit `7fa3f566`: +prose of std's ToyOS backend about the same handles says something else: -- `rust/library/std/src/sys/process/toyos.rs`, the comment over the match on +- `sdk/std/sys/process.rs`, the comment over the match on `launch`'s answer: "The launcher releases what it took." On a refused move the launcher took nothing; this process closed them. -- `rust/library/std/src/os/toyos/process.rs`, `CommandExt::provide` (and +- `sdk/std/os/process.rs`, `CommandExt::provide` (and `endow`, whose rule it cites): the handle leaves the parent "after a successful spawn". A `provide`d connector also leaves the parent on a spawn that failed after the send — a refused move, `Refused`, `Gone`, a lost @@ -26,10 +25,10 @@ std's code is right on every arm: it releases its duplicates only on `LaunchError::NotSent`, which still means nothing was consumed. Only the prose is false. -**Owner:** the next std fork commit that touches -`library/std/src/sys/process/toyos.rs` or `library/std/src/os/toyos/process.rs`. +**Owner:** the next commit that touches `sdk/std/sys/process.rs` or +`sdk/std/os/process.rs`. -**Exit:** a fork commit, landed with its gitlink bump, in which `provide`'s +**Exit:** a commit in which `provide`'s doc says the connector leaves the parent once the launch is sent, whatever the spawn answers, and the comment over `launch`'s answer names the close on a refused move. diff --git a/issues/std-says-this-machine-has-one-cpu.md b/issues/std-says-this-machine-has-one-cpu.md index 3d5ac50ffe4..f75a584c975 100644 --- a/issues/std-says-this-machine-has-one-cpu.md +++ b/issues/std-says-this-machine-has-one-cpu.md @@ -6,7 +6,7 @@ opened: 2026-08-23 # `std::thread::available_parallelism` answers 1 on every ToyOS machine -`rust/library/std/src/sys/thread/toyos.rs:66` returns a hardcoded +`sdk/std/sys/thread.rs:66` returns a hardcoded `NonZero::new_unchecked(1)`, and the comment above it says why: "ToyOS runs on QEMU with a known number of CPUs, but we don't expose a syscall for this yet." That is no longer true. `SYS_CPU_COUNT` exists, `toyos_abi::syscall::cpu_count` diff --git a/issues/std-stat-conflates-io-with-notfound.md b/issues/std-stat-conflates-io-with-notfound.md index 2ff292e605c..1a4a9e23ac7 100644 --- a/issues/std-stat-conflates-io-with-notfound.md +++ b/issues/std-stat-conflates-io-with-notfound.md @@ -8,12 +8,10 @@ opened: 2026-08-08 Filed out of the `vfs::FileSystem` error-channel entry when that closed. -`rust/library/std/src/sys/fs/toyos.rs`'s `stat` discards `syscall::open`'s error +`sdk/std/sys/fs.rs`'s `stat` discards `syscall::open`'s error and returns a hardcoded `io::ErrorKind::NotFound`, so `fs::metadata` on a volume that would not answer reports "no such file" — the exact conflation the kernel half of that task removed. `File::open`, `fs::read` and `fs::read_dir` propagate correctly; it is `stat`/`lstat` alone. -Three lines in the std fork. It cannot be made from a linked worktree, because -`rust/` is a stub there — it belongs to whoever is working in the primary -checkout. +Three lines in that file. diff --git a/issues/std-udp-socket-ignores-its-timeouts.md b/issues/std-udp-socket-ignores-its-timeouts.md index a1523cc1c57..562aa97d1e8 100644 --- a/issues/std-udp-socket-ignores-its-timeouts.md +++ b/issues/std-udp-socket-ignores-its-timeouts.md @@ -6,7 +6,7 @@ opened: 2026-09-26 # std's UdpSocket ignores its timeouts -The ToyOS net pal in the std fork (`library/std/src/sys/net/connection/toyos.rs`) +std's ToyOS net pal (`sdk/std/sys/net/connection.rs`) stores `UdpSocket::set_read_timeout` and `set_write_timeout` and answers them back, but `recv_from` and `send_to` never read either: a `recv_from` with a read timeout set waits for a datagram forever. A Rust program that bounds a diff --git a/issues/stds-toyos-backend-keeps-assembly-outside-an-architecture-module.md b/issues/stds-toyos-backend-keeps-assembly-outside-an-architecture-module.md new file mode 100644 index 00000000000..2016a9adbc1 --- /dev/null +++ b/issues/stds-toyos-backend-keeps-assembly-outside-an-architecture-module.md @@ -0,0 +1,19 @@ +--- +status: open +kind: defect +opened: 2026-10-07 +--- + +# std's ToyOS backend keeps assembly outside an architecture's module + +`sdk/std/sys/pal/mod.rs` holds both architectures' naked `_start`, each under +its own `cfg(target_arch)`, and `sdk/std/sys/pal/tls.rs` holds x86-64's naked +`__tls_get_addr` and its slow path the same way. `.claude/agents/reviewer.md` +("Fit") keeps assembly, a naked function, a `core::arch` path and +`target_arch` in an architecture's own module and its selector. The rule did +not reach these files in the `rust` fork; they came under it when the backend +moved into this tree, and the move changed none of their code, so that it +could be checked as a move. + +**Exit:** `target_arch`, `naked` and `core::arch` appear under `sdk/std` only +in an architecture's own module and its selector. diff --git a/issues/stds-toyos-files-carry-lines-rustfmt-would-rewrap.md b/issues/stds-toyos-files-carry-lines-rustfmt-would-rewrap.md new file mode 100644 index 00000000000..9a85137acc0 --- /dev/null +++ b/issues/stds-toyos-files-carry-lines-rustfmt-would-rewrap.md @@ -0,0 +1,28 @@ +--- +status: open +kind: defect +opened: 2026-10-03 +--- + +# std's ToyOS files carry lines rustfmt would rewrap + +Under `rust/rustfmt.toml`, the rustfmt `rust/src/stage0` pins (`1.9.0-nightly +77cf889bc1`, which applies the config's `group_imports` and +`imports_granularity`) rewrites seven places in four of std's ToyOS files, +measured at `rust` fork commit `a0d44493347`, which held them under +`library/std`, each file checked on its own text (`rustfmt --check +--config-path rustfmt.toml --edition 2024`). Stable 1.9.0, which skips those +two options, rewrites the same seven: + +| file | places | +|---|---| +| `sdk/std/os/fs.rs` | 1 | +| `sdk/std/sys/fs.rs` | 2 | +| `sdk/std/sys/pal/mod.rs` | 2 | +| `sdk/std/sys/process.rs` | 2: the three `setup_slot` calls and the `Outcome::Gone` arm | + +std's ToyOS backend is written as upstream would take it, and upstream formats +`library/std` with that configuration. + +**Exit**: the rustfmt `rust/src/stage0` pins, under `rust/rustfmt.toml` with +its two import options, changes no file under `sdk/std`. diff --git a/issues/the-c-allocators-set-no-errno-where-posix-has-them.md b/issues/the-c-allocators-set-no-errno-where-posix-has-them.md index 91bdbd61603..e4b21304bce 100644 --- a/issues/the-c-allocators-set-no-errno-where-posix-has-them.md +++ b/issues/the-c-allocators-set-no-errno-where-posix-has-them.md @@ -11,7 +11,7 @@ and `aligned_alloc` set `ENOMEM` when they answer null for want of memory, and `aligned_alloc` set `EINVAL` for an alignment it does not support. In a Rust program, C code allocates through std's C allocator -(`rust/library/std/src/sys/pal/toyos/mod.rs`, `c_allocator`), which sets +(`sdk/std/sys/pal/mod.rs`, `c_allocator`), which sets `errno` in none of the four. `errno` is libc's (`userland/libc/src/errno.rs`), and std names no libc symbol: setting it from std puts `__errno_location` in every Rust program's link. diff --git a/issues/the-std-forks-toyos-files-carry-lines-rustfmt-would-rewrap.md b/issues/the-std-forks-toyos-files-carry-lines-rustfmt-would-rewrap.md deleted file mode 100644 index b0469c600af..00000000000 --- a/issues/the-std-forks-toyos-files-carry-lines-rustfmt-would-rewrap.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-10-03 ---- - -# The std fork's ToyOS files carry lines rustfmt would rewrap - -Under `rust/rustfmt.toml`, the rustfmt `rust/src/stage0` pins (`1.9.0-nightly -77cf889bc1`, which applies the config's `group_imports` and -`imports_granularity`) rewrites seven places in four of the `rust` fork's -ToyOS files under `library/std` at `a0d44493347`, each file checked on its own -text (`rustfmt --check --config-path rustfmt.toml --edition 2024`). Stable -1.9.0, which skips those two options, rewrites the same seven: - -| file | places | -|---|---| -| `src/os/toyos/fs.rs` | 1 | -| `src/sys/fs/toyos.rs` | 2 | -| `src/sys/pal/toyos/mod.rs` | 2 | -| `src/sys/process/toyos.rs` | 2: the three `setup_slot` calls and the `Outcome::Gone` arm | - -A fork change is written as upstream would take it, and upstream formats -`library/std` with that configuration. - -**Owner**: the `rust` fork. - -**Exit**: the rustfmt `rust/src/stage0` pins, under `rust/rustfmt.toml` with -its two import options, changes no ToyOS file under `library/std`. diff --git a/issues/the-t14s-record-keeps-three-rows-of-a-boot-nothing-stages.md b/issues/the-t14s-record-keeps-three-rows-of-a-boot-nothing-stages.md new file mode 100644 index 00000000000..e86ea963ba1 --- /dev/null +++ b/issues/the-t14s-record-keeps-three-rows-of-a-boot-nothing-stages.md @@ -0,0 +1,21 @@ +--- +status: open +kind: tooling +opened: 2026-10-07 +--- + +# The T14's record keeps three rows of a boot nothing stages + +`tests/metal/lenovo-20w0003amz.toml` records `boot.testcases-window.complete_ms`, +`.panel_max_us` and `.panel_us`, and no registration names a +`testcases-window` boot: `git grep testcases-window` finds those three lines +and nothing else. A whole run of the metal profile says so on every run and +stays green ("3 recorded number(s) this run measured nothing for"), so a row +whose boot is gone is never removed, and the same line would not red on a boot +that silently stopped being staged. + +Owner: the metal suite (`tests/common/metal.rs`, `src/metaltimings.rs`). + +**Exit:** the three rows are deleted, and a whole run of the profile that +measures nothing for a recorded number is red by name; a filtered run, which +stages a part of the profile, still is not. diff --git a/issues/the-tree-says-who-uses-each-thing.md b/issues/the-tree-says-who-uses-each-thing.md index 5304d6bfacf..8bb1997f879 100644 --- a/issues/the-tree-says-who-uses-each-thing.md +++ b/issues/the-tree-says-who-uses-each-thing.md @@ -128,7 +128,7 @@ apps/ CLAUDE.md (sdk/ and registries only; Linux under Wayland, macOS, Windo calc editor files paint snake doom/ (doomgeneric/ DOOM1.WAD soundfont licences); toyfetch when built sdk/ CLAUDE.md (identity, sysroot, publication; std links abi and toyos) - abi/ toyos/ keymap/ font/ window/ filepicker/ libc/ (arch/) + abi/ toyos/ std/ keymap/ font/ window/ filepicker/ libc/ (arch/) lib/ acpi bcachefs blackbox blockhold blockring bootmap elf elide fat32 gpt hda i219 inspect logstream manifest osrelease quiesce rootimage swap symbols tco tmpdir tsc untrusted update userbound wallclock xhci @@ -160,6 +160,9 @@ Where this differs from the tree the owner adopted: the build, `libc` and the supervisor use it (rule step 5). - `lib/tsc` is `toyos-tsc`, which #721 added after the design; the kernel and the loader use it (rule step 5). +- `sdk/std/` is std's ToyOS backend, which left the `rust` fork after the + design (the owner: "MOVE IT"); the fork's `library/std`, outside this + repository, compiles it by `#[path]` (rule step 3). ## Stages diff --git a/issues/toyos-has-its-own-allocator.md b/issues/toyos-has-its-own-allocator.md index ff3d26c6237..dc1b7c2998f 100644 --- a/issues/toyos-has-its-own-allocator.md +++ b/issues/toyos-has-its-own-allocator.md @@ -11,8 +11,7 @@ with two fronts. The kernel's front comes first: built host-first, and swapped in for the kernel's `dlmalloc` (`kernel/src/mm/alloc.rs`) after the three steps of `issues/toyos-beats-linuxs-latency-on-the-t14.md`. std's front, for programs, comes later, after it is measured against the -`dlmalloc` ToyOS's std allocates with (`library/std/src/sys/alloc/toyos.rs` -in the `rust/` fork). The 2 MiB heap-growth stall is fixed now, on +`dlmalloc` ToyOS's std allocates with (`sdk/std/sys/alloc.rs`). The 2 MiB heap-growth stall is fixed now, on `dlmalloc`, and waits for neither front. The stall: when the kernel heap grows, `dlmalloc` calls diff --git a/issues/toyos-runs-on-arm64.md b/issues/toyos-runs-on-arm64.md index 3e1b388235b..7257d925c4c 100644 --- a/issues/toyos-runs-on-arm64.md +++ b/issues/toyos-runs-on-arm64.md @@ -81,10 +81,10 @@ lines: a 16550 at a port, `serial.rs:28-39,157-165,389-396`). `sched/driver.rs:1019-1062`), `irq_census.rs` 19, `nmi_gate.rs` 16, `main.rs` 13, `drivers/serial.rs` 13, `hw.rs` 10, `blackbox.rs` 6, `iommu/vtd/table.rs` 6, `panic_console/mod.rs:1294` 1, `xhci/wait/mod.rs:36` 1. Userland plus -`toyos-abi`: 54 lines (`libc/memory.rs` 23, `toyos-abi/src/syscall.rs` 20). The -rust fork's std has two naked-asm sites: `_start` -(`rust/library/std/src/sys/pal/toyos/mod.rs:44`) and `__tls_get_addr` reading -`fs:[8]` (`pal/toyos/tls.rs:43`). +`toyos-abi`: 54 lines (`libc/memory.rs` 23, `toyos-abi/src/syscall.rs` 20). std's +ToyOS backend has two naked-asm sites: `_start` +(`sdk/std/sys/pal/mod.rs:44`) and `__tls_get_addr` reading +`fs:[8]` (`sdk/std/sys/pal/tls.rs:43`). **Coupling.** 45 non-arch kernel files reference `arch::`; 145 `crate::arch::` paths name about 100 distinct `module::symbol` names (an upper diff --git a/issues/two-std-fork-maps-still-answer-other-for-a-dead-peer.md b/issues/two-std-maps-still-answer-other-for-a-dead-peer.md similarity index 83% rename from issues/two-std-fork-maps-still-answer-other-for-a-dead-peer.md rename to issues/two-std-maps-still-answer-other-for-a-dead-peer.md index bfcf3cf4ba9..2cb5ebc916d 100644 --- a/issues/two-std-fork-maps-still-answer-other-for-a-dead-peer.md +++ b/issues/two-std-maps-still-answer-other-for-a-dead-peer.md @@ -7,13 +7,13 @@ opened: 2026-09-01 # A `TcpStream` write to a departed peer is still `ErrorKind::Other` Making a broken pipe reach a Rust caller as `BrokenPipe` put one exhaustive -`SyscallError -> ErrorKind` map in `rust/library/std/src/sys/pal/toyos/mod.rs` -and pointed `sys/pipe`, `sys/stdio` and `sys/fs` at it. Two maps in the same -fork were not moved and still carry a `_ => Other` arm, at fork commit -`719118153253`: +`SyscallError -> ErrorKind` map in `sdk/std/sys/pal/mod.rs` +and pointed `sys/pipe`, `sys/stdio` and `sys/fs` at it. Two maps beside it +were not moved and still carry a `_ => Other` arm; the line numbers are those +of `rust` fork commit `719118153253`, which held these files: ```rust -// library/std/src/sys/net/connection/toyos.rs:46 +// sdk/std/sys/net/connection.rs:46 fn syscall_err(e: SyscallError) -> io::Error { match e { SyscallError::WouldBlock => io::ErrorKind::WouldBlock.into(), @@ -23,7 +23,7 @@ fn syscall_err(e: SyscallError) -> io::Error { ``` ```rust -// library/std/src/sys/process/toyos.rs:272 +// sdk/std/sys/process.rs:272 let kind = match e { toyos_abi::syscall::SyscallError::NotFound => io::ErrorKind::NotFound, _ => io::ErrorKind::Other, @@ -31,7 +31,7 @@ let kind = match e { ``` The first is on a write path. `TcpStream::write` -(`library/std/src/sys/net/connection/toyos.rs:199`) ends in +(`sdk/std/sys/net/connection.rs:199`) ends in `syscall::write(self.raw_handle(), buf).map_err(syscall_err)` at `:216`, and a netd socket's tx end is a pipe — so a Rust program writing to a socket whose peer has gone gets `ErrorKind::Other` — the same wrong word the pipe path was @@ -47,7 +47,7 @@ and in the pull request, not left to be rediscovered here. ## What closing it takes Pointing both at `sys::to_io_error` and deleting the local maps, as the three -already moved were. `sys/process/toyos.rs`'s is the spawn refusal and wants a +already moved were. `sdk/std/sys/process.rs`'s is the spawn refusal and wants a look at whether `NotFound` is doing work the shared map does not; the shared map answers `NotFound` for the same variant, so it is likely a plain deletion. diff --git a/issues/userlands-wall-clock-has-whole-seconds-and-an-mtime-has-nanoseconds.md b/issues/userlands-wall-clock-has-whole-seconds-and-an-mtime-has-nanoseconds.md index 81c046a3a2e..85b521a82ab 100644 --- a/issues/userlands-wall-clock-has-whole-seconds-and-an-mtime-has-nanoseconds.md +++ b/issues/userlands-wall-clock-has-whole-seconds-and-an-mtime-has-nanoseconds.md @@ -9,8 +9,7 @@ opened: 2026-09-28 The kernel stamps a file with `clock::mtime_now` (`kernel/src/clock.rs`), the RTC's second carried on by the counter, so a write inside a second carries the nanoseconds past it. Userland reads the wall clock only through -`SYS_CLOCK_EPOCH`, which answers whole seconds: std's `SystemTime::now` (the -fork's `library/std/src/sys/time/toyos.rs`) and libc's `clock_gettime(CLOCK_REALTIME)` +`SYS_CLOCK_EPOCH`, which answers whole seconds: std's `SystemTime::now` (`sdk/std/sys/time.rs`) and libc's `clock_gettime(CLOCK_REALTIME)` and `gettimeofday` (`userland/libc/src/time.rs`) all round down to the second. So a file written a moment ago reads as up to a second in the future against diff --git a/rust b/rust index 7fa3f566c28..cc9c8b1be68 160000 --- a/rust +++ b/rust @@ -1 +1 @@ -Subproject commit 7fa3f566c283e21a631e133e97b6934a38be7439 +Subproject commit cc9c8b1be686c6c01f70bda99e59fb743513923f diff --git a/sdk/std/os/ffi.rs b/sdk/std/os/ffi.rs new file mode 100644 index 00000000000..d1229bca7af --- /dev/null +++ b/sdk/std/os/ffi.rs @@ -0,0 +1,9 @@ +//! ToyOS-specific extensions to primitives in the [`std::ffi`] module +//! +//! [`std::ffi`]: crate::ffi + +#[path = "../../../rust/library/std/src/os/unix/ffi/os_str.rs"] +mod os_str; + +#[stable(feature = "toyos_ext", since = "1.0.0")] +pub use self::os_str::{OsStrExt, OsStringExt}; diff --git a/sdk/std/os/fs.rs b/sdk/std/os/fs.rs new file mode 100644 index 00000000000..f828c801f00 --- /dev/null +++ b/sdk/std/os/fs.rs @@ -0,0 +1,30 @@ +use crate::io; +use crate::path::Path; + +/// Creates a new symbolic link on the filesystem. +/// +/// The `link` path will be a symbolic link pointing to the `original` path. +#[stable(feature = "toyos_ext", since = "1.0.0")] +pub fn symlink, Q: AsRef>(original: P, link: Q) -> io::Result<()> { + crate::sys::fs::symlink(original.as_ref(), link.as_ref()) +} + +/// Resolve this process's files through `namespace`, a namespace handle it +/// owns and gives up here. +/// +/// For the one process no parent endows a namespace: the supervisor builds the machine's +/// directory capabilities itself. Refused, and the handle closed, when this +/// process already resolves through one. +/// +/// # Safety +/// +/// `namespace` must be a namespace handle the caller owns and uses nowhere +/// else from here on: it is closed with the process's resolution, as an +/// `OwnedFd` handed to `from_raw_fd` is. +#[stable(feature = "toyos_ext", since = "1.0.0")] +pub unsafe fn adopt_namespace(namespace: u32) -> io::Result<()> { + // SAFETY: the caller's contract above. + let ns = unsafe { toyos::namespace::Namespace::from_raw(toyos_abi::RawHandle(namespace)) }; + toyos::endow::adopt_namespace(ns) + .map_err(|_| io::const_error!(io::ErrorKind::AlreadyExists, "this process already has a namespace")) +} diff --git a/sdk/std/os/io.rs b/sdk/std/os/io.rs new file mode 100644 index 00000000000..3c740ec02ef --- /dev/null +++ b/sdk/std/os/io.rs @@ -0,0 +1,76 @@ +// Re-export standard fd traits +#[stable(feature = "toyos_ext", since = "1.0.0")] +pub use fd::*; + +use crate::os::fd; +use crate::sys::{AsInner, FromInner}; + +#[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() + } +} + +#[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 { + crate::fs::File::from_inner(crate::sys::fs::File::from_fd(toyos_abi::RawHandle(fd as u32))) + } +} + +#[stable(feature = "toyos_ext", since = "1.0.0")] +impl IntoRawFd for crate::fs::File { + #[inline] + fn into_raw_fd(self) -> RawFd { + self.as_inner().raw_fd() + } +} + +#[stable(feature = "toyos_ext", since = "1.0.0")] +impl AsRawFd for crate::net::TcpStream { + #[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::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) { + crate::sys::stdio::set_stdin_raw(raw); +} diff --git a/sdk/std/os/mod.rs b/sdk/std/os/mod.rs new file mode 100644 index 00000000000..e3a448655fc --- /dev/null +++ b/sdk/std/os/mod.rs @@ -0,0 +1,13 @@ +#![stable(feature = "toyos_ext", since = "1.0.0")] + +#[stable(feature = "toyos_ext", since = "1.0.0")] +pub mod ffi; + +#[stable(feature = "toyos_ext", since = "1.0.0")] +pub mod fs; + +#[stable(feature = "toyos_ext", since = "1.0.0")] +pub mod io; + +#[stable(feature = "toyos_ext", since = "1.0.0")] +pub mod process; diff --git a/sdk/std/os/process.rs b/sdk/std/os/process.rs new file mode 100644 index 00000000000..3eb0354132c --- /dev/null +++ b/sdk/std/os/process.rs @@ -0,0 +1,163 @@ +use crate::sys::{AsInner, AsInnerMut, FromInner, IntoInner, process as imp}; + +/// Create a `Stdio` that pipes through a tty-typed handle. +/// +/// Like `Stdio::piped()`, but the pipe endpoints are marked as tty so the +/// child process gets canonical mode (echo + line editing) on its stdin. +#[stable(feature = "toyos_ext", since = "1.0.0")] +pub fn tty_piped() -> crate::process::Stdio { + crate::process::Stdio::from_inner(imp::Stdio::MakeTtyPipe) +} + +/// 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. + #[stable(feature = "toyos_ext", since = "1.0.0")] + fn inherit_handle(&mut self, child_slot: u32, parent_handle: u32) -> &mut Self; + + /// Give the child a handle under a name it can look itself up by. + /// + /// The handle is **moved**: after a successful spawn the parent no longer + /// holds it, which is what lets a capability that admits only one holder — + /// a device claim — be handed over at all. A parent that wants to keep one + /// duplicates it first. + #[stable(feature = "toyos_ext", since = "1.0.0")] + fn endow(&mut self, label: &str, handle: u32) -> &mut Self; + + /// Put `connector` in the child's namespace under `name`, on top of what + /// the manifest says the child holds. + /// + /// **This is a launch, not a spawn.** A terminal's `surface` port exists + /// once per terminal, so the supervisor cannot know it and the manifest cannot + /// name it — but the shell's own `[programs]` row is what should decide the + /// rest of what a shell holds. So the caller supplies this one connector, + /// the supervisor supplies the row, and the child's namespace is the union. + /// + /// The connector is **moved**, like [`endow`](CommandExt::endow), and the + /// spawn fails if this process holds no `launcher` connector: there is + /// nowhere else the manifest row can come from. + #[stable(feature = "toyos_ext", since = "1.0.0")] + fn provide(&mut self, name: &str, connector: u32) -> &mut Self; + + /// Make now, on this thread, every file read this command's spawn needs: + /// find the program, judge the working directory, and read a program on a + /// file server into memory. The spawn then calls no file server for them — + /// for a caller whose spawning thread may not wait on one, as + /// `/system/bin/supervisor`'s loop may not wait on the file servers it starts + /// again. A later [`current_dir`](crate::process::Command::current_dir) + /// undoes it. + #[stable(feature = "toyos_ext", since = "1.0.0")] + fn prepare(&mut self) -> crate::io::Result<&mut Self>; + + /// Run the program at `path` rather than the one the command names, which + /// stays the child's `argv[0]`: the kernel opens `path`, or the spawn reads + /// it when a file server serves it, and nothing opens `argv[0]`. + /// + /// **A spawn, never a launch.** The launcher runs the program its manifest + /// row names, so with [`under_supervisor`](CommandExt::under_supervisor) or + /// [`provide`](CommandExt::provide) the spawn answers `PermissionDenied`. + #[stable(feature = "toyos_ext", since = "1.0.0")] + fn image_from(&mut self, path: &crate::path::Path) -> &mut Self; + + /// Place the child under the process `place` names rather than under this + /// one, so that its end, not this process's, takes the child down. + /// + /// `place` is a handle carrying `WRITE` to that process: a copy of the + /// handle it holds to itself, which it handed on. The spawn is refused + /// `PermissionDenied` for a handle without `WRITE`, or without `DUP` for a + /// program the supervisor launches, which carries a copy, and `BrokenPipe` for a + /// process whose end has begun. + #[stable(feature = "toyos_ext", since = "1.0.0")] + fn under(&mut self, place: u32) -> &mut Self; + + /// Ask the supervisor to be the child's parent: the one way for a child to outlive + /// this process. + /// + /// **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 + /// nothing. + #[stable(feature = "toyos_ext", since = "1.0.0")] + fn under_supervisor(&mut self) -> &mut Self; +} + +#[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); + self + } + + fn endow(&mut self, label: &str, handle: u32) -> &mut Self { + self.as_inner_mut().endow(label, handle); + self + } + + fn provide(&mut self, name: &str, connector: u32) -> &mut Self { + self.as_inner_mut().provide(name, connector); + self + } + + fn prepare(&mut self) -> crate::io::Result<&mut Self> { + self.as_inner_mut().prepare()?; + Ok(self) + } + + fn image_from(&mut self, path: &crate::path::Path) -> &mut Self { + self.as_inner_mut().image_from(path.as_os_str()); + self + } + + fn under(&mut self, place: u32) -> &mut Self { + self.as_inner_mut().under(place); + self + } + + fn under_supervisor(&mut self) -> &mut Self { + self.as_inner_mut().under_supervisor(); + self + } +} + +/// ToyOS-specific extensions to [`process::Child`]. +#[stable(feature = "toyos_ext", since = "1.0.0")] +pub trait ChildExt { + /// Give up this process's handle, for one about to be sent or endowed. + /// + /// After this the parent no longer holds the child: it cannot wait for it, + /// kill it or read its accounting. The supervisor's launcher is the caller — + /// it answers with the handle and keeps none, because a process that could + /// ask it to start `/bin/true` in a loop would otherwise exhaust the one + /// handle table the whole machine depends on. + #[stable(feature = "toyos_ext", since = "1.0.0")] + fn into_raw_handle(self) -> u32; + + /// This child's process handle, without giving it up. + /// + /// **A number to pass to the ABI, not a second owner.** It is what a caller + /// wanting more of a process than `wait` and `kill` — its accounting, a + /// narrowed duplicate to hand on — reaches through, and it stays valid only + /// while the `Child` is alive. std deliberately does not wrap those calls: + /// their argument and answer types are `toyos-abi`'s, and a std signature + /// naming them would drag every caller onto the sysroot's copy of that + /// crate rather than its own. + #[stable(feature = "toyos_ext", since = "1.0.0")] + fn as_raw_handle(&self) -> u32; +} + +#[stable(feature = "toyos_ext", since = "1.0.0")] +impl ChildExt for crate::process::Child { + fn into_raw_handle(self) -> u32 { + self.into_inner().into_raw_handle() + } + + fn as_raw_handle(&self) -> u32 { + self.as_inner().as_raw_handle() + } +} diff --git a/sdk/std/sys/alloc.rs b/sdk/std/sys/alloc.rs new file mode 100644 index 00000000000..928f5be2093 --- /dev/null +++ b/sdk/std/sys/alloc.rs @@ -0,0 +1,96 @@ +use core::cell::SyncUnsafeCell; + +use toyos_abi::syscall::{self, MmapFlags, MmapProt}; + +use crate::alloc::Layout; +use crate::ptr; +use crate::sync::atomic::{AtomicI32, Ordering}; + +/// dlmalloc backing allocator that provides memory via mmap/munmap syscalls. +struct ToyOsAllocator; + +unsafe impl dlmalloc::Allocator for ToyOsAllocator { + fn alloc(&self, size: usize) -> (*mut u8, usize, u32) { + let ptr = unsafe { + syscall::mmap( + ptr::null_mut(), + size, + MmapProt::READ | MmapProt::WRITE, + MmapFlags::ANONYMOUS, + ) + }; + if ptr.is_null() { (ptr::null_mut(), 0, 0) } else { (ptr, size, 0) } + } + + fn remap(&self, _ptr: *mut u8, _oldsize: usize, _newsize: usize, _can_move: bool) -> *mut u8 { + // No mremap equivalent + ptr::null_mut() + } + + fn free_part(&self, _ptr: *mut u8, _oldsize: usize, _newsize: usize) -> bool { + false + } + + fn free(&self, ptr: *mut u8, size: usize) -> bool { + unsafe { syscall::munmap(ptr, size).is_ok() } + } + + fn can_release_part(&self, _flags: u32) -> bool { + false + } + + fn allocates_zeros(&self) -> bool { + true // mmap returns zeroed pages + } + + fn page_size(&self) -> usize { + 0x1000 + } +} + +struct SyncDlmalloc(dlmalloc::Dlmalloc); +unsafe impl Sync for SyncDlmalloc {} + +static DLMALLOC: SyncUnsafeCell = + SyncUnsafeCell::new(SyncDlmalloc(dlmalloc::Dlmalloc::new_with_allocator(ToyOsAllocator))); + +static LOCKED: AtomicI32 = AtomicI32::new(0); + +struct DropLock; + +fn lock() -> DropLock { + while LOCKED.swap(1, Ordering::Acquire) != 0 { + core::hint::spin_loop(); + } + DropLock +} + +impl Drop for DropLock { + fn drop(&mut self) { + LOCKED.store(0, Ordering::Release); + } +} + +#[inline] +pub unsafe fn alloc(layout: Layout) -> *mut u8 { + let _lock = lock(); + unsafe { (*DLMALLOC.get()).0.malloc(layout.size(), layout.align()) } +} + +#[inline] +pub unsafe fn alloc_zeroed(layout: Layout) -> *mut u8 { + let _lock = lock(); + unsafe { (*DLMALLOC.get()).0.calloc(layout.size(), layout.align()) } +} + +#[inline] +pub unsafe fn dealloc(ptr: *mut u8, layout: Layout) { + let _lock = lock(); + unsafe { (*DLMALLOC.get()).0.free(ptr, layout.size(), layout.align()) } +} + +#[inline] +pub unsafe fn realloc(ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 { + let _lock = lock(); + unsafe { (*DLMALLOC.get()).0.realloc(ptr, layout.size(), layout.align(), new_size) } +} diff --git a/sdk/std/sys/args.rs b/sdk/std/sys/args.rs new file mode 100644 index 00000000000..bf25baf56e1 --- /dev/null +++ b/sdk/std/sys/args.rs @@ -0,0 +1,24 @@ +use core::sync::atomic::Ordering; + +pub use super::common::Args; +use crate::ffi::{CStr, OsString}; + +pub fn args() -> Args { + let argc = crate::sys::pal::ARGC.load(Ordering::Relaxed); + let argv = core::ptr::with_exposed_provenance::<*const u8>( + crate::sys::pal::ARGV.load(Ordering::Relaxed), + ); + + let mut vec = Vec::with_capacity(argc); + + for i in 0..argc { + let ptr = unsafe { *argv.add(i) }; + if ptr.is_null() { + break; + } + let cstr = unsafe { CStr::from_ptr(ptr.cast()) }; + vec.push(OsString::from(cstr.to_str().unwrap_or(""))); + } + + Args::new(vec) +} diff --git a/sdk/std/sys/env.rs b/sdk/std/sys/env.rs new file mode 100644 index 00000000000..41cb92c194f --- /dev/null +++ b/sdk/std/sys/env.rs @@ -0,0 +1,58 @@ +pub use super::common::Env; +use crate::collections::HashMap; +use crate::ffi::{OsStr, OsString}; +use crate::io; +use crate::sync::{Mutex, MutexGuard}; + +static ENV: Mutex>> = Mutex::new(None); + +/// Lock ENV and lazily initialize from the kernel if needed. +/// This ensures env works from cdylib .so files whose std was never explicitly initialized. +fn lock_env() -> MutexGuard<'static, Option>> { + let mut guard = ENV.lock().unwrap(); + if guard.is_none() { + let mut map = HashMap::new(); + let size = toyos_abi::syscall::get_env(&mut []); + if size > 0 { + let mut buf = vec![0u8; size]; + toyos_abi::syscall::get_env(&mut buf); + for entry in buf.split(|&b| b == 0) { + if entry.is_empty() { + continue; + } + if let Some(eq) = entry.iter().position(|&b| b == b'=') { + let key = OsString::from(crate::str::from_utf8(&entry[..eq]).unwrap_or("")); + let val = OsString::from(crate::str::from_utf8(&entry[eq + 1..]).unwrap_or("")); + map.insert(key, val); + } + } + } + *guard = Some(map); + } + guard +} + +pub fn init() { + drop(lock_env()); +} + +pub fn env() -> Env { + let guard = lock_env(); + let map = guard.as_ref().unwrap(); + let result = map.iter().map(|(k, v)| (k.clone(), v.clone())).collect(); + Env::new(result) +} + +pub fn getenv(k: &OsStr) -> Option { + lock_env().as_ref().unwrap().get(k).cloned() +} + +pub unsafe fn setenv(k: &OsStr, v: &OsStr) -> io::Result<()> { + lock_env().as_mut().unwrap().insert(k.to_owned(), v.to_owned()); + Ok(()) +} + +pub unsafe fn unsetenv(k: &OsStr) -> io::Result<()> { + lock_env().as_mut().unwrap().remove(k); + Ok(()) +} diff --git a/sdk/std/sys/fs.rs b/sdk/std/sys/fs.rs new file mode 100644 index 00000000000..77a253ccf96 --- /dev/null +++ b/sdk/std/sys/fs.rs @@ -0,0 +1,1041 @@ +//! Files on ToyOS. +//! +//! A path names a file on one of two kinds of server. The kernel serves +//! `/system` (the signed image) and `/tmp`; every other directory a process +//! may reach is a capability in its namespace, `fs:`, served by a +//! file server process (`toyos::fs`). A path is made absolute against the +//! working directory, normalized, and sent to the capability with the longest +//! matching prefix, relative to it; a path under no capability the process +//! holds is the kernel's. +//! +//! A file server that restarts is survived: its capability is connected +//! again. + +use toyos_abi::RawHandle; +use toyos_abi::syscall::{self, OpenFlags, SyscallError}; + +use crate::collections::BTreeMap; +use crate::ffi::OsString; +use crate::fmt; +use crate::fs::TryLockError; +use crate::hash::Hash; +use crate::io::{self, BorrowedCursor, IoSlice, IoSliceMut, SeekFrom}; +use crate::path::{Path, PathBuf}; +use crate::sync::{Arc, Condvar, Mutex, MutexGuard}; +pub use crate::sys::fs::common::Dir; +use crate::sys::time::SystemTime; +use crate::sys::to_io_error; + +/// The deepest directory a capability names, in components. +const MAX_CAPABILITY_DEPTH: usize = 4; + +/// The most absolute symlinks one path resolution follows. +const MAX_LINKS: usize = 40; + +pub struct File(Inner); + +enum Inner { + Kernel(RawHandle), + Served(Arc), +} + +/// A file open on a file server. +struct Served { + dir: Arc, + rel: String, + append: bool, + state: Mutex, +} + +struct Position { + fid: u64, + generation: u64, + offset: u64, +} + +/// One directory capability this process holds, connected. +struct Capability { + prefix: String, + dir: Mutex, + /// Hands `dir` over in ticket order: std's mutex lets the thread that let + /// go barge ahead of the one it woke. + turns: Mutex, + served: Condvar, +} + +struct Turns { + next: u64, + serving: u64, +} + +/// A directory's connection, held in turn. +struct Held<'a> { + // Dropped before `_turn`, so the next ticket finds `dir` free. + dir: MutexGuard<'a, toyos::fs::Dir>, + _turn: Turn<'a>, +} + +struct Turn<'a>(&'a Capability); + +impl Capability { + fn new(prefix: &str, dir: toyos::fs::Dir) -> Self { + Self { + prefix: String::from(prefix), + dir: Mutex::new(dir), + turns: Mutex::new(Turns { next: 0, serving: 0 }), + served: Condvar::new(), + } + } + + fn lock(&self) -> Held<'_> { + let mut turns = self.turns.lock().unwrap(); + let ticket = turns.next; + turns.next += 1; + drop(self.served.wait_while(turns, |t| t.serving != ticket).unwrap()); + let turn = Turn(self); + Held { dir: self.dir.lock().unwrap(), _turn: turn } + } +} + +impl Drop for Turn<'_> { + fn drop(&mut self) { + self.0.turns.lock().unwrap().serving += 1; + self.0.served.notify_all(); + } +} + +impl crate::ops::Deref for Held<'_> { + type Target = toyos::fs::Dir; + fn deref(&self) -> &toyos::fs::Dir { + &self.dir + } +} + +impl crate::ops::DerefMut for Held<'_> { + fn deref_mut(&mut self) -> &mut toyos::fs::Dir { + &mut self.dir + } +} + +#[derive(Clone)] +pub struct FileAttr { + size: u64, + file_type: FileType, + mtime: u64, +} + +pub struct ReadDir { + entries: Vec, + index: usize, +} + +pub struct DirEntry { + dir_path: PathBuf, + name: OsString, + size: u64, + file_type: FileType, +} + +#[derive(Clone, Debug)] +pub struct OpenOptions { + read: bool, + write: bool, + append: bool, + truncate: bool, + create: bool, + create_new: bool, +} + +#[derive(Copy, Clone, Debug, Default)] +pub struct FileTimes {} + +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct FilePermissions { + readonly: bool, +} + +#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] +pub struct FileType { + is_file: bool, + is_dir: bool, + is_symlink: bool, +} + +#[derive(Debug)] +pub struct DirBuilder {} + +impl FileType { + fn of_kind(kind: u64) -> FileType { + FileType { + is_file: kind == toyos::fs::KIND_FILE, + is_dir: kind == toyos::fs::KIND_DIR, + is_symlink: kind == toyos::fs::KIND_SYMLINK, + } + } + + pub fn is_dir(&self) -> bool { + self.is_dir + } + + pub fn is_file(&self) -> bool { + self.is_file + } + + pub fn is_symlink(&self) -> bool { + self.is_symlink + } +} + +impl FileAttr { + fn of(stat: toyos::fs::Stat) -> FileAttr { + FileAttr { size: stat.size, file_type: FileType::of_kind(stat.kind), mtime: stat.mtime } + } + + pub fn size(&self) -> u64 { + self.size + } + + pub fn perm(&self) -> FilePermissions { + FilePermissions { readonly: false } + } + + pub fn file_type(&self) -> FileType { + self.file_type + } + + pub fn modified(&self) -> io::Result { + match self.mtime { + 0 => Err(io::const_error!(io::ErrorKind::Unsupported, "the file is undated")), + nanos => Ok(SystemTime::from_nanos(nanos)), + } + } + + pub fn accessed(&self) -> io::Result { + Err(io::Error::new(io::ErrorKind::Unsupported, "ToyOS does not track access time")) + } + + pub fn created(&self) -> io::Result { + Err(io::Error::new(io::ErrorKind::Unsupported, "ToyOS does not track creation time")) + } +} + +impl FilePermissions { + pub fn readonly(&self) -> bool { + self.readonly + } + + pub fn set_readonly(&mut self, readonly: bool) { + self.readonly = readonly; + } +} + +impl FileTimes { + pub fn set_accessed(&mut self, _t: SystemTime) {} + pub fn set_modified(&mut self, _t: SystemTime) {} +} + +impl fmt::Debug for ReadDir { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("ReadDir").finish_non_exhaustive() + } +} + +impl Iterator for ReadDir { + type Item = io::Result; + + fn next(&mut self) -> Option> { + let e = self.entries.get(self.index)?; + self.index += 1; + Some(Ok(DirEntry { + dir_path: e.dir_path.clone(), + name: e.name.clone(), + size: e.size, + file_type: e.file_type, + })) + } +} + +impl DirEntry { + pub fn path(&self) -> PathBuf { + self.dir_path.join(&self.name) + } + + pub fn file_name(&self) -> OsString { + self.name.clone() + } + + pub fn metadata(&self) -> io::Result { + Ok(FileAttr { size: self.size, file_type: self.file_type, mtime: 0 }) + } + + pub fn file_type(&self) -> io::Result { + Ok(self.file_type) + } +} + +impl OpenOptions { + pub fn new() -> OpenOptions { + OpenOptions { + read: false, + write: false, + append: false, + truncate: false, + create: false, + create_new: false, + } + } + + pub fn read(&mut self, read: bool) { + self.read = read; + } + pub fn write(&mut self, write: bool) { + self.write = write; + } + pub fn append(&mut self, append: bool) { + self.append = append; + } + pub fn truncate(&mut self, truncate: bool) { + self.truncate = truncate; + } + pub fn create(&mut self, create: bool) { + self.create = create; + } + pub fn create_new(&mut self, create_new: bool) { + self.create_new = create_new; + } + + fn to_flags(&self) -> OpenFlags { + let mut flags = OpenFlags(0); + if self.read { + flags |= OpenFlags::READ; + } + if self.write || self.append { + flags |= OpenFlags::WRITE; + } + if self.append { + flags |= OpenFlags::APPEND; + } + if self.create || self.create_new { + flags |= OpenFlags::CREATE; + } + if self.truncate { + flags |= OpenFlags::TRUNCATE; + } + flags + } + + fn served_flags(&self) -> u64 { + use toyos::fs::*; + let mut flags = 0; + if self.read { + flags |= O_READ; + } + if self.write { + flags |= O_WRITE; + } + if self.append { + flags |= O_APPEND; + } + if self.create { + flags |= O_CREATE; + } + if self.create_new { + flags |= O_CREATE_NEW; + } + if self.truncate { + flags |= O_TRUNCATE; + } + flags + } +} + +/// Where a path is served. +enum Route { + Kernel(String), + Served(Arc, String), +} + +/// Every capability this process has looked for, found or not. +static CAPABILITIES: Mutex>>> = Mutex::new(BTreeMap::new()); + +fn served_error(e: SyscallError) -> io::Error { + match e { + SyscallError::Gone => io::const_error!( + io::ErrorKind::StaleNetworkFileHandle, + "the file server is gone, or restarted since this file was opened", + ), + e => to_io_error(e), + } +} + +/// `path`, absolute against the working directory and without `.` or `..`. +fn absolute(path: &Path) -> io::Result { + let text = path + .to_str() + .ok_or_else(|| io::const_error!(io::ErrorKind::InvalidInput, "path is not UTF-8"))?; + let joined = if text.starts_with('/') { + String::from(text) + } else { + let cwd = crate::env::current_dir()?; + let cwd = cwd.to_str().expect("getcwd answers UTF-8"); + format!("{cwd}/{text}") + }; + let mut parts: Vec<&str> = Vec::new(); + for part in joined.split('/') { + match part { + "" | "." => {} + ".." => { + parts.pop(); + } + other => parts.push(other), + } + } + Ok(format!("/{}", parts.join("/"))) +} + +/// The capability named `fs:`, remembering the answer either way. +/// +/// The connect waits for the server's hello, so it is made outside the lock: a +/// server that has not answered holds up only the threads that asked it, and +/// of two threads that connect at once the first answer kept is the one used. +fn capability(prefix: &str) -> io::Result>> { + if let Some(found) = CAPABILITIES.lock().unwrap().get(prefix) { + return Ok(found.clone()); + } + let Some(names) = toyos::endow::namespace() else { + return Ok(None); + }; + let name = format!("{}{prefix}", toyos::fs::CAPABILITY_PREFIX); + let found = match toyos::fs::Dir::connect(names, &name) { + Ok(dir) => Some(Arc::new(Capability::new(prefix, dir))), + Err(SyscallError::NotFound) | Err(SyscallError::InvalidArgument) => None, + Err(e) => return Err(served_error(e)), + }; + Ok(CAPABILITIES.lock().unwrap().entry(String::from(prefix)).or_insert(found).clone()) +} + +/// Which server `abs` is on, and the path there. +fn route(abs: &str) -> io::Result { + let parts: Vec<&str> = abs.split('/').filter(|p| !p.is_empty()).collect(); + for depth in (1..=parts.len().min(MAX_CAPABILITY_DEPTH)).rev() { + let prefix = format!("/{}", parts[..depth].join("/")); + if let Some(cap) = capability(&prefix)? { + return Ok(Route::Served(cap, parts[depth..].join("/"))); + } + } + Ok(Route::Kernel(String::from(abs))) +} + +/// Ask the server behind `path`, following an absolute link it meets to +/// wherever that lands in this process's own table. +fn on_path( + path: &Path, + mut kernel: impl FnMut(&str) -> io::Result, + mut served: impl FnMut(&Arc, &mut toyos::fs::Dir, &str) -> Result, +) -> io::Result { + let mut abs = absolute(path)?; + for _ in 0..MAX_LINKS { + let (cap, rel) = match route(&abs)? { + Route::Kernel(abs) => return kernel(&abs), + Route::Served(cap, rel) => (cap, rel), + }; + let mut dir = cap.lock(); + match served(&cap, &mut dir, &rel) { + Ok(answer) => return Ok(answer), + Err(toyos::fs::Refused::Error(e)) => return Err(served_error(e)), + Err(toyos::fs::Refused::Link(len)) => { + let mut buf = [0u8; toyos::fs::MAX_PATH]; + let target = dir.link_target(len, &mut buf); + let target = crate::str::from_utf8(target).map_err(|_| { + io::const_error!(io::ErrorKind::InvalidData, "a link target is not UTF-8") + })?; + abs = absolute(Path::new(target))?; + } + } + } + Err(io::const_error!(io::ErrorKind::FilesystemLoop, "too many symbolic links")) +} + +/// Whether `path` is on a file server rather than the kernel. +pub fn is_served(path: &Path) -> bool { + absolute(path).and_then(|abs| route(&abs)).is_ok_and(|r| matches!(r, Route::Served(..))) +} + +impl Served { + fn with( + &self, + op: impl FnOnce(&mut toyos::fs::Dir, &mut Position) -> Result, + ) -> io::Result { + let mut dir = self.dir.lock(); + let mut pos = self.state.lock().unwrap(); + op(&mut dir, &mut pos).map_err(served_error) + } +} + +impl Drop for Served { + fn drop(&mut self) { + let mut dir = self.dir.lock(); + let pos = self.state.lock().unwrap(); + dir.close(pos.fid, pos.generation); + } +} + +impl File { + pub fn from_fd(fd: RawHandle) -> 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. + 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"), + } + } + + pub fn as_raw_fd(&self) -> i32 { + self.raw_fd() + } + + /// A kernel handle a child may write this file through: the file's own, or + /// a pipe its server appends to the file from 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)> { + match &self.0 { + Inner::Kernel(h) => Ok((*h, None)), + Inner::Served(s) => { + let pipe = s.with(|dir, pos| { + let at = if s.append { dir.fstat(pos.fid, pos.generation)?.size } else { pos.offset }; + dir.stream(pos.fid, pos.generation, at) + })?; + Ok((toyos::AsHandle::as_handle(&pipe), Some(pipe))) + } + } + } + + pub fn open(path: &Path, opts: &OpenOptions) -> io::Result { + on_path( + path, + |abs| { + let fd = syscall::open(abs.as_bytes(), opts.to_flags()).map_err(to_io_error)?; + Ok(File(Inner::Kernel(fd))) + }, + |cap, dir, rel| { + let flags = opts.served_flags(); + let opened = dir.open(rel, flags)?; + Ok(File(Inner::Served(Arc::new(Served { + dir: Arc::clone(cap), + rel: String::from(rel), + append: opts.append, + state: Mutex::new(Position { + fid: opened.fid, + generation: opened.generation, + offset: 0, + }), + })))) + }, + ) + } + + pub fn file_attr(&self) -> io::Result { + match &self.0 { + Inner::Kernel(h) => { + let stat = syscall::fstat(*h).map_err(to_io_error)?; + Ok(FileAttr { + size: stat.size, + file_type: opened_file_type(stat.file_type), + mtime: stat.mtime, + }) + } + Inner::Served(s) => s.with(|dir, pos| dir.fstat(pos.fid, pos.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::Served(s) => s.with(|dir, pos| dir.fsync(pos.fid, pos.generation)), + } + } + + pub fn datasync(&self) -> io::Result<()> { + self.fsync() + } + + pub fn lock(&self) -> io::Result<()> { + Ok(()) + } + pub fn lock_shared(&self) -> io::Result<()> { + Ok(()) + } + pub fn try_lock(&self) -> Result<(), TryLockError> { + Ok(()) + } + pub fn try_lock_shared(&self) -> Result<(), TryLockError> { + Ok(()) + } + pub fn unlock(&self) -> io::Result<()> { + Ok(()) + } + + pub fn truncate(&self, size: u64) -> io::Result<()> { + match &self.0 { + Inner::Kernel(h) => syscall::ftruncate(*h, size).map_err(to_io_error), + Inner::Served(s) => s.with(|dir, pos| dir.truncate(pos.fid, pos.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::Served(s) => s.with(|dir, pos| { + let n = dir.read(pos.fid, pos.generation, pos.offset, buf)?; + pos.offset += n as u64; + Ok(n) + }), + } + } + + pub fn read_vectored(&self, bufs: &mut [IoSliceMut<'_>]) -> io::Result { + let mut total = 0; + for buf in bufs { + match self.read(buf) { + Ok(0) => break, + Ok(n) => total += n, + Err(e) => { + if total == 0 { + return Err(e); + } else { + break; + } + } + } + } + Ok(total) + } + + pub fn is_read_vectored(&self) -> bool { + false + } + + pub fn read_buf(&self, mut cursor: BorrowedCursor<'_, u8>) -> io::Result<()> { + let n = self.read(cursor.ensure_init())?; + unsafe { cursor.advance(n) }; + Ok(()) + } + + pub fn write(&self, buf: &[u8]) -> io::Result { + match &self.0 { + Inner::Kernel(h) => syscall::write(*h, buf).map_err(to_io_error), + Inner::Served(s) => s.with(|dir, pos| { + let written = dir.write(pos.fid, pos.generation, pos.offset, buf)?; + pos.offset = written.offset; + Ok(written.len) + }), + } + } + + pub fn write_vectored(&self, bufs: &[IoSlice<'_>]) -> io::Result { + let mut total = 0; + for buf in bufs { + match self.write(buf) { + Ok(0) => break, + Ok(n) => total += n, + Err(e) => { + if total == 0 { + return Err(e); + } else { + break; + } + } + } + } + Ok(total) + } + + pub fn is_write_vectored(&self) -> bool { + false + } + + pub fn flush(&self) -> io::Result<()> { + self.fsync() + } + + pub fn seek(&self, pos: SeekFrom) -> io::Result { + match &self.0 { + Inner::Kernel(h) => { + 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) + } + Inner::Served(s) => s.with(|dir, p| { + let (base, delta) = match pos { + SeekFrom::Start(n) => (n, 0), + SeekFrom::Current(d) => (p.offset, d), + SeekFrom::End(d) => (dir.fstat(p.fid, p.generation)?.size, d), + }; + let at = base + .checked_add_signed(delta) + .filter(|&at| at <= toyos::fs::MAX_FILE_BYTES) + .ok_or(SyscallError::InvalidArgument)?; + p.offset = at; + Ok(at) + }), + } + } + + pub fn size(&self) -> Option> { + Some(self.file_attr().map(|a| a.size)) + } + + pub fn tell(&self) -> io::Result { + self.seek(SeekFrom::Current(0)) + } + + 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::Served(s) => Ok(File(Inner::Served(Arc::clone(s)))), + } + } + + pub fn set_permissions(&self, _perm: FilePermissions) -> io::Result<()> { + Ok(()) + } + + pub fn set_times(&self, _times: FileTimes) -> io::Result<()> { + Ok(()) + } +} + +impl Drop for File { + fn drop(&mut self) { + if let Inner::Kernel(h) = self.0 { + syscall::close(h); + } + } +} + +impl DirBuilder { + pub fn new() -> DirBuilder { + DirBuilder {} + } + + pub fn mkdir(&self, p: &Path) -> io::Result<()> { + on_path( + p, + |abs| syscall::mkdir(abs.as_bytes()).map_err(to_io_error), + |_, dir, rel| dir.mkdir(rel), + ) + } +} + +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::Served(s) => write!(f, "File({}/{})", s.dir.prefix, s.rel), + } + } +} + +/// Whether the kernel's `path` names a directory, asked through `readdir`: a +/// directory too large to list is still one, which is why +/// `ResourceExhausted` is a yes. +fn kernel_is_dir(path: &[u8]) -> bool { + let mut buf = [0u8; 1]; + match syscall::readdir(path, &mut buf) { + Ok(_) | Err(SyscallError::ResourceExhausted) => true, + Err(_) => false, + } +} + +/// The type of something the kernel's `open` accepted, which is never a +/// directory. +fn opened_file_type(ty: syscall::FileType) -> FileType { + FileType { is_file: ty == syscall::FileType::File, is_dir: false, is_symlink: false } +} + +/// A listing too large for a first buffer is asked again at the size it +/// named; a directory that outgrows its own listing this many times is +/// refused rather than waited on. +const LIST_ATTEMPTS: usize = 4; + +pub fn readdir(p: &Path) -> io::Result { + let dir_path = p.to_path_buf(); + let entries = on_path( + p, + |abs| { + let mut buf = vec![0u8; 65536]; + let mut n = 0; + let mut fits = false; + for _ in 0..LIST_ATTEMPTS { + n = syscall::readdir(abs.as_bytes(), &mut buf).map_err(to_io_error)?; + if n <= buf.len() { + fits = true; + break; + } + buf.clear(); + buf.resize(n, 0); + } + if !fits { + return Err(io::const_error!( + io::ErrorKind::Interrupted, + "directory kept growing while it was being listed", + )); + } + Ok(kernel_entries(&dir_path, &buf[..n])) + }, + |_, dir, rel| { + let mut buf = vec![0u8; 65536]; + for _ in 0..LIST_ATTEMPTS { + let n = dir.read_dir(rel, &mut buf)?; + if n <= buf.len() { + let mut entries = Vec::new(); + toyos::fs::for_each_entry(&buf[..n], |kind, size, name| { + entries.push(DirEntry { + dir_path: dir_path.clone(), + name: OsString::from(name), + size, + file_type: FileType::of_kind(kind), + }) + })?; + return Ok(entries); + } + buf.resize(n, 0); + } + Err(toyos::fs::Refused::Error(SyscallError::ResourceExhausted)) + }, + )?; + Ok(ReadDir { entries, index: 0 }) +} + +/// The entries of a whole `readdir` listing, read by `syscall::dirent`. +fn kernel_entries(dir_path: &Path, data: &[u8]) -> Vec { + let mut entries = Vec::new(); + let mut at = 0; + while let Some(entry) = syscall::dirent(data, &mut at) { + entries.push(DirEntry { + dir_path: dir_path.to_path_buf(), + name: OsString::from(crate::str::from_utf8(entry.name).unwrap_or("")), + size: entry.size, + file_type: FileType { is_file: !entry.is_dir, is_dir: entry.is_dir, is_symlink: false }, + }); + } + entries +} + +pub fn unlink(p: &Path) -> io::Result<()> { + on_path( + p, + |abs| syscall::delete(abs.as_bytes()).map_err(to_io_error), + |_, dir, rel| dir.unlink(rel), + ) +} + +pub fn rename(old: &Path, new: &Path) -> io::Result<()> { + let (from, to) = (route(&absolute(old)?)?, route(&absolute(new)?)?); + match (from, to) { + (Route::Kernel(a), Route::Kernel(b)) => { + syscall::rename(a.as_bytes(), b.as_bytes()).map_err(to_io_error) + } + (Route::Served(ca, a), Route::Served(cb, b)) if Arc::ptr_eq(&ca, &cb) => { + ca.lock().rename(&a, &b).map_err(|e| match e { + toyos::fs::Refused::Error(e) => served_error(e), + toyos::fs::Refused::Link(_) => io::const_error!( + io::ErrorKind::CrossesDevices, + "the rename crosses a link out of its directory", + ), + }) + } + _ => Err(io::const_error!( + io::ErrorKind::CrossesDevices, + "the two paths are on different servers", + )), + } +} + +pub fn set_perm(_p: &Path, _perm: FilePermissions) -> io::Result<()> { + Ok(()) +} + +pub fn set_perm_nofollow(_p: &Path, _perm: FilePermissions) -> io::Result<()> { + Ok(()) +} + +pub fn rmdir(p: &Path) -> io::Result<()> { + on_path( + p, + |abs| syscall::rmdir(abs.as_bytes()).map_err(to_io_error), + |_, dir, rel| dir.rmdir(rel), + ) +} + +pub fn remove_dir_all(path: &Path) -> io::Result<()> { + for entry in readdir(path)? { + let entry = entry?; + let child_path = entry.path(); + if entry.file_type()?.is_dir() { + remove_dir_all(&child_path)?; + } else { + unlink(&child_path)?; + } + } + rmdir(path) +} + +pub fn exists(path: &Path) -> io::Result { + match stat(path) { + Ok(_) => Ok(true), + Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(false), + Err(e) => Err(e), + } +} + +pub fn readlink(p: &Path) -> io::Result { + on_path( + p, + |abs| { + let mut buf = [0u8; 4096]; + let n = syscall::readlink(abs.as_bytes(), &mut buf).map_err(to_io_error)?; + // SAFETY: The kernel returns valid UTF-8 paths as raw bytes. + Ok(PathBuf::from(unsafe { OsString::from_encoded_bytes_unchecked(buf[..n].to_vec()) })) + }, + |_, dir, rel| { + let mut buf = [0u8; toyos::fs::MAX_PATH]; + let n = dir.read_link(rel, &mut buf)?; + let text = crate::str::from_utf8(&buf[..n]).map_err(|_| SyscallError::Io)?; + Ok(PathBuf::from(text)) + }, + ) +} + +pub fn symlink(original: &Path, link: &Path) -> io::Result<()> { + let target = original + .to_str() + .ok_or_else(|| io::const_error!(io::ErrorKind::InvalidInput, "path is not UTF-8"))?; + on_path( + link, + |abs| syscall::symlink(target.as_bytes(), abs.as_bytes()).map_err(to_io_error), + |_, dir, rel| dir.symlink(target, rel), + ) +} + +pub fn link(_src: &Path, _dst: &Path) -> io::Result<()> { + Err(io::Error::new(io::ErrorKind::Unsupported, "no hard links on ToyOS")) +} + +fn kernel_stat(abs: &str) -> io::Result { + if let Ok(fd) = syscall::open(abs.as_bytes(), OpenFlags::READ) { + let result = syscall::fstat(fd); + syscall::close(fd); + let st = result.map_err(to_io_error)?; + return Ok(FileAttr { + size: st.size, + file_type: opened_file_type(st.file_type), + mtime: st.mtime, + }); + } + if kernel_is_dir(abs.as_bytes()) { + return Ok(FileAttr { + size: 0, + file_type: FileType { is_file: false, is_dir: true, is_symlink: false }, + mtime: 0, + }); + } + Err(io::Error::new(io::ErrorKind::NotFound, "file not found")) +} + +pub fn stat(path: &Path) -> io::Result { + on_path(path, kernel_stat, |_, dir, rel| dir.stat(rel, true).map(FileAttr::of)) +} + +pub fn lstat(path: &Path) -> io::Result { + on_path( + path, + |abs| { + let mut link_buf = [0u8; 4096]; + if let Ok(n) = syscall::readlink(abs.as_bytes(), &mut link_buf) { + return Ok(FileAttr { + size: n as u64, + file_type: FileType { is_file: false, is_dir: false, is_symlink: true }, + mtime: 0, + }); + } + kernel_stat(abs) + }, + |_, dir, rel| dir.stat(rel, false).map(FileAttr::of), + ) +} + +pub fn canonicalize(p: &Path) -> io::Result { + crate::path::absolute(p) +} + +pub fn copy(from: &Path, to: &Path) -> io::Result { + let mut read = OpenOptions::new(); + read.read(true); + let reader = File::open(from, &read)?; + let mut write = OpenOptions::new(); + write.write(true); + write.truncate(true); + write.create(true); + let writer = File::open(to, &write)?; + let mut buf = vec![0u8; 256 * 1024]; + let mut total = 0u64; + loop { + let n = reader.read(&mut buf)?; + if n == 0 { + break; + } + let mut done = 0; + while done < n { + done += writer.write(&buf[done..n])?; + } + total += n as u64; + } + Ok(total) +} + +pub fn set_times(_p: &Path, _times: FileTimes) -> io::Result<()> { + Ok(()) +} + +pub fn set_times_nofollow(_p: &Path, _times: FileTimes) -> io::Result<()> { + Ok(()) +} + +/// The whole of the file at `path` as a program image: what a spawn of a +/// program the kernel cannot open itself hands the kernel. +pub fn read_image(path: &Path) -> io::Result { + let mut opts = OpenOptions::new(); + opts.read(true); + let file = File::open(path, &opts)?; + let len = file.file_attr()?.size; + toyos::process::Image::read(len, |buf| file.read(buf)).map_err(|refused| match refused { + toyos::process::ImageRefused::Empty => { + io::const_error!(io::ErrorKind::InvalidData, "the program is an empty file") + } + toyos::process::ImageRefused::Memory(e) => to_io_error(e), + toyos::process::ImageRefused::Shrank => { + io::const_error!(io::ErrorKind::UnexpectedEof, "the program shrank while it was read") + } + toyos::process::ImageRefused::Read(e) => e, + }) +} + +/// Make the working directory `p`, which a file server judges when it serves +/// it and the kernel judges when it does. +pub fn chdir(p: &Path) -> io::Result<()> { + let abs = absolute(p)?; + if let Route::Served(..) = route(&abs)? { + if !stat(Path::new(&abs))?.file_type.is_dir { + return Err(io::const_error!(io::ErrorKind::NotADirectory, "not a directory")); + } + } + syscall::chdir(abs.as_bytes()).map_err(to_io_error) +} diff --git a/sdk/std/sys/net/connection.rs b/sdk/std/sys/net/connection.rs new file mode 100644 index 00000000000..10f89ee4ec3 --- /dev/null +++ b/sdk/std/sys/net/connection.rs @@ -0,0 +1,741 @@ +use toyos::AsHandle; +use toyos::net::{NetError, TcpSocketId, UdpSocketId}; +use toyos::poller::{Poller, READABLE, WRITABLE}; +use toyos_abi::RawHandle; +use toyos_abi::syscall::{self, SyscallError}; + +use crate::fmt; +use crate::io::{self, BorrowedCursor, IoSlice, IoSliceMut}; +use crate::net::{Ipv4Addr, Ipv6Addr, Shutdown, SocketAddr, SocketAddrV4, ToSocketAddrs}; +use crate::os::fd::{AsFd, AsRawFd, BorrowedFd, FromRawFd, OwnedFd}; +use crate::sync::Arc; +use crate::sync::atomic::Ordering::Relaxed; +use crate::sync::atomic::{AtomicBool, AtomicU32}; +use crate::time::{Duration, Instant}; + +// --- Helpers --- + +fn net_err_to_io(e: NetError) -> io::Error { + let kind = match e { + NetError::ConnectionRefused => io::ErrorKind::ConnectionRefused, + NetError::ConnectionReset => io::ErrorKind::ConnectionReset, + NetError::TimedOut => io::ErrorKind::TimedOut, + NetError::AddrInUse => io::ErrorKind::AddrInUse, + NetError::NotConnected => io::ErrorKind::NotConnected, + NetError::InvalidInput => io::ErrorKind::InvalidInput, + NetError::NetstackNotFound => io::ErrorKind::NotConnected, + _ => io::ErrorKind::Other, + }; + io::Error::new(kind, "netstack error") +} + +fn addr_to_v4(addr: &SocketAddr) -> io::Result<([u8; 4], u16)> { + match addr { + SocketAddr::V4(v4) => Ok((v4.ip().octets(), v4.port())), + SocketAddr::V6(_) => Err(io::Error::new(io::ErrorKind::InvalidInput, "IPv6 not supported")), + } +} + +fn duration_to_ms(d: Option) -> u32 { + match d { + Some(d) => d.as_millis().min(u32::MAX as u128) as u32, + None => 0, + } +} + +fn syscall_err(e: SyscallError) -> io::Error { + match e { + SyscallError::WouldBlock => io::ErrorKind::WouldBlock.into(), + _ => io::Error::new(io::ErrorKind::Other, "syscall error"), + } +} + +/// How long `TcpStream::connect` waits for each address to answer. +const CONNECT_TIMEOUT: Duration = Duration::from_secs(30); + +/// Wait until `handle` is ready for `flags` or `left` passes. +fn wait_ready(handle: RawHandle, flags: u32, left: Duration) { + let poller = Poller::new(1); + poller.watch_raw(handle, flags, 0); + poller.wait(1, left.as_nanos().min(u64::MAX as u128) as u64, |_| {}); +} + +/// Run `op` until it does not answer `WouldBlock`, waiting for `flags` on +/// `handle` in between; `None` once `timeout_ms` has passed. +fn with_timeout( + handle: RawHandle, + flags: u32, + timeout_ms: u32, + mut op: impl FnMut() -> Result, +) -> Option> { + let deadline = Instant::now() + Duration::from_millis(timeout_ms as u64); + loop { + match op() { + Err(SyscallError::WouldBlock) => {} + done => return Some(done), + } + let left = deadline.saturating_duration_since(Instant::now()); + if left.is_zero() { + return None; + } + wait_ready(handle, flags, left); + } +} + +const TIMED_OUT: io::Error = io::const_error!(io::ErrorKind::TimedOut, "timed out"); + +/// The connection was reset, or ended by netstack: its send pipe has no reader. +const RESET: io::Error = io::const_error!(io::ErrorKind::ConnectionReset, "connection reset"); + +const SHUT_DOWN: io::Error = + io::const_error!(io::ErrorKind::BrokenPipe, "the stream was shut down for writing"); + +/// Join rx/tx pipes into a single duplex kernel handle. +fn make_socket_fd(rx: toyos::Pipe, tx: toyos::Pipe) -> io::Result { + let socket_fd = + syscall::connection_join(rx.as_handle(), tx.as_handle()).map_err(syscall_err)?; + // The join takes references of its own; these two are consumed here. + drop(rx); + drop(tx); + Ok(unsafe { OwnedFd::from_raw_fd(socket_fd.0 as i32) }) +} + +// --- Shared socket ownership (prevents double-close on duplicate) --- + +enum NetstackSocket { + Tcp(TcpSocketId), + Udp(UdpSocketId), +} + +impl Drop for NetstackSocket { + fn drop(&mut self) { + let _ = match self { + NetstackSocket::Tcp(id) => toyos::net::tcp_close(*id), + NetstackSocket::Udp(id) => toyos::net::udp_close(*id), + }; + } +} + +// --- TcpStream --- + +pub struct TcpStream { + fd: OwnedFd, + socket: Arc, + /// `shutdown` was asked for this half, on this stream or a duplicate. + read_shut: Arc, + write_shut: Arc, + peer: SocketAddr, + local_port: u16, + read_timeout_ms: AtomicU32, + write_timeout_ms: AtomicU32, + nodelay: AtomicBool, + nonblocking: AtomicBool, +} + +impl TcpStream { + fn new(fd: OwnedFd, id: TcpSocketId, peer: SocketAddr, local_port: u16) -> TcpStream { + TcpStream { + fd, + socket: Arc::new(NetstackSocket::Tcp(id)), + read_shut: Arc::new(AtomicBool::new(false)), + write_shut: Arc::new(AtomicBool::new(false)), + peer, + local_port, + read_timeout_ms: AtomicU32::new(0), + write_timeout_ms: AtomicU32::new(0), + nodelay: AtomicBool::new(false), + nonblocking: AtomicBool::new(false), + } + } + + fn socket_id(&self) -> TcpSocketId { + match *self.socket { + NetstackSocket::Tcp(id) => id, + _ => unreachable!(), + } + } + + pub fn connect(addr: A) -> io::Result { + super::each_addr(addr, |addr| Self::connect_timeout(addr, CONNECT_TIMEOUT)) + } + + pub fn connect_timeout(addr: &SocketAddr, timeout: Duration) -> io::Result { + let (ip, port) = addr_to_v4(addr)?; + let conn = toyos::net::tcp_connect(ip, port, duration_to_ms(Some(timeout))) + .map_err(net_err_to_io)?; + let fd = make_socket_fd(conn.rx, conn.tx)?; + Ok(TcpStream::new(fd, conn.socket_id, *addr, conn.local_port)) + } + + fn raw_handle(&self) -> RawHandle { + RawHandle(self.fd.as_raw_fd() as u32) + } + + pub fn set_read_timeout(&self, dur: Option) -> io::Result<()> { + self.read_timeout_ms.store(duration_to_ms(dur), Relaxed); + Ok(()) + } + + pub fn set_write_timeout(&self, dur: Option) -> io::Result<()> { + self.write_timeout_ms.store(duration_to_ms(dur), Relaxed); + Ok(()) + } + + pub fn read_timeout(&self) -> io::Result> { + let ms = self.read_timeout_ms.load(Relaxed); + Ok(if ms == 0 { None } else { Some(Duration::from_millis(ms as u64)) }) + } + + pub fn write_timeout(&self) -> io::Result> { + let ms = self.write_timeout_ms.load(Relaxed); + Ok(if ms == 0 { None } else { Some(Duration::from_millis(ms as u64)) }) + } + + pub fn peek(&self, _buf: &mut [u8]) -> io::Result { + Err(io::Error::new(io::ErrorKind::Unsupported, "peek not supported")) + } + + pub fn read(&self, buf: &mut [u8]) -> io::Result { + if buf.is_empty() || self.read_shut.load(Relaxed) { + return Ok(0); + } + let handle = self.raw_handle(); + let read = if self.nonblocking.load(Relaxed) { + syscall::read_nonblock(handle, buf) + } else { + match self.read_timeout_ms.load(Relaxed) { + 0 => syscall::read(handle, buf), + ms => with_timeout(handle, READABLE, ms, || syscall::read_nonblock(handle, buf)) + .ok_or(TIMED_OUT)?, + } + }; + match read.map_err(syscall_err)? { + 0 => self.ended().map(|()| 0), + n => Ok(n), + } + } + + /// Whether the receive pipe's end was the peer's FIN or not. netstack ends + /// the send pipe too, and first, when the connection did not end in + /// order — a reset, a timeout — so a send pipe with no + /// reader behind a receive pipe at its end is a reset. + fn ended(&self) -> io::Result<()> { + match syscall::write_nonblock(self.raw_handle(), &[]) { + Ok(_) | Err(SyscallError::WouldBlock) => Ok(()), + Err(SyscallError::Gone) => Err(RESET), + Err(e) => Err(syscall_err(e)), + } + } + + pub fn read_buf(&self, mut cursor: BorrowedCursor<'_, u8>) -> io::Result<()> { + let n = self.read(cursor.ensure_init())?; + cursor.advance_checked(n); + Ok(()) + } + + pub fn read_vectored(&self, bufs: &mut [IoSliceMut<'_>]) -> io::Result { + crate::io::default_read_vectored(|b| self.read(b), bufs) + } + + pub fn is_read_vectored(&self) -> bool { + false + } + + pub fn write(&self, buf: &[u8]) -> io::Result { + if buf.is_empty() { + return Ok(0); + } + if self.write_shut.load(Relaxed) { + return Err(SHUT_DOWN); + } + let handle = self.raw_handle(); + let written = if self.nonblocking.load(Relaxed) { + syscall::write_nonblock(handle, buf) + } else { + match self.write_timeout_ms.load(Relaxed) { + 0 => syscall::write(handle, buf), + ms => with_timeout(handle, WRITABLE, ms, || syscall::write_nonblock(handle, buf)) + .ok_or(TIMED_OUT)?, + } + }; + written.map_err(|e| match e { + SyscallError::Gone => RESET, + e => syscall_err(e), + }) + } + + pub fn write_vectored(&self, bufs: &[IoSlice<'_>]) -> io::Result { + crate::io::default_write_vectored(|b| self.write(b), bufs) + } + + pub fn is_write_vectored(&self) -> bool { + false + } + + pub fn peer_addr(&self) -> io::Result { + Ok(self.peer) + } + + pub fn socket_addr(&self) -> io::Result { + Ok(SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(10, 0, 2, 15), self.local_port))) + } + + pub fn shutdown(&self, how: Shutdown) -> io::Result<()> { + let how_val = match how { + Shutdown::Read => 0u32, + Shutdown::Write => 1, + Shutdown::Both => 2, + }; + toyos::net::tcp_shutdown(self.socket_id(), how_val).map_err(net_err_to_io)?; + if matches!(how, Shutdown::Read | Shutdown::Both) { + self.read_shut.store(true, Relaxed); + } + if matches!(how, Shutdown::Write | Shutdown::Both) { + self.write_shut.store(true, Relaxed); + } + Ok(()) + } + + pub fn duplicate(&self) -> io::Result { + let new_fd = syscall::dup(self.raw_handle()).map_err(syscall_err)?; + Ok(TcpStream { + fd: unsafe { OwnedFd::from_raw_fd(new_fd.0 as i32) }, + socket: Arc::clone(&self.socket), + read_shut: Arc::clone(&self.read_shut), + write_shut: Arc::clone(&self.write_shut), + peer: self.peer, + local_port: self.local_port, + read_timeout_ms: AtomicU32::new(self.read_timeout_ms.load(Relaxed)), + write_timeout_ms: AtomicU32::new(self.write_timeout_ms.load(Relaxed)), + nodelay: AtomicBool::new(self.nodelay.load(Relaxed)), + nonblocking: AtomicBool::new(self.nonblocking.load(Relaxed)), + }) + } + + pub fn set_linger(&self, _linger: Option) -> io::Result<()> { + Ok(()) + } + + pub fn linger(&self) -> io::Result> { + Ok(None) + } + + pub fn set_nodelay(&self, nodelay: bool) -> io::Result<()> { + toyos::net::tcp_set_option(self.socket_id(), toyos::net::OPT_NODELAY, nodelay as u32) + .map_err(net_err_to_io)?; + self.nodelay.store(nodelay, Relaxed); + Ok(()) + } + + pub fn nodelay(&self) -> io::Result { + Ok(self.nodelay.load(Relaxed)) + } + + pub fn set_keepalive(&self, _keepalive: bool) -> io::Result<()> { + Err(io::Error::new(io::ErrorKind::Unsupported, "keepalive not supported")) + } + + pub fn keepalive(&self) -> io::Result { + Err(io::Error::new(io::ErrorKind::Unsupported, "keepalive not supported")) + } + + pub fn set_ttl(&self, _ttl: u32) -> io::Result<()> { + Ok(()) + } + + pub fn ttl(&self) -> io::Result { + Ok(64) + } + + pub fn take_error(&self) -> io::Result> { + Ok(None) + } + + pub fn set_nonblocking(&self, nonblocking: bool) -> io::Result<()> { + self.nonblocking.store(nonblocking, Relaxed); + Ok(()) + } + + pub fn as_fd(&self) -> BorrowedFd<'_> { + self.fd.as_fd() + } + + pub fn as_raw_fd(&self) -> i32 { + self.fd.as_raw_fd() + } +} + +// No Drop impl — Arc handles close on last drop. +// OwnedFd drop closes the pipe-backed socket fd. + +impl fmt::Debug for TcpStream { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "TcpStream(fd={}, peer={})", self.fd.as_raw_fd(), self.peer) + } +} + +// --- TcpListener --- + +pub struct TcpListener { + notify_fd: OwnedFd, + socket: Arc, + local: SocketAddr, + nonblocking: AtomicBool, +} + +impl TcpListener { + fn socket_id(&self) -> TcpSocketId { + match *self.socket { + NetstackSocket::Tcp(id) => id, + _ => unreachable!(), + } + } + + pub fn bind(addr: A) -> io::Result { + let addr = addr + .to_socket_addrs()? + .next() + .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "no addresses found"))?; + 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) }, + socket: Arc::new(NetstackSocket::Tcp(bound.socket_id)), + local: SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::from(ip), bound.bound_port)), + nonblocking: AtomicBool::new(false), + }) + } + + pub fn socket_addr(&self) -> io::Result { + Ok(self.local) + } + + 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); + if self.nonblocking.load(Relaxed) { + syscall::read_nonblock(notify_fd, &mut byte).map_err(syscall_err)?; + } else { + syscall::read(notify_fd, &mut byte).map_err(syscall_err)?; + } + + let accepted = toyos::net::tcp_accept(self.socket_id()).map_err(net_err_to_io)?; + let fd = make_socket_fd(accepted.rx, accepted.tx)?; + + let peer = SocketAddr::V4(SocketAddrV4::new( + Ipv4Addr::from(accepted.remote_addr), + accepted.remote_port, + )); + Ok((TcpStream::new(fd, accepted.socket_id, peer, accepted.local_port), 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) }, + socket: Arc::clone(&self.socket), + local: self.local, + nonblocking: AtomicBool::new(self.nonblocking.load(Relaxed)), + }) + } + + pub fn set_ttl(&self, _ttl: u32) -> io::Result<()> { + Ok(()) + } + + pub fn ttl(&self) -> io::Result { + Ok(64) + } + + pub fn set_only_v6(&self, _only_v6: bool) -> io::Result<()> { + Ok(()) + } + + pub fn only_v6(&self) -> io::Result { + Ok(false) + } + + pub fn take_error(&self) -> io::Result> { + Ok(None) + } + + pub fn set_nonblocking(&self, nonblocking: bool) -> io::Result<()> { + self.nonblocking.store(nonblocking, Relaxed); + Ok(()) + } + + pub fn as_fd(&self) -> BorrowedFd<'_> { + self.notify_fd.as_fd() + } + + pub fn as_raw_fd(&self) -> i32 { + self.notify_fd.as_raw_fd() + } +} + +// No Drop impl — Arc handles close on last drop. + +impl fmt::Debug for TcpListener { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "TcpListener(fd={}, local={})", self.notify_fd.as_raw_fd(), self.local) + } +} + +// --- UdpSocket --- + +pub struct UdpSocket { + socket: Arc, + tx_fd: OwnedFd, + rx_fd: OwnedFd, + local: SocketAddr, + peer: crate::sync::Mutex>, + read_timeout_ms: AtomicU32, + write_timeout_ms: AtomicU32, +} + +impl UdpSocket { + fn socket_id(&self) -> UdpSocketId { + match *self.socket { + NetstackSocket::Udp(id) => id, + _ => unreachable!(), + } + } + + pub fn bind(addr: A) -> io::Result { + let addr = addr + .to_socket_addrs()? + .next() + .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "no addresses found"))?; + let (ip, port) = addr_to_v4(&addr)?; + 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) }, + local: SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::from(ip), bound.bound_port)), + peer: crate::sync::Mutex::new(None), + read_timeout_ms: AtomicU32::new(0), + write_timeout_ms: AtomicU32::new(0), + }) + } + + pub fn peer_addr(&self) -> io::Result { + self.peer + .lock() + .unwrap() + .ok_or_else(|| io::Error::new(io::ErrorKind::NotConnected, "not connected")) + } + + pub fn socket_addr(&self) -> io::Result { + Ok(self.local) + } + + pub fn recv_from(&self, buf: &mut [u8]) -> io::Result<(usize, SocketAddr)> { + let resp = + toyos::net::udp_recv_from(self.socket_id(), buf.len() as u32).map_err(net_err_to_io)?; + + let n = (resp.len as usize).min(buf.len()); + if n > 0 { + let rx_fd = RawHandle(self.rx_fd.as_raw_fd() as u32); + syscall::read(rx_fd, &mut buf[..n]).map_err(syscall_err)?; + } + + let addr = Ipv4Addr::new(resp.addr[0], resp.addr[1], resp.addr[2], resp.addr[3]); + Ok((n, SocketAddr::V4(SocketAddrV4::new(addr, resp.port)))) + } + + pub fn peek_from(&self, _buf: &mut [u8]) -> io::Result<(usize, SocketAddr)> { + Err(io::Error::new(io::ErrorKind::Unsupported, "peek not supported")) + } + + pub fn send_to(&self, buf: &[u8], addr: &SocketAddr) -> io::Result { + 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); + if !buf.is_empty() { + syscall::write(tx_fd, buf).map_err(syscall_err)?; + } + + let sent = toyos::net::udp_send_to(self.socket_id(), ip, port, buf.len() as u16) + .map_err(net_err_to_io)?; + Ok(sent as usize) + } + + 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) }, + local: self.local, + peer: crate::sync::Mutex::new(*self.peer.lock().unwrap()), + read_timeout_ms: AtomicU32::new(self.read_timeout_ms.load(Relaxed)), + write_timeout_ms: AtomicU32::new(self.write_timeout_ms.load(Relaxed)), + }) + } + + pub fn set_read_timeout(&self, dur: Option) -> io::Result<()> { + self.read_timeout_ms.store(duration_to_ms(dur), Relaxed); + Ok(()) + } + + pub fn set_write_timeout(&self, dur: Option) -> io::Result<()> { + self.write_timeout_ms.store(duration_to_ms(dur), Relaxed); + Ok(()) + } + + pub fn read_timeout(&self) -> io::Result> { + let ms = self.read_timeout_ms.load(Relaxed); + Ok(if ms == 0 { None } else { Some(Duration::from_millis(ms as u64)) }) + } + + pub fn write_timeout(&self) -> io::Result> { + let ms = self.write_timeout_ms.load(Relaxed); + Ok(if ms == 0 { None } else { Some(Duration::from_millis(ms as u64)) }) + } + + pub fn set_broadcast(&self, _broadcast: bool) -> io::Result<()> { + Ok(()) + } + + pub fn broadcast(&self) -> io::Result { + Ok(false) + } + + pub fn set_multicast_loop_v4(&self, _: bool) -> io::Result<()> { + Ok(()) + } + + pub fn multicast_loop_v4(&self) -> io::Result { + Ok(false) + } + + pub fn set_multicast_ttl_v4(&self, _: u32) -> io::Result<()> { + Ok(()) + } + + pub fn multicast_ttl_v4(&self) -> io::Result { + Ok(1) + } + + pub fn set_multicast_loop_v6(&self, _: bool) -> io::Result<()> { + Ok(()) + } + + pub fn multicast_loop_v6(&self) -> io::Result { + Ok(false) + } + + pub fn join_multicast_v4(&self, _: &Ipv4Addr, _: &Ipv4Addr) -> io::Result<()> { + Err(io::Error::new(io::ErrorKind::Unsupported, "multicast not supported")) + } + + pub fn join_multicast_v6(&self, _: &Ipv6Addr, _: u32) -> io::Result<()> { + Err(io::Error::new(io::ErrorKind::Unsupported, "multicast not supported")) + } + + pub fn leave_multicast_v4(&self, _: &Ipv4Addr, _: &Ipv4Addr) -> io::Result<()> { + Err(io::Error::new(io::ErrorKind::Unsupported, "multicast not supported")) + } + + pub fn leave_multicast_v6(&self, _: &Ipv6Addr, _: u32) -> io::Result<()> { + Err(io::Error::new(io::ErrorKind::Unsupported, "multicast not supported")) + } + + pub fn set_ttl(&self, _: u32) -> io::Result<()> { + Ok(()) + } + + pub fn ttl(&self) -> io::Result { + Ok(64) + } + + pub fn take_error(&self) -> io::Result> { + Ok(None) + } + + pub fn set_nonblocking(&self, _: bool) -> io::Result<()> { + Ok(()) + } + + pub fn recv(&self, buf: &mut [u8]) -> io::Result { + let (n, _) = self.recv_from(buf)?; + Ok(n) + } + + pub fn peek(&self, _buf: &mut [u8]) -> io::Result { + Err(io::Error::new(io::ErrorKind::Unsupported, "peek not supported")) + } + + pub fn send(&self, buf: &[u8]) -> io::Result { + let peer = self + .peer + .lock() + .unwrap() + .ok_or_else(|| io::Error::new(io::ErrorKind::NotConnected, "not connected"))?; + self.send_to(buf, &peer) + } + + pub fn connect(&self, addr: A) -> io::Result<()> { + let addr = addr + .to_socket_addrs()? + .next() + .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "no addresses found"))?; + *self.peer.lock().unwrap() = Some(addr); + Ok(()) + } +} + +// No Drop impl — Arc handles close on last drop. +// OwnedFd drops close the pipe fds. + +impl fmt::Debug for UdpSocket { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "UdpSocket(local={})", self.local) + } +} + +// --- LookupHost (DNS) --- + +pub struct LookupHost { + addrs: Vec, + pos: usize, +} + +impl Iterator for LookupHost { + type Item = SocketAddr; + fn next(&mut self) -> Option { + if self.pos < self.addrs.len() { + let addr = self.addrs[self.pos]; + self.pos += 1; + Some(addr) + } else { + None + } + } +} + +pub fn lookup_host(host: &str, port: u16) -> io::Result { + if let Ok(ip) = host.parse::() { + return Ok(LookupHost { addrs: vec![SocketAddr::V4(SocketAddrV4::new(ip, port))], pos: 0 }); + } + + let mut results = [[0u8; 4]; 16]; + let count = toyos::net::dns_lookup(host, &mut results).map_err(net_err_to_io)?; + + if count == 0 { + return Err(io::Error::new(io::ErrorKind::Other, "DNS lookup failed: no results")); + } + + let addrs = results[..count] + .iter() + .map(|ip| SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::from(*ip), port))) + .collect(); + + Ok(LookupHost { addrs, pos: 0 }) +} diff --git a/sdk/std/sys/pal/futex.rs b/sdk/std/sys/pal/futex.rs new file mode 100644 index 00000000000..8f144c2eede --- /dev/null +++ b/sdk/std/sys/pal/futex.rs @@ -0,0 +1,30 @@ +use crate::sync::atomic::Atomic; +use crate::time::Duration; + +pub type Futex = Atomic; +pub type Primitive = u32; + +pub type SmallFutex = Atomic; +pub type SmallPrimitive = u32; + +pub fn futex_wait(futex: &Atomic, expected: u32, timeout: Option) -> bool { + let timeout_ns = timeout.map(|d| u64::try_from(d.as_nanos()).unwrap_or(u64::MAX)); + + // SAFETY: futex points to a valid Atomic that outlives this call. + let r = unsafe { toyos_abi::syscall::futex_wait(futex.as_ptr(), expected, timeout_ns) }; + r != 1 // 1 = timed out +} + +#[inline] +pub fn futex_wake(futex: &Atomic) -> bool { + // SAFETY: futex points to a valid Atomic that outlives this call. + (unsafe { toyos_abi::syscall::futex_wake(futex.as_ptr(), 1) }) > 0 +} + +#[inline] +pub fn futex_wake_all(futex: &Atomic) { + // SAFETY: futex points to a valid Atomic that outlives this call. + unsafe { + toyos_abi::syscall::futex_wake(futex.as_ptr(), u32::MAX); + } +} diff --git a/sdk/std/sys/pal/mod.rs b/sdk/std/sys/pal/mod.rs new file mode 100644 index 00000000000..b1a603d1681 --- /dev/null +++ b/sdk/std/sys/pal/mod.rs @@ -0,0 +1,298 @@ +pub mod futex; +pub mod os; +pub mod tls; + +#[expect(dead_code)] +#[path = "../../../../rust/library/std/src/sys/pal/unsupported/common.rs"] +mod unsupported_common; + +use core::sync::atomic::{AtomicUsize, Ordering}; + +pub use unsupported_common::{cleanup, init}; + +/// The kernel's word as the `ErrorKind` a caller can act on. +/// +/// Exhaustive: a new `SyscallError` has to be given a kind here rather than +/// reaching callers as `Other` from whichever module mapped it last. +pub fn to_io_error(e: toyos_abi::syscall::SyscallError) -> crate::io::Error { + use toyos_abi::syscall::SyscallError; + + use crate::io::ErrorKind; + + let kind = match e { + SyscallError::Unknown => ErrorKind::Uncategorized, + SyscallError::NotFound => ErrorKind::NotFound, + SyscallError::PermissionDenied => ErrorKind::PermissionDenied, + SyscallError::AlreadyExists => ErrorKind::AlreadyExists, + SyscallError::InvalidArgument => ErrorKind::InvalidInput, + SyscallError::BadAddress => ErrorKind::InvalidInput, + SyscallError::WouldBlock => ErrorKind::WouldBlock, + SyscallError::ResourceExhausted => ErrorKind::OutOfMemory, + SyscallError::NotSupported => ErrorKind::Unsupported, + SyscallError::Io => ErrorKind::Other, + SyscallError::Gone => ErrorKind::BrokenPipe, + }; + crate::io::Error::from(kind) +} + +// 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 + +// Stack layout at entry (set up by kernel), with the stack pointer 16-byte aligned: +// [sp] = argc +// [sp+8] = argv[0], argv[1], ..., NULL +#[cfg(target_arch = "x86_64")] +#[unsafe(no_mangle)] +#[unsafe(naked)] +unsafe extern "C" fn _start() -> ! { + core::arch::naked_asm!( + "mov rdi, [rsp]", + "lea rsi, [rsp + 8]", + "call {start_rust}", + "ud2", + start_rust = sym start_rust, + ); +} + +#[cfg(target_arch = "aarch64")] +#[unsafe(no_mangle)] +#[unsafe(naked)] +unsafe extern "C" fn _start() -> ! { + core::arch::naked_asm!( + "ldr x0, [sp]", + "add x1, sp, #8", + // The outermost frame record: a backtrace ends here. + "mov x29, xzr", + "mov x30, xzr", + "bl {start_rust}", + "brk #0x1", + start_rust = sym start_rust, + ); +} + +/// .init_array constructor: registers the EH frame finder for DWARF unwinding. +/// For executables, this runs before `_start`. For cdylib .so files loaded via +/// dlopen, the kernel returns the .init_array to userspace which calls it. +/// This ensures panic unwinding works from code inside shared libraries. +extern "C" fn init_eh_frame() { + eh_frame::init(); +} + +#[used] +#[unsafe(link_section = ".init_array")] +static INIT_EH_FRAME: extern "C" fn() = init_eh_frame; + +extern "C" fn start_rust(argc: usize, argv: *const *const u8) -> ! { + unsafe extern "C" { + fn main(argc: i32, argv: *const *const u8) -> i32; + } + ARGC.store(argc, Ordering::Relaxed); + ARGV.store(argv.expose_provenance(), Ordering::Relaxed); + + // Register EH frame finder (also in .init_array for cdylib, but exes don't run .init_array) + eh_frame::init(); + + // Initialize environment variables + crate::sys::env::init(); + + let code = unsafe { main(argc as i32, argv) }; + crate::sys::stdio::finish(); + toyos_abi::syscall::exit(code) +} + +pub fn abort_internal() -> ! { + toyos_abi::syscall::exit(128 + 6) // SIGABRT-like — kill entire process +} + +// C allocator shims — many crates (zlib-rs, etc.) call malloc/free/calloc +// via extern "C". Route through the Rust global allocator (arena+slab) +// to avoid per-allocation syscalls. +mod c_allocator { + use crate::alloc::{GlobalAlloc, Layout, System}; + + /// The alignment every block has at least, and the bytes in front of a + /// block that hold its size and its alignment: the layout `free` and + /// `realloc` release it at is the one it was allocated with. + const MIN_ALIGN: usize = 16; + + /// A block of `size` bytes at `align`, the allocation beginning `align` + /// bytes before it. + fn layout(size: usize, align: usize) -> Option { + Layout::from_size_align(align.checked_add(size)?, align).ok() + } + + /// The size and alignment in front of `block`. + unsafe fn header(block: *mut u8) -> (usize, usize) { + unsafe { ((block.sub(16) as *const usize).read(), (block.sub(8) as *const usize).read()) } + } + + /// `size` bytes aligned to `align`, a power of two. + unsafe fn alloc(size: usize, align: usize) -> *mut u8 { + let align = align.max(MIN_ALIGN); + let Some(layout) = layout(size, align) else { return core::ptr::null_mut() }; + let raw = unsafe { System.alloc(layout) }; + if raw.is_null() { + return raw; + } + unsafe { + let block = raw.add(align); + (block.sub(16) as *mut usize).write(size); + (block.sub(8) as *mut usize).write(align); + block + } + } + + #[unsafe(no_mangle)] + unsafe extern "C" fn malloc(size: usize) -> *mut u8 { + if size == 0 { + return core::ptr::null_mut(); + } + unsafe { alloc(size, MIN_ALIGN) } + } + + /// C11's: null for an alignment that is no power of two. + #[unsafe(no_mangle)] + unsafe extern "C" fn aligned_alloc(align: usize, size: usize) -> *mut u8 { + if !align.is_power_of_two() { + return core::ptr::null_mut(); + } + unsafe { alloc(size, align) } + } + + #[unsafe(no_mangle)] + unsafe extern "C" fn calloc(count: usize, size: usize) -> *mut u8 { + let total = count.saturating_mul(size); + let ptr = malloc(total); + if !ptr.is_null() && total > 0 { + unsafe { core::ptr::write_bytes(ptr, 0, total) }; + } + ptr + } + + #[unsafe(no_mangle)] + unsafe extern "C" fn free(ptr: *mut u8) { + if ptr.is_null() { + return; + } + let (size, align) = unsafe { header(ptr) }; + let layout = layout(size, align).expect("a block's header is the layout it was allocated with"); + unsafe { System.dealloc(ptr.sub(align), layout) }; + } + + /// `System.realloc` keeps the allocation's alignment, so the block stays + /// `align` bytes in, its header carried with its bytes. + #[unsafe(no_mangle)] + unsafe extern "C" fn realloc(ptr: *mut u8, new_size: usize) -> *mut u8 { + if ptr.is_null() { + return malloc(new_size); + } + if new_size == 0 { + free(ptr); + return core::ptr::null_mut(); + } + let (size, align) = unsafe { header(ptr) }; + let Some(new) = layout(new_size, align) else { return core::ptr::null_mut() }; + let old = layout(size, align).expect("a block's header is the layout it was allocated with"); + let raw = unsafe { System.realloc(ptr.sub(align), old, new.size()) }; + if raw.is_null() { + return raw; + } + unsafe { + let block = raw.add(align); + (block.sub(16) as *mut usize).write(new_size); + block + } + } +} + +/// DWARF EH frame finder for the `unwinding` crate. +/// Locates `.eh_frame_hdr` for a given PC via `SYS_QUERY_MODULES`. +mod eh_frame { + use toyos_abi::syscall::ModuleInfo; + + use crate::sync::Mutex; + + struct Module { + base: usize, + end: usize, + eh_frame_hdr: usize, + eh_frame_hdr_size: usize, + } + + static CACHE: Mutex> = Mutex::new(Vec::new()); + + fn load_modules() -> Vec { + let mut buf = vec![0u8; 4096]; + loop { + match toyos_abi::syscall::query_modules(&mut buf) { + Ok(count) => { + let info_size = core::mem::size_of::(); + let mut modules = Vec::with_capacity(count); + for i in 0..count { + let off = i * info_size; + if off + info_size > buf.len() { + break; + } + let info = unsafe { &*(buf.as_ptr().add(off) as *const ModuleInfo) }; + modules.push(Module { + base: info.base as usize, + end: info.text_end as usize, + eh_frame_hdr: info.eh_frame_hdr as usize, + eh_frame_hdr_size: info.eh_frame_hdr_size as usize, + }); + } + return modules; + } + Err(_) => { + buf.resize(buf.len() * 2, 0); + if buf.len() > 1024 * 1024 { + return Vec::new(); + } + } + } + } + } + + struct ToyOsEhFrameFinder; + static FINDER: ToyOsEhFrameFinder = ToyOsEhFrameFinder; + + unsafe impl unwind::EhFrameFinder for ToyOsEhFrameFinder { + fn find(&self, pc: usize) -> Option { + // Fast path: check cached modules + { + let cache = CACHE.lock().unwrap_or_else(|e| e.into_inner()); + if let Some(m) = cache.iter().find(|m| pc >= m.base && pc < m.end) { + if m.eh_frame_hdr != 0 { + return Some(unwind::FrameInfo { + text_base: Some(m.base), + kind: unwind::FrameInfoKind::EhFrameHdr(m.eh_frame_hdr), + }); + } + return None; + } + } + + // Cache miss — reload module list (handles dlopen) + let modules = load_modules(); + let result = modules.iter().find(|m| pc >= m.base && pc < m.end).and_then(|m| { + if m.eh_frame_hdr != 0 { + Some(unwind::FrameInfo { + text_base: Some(m.base), + kind: unwind::FrameInfoKind::EhFrameHdr(m.eh_frame_hdr), + }) + } else { + None + } + }); + *CACHE.lock().unwrap_or_else(|e| e.into_inner()) = modules; + result + } + } + + pub(super) fn init() { + let modules = load_modules(); + *CACHE.lock().unwrap_or_else(|e| e.into_inner()) = modules; + unwind::set_custom_eh_frame_finder(&FINDER).ok(); + } +} diff --git a/sdk/std/sys/pal/os.rs b/sdk/std/sys/pal/os.rs new file mode 100644 index 00000000000..4c820bcaea0 --- /dev/null +++ b/sdk/std/sys/pal/os.rs @@ -0,0 +1,119 @@ +use crate::ffi::{OsStr, OsString}; +use crate::marker::PhantomData; +use crate::path::{self, PathBuf}; +use crate::{fmt, io}; + +pub fn getcwd() -> io::Result { + // `getcwd` returns the length the path *needs*, so a return that exceeds + // the buffer means nothing was written and names the size to retry with. + // The fixed buffer alone was a silent-truncation bug: any cwd past 256 + // bytes came back as a shorter, valid-looking path to a *different* + // directory, and every path derived from it pointed somewhere wrong. + let mut buf = [0u8; 256]; + let n = toyos_abi::syscall::getcwd(&mut buf); + if n == 0 { + return Err(io::Error::new(io::ErrorKind::Other, "getcwd failed")); + } + if n <= buf.len() { + return path_from_utf8(&buf[..n]); + } + + // Too long for the stack buffer: allocate exactly what it asked for. The + // kernel bounds cwd, so this retries once rather than looping. + let mut heap: crate::vec::Vec = crate::vec::Vec::new(); + heap.resize(n, 0); + let got = toyos_abi::syscall::getcwd(&mut heap); + if got == 0 || got > heap.len() { + return Err(io::Error::new(io::ErrorKind::Other, "getcwd failed")); + } + path_from_utf8(&heap[..got]) +} + +fn path_from_utf8(bytes: &[u8]) -> io::Result { + let s = core::str::from_utf8(bytes) + .map_err(|_| io::Error::new(io::ErrorKind::Other, "invalid utf-8 in cwd"))?; + Ok(PathBuf::from(s)) +} + +pub fn chdir(p: &path::Path) -> io::Result<()> { + crate::sys::fs::chdir(p) +} + +pub struct SplitPaths<'a> { + iter: crate::vec::IntoIter, + _marker: PhantomData<&'a ()>, +} + +pub fn split_paths(unparsed: &OsStr) -> SplitPaths<'_> { + let s = unparsed.as_encoded_bytes(); + let paths: crate::vec::Vec = if s.is_empty() { + crate::vec::Vec::new() + } else { + s.split(|&b| b == b':') + .map(|p| PathBuf::from(unsafe { OsStr::from_encoded_bytes_unchecked(p) })) + .collect() + }; + SplitPaths { iter: paths.into_iter(), _marker: PhantomData } +} + +impl<'a> Iterator for SplitPaths<'a> { + type Item = PathBuf; + fn next(&mut self) -> Option { + self.iter.next() + } +} + +#[derive(Debug)] +pub struct JoinPathsError; + +pub fn join_paths(paths: I) -> Result +where + I: Iterator, + T: AsRef, +{ + let mut joined = OsString::new(); + for (i, path) in paths.enumerate() { + if i > 0 { + joined.push(":"); + } + let p = path.as_ref(); + if p.as_encoded_bytes().contains(&b':') { + return Err(JoinPathsError); + } + joined.push(p); + } + Ok(joined) +} + +impl fmt::Display for JoinPathsError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + "path contains colon separator".fmt(f) + } +} + +impl crate::error::Error for JoinPathsError {} + +pub fn current_exe() -> io::Result { + let args: crate::vec::Vec<_> = crate::env::args().collect(); + if let Some(arg0) = args.first() { + Ok(PathBuf::from(arg0)) + } else { + Err(io::Error::new(io::ErrorKind::NotFound, "no argv[0]")) + } +} + +pub fn temp_dir() -> PathBuf { + PathBuf::from("/tmp") +} + +pub fn home_dir() -> Option { + crate::env::var_os("HOME").filter(|s| !s.is_empty()).map(PathBuf::from) +} + +pub fn exit(code: i32) -> ! { + toyos_abi::syscall::exit(code) +} + +pub fn getpid() -> u32 { + toyos_abi::syscall::getpid().0 +} diff --git a/sdk/std/sys/pal/tls.rs b/sdk/std/sys/pal/tls.rs new file mode 100644 index 00000000000..37174982911 --- /dev/null +++ b/sdk/std/sys/pal/tls.rs @@ -0,0 +1,89 @@ +/// DTV-based TLS access for shared libraries. +/// +/// Called by shared library code when accessing `#[thread_local]` variables of +/// a module whose TLS block is found through the dynamic thread vector (DTV). +/// +/// DTV layout: +/// [0x00] generation: u64 +/// [0x08] len: u64 +/// [0x10] entries[0]: u64 (module_id=1) +/// [0x18] entries[1]: u64 (module_id=2) +/// ... +/// +/// Entry value is the base address of that module's TLS block, +/// or DTV_UNALLOCATED (!0) if not yet allocated. +/// +/// x86-64 (TLS variant II, GD/LD model): the linker preserves +/// `call __tls_get_addr` in .so files and emits R_X86_64_DTPMOD64/DTPOFF64 GOT +/// slot pairs. At load time, the kernel fills: +/// GOT[0] = module_id (DTV index, 1-based) +/// GOT[1] = offset within module's TLS segment +/// +/// TCB layout (at fs_base / TP): +/// fs:[0x00] = self_ptr +/// fs:[0x08] = dtv_ptr +/// +/// __tls_get_addr receives a pointer to TlsIndex {module_id, offset} in %rdi, +/// returns the address of the TLS variable in %rax. +/// +/// AArch64 (TLS variant I). TCB layout (at TPIDR_EL0 / TP): +/// [TP+0x00] = dtv_ptr +/// [TP+0x08] = reserved +/// followed by the executable's TLS block at TP + align_up(16, p_align). +/// A shared library's thread-locals need a TLS descriptor resolver, which +/// AArch64 does not have yet; the loader refuses R_AARCH64_TLSDESC. + +/// Slow path: the DTV entry is unallocated or out of range. +/// Calls SYS_TLS_ALLOC_BLOCK to allocate the TLS block on demand. +#[cfg(target_arch = "x86_64")] +#[inline(never)] +unsafe extern "C" fn __tls_get_addr_slow(module_id: u64, offset: u64) -> *mut u8 { + // The caller's ABI is an address and there is nobody to return an error + // to: a refusal added to `offset` is a pointer near the top of the address + // space that the caller would then dereference. + match toyos_abi::syscall::tls_alloc_block(module_id) { + Ok(block) => core::ptr::without_provenance_mut((block + offset) as usize), + Err(_) => rtabort!("no TLS block for a dlopen'd module"), + } +} + +/// Fast path: naked asm reads DTV directly from fs:[8], checks bounds and allocation, +/// falls through to slow path only when needed. +#[cfg(target_arch = "x86_64")] +#[unsafe(no_mangle)] +#[unsafe(naked)] +pub unsafe extern "C" fn __tls_get_addr(ti: *const [u64; 2]) -> *mut u8 { + core::arch::naked_asm!( + // ti is in %rdi: [module_id, offset] + "mov rsi, [rdi + 8]", // rsi = offset + "mov rdi, [rdi]", // rdi = module_id + + // module_id == 0 guard (shouldn't happen, but be safe) + "test rdi, rdi", + "jz 2f", + + // Load DTV pointer from TCB: fs:[8] + "mov rax, fs:[8]", + + // Bounds check: module_id <= dtv[1] (len) + "cmp rdi, [rax + 8]", + "ja 2f", + + // Load DTV entry: dtv[2 + (module_id - 1)] + "lea rcx, [rdi - 1]", + "mov rax, [rax + rcx * 8 + 16]", + + // Check for DTV_UNALLOCATED (!0) + "cmp rax, -1", + "je 2f", + + // Fast path: return entry + offset + "add rax, rsi", + "ret", + + // Slow path + "2:", + "jmp {slow}", + slow = sym __tls_get_addr_slow, + ); +} diff --git a/sdk/std/sys/paths.rs b/sdk/std/sys/paths.rs new file mode 100644 index 00000000000..e0e7fd7e4ee --- /dev/null +++ b/sdk/std/sys/paths.rs @@ -0,0 +1,4 @@ +pub use crate::sys::pal::os::{ + JoinPathsError, SplitPaths, chdir, current_exe, getcwd, home_dir, join_paths, split_paths, + temp_dir, +}; diff --git a/sdk/std/sys/pipe.rs b/sdk/std/sys/pipe.rs new file mode 100644 index 00000000000..d8ea9c2f8c0 --- /dev/null +++ b/sdk/std/sys/pipe.rs @@ -0,0 +1,82 @@ +use toyos_abi::{RawHandle, syscall}; + +use crate::io::{self, BorrowedCursor, IoSlice, IoSliceMut}; +use crate::sys::to_io_error; + +#[derive(Debug)] +pub struct Pipe { + fd: RawHandle, +} + +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 })) +} + +impl Pipe { + pub fn raw_fd(&self) -> i32 { + self.fd.0 as i32 + } + + pub fn try_clone(&self) -> io::Result { + let new_fd = syscall::dup(self.fd).map_err(to_io_error)?; + Ok(Pipe { fd: new_fd }) + } + + pub fn read(&self, buf: &mut [u8]) -> io::Result { + syscall::read(self.fd, buf).map_err(to_io_error) + } + + pub fn read_buf(&self, mut buf: BorrowedCursor<'_, u8>) -> io::Result<()> { + let spare = buf.ensure_init(); + let n = self.read(spare)?; + unsafe { buf.advance(n) }; + Ok(()) + } + + pub fn read_vectored(&self, bufs: &mut [IoSliceMut<'_>]) -> io::Result { + match bufs.first_mut() { + Some(b) => self.read(b), + None => Ok(0), + } + } + + pub fn is_read_vectored(&self) -> bool { + false + } + + pub fn read_to_end(&self, buf: &mut Vec) -> io::Result { + let mut total = 0; + let mut tmp = [0u8; 4096]; + loop { + let n = self.read(&mut tmp)?; + if n == 0 { + break; + } + buf.extend_from_slice(&tmp[..n]); + total += n; + } + Ok(total) + } + + pub fn write(&self, buf: &[u8]) -> io::Result { + syscall::write(self.fd, buf).map_err(to_io_error) + } + + pub fn write_vectored(&self, bufs: &[IoSlice<'_>]) -> io::Result { + match bufs.first() { + Some(b) => self.write(b), + None => Ok(0), + } + } + + pub fn is_write_vectored(&self) -> bool { + false + } +} + +impl Drop for Pipe { + fn drop(&mut self) { + syscall::close(self.fd); + } +} diff --git a/sdk/std/sys/process.rs b/sdk/std/sys/process.rs new file mode 100644 index 00000000000..eab4e112298 --- /dev/null +++ b/sdk/std/sys/process.rs @@ -0,0 +1,894 @@ +use super::env::{CommandEnv, CommandEnvs, CommandResolvedEnvs}; +use crate::collections::BTreeMap; +pub use crate::ffi::OsString as EnvKey; +use crate::ffi::{OsStr, OsString}; +use crate::num::NonZero; +use crate::path::Path; +use crate::process::StdioPipes; +use crate::sys::fs::File; +use crate::sys::pipe::Pipe; +use crate::{fmt, io}; + +//////////////////////////////////////////////////////////////////////////////// +// Command +//////////////////////////////////////////////////////////////////////////////// + +pub struct Command { + program: OsString, + args: Vec, + env: CommandEnv, + + cwd: Option, + stdin: Option, + stdout: Option, + stderr: Option, + extra_slots: Vec<[u32; 2]>, + endowments: Vec<(String, u32)>, + provided: Vec<(String, u32)>, + parent: Parent, + /// The program the child runs, when it is not the one `argv[0]` names. + image_path: Option, + prepared: Option, +} + +/// Whom a child is placed under, whose end takes it down. +#[derive(Clone, Copy)] +enum Parent { + /// This process. + Caller, + /// The process a handle carrying `WRITE` names. + Place(u32), + /// The supervisor, asked through the launcher. + Supervisor, +} + +/// The file reads a spawn needs, made ahead of it ([`Command::prepare`]). +struct Prepared { + program: OsString, + cwd: String, + /// The program, when it is on a file server. + image: Option, +} + +/// Where a spawn goes once the launcher has been asked, or could not be. +enum Routed { + /// The supervisor started it. + Started(Process), + /// This process spawns it, with the `HOME` the supervisor answered for it if it did. + Direct { home: Option }, +} + +/// `KEY=VALUE\0` for each variable: the blob `SYS_SPAWN` and a launch both take. +fn env_blob(env: &BTreeMap) -> Vec { + let mut blob = Vec::new(); + for (key, value) in env { + blob.extend_from_slice(key.as_encoded_bytes()); + blob.push(b'='); + blob.extend_from_slice(value.as_encoded_bytes()); + blob.push(0); + } + blob +} + +#[derive(Debug)] +pub enum Stdio { + Inherit, + Null, + MakePipe, + MakeTtyPipe, + ParentStdout, + ParentStderr, + InheritFile(File), + InheritPipe(Pipe), +} + +impl Command { + pub fn new(program: &OsStr) -> Command { + Command { + program: program.to_owned(), + args: vec![program.to_owned()], + env: Default::default(), + cwd: None, + stdin: None, + stdout: None, + stderr: None, + extra_slots: Vec::new(), + endowments: Vec::new(), + provided: Vec::new(), + parent: Parent::Caller, + image_path: None, + prepared: None, + } + } + + pub fn arg(&mut self, arg: &OsStr) { + self.args.push(arg.to_owned()); + } + + /// Run the program at `path` rather than the one `argv[0]` names, which + /// stays the child's first argument. + pub fn image_from(&mut self, path: &OsStr) { + self.image_path = Some(path.to_owned()); + self.prepared = None; + } + + pub fn env_mut(&mut self) -> &mut CommandEnv { + &mut self.env + } + + pub fn cwd(&mut self, dir: &OsStr) { + self.cwd = Some(dir.to_owned()); + self.prepared = None; + } + + /// Find the program, judge the working directory and read a program on a + /// file server, so that [`Self::spawn`] calls no file server for them. + pub fn prepare(&mut self) -> io::Result<()> { + let program = self.resolve_program()?; + let cwd = self.child_cwd()?; + let source = Path::new(self.image_path.as_deref().unwrap_or(&program)); + let image = match crate::sys::fs::is_served(source) { + true => Some(crate::sys::fs::read_image(source)?), + false => None, + }; + self.prepared = Some(Prepared { program, cwd, image }); + Ok(()) + } + + pub fn stdin(&mut self, stdin: Stdio) { + self.stdin = Some(stdin); + } + + pub fn stdout(&mut self, stdout: Stdio) { + self.stdout = Some(stdout); + } + + pub fn stderr(&mut self, stderr: Stdio) { + self.stderr = Some(stderr); + } + + pub fn get_program(&self) -> &OsStr { + &self.program + } + + pub fn get_args(&self) -> CommandArgs<'_> { + let mut iter = self.args.iter(); + iter.next(); + CommandArgs { iter } + } + + pub fn get_envs(&self) -> CommandEnvs<'_> { + self.env.iter() + } + + pub fn get_env_clear(&self) -> bool { + self.env.does_clear() + } + + pub fn get_resolved_envs(&self) -> CommandResolvedEnvs { + CommandResolvedEnvs::new(self.env.capture()) + } + + pub fn get_current_dir(&self) -> Option<&Path> { + 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 `handle` under the name `label`. + /// + /// The handle is **moved**: after a successful spawn this process no longer + /// holds it. A caller that wants to keep one duplicates it first. + pub fn endow(&mut self, label: &str, handle: u32) { + self.endowments.push((label.to_owned(), handle)); + } + + /// Give the child `connector` under `name`, on top of its manifest row. + /// + /// Routes the spawn through the launcher, which is the only thing that can + /// build a child's authority out of the child's own declaration. + pub fn provide(&mut self, name: &str, connector: u32) { + self.provided.push((name.to_owned(), connector)); + } + + /// Place the child under the process `place` names rather than under this + /// one. + pub fn under(&mut self, place: u32) { + self.parent = Parent::Place(place); + } + + /// Ask the supervisor to be the child's parent, through the launcher or not at all. + pub fn under_supervisor(&mut self) { + self.parent = Parent::Supervisor; + } + + /// A duplicate of this process's own namespace handle, for the child to be + /// endowed under `svc`. + /// + /// `None` when the caller endowed one itself — it has decided what its + /// child may reach — and when this process has no namespace, which is a + /// program the manifest gives no `receives` and whose children therefore + /// have nothing to inherit. + fn inherited_namespace(&self) -> Option { + if self.endowments.iter().any(|(label, _)| label == toyos_abi::syscall::SVC_LABEL) { + return None; + } + let namespace = toyos::endow::namespace()?; + toyos_abi::syscall::dup(toyos::AsHandle::as_handle(namespace)).ok() + } + + fn resolve_program(&self) -> io::Result { + let prog = self.program.to_str().unwrap_or(""); + if prog.contains('/') { + return Ok(self.program.clone()); + } + // Search PATH for the executable + if let Some(path_var) = crate::env::var_os("PATH") { + for dir in crate::env::split_paths(&path_var) { + let candidate = dir.join(prog); + if candidate.exists() { + return Ok(candidate.into_os_string()); + } + } + } + Err(io::Error::from(io::ErrorKind::NotFound)) + } + + /// The directory the child starts in, always absolute: the kernel starts a + /// child only where its spawn says, and a relative `cwd` is relative to ours. + fn child_cwd(&self) -> io::Result { + let dir = match &self.cwd { + Some(dir) if Path::new(dir).is_absolute() => Path::new(dir).to_path_buf(), + Some(dir) => crate::env::current_dir()?.join(dir), + None => crate::env::current_dir()?, + }; + // The kernel judges a directory it serves itself; one on a file server + // is judged here, by that server, since the kernel cannot. + if crate::sys::fs::is_served(&dir) && !crate::fs::metadata(&dir)?.is_dir() { + return Err(io::const_error!( + io::ErrorKind::NotFound, + "working directory is not a directory", + )); + } + dir.into_os_string().into_string().map_err(|_| { + io::const_error!(io::ErrorKind::InvalidInput, "working directory is not UTF-8") + }) + } + + pub fn spawn( + &mut self, + default: Stdio, + _needs_stdin: bool, + ) -> io::Result<(Process, StdioPipes)> { + // The supervisor is reached only by a launch, which carries no endowment and no + // slot beyond stdio. + if matches!(self.parent, Parent::Supervisor) + && (!self.endowments.is_empty() || !self.extra_slots.is_empty()) + { + return Err(io::const_error!( + io::ErrorKind::PermissionDenied, + "the supervisor starts only a launch, which carries no endowment and no extra slot", + )); + } + // A launch runs the program its row names, never one the caller chose. + if self.image_path.is_some() + && (matches!(self.parent, Parent::Supervisor) || !self.provided.is_empty()) + { + return Err(io::const_error!( + io::ErrorKind::PermissionDenied, + "a launch runs its row's own program, so it takes no image the caller names", + )); + } + let prepared = self.prepared.take(); + let (resolved, cwd, prepared_image) = match prepared { + Some(Prepared { program, cwd, image }) => (program, cwd, image), + None => (self.resolve_program()?, self.child_cwd()?, None), + }; + let mut argv_buf = Vec::new(); + argv_buf.extend_from_slice(resolved.as_encoded_bytes()); + for arg in &self.args[1..] { + argv_buf.push(0); + argv_buf.extend_from_slice(arg.as_encoded_bytes()); + } + + let stdin = self.stdin.as_ref().unwrap_or(&default); + 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(); + let mut stdin_pipe: Option = None; + let mut stdout_pipe: Option = None; + let mut stderr_pipe: Option = None; + + // Resolve each stdio to a slot_map entry: [child_slot, parent_handle] + Self::setup_slot(&mut slot_map, &mut child_pipes, &mut streams, &mut stdin_pipe, stdin, 0, true)?; + Self::setup_slot(&mut slot_map, &mut child_pipes, &mut streams, &mut stdout_pipe, stdout, 1, false)?; + Self::setup_slot(&mut slot_map, &mut child_pipes, &mut streams, &mut stderr_pipe, stderr, 2, false)?; + + // Add extra handle mappings (e.g., for jobserver pipes) + slot_map.extend_from_slice(&self.extra_slots); + + let capture = self.env.capture(); + let env_buf = env_blob(&capture); + + // **The routing rule.** + // A caller that endowed a handle, named an extra slot or named the image + // has decided what its child holds or runs, and the launcher would + // overwrite that decision with a manifest row — so those spawn directly. + // Everything else asks the launcher when it holds one, and falls back for + // a program the image does not declare. A caller with no `launcher` connector gets plain + // inheritance, which is what a program endowed nothing should get — + // of everything but `HOME` (`direct_env`). A child asked of the supervisor is a + // launch or nothing. + let decided = !self.endowments.is_empty() + || !self.extra_slots.is_empty() + || self.image_path.is_some(); + let mut home_from_supervisor = None; + if !decided { + match self.launch(&resolved, &argv_buf, &env_buf, &cwd, &slot_map)? { + Routed::Started(process) => { + drop(child_pipes); + return Ok(( + process, + StdioPipes { stdin: stdin_pipe, stdout: stdout_pipe, stderr: stderr_pipe }, + )); + } + Routed::Direct { home } => home_from_supervisor = home, + } + } + // The label blob and the entries that index it, made after the routing: + // a launch carries neither, and a namespace copy made for one would be a + // handle nothing closes. Built here because the kernel reads both out of + // one call and keeps the blob for the child's life. + let mut labels = Vec::new(); + let mut endow = Vec::with_capacity(self.endowments.len() + 1); + let mut push = |label: &str, handle: u32, labels: &mut Vec| { + endow.push(toyos_abi::syscall::EndowEntry { + label_off: labels.len() as u32, + label_len: label.len() as u32, + handle: toyos_abi::RawHandle(handle), + _pad: 0, + }); + labels.extend_from_slice(label.as_bytes()); + }; + for (label, handle) in &self.endowments { + push(label, *handle, &mut labels); + } + // The child inherits this process's namespace unless the caller decided + // otherwise. 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. A caller that endows `svc` has decided what its child + // may reach and is not overruled here. + let inherited = self.inherited_namespace(); + if let Some(handle) = inherited { + push(toyos_abi::syscall::SVC_LABEL, handle.0, &mut labels); + } + let env_buf = env_blob(&self.direct_env(capture, home_from_supervisor)); + // A program on a file server is read here, into a memory object of + // this process's own that the kernel pages the child from: the kernel + // opens only what it serves itself. + let source = self.image_path.as_deref().unwrap_or(&resolved); + let image = match prepared_image { + Some(image) => Some(image), + None if crate::sys::fs::is_served(Path::new(source)) => { + Some(crate::sys::fs::read_image(Path::new(source)).map_err(|e| { + if let Some(handle) = inherited { + toyos_abi::syscall::close(handle); + } + e + })?) + } + None => None, + }; + + let spawn_args = toyos_abi::syscall::SpawnArgs { + path_ptr: source.as_encoded_bytes().as_ptr().expose_provenance() as u64, + path_len: source.len() as u64, + argv_ptr: argv_buf.as_ptr().expose_provenance() as u64, + argv_len: argv_buf.len() as u64, + slot_map_ptr: slot_map.as_ptr().expose_provenance() as u64, + slot_map_count: slot_map.len() as u64, + env_ptr: env_buf.as_ptr().expose_provenance() as u64, + env_len: env_buf.len() as u64, + endow_ptr: endow.as_ptr().expose_provenance() as u64, + endow_count: endow.len() as u64, + labels_ptr: labels.as_ptr().expose_provenance() as u64, + labels_len: labels.len() as u64, + cwd_ptr: cwd.as_ptr().expose_provenance() as u64, + cwd_len: cwd.len() as u64, + image: image.as_ref().map_or(0, |image| image.spawn_words().0), + image_len: image.as_ref().map_or(0, |image| image.spawn_words().1), + place: match self.parent { + Parent::Caller => u64::from(toyos_abi::HANDLE_INVALID.0), + Parent::Place(place) => u64::from(place), + Parent::Supervisor => { + unreachable!("a child asked of the supervisor is launched or refused") + } + }, + }; + // SAFETY: spawn_args contains valid pointers to stack-local buffers that outlive the call. + let spawned = unsafe { toyos_abi::syscall::spawn(&spawn_args) }; + // The child keeps the object alive; this process's handle, and with it + // its own mapping, goes. + drop(image); + + // Close child-side pipe ends in the parent + drop(child_pipes); + drop(streams); + + let handle = spawned.map_err(|e| { + // An endowment moves only on a spawn that happened, so the + // duplicate this call made is still ours and is ours to close. + if let Some(handle) = inherited { + toyos_abi::syscall::close(handle); + } + crate::sys::to_io_error(e) + })?; + + Ok(( + // SAFETY: the kernel installed this handle in our table for this + // call and no other. + Process { handle: unsafe { toyos::process::Process::from_raw(handle) } }, + StdioPipes { stdin: stdin_pipe, stdout: stdout_pipe, stderr: stderr_pipe }, + )) + } + + /// The environment a direct spawn carries: the caller's, except `HOME`. + /// + /// **A direct child's `HOME` is one its caller named or one the supervisor answered + /// for it, never the one this process was started with.** The supervisor decides + /// every program's `HOME` from its row, and a service's is its own + /// `/state/`: a child the supervisor never saw would otherwise carry a location + /// decided for its parent alone. + fn direct_env( + &self, + mut env: BTreeMap, + from_supervisor: Option, + ) -> BTreeMap { + let home = OsStr::new("HOME"); + let named = self.env.iter().find(|(key, _)| *key == home).map(|(_, value)| value); + env.remove(home); + match named { + Some(Some(value)) => { + env.insert(home.to_owned(), value.to_owned()); + } + Some(None) => {} + None => { + if let Some(value) = from_supervisor { + env.insert(home.to_owned(), value); + } + } + } + env + } + + /// Ask the supervisor to start this program, or answer [`Routed::Direct`] for + /// a caller that cannot or a program the manifest does not declare — but a + /// child asked of the supervisor is launched or refused `PermissionDenied`, never + /// spawned here. + /// + /// The stdio handles and the place are **duplicated** before they go: a + /// launch moves what it carries, and `Stdio::Inherit` names the parent's + /// own slot 1. + fn launch( + &self, + resolved: &OsStr, + argv: &[u8], + env: &[u8], + cwd: &str, + slot_map: &[[u32; 2]], + ) -> io::Result { + use toyos::launch::{self, Launch, LaunchError, MAX_LAUNCH_EXTRAS, Outcome}; + + let for_supervisor = matches!(self.parent, Parent::Supervisor); + // Where a launch that is not made goes: the direct spawn, which places + // the child where this one would have, or nowhere for the supervisor. + let direct = |home: Option| { + if for_supervisor { + Err(io::const_error!( + io::ErrorKind::PermissionDenied, + "the supervisor did not launch it" + )) + } else { + Ok(Routed::Direct { home }) + } + }; + + // A `provide` is a statement that the child's authority comes from its + // own manifest row plus this connector, and only the launcher can build + // that. Without one there is no weaker thing to fall back to that would + // still be what the caller asked for. + let Some(conn) = + toyos::endow::launcher().and_then(|held| held.open(toyos::launch::LAUNCHER).ok()) + else { + return if self.provided.is_empty() { + direct(None) + } else { + Err(io::Error::from(io::ErrorKind::PermissionDenied)) + }; + }; + + // The whole path, not a key: `/bin/ls` is a symlink to `/bin/toybox` + // and the row that says what an applet holds is `toybox`'s. Resolving + // that is the supervisor's — it holds the manifest and this process must not + // become a second reader of it. + let program = resolved.to_str().unwrap_or(""); + + // A place is one of the batch's extras. Refused rather than spawned + // directly: that child would hold this process's namespace, not its row. + if self.provided.len() + usize::from(!for_supervisor) > MAX_LAUNCH_EXTRAS { + return Err(io::const_error!( + io::ErrorKind::InvalidInput, + "more connectors provided than a launch carries beside its place", + )); + } + + let place = match self.parent { + Parent::Supervisor => None, + Parent::Place(place) => Some(toyos_abi::RawHandle(place)), + Parent::Caller => Some(toyos::AsHandle::as_handle(toyos::endow::this_process())), + }; + let parent = match place { + None => launch::Parent::Supervisor, + Some(place) => launch::Parent::Place( + toyos_abi::syscall::dup(place).map_err(crate::sys::to_io_error)?, + ), + }; + let release = |slots: &[(u32, toyos_abi::RawHandle)]| { + for (_, h) in slots { + toyos_abi::syscall::close(*h); + } + if let launch::Parent::Place(copy) = parent { + toyos_abi::syscall::close(copy); + } + }; + + let mut slots: Vec<(u32, toyos_abi::RawHandle)> = Vec::with_capacity(slot_map.len()); + for &[child_slot, parent] in slot_map { + match toyos_abi::syscall::dup(toyos_abi::RawHandle(parent)) { + Ok(copy) => slots.push((child_slot, copy)), + Err(e) => { + release(&slots); + return Err(crate::sys::to_io_error(e)); + } + } + } + let extras: Vec<(&str, toyos_abi::RawHandle)> = self + .provided + .iter() + .map(|(name, handle)| (name.as_str(), toyos_abi::RawHandle(*handle))) + .collect(); + let request = Launch { program, argv, env, cwd, extras: &extras, slots: &slots, parent }; + let mut home = crate::vec![0u8; toyos::ipc::MAX_FRAME_LEN as usize]; + let answer = launch::launch(&conn, &request, &mut home); + + // **The send moved them.** Every arm below but `NotSent` is past the + // point where these duplicates left this table, so closing them here + // would be this process naming a handle it does not hold — which the + // kernel answers by ending it. The launcher releases what it took. + match answer { + Ok(Outcome::Started(handle)) => { + // SAFETY: the supervisor moved this handle into our table and holds none. + Ok(Routed::Started(Process { + handle: unsafe { toyos::process::Process::from_raw(handle) }, + })) + } + // The direct path, which is what §4.5 clause 2 says an undeclared + // program gets. A caller that transferred connectors loses nothing + // by it: the supervisor merges a launched program's extras into the namespace + // it builds, so a caller that was itself launched already carries + // them, and the child inherits that. What the direct path cannot do + // is merge a name into an *inherited* namespace — no caller in the + // tree needs it, and + // `issues/isolation/a-provided-name-cannot-reach-an-undeclared-child.md` + // is where that is written down. + // It carries the `HOME` the supervisor decided for the program, which the + // direct spawn hands on in place of this process's own. + Ok(Outcome::NotDeclared { home }) => direct(Some(home.into())), + Ok(Outcome::Refused) | Err(LaunchError::Sent(_)) => { + Err(io::Error::from(io::ErrorKind::Other)) + } + Ok(Outcome::Gone) => Err(crate::sys::to_io_error(toyos_abi::syscall::SyscallError::Gone)), + Err(LaunchError::NotSent(_)) => { + release(&slots); + direct(None) + } + } + } + + fn setup_slot( + slot_map: &mut Vec<[u32; 2]>, + child_pipes: &mut Vec, + streams: &mut Vec, + parent_pipe: &mut Option, + stdio: &Stdio, + child_slot: u32, + is_input: bool, + ) -> io::Result<()> { + match stdio { + Stdio::Inherit => slot_map.push([child_slot, child_slot]), + 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)); + } + 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); + } + Stdio::InheritPipe(pipe) => slot_map.push([child_slot, pipe.raw_fd() as u32]), + Stdio::ParentStdout => slot_map.push([child_slot, 1]), + Stdio::ParentStderr => slot_map.push([child_slot, 2]), + Stdio::Null => {} + } + Ok(()) + } +} + +impl From for Stdio { + fn from(pipe: ChildPipe) -> Stdio { + Stdio::InheritPipe(pipe) + } +} + +impl From for Stdio { + fn from(_: io::Stdout) -> Stdio { + Stdio::ParentStdout + } +} + +impl From for Stdio { + fn from(_: io::Stderr) -> Stdio { + Stdio::ParentStderr + } +} + +impl From for Stdio { + fn from(file: File) -> Stdio { + Stdio::InheritFile(file) + } +} + +impl fmt::Debug for Command { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if f.alternate() { + let mut debug_command = f.debug_struct("Command"); + debug_command.field("program", &self.program).field("args", &self.args); + if !self.env.is_unchanged() { + debug_command.field("env", &self.env); + } + + if self.cwd.is_some() { + debug_command.field("cwd", &self.cwd); + } + + if self.stdin.is_some() { + debug_command.field("stdin", &self.stdin); + } + if self.stdout.is_some() { + debug_command.field("stdout", &self.stdout); + } + if self.stderr.is_some() { + debug_command.field("stderr", &self.stderr); + } + + debug_command.finish() + } else { + if let Some(ref cwd) = self.cwd { + write!(f, "cd {cwd:?} && ")?; + } + if self.env.does_clear() { + write!(f, "env -i ")?; + } else { + let mut any_removed = false; + for (key, value_opt) in self.get_envs() { + if value_opt.is_none() { + if !any_removed { + write!(f, "env ")?; + any_removed = true; + } + write!(f, "-u {} ", key.to_string_lossy())?; + } + } + } + for (key, value_opt) in self.get_envs() { + if let Some(value) = value_opt { + write!(f, "{}={value:?} ", key.to_string_lossy())?; + } + } + if self.program != self.args[0] { + write!(f, "[{:?}] ", self.program)?; + } + write!(f, "{:?}", self.args[0])?; + + for arg in &self.args[1..] { + write!(f, " {:?}", arg)?; + } + Ok(()) + } + } +} + +#[derive(PartialEq, Eq, Clone, Copy, Debug)] +pub struct ExitStatus(i32); + +impl Default for ExitStatus { + fn default() -> Self { + ExitStatus(0) + } +} + +impl ExitStatus { + pub fn exit_ok(&self) -> Result<(), ExitStatusError> { + if self.0 == 0 { Ok(()) } else { Err(ExitStatusError(self.0)) } + } + + pub fn code(&self) -> Option { + Some(self.0) + } +} + +impl fmt::Display for ExitStatus { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "exit status: {}", self.0) + } +} + +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct ExitStatusError(i32); + +impl Into for ExitStatusError { + fn into(self) -> ExitStatus { + ExitStatus(self.0) + } +} + +impl ExitStatusError { + pub fn code(self) -> Option> { + NonZero::new(self.0) + } +} + +#[derive(PartialEq, Eq, Clone, Copy, Debug)] +pub struct ExitCode(u8); + +impl ExitCode { + pub const SUCCESS: ExitCode = ExitCode(0); + pub const FAILURE: ExitCode = ExitCode(1); + + pub fn as_i32(&self) -> i32 { + self.0 as i32 + } +} + +impl From for ExitCode { + fn from(code: u8) -> Self { + Self(code) + } +} + +/// A child, as the handle its spawn answered with. +/// +/// There is no pid in here and no way back from one: what may wait for this +/// child, kill it or read its accounting is exactly what holds this handle. +pub struct Process { + handle: toyos::process::Process, +} + +impl Process { + /// The child's pid, which is a name and not a key. + /// + /// Read out of the accounting record rather than kept beside the handle: + /// this is a diagnostic and nothing in the tree calls it on a hot path, so + /// paying a syscall for it is better than a second copy of the identity. + /// Zero for a child whose accounting the kernel would not answer for, which + /// is a process torn down between the spawn and the question. + pub fn id(&self) -> u32 { + self.handle.stats().map_or(0, |s| s.pid) + } + + pub fn kill(&mut self) -> io::Result<()> { + self.handle.kill().map_err(|_| io::Error::from(io::ErrorKind::Other)) + } + + pub fn wait(&mut self) -> io::Result { + self.handle.wait().map(ExitStatus).map_err(|_| io::Error::from(io::ErrorKind::Other)) + } + + /// See `os::toyos::process::ChildExt::as_raw_handle`. + pub fn as_raw_handle(&self) -> u32 { + toyos::AsHandle::as_handle(&self.handle).0 + } + + /// Give up the handle. See `os::toyos::process::ChildExt::into_raw_handle`. + pub fn into_raw_handle(self) -> u32 { + self.handle.into_raw().0 + } + + pub fn try_wait(&mut self) -> io::Result> { + match self.handle.try_wait() { + Ok(code) => Ok(Some(ExitStatus(code))), + Err(toyos_abi::syscall::SyscallError::WouldBlock) => Ok(None), + Err(_) => Err(io::Error::from(io::ErrorKind::Other)), + } + } +} + +pub struct CommandArgs<'a> { + iter: crate::slice::Iter<'a, OsString>, +} + +impl<'a> Iterator for CommandArgs<'a> { + type Item = &'a OsStr; + fn next(&mut self) -> Option<&'a OsStr> { + self.iter.next().map(|os| &**os) + } + fn size_hint(&self) -> (usize, Option) { + self.iter.size_hint() + } +} + +impl<'a> ExactSizeIterator for CommandArgs<'a> { + fn len(&self) -> usize { + self.iter.len() + } + fn is_empty(&self) -> bool { + self.iter.is_empty() + } +} + +impl<'a> fmt::Debug for CommandArgs<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_list().entries(self.iter.clone()).finish() + } +} + +pub type ChildPipe = Pipe; + +pub fn getpid() -> u32 { + toyos_abi::syscall::getpid().0 +} + +pub fn read_output( + out: ChildPipe, + stdout: &mut Vec, + err: ChildPipe, + stderr: &mut Vec, +) -> io::Result<()> { + // Read both pipes concurrently to avoid deadlock: if the child fills one + // pipe buffer while we're blocking on the other, both sides stall. + use crate::thread; + let err_thread = thread::spawn(move || { + let mut buf = Vec::new(); + err.read_to_end(&mut buf).map(|_| buf) + }); + out.read_to_end(stdout)?; + match err_thread.join() { + Ok(Ok(buf)) => { + *stderr = buf; + Ok(()) + } + Ok(Err(e)) => Err(e), + Err(_) => Err(io::Error::new(io::ErrorKind::Other, "stderr reader thread panicked")), + } +} diff --git a/sdk/std/sys/random.rs b/sdk/std/sys/random.rs new file mode 100644 index 00000000000..cc2c2434fd0 --- /dev/null +++ b/sdk/std/sys/random.rs @@ -0,0 +1,5 @@ +pub fn fill_bytes(buf: &mut [u8]) { + if let Err(e) = toyos_abi::syscall::random(buf) { + panic!("failed to generate random data: {e:?}"); + } +} diff --git a/sdk/std/sys/stdio.rs b/sdk/std/sys/stdio.rs new file mode 100644 index 00000000000..02b9b7b0df2 --- /dev/null +++ b/sdk/std/sys/stdio.rs @@ -0,0 +1,227 @@ +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; + +const STDIN: RawHandle = RawHandle(0); + +// --------------------------------------------------------------------------- +// Stdin mode flag (canonical by default, raw when explicitly switched) +// --------------------------------------------------------------------------- + +static STDIN_RAW: AtomicBool = AtomicBool::new(false); + +pub fn set_stdin_raw(raw: bool) { + STDIN_RAW.store(raw, Ordering::Relaxed); +} + +// --------------------------------------------------------------------------- +// Canonical line buffer +// --------------------------------------------------------------------------- + +const LINE_BUF_CAP: usize = 256; + +struct LineBuf { + buf: [u8; LINE_BUF_CAP], + len: usize, + pos: usize, +} + +// Safety: Stdin::read() is always called under the global Stdin mutex and ToyOS +// is single-threaded, so there is no concurrent access. +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)?; + if n == 0 { Err(io::Error::new(io::ErrorKind::UnexpectedEof, "eof")) } else { Ok(byte[0]) } +} + +fn echo(bytes: &[u8]) { + let _ = log_stdio::write(Stream::Out, bytes); +} + +/// Canonical read: line editing with echo. Buffers a complete line, then +/// serves bytes from the buffer on subsequent calls. +fn canonical_read(buf: &mut [u8]) -> io::Result { + // Safety: see LINE_BUF comment above. + let lb = unsafe { &mut *core::ptr::addr_of_mut!(LINE_BUF) }; + + // Serve remaining data from a previous line first. + if lb.pos < lb.len { + let avail = lb.len - lb.pos; + let n = avail.min(buf.len()); + buf[..n].copy_from_slice(&lb.buf[lb.pos..lb.pos + n]); + lb.pos += n; + return Ok(n); + } + + // Read a new line with echo + backspace handling. + lb.len = 0; + lb.pos = 0; + + loop { + let ch = read_one()?; + match ch { + b'\r' | b'\n' => { + // Translate CR to LF (like Unix terminal driver ICRNL) + echo(b"\n"); + if lb.len < LINE_BUF_CAP { + lb.buf[lb.len] = b'\n'; + lb.len += 1; + } + let n = lb.len.min(buf.len()); + buf[..n].copy_from_slice(&lb.buf[..n]); + lb.pos = n; + return Ok(n); + } + 0x08 | 0x7F => { + // Backspace: erase last UTF-8 character + if lb.len > 0 { + // Scan past continuation bytes (10xxxxxx) + lb.len -= 1; + while lb.len > 0 && (lb.buf[lb.len] & 0xC0) == 0x80 { + lb.len -= 1; + } + echo(b"\x08 \x08"); + } + } + ch if ch >= 0x20 || (ch & 0xC0) == 0x80 => { + // Printable ASCII or UTF-8 continuation byte + if lb.len < LINE_BUF_CAP - 1 { + lb.buf[lb.len] = ch; + lb.len += 1; + echo(&[ch]); + } + } + ch if (ch & 0xC0) == 0xC0 => { + // UTF-8 lead byte + if lb.len < LINE_BUF_CAP - 1 { + lb.buf[lb.len] = ch; + lb.len += 1; + echo(&[ch]); + } + } + _ => {} // ignore other control characters + } + } +} + +// --------------------------------------------------------------------------- +// Stdin +// --------------------------------------------------------------------------- + +pub struct Stdin; +pub struct Stdout; +pub struct Stderr; + +impl Stdin { + pub const fn new() -> Stdin { + Stdin + } +} + +impl io::Read for Stdin { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + let stat = syscall::fstat(STDIN).ok(); + let interactive = + stat.is_some_and(|s| s.file_type == FileType::Keyboard || s.file_type == FileType::Tty); + if STDIN_RAW.load(Ordering::Relaxed) || !interactive { + syscall::read(STDIN, buf).map_err(to_io_error) + } else { + canonical_read(buf) + } + } + + fn read_vectored(&mut self, bufs: &mut [IoSliceMut<'_>]) -> io::Result { + let buf = match bufs.first_mut() { + Some(b) => b, + None => return Ok(0), + }; + self.read(buf) + } + + fn is_read_vectored(&self) -> bool { + false + } +} + +impl Stdout { + pub const fn new() -> Stdout { + Stdout + } +} + +impl io::Write for Stdout { + fn write(&mut self, buf: &[u8]) -> io::Result { + log_stdio::write(Stream::Out, buf).map_err(to_io_error) + } + + fn write_vectored(&mut self, bufs: &[IoSlice<'_>]) -> io::Result { + let mut total = 0; + for buf in bufs { + total += self.write(buf)?; + } + Ok(total) + } + + fn is_write_vectored(&self) -> bool { + false + } + + fn flush(&mut self) -> io::Result<()> { + log_stdio::flush(Stream::Out); + Ok(()) + } +} + +impl Stderr { + pub const fn new() -> Stderr { + Stderr + } +} + +impl io::Write for Stderr { + fn write(&mut self, buf: &[u8]) -> io::Result { + log_stdio::write(Stream::Err, buf).map_err(to_io_error) + } + + fn write_vectored(&mut self, bufs: &[IoSlice<'_>]) -> io::Result { + let mut total = 0; + for buf in bufs { + total += self.write(buf)?; + } + Ok(total) + } + + fn is_write_vectored(&self) -> bool { + false + } + + fn flush(&mut self) -> io::Result<()> { + log_stdio::flush(Stream::Err); + Ok(()) + } +} + +pub const STDIN_BUF_SIZE: usize = 64; + +pub fn is_ebadf(_err: &io::Error) -> bool { + true +} + +pub fn panic_output() -> Option { + Some(Stderr::new()) +} + +/// What either stream holds of a line the process has not ended, written out +/// as that line's end as the process leaves: a stream that is a log ring +/// keeps a partial line until its newline. +pub fn finish() { + log_stdio::end(Stream::Out); + log_stdio::end(Stream::Err); +} diff --git a/sdk/std/sys/thread.rs b/sdk/std/sys/thread.rs new file mode 100644 index 00000000000..b19a3f53cf4 --- /dev/null +++ b/sdk/std/sys/thread.rs @@ -0,0 +1,98 @@ +use toyos_abi::syscall; + +use crate::ffi::CStr; +use crate::io; +use crate::num::NonZero; +use crate::thread::ThreadInit; +use crate::time::Duration; + +pub struct Thread { + tid: u32, +} + +unsafe impl Send for Thread {} +unsafe impl Sync for Thread {} + +pub const DEFAULT_MIN_STACK_SIZE: usize = 2 * 1024 * 1024; + +impl Thread { + pub unsafe fn new(stack: usize, init: Box) -> io::Result { + let stack_size = stack.max(DEFAULT_MIN_STACK_SIZE); + // Allocate user stack (page-aligned) + let layout = crate::alloc::Layout::from_size_align(stack_size, 4096).unwrap(); + let stack_base = unsafe { crate::alloc::alloc(layout) }; + if stack_base.is_null() { + return Err(io::const_error!( + io::ErrorKind::OutOfMemory, + "thread stack allocation failed" + )); + } + let stack_top = stack_base.expose_provenance() as u64 + stack_size as u64; + + let data = Box::into_raw(init); + // SAFETY: entry point and stack are valid; data is a valid pointer to the thread init. + let tid = unsafe { + syscall::thread_spawn( + (thread_trampoline as *const ()).expose_provenance() as u64, + stack_top, + data.expose_provenance() as u64, + stack_base.expose_provenance() as u64, + ) + }; + + if syscall::SyscallError::from_u64(tid).is_some() { + unsafe { + drop(Box::from_raw(data)); + } + unsafe { + crate::alloc::dealloc(stack_base, layout); + } + return Err(io::const_error!(io::ErrorKind::Uncategorized, "thread spawn failed")); + } + + Ok(Thread { tid: tid as u32 }) + } + + pub fn join(self) { + syscall::thread_join(self.tid as u64); + } +} + +extern "C" fn thread_trampoline(data: u64) { + let init = unsafe { + Box::from_raw(crate::ptr::with_exposed_provenance_mut::(data as usize)) + }; + let main = init.init(); + main(); + // Run TLS destructors then clean up the current thread handle. + // The guard module is no-op on ToyOS, so we drive cleanup explicitly. + unsafe { + crate::sys::thread_local::destructors::run(); + } + crate::rt::thread_cleanup(); + syscall::thread_exit(0); +} + +pub fn available_parallelism() -> io::Result> { + // ToyOS runs on QEMU with a known number of CPUs, but we don't expose + // a syscall for this yet. Return 1 for now. + Ok(unsafe { NonZero::new_unchecked(1) }) +} + +pub fn current_os_id() -> Option { + None +} + +pub fn yield_now() { + // No yield syscall yet — spin hint + core::hint::spin_loop(); +} + +pub fn set_name(name: &CStr) { + syscall::set_thread_name(name.to_bytes()); +} + +pub fn sleep(dur: Duration) { + let nanos = dur.as_nanos().min(u64::MAX as u128) as u64; + syscall::nanosleep(nanos); +} diff --git a/sdk/std/sys/time.rs b/sdk/std/sys/time.rs new file mode 100644 index 00000000000..b8f0041b87f --- /dev/null +++ b/sdk/std/sys/time.rs @@ -0,0 +1,58 @@ +use crate::time::Duration; + +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)] +pub struct Instant(Duration); + +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)] +pub struct SystemTime(Duration); + +pub const UNIX_EPOCH: SystemTime = SystemTime(Duration::from_secs(0)); + +impl Instant { + pub fn now() -> Instant { + Instant(Duration::from_nanos(toyos_abi::clock::nanos_since_boot())) + } + + pub fn checked_sub_instant(&self, other: &Instant) -> Option { + self.0.checked_sub(other.0) + } + + pub fn checked_add_duration(&self, other: &Duration) -> Option { + Some(Instant(self.0.checked_add(*other)?)) + } + + pub fn checked_sub_duration(&self, other: &Duration) -> Option { + Some(Instant(self.0.checked_sub(*other)?)) + } +} + +impl SystemTime { + pub const MAX: SystemTime = SystemTime(Duration::MAX); + + pub const MIN: SystemTime = SystemTime(Duration::ZERO); + + /// Panics on a machine whose clock will not answer, as the unix + /// implementation does for a failing `clock_gettime`: 1970 would be + /// indistinguishable from a machine that really is at the epoch. + pub fn now() -> SystemTime { + let secs = toyos_abi::syscall::clock_epoch() + .expect("SYS_CLOCK_EPOCH: this machine will not say what time it is"); + SystemTime(Duration::from_secs(secs)) + } + + pub fn sub_time(&self, other: &SystemTime) -> Result { + self.0.checked_sub(other.0).ok_or_else(|| other.0 - self.0) + } + + pub fn checked_add_duration(&self, other: &Duration) -> Option { + Some(SystemTime(self.0.checked_add(*other)?)) + } + + pub fn checked_sub_duration(&self, other: &Duration) -> Option { + Some(SystemTime(self.0.checked_sub(*other)?)) + } + + pub(crate) fn from_nanos(nanos: u64) -> SystemTime { + SystemTime(Duration::from_nanos(nanos)) + } +} diff --git a/src/CLAUDE.md b/src/CLAUDE.md index 8305ccad4ef..2729d46d48f 100644 --- a/src/CLAUDE.md +++ b/src/CLAUDE.md @@ -15,7 +15,7 @@ Loads when you read a file under `src/` — the root cargo project, package name - Everything under a worktree — targets, images, `.build-locks/`, its fork checkout — is its own; the object stores, the compiler and the rustup link are the primary checkout's, and ownership is derived from `git rev-parse --git-common-dir`, never recorded. - **A linked worktree's `main` ref is only as current as the primary's last `git pull`: anything asking "does this branch differ from main" diffs against `origin/main`.** -- **Type-checking a std edit without building a sysroot**: point `__CARGO_TESTS_ONLY_SRC_ROOT` at a tree holding a copy of `rust/library`, a workspace `Cargo.toml` naming `library/std`, and symlinks to `toyos-abi`/`toyos`; then `CARGO_TARGET_DIR= cargo +toyos build -Z build-std=std,panic_abort --target x86_64-unknown-toyos --offline`. Delete `/**/.fingerprint/std-*` between runs — cargo does not re-fingerprint std under `-Zbuild-std`. +- **Type-checking a std edit without building a sysroot**: point `__CARGO_TESTS_ONLY_SRC_ROOT` at a tree holding a copy of `rust/library`, a workspace `Cargo.toml` naming `library/std`, and symlinks to `toyos-abi`, `toyos` and `sdk` — through the `sdk` link the backend's two `#[path]`s back into the fork read `os_str.rs` and `common.rs` from the worktree's own `rust/library`, never the copy, so that checkout must exist and an edit to either is made there; then `CARGO_TARGET_DIR= cargo +toyos build -Z build-std=std,panic_abort --target x86_64-unknown-toyos --offline`. Delete `/**/.fingerprint/std-*` between runs — cargo does not re-fingerprint std under `-Zbuild-std`. ## Caveats that bite every agent diff --git a/src/sourcegate.rs b/src/sourcegate.rs index 092bc52b608..9df06dc8134 100644 --- a/src/sourcegate.rs +++ b/src/sourcegate.rs @@ -159,6 +159,7 @@ const GUEST_CODE: &[&str] = &[ "kernel/src", "toyos/src", "toyos-abi/src", + "sdk/std", "userland", "tests/toyos-rust-tests", "tests/testcases", diff --git a/src/sysroot.rs b/src/sysroot.rs index fb204c696b4..bd8b40d609b 100644 --- a/src/sysroot.rs +++ b/src/sysroot.rs @@ -23,8 +23,9 @@ //! moved to the commit its tree pins; //! a linked worktree in its own `rust/`, made on first need as a git worktree of //! the primary's fork repository at the commit this tree pins ([`fork_checkout`]). -//! `library/std` names `toyos-abi` and `toyos` as `../../../`, so each -//! checkout's std compiles against its own worktree's ABI with nothing +//! `library/std` names `toyos-abi` and `toyos` as `../../../` and each file of +//! its ToyOS backend, `sdk/std`, by a `#[path]` as far up, so each checkout's +//! std compiles against its own worktree's ABI and backend with nothing //! rewritten. The build is bootstrap's stage-0 local rebuild: the compiler the //! checkout names (`src/compiler.rs` — the primary's `stage2`, or one of the //! worktree's own where its `compiler/` differs) compiles the checkout's @@ -60,11 +61,12 @@ use crate::toolchain::{self, host_triple, GuestTarget, Owner, Role, GUEST_TARGET use whole_toolchain::{whole, Whole}; /// The per-worktree sources that end up inside a sysroot: std links `toyos-abi` -/// and `toyos`, and `libtoyos_c.a` is `userland/libc` with `toyos-elf` and -/// `toyos-osrelease`. -pub const SYSROOT_SOURCES: [&str; 6] = [ +/// and `toyos` and compiles its ToyOS backend from `sdk/std`, and +/// `libtoyos_c.a` is `userland/libc` with `toyos-elf` and `toyos-osrelease`. +pub const SYSROOT_SOURCES: [&str; 7] = [ "toyos-abi/src", "toyos/src", + "sdk/std", "toyos-elf/src", "toyos-osrelease/src", "userland/libc/src", @@ -1222,7 +1224,7 @@ mod tests { write(&abi, "/// A.\npub struct A;\n"); same("toyos-abi as it was"); - for tree in ["toyos/src", "userland/libc/src"] { + for tree in ["toyos/src", "sdk/std", "userland/libc/src"] { write(&root.join(tree).join("lib.rs"), "/// A.\npub struct A(u8);\n"); sysroot_only(tree); write(&root.join(tree).join("lib.rs"), "/// A.\npub struct A;\n"); diff --git a/src/toolchain.rs b/src/toolchain.rs index b79672f04e1..5751ed7bc81 100644 --- a/src/toolchain.rs +++ b/src/toolchain.rs @@ -73,7 +73,8 @@ pub fn rust_dir(root: &Path) -> PathBuf { } } -/// Of the sysroot's sources, the ones std compiles. +/// Of the sysroot's sources, the crates std links, which its dep-info names +/// by the path cargo resolved; `sdk/std` it names through the fork. const STD_SOURCES: [&str; 2] = ["toyos-abi/src", "toyos/src"]; /// Every target a guest artifact is built for: ToyOS userland, the kernel's @@ -1455,9 +1456,9 @@ mod tests { ["/Users/jan/Dev/jan/toyos-endow/toyos-abi/src/lib.rs"], ); - // `rust/library/std/src/sys/pal/toyos/` is not one of these trees. + // std's ToyOS backend is not one of these trees. assert!( - toyos_sources_in_dep_info("/x/rust/library/std/src/sys/pal/toyos/mod.rs").is_empty() + toyos_sources_in_dep_info("library/std/src/sys/pal/../../../../../../sdk/std/sys/pal/mod.rs").is_empty() ); } diff --git a/tests/metal/lenovo-20w0003amz.toml b/tests/metal/lenovo-20w0003amz.toml index 2d83f9bd26c..929ff2ad3bf 100644 --- a/tests/metal/lenovo-20w0003amz.toml +++ b/tests/metal/lenovo-20w0003amz.toml @@ -58,6 +58,9 @@ bios = "N34ET71W (1.71 )" "boot.shared.complete_ms" = 1154 "boot.shared.panel_max_us" = 3608 "boot.shared.panel_us" = 21429 +"boot.testcases-bounds.complete_ms" = 1137 +"boot.testcases-bounds.panel_max_us" = 2493 +"boot.testcases-bounds.panel_us" = 12748 "boot.testcases-deaf.complete_ms" = 1165 "boot.testcases-deaf.panel_max_us" = 5247 "boot.testcases-deaf.panel_us" = 26659