Repository navigation
UserSafe is proved by the compiler: user_safe! declares a struct, its impl and its no-padding assertion, and usersafe::bytes is the one view of its bytes - #747
Conversation
…ts impl and its no-padding assertion together `UserSafe` was an `unsafe trait` in `kernel/src/user_ptr.rs` with fifteen hand-written impls, each resting on a `SAFETY` comment. One had already drifted (`LogCursor` "88 bytes"; it is 80), four structs had no size assertion at all, and three asserted a literal total a field added with its new total still satisfies with a gap inside. The trait moves to `toyos-abi`, beside the structs, and `user_safe!` is the only way a struct implements it: the macro emits the struct under `#[repr(C)]` (or `#[repr(transparent)]` for one unnamed field), the impl, and a const assertion that the struct's size is the sum of its fields' sizes, each field bounded `UserSafe`. A gap or a tail fails const evaluation; a field type with an invalid bit pattern has no impl and fails the bound. The integers and `[T; N]` are the base cases, written once. A published crate was refused: every one of these structs but `Stat` lives in `toyos-abi`, which std depends on, so zerocopy or bytemuck would have to build as `rustc-dep-of-std`, and zerocopy's derive is a proc-macro. No layout changed and none had padding: every struct built under the macro unchanged. `toyos-userbound/src/span.rs`'s table of thirteen type names with sizes written beside them is replaced by every size and alignment up to 256 bytes, which is all `is_user_object` reads of a type. Closes issues/usersafe-layouts-are-checked-by-hand.md. Files issues/fstats-answer-is-declared-twice.md, found on the way: the kernel's `Stat` and the ABI's are two declarations of one layout. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
Evidence for the negative controls1. The two refusals with the
|
Review, round 1, head
|
…s the only unsafe block Review round 1 found two mechanisms for one promise. Twelve structs had "no byte is padding" proved by `user_safe!`; eight more (`AcpiInfo`, `HdaInfo`, `PartitionInfo`, `PciFunctionInfo`, `LogRecord`, `ModuleInfo`, `TraceRecord`, `VirtioSoundInfo`) still asserted it with a literal sum written by hand under an `unsafe` `as_bytes`, and three of the twelve kept an `as_bytes` of their own. The eight, and `acpi::Block` which `AcpiInfo` holds, are declared through `user_safe!`. `toyos_abi::usersafe::bytes<T: UserSafe>` is the one byte view; the eleven `as_bytes` methods, the eight hand sums and their doc comments are deleted. `LogRecord` keeps `align(64)` as a caller attribute and its `RECORD_BYTES`, `align_of` and `offset_of` assertions, which are other claims. `log::user::read_rings` loses its `bytes` parameter, which had one value per record type: the record is bound `UserSafe` instead. The macro names `::core::assert!` and `::core::concat!`, so a `macro_rules! assert` in scope at an expansion site cannot replace the check. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
…erSafe branch Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
`DmaGrant`, `DmaMapping` and `DeviceIrqRecord` reach user memory through the kernel's own `from_raw_parts`, outside `usersafe::bytes` and outside this branch's brief. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
Round 2 patches and outputsHead
diff --git a/kernel/src/object/ops.rs b/kernel/src/object/ops.rs
index 67260edad..af584dbf8 100644
--- a/kernel/src/object/ops.rs
+++ b/kernel/src/object/ops.rs
@@ -613,7 +613,7 @@ toyos_abi::user_safe! {
pub struct Stat {
pub file_type: u64,
pub size: u64,
- pub mtime: u64,
+ pub mtime: u32,
}
}
@@ -624,7 +624,7 @@ pub fn fstat(object: &KObjectRef) -> Stat {
KObjectRef::File(f) => f.with(|state| Stat {
file_type: FileType::File as u64,
size: file_cache::size(state.file_id),
- mtime: state.mtime,
+ mtime: state.mtime as u32,
}),
KObjectRef::PipeRead(r) => {
plain(if r.is_tty() { FileType::Tty } else { FileType::Pipe })
diff --git a/kernel/src/object/ops.rs b/kernel/src/object/ops.rs
index e0803305f..9401c2361 100644
--- a/kernel/src/object/ops.rs
+++ b/kernel/src/object/ops.rs
@@ -613,7 +613,7 @@ pub fn seek(object: &KObjectRef, pos: SeekFrom) -> u64 {
pub struct Stat {
pub file_type: u64,
pub size: u64,
- pub mtime: u64,
+ pub mtime: u32,
}
/// What kind of thing this is, and how big.
@@ -623,7 +623,7 @@ pub fn fstat(object: &KObjectRef) -> Stat {
KObjectRef::File(f) => f.with(|state| Stat {
file_type: FileType::File as u64,
size: file_cache::size(state.file_id),
- mtime: state.mtime,
+ mtime: state.mtime as u32,
}),
KObjectRef::PipeRead(r) => {
plain(if r.is_tty() { FileType::Tty } else { FileType::Pipe })
diff --git a/toyos-abi/src/usersafe.rs b/toyos-abi/src/usersafe.rs
index da4c5f6db..f91044ff6 100644
--- a/toyos-abi/src/usersafe.rs
+++ b/toyos-abi/src/usersafe.rs
@@ -50,7 +50,7 @@ pub const fn field<T: UserSafe>() -> usize {
/// }
/// ```
///
-/// ```compile_fail
+/// ```
/// toyos_abi::user_safe! {
/// #[derive(Clone, Copy)]
/// struct Tail { a: u64, b: u32 }
@@ -67,7 +67,7 @@ pub const fn field<T: UserSafe>() -> usize {
/// }
/// ```
///
-/// ```compile_fail
+/// ```
/// toyos_abi::user_safe! {
/// #[derive(Clone, Copy)]
/// struct Scalar { a: u32, b: char }
|
Review, round 2, head
|
… share one weakness Review round 2's NOTE. The issue said `DeviceIrqRecord`'s `SAFETY` comment rested on an assertion that does not exist and that a field added to it fails nothing. `toyos-abi/src/pci.rs` asserts `DeviceIrqRecord::SIZE == 4`, so an added field fails the build until the literal is moved: a total written by hand where `DmaGrant` and `DmaMapping` have a sum written by hand, and the same weakness, a number nothing ties to the fields. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
Review, round 3, head
|
…s's branch - toyos-abi/src/acpi.rs: `AcpiInfo`, with this branch's `rsdp` and `reserved`, and `Access` are declared through `user_safe!`; their hand size assertions and `as_bytes` go with main's. - kernel/src/user_ptr.rs: main's side whole; the hand impl for `Access` goes with every other. - kernel/src/arch/x86_64/acpi_mode.rs: main's `smi_cmd` owns the port, its lock and its declaration; `settle` here keeps the holder's half alone. - kernel/src/arch/x86_64/smi_cmd.rs: the declaration says `ReadOnly`, the argument this branch made `pio::declare` require. - kernel/src/arch/x86_64/power.rs: the power-off calls both settles. The merge had taken main's line, which replaced the call this branch's wait hangs off, without a conflict. - tests/toyos.rs: `acpi_death_on_metal` reads main's line shape and its boot-processor judgement, and this branch's lock-word line. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
…workspace Git merged every file without a conflict. #752's two `env:` lines (`CARGO_PROFILE_DEV_DEBUG: line-tables-only` in both workflows' `host` jobs) and its shortened `carry()` survive as it wrote them. No manifest and no lock moved, so `Cargo.lock` is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
…nd the SDK resolve together (#746) Stage 3 of `issues/the-tree-says-who-uses-each-thing.md`. The root, `kernel/`, `bootloader/`, `userland/` and `toyos/` were five Cargo resolutions; they are one workspace with one `Cargo.lock`, one `[profile.toyos]`, one `[patch]` table and one tracked `.cargo/config.toml`. No directory moves. Head `dd12c0b32`, on `origin/main` `6f87cdb9c` (#749; none of #757, #759 or #762 had landed when it was merged and measured). It is `9ef866436`, where the CI readings of the fold itself were taken, plus two merges of `main` and the close of the stage's issue. Everything owed at the merged head is in the next section, measured at `dd12c0b32`. ## The merge of #749, measured at `dd12c0b32` #749 wrote its new dependency edges into `kernel/Cargo.lock` and `userland/Cargo.lock`, which this branch deletes. Both modify/delete conflicts are resolved by deleting the file and re-resolving the root lock. Git merged the root `Cargo.lock` without a conflict into a lock that is wrong, as the review found: `cargo metadata --locked` on it exits 101 (`cannot update the lock file … because --locked was passed`). It had `toyos-userbound`'s edges to `toyos-abi` and `toyos-bootmap`, which #749 also wrote into the root lock, and not `acpiserver`'s to `toyos-acpi` and `toyos-aml`, which #749 wrote into userland's alone. `cargo metadata --offline` re-resolved it. `diff` of git's merged lock against the re-resolved one is those two lines under `acpiserver` and nothing else: no package added, no version moved. | Owed | Command | Result | |---|---|---| | The lock resolves as committed | `cargo metadata --locked --format-version 1` | exit 0 at this head by the host suite's step "the licences of what ships", which runs `cargo metadata --locked` for every shipped crate's manifest and is green in `host.log` and in run 37757675374; the hand run's empty stderr (`metadata-locked.err`) predates the merge commit and recorded no exit | | The lock's (name, version) pairs are the union of `main`'s five | `pairs.sh <worktree> 6f87cdb`: the pairs of `Cargo.lock`, `kernel/`, `bootloader/`, `userland/` and `toyos/Cargo.lock` at `6f87cdb9c`, `sort -u`, against the root lock's | 692 against 692, `diff` exit 0 (`pairs.out`) | | The folded kernel and loader are the control's bytes | `prove.sh 6f87cdb dd12c0b …`, the round 3 script unchanged | exit 0; all four `control vs fold` byte rows `cmp` exit 0; every row in "The checks" below (`prove.out`) | | No new reader of a compiled-in path | `git diff -U0 e3bdff8 dd12c0b -- tests src toyos-blackbox toyos-symbols userland/symbolize`, its added lines searched for `\.rs`, `taken at`, `panicked at`, `Location`, `file()`, `PREVIOUS_PANIC`, `strip_prefix`, `src/`, `pure/` | the merge touches five files there, all under `tests/`; 13 hits, of which 4 are diff headers and 9 the field `info.rsdp`; none reads a path. `tests/common/power.rs:429` is still the one reader outside fixtures, and reads `taken at kernel/src/hardlockup/probe.rs` (`readers-merge.diff`, `readers-hits.txt`) | | `cargo run -- --ci host`, once, on the development machine | at `dd12c0b32`, `cargo run -- --ci host > host.log 2>&1; echo EXIT=$?` | exit 0; the log ends `[ci] Host: 77 step(s), all green`; 1-minute load 26.60 when it started (`host.log`) | The logs are in the round's scratch directory (`orch/oneworkspace-r4/`), which a reader of this pull request cannot reach; `prove.out` and `pairs.sh` are in the round 4 comment. ## What changed, per decision - **Members.** `kernel`, `bootloader`, `toyos` and userland's 40 packages join the root `[workspace]`. `userland/Cargo.toml`, four locks, three `rust-toolchain.toml` and three per-directory `.cargo/config.toml` are deleted. The toolchain files chose nothing the build read: every guest `cargo` already runs under `RUSTUP_TOOLCHAIN` naming its sysroot. - **Flags.** `build.target` is gone, since every guest build already passes `--target`. The root config holds one `[target.<triple>]` table per guest triple, six, each with the flags its directory's config gave it. A host build takes none, as before. Two things do change: - The guest crates outside the workspace that are built from their own directory for a ToyOS triple (`tests/toyos-rust-tests` and its `tls-*` crates) now take `-Dwarnings`, because cargo reads the tracked root config from above them. On a checkout without a local config they took no flags. - The one build that sets `RUSTFLAGS` itself (the test binaries linked against a `cdylib`, `src/build.rs`) takes none of the table: the variable replaces it. - **Profiles.** The root's `[profile.dev]` is `opt-level = 2`. The kernel library's host tests, its model controls, the SDK's tests and every surveyed userland crate's host tests used to resolve in their own workspaces and ran at `opt-level = 0`; they now run at 2. - **What stays apart** (the root manifest's `exclude` says why at each entry): `rust/`; `tests/toyos-rust-tests` and its `tls-*` crates and `tests/ssh-client-host`, because `[patch]` is workspace-wide and they patch or refuse what the root patches; and `userland/libc`. libc keeps its own lock because that lock is an input of the sysroot key: as a member it would be resolved by the root lock, and every dependency change of any member would move the key and rebuild every sysroot. The price is a sixth resolution of `toyos`, `toyos-abi`, `toyos-elf`, `toyos-osrelease` and `dlmalloc` that nothing holds to the root's (both carry `dlmalloc` 0.2.13 today). - **The lock** is every `[[package]]` of the five locks, deduplicated and resolved by `cargo metadata`. Nothing was `cargo update`d. Since the lock reviewed at `88bcbf4d3` it has changed by #749's four edges alone (see the section above); `cargo metadata --locked` exits 0 at this head. - **One target directory.** Every guest is built at the root with `-p` into `target/`. A stale sysroot used to `cargo clean` a crate's own target; that would now empty the build system's own, so `Stale::All` removes `target/toyos` and the guest triples' directories instead, and the `cargo clean` path, its member assertion and its test are deleted. - **Kernel and loader build one after the other.** They were built on two threads. In one target directory cargo serialises them anyway (measured in round 1: the second prints `Blocking waiting for file lock on artifact directory`), so the thread scope is deleted. - **The host suite** can no longer be `--workspace`: the kernel binary, the loader and most of userland do not build for a host. `src/hostws.rs` says which members a host tests, and the workspace test and clippy runs `--exclude` the rest by package name. - **Fork clones.** The tracked config includes the gitignored `.cargo/local.toml` when it exists, and `implementer.md` names it. - **The merge of #745.** `src/sysroot.rs` keeps both sides: `SYSROOT_SOURCES` carries `"sdk/std"` and `SYSROOT_MANIFESTS` ends in `".cargo/config.toml"`; its test keeps both loops; `issues/toyos-has-its-own-allocator.md` keeps `sdk/std/sys/alloc.rs` and "from the kernel's graph in `Cargo.lock`". Git merged all three without a conflict. - **The host's own apps build where the userland tests build.** `src/ci.rs`'s apps step passes `--target` only where it checks another host's triple. On `main` the `userland/*` test steps and the host's apps step both named the host triple and shared `userland/target/<host triple>`. The fold took `--target` off the test steps, which no longer need it to keep a guest triple out, and left it on the apps step: the tests filled `target/debug`, the apps `target/<host triple>`, and every dependency was compiled twice. That is what made the sealed tree larger than `main`'s (see CI). - **The merges of `main`.** #747, #748, #750, #751, #753 and #755 merged without a conflict. #749 did not: see the section above. - **The merge of #752.** Git merged it without a conflict: both workflows' `host` jobs keep `CARGO_PROFILE_DEV_DEBUG: line-tables-only` in `env:`, and `carry()` no longer sets it. No manifest and no lock moved in the merge. - **Issues.** `issues/the-tree-resolves-in-five-cargo-locks-not-one.md` is deleted: the one thing it named as left, the T14's run of the metal profile on the folded build, ran green at `db55db96a`, and review round 2 ruled no boot owed for what followed on two conditions, both in the section above. Stages 2 and 3 of `issues/the-tree-says-who-uses-each-thing.md` now read "Landed in #724, #732 and #738" and "Landed in #746". What the file carried that stays true is the root manifest's `exclude`, which says why each excluded directory keeps its own resolution; the deleting commit's message carries the rest (libc's second resolution of five crates, and `miniz_oxide` 0.8.9 beside 0.9.1 until `png` takes 0.9). `issues/cargo-run-inside-kernel-loom-or-kernel-sim-builds-for-a-bare-target.md` is closed on the two in-directory runs at `9ef866436`. `issues/the-sdk-is-linted-by-no-clippy-run.md` is filed and names its owner, the build system. ## The fold changed the kernel's source paths, and the proof did not see it The T14's run of the whole metal profile at `88bcbf4d3` exited 1: 295 passed, 1 failed, 30 boots. The red row was `hard_lockup_ends_a_deaf_cpu`. Its judge looked for `taken at src/hardlockup/probe.rs` in the previous boot's panic record, and the readback's loader log says `taken at kernel/src/hardlockup/probe.rs:145:29`. **What changed in the kernel's strings.** Cargo hands rustc a workspace member's source by its path from the workspace root, and rustc writes that path into every panic and `Location`. The kernel's root was `kernel/`; it is now the repository. So `src/...` became `kernel/src/...`, and a path dependency outside the old root, which the base named by the checkout's absolute path, is now named from the repository root (`toyos-abi/src/...`). Read from the actuator kernel staged at this head: 254 distinct `.rs` paths, 158 under `kernel/`, 38 under a `toyos-*` crate or `bcachefs`, none bare `src/` or `pure/`, none naming the worktree. **Why the proof did not see it.** Both of its oracles were blind to it by construction: - The byte row compared the fold against a control that is the base with its workspace root moved up. The control moved the root too, so it carries the same new paths and the bytes agree. - The `rustc`-lines row compared base against fold after a `sed` that rewrites `(kernel/|bootloader/)?(src|pure)/x.rs` and `ROOT/<crate>/src/lib.rs` to one form. That rewrite is needed, or every path crate's line differs and the row can show nothing else; but it absorbed the change without reporting it. `prove.sh` now reports what that rewrite absorbs: per artifact, how many crates' source arguments were renamed, and a diff of the `.rs` paths the artifact carries, base against fold and control against fold. The script is in the round 3 comment and has run twice since, at `9ef866436` and at this head. **Every reader of a compiled-in path.** I searched the harness, the guest tests, the build system, `toyos-blackbox`, `toyos-symbols`, `userland/symbolize`, the loader and the kernel's panic path for path literals, prefix strips and `Location` readers. One reader matches a compiled-in path by its prefix: `tests/common/power.rs`, the red row's judge, now fixed to the path the kernel records. Everything else is prefix-blind (`panicked at`, a file name with its line) or a synthetic fixture. The kernel's panic slot keeps the last 96 bytes of a path; the longest kernel path is 46, so nothing is cut. Userland's panic sites gain a `userland/` prefix the same way; no test reads one. **The record rows.** The judging asked to record three `boot.testcases-bounds.*` rows. They are not this change's: that boot was already staged on the base and unrecorded, and `main` recorded it in #745. They arrive with the merge and nothing is committed here. ## What the fold changes in what is built The lock row was measured at `dd12c0b32` against `6f87cdb9c`, the `rustc` row by `prove.sh` at the same pair; the two `cargo tree` rows at `88bcbf4d3`, and were not taken again. | Measured | Result | |---|---| | Lock: (name, version) pairs, the fold's against the union of `origin/main`'s five | identical, 692 pairs, `diff` exit 0 | | Lock: sources | registry `getrandom` 0.2.17, 0.3.4, 0.4.2 are gone; the forks at the same versions remain | | Kernel and loader, both arches: every `rustc` command line of `cargo build -v`, base against fold, path and cargo's path-derived hashes taken out | identical, `diff` exit 0: 31 units per kernel, 55 and 37 per loader | | Userland, both triples: `cargo tree -e features` over every program, base against fold | identical, `cmp` exit 0 | | Host members: the same | `diff` exit 1, on `getrandom`'s source alone | So one resolved crate changes: the build system and the other host members compile the ToyOS forks of `getrandom` 0.2.17, 0.3.4 and 0.4.2 instead of the registry's, same versions, same features. And every source path compiled into the kernel and the loader changes, as above. ## The checks (high-risk: build system) Measured at `dd12c0b32` against `origin/main` `6f87cdb9c` by `prove.sh` (the script of the round 3 comment, unchanged), exit 0; its output is in the round 4 comment. Round 3 measured the same rows at `9ef866436` against `b432ed21c`, round 1 at `88bcbf4d3` against `e7010129f`. **Negative control.** The whole change reverted is the base. A second control is the base with only its workspace root moved up, keeping the crate's own base lock and its profile. It is given the fold's root `.cargo/config.toml`, so the control does not hold the flags: the one row that does is base against fold on normalised `rustc` lines. **Oracle.** Bytes and cargo's own command lines. Each cell is its own `cmp` or `diff` exit: | | kernel x86_64 | kernel AArch64 | loader x86_64 | loader AArch64 | |---|---|---|---|---| | control vs fold, bytes | 0 | 0 | 0 | 0 | | fold vs fold rebuilt, bytes | 0 | 0 | 0 | 0 | | base vs fold, bytes | 1 | 1 | 1 | 1 | | base vs fold, `rustc` lines normalised | 0 | 0 | 0 | 0 | | control vs fold, `rustc` lines verbatim | 0 | 0 | 0 | 0 | What the normalisation absorbs, reported by the three `paths` rows: base against fold, cargo hands rustc another source path for 29 of 31 crates of each kernel and for 35 of 50 and 13 of 35 crates of the loaders (`diff` exit 1 each, as expected); the `.rs` paths the x86-64 kernel carries are 250 on both sides, of which the base has 39 under the tree's absolute path and 150 from the crate's own root and the fold none of either (`diff` exit 1); control against fold the artifacts' paths are identical (`diff` exit 0, all four). **Mutations**, each on a fresh copy of the fold: M1 (drop the `[target.x86_64-unknown-uefi]` table) loader build exit 101; M2 (lock `dlmalloc` at 0.2.12) `cmp` exit 1 and lines `diff` exit 1; M3 (select `bcachefs` beside the kernel in one `cargo`) kernel build exit 101. ## Gates The rows of the section "The merge of #749" were read at `dd12c0b32`. Every row below was read at `9ef866436` unless it says otherwise, each once, the narrowest that judges it. `ci.yml` runs on the push of `dd12c0b32`; its result is not in this body. | Gate | Result | |---|---| | `cargo run -- --ci host` | at `dd12c0b32`, development machine: exit 0, `[ci] Host: 77 step(s), all green`. Linux runner at `9ef866436`: `ci.yml` run 37740454881 `host` success; cold inside `--ci seal`, nightly run 37740449787: `[ci] Seal: 80 step(s), all green`; on macOS, the same nightly's `portability-macos`: success | | `cargo test --lib ci::tests` (the changed step's own test) | exit 0, 13 passed | | The images and the guest suite | at `9ef866436`, run 37740454881: `toolchain / build` and `guest / suite` success (KVM); run 37740449787: `toolchain / build` and `tcg / suite` success. At `e3bdff8af`, run 37755369755: `host` and `toolchain / build` success, `guest / suite` still running when read. Not run locally, and not read at `dd12c0b32` | | `prove.sh 6f87cdb dd12c0b …` | exit 0; every row as in the table above | | `cargo test` inside `kernel/loom` | exit 0 | | `cargo test` inside `kernel/sim` | exit 0 | | Cold wall clock, x86-64 kernel and loader (`wall.sh`, one run) | base, two cargos side by side: 24 s, 1-minute load 34.92 before it. Fold, one after the other: 20 s, load 42.23. Other agents' builds were running, so the two are not a controlled pair; the fold was not slower | | Metal profile | every row green at `db55db96a` (comment 6048782042). Since then the branch changed `src/ci.rs` and the root lock's two `acpiserver` edges; the kernel sources that moved are `main`'s own landings (#747, #748, #749), merged in. Review round 2, ruling (3), owes no boot for the merge of #749 on two conditions, both met above | | `cargo test --manifest-path userland/acpiserver/aml/Cargo.toml` at `e3bdff8af` | exit 0 | | `git status --porcelain --ignore-submodules=none` at `dd12c0b32` | empty | `issues/cargo-run-inside-kernel-loom-or-kernel-sim-builds-for-a-bare-target.md`'s close now stands on the two in-directory runs at `9ef866436`. The logs of these rows are files in the scratch directory of the round that took them (`orch/oneworkspace-r3/`), which a reader of this pull request cannot reach; the proof's and the measurements' outputs are in the round 3 comment. ## CI **Why the sealed tree was larger than `main`'s, measured.** A `workflow_dispatch` of `nightly.yml` at `88bcbf4d3` (run 37685714260) sealed `9348536345 B in 20064 files`, red. Units compiled per step, counted from that log and from `main`'s nightly at `b432ed21c` (run 37717000719, sealed `18708 files, 7664839895 B`): | step | `88bcbf4d3` | `main` | |---|---|---| | the driver's own build | 203 | 203 | | the build system | 207 | 207 | | the workspace's host members | 99 | 99 | | clippy, warnings denied | 412 | 417 | | the controls | 56 | 56 | | `userland/*` | 225 | 289 | | the apps for linux | 282 | 47 | | the apps for macos | 248 | 248 | | the apps for windows | 239 | 239 | Of the 256 distinct crates the apps-for-linux step compiled at `88bcbf4d3`, 226 had been compiled by an earlier step of the same run; 30 by none. The cause is the target directory: the test steps built without `--target` into `target/debug`, the apps step with `--target x86_64-unknown-linux-gnu` into `target/x86_64-unknown-linux-gnu`. Profile, features and `RUSTFLAGS` are the same in both. **The fix, measured once on the development machine** (`share.sh` in the round 3 comment; cold, a target of its own, `aarch64-apple-darwin`): after the fourteen test steps' builds (271 units), the ten apps with `--target <host>` compile 270 units and add 616,616 KiB under `target/<host>` and 135,064 KiB under `target/debug`; the same ten without `--target` then compile 47 units and add 107,856 KiB. The 47 are the same crates `main`'s step compiles on the runner. **The seal at `9ef866436`, nightly run 37740449787** (conclusion success: `host`, `portability-linux`, `portability-macos`, `toolchain / build`, `tcg / suite`): ``` the cache entry, read by content: none restored: the run is cold the apps for linux: 10 app(s) pass `cargo build`; … the tree, sealed as the host cache's entry: 17010 files, 7493968284 B of the 8000000000 B an entry may hold; sealed: 2496 sources, built on Linux X64 ubuntu24 20261004.327.1, every target dated as built [ci] Seal: 80 step(s), all green ``` Units per step in its log, against the two columns above: | step | `9ef866436` | `88bcbf4d3` | `main` | |---|---|---|---| | `userland/*` | 225 | 225 | 289 | | the apps for linux | 42 | 282 | 47 | | the apps for macos | 264 | 248 | 248 | | the apps for windows | 240 | 239 | 239 | Every other step compiles what it did at `88bcbf4d3`. I expected 47 for the Linux apps: it is `main`'s 47 less `crc32fast`, `log`, `memchr`, `smallvec` and `toyos-keymap`, which an earlier step had compiled. I did not expect the macOS step's 16 more: all are host-side units (`syn`, `thiserror-impl`, `tokio-macros`, `futures-macro`, `autocfg` and the like), which the Linux step's `--target` build used to compile for the host and which the first `--target` step now compiles instead. The four steps together compile 771 units against 994 at `88bcbf4d3` and 823 on `main`. - Margin: 506,031,716 B under the limit, 6.3 %, on image 20261004.327.1. `main`'s 7,664,839,895 B was sealed on 20260927.320.1. The one pair of figures there is for the two images is `f260e0b98`, built with full debuginfo before #752 cut it to line tables: 8,540,783,725 B on 20260927.320.1 (run 37292450697) against 8,182,940,473 B on 20261004.327.1 (run 37601225884), 357,843,252 B or 4.2 % less on the newer. So this head's figure and `main`'s are not a pair, and this head is unmeasured on the older image. - Against the same branch before the fix and before #752: 9,348,536,345 B at `88bcbf4d3` on 20260927.320.1. **`ci.yml` run 37740454881 at `9ef866436`:** `host`, `toolchain / build` and `guest / suite` success. After this lands every `host` check runs cold until the first nightly on `main` seals and saves: the path list is the cache's version. **Toolchain keys.** The fold moves the sysroot key once: `userland/.cargo/config.toml` was one of its inputs and `.cargo/config.toml` replaces it. From now on a change to any guest triple's flags moves that key. The merges of #747 and #749 moved `toyos-abi` and `toyos`, so the sysroot key moved with `main`; the key at this head was not read here. ## No new gate, test or dependency No guest test is added or changed. No dependency is added. The proof is a one-off script because its subject is this one change against its base. ## Size `git diff --shortstat origin/main...HEAD` at `dd12c0b32`: 53 files, +5454 −7765. Without the locks: 48 files, +446 −704. `src/`, `tests/toyos.rs` and `tests/common/`: 12 files, +237 −360, of which tests are roughly +65 −115 by my reading of the hunks (an estimate, not a count). `issues/`: 16 files, +49 −115. ## What I am unsure of - **The seal on the older runner image.** GitHub serves two; this branch was sealed on the newer one, at `9ef866436`. The only pair of figures for the two is `f260e0b98` with full debuginfo (above): the older image sealed it 357,843,252 B larger. Carried unscaled onto this tree that leaves 148,188,464 B under the limit on the older image; scaled by the pair's ratio, about 178 MB. Both are arithmetic, not a run. - **`dd12c0b32` itself:** `ci.yml` run 37757675374 has `host` and `toolchain / build` success at it; its `guest / suite` is what the landing waits on. #757 (`b6bcb9691`) landed on `main` after this head was merged and measured: ten source files under `kernel/src`, `toyos-abi/src`, `toyos-userbound/src` and `tests/toyos-rust-tests`, no manifest and no lock; `git merge-tree --write-tree dd12c0b b6bcb96` exits 0. Nothing here was measured with it in. It differs from the sealed head by `main`'s #749, #750, #753 and #755 and the root lock's two edges; the seal's byte count at this head is unmeasured. - **Build wall clock.** One run under load; the fold was not slower. - **Clippy reaches further.** The bare-target shapes now also lint the kernel's and the loader's path dependencies for those targets. - **The licence gate reads a superset.** `--all-features` for the kernel now turns on every member's features. It judges more than ships. - **The runner's cargo.** The nightly's `host` at `88bcbf4d3` parsed the optional `include`, so the runner's cargo accepts it. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
Closes
issues/usersafe-layouts-are-checked-by-hand.md. Head196ec3b67, merged withorigin/maine16cb0841(#745). Logs are underorch/usersafe-r2/in the job scratchpad, one directory per head they were measured at.What changed, per decision
UserSafeis implemented only bytoyos_abi::user_safe!, which declares the struct, its impl and its assertion together. The trait lives intoyos-abi/src/usersafe.rs, beside the structs. The macro emits the struct under#[repr(C)](named fields) or#[repr(transparent)](one unnamed field), thenconst _: () = ::core::assert!(size_of::<S>() == 0 + field::<F1>() + ...)wherefield<T: UserSafe>()issize_of::<T>(), then theunsafe impl.E0080, "a byte ofSbelongs to no field".E0277on thefield::<T>bound.u8 i8 u16 i16 u32 i32 u64 i64, and[T; N]forT: UserSafe. "No impl is written by hand" holds for structs only.One byte view, and one
unsafeblock behind it (review round 1's BLOCKER).toyos_abi::usersafe::bytes<T: UserSafe>(&T) -> &[u8]is the only way aUserSafevalue becomes bytes. The eight structs that still asserted "no padding" with a sum written by hand (AcpiInfo,HdaInfo,PartitionInfo,PciFunctionInfo,LogRecord,ModuleInfo,TraceRecord,VirtioSoundInfo) are declared throughuser_safe!, and so isacpi::Block, whichAcpiInfoholds. Deleted: the elevenas_bytesmethods with theirunsafeblocks, the eight hand sums, and their doc comments.LogRecordpasses#[repr(align(64))]as a caller attribute and keeps itsRECORD_BYTES,align_ofandoffset_ofassertions;TraceRecordkeepsRECORD_BYTES. Those are other claims.kernel/src/log/user.rs:read_ringsloses itsbytes: fn(&Record) -> &[u8]parameter, which had one value per record type; the record is boundUserSafeand the sink callsusersafe::bytes.log.rs,syscall.rs) stay, callingusersafe::bytes: they hold field order, which the macro does not.toyos/src/device.rs: theSAFETYcomment ondevice_info!cited the deleted assertions andas_bytes; it now cites the macro.The macro names
::core::assert!and::core::concat!(NOTE), so amacro_rules! assertin scope at an expansion site cannot replace the check.Why not a published crate, and why not a derive. Every struct but the kernel's
Statis declared intoyos-abi, and a foreign trait can only be implemented there.toyos-abiis in std's dependency graph, so zerocopy or bytemuck would have to build as a dependency of std, which neither does unforked, and a derive bringssyn,quoteandproc-macro2under the kernel and std.macro_rulesreaches both refusals with no dependency.Lines (
git diff --shortstat origin/main...196ec3b67): 630 insertions, 749 deletions, 24 files; with whitespace ignored, 270 and 389, the rest being wrapped structs re-indented.kernel/,toyos-abi/,toyos/: +544 −661, and +184 −301 with whitespace ignored.toyos-abi/src/usersafe.rsis 108 lines.No layout changed, and no struct had padding. Every struct built under the macro as it stood.
toyos-userbound/src/span.rs's table of thirteen hand-written sizes is deleted; the tests walk every multiple of each alignment 1, 2, 4, 8 up to 256 bytes.Filed on the way:
issues/fstats-answer-is-declared-twice.md.issues/three-kernel-byte-views-still-rest-on-a-hand-layout-claim.md:DmaGrant,DmaMappingandDeviceIrqRecordreach user memory throughfrom_raw_partsblocks of the kernel's own (kernel/src/syscall/device.rs,kernel/src/object/ops.rs), each resting on an assertion that compares the struct's size with a number written by hand (two sums and, forDeviceIrqRecord, the total4). They are the same shape as the eight and were not in the review's count or this round's brief, so they are recorded and not fixed.The merge with #745
origin/maine16cb0841is merged atcc1a2c311; no file is touched by both sides.at-cc1a2c311/build-x86_64.log:10isBuilding sysroot 8bdf2ee4c8ec9cd6, for both targets in the one run:Compiling stdat lines 23 and 79,Compiling toyos-abiunder it at 28 and 83, and std's warnings at 46-60 and 102-116 namesdk/std/sys/pal/os.rsandsdk/std/sys/pal/mod.rs, so the backend is read fromsdk/std.src/sysroot.rs:67"toyos-abi/src",src/toolchain.rs:78STD_SOURCES = ["toyos-abi/src", "toyos/src"].usersafe.rsis inside it.sdk/std/sys/process.rs:392(SpawnArgsliteral),sdk/std/sys/fs.rs:190(toyos::fs::Stat),sdk/std/sys/pal/mod.rs:212-237(ModuleInfo, read through a pointer cast, noas_bytes). No file undersdk/stdcalled any deletedas_bytes.Gates
The head,
196ec3b67, differs fromcc1a2c311by one file, the second filed issue (git diff --stat cc1a2c311 196ec3b67: 41 insertions inissues/three-kernel-byte-views-still-rest-on-a-hand-layout-claim.md), so the source every local gate below compiled is the head's. Every local measurement was taken at the head its row names, none at196ec3b67.cargo run -- --ci hostcc1a2c311at-cc1a2c311/ci-host.log, ends[ci] Host: 77 step(s), all green(line 7826)cargo run -- --build-only --arch x86_64cc1a2c311at-cc1a2c311/build-x86_64.log:560Build finished.cargo run -- --build-only --arch x86_643861d296bat-3861d296b/build-x86_64.log:502Build finished.cargo run -- --build-only --arch aarch64cc1a2c311at-cc1a2c311/build-aarch64.log:394Build finished.cargo test(the guest suite, whole)cc1a2c311at-cc1a2c311/guest-suite.log:855,30 passed, 30 totalNothing was run locally at
196ec3b67, and why. The re-run at3861d296bwas stopped after its x86_64 build: the owner reserved this machine's compute for another task while it ran, and196ec3b67, which corrects the filed issue's text (review round 2's NOTE), was made with no build or test at all.--ci host, the builds, the guest suite, the compile-fail cases and the control have no local measurement at196ec3b67.--ci hostreads tracked files that are not compiled, the issue among them, so the table's row atcc1a2c311does not stand for it at the head: CI at196ec3b67is the measurement forhostandguest / suite, and it is recorded below here. CI must show, at196ec3b67:hostgreen withDoc-tests toyos_abilisting the twocompile fail ... okcases and their two twins;toolchaingreen, which is the macro compiling as a dependency of std on both targets;guest / suitegreen.CI at
196ec3b67, run 37696165075 (ci, on the pull request's head), all three concluded success:host(cargo run -- --ci host):[ci] Host: 78 step(s), all green. UnderDoc-tests toyos_abi, exactly four lines:usersafe::user_safe (line 53) - compile fail ... ok,(line 70) - compile fail ... ok,(line 46) ... ok,(line 63) ... ok.toolchain / build: success; at this head it restored sysroot6a6e3fbcd05fbd1cfrom the cache and compiled no std. The compile oftoyos-abiwith the macro under std, for both targets againstsdk/std, is run 37694635644 at3861d296b, which built that same sysroot key;196ec3b67differs from3861d296bby one issue file and computes the same key.guest / suite:test result: ok. 30 passed, 30 total, run and not skipped.No new guest test, no new dependency, no new gate.
High-risk checks (ABI, the kernel's trust boundary)
The two compile-fail cases, each beside its accepted twin, doc-tests on
user_safe!. In--ci hostatcc1a2c311:ci-host.log:4601-4604, bothcompile fail ... ok, both twinsok. The host's rustdoc reads no error code on acompile_failblock, so each reason was read with the fence removed (control/unfence.patch, applied checked and reversed in the script, tree clean after),cargo test -p toyos-abi --doc, EXIT=101,at-cc1a2c311/abi-doc-unfenced.log:{ a: u64, b: u64 }{ a: u64, b: u32 }error[E0080]: evaluation panicked: a byte of \Tail` belongs to no field` (line 14){ a: u32, b: u32 }{ a: u32, b: char }error[E0277]: the trait bound \char: UserSafe` is not satisfied` (line 30)The whole change reverted, last run at
cc1a2c311, not at the head. Mutation: the kernel'sStat.mtimenarrowed fromu64tou32, four tail bytes in a structSYS_FSTATcopies out.cargo run -- --build-only --arch x86_64, each arm a checked patch applied and reversed in one script (gates.sh),at-cc1a2c311/control/exits.txt, porcelain empty after.control/revert.patchisgit diff <head> origin/mainless the filed issues.error[E0080]: evaluation panicked: a byte of \Stat` belongs to no fieldatsrc/object/ops.rs:611`at-cc1a2c311/control/arm-a-branch.log:11e16cb0841+ mutationCompiling kernelthenBuild finished.at-cc1a2c311/control/arm-b-reverted.log:171,:509Independent oracle: the compiler's own layout,
size_of, is what the assertion reads; the macro computes no offset.The patches are in the round 2 comment.
Unsure of
unsafe impl UserSafe; one outsideusersafe.rsis what a reader of a diff catches.#[repr(packed)]would be accepted, soundly;#[repr(align(N))]is refused whenever it adds a byte, andLogRecord's adds none.issues/clippy-stage-two-is-lints-one-at-a-time.mdsays in its record of a past sweep thatLogRecord::as_bytesexists. It no longer does; that file is another track's history and is not edited here.toyos-abi/src/audio.rskeeps a hand sum onAudioCompletionRecord. Nounsaferests on it: both kernel drivers write the record field by field into a byte array.🤖 Generated with Claude Code
https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A