Skip to content

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

Merged
Japabu merged 5 commits into
mainfrom
wt/toyos-usersafe
Oct 7, 2026
Merged

Japabu merged 5 commits into
mainfrom
wt/toyos-usersafe

Conversation

@Japabu

@Japabu Japabu commented Oct 7, 2026 •

Copy link
Copy Markdown
Collaborator

Closes issues/usersafe-layouts-are-checked-by-hand.md. Head 196ec3b67, merged with origin/main e16cb0841 (#745). Logs are under orch/usersafe-r2/ in the job scratchpad, one directory per head they were measured at.

What changed, per decision

UserSafe is implemented only by toyos_abi::user_safe!, which declares the struct, its impl and its assertion together. The trait lives in toyos-abi/src/usersafe.rs, beside the structs. The macro emits the struct under #[repr(C)] (named fields) or #[repr(transparent)] (one unnamed field), then const _: () = ::core::assert!(size_of::<S>() == 0 + field::<F1>() + ...) where field<T: UserSafe>() is size_of::<T>(), then the unsafe impl.

  • A padding byte anywhere makes the size exceed the sum: E0080, "a byte of S belongs to no field".
  • A field whose type is not valid for every bit pattern has no impl: E0277 on the field::<T> bound.
  • The base cases are written by hand once, in that file: u8 i8 u16 i16 u32 i32 u64 i64, and [T; N] for T: UserSafe. "No impl is written by hand" holds for structs only.

One byte view, and one unsafe block behind it (review round 1's BLOCKER). toyos_abi::usersafe::bytes<T: UserSafe>(&T) -> &[u8] is the only way a UserSafe value 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 through user_safe!, and so is acpi::Block, which AcpiInfo holds. Deleted: the eleven as_bytes methods with their unsafe blocks, the eight hand sums, and their doc comments. LogRecord passes #[repr(align(64))] as a caller attribute and keeps its RECORD_BYTES, align_of and offset_of assertions; TraceRecord keeps RECORD_BYTES. Those are other claims.

  • kernel/src/log/user.rs: read_rings loses its bytes: fn(&Record) -> &[u8] parameter, which had one value per record type; the record is bound UserSafe and the sink calls usersafe::bytes.
  • The two tests that decode a record's bytes field by field (log.rs, syscall.rs) stay, calling usersafe::bytes: they hold field order, which the macro does not.
  • toyos/src/device.rs: the SAFETY comment on device_info! cited the deleted assertions and as_bytes; it now cites the macro.

The macro names ::core::assert! and ::core::concat! (NOTE), so a macro_rules! assert in 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 Stat is declared in toyos-abi, and a foreign trait can only be implemented there. toyos-abi is 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 brings syn, quote and proc-macro2 under the kernel and std. macro_rules reaches 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.rs is 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, DmaMapping and DeviceIrqRecord reach user memory through from_raw_parts blocks 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, for DeviceIrqRecord, the total 4). 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/main e16cb0841 is merged at cc1a2c311; no file is touched by both sides.

  1. at-cc1a2c311/build-x86_64.log:10 is Building sysroot 8bdf2ee4c8ec9cd6, for both targets in the one run: Compiling std at lines 23 and 79, Compiling toyos-abi under it at 28 and 83, and std's warnings at 46-60 and 102-116 name sdk/std/sys/pal/os.rs and sdk/std/sys/pal/mod.rs, so the backend is read from sdk/std.
  2. The merged key lists still name the directory: src/sysroot.rs:67 "toyos-abi/src", src/toolchain.rs:78 STD_SOURCES = ["toyos-abi/src", "toyos/src"]. usersafe.rs is inside it.
  3. std's uses of the wrapped structs built in that sysroot: sdk/std/sys/process.rs:392 (SpawnArgs literal), sdk/std/sys/fs.rs:190 (toyos::fs::Stat), sdk/std/sys/pal/mod.rs:212-237 (ModuleInfo, read through a pointer cast, no as_bytes). No file under sdk/std called any deleted as_bytes.

Gates

The head, 196ec3b67, differs from cc1a2c311 by one file, the second filed issue (git diff --stat cc1a2c311 196ec3b67: 41 insertions in issues/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 at 196ec3b67.

command measured at exit log
cargo run -- --ci host cc1a2c311 EXIT=0 at-cc1a2c311/ci-host.log, ends [ci] Host: 77 step(s), all green (line 7826)
cargo run -- --build-only --arch x86_64 cc1a2c311 EXIT=0 at-cc1a2c311/build-x86_64.log:560 Build finished.
cargo run -- --build-only --arch x86_64 3861d296b EXIT=0 at-3861d296b/build-x86_64.log:502 Build finished.
cargo run -- --build-only --arch aarch64 cc1a2c311 EXIT=0 at-cc1a2c311/build-aarch64.log:394 Build finished.
cargo test (the guest suite, whole) cc1a2c311 EXIT=0 at-cc1a2c311/guest-suite.log:855, 30 passed, 30 total

Nothing was run locally at 196ec3b67, and why. The re-run at 3861d296b was stopped after its x86_64 build: the owner reserved this machine's compute for another task while it ran, and 196ec3b67, 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 at 196ec3b67. --ci host reads tracked files that are not compiled, the issue among them, so the table's row at cc1a2c311 does not stand for it at the head: CI at 196ec3b67 is the measurement for host and guest / suite, and it is recorded below here. CI must show, at 196ec3b67: host green with Doc-tests toyos_abi listing the two compile fail ... ok cases and their two twins; toolchain green, which is the macro compiling as a dependency of std on both targets; guest / suite green.

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. Under Doc-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 sysroot 6a6e3fbcd05fbd1c from the cache and compiled no std. The compile of toyos-abi with the macro under std, for both targets against sdk/std, is run 37694635644 at 3861d296b, which built that same sysroot key; 196ec3b67 differs from 3861d296b by 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 host at cc1a2c311: ci-host.log:4601-4604, both compile fail ... ok, both twins ok. The host's rustdoc reads no error code on a compile_fail block, 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:

struct result
{ a: u64, b: u64 } builds
{ a: u64, b: u32 } error[E0080]: evaluation panicked: a byte of \Tail` belongs to no field` (line 14)
{ a: u32, b: u32 } builds
{ 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's Stat.mtime narrowed from u64 to u32, four tail bytes in a struct SYS_FSTAT copies 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.patch is git diff <head> origin/main less the filed issues.

arm exit log
branch + mutation EXIT=101, error[E0080]: evaluation panicked: a byte of \Stat` belongs to no fieldatsrc/object/ops.rs:611` at-cc1a2c311/control/arm-a-branch.log:11
change reverted onto e16cb0841 + mutation EXIT=0, Compiling kernel then Build finished. at-cc1a2c311/control/arm-b-reverted.log:171, :509

Independent 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

  • Rust cannot forbid a hand-written unsafe impl UserSafe; one outside usersafe.rs is what a reader of a diff catches.
  • The macro passes caller attributes through. #[repr(packed)] would be accepted, soundly; #[repr(align(N))] is refused whenever it adds a byte, and LogRecord's adds none.
  • issues/clippy-stage-two-is-lints-one-at-a-time.md says in its record of a past sweep that LogRecord::as_bytes exists. It no longer does; that file is another track's history and is not edited here.
  • toyos-abi/src/audio.rs keeps a hand sum on AudioCompletionRecord. No unsafe rests 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

…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
@Japabu

Japabu commented Oct 7, 2026

Copy link
Copy Markdown
Collaborator Author

Evidence for the negative controls

1. The two refusals with the compile_fail fence removed (cargo test -p toyos-abi --doc, EXIT=101, restored)

   Compiling toyos-abi v0.16.0 (<worktree>/toyos-abi)
    Finished `test` profile [optimized + debuginfo] target(s) in 0.55s
   Doc-tests toyos_abi

running 4 tests
test toyos-abi/src/usersafe.rs - usersafe::user_safe (line 64) ... FAILED
test toyos-abi/src/usersafe.rs - usersafe::user_safe (line 47) ... FAILED
test toyos-abi/src/usersafe.rs - usersafe::user_safe (line 40) ... ok
test toyos-abi/src/usersafe.rs - usersafe::user_safe (line 57) ... ok

failures:

---- toyos-abi/src/usersafe.rs - usersafe::user_safe (line 64) stdout ----
error[E0277]: the trait bound `char: UserSafe` is not satisfied
  --> toyos-abi/src/usersafe.rs:68:32
   |
68 |     struct Scalar { a: u32, b: char }
   |                                ^^^^ the trait `UserSafe` is not implemented for `char`
   |
   = help: the following other types implement trait `UserSafe`:
             FramebufferInfo
             InboxSetup
             LogCursor
             MouseEvent
             NamespaceBuild
             ProcessStats
             RawHandle
             RawKeyEvent
           and 13 others
note: required by a bound in `toyos_abi::usersafe::field`
  --> toyos-abi/src/usersafe.rs:31:23
   |
31 | pub const fn field<T: UserSafe>() -> usize {
   |                       ^^^^^^^^ required by this bound in `field`

error: aborting due to 1 previous error

For more information about this error, try `rustc --explain E0277`.
Couldn't compile the test.
---- toyos-abi/src/usersafe.rs - usersafe::user_safe (line 47) stdout ----
error[E0080]: evaluation panicked: a byte of `Tail` belongs to no field
  --> toyos-abi/src/usersafe.rs:49:1
   |
49 | / toyos_abi::user_safe! {
50 | |     #[derive(Clone, Copy)]
51 | |     struct Tail { a: u64, b: u32 }
52 | | }
   | |_^ evaluation of `main::_doctest_main_toyos_abi_src_usersafe_rs_47_0::_` failed here
   |
   = note: this error originates in the macro `$crate::panic::panic_2021` which comes from the expansion of the macro `toyos_abi::user_safe` (in Nightly builds, run with -Z macro-backtrace for more info)

error: aborting due to 1 previous error

For more information about this error, try `rustc --explain E0080`.
Couldn't compile the test.

failures:
    toyos-abi/src/usersafe.rs - usersafe::user_safe (line 47)
    toyos-abi/src/usersafe.rs - usersafe::user_safe (line 64)

test result: FAILED. 2 passed; 2 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.51s

error: doctest failed, to rerun pass `-p toyos-abi --doc`

2. Mutation on the branch (control/mutation-branch.patch)

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 })

3. The same mutation on the base, applied after the whole change is reverted (git diff HEAD origin/main)

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 })

