From 33f6d2dee7d6ad7b3c7c6b4f3253b144f09c7a9f Mon Sep 17 00:00:00 2001 From: japabu Date: Sun, 4 Oct 2026 12:18:37 +0200 Subject: [PATCH 1/5] Every image says which build it is: /system/etc/os-release in ROOT The owner could not tell which build a machine runs. The build now writes /system/etc/os-release into ROOT, in the freedesktop os-release format with ToyOS's fields prefixed TOYOS_: the commit, whether the tree was that commit's (untracked files count), the sysroot key the image was compiled against, the architecture, and the commit's committer time. VERSION_ID is the short commit, so a reader such as sysinfo's os_version has one. - toyos-osrelease is the one definition: the build renders it, the supervisor and libc parse it. no_std and no alloc, because libc reads it; shaped like toyos-manifest, not in toyos-abi, because the kernel reads nothing of it. The parser refuses by name a missing, doubled or unwritable field, and refuses any file that is not byte for byte what the build writes for the fields it names. - The supervisor reads it first thing, before the manifest, and logs one line: `supervisor: build , committed , toolchain , `. A ROOT without it, or with one the build did not write, panics the supervisor by name. - The kernel's early boot: records name root=, the ROOT UUID, which hashes every ROOT file and so this one: a panic before the mount names its build. - libc's uname answers from the file: ToyOS, an empty node name, the short commit (-dirty after it) as release, the commit as version, the machine. gethostname stays refused. - The toolchain identity is the sysroot key the build already computes, which exists on an installed toolchain (CI's guest jobs) where no rust/ checkout does. The git reads are rev-parse, cat-file and status --porcelain --ignore-submodules=all: recursing into rust/ cost 0.96-1.28 s on the primary against 0.03-0.09 s without, and the fork's state is the toolchain key's. The time is the commit's, so a rebuild of one tree writes the same bytes; the two clock reads left in src/image.rs are filed. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01WcU2Dsw6mDYtwYfzVHPzM8 --- Cargo.lock | 5 + Cargo.toml | 2 + ...n-and-its-fat-dates-are-the-hosts-clock.md | 22 + ...bc-refuses-what-toyos-cannot-yet-answer.md | 3 +- kernel/src/main.rs | 4 + kernel/src/rootfs.rs | 7 + src/build.rs | 85 ++++ src/sysroot.rs | 23 +- tests/common/qemu.rs | 24 ++ tests/testcases/tinycc/206_libc_refusals.c | 31 +- .../testcases/tinycc/206_libc_refusals.expect | 6 +- toyos-osrelease/Cargo.toml | 6 + toyos-osrelease/src/lib.rs | 396 ++++++++++++++++++ userland/Cargo.lock | 6 + userland/libc/Cargo.lock | 5 + userland/libc/Cargo.toml | 1 + userland/libc/src/lib.rs | 1 + userland/libc/src/posix_io.rs | 2 +- userland/libc/src/refused.rs | 6 - userland/libc/src/uname.rs | 74 ++++ userland/supervisor/Cargo.toml | 3 + userland/supervisor/src/main.rs | 34 +- 22 files changed, 723 insertions(+), 23 deletions(-) create mode 100644 issues/build/an-images-signed-version-and-its-fat-dates-are-the-hosts-clock.md create mode 100644 toyos-osrelease/Cargo.toml create mode 100644 toyos-osrelease/src/lib.rs create mode 100644 userland/libc/src/uname.rs diff --git a/Cargo.lock b/Cargo.lock index 459db388e8b..9efa9130349 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1206,6 +1206,7 @@ dependencies = [ "toyos-keymap", "toyos-logstream", "toyos-manifest", + "toyos-osrelease", "toyos-quiesce", "toyos-swap", "toyos-symbols", @@ -1377,6 +1378,10 @@ dependencies = [ name = "toyos-net-wire" version = "0.1.0" +[[package]] +name = "toyos-osrelease" +version = "0.1.0" + [[package]] name = "toyos-pci" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 9c8e75f44c8..2563a890a00 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,6 +52,7 @@ members = [ "toyos-net-shard/testnet", "toyos-net-udp", "toyos-net-wire", + "toyos-osrelease", "toyos-quiesce", "toyos-rootimage", "toyos-swap", @@ -141,6 +142,7 @@ toyos-keymap = { path = "toyos-keymap" } # on, so the judges reading the log and `logkeeper` writing it read one declaration. toyos-logstream = { path = "toyos-logstream" } toyos-manifest = { path = "toyos-manifest" } +toyos-osrelease = { path = "toyos-osrelease" } # The shutdown's stop record: the kernel renders it and this crate reads it back # off a stick, so one declaration is what both sides spell it with. toyos-quiesce = { path = "toyos-quiesce" } diff --git a/issues/build/an-images-signed-version-and-its-fat-dates-are-the-hosts-clock.md b/issues/build/an-images-signed-version-and-its-fat-dates-are-the-hosts-clock.md new file mode 100644 index 00000000000..9c1348602e9 --- /dev/null +++ b/issues/build/an-images-signed-version-and-its-fat-dates-are-the-hosts-clock.md @@ -0,0 +1,22 @@ +--- +status: open +kind: defect +opened: 2026-10-04 +--- + +# An image's signed version and its FAT dates are the host's clock + +Two builds of one tree write different images, so ToyOS rebuilding itself +cannot reproduce the host's bytes. Two sites in `src/image.rs` read the clock: + +- `version_now()`, the signed header's anti-rollback version for a build that + names none (`src/build.rs`'s plans call it). +- `build_time()`, which dates every file and directory `populate` writes onto + the ESP and LOG volumes. + +ROOT is a function of its files (`root_uuid` hashes them), and +`/system/etc/os-release` records only source facts, its time being the +commit's (`src/build.rs`'s `release`). + +**Exit**: both derive from the commit's committer time, as +`TOYOS_COMMIT_TIME` does, or this file records why the signed version cannot. diff --git a/issues/build/libc-refuses-what-toyos-cannot-yet-answer.md b/issues/build/libc-refuses-what-toyos-cannot-yet-answer.md index aec0c62ef78..0c1512038b1 100644 --- a/issues/build/libc-refuses-what-toyos-cannot-yet-answer.md +++ b/issues/build/libc-refuses-what-toyos-cannot-yet-answer.md @@ -19,8 +19,7 @@ These answer failure in their POSIX form and do nothing - `getrlimit` and `setrlimit`, `ENOSYS`: no call answers a process's limits. LLVM's `getDefaultStackSize` (`llvm/lib/Support/ProgramStack.cpp`) ignores the failure and reads the `rlimit` it did not get. -- `gethostname` and `uname`, `ENOSYS`: no host name, release or version is - published to a process. +- `gethostname`, `ENOSYS`: no host name is published to a process. - `realpath`, `ENOSYS`: the kernel resolves a path by rules of its own (`..` read off the text, only the last name's link followed, a relative link read against its mount), and no call answers where a path leads. diff --git a/kernel/src/main.rs b/kernel/src/main.rs index 4a1ce8185ab..d5e5cac4868 100644 --- a/kernel/src/main.rs +++ b/kernel/src/main.rs @@ -326,6 +326,10 @@ pub(crate) unsafe extern "C" fn kernel_main(kernel_args: &KernelArgs) -> ! { "boot: cmdline {:#x}+{}", kernel_args.cmdline_addr, kernel_args.cmdline_len ); + match rootfs::named() { + Some(root) => log!("boot: root={root}"), + None => log!("boot: root= names no filesystem this kernel can parse"), + } // Before `mm::init`, which may hand the parameter's memory out. This record // is how a slot that died or was refused reaches the next boot's `/log`. match params::slot(cmdline) { diff --git a/kernel/src/rootfs.rs b/kernel/src/rootfs.rs index 926cbd5e04e..3b23c74c98f 100644 --- a/kernel/src/rootfs.rs +++ b/kernel/src/rootfs.rs @@ -123,6 +123,13 @@ pub fn init(cmdline: &str, args: &KernelArgs, map: &[MemoryMapEntry]) -> Region region } +/// The filesystem `root=` names, if this kernel can parse it: a hash of every +/// file ROOT carries, `/system/etc/os-release` among them, so it names the +/// build before anything is mounted. +pub fn named() -> Option { + BOOT.lock().named +} + /// Mount the filesystem the boot parameter named, off the image the loader /// handed. Panics when there is no image, or it is not that filesystem. pub fn mount() -> Mounted { diff --git a/src/build.rs b/src/build.rs index 65c9c8f7db5..6a3417e46dc 100644 --- a/src/build.rs +++ b/src/build.rs @@ -704,6 +704,41 @@ fn render_manifest(config: &SystemConfig) -> Vec { .unwrap_or_else(|e| panic!("system.toml cannot be rendered as a manifest: {e:?}")) } +/// Which build `root`'s tree makes for `arch` against the sysroot whose key +/// for it is `toolchain`, as `/system/etc/os-release` records it. +/// +/// `--ignore-submodules=all`: the fork's state is the toolchain key's, and +/// recursing into its tree costs a second of every image build. +fn release(root: &Path, toolchain: &crate::keystore::Key, arch: Arch) -> toyos_osrelease::Release { + use crate::sysroot::git_out; + let head = git_out(root, &["rev-parse", "HEAD"]); + let commit = toyos_osrelease::Hex::parse(head.trim()) + .unwrap_or_else(|| panic!("`git rev-parse HEAD` in {} said {head:?}, which is no commit", root.display())); + let object = git_out(root, &["cat-file", "commit", commit.as_str()]); + let committed = committer_time(&object) + .unwrap_or_else(|| panic!("commit {} names no committer time:\n{object}", commit.as_str())); + let status = git_out(root, &["status", "--porcelain", "--ignore-submodules=all"]); + let tree = if status.is_empty() { toyos_osrelease::Tree::Clean } else { toyos_osrelease::Tree::Dirty }; + toyos_osrelease::Release { + commit, + tree, + toolchain: toyos_osrelease::Hex::parse(toolchain.as_str()).expect("a key is sixteen hex digits"), + arch: match arch { + Arch::X86_64 => toyos_osrelease::Arch::X86_64, + Arch::Aarch64 => toyos_osrelease::Arch::Aarch64, + }, + committed, + } +} + +/// The Unix seconds of a commit object's `committer` header, which ends +/// ` `. +fn committer_time(object: &str) -> Option { + let headers = object.split("\n\n").next()?; + let committer = headers.lines().find_map(|line| line.strip_prefix("committer "))?; + committer.rsplit(' ').nth(1)?.parse().ok() +} + fn build_and_assemble( root: &Path, config: &SystemConfig, @@ -715,6 +750,8 @@ fn build_and_assemble( let mut root_files: Vec<(String, Vec)> = Vec::new(); build_programs(root, config, env, quiet, arch, &mut root_files); root_files.push((toyos_manifest::PATH.to_string(), render_manifest(config))); + let release = release(root, env.sysroot.identity.of_target(arch.userland()), arch); + root_files.push((toyos_osrelease::PATH.to_string(), release.to_string().into_bytes())); if config.hosted_rustc { assert!( @@ -2267,6 +2304,54 @@ fn collect_hosted_rustc(root: &Path, toolchain: &Path, root_files: &mut Vec<(Str mod tests { use super::*; + /// **The release a tree records is its commit, at that commit's own time, + /// and dirty from the first file that is not that commit's**: an + /// untracked file counts, and so does an edit to a tracked one. The time + /// is the committer's, in a zone not UTC's, and never the author's or the + /// host clock's. + #[test] + fn a_tree_records_its_commit_its_commit_time_and_whether_it_is_dirty() { + let (_dir, _origin, work) = crate::gitfixture::repo("release"); + fs::write(work.join("f"), "next\n").unwrap(); + crate::gitfixture::sh(&work, &["add", "f"]); + let committed = Command::new("git") + .args(["commit", "-qm", "next"]) + .env("GIT_AUTHOR_DATE", "@1000000000 +0000") + .env("GIT_COMMITTER_DATE", "@1791089159 +0200") + .current_dir(&work) + .status() + .unwrap(); + assert!(committed.success()); + let head = crate::sysroot::git_out(&work, &["rev-parse", "HEAD"]); + let key = crate::keystore::Key::parse("0123456789abcdef").unwrap(); + + let clean = release(&work, &key, Arch::Aarch64); + assert_eq!(clean.commit.as_str(), head.trim()); + assert_eq!(clean.committed, 1_791_089_159); + assert_eq!(clean.tree, toyos_osrelease::Tree::Clean); + assert_eq!(clean.toolchain.as_str(), key.as_str()); + assert_eq!(clean.arch, toyos_osrelease::Arch::Aarch64); + + fs::write(work.join("untracked"), "x").unwrap(); + assert_eq!(release(&work, &key, Arch::Aarch64).tree, toyos_osrelease::Tree::Dirty); + fs::remove_file(work.join("untracked")).unwrap(); + assert_eq!(release(&work, &key, Arch::Aarch64).tree, toyos_osrelease::Tree::Clean); + fs::write(work.join("f"), "edited\n").unwrap(); + assert_eq!(release(&work, &key, Arch::Aarch64).tree, toyos_osrelease::Tree::Dirty); + // An ignored file is the build's own output, not a change to the tree. + fs::write(work.join("f"), "next\n").unwrap(); + fs::create_dir(work.join("target")).unwrap(); + fs::write(work.join("target/out"), "x").unwrap(); + assert_eq!(release(&work, &key, Arch::Aarch64).tree, toyos_osrelease::Tree::Clean); + } + + #[test] + fn a_commit_without_a_committer_time_has_none() { + assert_eq!(committer_time("tree t\ncommitter A 17 +0200\n\nmsg\n"), Some(17)); + assert_eq!(committer_time("tree t\nauthor A 17 +0200\n\ncommitter A 18 +0200\n"), None); + assert_eq!(committer_time("tree t\ncommitter A +0200\n\n"), None); + } + /// `console` is reached by `console/system.toml` alone and the supervisor by no /// `[programs]` row, so a reader that drops a mode or the supervisor loses one. #[test] diff --git a/src/sysroot.rs b/src/sysroot.rs index b1e608fc4e3..71ef1880eea 100644 --- a/src/sysroot.rs +++ b/src/sysroot.rs @@ -59,16 +59,24 @@ 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`. -pub const SYSROOT_SOURCES: [&str; 5] = - ["toyos-abi/src", "toyos/src", "toyos-elf/src", "userland/libc/src", "userland/libc/include"]; +/// and `toyos`, and `libtoyos_c.a` is `userland/libc` with `toyos-elf` and +/// `toyos-osrelease`. +pub const SYSROOT_SOURCES: [&str; 6] = [ + "toyos-abi/src", + "toyos/src", + "toyos-elf/src", + "toyos-osrelease/src", + "userland/libc/src", + "userland/libc/include", +]; /// Their manifests, and the lockfile and cargo configuration libc is built /// under: the features, versions and flags of the same build. -pub(crate) const SYSROOT_MANIFESTS: [&str; 6] = [ +pub(crate) const SYSROOT_MANIFESTS: [&str; 7] = [ "toyos-abi/Cargo.toml", "toyos/Cargo.toml", "toyos-elf/Cargo.toml", + "toyos-osrelease/Cargo.toml", "userland/libc/Cargo.toml", "userland/libc/Cargo.lock", "userland/.cargo/config.toml", @@ -198,6 +206,13 @@ impl Identity { Self::new(of(compiler), &of(freestanding), &of(key)) } + /// The key of `triple`'s libraries, which reads the compiler's key and the + /// freestanding libraries' too: everything an image of that target's + /// programs was compiled with. + pub fn of_target(&self, triple: &str) -> &Key { + self.libraries.get(triple).unwrap_or_else(|| panic!("{triple} is no guest target of this sysroot")) + } + /// What this leaves stale of a target directory whose `.deps-stamp` says /// `stamp`, if it has one: nothing, if it is this. pub fn stale(&self, stamp: Option<&str>) -> Option { diff --git a/tests/common/qemu.rs b/tests/common/qemu.rs index 8847ed12e05..dbaf3e14efd 100644 --- a/tests/common/qemu.rs +++ b/tests/common/qemu.rs @@ -2251,6 +2251,15 @@ fn spawn_and_wait_ready(mut qemu: Command, options: &BootOptions, files: Files) } else { wait_for_ready(&mut child, &rx, options, &uart_log) }; + // The ready marker is test-runner's, which the supervisor starts after its + // first line, so a boot that reached it has said its build. + if options.ready_marker == DEFAULT_READY && !options.mute { + let said = format!("{}{} ", toyos_osrelease::SAID, built_commit()); + if !boot_log.lines().any(|line| line.contains(&said)) { + let _ = child.kill(); + panic!("[qemu] the boot never said `{said}`, the commit this run built\nconsole:\n{boot_log}"); + } + } QemuInstance { child, @@ -2269,6 +2278,21 @@ fn spawn_and_wait_ready(mut qemu: Command, options: &BootOptions, files: Files) } } +/// The commit this run's images are built from, read by `git` itself rather +/// than by the build that records it. +fn built_commit() -> &'static str { + static COMMIT: std::sync::OnceLock = std::sync::OnceLock::new(); + COMMIT.get_or_init(|| { + let out = Command::new("git") + .args(["rev-parse", "HEAD"]) + .current_dir(super::compile::repo_root()) + .output() + .expect("run git"); + assert!(out.status.success(), "git rev-parse HEAD: {}", String::from_utf8_lossy(&out.stderr)); + String::from_utf8(out.stdout).expect("a commit is text").trim().to_string() + }) +} + /// One finished console line into everything that keeps one. /// /// `false` means nothing is left to read for: the receiver has gone, or the diff --git a/tests/testcases/tinycc/206_libc_refusals.c b/tests/testcases/tinycc/206_libc_refusals.c index 8ea2bebd395..8b6950cc358 100644 --- a/tests/testcases/tinycc/206_libc_refusals.c +++ b/tests/testcases/tinycc/206_libc_refusals.c @@ -1,13 +1,14 @@ /* What libc refuses: each call answers failure in its own POSIX form, errno says why, and nothing is done: ENOSYS where ToyOS lacks the function, and the errno POSIX names for a lock or a mapping it cannot take, or memory it - cannot give. */ + cannot give. uname, which answers, is read back beside gethostname. */ #include #include #include #include #include #include +#include #include #include #include @@ -17,6 +18,20 @@ #define FILE_PATH "/tmp/206_libc_refusals" +#if defined(__x86_64__) +#define MACHINE "x86_64" +#elif defined(__aarch64__) +#define MACHINE "aarch64" +#endif + +/* Whether `s` is `n` lowercase hex digits and then `rest`. */ +static int hex_then(const char *s, int n, const char *rest) { + for (int i = 0; i < n; i++) + if (!((s[i] >= '0' && s[i] <= '9') || (s[i] >= 'a' && s[i] <= 'f'))) + return 0; + return strcmp(s + n, rest) == 0; +} + static const char *errno_name(int e) { switch (e) { case 0: return "no errno"; @@ -71,7 +86,6 @@ int main(void) { said("setsid", setsid()); said("getsid", getsid(0)); said("gethostname", gethostname(buf, sizeof buf)); - said("uname", uname(&uts)); said("link", link(FILE_PATH, FILE_PATH ".link")); said("symlink", symlink(FILE_PATH, FILE_PATH ".symlink")); said("fchown", fchown(fd, 0, 0)); @@ -114,6 +128,19 @@ int main(void) { said("fcntl F_SETFL", fcntl(fd, F_SETFL, O_NONBLOCK)); said("fcntl 12345", fcntl(fd, 12345)); + /* uname answers the build ROOT carries, and no node name: the release is + the commit's first twelve digits, and -dirty after them for a tree + that was not that commit's; the version is the whole commit; the + machine is the one this case was compiled for. */ + said("uname", uname(&uts)); + printf("uname sysname: %s; nodename: %s\n", uts.sysname, uts.nodename[0] ? uts.nodename : "empty"); + printf("uname version: %s\n", hex_then(uts.version, 40, "") ? "a commit" : uts.version); + printf("uname release: %s\n", + (hex_then(uts.release, 12, "") || hex_then(uts.release, 12, "-dirty")) && strncmp(uts.release, uts.version, 12) == 0 + ? "the version's first twelve digits" + : uts.release); + printf("uname machine: %s\n", strcmp(uts.machine, MACHINE) == 0 ? "the one this case was compiled for" : uts.machine); + /* The _r lookups answer their error, and null for the entry. */ int answer = getpwnam_r("root", &pw, buf, sizeof buf, &found); printf("getpwnam_r: %s, entry %s\n", errno_name(answer), found ? "set" : "null"); diff --git a/tests/testcases/tinycc/206_libc_refusals.expect b/tests/testcases/tinycc/206_libc_refusals.expect index 5de407a79ad..3879772d31f 100644 --- a/tests/testcases/tinycc/206_libc_refusals.expect +++ b/tests/testcases/tinycc/206_libc_refusals.expect @@ -3,7 +3,6 @@ execve: -1, ENOSYS setsid: -1, ENOSYS getsid: -1, ENOSYS gethostname: -1, ENOSYS -uname: -1, ENOSYS link: -1, ENOSYS symlink: -1, ENOSYS fchown: -1, ENOSYS @@ -31,6 +30,11 @@ fcntl F_DUPFD_CLOEXEC: -1, ENOSYS fcntl F_GETFL: -1, ENOSYS fcntl F_SETFL: -1, ENOSYS fcntl 12345: -1, EINVAL +uname: 0, no errno +uname sysname: ToyOS; nodename: empty +uname version: a commit +uname release: the version's first twelve digits +uname machine: the one this case was compiled for getpwnam_r: ENOSYS, entry null getpwuid_r: ENOSYS, entry null mmap a file: -1, ENODEV diff --git a/toyos-osrelease/Cargo.toml b/toyos-osrelease/Cargo.toml new file mode 100644 index 00000000000..82270851eec --- /dev/null +++ b/toyos-osrelease/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "toyos-osrelease" +description = "The one definition of /system/etc/os-release: which build an image is, written by the build and read by the supervisor and libc's uname (no_std, no alloc)." +version = "0.1.0" +edition = "2021" +license = "MIT OR Apache-2.0" diff --git a/toyos-osrelease/src/lib.rs b/toyos-osrelease/src/lib.rs new file mode 100644 index 00000000000..02b0a954496 --- /dev/null +++ b/toyos-osrelease/src/lib.rs @@ -0,0 +1,396 @@ +//! `/system/etc/os-release`: which build an image is, written by the build +//! system into ROOT and read by `/system/bin/supervisor` and libc's `uname`. +//! +//! The freedesktop os-release format (`os-release(5)`), ToyOS's own fields +//! prefixed `TOYOS_` as it asks. The build is the file's one writer, so +//! [`parse`] takes exactly what [`Release`]'s `Display` writes and refuses +//! anything else by name: every field that is not one of [`Release`]'s is a +//! function of them, and a file whose bytes are not that function's is +//! [`Malformed::NotAsWritten`]. +//! +//! Every field is a fact of the source — the commit, whether the files were +//! that commit's, the toolchain's key, the commit's own time — and none is the +//! host's or its clock's, so two builds of one tree write the same bytes. +//! +//! ```text +//! NAME="ToyOS" +//! ID="toyos" +//! VERSION_ID="<12-hex commit>" +//! PRETTY_NAME="ToyOS <12-hex commit>[ (dirty)]" +//! BUILD_ID="<40-hex commit>" +//! ARCHITECTURE="x86-64" | "arm64" +//! HOME_URL="https://github.com/ToyOSOrg/ToyOS" +//! TOYOS_TREE="clean" | "dirty" untracked files count +//! TOYOS_TOOLCHAIN="<16-hex key>" the sysroot key the image was compiled against +//! TOYOS_COMMIT_TIME="" the commit's committer time +//! ``` + +#![no_std] +#![forbid(unsafe_code)] + +use core::fmt::{self, Write}; + +/// Where ROOT carries it, without a leading slash — that volume's own +/// spelling. [`GUEST_PATH`] is what a process opens. +pub const PATH: &str = "etc/os-release"; + +/// The path a process opens. +pub const GUEST_PATH: &str = "/system/etc/os-release"; + +/// The operating system's name, `NAME` and `uname`'s `sysname`. +pub const NAME: &str = "ToyOS"; + +/// What `/system/bin/supervisor` says a release under, followed by its commit. +pub const SAID: &str = "supervisor: build "; + +/// The architecture an image is built for. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Arch { + X86_64, + Aarch64, +} + +impl Arch { + /// `ARCHITECTURE`'s spelling, which is systemd's. + fn os_release(self) -> &'static str { + match self { + Arch::X86_64 => "x86-64", + Arch::Aarch64 => "arm64", + } + } + + /// `uname`'s `machine`. + pub fn machine(self) -> &'static str { + match self { + Arch::X86_64 => "x86_64", + Arch::Aarch64 => "aarch64", + } + } +} + +/// Whether the files an image was built from were its commit's. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Tree { + Clean, + Dirty, +} + +impl fmt::Display for Tree { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Tree::Clean => "clean", + Tree::Dirty => "dirty", + }) + } +} + +/// `N` lowercase hex digits. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Hex([u8; N]); + +impl Hex { + /// `text`, if it is exactly `N` lowercase hex digits. + pub fn parse(text: &str) -> Option { + let digits = <[u8; N]>::try_from(text.as_bytes()).ok()?; + digits.iter().all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'f')).then_some(Self(digits)) + } + + pub fn as_str(&self) -> &str { + core::str::from_utf8(&self.0).expect("hex digits are ASCII") + } +} + +/// Which build an image is. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Release { + pub commit: Hex<40>, + pub tree: Tree, + /// The key of the sysroot the image was compiled against, which names its + /// compiler and std as well as its libc. + pub toolchain: Hex<16>, + pub arch: Arch, + /// The commit's committer time, in Unix seconds. + pub committed: u64, +} + +impl Release { + /// The commit's first twelve digits. + pub fn short(&self) -> &str { + &self.commit.as_str()[..12] + } +} + +/// The file's bytes. +impl fmt::Display for Release { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let short = self.short(); + let dirty = match self.tree { + Tree::Clean => "", + Tree::Dirty => " (dirty)", + }; + writeln!(f, "NAME=\"{NAME}\"")?; + writeln!(f, "ID=\"toyos\"")?; + writeln!(f, "VERSION_ID=\"{short}\"")?; + writeln!(f, "PRETTY_NAME=\"{NAME} {short}{dirty}\"")?; + writeln!(f, "{BUILD_ID}=\"{}\"", self.commit.as_str())?; + writeln!(f, "{ARCHITECTURE}=\"{}\"", self.arch.os_release())?; + writeln!(f, "HOME_URL=\"https://github.com/ToyOSOrg/ToyOS\"")?; + writeln!(f, "{TOYOS_TREE}=\"{}\"", self.tree)?; + writeln!(f, "{TOYOS_TOOLCHAIN}=\"{}\"", self.toolchain.as_str())?; + writeln!(f, "{TOYOS_COMMIT_TIME}=\"{}\"", self.committed) + } +} + +const BUILD_ID: &str = "BUILD_ID"; +const ARCHITECTURE: &str = "ARCHITECTURE"; +const TOYOS_TREE: &str = "TOYOS_TREE"; +const TOYOS_TOOLCHAIN: &str = "TOYOS_TOOLCHAIN"; +const TOYOS_COMMIT_TIME: &str = "TOYOS_COMMIT_TIME"; + +/// The fields a [`Release`] is read from; every other is a function of them. +const FIELDS: [&str; 5] = [BUILD_ID, TOYOS_TREE, TOYOS_TOOLCHAIN, ARCHITECTURE, TOYOS_COMMIT_TIME]; + +/// Why a file is not one the build wrote. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Malformed { + NotUtf8, + Missing(&'static str), + Duplicate(&'static str), + /// The field is there, and its value is not one the build writes for it. + Value(&'static str), + /// Every field a [`Release`] is read from is, and the file is not what the + /// build writes for them: another derived field, another field, another + /// order or another quoting. + NotAsWritten, +} + +/// The release `bytes` name, if they are what the build writes for one. +pub fn parse(bytes: &[u8]) -> Result { + let text = core::str::from_utf8(bytes).map_err(|_| Malformed::NotUtf8)?; + let mut found: [Option<&str>; FIELDS.len()] = [None; FIELDS.len()]; + for (key, value) in text.lines().filter_map(|line| line.split_once('=')) { + if let Some(at) = FIELDS.iter().position(|field| *field == key) { + if found[at].replace(value).is_some() { + return Err(Malformed::Duplicate(FIELDS[at])); + } + } + } + let value = |field: &'static str| -> Result<&str, Malformed> { + let at = FIELDS.iter().position(|f| *f == field).expect("a field this reads"); + let quoted = found[at].ok_or(Malformed::Missing(field))?; + quoted.strip_prefix('"').and_then(|v| v.strip_suffix('"')).ok_or(Malformed::Value(field)) + }; + let release = Release { + commit: Hex::parse(value(BUILD_ID)?).ok_or(Malformed::Value(BUILD_ID))?, + tree: match value(TOYOS_TREE)? { + "clean" => Tree::Clean, + "dirty" => Tree::Dirty, + _ => return Err(Malformed::Value(TOYOS_TREE)), + }, + toolchain: Hex::parse(value(TOYOS_TOOLCHAIN)?).ok_or(Malformed::Value(TOYOS_TOOLCHAIN))?, + arch: match value(ARCHITECTURE)? { + "x86-64" => Arch::X86_64, + "arm64" => Arch::Aarch64, + _ => return Err(Malformed::Value(ARCHITECTURE)), + }, + committed: value(TOYOS_COMMIT_TIME)?.parse().map_err(|_| Malformed::Value(TOYOS_COMMIT_TIME))?, + }; + let mut rest = Against(text); + match write!(rest, "{release}") { + Ok(()) if rest.0.is_empty() => Ok(release), + _ => Err(Malformed::NotAsWritten), + } +} + +/// A writer that takes only the text it holds, in order, consuming it. +struct Against<'a>(&'a str); + +impl Write for Against<'_> { + fn write_str(&mut self, s: &str) -> fmt::Result { + self.0 = self.0.strip_prefix(s).ok_or(fmt::Error)?; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + extern crate std; + use super::*; + use std::string::{String, ToString}; + use std::vec::Vec; + + fn release(tree: Tree, arch: Arch) -> Release { + Release { + commit: Hex::parse("1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b").unwrap(), + tree, + toolchain: Hex::parse("0123456789abcdef").unwrap(), + arch, + committed: 1_791_089_159, + } + } + + fn every() -> [Release; 4] { + [ + release(Tree::Clean, Arch::X86_64), + release(Tree::Dirty, Arch::X86_64), + release(Tree::Clean, Arch::Aarch64), + release(Tree::Dirty, Arch::Aarch64), + ] + } + + /// The file `text` with `field`'s line replaced by `line`, or dropped for `None`. + fn with(text: &str, field: &str, line: Option<&str>) -> String { + let mut out = String::new(); + for l in text.lines() { + match l.split_once('=') { + Some((key, _)) if key == field => { + if let Some(line) = line { + out.push_str(line); + out.push('\n'); + } + } + _ => { + out.push_str(l); + out.push('\n'); + } + } + } + out + } + + #[test] + fn what_the_build_writes_is_what_a_reader_reads() { + for r in every() { + assert_eq!(parse(r.to_string().as_bytes()), Ok(r)); + } + } + + #[test] + fn the_guest_path_is_where_root_carries_it() { + assert_eq!(GUEST_PATH.strip_prefix("/system/"), Some(PATH)); + } + + /// The bytes, whole: what `os-release(5)` readers other than ours see. + #[test] + fn a_dirty_x86_build_reads_so() { + assert_eq!( + release(Tree::Dirty, Arch::X86_64).to_string(), + "NAME=\"ToyOS\"\n\ + ID=\"toyos\"\n\ + VERSION_ID=\"1a2b3c4d5e6f\"\n\ + PRETTY_NAME=\"ToyOS 1a2b3c4d5e6f (dirty)\"\n\ + BUILD_ID=\"1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b\"\n\ + ARCHITECTURE=\"x86-64\"\n\ + HOME_URL=\"https://github.com/ToyOSOrg/ToyOS\"\n\ + TOYOS_TREE=\"dirty\"\n\ + TOYOS_TOOLCHAIN=\"0123456789abcdef\"\n\ + TOYOS_COMMIT_TIME=\"1791089159\"\n" + ); + } + + /// **The independent oracle: `os-release(5)`'s grammar**, held against + /// every file the build can write. Each line is a variable assignment + /// whose name is upper case, digits and underscores; a value is quoted + /// and carries none of the shell specials the specification makes + /// a writer escape; `ID` and `VERSION_ID` are lower case, digits, `.`, + /// `_` and `-` alone; `ARCHITECTURE` is one of systemd's names; and a + /// vendor field is prefixed with the vendor's name. + #[test] + fn every_file_the_build_writes_is_os_release() { + const STANDARD: [&str; 6] = ["NAME", "ID", "VERSION_ID", "PRETTY_NAME", "BUILD_ID", "HOME_URL"]; + const SYSTEMD_ARCHITECTURES: [&str; 2] = ["x86-64", "arm64"]; + for r in every() { + let text = r.to_string(); + assert!(text.ends_with('\n')); + let mut names = Vec::new(); + for line in text.lines() { + let (name, quoted) = line.split_once('=').unwrap(); + assert!(name.bytes().all(|b| b.is_ascii_uppercase() || b.is_ascii_digit() || b == b'_'), "{line}"); + assert!(STANDARD.contains(&name) || name == "ARCHITECTURE" || name.starts_with("TOYOS_"), "{line}"); + let value = quoted.strip_prefix('"').and_then(|v| v.strip_suffix('"')).unwrap(); + assert!(!value.contains(['"', '\\', '$', '`', '\'']), "{line}"); + if name == "ID" || name == "VERSION_ID" { + assert!( + value.bytes().all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b"._-".contains(&b)), + "{line}" + ); + } + if name == "ARCHITECTURE" { + assert!(SYSTEMD_ARCHITECTURES.contains(&value), "{line}"); + } + names.push(name.to_string()); + } + let unique: std::collections::BTreeSet<&String> = names.iter().collect(); + assert_eq!(unique.len(), names.len(), "a field twice in {text}"); + } + } + + #[test] + fn a_file_that_is_not_text_is_refused() { + let mut bytes = release(Tree::Clean, Arch::X86_64).to_string().into_bytes(); + bytes[8] = 0xff; + assert_eq!(parse(&bytes), Err(Malformed::NotUtf8)); + } + + #[test] + fn a_missing_or_doubled_field_is_refused_by_its_name() { + let text = release(Tree::Clean, Arch::X86_64).to_string(); + for field in FIELDS { + assert_eq!(parse(with(&text, field, None).as_bytes()), Err(Malformed::Missing(field)), "{field}"); + let line = text.lines().find(|l| l.starts_with(&std::format!("{field}="))).unwrap(); + let doubled = std::format!("{text}{line}\n"); + assert_eq!(parse(doubled.as_bytes()), Err(Malformed::Duplicate(field)), "{field}"); + } + } + + #[test] + fn a_value_the_build_does_not_write_is_refused_by_its_field() { + let text = release(Tree::Clean, Arch::X86_64).to_string(); + let refused = [ + (BUILD_ID, "BUILD_ID=\"1A2B3C4D5E6F7A8B9C0D1E2F3A4B5C6D7E8F9A0B\""), + (BUILD_ID, "BUILD_ID=\"1a2b3c4d5e6f\""), + (BUILD_ID, "BUILD_ID=1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b"), + (TOYOS_TREE, "TOYOS_TREE=\"maybe\""), + (TOYOS_TREE, "TOYOS_TREE=\"clean"), + (TOYOS_TOOLCHAIN, "TOYOS_TOOLCHAIN=\"0123456789abcdeg\""), + (ARCHITECTURE, "ARCHITECTURE=\"x86_64\""), + (ARCHITECTURE, "ARCHITECTURE=\"riscv64\""), + (TOYOS_COMMIT_TIME, "TOYOS_COMMIT_TIME=\"-1\""), + (TOYOS_COMMIT_TIME, "TOYOS_COMMIT_TIME=\"18446744073709551616\""), + (TOYOS_COMMIT_TIME, "TOYOS_COMMIT_TIME=\"1791089159 \""), + ]; + for (field, line) in refused { + assert_eq!(parse(with(&text, field, Some(line)).as_bytes()), Err(Malformed::Value(field)), "{line}"); + } + } + + /// Each field a [`Release`] is read from parses, and the file is still not + /// the one the build writes. + #[test] + fn a_file_the_build_did_not_write_is_refused() { + let clean = release(Tree::Clean, Arch::X86_64).to_string(); + let dirty = release(Tree::Dirty, Arch::X86_64).to_string(); + let refused = [ + with(&clean, "PRETTY_NAME", Some("PRETTY_NAME=\"ToyOS 1a2b3c4d5e6f (dirty)\"")), + with(&dirty, "PRETTY_NAME", Some("PRETTY_NAME=\"ToyOS 1a2b3c4d5e6f\"")), + with(&clean, "VERSION_ID", Some("VERSION_ID=\"ffffffffffff\"")), + with(&clean, "NAME", Some("NAME=ToyOS")), + with(&clean, "ID", None), + with(&clean, "TOYOS_COMMIT_TIME", Some("TOYOS_COMMIT_TIME=\"+1791089159\"")), + with(&clean, "TOYOS_COMMIT_TIME", Some("TOYOS_COMMIT_TIME=\"01791089159\"")), + std::format!("{clean}VARIANT=\"x\"\n"), + std::format!("# a comment\n{clean}"), + clean.replace('\n', "\r\n"), + clean.trim_end().to_string(), + std::format!("{clean}\n"), + { + let mut lines: Vec<&str> = clean.lines().collect(); + lines.swap(0, 1); + lines.iter().map(|l| std::format!("{l}\n")).collect() + }, + ]; + for text in refused { + assert_eq!(parse(text.as_bytes()), Err(Malformed::NotAsWritten), "{text}"); + } + } +} diff --git a/userland/Cargo.lock b/userland/Cargo.lock index 856df3b4c2f..ea99c33adb5 100644 --- a/userland/Cargo.lock +++ b/userland/Cargo.lock @@ -3946,9 +3946,11 @@ dependencies = [ "toyos-gpt", "toyos-logstream", "toyos-manifest", + "toyos-osrelease", "toyos-quiesce", "toyos-swap", "toyos-update", + "toyos-wallclock", ] [[package]] @@ -4249,6 +4251,10 @@ dependencies = [ name = "toyos-mixer" version = "0.1.0" +[[package]] +name = "toyos-osrelease" +version = "0.1.0" + [[package]] name = "toyos-quiesce" version = "0.1.0" diff --git a/userland/libc/Cargo.lock b/userland/libc/Cargo.lock index 17bddce55cc..2c169500537 100644 --- a/userland/libc/Cargo.lock +++ b/userland/libc/Cargo.lock @@ -48,8 +48,13 @@ dependencies = [ "toyos", "toyos-abi", "toyos-elf", + "toyos-osrelease", ] +[[package]] +name = "toyos-osrelease" +version = "0.1.0" + [[package]] name = "windows-link" version = "0.2.1" diff --git a/userland/libc/Cargo.toml b/userland/libc/Cargo.toml index 91181462d6e..99af4f6ddc4 100644 --- a/userland/libc/Cargo.toml +++ b/userland/libc/Cargo.toml @@ -12,6 +12,7 @@ std-runtime = [] toyos-abi = { path = "../../toyos-abi" } toyos = { path = "../../toyos" } toyos-elf = { path = "../../toyos-elf" } +toyos-osrelease = { path = "../../toyos-osrelease" } dlmalloc = "0.2.7" [profile.dev] diff --git a/userland/libc/src/lib.rs b/userland/libc/src/lib.rs index 77da705e7b9..8ea47af3be4 100644 --- a/userland/libc/src/lib.rs +++ b/userland/libc/src/lib.rs @@ -30,6 +30,7 @@ mod string; mod strtonum; mod text; mod time; +mod uname; mod utf8; mod wchar; diff --git a/userland/libc/src/posix_io.rs b/userland/libc/src/posix_io.rs index f88fdaeb7cb..a08d3e5816a 100644 --- a/userland/libc/src/posix_io.rs +++ b/userland/libc/src/posix_io.rs @@ -36,7 +36,7 @@ const S_IFLNK: u32 = 0o120000; // Helper: set errno from toyos-abi error -fn set_errno(e: toyos_abi::syscall::SyscallError) -> i32 { +pub(crate) fn set_errno(e: toyos_abi::syscall::SyscallError) -> i32 { use toyos_abi::syscall::SyscallError; let code = match e { SyscallError::NotFound => ENOENT, diff --git a/userland/libc/src/refused.rs b/userland/libc/src/refused.rs index ea313350d88..ab7c08b432a 100644 --- a/userland/libc/src/refused.rs +++ b/userland/libc/src/refused.rs @@ -40,12 +40,6 @@ pub unsafe extern "C" fn gethostname(_name: *mut u8, _len: usize) -> i32 { refuse() } -/// No release, version or node name is published to a process. -#[no_mangle] -pub unsafe extern "C" fn uname(_buf: *mut u8) -> i32 { - refuse() -} - /// No filesystem ToyOS mounts keeps a second name for a file. #[no_mangle] pub unsafe extern "C" fn link(_existing: *const u8, _new: *const u8) -> i32 { diff --git a/userland/libc/src/uname.rs b/userland/libc/src/uname.rs new file mode 100644 index 00000000000..4c0a5b07d4a --- /dev/null +++ b/userland/libc/src/uname.rs @@ -0,0 +1,74 @@ +//! `uname`, answered from the build ROOT carries (`toyos-osrelease`): `ToyOS`, +//! no node name — no host name is published to a process — the commit's +//! twelve digits as the release, `-dirty` after them for a tree that was not +//! that commit's, the whole commit as the version, and the machine. + +use toyos_abi::syscall::{self, OpenFlags}; + +use crate::errno::{self, EIO}; + +#[repr(C)] +pub struct Utsname { + sysname: [u8; 65], + nodename: [u8; 65], + release: [u8; 65], + version: [u8; 65], + machine: [u8; 65], +} + +/// Room for any file the build writes, and a file this fills is not one. +const FILE_BYTES: usize = 512; + +#[no_mangle] +pub unsafe extern "C" fn uname(buf: *mut Utsname) -> i32 { + let mut file = [0u8; FILE_BYTES]; + let len = match read(toyos_osrelease::GUEST_PATH, &mut file) { + Ok(len) => len, + Err(e) => return crate::posix_io::set_errno(e), + }; + let Ok(release) = toyos_osrelease::parse(&file[..len]) else { + errno::set(EIO); + return -1; + }; + let out = &mut *buf; + put(&mut out.sysname, &[toyos_osrelease::NAME]); + put(&mut out.nodename, &[]); + let dirty = match release.tree { + toyos_osrelease::Tree::Clean => "", + toyos_osrelease::Tree::Dirty => "-dirty", + }; + put(&mut out.release, &[release.short(), dirty]); + put(&mut out.version, &[release.commit.as_str()]); + put(&mut out.machine, &[release.arch.machine()]); + 0 +} + +/// `parts`, one after another and NUL-terminated, into `field`. +fn put(field: &mut [u8; 65], parts: &[&str]) { + let mut at = 0; + for part in parts { + field[at..at + part.len()].copy_from_slice(part.as_bytes()); + at += part.len(); + } + field[at..].fill(0); +} + +/// As much of the file at `path` as `into` holds. +fn read(path: &str, into: &mut [u8]) -> Result { + let handle = syscall::open(path.as_bytes(), OpenFlags::READ)?; + let mut len = 0; + let read = loop { + match syscall::read(handle, &mut into[len..]) { + Ok(0) => break Ok(len), + Ok(n) => { + len += n; + if len == into.len() { + break Ok(len); + } + } + Err(e) => break Err(e), + } + }; + syscall::close(handle); + read +} diff --git a/userland/supervisor/Cargo.toml b/userland/supervisor/Cargo.toml index d3edec4b5b6..3b9ddb4d09c 100644 --- a/userland/supervisor/Cargo.toml +++ b/userland/supervisor/Cargo.toml @@ -8,6 +8,9 @@ license = "MIT OR Apache-2.0" toyos = { path = "../../toyos" } toyos-abi = { path = "../../toyos-abi" } toyos-manifest = { path = "../../toyos-manifest" } +# Which build this is, said first thing. +toyos-osrelease = { path = "../../toyos-osrelease" } +toyos-wallclock = { path = "../../toyos-wallclock" } toyos-logstream = { path = "../../toyos-logstream" } toyos-swap = { path = "../../toyos-swap" } # The slot table and which slot is idle, for the one row that asks for it. diff --git a/userland/supervisor/src/main.rs b/userland/supervisor/src/main.rs index 0af8366260f..a8b1af3dce0 100644 --- a/userland/supervisor/src/main.rs +++ b/userland/supervisor/src/main.rs @@ -337,13 +337,16 @@ fn main() { // Before anything is started, and before the supervisor says anything: from here // the supervisor's own lines are records in its ring. let log = Log::open(); + say_release(); let syscap: SysCap = Endowments::get() .take(SYSCAP_LABEL) .expect("supervisor: the kernel spawns this program holding the system capability"); - let text = read_manifest() + let bytes = read_root(toyos_manifest::GUEST_PATH) .unwrap_or_else(|e| panic!("supervisor: cannot read {}: {e:?}", toyos_manifest::GUEST_PATH)); + let text = String::from_utf8(bytes) + .unwrap_or_else(|_| panic!("supervisor: {} is not text", toyos_manifest::GUEST_PATH)); // For the machine's life, and `'static` so a launch's path can be resolved // against it on the supervisor's file worker. let system: &'static Manifest = Box::leak(Box::new(toyos_manifest::parse(&text))); @@ -2329,15 +2332,15 @@ fn storage_endowment(program: &Program, role: Option<&str>, syscap: &SysCap) -> Ok(storage) } -/// The manifest, off ROOT through the kernel's own `open`. +/// A file of ROOT, through the kernel's own `open`. /// /// **Not through std**: std resolves a path through this process's namespace, /// and the first path it resolves fixes that namespace for the process's life. -/// The supervisor builds its namespace out of the manifest, so the manifest is read +/// The supervisor builds its namespace out of the manifest, so ROOT's files are read /// before std is asked anything. -fn read_manifest() -> Result { +fn read_root(path: &str) -> Result, SyscallError> { use toyos_abi::syscall::{self, OpenFlags}; - let handle = syscall::open(toyos_manifest::GUEST_PATH.as_bytes(), OpenFlags::READ)?; + let handle = syscall::open(path.as_bytes(), OpenFlags::READ)?; let mut bytes = Vec::new(); let mut buf = [0u8; 4096]; let read = loop { @@ -2348,6 +2351,23 @@ fn read_manifest() -> Result { } }; syscall::close(handle); - read?; - String::from_utf8(bytes).map_err(|_| SyscallError::InvalidArgument) + read.map(|()| bytes) +} + +/// Say which build this machine runs: the first thing it says, so a boot +/// that dies past here names its build in its log. +fn say_release() { + let path = toyos_osrelease::GUEST_PATH; + let bytes = read_root(path).unwrap_or_else(|e| panic!("supervisor: cannot read {path}: {e:?}")); + let release = toyos_osrelease::parse(&bytes) + .unwrap_or_else(|e| panic!("supervisor: {path} is not one the build wrote: {e:?}")); + say!( + "{}{} {}, committed {} UTC, toolchain {}, {}", + toyos_osrelease::SAID, + release.commit.as_str(), + release.tree, + toyos_wallclock::Civil::from_unix_secs(release.committed), + release.toolchain.as_str(), + release.arch.machine() + ); } From bac28df91572158f9af54fbc0c00752cb0412ffc Mon Sep 17 00:00:00 2001 From: japabu Date: Sun, 4 Oct 2026 12:59:09 +0200 Subject: [PATCH 2/5] Issue: a supervisor panic before logkeeper starts reaches no console Found by this branch's negative control: a ROOT without os-release makes the supervisor exit 101 at 0.314 s on machine_shutdown, and its panic's words reach no console, because its stderr is a ring only logkeeper drains. The harness then waited out its 63 s boot ceiling. The manifest read's refusal has the same blind spot. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01WcU2Dsw6mDYtwYfzVHPzM8 --- ...ore-logkeeper-starts-reaches-no-console.md | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 issues/build/a-supervisor-panic-before-logkeeper-starts-reaches-no-console.md diff --git a/issues/build/a-supervisor-panic-before-logkeeper-starts-reaches-no-console.md b/issues/build/a-supervisor-panic-before-logkeeper-starts-reaches-no-console.md new file mode 100644 index 00000000000..c5a80194c46 --- /dev/null +++ b/issues/build/a-supervisor-panic-before-logkeeper-starts-reaches-no-console.md @@ -0,0 +1,22 @@ +--- +status: open +kind: defect +opened: 2026-10-04 +--- + +# A supervisor panic before logkeeper starts reaches no console + +`/system/bin/supervisor` makes its own lines records in its log ring first thing +(`Log::open` in `userland/supervisor/src/main.rs`), and only `logkeeper` +drains a ring. So a panic before `logkeeper` runs leaves its message in a +ring nobody reads. The refusals of a ROOT without +`/system/etc/os-release`, or without a readable `/system/etc/system.manifest`, +are such panics. + +Measured with ROOT carrying no os-release (`machine_shutdown`, x86-64, QEMU): +the console's last kernel record is `exit: supervisor pid=0 code=101`, the +panic's words appear nowhere, and the harness waits out its 63 s boot ceiling +for `===READY===`. It does not treat the supervisor's end as a death. + +**Exit**: a supervisor panic before `logkeeper` serves puts its message on the +console, and the harness ends a boot whose supervisor exited, naming the exit. From 44f46af0fad4f77adb4740cd01b61da36495c777 Mon Sep 17 00:00:00 2001 From: japabu Date: Sun, 4 Oct 2026 13:26:07 +0200 Subject: [PATCH 3/5] Answer review round 1: gix reads the build, the toolchain is the tag's key, uname has a case - The build reads HEAD, its committer time and the tree's status with gitoxide (gix 0.88, status and sha1 only) instead of three `git` runs, and the harness reads HEAD with it too: row 21 of the host-tools issue refuses `git` for reads because gitoxide does them. gix writes no index (no `index.lock` against the worktree's own commits), and the walk is set whole, because `status()` drops it for a checkout configured with `status.showUntrackedFiles=no`, which the host test now covers, as it covers a change that is only staged. On this worktree and on the primary, gix and `git status --porcelain --ignore-submodules=all` name the same 17 and 0 paths; 14 to 55 ms at load 61. - The release is read before the programs are built. - An installed toolchain's identity is the sysroot key its TOOLCHAIN's tag names (`release::named_key`), not a hash of the file, so TOYOS_TOOLCHAIN is one value for one toolchain wherever it was built. - `Release::uname_release` is the `[-dirty]` string; libc writes it. - The kernel logs `root=` apart from a cmdline without one, from the cmdline directly; `rootfs::named` goes. The harness asserts `boot: root=` on every ready boot (`image::root_uuid_on`). - uname moves out of 206 into its own case, 209_libc_uname. - GUEST_PATH is derived from PATH; the test that compared them goes. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01WcU2Dsw6mDYtwYfzVHPzM8 --- Cargo.lock | 1409 ++++++++++++++++- Cargo.toml | 3 + kernel/src/main.rs | 9 +- kernel/src/rootfs.rs | 7 - src/build.rs | 88 +- src/image.rs | 14 + src/release.rs | 15 +- src/sysroot.rs | 8 +- tests/common/qemu.rs | 26 +- tests/testcases/LICENSE | 4 +- tests/testcases/tinycc/206_libc_refusals.c | 32 +- .../testcases/tinycc/206_libc_refusals.expect | 5 - tests/testcases/tinycc/209_libc_uname.c | 39 + tests/testcases/tinycc/209_libc_uname.expect | 5 + toyos-osrelease/src/lib.rs | 34 +- userland/libc/src/uname.rs | 53 +- userland/supervisor/Cargo.toml | 1 - 17 files changed, 1552 insertions(+), 200 deletions(-) create mode 100644 tests/testcases/tinycc/209_libc_uname.c create mode 100644 tests/testcases/tinycc/209_libc_uname.expect diff --git a/Cargo.lock b/Cargo.lock index 9efa9130349..f93dcea121d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -38,6 +38,15 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "arc-swap" +version = "1.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c049c0be4daef0b145cb3555416b3b8ef5b7888a38aea1a3a155801fe7b0810b" +dependencies = [ + "rustversion", +] + [[package]] name = "arrayvec" version = "0.7.8" @@ -46,9 +55,9 @@ checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" [[package]] name = "autocfg" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "base64" @@ -84,6 +93,17 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bstr" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bb31b46c14244e20ee9984b11bf5c992b91fb6939fea616e3512c8baecdbe5f" +dependencies = [ + "memchr", + "regex-automata", + "serde_core", +] + [[package]] name = "bumpalo" version = "3.20.2" @@ -152,12 +172,27 @@ dependencies = [ "windows-link", ] +[[package]] +name = "clru" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "197fd99cb113a8d5d9b6376f3aa817f32c1078f2343b714fff7d2ca44fdf67d5" +dependencies = [ + "hashbrown 0.16.1", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "core_detect" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f8f80099a98041a3d1622845c271458a2d73e688351bf3cb999266764b81d48" + [[package]] name = "cpufeatures" version = "0.2.17" @@ -191,6 +226,21 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98b0cc327b5bc766e7fda9c9260cc0fa81b43a8e240440422dff70788e3f9ef1" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" + [[package]] name = "crypto-common" version = "0.1.7" @@ -227,6 +277,60 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "dashmap" +version = "6.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6361d5c062261c78a176addb82d4c821ae42bed6089de0e12603cd25de2059c" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "defmt" +version = "0.3.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0963443817029b2024136fc4dd07a5107eb8f977eaf18fcd1fdeb11306b64ad" +dependencies = [ + "defmt 1.1.1", +] + +[[package]] +name = "defmt" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8" +dependencies = [ + "defmt-parser", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror 2.0.18", +] + [[package]] name = "derive-into-owned" version = "0.2.0" @@ -256,9 +360,15 @@ checksum = "9f5b01c17f85ee988d832c40e549a64bd89ab2c9f8d8a613bdf5122ae507e294" dependencies = [ "cfg-if", "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + [[package]] name = "ed25519" version = "2.2.3" @@ -280,12 +390,36 @@ dependencies = [ "subtle", ] +[[package]] +name = "encoding_rs" +version = "0.8.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e985e0451871ad22fb8d2b6b076e2028a502a0d3950998c2c5c0a4f9b5d9679" +dependencies = [ + "cfg-if", + "core_detect", + "multiversion_no_op", + "rustversion", + "scopeguard", + "simdutf8", +] + [[package]] name = "equivalent" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + [[package]] name = "etherparse" version = "0.21.0" @@ -295,131 +429,964 @@ dependencies = [ "arrayvec", ] +[[package]] +name = "faster-hex" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04839bdf9d8c10f66806fad16b852fc72aab80873aebc3cb69d85b4fa41543ed" +dependencies = [ + "autocfg", + "defmt 0.3.100", + "heapless", + "serde", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + [[package]] name = "fatfs" version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05669f8e7e2d7badc545c513710f0eba09c2fbef683eb859fd79c46c355048e0" dependencies = [ - "bitflags 1.3.2", - "byteorder", - "chrono", - "log", + "bitflags 1.3.2", + "byteorder", + "chrono", + "log", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "filetime" +version = "0.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" +dependencies = [ + "cfg-if", + "libc", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flate2" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "fontdue" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e57e16b3fe8ff4364c0661fdaac543fb38b29ea9bc9c2f45612d90adf931d2b" +dependencies = [ + "hashbrown 0.15.5", + "ttf-parser", +] + +[[package]] +name = "generator" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3b854b0e584ead1a33f18b2fcad7cf7be18b3875c78816b753639aa501513ae" +dependencies = [ + "cc", + "cfg-if", + "libc", + "log", + "rustversion", + "windows-link", + "windows-result", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + +[[package]] +name = "gix" +version = "0.88.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32d6dd6028308ad2f2662207506e396b97f9ec899f74bd537c0775248c774f21" +dependencies = [ + "gix-actor", + "gix-attributes", + "gix-command", + "gix-commitgraph", + "gix-config", + "gix-date", + "gix-diff", + "gix-dir", + "gix-discover", + "gix-error", + "gix-features", + "gix-filter", + "gix-fs", + "gix-glob", + "gix-hash", + "gix-hashtable", + "gix-ignore", + "gix-index", + "gix-lock", + "gix-note", + "gix-object", + "gix-odb", + "gix-pack", + "gix-path", + "gix-pathspec", + "gix-protocol", + "gix-quote", + "gix-ref", + "gix-refspec", + "gix-revision", + "gix-revwalk", + "gix-sec", + "gix-shallow", + "gix-status", + "gix-submodule", + "gix-tempfile", + "gix-trace", + "gix-traverse", + "gix-url", + "gix-utils", + "gix-validate", + "gix-worktree", + "gix-worktree-stream", + "gix-zlib", + "nonempty", + "smallvec", +] + +[[package]] +name = "gix-actor" +version = "0.43.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e046e9a929e8e1f40f9a34f736408c4c079f9cf004429f3e718d270e847be96" +dependencies = [ + "bstr", + "gix-date", + "gix-error", +] + +[[package]] +name = "gix-attributes" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3d57bcf18768dc259868e335fae99a9063174178b78b00348603f0791cd780a" +dependencies = [ + "bstr", + "gix-error", + "gix-features", + "gix-glob", + "gix-path", + "gix-quote", + "gix-trace", + "smallvec", + "unicode-bom", +] + +[[package]] +name = "gix-bitmap" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e9d28a8333a322df5c4fdc7b86a9928965e50794523c0edff57d1fbf2a520cf" +dependencies = [ + "gix-error", +] + +[[package]] +name = "gix-chunk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25303d07ce2b8f98aab48ce4251317d785a3208b7ed65698928e4a2f31889936" +dependencies = [ + "gix-error", +] + +[[package]] +name = "gix-command" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3c2e61f97adc08764060e9becb0c819d8f2ba27896434710e039ae57b1d2a7" +dependencies = [ + "bstr", + "gix-error", + "gix-path", + "gix-quote", + "gix-trace", +] + +[[package]] +name = "gix-commitgraph" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0462cbffd5828447f376559d397d9b29f1979dac3d980b5212b17fa174b174eb" +dependencies = [ + "bstr", + "gix-chunk", + "gix-error", + "gix-hash", + "memmap2", + "nonempty", +] + +[[package]] +name = "gix-config" +version = "0.61.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb2cb454dce2f1895cf9def1df8b0a6bbc579d86d8313bdf60f1ab429d52df12" +dependencies = [ + "bstr", + "gix-config-value", + "gix-error", + "gix-features", + "gix-glob", + "gix-path", + "gix-ref", + "gix-sec", + "gix-utils", + "smallvec", + "unicode-bom", +] + +[[package]] +name = "gix-config-value" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1894e816c774650b5157d965853b50d7e8f7137ea74055a6bac2f2ec051cf997" +dependencies = [ + "bitflags 2.11.0", + "bstr", + "gix-error", + "gix-path", + "libc", +] + +[[package]] +name = "gix-date" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb8beceb16a77fb222923592864b94beb4ad4c2b175a7055c2cac6815ee0370a" +dependencies = [ + "bstr", + "gix-error", + "itoa", + "jiff", +] + +[[package]] +name = "gix-diff" +version = "0.68.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "939aee7bbc9f233de858839e865c82886e0baceb8f1fe792ea1703fa895f09e1" +dependencies = [ + "bstr", + "gix-attributes", + "gix-command", + "gix-error", + "gix-filter", + "gix-fs", + "gix-hash", + "gix-imara-diff", + "gix-index", + "gix-object", + "gix-path", + "gix-pathspec", + "gix-tempfile", + "gix-trace", + "gix-traverse", + "gix-worktree", +] + +[[package]] +name = "gix-dir" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2e41d125c2c6f981aad27299a6233a7ace3413289c1e2994f57e5bab1c34847" +dependencies = [ + "bstr", + "gix-discover", + "gix-error", + "gix-fs", + "gix-ignore", + "gix-index", + "gix-object", + "gix-path", + "gix-pathspec", + "gix-trace", + "gix-utils", + "gix-worktree", +] + +[[package]] +name = "gix-discover" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4476ba055ec5fbb05f59dd98380b596428da060beac5f9dccd7374302ad1fbfb" +dependencies = [ + "bstr", + "dunce", + "gix-error", + "gix-fs", + "gix-path", + "gix-ref", + "gix-sec", +] + +[[package]] +name = "gix-error" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c4a5c4cd326ddfe7f046ad9eed05461c59af6ea98b681648bac73954317addd" +dependencies = [ + "bstr", +] + +[[package]] +name = "gix-features" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb3a430dbc02d6c5e30a9f92083459593495ea22a476e837d248b22d800eed80" +dependencies = [ + "bytes", + "crc32fast", + "crossbeam-channel", + "gix-path", + "gix-trace", + "gix-utils", + "libc", + "once_cell", + "parking_lot", + "prodash", + "walkdir", +] + +[[package]] +name = "gix-filter" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f31a39e2c9b04843709d1f72b2c39c98dc0ee591e2f67d21302dbfbd06d2956" +dependencies = [ + "bstr", + "encoding_rs", + "gix-attributes", + "gix-command", + "gix-error", + "gix-hash", + "gix-object", + "gix-packetline", + "gix-path", + "gix-quote", + "gix-trace", + "gix-utils", + "smallvec", +] + +[[package]] +name = "gix-fs" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0105ab1cef4542d092c27766d2daf1548fcbad7e460dc2208642c120bb19b3ad" +dependencies = [ + "bstr", + "gix-error", + "gix-features", + "gix-path", + "gix-utils", +] + +[[package]] +name = "gix-glob" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bba50803dea16646cafb9a2d67e95b8cdf5b21c7e8c45bcd18f38bafe02fb81f" +dependencies = [ + "bitflags 2.11.0", + "bstr", + "gix-features", + "gix-path", + "gix-utils", +] + +[[package]] +name = "gix-hash" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a22b6febaaee8546eb0c9dd21ecdd93aca269a6cb3897e1250f0f60933c0174" +dependencies = [ + "faster-hex", + "gix-error", + "gix-features", + "sha1dc", +] + +[[package]] +name = "gix-hashtable" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c55bff2f32f6f5a95bdd43e5e5e01b0656250cffbc48eda19db0afa3d73adf2f" +dependencies = [ + "gix-hash", + "hashbrown 0.17.1", + "parking_lot", +] + +[[package]] +name = "gix-ignore" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bfd910e271ecd1a20e4df4009a0d9d844a0c51ee5444aaa3fadd1b1b077930b" +dependencies = [ + "bstr", + "gix-glob", + "gix-path", + "gix-trace", + "unicode-bom", +] + +[[package]] +name = "gix-imara-diff" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "305657ad44356151d73c145f567c25b24fdf2466bfb3e90727e1d5461a4e9748" +dependencies = [ + "bstr", + "hashbrown 0.16.1", +] + +[[package]] +name = "gix-index" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfd5b484b5b4e218a5ac114f0982949f9bd7582ff67986665a0cf85895332433" +dependencies = [ + "bitflags 2.11.0", + "bstr", + "filetime", + "fnv", + "gix-bitmap", + "gix-error", + "gix-features", + "gix-fs", + "gix-hash", + "gix-lock", + "gix-object", + "gix-traverse", + "gix-utils", + "gix-validate", + "hashbrown 0.17.1", + "itoa", + "libc", + "memmap2", + "rustix", + "smallvec", +] + +[[package]] +name = "gix-lock" +version = "25.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd5ddfd0cc476fef744ac544783b96a9ff5d49f8de541e6e9d6d6002aa90720" +dependencies = [ + "gix-error", + "gix-tempfile", + "gix-utils", +] + +[[package]] +name = "gix-macros" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f11f5429ca73cc3758c9f53af56a9fba100a8701c5b4bc30e29b8b0b15288991" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "gix-note" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d6879cc9e5a9a0429e40b05970af6c8dad8734098a3142fec23e32eeb567580" +dependencies = [ + "gix-error", + "gix-hash", + "gix-object", +] + +[[package]] +name = "gix-object" +version = "0.65.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a72909eeeed845721a143ef3dbeb4d6cd5df79c839fe7eefc138cb4f982eba3e" +dependencies = [ + "bstr", + "gix-actor", + "gix-command", + "gix-date", + "gix-error", + "gix-features", + "gix-hash", + "gix-hashtable", + "gix-path", + "gix-tempfile", + "gix-utils", + "gix-validate", + "itoa", + "smallvec", +] + +[[package]] +name = "gix-odb" +version = "0.85.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5794b85ff563a290ea18867fa1f0a5b0b84709f2f34dddbb3614556fbe88c629" +dependencies = [ + "arc-swap", + "gix-error", + "gix-features", + "gix-fs", + "gix-hash", + "gix-hashtable", + "gix-object", + "gix-pack", + "gix-path", + "gix-quote", + "gix-zlib", + "memmap2", + "parking_lot", + "tempfile", +] + +[[package]] +name = "gix-pack" +version = "0.75.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0451a48faef8e0e7a8f6d903c8329c7d18a793b3030907ca841f889dc695d1df" +dependencies = [ + "clru", + "gix-chunk", + "gix-error", + "gix-features", + "gix-hash", + "gix-hashtable", + "gix-object", + "gix-path", + "gix-zlib", + "memmap2", + "smallvec", +] + +[[package]] +name = "gix-packetline" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ba540112a3fcc6388eb2aa4dc4837040478c285a35178cd9a44b07345cd6636" +dependencies = [ + "bstr", + "faster-hex", + "gix-error", + "gix-trace", +] + +[[package]] +name = "gix-path" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd051ac60c5dbce2228daabf25b6a76408ac732d416c678cfa0a4914d13f60b8" +dependencies = [ + "bstr", + "gix-error", + "gix-trace", + "gix-validate", +] + +[[package]] +name = "gix-pathspec" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ee883ab64bad641bf31ff338044edd7aa010605f19e866d98c9164e635c38b1" +dependencies = [ + "bitflags 2.11.0", + "bstr", + "gix-attributes", + "gix-config-value", + "gix-error", + "gix-glob", + "gix-path", +] + +[[package]] +name = "gix-protocol" +version = "0.66.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "331ac489c461790b6b692afb322085c42041c6ff7621c30ac3dbb00b12e05e89" +dependencies = [ + "bstr", + "gix-date", + "gix-error", + "gix-features", + "gix-hash", + "gix-macros", + "gix-ref", + "gix-shallow", + "gix-transport", + "gix-utils", + "nonempty", +] + +[[package]] +name = "gix-quote" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6b040cfa7af50f131bfa34119cceee804c3cc40c766daaba8e0c8149340aea7" +dependencies = [ + "bstr", + "gix-error", + "gix-utils", +] + +[[package]] +name = "gix-ref" +version = "0.68.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c90ceeec67de03b737c30b1b69116ca56fec803edde022f3d25181f0f597083d" +dependencies = [ + "gix-actor", + "gix-error", + "gix-features", + "gix-fs", + "gix-hash", + "gix-lock", + "gix-object", + "gix-path", + "gix-tempfile", + "gix-utils", + "gix-validate", + "memmap2", +] + +[[package]] +name = "gix-refspec" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4caa89a034a055c0578150092d3edb2f1c3f74cca86b883263a28caa26c5812" +dependencies = [ + "bstr", + "gix-error", + "gix-hash", + "gix-validate", + "smallvec", +] + +[[package]] +name = "gix-revision" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8438d38e197fa3ea7bb297302db1701879a64a343f23a3d150a602a609a8751" +dependencies = [ + "bstr", + "gix-commitgraph", + "gix-date", + "gix-error", + "gix-hash", + "gix-object", + "gix-revwalk", + "nonempty", +] + +[[package]] +name = "gix-revwalk" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fa6d913e7cf8c6407d3954f9794b4b148c2113c672c34b63c59ea654a1e060e" +dependencies = [ + "gix-commitgraph", + "gix-date", + "gix-error", + "gix-hash", + "gix-hashtable", + "gix-object", + "smallvec", +] + +[[package]] +name = "gix-sec" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d8eb983a830ba586b61f7005deaf44609e1a4186cbf13a0e8668154ca910461" +dependencies = [ + "bitflags 2.11.0", + "gix-path", + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "gix-shallow" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "119bae1301715df40e8fd136deeff4fe8a3be4cfc001ed5e72bad7b47ebbc090" +dependencies = [ + "bstr", + "gix-error", + "gix-hash", + "gix-lock", + "nonempty", +] + +[[package]] +name = "gix-status" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb7e035b6d9dce932f6b6c8d33d29b31b46e05da544bf810e324bf61aaa5b84" +dependencies = [ + "bstr", + "filetime", + "gix-diff", + "gix-dir", + "gix-error", + "gix-features", + "gix-filter", + "gix-fs", + "gix-hash", + "gix-index", + "gix-object", + "gix-path", + "gix-pathspec", + "gix-worktree", + "hashbrown 0.16.1", + "portable-atomic", + "windows-sys 0.61.2", ] [[package]] -name = "fiat-crypto" -version = "0.2.9" +name = "gix-submodule" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" +checksum = "0b498192e5f89453fbd728263d52088aeec634a390e732f1da6d0a491c552091" +dependencies = [ + "bstr", + "gix-config", + "gix-error", + "gix-path", + "gix-pathspec", + "gix-refspec", + "gix-url", +] [[package]] -name = "filetime" -version = "0.2.29" +name = "gix-tempfile" +version = "25.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" +checksum = "3dd7dd8df3345933caddd6ce67821c9eec17192564f1011cf0bd0b8cb0d07ffb" dependencies = [ - "cfg-if", + "dashmap", + "gix-fs", "libc", + "parking_lot", + "tempfile", + "windows-sys 0.61.2", ] [[package]] -name = "find-msvc-tools" -version = "0.1.9" +name = "gix-trace" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +checksum = "df45b2cf6052d9e52681130c17d67505b023e8933ad974f94a2be0b0d24f636a" [[package]] -name = "flate2" -version = "1.1.10" +name = "gix-transport" +version = "0.60.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" +checksum = "5d2beb7b0387bbf4d7c13763c6e168f9957319aa811f09349dc59601be3b2951" dependencies = [ - "crc32fast", - "miniz_oxide", + "bstr", + "gix-command", + "gix-error", + "gix-features", + "gix-packetline", + "gix-path", + "gix-quote", + "gix-sec", + "gix-url", ] [[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - -[[package]] -name = "foldhash" -version = "0.2.0" +name = "gix-traverse" +version = "0.62.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" +checksum = "dd4864934e4efd92082ecc37107bce0c389b58d82888db634817f9c711efb7ed" +dependencies = [ + "bitflags 2.11.0", + "gix-commitgraph", + "gix-date", + "gix-error", + "gix-hash", + "gix-hashtable", + "gix-object", + "gix-revwalk", + "smallvec", +] [[package]] -name = "fontdue" -version = "0.9.3" +name = "gix-url" +version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e57e16b3fe8ff4364c0661fdaac543fb38b29ea9bc9c2f45612d90adf931d2b" +checksum = "1f30fe2a228d7de5f9d582463d9b724debcb870162d9fbe908cd5d76ccdecf0d" dependencies = [ - "hashbrown 0.15.5", - "ttf-parser", + "bstr", + "gix-error", + "gix-path", + "gix-utils", + "percent-encoding", ] [[package]] -name = "generator" -version = "0.8.9" +name = "gix-utils" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3b854b0e584ead1a33f18b2fcad7cf7be18b3875c78816b753639aa501513ae" +checksum = "70525766f53426ca4cd2ea4d58b0b31488dfedb416b3254e2efdd9dc184cc880" dependencies = [ - "cc", - "cfg-if", - "libc", - "log", - "rustversion", - "windows-link", - "windows-result", + "bstr", + "fastrand", + "getrandom 0.4.2", + "unicode-normalization", ] [[package]] -name = "generic-array" -version = "0.14.7" +name = "gix-validate" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +checksum = "30574f81fc41b2ca3582ece2bdf8696bdfe9c170aa7e8bcafb81199c86b1ceea" dependencies = [ - "typenum", - "version_check", + "bstr", + "gix-error", ] [[package]] -name = "getrandom" -version = "0.2.17" +name = "gix-worktree" +version = "0.57.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +checksum = "04fa19b9b3961aafb3b9e60005b9bcef0100f8b211f46ab5269a6ac896073780" dependencies = [ - "cfg-if", - "libc", - "wasi", + "bstr", + "gix-attributes", + "gix-error", + "gix-fs", + "gix-glob", + "gix-hash", + "gix-ignore", + "gix-index", + "gix-object", + "gix-path", + "gix-validate", ] [[package]] -name = "getrandom" -version = "0.3.4" +name = "gix-worktree-stream" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +checksum = "2021e1609beae0edbeefa76ac641be0571b725afacb77ac5b12f33b436e0d3d9" dependencies = [ - "cfg-if", - "libc", - "r-efi 5.3.0", - "wasip2", + "gix-attributes", + "gix-error", + "gix-features", + "gix-filter", + "gix-fs", + "gix-hash", + "gix-object", + "gix-path", + "gix-traverse", + "parking_lot", ] [[package]] -name = "getrandom" -version = "0.4.2" +name = "gix-zlib" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +checksum = "44a1a9c82ec5b712082566cbef8f5782472a0d3973132911e5a83380a5298fe2" dependencies = [ - "cfg-if", - "libc", - "r-efi 6.0.0", - "wasip2", - "wasip3", + "gix-error", + "zlib-rs", ] [[package]] @@ -434,6 +1401,21 @@ dependencies = [ "uuid", ] +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + [[package]] name = "hashbrown" version = "0.15.5" @@ -451,9 +1433,32 @@ version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "allocator-api2", + "equivalent", "foldhash 0.2.0", ] +[[package]] +name = "heapless" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" +dependencies = [ + "hash32", + "stable_deref_trait", +] + [[package]] name = "heck" version = "0.5.0" @@ -538,6 +1543,60 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +[[package]] +name = "jiff" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ab1baf72f08796de0260609515130699b890ac25f30e610ad894bc5856cafdb" +dependencies = [ + "defmt 1.1.1", + "jiff-core", + "jiff-static", + "jiff-tzdb-platform", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", + "windows-link", +] + +[[package]] +name = "jiff-core" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e52fe76043ccecc9005d2305ebaadf7d7fc0cc89ca6baa10a94d6bc68c7128c" +dependencies = [ + "defmt 1.1.1", + "log", +] + +[[package]] +name = "jiff-static" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "378268a1116ad67ae6228701118ac9f491d78fda38a40a1f1a9e1348de6f7212" +dependencies = [ + "jiff-core", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "jiff-tzdb" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "142bd39932ad231f10513df9ab62661fead8719872150b7ad02a2df79f4e141e" + +[[package]] +name = "jiff-tzdb-platform" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" +dependencies = [ + "jiff-tzdb", +] + [[package]] name = "js-sys" version = "0.3.91" @@ -611,9 +1670,24 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.183" +version = "0.2.190" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "lock_api" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] [[package]] name = "log" @@ -648,6 +1722,15 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +[[package]] +name = "memmap2" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0" +dependencies = [ + "libc", +] + [[package]] name = "miniz_oxide" version = "0.9.1" @@ -668,6 +1751,18 @@ dependencies = [ "pxfm", ] +[[package]] +name = "multiversion_no_op" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743fb55ba31b18fb1ecef6bdc9aa2743314978ac084044301a7eee33fb99a20d" + +[[package]] +name = "nonempty" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9737e026353e5cd0736f98eddae28665118eb6f6600902a7f50db585621fecb6" + [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -700,9 +1795,32 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] [[package]] name = "pcap-file" @@ -727,6 +1845,21 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "portable-atomic" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" + +[[package]] +name = "portable-atomic-util" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715" +dependencies = [ + "portable-atomic", +] + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -755,6 +1888,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "prodash" +version = "31.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "962200e2d7d551451297d9fdce85138374019ada198e30ea9ede38034e27604c" +dependencies = [ + "parking_lot", +] + [[package]] name = "pxfm" version = "0.1.28" @@ -811,6 +1953,15 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.11.0", +] + [[package]] name = "regex-automata" version = "0.4.18" @@ -863,6 +2014,19 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d" +dependencies = [ + "bitflags 2.11.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + [[package]] name = "rustls" version = "0.23.45" @@ -904,12 +2068,27 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "scoped-tls" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "semver" version = "1.0.27" @@ -968,6 +2147,12 @@ dependencies = [ "serde", ] +[[package]] +name = "sha1dc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fdf1713fb0547b120571e5f7b37dc2650dc03d2abf46a18928f54372000947f" + [[package]] name = "sha2" version = "0.10.9" @@ -1006,12 +2191,24 @@ version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + [[package]] name = "subtle" version = "2.6.1" @@ -1050,6 +2247,19 @@ dependencies = [ "libc", ] +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -1099,6 +2309,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "tinyvec" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" + [[package]] name = "toml" version = "0.8.23" @@ -1187,6 +2403,7 @@ dependencies = [ "flate2", "fontdue", "getrandom 0.3.4", + "gix", "gpt", "image", "kernel", @@ -1527,12 +2744,27 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +[[package]] +name = "unicode-bom" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eec5d1121208364f6793f7d2e222bf75a915c19557537745b195b253dd64217" + [[package]] name = "unicode-ident" version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + [[package]] name = "unicode-xid" version = "0.2.6" @@ -1602,6 +2834,16 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -1714,6 +2956,15 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "windows-core" version = "0.62.2" @@ -1978,6 +3229,12 @@ version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" +[[package]] +name = "zlib-rs" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112" + [[package]] name = "zmij" version = "1.0.21" diff --git a/Cargo.toml b/Cargo.toml index 2563a890a00..d9eff86a337 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -178,6 +178,9 @@ flate2 = { version = "1", default-features = false, features = ["rust_backend"] # takes (`issues/design-debt/the-internet-clients-work-unchanged.md`), whose C # and assembly the host's `cc` compiles. ureq = { version = "3", default-features = false, features = ["rustls"] } +# The commit an image is built from, its time and whether the tree is that +# commit's, where `git` is a host's C tool (`src/build.rs`'s `release`). +gix = { version = "0.88", default-features = false, features = ["status", "sha1"] } [dev-dependencies] # `USER_TOP`, so the harness judges a held `rsp` against the bound the kernel diff --git a/kernel/src/main.rs b/kernel/src/main.rs index 23a6bd14991..3c7e3104d48 100644 --- a/kernel/src/main.rs +++ b/kernel/src/main.rs @@ -326,9 +326,12 @@ pub(crate) unsafe extern "C" fn kernel_main(kernel_args: &KernelArgs) -> ! { "boot: cmdline {:#x}+{}", kernel_args.cmdline_addr, kernel_args.cmdline_len ); - match rootfs::named() { - Some(root) => log!("boot: root={root}"), - None => log!("boot: root= names no filesystem this kernel can parse"), + // ROOT's name hashes every file it carries, `/system/etc/os-release` among + // them: the build, named before anything is mounted. + match toyos_abi::boot::root_uuid(cmdline).map(bcachefs::FsUuid::parse) { + Some(Some(root)) => log!("boot: root={root}"), + Some(None) => log!("boot: root= names no filesystem this kernel can parse"), + None => log!("boot: the cmdline carries no root="), } // Before `mm::init`, which may hand the parameter's memory out. This record // is how a slot that died or was refused reaches the next boot's `/log`. diff --git a/kernel/src/rootfs.rs b/kernel/src/rootfs.rs index 3b23c74c98f..926cbd5e04e 100644 --- a/kernel/src/rootfs.rs +++ b/kernel/src/rootfs.rs @@ -123,13 +123,6 @@ pub fn init(cmdline: &str, args: &KernelArgs, map: &[MemoryMapEntry]) -> Region region } -/// The filesystem `root=` names, if this kernel can parse it: a hash of every -/// file ROOT carries, `/system/etc/os-release` among them, so it names the -/// build before anything is mounted. -pub fn named() -> Option { - BOOT.lock().named -} - /// Mount the filesystem the boot parameter named, off the image the loader /// handed. Panics when there is no image, or it is not that filesystem. pub fn mount() -> Mounted { diff --git a/src/build.rs b/src/build.rs index 04775dd1185..6c6ec9c3793 100644 --- a/src/build.rs +++ b/src/build.rs @@ -742,23 +742,44 @@ fn render_manifest(config: &SystemConfig) -> Vec { } /// Which build `root`'s tree makes for `arch` against the sysroot whose key -/// for it is `toolchain`, as `/system/etc/os-release` records it. -/// -/// `--ignore-submodules=all`: the fork's state is the toolchain key's, and -/// recursing into its tree costs a second of every image build. +/// for it is `toolchain`, as `/system/etc/os-release` records it, read with +/// gitoxide (`issues/build/the-build-runs-host-tools-outside-rust-and-qemu.md`, +/// row 21). Untracked files are dirty whatever `status.showUntrackedFiles` +/// says; submodules are not read, because the fork's state is the toolchain +/// key's. fn release(root: &Path, toolchain: &crate::keystore::Key, arch: Arch) -> toyos_osrelease::Release { - use crate::sysroot::git_out; - let head = git_out(root, &["rev-parse", "HEAD"]); - let commit = toyos_osrelease::Hex::parse(head.trim()) - .unwrap_or_else(|| panic!("`git rev-parse HEAD` in {} said {head:?}, which is no commit", root.display())); - let object = git_out(root, &["cat-file", "commit", commit.as_str()]); - let committed = committer_time(&object) - .unwrap_or_else(|| panic!("commit {} names no committer time:\n{object}", commit.as_str())); - let status = git_out(root, &["status", "--porcelain", "--ignore-submodules=all"]); - let tree = if status.is_empty() { toyos_osrelease::Tree::Clean } else { toyos_osrelease::Tree::Dirty }; + let repo = gix::open(root).unwrap_or_else(|e| panic!("{} is no git checkout: {e}", root.display())); + let head = repo.head_commit().unwrap_or_else(|e| panic!("{}'s HEAD names no commit: {e}", root.display())); + let commit = toyos_osrelease::Hex::parse(&head.id.to_string()).expect("a SHA-1 commit is forty hex digits"); + let time = head.time().unwrap_or_else(|e| panic!("commit {} names no committer time: {e}", head.id)); + let committed = u64::try_from(time.seconds) + .unwrap_or_else(|_| panic!("commit {} was committed before 1970: {}", head.id, time.seconds)); + // Set whole: the platform `status` makes has no walk at all for a + // checkout configured to show no untracked files. + let walk = repo + .dirwalk_options() + .unwrap_or_else(|e| panic!("the status of {}: {e}", root.display())) + .emit_untracked(gix::dir::walk::EmissionMode::CollapseDirectory); + let changes = repo + .status(gix::progress::Discard) + .and_then(|status| { + status + .index_worktree_options_mut(|options| options.dirwalk_options = Some(walk)) + .index_worktree_submodules(None) + .into_iter(None) + }) + .unwrap_or_else(|e| panic!("the status of {}: {e}", root.display())); + // An index entry whose stat alone moved, and an ignored file the walk + // passed, summarise to nothing. + let dirty = changes.map(|item| item.unwrap_or_else(|e| panic!("the status of {}: {e}", root.display()))).any( + |item| match item { + gix::status::Item::IndexWorktree(change) => change.summary().is_some(), + gix::status::Item::TreeIndex(_) => true, + }, + ); toyos_osrelease::Release { commit, - tree, + tree: if dirty { toyos_osrelease::Tree::Dirty } else { toyos_osrelease::Tree::Clean }, toolchain: toyos_osrelease::Hex::parse(toolchain.as_str()).expect("a key is sixteen hex digits"), arch: match arch { Arch::X86_64 => toyos_osrelease::Arch::X86_64, @@ -768,14 +789,6 @@ fn release(root: &Path, toolchain: &crate::keystore::Key, arch: Arch) -> toyos_o } } -/// The Unix seconds of a commit object's `committer` header, which ends -/// ` `. -fn committer_time(object: &str) -> Option { - let headers = object.split("\n\n").next()?; - let committer = headers.lines().find_map(|line| line.strip_prefix("committer "))?; - committer.rsplit(' ').nth(1)?.parse().ok() -} - fn build_and_assemble( root: &Path, config: &SystemConfig, @@ -784,11 +797,13 @@ fn build_and_assemble( quiet: bool, arch: Arch, ) -> Vec { - let mut root_files: Vec<(String, Vec)> = Vec::new(); + // Before anything is built, so a file edited during the build is not + // credited to the state it was read in. + let release = release(root, env.sysroot.identity.of_target(arch.userland()), arch); + let mut root_files: Vec<(String, Vec)> = + vec![(toyos_osrelease::PATH.to_string(), release.to_string().into_bytes())]; build_programs(root, config, env, quiet, arch, &mut root_files); root_files.push((toyos_manifest::PATH.to_string(), render_manifest(config))); - let release = release(root, env.sysroot.identity.of_target(arch.userland()), arch); - root_files.push((toyos_osrelease::PATH.to_string(), release.to_string().into_bytes())); if config.hosted_rustc { assert!( @@ -2343,9 +2358,10 @@ mod tests { /// **The release a tree records is its commit, at that commit's own time, /// and dirty from the first file that is not that commit's**: an - /// untracked file counts, and so does an edit to a tracked one. The time - /// is the committer's, in a zone not UTC's, and never the author's or the - /// host clock's. + /// untracked file counts, and so does an edit to a tracked one, staged or + /// not, whatever the checkout's own status shows. The time is the + /// committer's, in a zone not UTC's, and never the author's or the host + /// clock's. #[test] fn a_tree_records_its_commit_its_commit_time_and_whether_it_is_dirty() { let (_dir, _origin, work) = crate::gitfixture::repo("release"); @@ -2380,13 +2396,15 @@ mod tests { fs::create_dir(work.join("target")).unwrap(); fs::write(work.join("target/out"), "x").unwrap(); assert_eq!(release(&work, &key, Arch::Aarch64).tree, toyos_osrelease::Tree::Clean); - } - - #[test] - fn a_commit_without_a_committer_time_has_none() { - assert_eq!(committer_time("tree t\ncommitter A 17 +0200\n\nmsg\n"), Some(17)); - assert_eq!(committer_time("tree t\nauthor A 17 +0200\n\ncommitter A 18 +0200\n"), None); - assert_eq!(committer_time("tree t\ncommitter A +0200\n\n"), None); + // A checkout that hides untracked files from its own status. + crate::gitfixture::sh(&work, &["config", "status.showUntrackedFiles", "no"]); + fs::write(work.join("untracked"), "x").unwrap(); + assert_eq!(release(&work, &key, Arch::Aarch64).tree, toyos_osrelease::Tree::Dirty); + fs::remove_file(work.join("untracked")).unwrap(); + // A change staged, with the files as staged: only HEAD's tree differs. + fs::write(work.join("f"), "staged\n").unwrap(); + crate::gitfixture::sh(&work, &["add", "f"]); + assert_eq!(release(&work, &key, Arch::Aarch64).tree, toyos_osrelease::Tree::Dirty); } /// `console` is reached by `console/system.toml` alone and the supervisor by no diff --git a/src/image.rs b/src/image.rs index 68e82bda228..3a0ae3c00ac 100644 --- a/src/image.rs +++ b/src/image.rs @@ -309,6 +309,20 @@ fn cmdline_of(path: &Path) -> Result { String::from_utf8(bytes).map_err(|e| format!("the boot parameter on {} is not text: {e}", path.display())) } +/// The name the ROOT of the image's marked slot carries in its superblock, +/// read back off the image at `path`. +pub fn root_uuid_on(path: &Path) -> Result { + let mut file = std::fs::File::open(path).map_err(|e| format!("opening {}: {e}", path.display()))?; + let table = slot_table_of(&mut file).map_err(|why| format!("{}: {why}", path.display()))?; + let slot = table.slot(table.marked).expect("a table marks a slot it carries"); + let (start, _) = partition_extent(&mut file, slot.root).map_err(|why| format!("{}: {why}", path.display()))?; + let mut block = [0u8; 4096]; + file.seek(SeekFrom::Start(start)) + .and_then(|_| file.read_exact(&mut block)) + .map_err(|e| format!("reading {}'s ROOT at byte {start}: {e}", path.display()))?; + Ok(root_uuid_of(&block)) +} + /// The slot table on the disk image `file`, as the loader reads it. pub fn slot_table_of(file: &mut std::fs::File) -> Result { table_on(file).map(|(table, _, _)| table) diff --git a/src/release.rs b/src/release.rs index 2ba36b343ec..828a56f1354 100644 --- a/src/release.rs +++ b/src/release.rs @@ -68,9 +68,16 @@ impl Layer { } } +const TAG: &str = "toolchain-linux-x86_64-"; + /// The release of the sysroot `key` names. fn tag(key: &Key) -> String { - format!("toolchain-linux-x86_64-{key}") + format!("{TAG}{key}") +} + +/// The sysroot key a [`manifest`]'s `text` names, read back off its first line. +pub(crate) fn named_key(text: &str) -> Option { + text.lines().next()?.strip_prefix("toolchain ")?.strip_prefix(TAG).and_then(Key::parse) } pub(crate) fn sha256_hex(bytes: &[u8]) -> String { @@ -863,6 +870,12 @@ mod tests { assert_eq!(fs::read_to_string(crate::toolchain::witness_path(&rust_dir)).unwrap(), witness); let toolchain = fs::read_to_string(crate::toolchain::manifest_path(&rust_dir)).unwrap(); assert_eq!(toolchain, manifest("toolchain-linux-x86_64-0123456789abcdef")); + // What an image built against it records as its toolchain: the key its + // release is tagged with, as a build that made its own sysroot records. + let installed = crate::sysroot::Sysroot::installed(crate::toolchain::stage2(&rust_dir), &toolchain); + for arch in [crate::arch::Arch::X86_64, crate::arch::Arch::Aarch64] { + assert_eq!(installed.identity.of_target(arch.userland()).as_str(), "0123456789abcdef"); + } fs::create_dir_all(rust_dir.join("build/sysroots/not-a-key")).unwrap(); assert!(lay_out(&root).unwrap_err().contains("named by no key")); } diff --git a/src/sysroot.rs b/src/sysroot.rs index 71ef1880eea..436dca9ad3e 100644 --- a/src/sysroot.rs +++ b/src/sysroot.rs @@ -148,10 +148,12 @@ pub struct Sysroot { impl Sysroot { /// A checkout whose toolchain arrived as an artifact has one sysroot, the /// artifact's, which `toolchain::check_installed_toolchain` has matched to - /// these sources; `release`, the `TOOLCHAIN` it was published with, is the - /// identity of all of it. + /// these sources; the sysroot key named by `release`, the `TOOLCHAIN` it + /// was published with, is the identity of all of it: the key a build of + /// these sources computes where it builds its own. pub(crate) fn installed(stage2: PathBuf, release: &str) -> Self { - let id = Key::of(release.as_bytes()); + let id = crate::release::named_key(release) + .unwrap_or_else(|| panic!("the installed TOOLCHAIN names no sysroot key:\n{release}")); Self { dir: stage2, primary_compiler: true, identity: Identity::new(id.clone(), &id, &id), _using: None } } diff --git a/tests/common/qemu.rs b/tests/common/qemu.rs index dbaf3e14efd..092283aaf06 100644 --- a/tests/common/qemu.rs +++ b/tests/common/qemu.rs @@ -2252,12 +2252,17 @@ fn spawn_and_wait_ready(mut qemu: Command, options: &BootOptions, files: Files) wait_for_ready(&mut child, &rx, options, &uart_log) }; // The ready marker is test-runner's, which the supervisor starts after its - // first line, so a boot that reached it has said its build. + // first line, so a boot that reached it has said its build, and the kernel + // named the ROOT that carries it before that. if options.ready_marker == DEFAULT_READY && !options.mute { + let root = toyos_build::image::root_uuid_on(&boot_image).unwrap_or_else(|why| panic!("[qemu] {why}")); + let named = format!("boot: root={root}"); let said = format!("{}{} ", toyos_osrelease::SAID, built_commit()); - if !boot_log.lines().any(|line| line.contains(&said)) { - let _ = child.kill(); - panic!("[qemu] the boot never said `{said}`, the commit this run built\nconsole:\n{boot_log}"); + for line in [&named, &said] { + if !boot_log.lines().any(|l| l.contains(line.as_str())) { + let _ = child.kill(); + panic!("[qemu] the boot never said `{line}`, of the image this run built\nconsole:\n{boot_log}"); + } } } @@ -2278,18 +2283,13 @@ fn spawn_and_wait_ready(mut qemu: Command, options: &BootOptions, files: Files) } } -/// The commit this run's images are built from, read by `git` itself rather -/// than by the build that records it. +/// The commit this run's images are built from, read off the checkout by the +/// harness rather than taken from the build that records it. fn built_commit() -> &'static str { static COMMIT: std::sync::OnceLock = std::sync::OnceLock::new(); COMMIT.get_or_init(|| { - let out = Command::new("git") - .args(["rev-parse", "HEAD"]) - .current_dir(super::compile::repo_root()) - .output() - .expect("run git"); - assert!(out.status.success(), "git rev-parse HEAD: {}", String::from_utf8_lossy(&out.stderr)); - String::from_utf8(out.stdout).expect("a commit is text").trim().to_string() + let repo = gix::open(super::compile::repo_root()).expect("the checkout is a git repository"); + repo.head_id().expect("HEAD names a commit").to_string() }) } diff --git a/tests/testcases/LICENSE b/tests/testcases/LICENSE index b4eab569b22..94e8eee4c7b 100644 --- a/tests/testcases/LICENSE +++ b/tests/testcases/LICENSE @@ -21,9 +21,9 @@ against tinycc upstream at 64552b3faa39ee7948a9ea21bfcc11045b90c70d (repo.or.cz/tinycc.git, 2026-08-05), allowing for the `-` → `_` renames this project made to some filenames. - tinycc/ 326 files: 239 byte-identical to tinycc's `tests/tests2`, + tinycc/ 328 files: 239 byte-identical to tinycc's `tests/tests2`, 17 tinycc files this project modified, - 70 not upstream at all — 69 cases written here, plus + 72 not upstream at all — 71 cases written here, plus `fred.txt`, which is output `40_stdio.c` writes when it runs and which was committed by accident. diff --git a/tests/testcases/tinycc/206_libc_refusals.c b/tests/testcases/tinycc/206_libc_refusals.c index 8b6950cc358..c0fc5e2cbca 100644 --- a/tests/testcases/tinycc/206_libc_refusals.c +++ b/tests/testcases/tinycc/206_libc_refusals.c @@ -1,37 +1,21 @@ /* What libc refuses: each call answers failure in its own POSIX form, errno says why, and nothing is done: ENOSYS where ToyOS lacks the function, and the errno POSIX names for a lock or a mapping it cannot take, or memory it - cannot give. uname, which answers, is read back beside gethostname. */ + cannot give. */ #include #include #include #include #include #include -#include #include #include #include #include -#include #include #define FILE_PATH "/tmp/206_libc_refusals" -#if defined(__x86_64__) -#define MACHINE "x86_64" -#elif defined(__aarch64__) -#define MACHINE "aarch64" -#endif - -/* Whether `s` is `n` lowercase hex digits and then `rest`. */ -static int hex_then(const char *s, int n, const char *rest) { - for (int i = 0; i < n; i++) - if (!((s[i] >= '0' && s[i] <= '9') || (s[i] >= 'a' && s[i] <= 'f'))) - return 0; - return strcmp(s + n, rest) == 0; -} - static const char *errno_name(int e) { switch (e) { case 0: return "no errno"; @@ -54,7 +38,6 @@ int main(void) { char *argv[] = { "shell", NULL }; char *envp[] = { NULL }; char buf[256]; - struct utsname uts; struct statvfs vfs; struct rlimit limit = { 0, 0 }; struct passwd pw, *found = &pw; @@ -128,19 +111,6 @@ int main(void) { said("fcntl F_SETFL", fcntl(fd, F_SETFL, O_NONBLOCK)); said("fcntl 12345", fcntl(fd, 12345)); - /* uname answers the build ROOT carries, and no node name: the release is - the commit's first twelve digits, and -dirty after them for a tree - that was not that commit's; the version is the whole commit; the - machine is the one this case was compiled for. */ - said("uname", uname(&uts)); - printf("uname sysname: %s; nodename: %s\n", uts.sysname, uts.nodename[0] ? uts.nodename : "empty"); - printf("uname version: %s\n", hex_then(uts.version, 40, "") ? "a commit" : uts.version); - printf("uname release: %s\n", - (hex_then(uts.release, 12, "") || hex_then(uts.release, 12, "-dirty")) && strncmp(uts.release, uts.version, 12) == 0 - ? "the version's first twelve digits" - : uts.release); - printf("uname machine: %s\n", strcmp(uts.machine, MACHINE) == 0 ? "the one this case was compiled for" : uts.machine); - /* The _r lookups answer their error, and null for the entry. */ int answer = getpwnam_r("root", &pw, buf, sizeof buf, &found); printf("getpwnam_r: %s, entry %s\n", errno_name(answer), found ? "set" : "null"); diff --git a/tests/testcases/tinycc/206_libc_refusals.expect b/tests/testcases/tinycc/206_libc_refusals.expect index 3879772d31f..b54c90790ec 100644 --- a/tests/testcases/tinycc/206_libc_refusals.expect +++ b/tests/testcases/tinycc/206_libc_refusals.expect @@ -30,11 +30,6 @@ fcntl F_DUPFD_CLOEXEC: -1, ENOSYS fcntl F_GETFL: -1, ENOSYS fcntl F_SETFL: -1, ENOSYS fcntl 12345: -1, EINVAL -uname: 0, no errno -uname sysname: ToyOS; nodename: empty -uname version: a commit -uname release: the version's first twelve digits -uname machine: the one this case was compiled for getpwnam_r: ENOSYS, entry null getpwuid_r: ENOSYS, entry null mmap a file: -1, ENODEV diff --git a/tests/testcases/tinycc/209_libc_uname.c b/tests/testcases/tinycc/209_libc_uname.c new file mode 100644 index 00000000000..9d8fda4b2f8 --- /dev/null +++ b/tests/testcases/tinycc/209_libc_uname.c @@ -0,0 +1,39 @@ +/* What uname answers: the build ROOT carries, and no node name. The release + is the commit's first twelve digits, with -dirty after them for a tree + that was not that commit's; the version is the whole commit; the machine + is the one this case was compiled for. */ +#include +#include +#include +#include + +#if defined(__x86_64__) +#define MACHINE "x86_64" +#elif defined(__aarch64__) +#define MACHINE "aarch64" +#endif + +/* Whether `s` is `n` lowercase hex digits and then `rest`. */ +static int hex_then(const char *s, int n, const char *rest) { + for (int i = 0; i < n; i++) + if (!((s[i] >= '0' && s[i] <= '9') || (s[i] >= 'a' && s[i] <= 'f'))) + return 0; + return strcmp(s + n, rest) == 0; +} + +int main(void) { + struct utsname uts; + errno = 0; + int answer = uname(&uts); + printf("uname: %d, %s\n", answer, errno == 0 ? "no errno" : "an errno"); + if (answer != 0) + return 1; + printf("sysname: %s; nodename: %s\n", uts.sysname, uts.nodename[0] ? uts.nodename : "empty"); + printf("version: %s\n", hex_then(uts.version, 40, "") ? "a commit" : uts.version); + printf("release: %s\n", + (hex_then(uts.release, 12, "") || hex_then(uts.release, 12, "-dirty")) && strncmp(uts.release, uts.version, 12) == 0 + ? "the version's first twelve digits" + : uts.release); + printf("machine: %s\n", strcmp(uts.machine, MACHINE) == 0 ? "the one this case was compiled for" : uts.machine); + return 0; +} diff --git a/tests/testcases/tinycc/209_libc_uname.expect b/tests/testcases/tinycc/209_libc_uname.expect new file mode 100644 index 00000000000..7c57645b2cf --- /dev/null +++ b/tests/testcases/tinycc/209_libc_uname.expect @@ -0,0 +1,5 @@ +uname: 0, no errno +sysname: ToyOS; nodename: empty +version: a commit +release: the version's first twelve digits +machine: the one this case was compiled for diff --git a/toyos-osrelease/src/lib.rs b/toyos-osrelease/src/lib.rs index 02b0a954496..b0197831ad0 100644 --- a/toyos-osrelease/src/lib.rs +++ b/toyos-osrelease/src/lib.rs @@ -30,12 +30,18 @@ use core::fmt::{self, Write}; +macro_rules! path { + () => { + "etc/os-release" + }; +} + /// Where ROOT carries it, without a leading slash — that volume's own /// spelling. [`GUEST_PATH`] is what a process opens. -pub const PATH: &str = "etc/os-release"; +pub const PATH: &str = path!(); /// The path a process opens. -pub const GUEST_PATH: &str = "/system/etc/os-release"; +pub const GUEST_PATH: &str = concat!("/system/", path!()); /// The operating system's name, `NAME` and `uname`'s `sysname`. pub const NAME: &str = "ToyOS"; @@ -118,6 +124,25 @@ impl Release { pub fn short(&self) -> &str { &self.commit.as_str()[..12] } + + /// `uname`'s `release`: [`Release::short`], and `-dirty` after it for a + /// tree that was not that commit's. + pub fn uname_release(&self) -> UnameRelease<'_> { + UnameRelease(self) + } +} + +/// [`Release::uname_release`]. +pub struct UnameRelease<'a>(&'a Release); + +impl fmt::Display for UnameRelease<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.0.short())?; + match self.0.tree { + Tree::Clean => Ok(()), + Tree::Dirty => f.write_str("-dirty"), + } + } } /// The file's bytes. @@ -266,8 +291,9 @@ mod tests { } #[test] - fn the_guest_path_is_where_root_carries_it() { - assert_eq!(GUEST_PATH.strip_prefix("/system/"), Some(PATH)); + fn uname_names_a_dirty_tree() { + assert_eq!(release(Tree::Clean, Arch::X86_64).uname_release().to_string(), "1a2b3c4d5e6f"); + assert_eq!(release(Tree::Dirty, Arch::X86_64).uname_release().to_string(), "1a2b3c4d5e6f-dirty"); } /// The bytes, whole: what `os-release(5)` readers other than ours see. diff --git a/userland/libc/src/uname.rs b/userland/libc/src/uname.rs index 4c0a5b07d4a..c214a62376a 100644 --- a/userland/libc/src/uname.rs +++ b/userland/libc/src/uname.rs @@ -1,7 +1,9 @@ //! `uname`, answered from the build ROOT carries (`toyos-osrelease`): `ToyOS`, -//! no node name — no host name is published to a process — the commit's -//! twelve digits as the release, `-dirty` after them for a tree that was not -//! that commit's, the whole commit as the version, and the machine. +//! no node name — no host name is published to a process — the release +//! `toyos_osrelease::Release::uname_release` names, the whole commit as the +//! version, and the machine. + +use core::fmt::{self, Write}; use toyos_abi::syscall::{self, OpenFlags}; @@ -31,28 +33,41 @@ pub unsafe extern "C" fn uname(buf: *mut Utsname) -> i32 { return -1; }; let out = &mut *buf; - put(&mut out.sysname, &[toyos_osrelease::NAME]); - put(&mut out.nodename, &[]); - let dirty = match release.tree { - toyos_osrelease::Tree::Clean => "", - toyos_osrelease::Tree::Dirty => "-dirty", - }; - put(&mut out.release, &[release.short(), dirty]); - put(&mut out.version, &[release.commit.as_str()]); - put(&mut out.machine, &[release.arch.machine()]); + put(&mut out.sysname, toyos_osrelease::NAME); + put(&mut out.nodename, ""); + put(&mut out.release, release.uname_release()); + put(&mut out.version, release.commit.as_str()); + put(&mut out.machine, release.arch.machine()); 0 } -/// `parts`, one after another and NUL-terminated, into `field`. -fn put(field: &mut [u8; 65], parts: &[&str]) { - let mut at = 0; - for part in parts { - field[at..at + part.len()].copy_from_slice(part.as_bytes()); - at += part.len(); - } +/// `value`, NUL-terminated, into `field`; every value above is at most forty +/// bytes. +fn put(field: &mut [u8; 65], value: impl fmt::Display) { + let mut at = Field { field, at: 0 }; + write!(at, "{value}").expect("a uname value fits its field"); + let Field { field, at } = at; field[at..].fill(0); } +/// A field being written, the last byte kept for its NUL. +struct Field<'a> { + field: &'a mut [u8; 65], + at: usize, +} + +impl Write for Field<'_> { + fn write_str(&mut self, s: &str) -> fmt::Result { + let end = self.at + s.len(); + if end >= self.field.len() { + return Err(fmt::Error); + } + self.field[self.at..end].copy_from_slice(s.as_bytes()); + self.at = end; + Ok(()) + } +} + /// As much of the file at `path` as `into` holds. fn read(path: &str, into: &mut [u8]) -> Result { let handle = syscall::open(path.as_bytes(), OpenFlags::READ)?; diff --git a/userland/supervisor/Cargo.toml b/userland/supervisor/Cargo.toml index 3b9ddb4d09c..78c241eab8d 100644 --- a/userland/supervisor/Cargo.toml +++ b/userland/supervisor/Cargo.toml @@ -8,7 +8,6 @@ license = "MIT OR Apache-2.0" toyos = { path = "../../toyos" } toyos-abi = { path = "../../toyos-abi" } toyos-manifest = { path = "../../toyos-manifest" } -# Which build this is, said first thing. toyos-osrelease = { path = "../../toyos-osrelease" } toyos-wallclock = { path = "../../toyos-wallclock" } toyos-logstream = { path = "../../toyos-logstream" } From a8ef7bc0b5aa4daaa623e9f6b72950e95182807e Mon Sep 17 00:00:00 2001 From: japabu Date: Sun, 4 Oct 2026 13:44:40 +0200 Subject: [PATCH 4/5] The release test reads its expected commit off the ref git wrote Its oracle stays independent of gitoxide without running `git rev-parse`, a read row 21 of the host-tools issue refuses: the fixture's own `.git/refs/heads/wt`, which `git commit` wrote. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01WcU2Dsw6mDYtwYfzVHPzM8 --- src/build.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/build.rs b/src/build.rs index 6c6ec9c3793..7e412bf5b7c 100644 --- a/src/build.rs +++ b/src/build.rs @@ -2375,7 +2375,8 @@ mod tests { .status() .unwrap(); assert!(committed.success()); - let head = crate::sysroot::git_out(&work, &["rev-parse", "HEAD"]); + // The ref as `git` wrote it, for an oracle that is not gitoxide. + let head = fs::read_to_string(work.join(".git/refs/heads/wt")).unwrap(); let key = crate::keystore::Key::parse("0123456789abcdef").unwrap(); let clean = release(&work, &key, Arch::Aarch64); From 68573c408dd86bf4e47b423423c7d81c5f460796 Mon Sep 17 00:00:00 2001 From: japabu Date: Sun, 4 Oct 2026 14:02:11 +0200 Subject: [PATCH 5/5] The boot's build line is checked against the release its own image carries The harness read HEAD once per test process and compared every boot's `supervisor: build` line with it, while each boot's image reads HEAD again when it is built: a commit during a guest run reddened every later boot. The harness now reads `etc/os-release` and ROOT's name off the image the boot was handed, through `bcachefs`'s mount path, and checks the whole line the supervisor says against that file's fields. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01WcU2Dsw6mDYtwYfzVHPzM8 --- src/image.rs | 20 +++++++++++++------- tests/common/qemu.rs | 28 +++++++++++++++------------- 2 files changed, 28 insertions(+), 20 deletions(-) diff --git a/src/image.rs b/src/image.rs index 3a0ae3c00ac..5ef1abdbfaa 100644 --- a/src/image.rs +++ b/src/image.rs @@ -309,18 +309,24 @@ fn cmdline_of(path: &Path) -> Result { String::from_utf8(bytes).map_err(|e| format!("the boot parameter on {} is not text: {e}", path.display())) } -/// The name the ROOT of the image's marked slot carries in its superblock, -/// read back off the image at `path`. -pub fn root_uuid_on(path: &Path) -> Result { +/// The ROOT of the image's marked slot, read back off the image at `path`: +/// its name, and the file `name` it carries, read with the driver the kernel +/// mounts it with. +pub fn root_file_on(path: &Path, name: &str) -> Result<(FsUuid, Vec), String> { let mut file = std::fs::File::open(path).map_err(|e| format!("opening {}: {e}", path.display()))?; let table = slot_table_of(&mut file).map_err(|why| format!("{}: {why}", path.display()))?; let slot = table.slot(table.marked).expect("a table marks a slot it carries"); - let (start, _) = partition_extent(&mut file, slot.root).map_err(|why| format!("{}: {why}", path.display()))?; - let mut block = [0u8; 4096]; + let (start, len) = partition_extent(&mut file, slot.root).map_err(|why| format!("{}: {why}", path.display()))?; + let len = usize::try_from(len).map_err(|_| format!("a {len}-byte ROOT on {}", path.display()))?; + let mut volume = vec![0u8; len]; file.seek(SeekFrom::Start(start)) - .and_then(|_| file.read_exact(&mut block)) + .and_then(|_| file.read_exact(&mut volume)) .map_err(|e| format!("reading {}'s ROOT at byte {start}: {e}", path.display()))?; - Ok(root_uuid_of(&block)) + let uuid = root_uuid_of(&volume); + let fs = bcachefs::Mounted::<_, bcachefs::ReadOnly>::open(VecBlockIO::from_vec(volume)) + .map_err(|e| format!("{}'s ROOT does not mount: {e:?}", path.display()))?; + let bytes = fs.read_file(name).map_err(|e| format!("{}'s ROOT: reading {name}: {e:?}", path.display()))?; + Ok((uuid, bytes)) } /// The slot table on the disk image `file`, as the loader reads it. diff --git a/tests/common/qemu.rs b/tests/common/qemu.rs index 092283aaf06..e45001e7a18 100644 --- a/tests/common/qemu.rs +++ b/tests/common/qemu.rs @@ -2253,11 +2253,23 @@ fn spawn_and_wait_ready(mut qemu: Command, options: &BootOptions, files: Files) }; // The ready marker is test-runner's, which the supervisor starts after its // first line, so a boot that reached it has said its build, and the kernel - // named the ROOT that carries it before that. + // named the ROOT that carries it before that. Both are read off the image + // this boot was handed, never off the checkout, which may have moved since. if options.ready_marker == DEFAULT_READY && !options.mute { - let root = toyos_build::image::root_uuid_on(&boot_image).unwrap_or_else(|why| panic!("[qemu] {why}")); + let (root, bytes) = toyos_build::image::root_file_on(&boot_image, toyos_osrelease::PATH) + .unwrap_or_else(|why| panic!("[qemu] {why}")); + let release = toyos_osrelease::parse(&bytes) + .unwrap_or_else(|why| panic!("[qemu] {} on {} is not the build's: {why:?}", toyos_osrelease::PATH, boot_image.display())); let named = format!("boot: root={root}"); - let said = format!("{}{} ", toyos_osrelease::SAID, built_commit()); + let said = format!( + "{}{} {}, committed {} UTC, toolchain {}, {}", + toyos_osrelease::SAID, + release.commit.as_str(), + release.tree, + toyos_wallclock::Civil::from_unix_secs(release.committed), + release.toolchain.as_str(), + release.arch.machine() + ); for line in [&named, &said] { if !boot_log.lines().any(|l| l.contains(line.as_str())) { let _ = child.kill(); @@ -2283,16 +2295,6 @@ fn spawn_and_wait_ready(mut qemu: Command, options: &BootOptions, files: Files) } } -/// The commit this run's images are built from, read off the checkout by the -/// harness rather than taken from the build that records it. -fn built_commit() -> &'static str { - static COMMIT: std::sync::OnceLock = std::sync::OnceLock::new(); - COMMIT.get_or_init(|| { - let repo = gix::open(super::compile::repo_root()).expect("the checkout is a git repository"); - repo.head_id().expect("HEAD names a commit").to_string() - }) -} - /// One finished console line into everything that keeps one. /// /// `false` means nothing is left to read for: the receiver has gone, or the