diff --git a/kernel/src/Api/BuildNo.hpp b/kernel/src/Api/BuildNo.hpp index 47643ad..9aa0514 100644 --- a/kernel/src/Api/BuildNo.hpp +++ b/kernel/src/Api/BuildNo.hpp @@ -12,4 +12,4 @@ #pragma once -#define MONTAUK_BUILD_NUMBER 1 +#define MONTAUK_BUILD_NUMBER 3 diff --git a/kernel/src/Api/Filesystem.hpp b/kernel/src/Api/Filesystem.hpp index fb69bcf..42627fd 100644 --- a/kernel/src/Api/Filesystem.hpp +++ b/kernel/src/Api/Filesystem.hpp @@ -27,6 +27,23 @@ namespace montauk::abi { return required == 0 || Sched::HasCapability(required); } + // Drivers fail a create under a missing parent with a bare -1; report it + // as NoSuchPath so callers can tell it apart (Rust's create_dir_all only + // creates the missing ancestors when mkdir says NotFound). + static bool ParentDirExists(const char* resolved) { + char parent[256]; + int len = Lib::strlen(resolved); + if (len >= (int)sizeof(parent)) return false; + memcpy(parent, resolved, len + 1); + while (len > 0 && parent[len - 1] == '/') len--; + while (len > 0 && parent[len - 1] != '/') len--; + while (len > 0 && parent[len - 1] == '/') len--; + if (len == 0 || parent[len - 1] == ':') return true; // drive root + parent[len] = '\0'; + Fs::Vfs::StatInfo info; + return Fs::Vfs::VfsStat(parent, info) == 0 && info.isDir; + } + static int Sys_Open(const char* path) { char resolved[256]; if (!ResolveProcessPath(path, resolved, sizeof(resolved))) return InvalidArgument; @@ -117,6 +134,7 @@ namespace montauk::abi { char resolved[256]; if (!ResolveProcessPath(path, resolved, sizeof(resolved))) return InvalidArgument; if (!CanModifyFilePath(resolved)) return SystemIntegrityError; + if (!ParentDirExists(resolved)) return NoSuchPath; return Ipc::CreateFileHandle(resolved); } @@ -171,6 +189,7 @@ namespace montauk::abi { char resolved[256]; if (!ResolveProcessPath(path, resolved, sizeof(resolved))) return InvalidArgument; if (!CanModifyFilePath(resolved)) return SystemIntegrityError; + if (!ParentDirExists(resolved)) return NoSuchPath; return Fs::Vfs::VfsMkdir(resolved); } diff --git a/kernel/src/Api/Process.hpp b/kernel/src/Api/Process.hpp index f1e3801..6009de5 100644 --- a/kernel/src/Api/Process.hpp +++ b/kernel/src/Api/Process.hpp @@ -228,6 +228,14 @@ namespace montauk::abi { Sched::ExitProcess(); } + // Like Sys_Abort, but a normal exit with the caller's code: every thread + // goes, and waiters see exitCode & 0xFF, the same as SYS_EXIT on the main + // thread. + static void Sys_ExitGroup(int exitCode) { + Sched::KillProcess(Sched::GetCurrentPid(), exitCode & 0xFF); + Sched::ExitProcess(); + } + static int Sys_Kill(int pid) { // A process may always kill itself (kill(getpid(), ...)); waiters // see TerminatedKilled. abort() uses SYS_ABORT instead. diff --git a/kernel/src/Api/Syscall.cpp b/kernel/src/Api/Syscall.cpp index 704bd13..a173fb0 100644 --- a/kernel/src/Api/Syscall.cpp +++ b/kernel/src/Api/Syscall.cpp @@ -403,6 +403,9 @@ namespace montauk::abi { case SYS_ABORT: Sys_Abort(); return Success; + case SYS_EXIT_GROUP: + Sys_ExitGroup((int)frame->arg1); + return Success; case SYS_SETSESSION: return (int64_t)Sys_SetSession(); case SYS_KILLSESSION: diff --git a/kernel/src/Api/Syscall.hpp b/kernel/src/Api/Syscall.hpp index baf43fb..d092432 100644 --- a/kernel/src/Api/Syscall.hpp +++ b/kernel/src/Api/Syscall.hpp @@ -380,6 +380,12 @@ namespace montauk::abi { sibling thread ends only that thread, so abort() needs its own call. */ static constexpr uint64_t SYS_ABORT = 198; // () -> does not return + /* Normal termination of the whole process from any thread, with an exit + code (0..255) instead of SYS_ABORT's fixed TerminatedAborted. libc + exit() and Rust std::process::exit() use it, so exiting from a sibling + thread ends the process rather than only that thread. */ + static constexpr uint64_t SYS_EXIT_GROUP = 199; // (exitCode) -> does not return + /* Kernel-owned process capabilities. User identities may namespace per-user resources, but never participate in authorization decisions. */ static constexpr uint64_t CAP_PROCESS_ADMIN = 1ULL << 0; diff --git a/kernel/src/Fs/Ramdisk.cpp b/kernel/src/Fs/Ramdisk.cpp index 328220e..e65e907 100644 --- a/kernel/src/Fs/Ramdisk.cpp +++ b/kernel/src/Fs/Ramdisk.cpp @@ -89,6 +89,20 @@ namespace Fs::Ramdisk { return -1; } + // Mkdir/Create must not conjure entries under a missing parent: the + // table is flat, so "a/b/" with no "a/" entry is invisible to ReadDir of + // the parent and makes "a" fail Stat, Delete and recursive removal. + static bool ParentExists(const char* path) { + char normalized[MaxNameLen]; + int len = NormalizePath(path, normalized, MaxNameLen); + if (len < 0) return false; + while (len > 0 && normalized[len - 1] != '/') len--; + if (len <= 1) return true; // parent is the root + normalized[len - 1] = '\0'; + int idx = FindEntryByPath(normalized); + return idx >= 0 && fileTable[idx].isDirectory; + } + void Initialize(void* moduleData, uint64_t moduleSize) { Kt::KernelLogStream(Kt::OK, "Ramdisk") << "Parsing USTAR archive (" << moduleSize << " bytes)"; @@ -442,6 +456,8 @@ namespace Fs::Ramdisk { } } + if (!ParentExists(path)) return -1; + // Create new file entry FileEntry& entry = fileTable[fileCount]; @@ -531,6 +547,8 @@ namespace Fs::Ramdisk { } } + if (!ParentExists(path)) return -1; + // Create directory entry (stored with trailing slash for tar convention) FileEntry& entry = fileTable[fileCount]; diff --git a/programs/GNUmakefile b/programs/GNUmakefile index ea34413..60f1029 100644 --- a/programs/GNUmakefile +++ b/programs/GNUmakefile @@ -320,22 +320,32 @@ mandelbrot: libc screenshot: libc libjpegwrite libloader dialogs $(MAKE) -C src/screenshot -# Rust programs (programs/rust, Phase 1: no_std + alloc via -Zbuild-std). -# Optional: skipped with a notice when no rustup-managed cargo with the -# pinned nightly is installed, so the default build never requires Rust. -# RUSTUP_AUTO_INSTALL=0 keeps 'make' from downloading a toolchain itself. +# Rust programs (programs/rust): std and no_std programs for the built-in +# x86_64-unknown-montauk target of the forked rustc (Phase 2), registered with +# rustup as the 'montauk' toolchain (see programs/rust/README.md). Optional: +# skipped with a notice when that toolchain is missing, so the default build +# never requires Rust. RUSTUP_AUTO_INSTALL=0 keeps 'make' from downloading one. # Rust links the toolchain sysroot libc (like ports), not lib/libc directly: -# re-run build-montauk-toolchain.sh after libc changes. +# re-run build-montauk-toolchain.sh after libc changes. Cargo tracks neither +# that archive nor a rebuilt std in the linked toolchain, so the programs are +# cleaned (and rebuilt) when either is newer than the last build. CARGO ?= $(firstword $(wildcard $(HOME)/.cargo/bin/cargo) $(shell command -v cargo 2>/dev/null)) RUST_DIR := rust RUST_OUT := $(RUST_DIR)/target/x86_64-unknown-montauk/release -RUST_PROGRAMS := rusthello +RUST_PROGRAMS := rusthello ruststd +RUST_LIBC := ../toolchain/sysroot/usr/lib/libc.a rust: - @if [ -z "$(CARGO)" ] || ! (cd $(RUST_DIR) && RUSTUP_AUTO_INSTALL=0 $(CARGO) --version >/dev/null 2>&1); then \ - echo "rust: no nightly cargo found - skipping Rust programs (see programs/rust/README.md)"; \ + @if [ -z "$(CARGO)" ] || ! (cd $(RUST_DIR) && RUSTUP_AUTO_INSTALL=0 rustc --print target-list 2>/dev/null | grep -qx x86_64-unknown-montauk); then \ + echo "rust: no 'montauk' rustup toolchain - skipping Rust programs (see programs/rust/README.md)"; \ else \ + stdlib=$$(cd $(RUST_DIR) && RUSTUP_AUTO_INSTALL=0 rustc --print sysroot)/lib/rustlib/x86_64-unknown-montauk/lib; \ + if [ ! -f $(RUST_OUT)/.link-stamp ] || [ $(RUST_LIBC) -nt $(RUST_OUT)/.link-stamp ] || \ + [ -n "$$(find $$stdlib -name '*.rlib' -newer $(RUST_OUT)/.link-stamp 2>/dev/null | head -1)" ]; then \ + (cd $(RUST_DIR) && RUSTUP_AUTO_INSTALL=0 $(CARGO) clean --release --quiet $(addprefix -p ,$(RUST_PROGRAMS))) || exit 1; \ + fi; \ (cd $(RUST_DIR) && RUSTUP_AUTO_INSTALL=0 $(CARGO) build --release) || exit 1; \ + touch $(RUST_OUT)/.link-stamp; \ mkdir -p $(BINDIR)/os; \ for p in $(RUST_PROGRAMS); do cp $(RUST_OUT)/$$p $(BINDIR)/os/$$p.elf || exit 1; done; \ fi diff --git a/programs/include/Api/Syscall.hpp b/programs/include/Api/Syscall.hpp index 0db1028..1cc1860 100644 --- a/programs/include/Api/Syscall.hpp +++ b/programs/include/Api/Syscall.hpp @@ -293,6 +293,12 @@ namespace montauk::abi { sibling thread ends only that thread, so abort() needs its own call. */ static constexpr uint64_t SYS_ABORT = 198; // () -> does not return + /* Normal termination of the whole process from any thread, with an exit + code (0..255) instead of SYS_ABORT's fixed TerminatedAborted. libc + exit() and Rust std::process::exit() use it, so exiting from a sibling + thread ends the process rather than only that thread. */ + static constexpr uint64_t SYS_EXIT_GROUP = 199; // (exitCode) -> does not return + /* Kernel-owned process capabilities. User identities may namespace per-user resources, but never participate in authorization decisions. */ static constexpr uint64_t CAP_PROCESS_ADMIN = 1ULL << 0; diff --git a/programs/include/libc/montauk.h b/programs/include/libc/montauk.h index d189c08..a8c6a04 100644 --- a/programs/include/libc/montauk.h +++ b/programs/include/libc/montauk.h @@ -222,6 +222,7 @@ extern "C" { #define MTK_SYS_FUTEX_WAIT 196 #define MTK_SYS_FUTEX_WAKE 197 #define MTK_SYS_ABORT 198 +#define MTK_SYS_EXIT_GROUP 199 /* @SYSCALLS-END */ /* Syscall status codes (montauk/statuscodes.h for C++). */ diff --git a/programs/lib/libc/libc.c b/programs/lib/libc/libc.c index 8f7c434..c7137b9 100644 --- a/programs/lib/libc/libc.c +++ b/programs/lib/libc/libc.c @@ -131,6 +131,7 @@ static inline long _mtk_syscall4(long nr, long a1, long a2, long a3, long a4) { #define SYS_SETENVIRON 172 #define SYS_SPAWN_ENV 173 #define SYS_ABORT 198 +#define SYS_EXIT_GROUP 199 /* ======================================================================== errno @@ -1240,7 +1241,8 @@ int atexit(void (*func)(void)) { void exit(int status) { for (int i = _atexit_count - 1; i >= 0; i--) _atexit_funcs[i](); - _mtk_syscall1(SYS_EXIT, (long)status); + /* SYS_EXIT from a sibling thread would end only that thread. */ + _mtk_syscall1(SYS_EXIT_GROUP, (long)status); __builtin_unreachable(); } @@ -4066,7 +4068,7 @@ pid_t waitpid(pid_t pid, int *status, int options) { } void _exit(int status) { - _mtk_syscall1(SYS_EXIT, (long)status); + _mtk_syscall1(SYS_EXIT_GROUP, (long)status); __builtin_unreachable(); } diff --git a/programs/rust/.cargo/config.toml b/programs/rust/.cargo/config.toml index e7de6f3..60696ed 100644 --- a/programs/rust/.cargo/config.toml +++ b/programs/rust/.cargo/config.toml @@ -1,13 +1,7 @@ -# Phase 1 Rust support: no_std + alloc on a stock nightly, built with -# -Zbuild-std against the custom target spec next to this directory. # Paths below are relative to programs/rust/ (the parent of .cargo/). [build] -target = "x86_64-unknown-montauk.json" - -[unstable] -build-std = ["core", "alloc"] -json-target-spec = true +target = "x86_64-unknown-montauk" [target.x86_64-unknown-montauk] # The montauk GCC driver supplies crt1/crti/crtbegin, -lc and libgcc, and diff --git a/programs/rust/Cargo.lock b/programs/rust/Cargo.lock index cdf6ed7..1476555 100644 --- a/programs/rust/Cargo.lock +++ b/programs/rust/Cargo.lock @@ -12,3 +12,7 @@ version = "0.1.0" dependencies = [ "montauk-rt", ] + +[[package]] +name = "ruststd" +version = "0.1.0" diff --git a/programs/rust/Cargo.toml b/programs/rust/Cargo.toml index e9f04c7..5342e9b 100644 --- a/programs/rust/Cargo.toml +++ b/programs/rust/Cargo.toml @@ -1,6 +1,6 @@ [workspace] resolver = "2" -members = ["montauk-rt", "rusthello"] +members = ["montauk-rt", "rusthello", "ruststd"] [profile.dev] panic = "abort" diff --git a/programs/rust/README.md b/programs/rust/README.md index 98fc0ec..b89477d 100644 --- a/programs/rust/README.md +++ b/programs/rust/README.md @@ -1,18 +1,31 @@ # Rust on MontaukOS -Phase 1: `no_std` + `alloc` programs, cross-compiled from Linux with a -stock nightly toolchain and a custom target spec. No compiler fork. +Rust programs are built for `x86_64-unknown-montauk`, a built-in target of a +forked rustc with a native `std` (`target_os = "montauk"`, not a Unix). Both +`std` programs and `no_std` programs (on `montauk-rt`) are supported. ## Setup (once) ```bash +# rustup, plus a nightly whose cargo the linked toolchain borrows curl --proto '=https' -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain none -~/.cargo/bin/rustup toolchain install nightly --component rust-src +~/.cargo/bin/rustup toolchain install nightly + +# The fork: rust-lang/rust, branch `montauk` (bootstrap.toml is set up) +cd ~/dev/mtk/rust +./x build library --stage 1 --target x86_64-unknown-linux-gnu,x86_64-unknown-montauk +rustup toolchain link montauk ~/dev/mtk/rust/build/host/stage1 ``` -`rust-toolchain.toml` selects nightly + `rust-src` for this directory. -`make programs` builds the Rust programs when that toolchain is present -and prints a notice and skips them otherwise. It never downloads a toolchain +The stage1 build takes about five minutes (it downloads CI LLVM). After +changing only `library/`, rebuild with +`./x build library --stage 1 --target x86_64-unknown-montauk` (seconds); the +link stays valid. + +`rust-toolchain.toml` selects the `montauk` toolchain for this directory. It +has no cargo of its own; rustup falls back to the nightly's cargo. +`make programs` builds the Rust programs when that toolchain is linked, and +prints a notice and skips them otherwise. It never downloads a toolchain (`RUSTUP_AUTO_INSTALL=0`). ## Building @@ -26,45 +39,67 @@ Output: `target/x86_64-unknown-montauk/release/`, installed as `programs/bin/os/.elf`. Add new binaries to `RUST_PROGRAMS` in `programs/GNUmakefile` and to the workspace `members`. +The GCC driver links crt1/crti/crtbegin, the libc and libgcc from +`toolchain/sysroot`, the same as ports. After changing `programs/lib/libc`, +re-run `toolchain/build-montauk-toolchain.sh` so Rust programs pick it up. +`make rust` relinks the programs when the sysroot libc or the toolchain's std +is newer than the last build (cargo tracks neither). + ## Layout | Path | Purpose | |------|---------| -| `x86_64-unknown-montauk.json` | Target spec: static non-PIE ET_EXEC, no red zone, local-exec TLS, panic=abort | -| `.cargo/config.toml` | `-Zbuild-std=core,alloc`, linker = `x86_64-montauk-gcc` | -| `montauk-rt/` | Runtime: syscalls, allocator, `print!`, argv, threads, futex `Mutex`, panic handler | -| `rusthello/` | Smoke test (`rusthello`, `rusthello panic`, `rusthello panic-thread`) | +| `.cargo/config.toml` | target = `x86_64-unknown-montauk`, linker = `x86_64-montauk-gcc` | +| `rust-toolchain.toml` | the linked `montauk` toolchain | +| `ruststd/` | `std` smoke test (`ruststd`, `ruststd net [port]`) | +| `montauk-rt/` | `no_std` runtime: syscalls, allocator, `print!`, argv, threads, futex `Mutex`, panic handler | +| `rusthello/` | `no_std` smoke test (`rusthello`, `rusthello panic`, `rusthello panic-thread`, `rusthello fault-thread`) | -The GCC driver links crt1/crti/crtbegin, the libc and libgcc from -`toolchain/sysroot`, the same as ports. After changing `programs/lib/libc`, -re-run `toolchain/build-montauk-toolchain.sh` so Rust programs pick it up. +## The std port (in the fork) -Syscall numbers and status codes are generated at build time by -`montauk-rt/build.rs` from `programs/include/Api/Syscall.hpp` and -`kernel/src/Common/StatusCodes.hpp`, so there is nothing to keep in sync by hand. +| Fork path | What | +|-----------|------| +| `compiler/rustc_target/src/spec/base/montauk.rs`, `targets/x86_64_unknown_montauk.rs` | Target: static non-PIE ET_EXEC, no red zone, local-exec TLS, panic=abort, linker `x86_64-montauk-gcc` | +| `library/std/src/sys/pal/montauk/` | Platform layer: `abi.rs` (syscall numbers, status codes, `SYSCALL` wrappers) | +| `library/std/src/sys/*/montauk.rs` | alloc, env, fs, io errors, net, paths, process, random, stdio, futex, thread, time | +| `library/std/src/os/montauk/` | `std::os::montauk::ffi::{OsStrExt, OsStringExt}` | -## Writing a program +std calls the kernel directly. The libc is used only where C code in the same +process must agree: the heap (`malloc`), the environment table (`setenv`), +`exit` (atexit handlers) and line-buffered terminal input (`read(0)`). The +syscall numbers in `abi.rs` are a hand-kept subset of `Api/Syscall.hpp`. -```rust -#![no_std] -#![no_main] -extern crate alloc; +## Platform behaviour -montauk_rt::entry!(main); - -fn main() -> i32 { - montauk_rt::println!("hello from Rust"); - 0 -} -``` - -## Limits (Phase 1) - -- No `std`. No `std::fs`, `std::net`, `std::process` or `std::thread`; use - `montauk_rt` equivalents where they exist. -- `panic = "abort"`. A panic on any thread prints the message and kills the - whole process. -- Thread stacks are owned by the `JoinHandle`. An unjoined thread leaks its stack. - -Phase 2 (a forked rustc at `~/dev/mtk/rust` with a native `target_os = -"montauk"` std) replaces the JSON spec with a built-in target. +- Errors: the raw OS error of an `io::Error` is the kernel status code + (positive, `montauk/statuscodes.h`); `ErrorKind` is mapped from it. +- Paths: `0:/os/x.elf` is absolute; joining onto it replaces the base. Search + paths (`PATH`, `env::split_paths`) are separated by `;`, not `:`. +- Files: kernel handles have no position, so `File` keeps its own cursor. + `OpenOptions::truncate` deletes and recreates the file; `set_len` can only + grow a file. No links, permissions are read-only, `fsync` is a no-op. +- Threads: stacks are mapped with `SYS_MMAP_ANON` (no guard page). A detached + thread is reaped (joined, stack freed) by the next spawn or join, so its + kernel slot does not leak. `available_parallelism()` is an error (the kernel + does not report a CPU count). `std::process::exit` from any thread ends the + process (libc `exit` -> `SYS_EXIT_GROUP`). +- Sync: `Mutex`, `RwLock`, `Condvar`, `Once` and thread parking use the + generic futex implementations over `SYS_FUTEX_WAIT`/`SYS_FUTEX_WAKE`. +- `thread_local!` uses native `#[thread_local]` (PT_TLS); destructors run when + a spawned thread exits (not on the main thread). +- Time: `Instant` has 1 ms resolution; `SystemTime` comes from the wall clock + (local time minus the timezone offset) with 1 s resolution. +- Processes: `Command` supports arguments, environment, `current_dir` (entered + in the parent for the duration of the spawn), `wait`, `try_wait` and `kill`. + Only inherited stdio is supported; piped/null stdio and `output()` return + `Unsupported`, because a redirected child's output stream disappears with + the child. A bare program name is searched in `PATH`, else `0:/os` and + `0:/games`; `.elf` may be omitted. `ExitStatus` shows abnormal ends as + "terminated by signal N". +- Net: IPv4 only. TCP connect/accept/read/write with read timeouts and + non-blocking mode, UDP send_to/recv_from/connect, DNS via `SYS_RESOLVE`. + No socket options, no `peek`, no `shutdown(Write)`, and accepted + connections do not know the peer's address. +- `panic = "abort"`: a panic on any thread prints the message and kills the + whole process (exit status "signal 6"). +- `HashMap`'s random keys come from `SYS_GETRANDOM`, which is not a CSPRNG. diff --git a/programs/rust/montauk-rt/src/lib.rs b/programs/rust/montauk-rt/src/lib.rs index dab5956..9921e75 100644 --- a/programs/rust/montauk-rt/src/lib.rs +++ b/programs/rust/montauk-rt/src/lib.rs @@ -1,9 +1,9 @@ //! Minimal `no_std` runtime for Rust programs on MontaukOS. //! -//! Phase 1 of Rust support: `core` + `alloc` on a stock nightly with a -//! custom target spec. Programs link against the Montauk libc through the -//! x86_64-montauk GCC driver, so crt1.o owns `_start` (argv, environment, -//! .init_array) and calls the `main` that [`entry!`] defines. +//! For programs that use `core` + `alloc` without `std` (most programs can use +//! `std` directly; see ../README.md). Programs link against the Montauk libc +//! through the x86_64-montauk GCC driver, so crt1.o owns `_start` (argv, +//! environment, .init_array) and calls the `main` that [`entry!`] defines. //! //! ```ignore //! #![no_std] diff --git a/programs/rust/rust-toolchain.toml b/programs/rust/rust-toolchain.toml index f70d225..eea8e44 100644 --- a/programs/rust/rust-toolchain.toml +++ b/programs/rust/rust-toolchain.toml @@ -1,3 +1,8 @@ +# Phase 2 Rust support: the forked rustc with the built-in +# x86_64-unknown-montauk target and a native std. Build it in ~/dev/mtk/rust, +# then register it with: +# rustup toolchain link montauk ~/dev/mtk/rust/build/host/stage1 +# The linked toolchain has no cargo of its own; rustup falls back to the +# nightly toolchain's cargo. [toolchain] -channel = "nightly" -components = ["rust-src"] +channel = "montauk" diff --git a/programs/rust/rusthello/src/main.rs b/programs/rust/rusthello/src/main.rs index 734898b..304a8ae 100644 --- a/programs/rust/rusthello/src/main.rs +++ b/programs/rust/rusthello/src/main.rs @@ -1,4 +1,4 @@ -//! rusthello - Phase 1 Rust smoke test for MontaukOS. +//! rusthello - `no_std` Rust smoke test for MontaukOS (montauk-rt). //! //! Exercises alloc collections, formatting, argv, a #[thread_local] //! (PT_TLS + per-thread %fs blocks), threads, and the futex mutex. diff --git a/programs/rust/ruststd/Cargo.toml b/programs/rust/ruststd/Cargo.toml new file mode 100644 index 0000000..8bd92d0 --- /dev/null +++ b/programs/rust/ruststd/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "ruststd" +version = "0.1.0" +edition = "2024" diff --git a/programs/rust/ruststd/src/main.rs b/programs/rust/ruststd/src/main.rs new file mode 100644 index 0000000..b6300d7 --- /dev/null +++ b/programs/rust/ruststd/src/main.rs @@ -0,0 +1,477 @@ +//! ruststd - Phase 2 Rust smoke test for MontaukOS: the native `std`. +//! +//! Runs a set of PASS/FAIL checks over the std platform layer: collections and +//! formatting, args and environment, the filesystem, threads (joined, scoped +//! and detached), the futex-backed sync primitives, thread_local! destructors, +//! time, and child processes (exit codes, arguments, environment, working +//! directory, std::process::exit from a worker thread, panics). +//! +//! `ruststd net [port]` additionally fetches `http:///` over TCP. +//! The `child-*` arguments are the child-process halves of the checks. + +use std::collections::{BTreeMap, HashMap}; +use std::fs::{self, File, OpenOptions}; +use std::io::{BufRead, BufReader, Read, Seek, SeekFrom, Write}; +use std::path::{Path, PathBuf}; +use std::process::{Command, ExitCode}; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::{Arc, Barrier, Condvar, Mutex, OnceLock, RwLock, mpsc}; +use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; +use std::{env, thread}; + +static FAILURES: AtomicUsize = AtomicUsize::new(0); + +fn check(ok: bool, name: &str) { + println!("{} {}", if ok { "PASS" } else { "FAIL" }, name); + if !ok { + FAILURES.fetch_add(1, Ordering::Relaxed); + } +} + +fn main() -> ExitCode { + let args: Vec = env::args().collect(); + match args.get(1).map(String::as_str) { + Some(mode) if mode.starts_with("child-") => return ExitCode::from(child(mode, &args)), + Some("net") => return net(&args), + _ => {} + } + + println!("ruststd: Rust std on MontaukOS"); + println!("argv = {args:?}"); + + basics(); + environment(); + filesystem(); + threads(); + sync(); + thread_locals(); + time(); + processes(&args); + + let failures = FAILURES.load(Ordering::Relaxed); + if failures == 0 { + println!("ruststd: all passed"); + ExitCode::SUCCESS + } else { + println!("ruststd: {failures} FAILED"); + ExitCode::FAILURE + } +} + +// ==================================================================== +// Child-process halves +// ==================================================================== + +fn child(mode: &str, args: &[String]) -> u8 { + match mode { + "child-exit" => 7, + "child-args" => { + let ok = args[2..] == ["with space", "", "it's", "say \"hi\"", "plain"]; + if ok { 8 } else { 1 } + } + "child-env" => { + if env::var("RUSTSTD_VAR").as_deref() == Ok("hello there") { 9 } else { 1 } + } + "child-cwd" => { + let cwd = env::current_dir().unwrap_or_default(); + if cwd.ends_with("os") { 10 } else { 1 } + } + "child-exit-thread" => { + // std::process::exit from a worker must end the whole process. + let h = thread::spawn(|| std::process::exit(11)); + let _ = h.join(); + thread::sleep(Duration::from_secs(5)); + 1 + } + "child-panic-thread" => { + let h = thread::spawn(|| panic!("requested panic on a worker thread")); + let _ = h.join(); + thread::sleep(Duration::from_secs(5)); + 1 + } + _ => 2, + } +} + +// ==================================================================== +// Checks +// ==================================================================== + +fn basics() { + let mut map = HashMap::new(); + for (i, w) in "Montauk speaks Rust with std".split(' ').enumerate() { + map.insert(w.to_string(), i); + } + check(map["Rust"] == 2 && map.len() == 5, "HashMap (RandomState keys from SYS_GETRANDOM)"); + + let sorted: BTreeMap<_, _> = map.iter().map(|(k, v)| (*v, k.clone())).collect(); + check(sorted.values().next().map(String::as_str) == Some("Montauk"), "BTreeMap"); + + check( + format!("{:>8.3}|{:e}|{:#06x}", std::f64::consts::PI, 1234.5f64, 255) == " 3.142|1.2345e3|0x00ff", + "float and integer formatting", + ); + + let mut out = std::io::stdout().lock(); + check(writeln!(out, " (stdout lock + writeln!)").is_ok(), "io::stdout().lock()"); + eprintln!(" (eprintln! goes to the terminal too)"); +} + +fn environment() { + let exe = env::current_exe(); + check(exe.as_ref().is_ok_and(|p| p.is_absolute()), "env::current_exe() is absolute"); + println!(" current_exe = {exe:?}"); + + let cwd = env::current_dir(); + check(cwd.is_ok(), "env::current_dir()"); + + unsafe { env::set_var("RUSTSTD_TEST", "value=with=equals") }; + check(env::var("RUSTSTD_TEST").as_deref() == Ok("value=with=equals"), "set_var / var"); + check(env::vars().any(|(k, v)| k == "RUSTSTD_TEST" && v == "value=with=equals"), "env::vars() lists it"); + unsafe { env::remove_var("RUSTSTD_TEST") }; + check(env::var_os("RUSTSTD_TEST").is_none(), "remove_var"); + + let joined = env::join_paths(["0:/os", "0:/games"]).unwrap(); + let split: Vec = env::split_paths(&joined).collect(); + check(joined == "0:/os;0:/games" && split.len() == 2, "join_paths / split_paths use ';'"); + + check(Path::new("0:/os/shell.elf").is_absolute(), "drive paths are absolute"); + check(Path::new("0:/os").join("0:/apps") == Path::new("0:/apps"), "joining a drive path replaces"); + check(Path::new("0:/os/shell.elf").parent() == Some(Path::new("0:/os")), "Path::parent"); +} + +fn filesystem() { + let base = env::temp_dir().join(format!("ruststd-{}", std::process::id())); + let _ = fs::remove_dir_all(&base); + check(fs::create_dir_all(base.join("sub/deeper")).is_ok(), "create_dir_all"); + check(fs::create_dir(&base).is_err_and(|e| e.kind() == std::io::ErrorKind::AlreadyExists), + "create_dir on an existing dir fails with AlreadyExists"); + + let file = base.join("hello.txt"); + check(fs::write(&file, b"line one\nline two\n").is_ok(), "fs::write"); + check(fs::read_to_string(&file).ok().as_deref() == Some("line one\nline two\n"), "fs::read_to_string"); + + let appended = OpenOptions::new().append(true).open(&file).and_then(|mut f| f.write_all(b"line three\n")); + check(appended.is_ok(), "append"); + let lines: Vec = BufReader::new(File::open(&file).unwrap()).lines().map(Result::unwrap).collect(); + check(lines == ["line one", "line two", "line three"], "BufReader::lines after append"); + + let meta = fs::metadata(&file); + check(meta.as_ref().is_ok_and(|m| m.is_file() && m.len() == 29), "metadata: is_file, len"); + if let Ok(m) = &meta { + println!(" modified = {:?}", m.modified().ok().and_then(|t| t.duration_since(UNIX_EPOCH).ok())); + } + check(fs::metadata(&base).is_ok_and(|m| m.is_dir()), "metadata: is_dir"); + + // Seek + overwrite in place. + { + let mut f = OpenOptions::new().read(true).write(true).open(&file).unwrap(); + f.seek(SeekFrom::Start(5)).unwrap(); + f.write_all(b"ONE").unwrap(); + f.seek(SeekFrom::End(-6)).unwrap(); + let mut tail = String::new(); + f.read_to_string(&mut tail).unwrap(); + check(tail == "three\n", "seek from end + read"); + } + check(fs::read_to_string(&file).unwrap().starts_with("line ONE\n"), "seek + overwrite"); + + // Truncate on open. + fs::write(&file, b"short").unwrap(); + check(fs::read(&file).ok().as_deref() == Some(&b"short"[..]), "File::create truncates"); + + check(fs::copy(&file, base.join("sub/copy.txt")).ok() == Some(5), "fs::copy"); + check(fs::rename(base.join("sub/copy.txt"), base.join("sub/moved.txt")).is_ok(), "fs::rename"); + + let mut names: Vec = fs::read_dir(&base) + .map(|rd| rd.filter_map(|e| e.ok()).map(|e| e.file_name().to_string_lossy().into_owned()).collect()) + .unwrap_or_default(); + names.sort(); + check(names == ["hello.txt", "sub"], "read_dir lists entries"); + let sub_types: Vec<(String, bool)> = fs::read_dir(base.join("sub")) + .unwrap() + .map(|e| e.unwrap()) + .map(|e| (e.file_name().to_string_lossy().into_owned(), e.file_type().unwrap().is_dir())) + .collect(); + check(sub_types.contains(&("deeper".into(), true)) && sub_types.contains(&("moved.txt".into(), false)), + "DirEntry::file_type"); + + check(File::open(base.join("missing")).is_err_and(|e| e.kind() == std::io::ErrorKind::NotFound), + "open missing file -> NotFound"); + check(fs::remove_file(&file).is_ok() && !file.exists(), "remove_file"); + check(fs::remove_dir_all(&base).is_ok() && !base.exists(), "remove_dir_all"); +} + +fn threads() { + let handles: Vec<_> = (0..4).map(|i| thread::spawn(move || i * 10)).collect(); + let results: Vec = handles.into_iter().map(|h| h.join().unwrap()).collect(); + check(results == [0, 10, 20, 30], "spawn + join return values"); + + let named = thread::Builder::new().name("worker".into()).spawn(|| { + thread::current().name().map(str::to_owned) + }); + check(named.unwrap().join().unwrap().as_deref() == Some("worker"), "named thread"); + + let mut data = vec![1, 2, 3, 4]; + thread::scope(|s| { + for x in data.iter_mut() { + s.spawn(move || *x *= 2); + } + }); + check(data == [2, 4, 6, 8], "thread::scope borrows"); + + // Detached threads hold a kernel slot until reaped; 300 of them would + // exhaust the 256-slot table if they were not. + let done = Arc::new(AtomicUsize::new(0)); + for _ in 0..15 { + for _ in 0..20 { + let done = done.clone(); + drop(thread::spawn(move || { + done.fetch_add(1, Ordering::Relaxed); + })); + } + thread::sleep(Duration::from_millis(20)); + } + let deadline = Instant::now() + Duration::from_secs(5); + while done.load(Ordering::Relaxed) < 300 && Instant::now() < deadline { + thread::sleep(Duration::from_millis(10)); + } + check(done.load(Ordering::Relaxed) == 300, "300 detached threads are reaped"); + + let big = thread::Builder::new().stack_size(4 << 20).spawn(|| { + let buf = [7u8; 2 << 20]; + buf.iter().map(|&b| b as usize).sum::() + }); + check(big.unwrap().join().unwrap() == 7 * (2 << 20), "4 MiB thread stack"); +} + +fn sync() { + const THREADS: usize = 4; + const ITERS: usize = 10_000; + + let counter = Arc::new(Mutex::new(0usize)); + let handles: Vec<_> = (0..THREADS) + .map(|_| { + let counter = counter.clone(); + thread::spawn(move || { + for i in 0..ITERS { + let mut g = counter.lock().unwrap(); + if i % 1000 == 0 { + thread::yield_now(); + } + *g += 1; + } + }) + }) + .collect(); + handles.into_iter().for_each(|h| h.join().unwrap()); + check(*counter.lock().unwrap() == THREADS * ITERS, "Mutex keeps the counter exact"); + + let pair = Arc::new((Mutex::new(false), Condvar::new())); + let pair2 = pair.clone(); + let waiter = thread::spawn(move || { + let (lock, cvar) = &*pair2; + let guard = cvar.wait_while(lock.lock().unwrap(), |ready| !*ready).unwrap(); + *guard + }); + thread::sleep(Duration::from_millis(20)); + *pair.0.lock().unwrap() = true; + pair.1.notify_all(); + check(waiter.join().unwrap(), "Condvar wait/notify"); + + let (lock, cvar) = (Mutex::new(()), Condvar::new()); + let t0 = Instant::now(); + let (_g, res) = cvar.wait_timeout(lock.lock().unwrap(), Duration::from_millis(30)).unwrap(); + check(res.timed_out() && t0.elapsed() >= Duration::from_millis(30), "Condvar wait_timeout"); + + let rw = Arc::new(RwLock::new(vec![1])); + let readers: Vec<_> = (0..3) + .map(|_| { + let rw = rw.clone(); + thread::spawn(move || rw.read().unwrap().len()) + }) + .collect(); + rw.write().unwrap().push(2); + let lens: Vec = readers.into_iter().map(|h| h.join().unwrap()).collect(); + check(lens.iter().all(|&l| l == 1 || l == 2) && rw.read().unwrap().len() == 2, "RwLock"); + + static ONCE: OnceLock = OnceLock::new(); + let inits = Arc::new(AtomicUsize::new(0)); + let hs: Vec<_> = (0..4) + .map(|_| { + let inits = inits.clone(); + thread::spawn(move || *ONCE.get_or_init(|| inits.fetch_add(1, Ordering::SeqCst) + 42)) + }) + .collect(); + let vals: Vec = hs.into_iter().map(|h| h.join().unwrap()).collect(); + check(vals.iter().all(|&v| v == 42) && inits.load(Ordering::SeqCst) == 1, "OnceLock initializes once"); + + let (tx, rx) = mpsc::channel(); + for t in 0..3 { + let tx = tx.clone(); + thread::spawn(move || { + for i in 0..100 { + tx.send(t * 1000 + i).unwrap(); + } + }); + } + drop(tx); + let received: Vec = rx.iter().collect(); + check(received.len() == 300, "mpsc channel with 3 senders"); + + let barrier = Arc::new(Barrier::new(3)); + let hs: Vec<_> = (0..3) + .map(|_| { + let b = barrier.clone(); + thread::spawn(move || b.wait().is_leader()) + }) + .collect(); + let leaders = hs.into_iter().map(|h| h.join().unwrap()).filter(|&l| l).count(); + check(leaders == 1, "Barrier"); + + let parked = thread::spawn(|| { + thread::park(); + true + }); + thread::sleep(Duration::from_millis(10)); + parked.thread().unpark(); + check(parked.join().unwrap(), "park / unpark"); +} + +fn thread_locals() { + static DROPS: AtomicUsize = AtomicUsize::new(0); + + struct Tracker(u32); + impl Drop for Tracker { + fn drop(&mut self) { + DROPS.fetch_add(1, Ordering::SeqCst); + } + } + + thread_local! { + static COUNTER: std::cell::Cell = const { std::cell::Cell::new(5) }; + static TRACKER: Tracker = Tracker(1); + } + + COUNTER.with(|c| c.set(100)); + let seen = thread::spawn(|| { + TRACKER.with(|t| assert_eq!(t.0, 1)); + COUNTER.with(|c| c.get()) + }) + .join() + .unwrap(); + check(seen == 5 && COUNTER.with(|c| c.get()) == 100, "thread_local! is per thread"); + check(DROPS.load(Ordering::SeqCst) == 1, "thread_local! destructor runs at thread exit"); +} + +fn time() { + let t0 = Instant::now(); + thread::sleep(Duration::from_millis(50)); + let dt = t0.elapsed(); + check(dt >= Duration::from_millis(50) && dt < Duration::from_secs(2), "Instant + sleep(50 ms)"); + + let now = SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0); + // 2025-01-01T00:00:00Z + check(now > 1_735_689_600, "SystemTime::now() is after 2025"); + println!(" unix time = {now}"); +} + +fn processes(args: &[String]) { + let Ok(me) = env::current_exe() else { + check(false, "current_exe for child processes"); + return; + }; + let _ = args; + let run = |cmd: &mut Command| cmd.status().map(|s| s.code()); + + check(run(Command::new(&me).arg("child-exit")).ok() == Some(Some(7)), "child exit code"); + check( + run(Command::new(&me).args(["child-args", "with space", "", "it's", "say \"hi\"", "plain"])).ok() == Some(Some(8)), + "child arguments with spaces and quotes", + ); + check( + run(Command::new(&me).arg("child-env").env("RUSTSTD_VAR", "hello there")).ok() == Some(Some(9)), + "child environment", + ); + check( + run(Command::new(&me).arg("child-cwd").current_dir("0:/os")).ok() == Some(Some(10)), + "child working directory", + ); + check( + run(Command::new(&me).arg("child-exit-thread")).ok() == Some(Some(11)), + "process::exit from a worker thread ends the process", + ); + let panicked = Command::new(&me).arg("child-panic-thread").status(); + check( + panicked.as_ref().is_ok_and(|s| s.code().is_none() && s.to_string().contains("signal 6")), + "panic on a worker thread aborts the process", + ); + if let Ok(s) = panicked { + println!(" panicking child: {s}"); + } + + let mut child = Command::new(&me).arg("child-exit").spawn().unwrap(); + let mut polled = None; + for _ in 0..200 { + if let Ok(Some(s)) = child.try_wait() { + polled = Some(s); + break; + } + thread::sleep(Duration::from_millis(10)); + } + check(polled.and_then(|s| s.code()) == Some(7), "Child::try_wait"); + + check(Command::new("no-such-program").status().is_err(), "spawning a missing program fails"); + check( + Command::new(&me).arg("child-exit").output().is_err_and(|e| e.kind() == std::io::ErrorKind::Unsupported), + "output() reports Unsupported (no stdio redirection yet)", + ); +} + +// ==================================================================== +// Network (optional: needs a link) +// ==================================================================== + +fn net(args: &[String]) -> ExitCode { + use std::net::{TcpStream, ToSocketAddrs}; + + let Some(host) = args.get(2) else { + println!("usage: ruststd net [port]"); + return ExitCode::FAILURE; + }; + let port: u16 = args.get(3).and_then(|p| p.parse().ok()).unwrap_or(80); + + let addrs: Vec<_> = match (host.as_str(), port).to_socket_addrs() { + Ok(a) => a.collect(), + Err(e) => { + println!("resolve {host}: {e}"); + return ExitCode::FAILURE; + } + }; + println!("{host} -> {addrs:?}"); + + let mut stream = match TcpStream::connect((host.as_str(), port)) { + Ok(s) => s, + Err(e) => { + println!("connect: {e}"); + return ExitCode::FAILURE; + } + }; + stream.set_read_timeout(Some(Duration::from_secs(10))).unwrap(); + let request = format!("GET / HTTP/1.0\r\nHost: {host}\r\nConnection: close\r\n\r\n"); + if let Err(e) = stream.write_all(request.as_bytes()) { + println!("send: {e}"); + return ExitCode::FAILURE; + } + let mut response = Vec::new(); + match stream.read_to_end(&mut response) { + Ok(n) => { + let text = String::from_utf8_lossy(&response); + println!("received {n} bytes; status line: {:?}", text.lines().next().unwrap_or("")); + ExitCode::SUCCESS + } + Err(e) => { + println!("recv: {e}"); + ExitCode::FAILURE + } + } +} diff --git a/programs/rust/x86_64-unknown-montauk.json b/programs/rust/x86_64-unknown-montauk.json deleted file mode 100644 index dc1bd6a..0000000 --- a/programs/rust/x86_64-unknown-montauk.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "arch": "x86_64", - "cpu": "x86-64", - "data-layout": "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128", - "llvm-target": "x86_64-unknown-none-elf", - "os": "montauk", - "vendor": "unknown", - "target-pointer-width": 64, - "max-atomic-width": 64, - "executables": true, - "linker-flavor": "gnu-cc", - "linker": "x86_64-montauk-gcc", - "no-default-libraries": false, - "crt-objects-fallback": "false", - "relocation-model": "static", - "code-model": "small", - "position-independent-executables": false, - "static-position-independent-executables": false, - "relro-level": "off", - "disable-redzone": true, - "has-thread-local": true, - "tls-model": "local-exec", - "panic-strategy": "abort", - "plt-by-default": false, - "stack-probes": { "kind": "inline" }, - "metadata": { - "description": "MontaukOS userspace (static ET_EXEC, Montauk libc)", - "host_tools": false, - "std": false, - "tier": 3 - } -}