4. Exits (cargo run -- --build-only --arch x86_64 per arm, at e24fd3b55)

ARM A (branch + mutation) EXIT=101
ARM B (change reverted + mutation) EXIT=0
porcelain: []
DONE

5. Arm A's refusal, from its log

   Compiling kernel v0.1.0 (<worktree>/kernel)
   Compiling bootloader v0.1.0 (<worktree>/bootloader)
    Finished `toyos` profile [optimized + debuginfo] target(s) in 1.22s
error[E0080]: evaluation panicked: a byte of `Stat` belongs to no field
   --> src/object/ops.rs:611:1
    |
611 | / toyos_abi::user_safe! {
612 | |     #[derive(Clone, Copy)]
613 | |     pub struct Stat {
614 | |         pub file_type: u64,
...   |
618 | | }
    | |_^ evaluation of `object::ops::_` failed here
    |
    = note: this error originates in the macro `$crate::panic::panic_2021` which comes from the expansion of the macro `toyos_abi::user_safe` (in Nightly builds, run with -Z macro-backtrace for more info)

For more information about this error, try `rustc --explain E0080`.
error: could not compile `kernel` (bin "kernel") due to 1 previous error

thread '<unnamed>' (172605810) panicked at src/build.rs:432:9:
cargo build failed in <worktree>/kernel
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

thread 'main' (172605244) panicked at src/build.rs:1805:27:
kernel build thread panicked: Any { .. }

@Japabu

Japabu commented Oct 7, 2026

Copy link
Copy Markdown
Collaborator Author

Review, round 1, head 842f168a6 against origin/main e7010129f

Read, nothing run. Net lines (git diff --shortstat origin/main...842f168a6): 392 insertions, 378 deletions, 13 files. Production (kernel/, toyos-abi/): +347 −290, of which toyos-abi/src/usersafe.rs is +102 with four doc-tests inside it. Tests (toyos-userbound/src/span.rs's mod tests): +17 −29. issues/: +28 −59.

BLOCKER

  • toyos-abi/src/input.rs:39, toyos-abi/src/input.rs:63, toyos-abi/src/lib.rs:99, and toyos-abi/src/{acpi.rs:57, hda.rs:58, part.rs:137, pci.rs:54, log.rs:169, syscall.rs:2486, trace.rs:53, virtio_sound.rs:154} — the branch leaves the tree with two mechanisms for one promise, and production grows where it could shrink — the pull request body's "Unsure of" names this compromise and neither removes nor records it. "No byte of this struct is padding, so its bytes may cross the boundary" is now proved by user_safe! for twelve structs and still asserted by hand for eight (AcpiInfo, HdaInfo, PartitionInfo, PciFunctionInfo, LogRecord, ModuleInfo, TraceRecord, VirtioSoundInfo): a literal sum beside each (4 * 4 + 2 + 2, 7 * 4, 8 + 16, ...), the shape the closed issue called hand-checked, and an unsafe from_raw_parts under it. Three more (RawKeyEvent, MouseEvent, FramebufferInfo) are UserSafe and keep their own unsafe block, whose SAFETY comment this diff rewrites to cite the macro: that sentence three times is T: UserSafe once. The deletion: one safe pub fn bytes<T: UserSafe>(value: &T) -> &[u8] in usersafe.rs, with the only unsafe block; the eight structs declared through user_safe! (every field of each is an integer, an array of them, RawHandle or acpi::Block, which goes through the macro too; LogRecord's align(64) passes as a caller attribute and its RECORD_BYTES, align_of and offset_of assertions stay, being other claims); eleven hand unsafe blocks, eight hand sums and their doc comments gone. Root CLAUDE.md gives a found compromise two outcomes. If the implementer's fence excludes the eight, the three inside the diff still go through the one function, and the eight are filed in issues/ with an owner and an exit a build can fail.

NOTE

  • toyos-abi/src/usersafe.rs:93 — assert! and concat! are named bare, so a macro_rules! assert in textual scope at an expansion site replaces the check; ::core::assert! and ::core::concat! close it, as ::core::mem::size_of already is. None exists in kernel/ or toyos-abi/ at this head (git grep 'macro_rules! assert\b').
  • PR body, "Gates" — the table gives command and exit and no log. The logs exist at this head and were read: orch/usersafe/head/head.txt is 842f168a69bd...; ci-host.log ends [ci] Host: 77 step(s), all green and carries the four doc-tests at lines 4598-4601 and span::tests at 4298-4309; guest-suite.log:806 is 30 passed, 30 total; both build logs end Build finished. The body names them, as File servers share by service and by login session, and no launch gives either a second share #742's does.
  • PR body, "so no impl is written by hand" as the issue's exit words it is met for structs only: nine impls in usersafe.rs are by hand, the eight integers and [T; N]. The body says so; the close stands on it.

Judged, no finding

The macro's check is the promise. Fields of a struct occupy disjoint bytes, so size_of::<S>() equal to the sum of field sizes leaves no byte outside a field under any repr; every field UserSafe makes every byte of S a byte of a type with no invalid pattern. Shapes tried against it, each refused or sound:

  • alignment: #[repr(align(N))] as a caller attribute adds bytes and fails the sum, or adds none and is harmless; #[repr(packed)] removes padding and is sound, and object::<T> reads its align_of of 1.
  • zero-sized: [u64; 0] is UserSafe with size 0 and alignment 8; { a: u32, b: [u64; 0] } is 8 bytes against a sum of 4 and is refused.
  • a field under #[cfg]: its type stays in the sum while the field leaves the struct, so the sum exceeds the size and the build fails closed.
  • a type alias or a shadowed name (type u32 = char): the bound is on the type, not its spelling; refused.
  • generics, lifetimes, enums, unions: no arm matches. bool, char, NonZero*, references, raw pointers, fn pointers, Cell<_>, floats, u128, usize: no impl, E0277.
  • the repr(transparent) arm: one field, same @impl, same bound.
  • a field whose own impl is by hand: only the nine in the file. u8 i8 u16 i16 u32 i32 u64 i64 each have no padding and no invalid pattern; [T; N] for T: UserSafe is N elements end to end, Copy with T. Each is right. The four the kernel wrote (u32, u64, [u32; 2], [u64; 2]) are covered, and every copy_in/copy_out caller in kernel/src/syscall/ built.
  • a dropped #[repr(C)] in the first arm would keep every test green and stay UserSafe-sound while unpinning the ABI's field order; that is a line a reader of the macro's diff sees, not a mutation to run.

Can a type refuse a hand-written unsafe impl UserSafe outside the file? No. A macro_rules expansion is tokens at the call site: whatever user_safe! names from kernel/src/object/ops.rs or from a doc-test, a hand can name from the same place, so a sealed supertrait or a token has to be pub and only renames what the hand writes (zerocopy's only_derive_is_allowed_to_implement_this_trait is that, an obstacle and not a refusal). A real seal needs the macro unexported, which needs the kernel's Stat inside toyos-abi (the filed issue's exit) and gives up the issue's own exit, the two refused structs handed to the macro from outside the crate, because nothing in-crate can show a compile failure. So no named change here, and no #[doc(hidden)] supertrait either: it would be code guarding what a reader of a diff checks. The implementer's sentence is the remaining form; where it goes is the owner's.

The issue's exit, clause by clause.

  • refused by the compiler, one macro writing impl and assertion together: usersafe.rs:72-101; the kernel's fifteen impls gone (kernel/src/user_ptr.rs, −42).
  • { a: u64, b: u32 } refused beside { a: u64, b: u64 }: doc-tests at lines 47 and 40; reason read with the fence off, E0080 ... a byte of 'Tail' belongs to no field, EXIT=101 (abi-doc-unfenced.log, the PR's first comment).
  • { a: u32, b: char } refused beside { a: u32, b: u32 }: lines 64 and 57; E0277 ... char: UserSafe, same run.
  • no code builds or writes the four: they are doc-tests, and --ci host runs them (Doc-tests toyos_abi).
  • span.rs's table deleted; the issue file deleted; git grep usersafe-layouts 842f168a6 finds no citation left.
  • published crate or own, argued: accepted. The orphan rule puts the trait where the structs are, toyos-abi builds as rustc-dep-of-std, and no dependency arrives.

The whole-change-reverted control. control/revert.patch is byte-identical to git diff 842f168a6 origin/main less the filed issue, and git diff --stat e24fd3b55 842f168a6 is that one file, so the arms measured at e24fd3b55 are this head's source. Arm A, branch plus Stat.mtime: u32: EXIT=101, E0080 at src/object/ops.rs:611. Arm B, reverted plus the same mutation: EXIT=0, Compiling kernel then Build finished., tree clean after. The oracle is rustc's layout through size_of; the macro computes no offset.

span.rs. shapes() is every multiple of each alignment 1, 2, 4, 8 up to 256 bytes: a superset of the thirteen deleted rows and of LogCursor/TraceCursor (80, 8), which the table lacked. is_user_object takes only the two numbers, so the names were never tied to a type; nothing weaker.

issues/fstats-answer-is-declared-twice.md. Frontmatter is legal against issues/README.md; true of the tree (kernel/src/object/ops.rs:613, three u64; toyos-abi/src/syscall.rs:661, FileType #[repr(u64)] first); the exit is one a build can fail.

What #745's merge must check

toyos-abi is compiled into std, and this branch changes its code, so its identity moves and a sysroot is built. Neither head's measurements cover the merged tree:

  1. On the merge result, not on either head: cargo run -- --build-only --arch x86_64 and --arch aarch64 each build a new sysroot whose log shows Compiling toyos-abi under Compiling std with std's backend read from sdk/std, then cargo run -- --ci host and the guest suite. This branch's own first build shows the macro compiling as rustc-dep-of-std on both targets (build-x86_64.log:97 and :153), against rust/library/std, not sdk/std.
  2. std's ToyOS backend lives in sdk/std, and the fork names it by #[path] #745 rewrites src/sysroot.rs and src/toolchain.rs, which key the sysroot on toyos-abi/src as a directory and refuse a std whose dep-info read a file the key does not name. The merged lists must still name that directory, so the new toyos-abi/src/usersafe.rs is in the witness.
  3. sdk/std/sys/process.rs:392 builds a SpawnArgs literal and sdk/std/sys/fs.rs:190 reads toyos::fs::Stat; both shapes are unchanged here, and the merged build is what says so.
  4. No file is touched by both (gh pr view 745 --json files), so a textual conflict is not the signal; the sysroot build is.

SEND BACK

Japabu and others added 3 commits October 7, 2026 23:29
…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
@Japabu Japabu changed the title UserSafe is proved by the compiler: user_safe! declares a struct, its impl and its no-padding assertion together 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 Oct 7, 2026
@Japabu

Japabu commented Oct 7, 2026

Copy link
Copy Markdown
Collaborator Author

Round 2 patches and outputs

Head 3861d296b. The control and the unfenced doc-tests were last run at cc1a2c311, which differs from the head by one filed issue file.

at-cc1a2c311/control/exits.txt:

ARM A (branch + mutation) EXIT=101
ARM B (change reverted + mutation) EXIT=0
porcelain: []
head: cc1a2c311d82341d06899609f9875d27a7a16965
UNFENCED DOC-TESTS EXIT=101
porcelain after unfence: []

control/mutation-branch.patch:

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 })

control/mutation-base.patch:

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 })

control/unfence.patch:

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 }

control/revert.patch is git diff 3861d296b origin/main -- . ':!issues/fstats-answer-is-declared-twice.md' ':!issues/three-kernel-byte-views-still-rest-on-a-hand-layout-claim.md' (1814 lines; its source hunks are those of the run at cc1a2c311).

at-cc1a2c311/abi-doc-unfenced.log:

   Compiling toyos-abi v0.16.0 (/Users/jan/Dev/jan/toyos-usersafe/toyos-abi)
    Finished `test` profile [optimized + debuginfo] target(s) in 1.55s
   Doc-tests toyos_abi

running 4 tests
test toyos-abi/src/usersafe.rs - usersafe::user_safe (line 53) ... FAILED
test toyos-abi/src/usersafe.rs - usersafe::user_safe (line 70) ... FAILED
test toyos-abi/src/usersafe.rs - usersafe::user_safe (line 63) ... ok
test toyos-abi/src/usersafe.rs - usersafe::user_safe (line 46) ... ok

failures:

---- toyos-abi/src/usersafe.rs - usersafe::user_safe (line 53) stdout ----
error[E0080]: evaluation panicked: a byte of `Tail` belongs to no field
  --> toyos-abi/src/usersafe.rs:55:1
   |
55 | / toyos_abi::user_safe! {
56 | |     #[derive(Clone, Copy)]
57 | |     struct Tail { a: u64, b: u32 }
58 | | }
   | |_^ evaluation of `main::_doctest_main_toyos_abi_src_usersafe_rs_53_0::_` failed here
   |
   = note: this error originates in the macro `$crate::panic::panic_2021` which comes from the expansion of the macro `toyos_abi::user_safe` (in Nightly builds, run with -Z macro-backtrace for more info)

error: aborting due to 1 previous error

For more information about this error, try `rustc --explain E0080`.
Couldn't compile the test.
---- toyos-abi/src/usersafe.rs - usersafe::user_safe (line 70) stdout ----
error[E0277]: the trait bound `char: UserSafe` is not satisfied
  --> toyos-abi/src/usersafe.rs:74:32
   |
74 |     struct Scalar { a: u32, b: char }
   |                                ^^^^ the trait `UserSafe` is not implemented for `char`
   |
   = help: the following other types implement trait `UserSafe`:
             AcpiInfo
             FramebufferInfo
             HdaInfo
             InboxSetup
             LogCursor
             LogRecord
             ModuleInfo
             MouseEvent
           and 22 others
note: required by a bound in `toyos_abi::usersafe::field`
  --> toyos-abi/src/usersafe.rs:37:23
   |
37 | pub const fn field<T: UserSafe>() -> usize {
   |                       ^^^^^^^^ required by this bound in `field`

error: aborting due to 1 previous error

For more information about this error, try `rustc --explain E0277`.
Couldn't compile the test.

failures:
    toyos-abi/src/usersafe.rs - usersafe::user_safe (line 53)
    toyos-abi/src/usersafe.rs - usersafe::user_safe (line 70)

test result: FAILED. 2 passed; 2 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.63s

error: doctest failed, to rerun pass `-p toyos-abi --doc`

@Japabu
Japabu marked this pull request as ready for review October 7, 2026 22:12
@Japabu

Japabu commented Oct 7, 2026

Copy link
Copy Markdown
Collaborator Author

Review, round 2, head 3861d296b against origin/main e16cb0841

Read, nothing run. Net lines (git diff --shortstat origin/main...3861d296b): 628 insertions, 749 deletions, 24 files. Production (kernel/, toyos-abi/, toyos/): +544 −661. Tests (toyos-userbound/src/span.rs's mod tests): +17 −29. issues/: +67 −59. Production shrinks; accepted.

Round 1's findings

  • BLOCKER, two mechanisms for one promise: CLOSED. By reading the tree at the head and by the build that compiled it. git grep 'fn as_bytes' 3861d296b -- toyos-abi kernel toyos toyos-userbound sdk leaves one hit, toyos/src/ipc.rs:573, which is IpcPayload's and another promise. toyos-abi/src/usersafe.rs:30-33 is the only from_raw_parts over an ABI struct in toyos-abi; the eight structs and acpi::Block are under user_safe!; the eight hand sums and eleven unsafe blocks are gone; LogRecord keeps RECORD_BYTES, align_of and offset_of, TraceRecord keeps RECORD_BYTES. read_rings lost its one-valued bytes parameter for a Stream<Record: UserSafe> bound. No caller of a deleted method is left in the tree (sdk/std included), and at-cc1a2c311/build-x86_64.log:560, build-aarch64.log:394 and ci-host.log:7826 are that source building on both targets and the host.
  • NOTE, bare assert!/concat!: closed. usersafe.rs:101,103 name ::core::assert! and ::core::concat!.
  • NOTE, gates without logs: closed. The body's table names a log and line per row; each was read and says what the body says (ci-host.log:4598-4606 the four doc-tests, :7826 77 step(s), all green; guest-suite.log:855 30 passed, 30 total; control/exits.txt A=101, B=0, porcelain empty, head cc1a2c311).
  • NOTE, "no impl by hand" holds for structs only: closed. The body says so.

BLOCKER

  • PR body, "Gates" — cargo run -- --ci host and the guest suite have no measurement at 3861d296b — Evidence. The body says so itself. git diff --stat cc1a2c311 3861d296b is one issues/ file, so every compiled input is the one measured at cc1a2c311, and I accept the builds, the guest suite, the unfenced doc-tests and the control on that ground, as round 1 accepted the control one issue file behind its head. --ci host is not covered the same way: it reads tracked files that are not compiled, and the head adds one. At the time of this comment gh pr checks 747 shows toolchain / build pass (10m2s), host pending, guest / suite pending. This closes with no code change, by CI at the head that lands (see the NOTE below, which moves it):
    • host: green, its log ending [ci] Host: <N> step(s), all green, and under Doc-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. Three lines, or a compile fail line missing its twin, is red whatever the summary says.
    • toolchain / build: green, which is toyos-abi with the macro compiling under Compiling std for both targets against sdk/std. It is green at 3861d296b.
    • guest / suite: green and run, not skipped: 30 passed, 30 total or the count main has, no test filtered.
      The body then names those three runs with their head. The verdict stands on them and on nothing else.

NOTE

  • issues/three-kernel-byte-views-still-rest-on-a-hand-layout-claim.md:19,23-24 — the DeviceIrqRecord row says its SAFETY comment rests on "an assertion that does not exist", and the paragraph under the table says a field added to it "fails nothing" — false of the tree: toyos-abi/src/pci.rs:103 is const _: () = assert!(DeviceIrqRecord::SIZE == 4);, so an added field fails the build unless the literal is moved with it, the same weakness as the other two rows (a literal total, where theirs is a hand sum). The file is this branch's and in the tree, so the implementer corrects it, not the orchestrator; the correction is a new head.
  • issues/clippy-stage-two-is-lints-one-at-a-time.md:486 — "LogRecord::as_bytes exists" is false of the tree once this lands. The body discloses it; it is another track's file.

The three kernel byte views: filed is right

DmaGrant, DmaMapping and DeviceIrqRecord are round 1's shape and round 1's miss: its count was eleven and the tree had fourteen. They do not reopen the BLOCKER, and they are not owed in this diff:

  • None is UserSafe, at base or at head, and no T: UserSafe path reaches one. The branch's claim is about a UserSafe value's bytes, and that claim is true of the tree; the body and title do not say the tree has one unsafe byte view, and the body names the three.
  • The branch does not write, move or widen them: kernel/src/syscall/device.rs is not in the diff, and grant_bytes, record_bytes and the inline block are main's bytes. Nothing regressed: fourteen hand views became three.
  • Root CLAUDE.md gives a found compromise two outcomes and tells an agent to file what is off its task. Round 1 named filing as the legal outcome for what lay outside the fence. The file is legal against issues/README.md (defect, open, three fields), names its files, and its exit is one a build fails on: a padded struct under user_safe! is E0080.
  • Folding them in changes toyos-abi/src/pci.rs and the DMA grant path, which is device and ABI code: a new sysroot, the gates and the control again at a new source. Filing leaves the measured source frozen, which is what the evidence above rests on.

It would be a BLOCKER if the body claimed one byte view for the tree, or if the issue had no exit. Neither is so, once the NOTE above is corrected.

The round adds nothing else

  • git diff 14dd1b9a8 cc1a2c311 -- toyos-abi kernel toyos toyos-userbound is empty: the merge of std's ToyOS backend lives in sdk/std, and the fork names it by #[path] #745 touched none of this branch's source, and no file is touched by both sides. src/sysroot.rs and src/toolchain.rs still key on toyos-abi/src as a directory, so usersafe.rs is in the witness; build-x86_64.log:10,23,28,79,83 is the new sysroot with toyos-abi under std twice.
  • git grep usersafe-layouts 3861d296b and git grep 'user_ptr::UserSafe' 3861d296b find nothing: no citation of the deleted issue, no path to the moved trait.
  • bytes is sound as written: &T is readable for size_of::<T>(), u8 has alignment 1, the lifetime is the borrow's, and T: UserSafe is the no-padding half. UserRecords writes at written * bytes.len(), the stride it had.
  • No new dependency, feature, cfg, package, workflow, guest test or host tool. The two decoding tests kept hold field order, which the macro does not, and can fail on it.
  • Control at cc1a2c311: arm A E0080 ... a byte of 'Stat' belongs to no field at src/object/ops.rs:611, EXIT=101; arm B Compiling kernel, Build finished., EXIT=0. The mutation patches in comment 6047879819 are the same change on both arms.

SEND BACK

… 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
@Japabu

Japabu commented Oct 7, 2026 •

Copy link
Copy Markdown
Collaborator Author

Review, round 3, head 196ec3b67 against origin/main e16cb0841

Read, nothing run. Net lines (git diff --shortstat origin/main...196ec3b67): 630 insertions, 749 deletions, 24 files. Production (kernel/, toyos-abi/, toyos/): +544 −661, unchanged from round 2. Tests: +17 −29, unchanged. issues/: +69 −59. git diff --stat 3861d296b 196ec3b67 is issues/three-kernel-byte-views-still-rest-on-a-hand-layout-claim.md alone (8 insertions, 6 deletions); origin/main has not moved. Nothing else moved.

Round 2's findings

  • BLOCKER, no measurement of host and guest / suite at the landing head: CLOSED. Run 37696165075 (ci, pull_request), headSha 196ec3b67, all three jobs success; each job's checkout line is HEAD is now at cfed847 Merge 196ec3b67… into e16cb0841…, so what ran is the head merged onto the main it lands on. The three job logs were fetched with gh run view 37696165075 --job <id> --log and read; the fetched host log is byte-identical to the saved usersafe-r3/ci-host.log (cmp exit 0).
    • host (job 113048790048, step Run cargo run -- --ci host = success): line 9612 [ci] Host: 78 step(s), all green. Lines 5341-5349: Doc-tests toyos_abi, running 4 tests, usersafe::user_safe (line 53) - compile fail ... ok, (line 46) ... ok, (line 63) ... ok, (line 70) - compile fail ... ok, test result: ok. 4 passed; 0 failed. Four lines, both compile fail cases beside their twins: what round 2 asked. The log holds 67 ... FAILED lines, lines 6381-8026; every one lies inside a === [ci] control '<name>' step between kernel-loom without loom (22:37:49) and userland/acpiserver (22:39:11), each step closing N verdict(s) reached. They are the red arms of main's own controls and not a finding.
    • guest / suite (job 113050060747, step Run cargo run -- --ci guest = success, not skipped; runner label kvm: true, /dev/kvm opens): line 681 installed sysroot 6a6e3fbcd05fbd1c, then Compiling kernel and Compiling toyos-userbound at this head, running 30 tests, 1 wide, thirty RUN/PASS pairs and no other outcome, line 857 test result: ok. 30 passed, 30 total, line 862 [ci] Guest: 5 step(s), all green. The count is the one measured locally at cc1a2c311.
    • toolchain / build (job 113048790324): green, line 535 [ci] Toolchain: 1 step(s), all green, line 623 [ci] Bootstrap: 1 step(s), all green. At this head the job compiled no std: line 603 is Cache hit for: toolchain-sysroot-6a6e3fbcd05fbd1c and line 622 says sysroot 6a6e3fbcd05fbd1c restored. The compile round 2 asked for is in run 37694635644, job 113043227491, head 3861d296b (toolchain / build = success; the run's other two jobs were cancelled by the push): Cache not found for input keys: toolchain-sysroot-6a6e3fbcd05fbd1c, Building sysroot 6a6e3fbcd05fbd1c, Compiling std at 22:17:26 and 22:19:06 with Compiling toyos-abi v0.16.0 under each at 22:18:11 and 22:19:55, sysroot 6a6e3fbcd05fbd1c built, Bootstrap: 1 step(s), all green, Cache saved with key: toolchain-sysroot-6a6e3fbcd05fbd1c. The key is computed from the tree and names toyos-abi/src (src/sysroot.rs, src/toolchain.rs); the head computes the same key, git diff 3861d296b 196ec3b67 touches no source, and the guest suite built its kernel and passed 30 of 30 on that sysroot. I accept it as the measurement of the head's source. See the NOTE on how the body words it.
  • NOTE, the DeviceIrqRecord row: closed. The row now reads SIZE == 4, a total written by hand, and the paragraph says the same weakness of all three. Against the tree at the head: toyos-abi/src/pci.rs:65 size_of::<DmaGrant>() == 4 + 4 + 8 + 8, :82 size_of::<DmaMapping>() == 8 + 8, :100 SIZE = size_of::<Self>(), :103 assert!(DeviceIrqRecord::SIZE == 4); the three views are kernel/src/syscall/device.rs:280 (inline, DmaMapping), :308 (grant_bytes), kernel/src/object/ops.rs:491 (record_bytes), each SAFETY comment citing the assertion beside the declaration. Every statement in the file is true of the tree. Frontmatter open / defect / opened is legal against issues/README.md; the exit is one a build fails on.
  • NOTE, issues/clippy-stage-two-is-lints-one-at-a-time.md:486: stands as recorded, another track's file, disclosed in the body.

BLOCKER

None.

NOTE

  • PR body, "CI at 196ec3b67", toolchain / build — "success, which is toyos-abi with the macro compiled under std for both targets against sdk/std" is false of run 37696165075's log: that job restored sysroot 6a6e3fbcd05fbd1c from the cache and compiled no std. The compile is run 37694635644, job 113043227491, at 3861d296b, under the same key. Prose in a record; the orchestrator corrects the body to name both runs.

LAND

@Japabu
Japabu added this pull request to the merge queue Oct 7, 2026
Merged via the queue into main with commit 5e3332d Oct 7, 2026
3 checks passed
@Japabu
Japabu deleted the wt/toyos-usersafe branch October 7, 2026 23:22
Japabu added a commit that referenced this pull request Oct 8, 2026
…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
Japabu added a commit that referenced this pull request Oct 8, 2026
…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
github-merge-queue Bot pushed a commit that referenced this pull request Oct 8, 2026
…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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant