Repository navigation
Every write to SMI_CMD is made on the boot processor, by one function that reads its CPU beside the out - #748
Conversation
ACPI 6.5 Table 5.9 has OSPM issue commands to the SMI_CMD port "synchronously from the boot processor", and write ACPI_DISABLE "from the boot processor". acpi_mode wrote ACPI_ENABLE on the CPU its claimant minted on and ACPI_DISABLE on the CPU the claim's last handle went on: on the T14's acpi_server_death boots at ff4945d, 8d7004d and 922a6b7 the enable came from cpu7 and took. arch/x86_64/smi_cmd.rs now holds the port and the only `out` to it. A caller on another CPU asks a one-target round of Shootdown's protocol, the one a counters read asks of every CPU, kicks cpu0 and spins until cpu0 has answered it; cpu0 answers from its kick handler, or from a lock's spin where its interrupts are closed (tlb::poll), since the release runs under the dying process's data lock and a boot processor spinning on that lock with IF clear takes no kick. A boot processor that answers neither way in time::DEAF_CPU is a panic, the TLB shootdown's rule. The lock that kept a write out of the power-off moves with the write. The kick handler gains three instructions on a CPU that is not cpu0 and seven on cpu0 owing nothing, read off the built kernel. Both lines now say where the write ran, read on that CPU beside the `out`, which CPU asked, how long the `out` held the writer and its SMI count either side. The three T14 rows that boot through a write refuse a line that names any CPU but cpu0. Closes issues/the-kernel-writes-smi-cmd-from-whichever-cpu-its-caller-runs-on.md. Its note that nothing reads which SCI sources are enabled at the disable is filed as a finding of its own; that the release leaves SCI_EN to the hardware is said at `leave`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
|
The hand edit behind "the judge's number, on a hand-edited line" at s, n1 = re.subn(
r"written to SMI_CMD 0xb2, SCI_EN set (\d+ns) after; cpu(\d+)'s SMI count (\d+) before the write and (\d+) after",
lambda m: f"written to SMI_CMD 0xb2 on cpu{w}, asked from cpu{m.group(2)}; the write held cpu{w} 0ns, "
f"its SMI count {m.group(3)} before the write and {m.group(4)} after; SCI_EN set {m.group(1)} after", s)
s, n2 = re.subn(
r"ACPI_DISABLE 0xf1 written to SMI_CMD, PM1a_CNT",
f"ACPI_DISABLE 0xf1 written to SMI_CMD 0xb2 on cpu{w}, asked from cpu0; the write held cpu{w} 0ns, "
f"its SMI count 0 before the write and 0 after; PM1a_CNT", s)Substitutions made: The unedited readbacks, same command: |
|
Review of Net lines ( BLOCKER
NOTE
Asked of this round
SEND BACK |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
…claim is asked off the boot processor smi_cmd::answer reads which CPU it is on under its own interrupt guard and returns on any but the boot processor, so the check and the `out` are one CPU's whatever its caller holds. serve_here keeps only the relaxed hint, and write calls answer and kicks only where its round is still unserved: the `asked_from == BOOT` branch is gone. With no round in flight a kick or a lock's spin now pays two loads on every CPU, where a CPU other than the boot processor paid the load of its id. The acpi_release job claims from a thread that read its x2APIC id (CPUID leaf 0BH) non-zero as its first act, started in turn until one does, and the acpi_server_death judge refuses an enable asked from cpu0. Until now the crossing was the scheduler's habit: every recorded boot had the claimant on cpu7. Measured under QEMU with a temporary patch to stop_short on a two-CPU guest: of threads started in turn, one of the first four read a non-zero id and one of twenty-four read zero. The disable's asker is the killed server's last thread, which the job does not place; that and the answer from a lock's spin are filed as issues/no-t14-row-reads-an-acpi-disable-asked-off-the-boot-processor.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
|
The mutation review round 1 asks for, in the shape of the round-2 head --- a/kernel/src/arch/x86_64/smi_cmd.rs
+++ b/kernel/src/arch/x86_64/smi_cmd.rs
@@ -139,9 +139,6 @@
/// allocates nothing.
fn answer() {
let _closed = IrqGuard::close();
- if percpu::cpu_id() != BOOT {
- return;
- }
ROUND.serve_if_owed(BOOT as usize, || {
let port = PORT.get().expect("smi_cmd: a write asked of a machine whose FADT names no SMI_CMD").port(0);
let value = ASKED.load(Relaxed);The temporary patch behind the x2APIC measurement (applied, run with --- a/tests/common/power.rs
+++ b/tests/common/power.rs
@@ -76,7 +76,6 @@
);
let options = BootOptions {
qmp: true,
- smp: 1,
kernel_params: &["stop-budget-spent"],
extra_root_files: vec![("bin/test_rs_stop_short".to_string(), short)],
..Default::default()
--- a/tests/toyos-rust-tests/src/bin/stop_short.rs
+++ b/tests/toyos-rust-tests/src/bin/stop_short.rs
@@ -5,10 +5,14 @@
use toyos::power::{stop, Stop};
+#[path = "../arch/cpu.rs"]
+mod cpu;
+
fn main() {
- std::thread::spawn(|| loop {
- std::hint::spin_loop();
- });
+ let ids: Vec<u32> = (0..24)
+ .map(|_| std::thread::scope(|threads| threads.spawn(cpu::x2apic_id).join().expect("probe thread")))
+ .collect();
+ assert!(ids[..4].iter().any(|&id| id != 0) && ids.iter().any(|&id| id == 0), "PROBE {ids:?}");
let refused = stop(Stop::Shutdown);
panic!("stop_short: the power-off was refused: {refused:?}");
} |
|
T14 at
The kernel's lines:
So both writes crossed CPUs at the head (the enable from cpu1, the disable from cpu2), and the same job with the check removed writes where it was asked. |
|
Review of Net lines ( Round 1's BLOCKERs
Round 1's NOTEs
The host and guest suites at this headNot run locally. CI ran them on BLOCKERNone. NOTE
LAND AFTER NAMED CHANGES |
…records the job's window The file's slug, title and third paragraph said no machine had run the disable's wait under the dying process's lock. The T14's `acpicase` boot of 289906d did: its line reads `ACPI_DISABLE 0xf1 written to SMI_CMD 0xb2 on cpu0, asked from cpu2`, and the same head with the CPU check removed from `answer` (ca386ee, never to land) reads `on cpu2, asked from cpu2`. The crossing was the scheduler's doing: nothing arranges that asker, and round 1's boot at 448107b read `asked from cpu0`. So the file stays open and its slug says "arranges". It also takes a third item: the job's claiming thread reads its x2APIC id and then enters the kernel, and a preemption between that read and the kernel's lock can leave the claim asked from the boot processor. The two boots of the job did not hit it (`asked from cpu1`, `claimed from the CPU of x2APIC id 2` on both). The file names the judge's line such a red reads, so the first one is recognised and not re-run. No file under kernel/, tests/ or src/ changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
…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/the-kernel-writes-smi-cmd-from-whichever-cpu-its-caller-runs-on.md. Head307bef0f6, onorigin/maine16cb0841. Every T14 reading below is of289906d5b, andgit diff 289906d5b 307bef0f6 -- kernel tests srcis empty (git diff --quieton it exits 0; the output, zero bytes, isorch/smicmd-r3/kts.diff): the head adds one commit that touchesissues/alone, so the six boots stand for it. Log names are under the orchestrator's scratchpad,orch/.What changed, per decision
SMI_CMD, and it writes on cpu0. ACPI 6.5 Table 5.9 (read from the Internet Archive's capture of the specification; quoted in the module's header): "OSPM issues commands to the SMI_CMD port synchronously from the boot processor", and ofACPI_DISABLE, "writing ACPI_DISABLE to the SMI_CMD port from the boot processor".kernel/src/arch/x86_64/smi_cmd.rsdeclares the port and keeps it private, so theoutin itsansweris the only one the kernel can make to it;acpi_mode's enable and disable both go throughsmi_cmd::write.out.answercloses interrupts, reads which CPU it is on and returns on any but cpu0; the check and theoutare one CPU's whatever the caller holds.serve_herekeeps only the relaxedoweshint.writeissues its round, callsanswer, and kicks cpu0 and spins only where the round is still unserved; there is noasked_from == BOOTbranch.Shootdown's generation protocol answered from the kick handler. A caller off cpu0 stores the value, issues a round whose one target is cpu0, kicks cpu0 and spins until that round reads served; cpu0 writes from its kick handler, idle or busy. The tree has no affinity and no cross-call, and none is added.tlb::poll, one line).acpi_mode::releaseis reached fromops::close_allunder the dying process'sProcessDatalock, with preemption off. A cpu0 spinning on that lock withIFclear takes no kick;Lock::acquirealready polls TLB shootdowns for that spinner, and now polls this too.time::DEAF_CPU, and its expiry is a panic, the TLB shootdown's rule for a CPU that takes no interrupt.smi_cmd::writerefuses oncequiesce::begun(), under the locksmi_cmd::settlewaits out.outran on, the CPU that asked, how long theoutheld the writer, and the writer's SMI count either side.counters,acpi_server_events,acpi_server_death) refuse a line whose writer is not cpu0.acpi_server_deathrow's enable crosses CPUs by the job's doing.acpi_releasestarts threads in turn until one reads a non-zero x2APIC id (CPUID leaf 0BH,EDX) as its first act, and that thread claims; the judge refuses an enableasked from cpu0. 64 starts without one is the job's panic. Measured before building on it, under QEMU on a two-CPU TCG guest with a temporary patch tostop_short(restored;orch/smicmd-r2/probe3.patch,probe3.log): one of the first four threads started in turn read a non-zero id and one of twenty-four read zero, so the leaf answers for the CPU the thread is on and a spawn leaves cpu0 within four starts there.The T14, at
289906d5bSix unattended boots by the orchestrator (
orch/t14-748r2.log; the record is the pull request's comment 6048082355): each image's sha256 checked againstorch/smicmd-r2/images.sha256before its flash, the worktree clean at289906d5bbefore every boot and every judgement, every boot rc 0, nobody at the machine. Each judged bycargo test --test toyos-build -- --metal --metal-readback <dir> <filter>,<dir>underorch/smicmd-r2/.<dir>and filtershared,shared-debug,testcasesmetal-counters,counters[metal] 3 passed, 0 failed, 3 boot(s)metal-counters/judge-counters.logacpicase,testcases-holdmetal-acpi_server,acpi_server_[metal] 2 passed, 0 failed, 2 boot(s)metal-acpi_server/judge-acpi_server_.logacpicase, built fromca386eedbmutation/metal-acpi_server,acpi_server_death[metal] 0 passed, 1 failed, 1 boot(s)mutation/metal-acpi_server/judge-acpi_server_death.log(a)
counters:PASS counters. Itstestcasesboot's line:acpi: ACPI mode: ACPI_ENABLE 0xf0 written to SMI_CMD 0xb2 on cpu0, asked from cpu0; the write held cpu0 2078830ns, its SMI count 4823 before the write and 4824 after; SCI_EN set 13081ns afterThe writer is cpu0 and its SMI count moved by one across the write. The judge's
loaded: cpu0 kick late p50=0.1us p99=9.1us max=17.0us, beside the last recorded 0.2 / 9.6 / 22.9 µs and round 1's 0.3 / 9.1 / 16.6 µs at448107b54. Thesharedandshared-debugboots' lines read the same shape, cpu0 both, held 2049678 ns and 2063963 ns.(b)
acpi_server_death:PASS acpi_server_death. Itsacpicaseboot's two lines:acpi: ACPI mode: ACPI_ENABLE 0xf0 written to SMI_CMD 0xb2 on cpu0, asked from cpu1; the write held cpu0 2087832ns, its SMI count 4819 before the write and 4820 after; SCI_EN set 14127ns afteracpi: legacy mode again: ACPI_DISABLE 0xf1 written to SMI_CMD 0xb2 on cpu0, asked from cpu2; the write held cpu0 146295ns, its SMI count 4820 before the write and 4821 after; PM1a_CNT reads 0x0000 15084ns after, SCI_EN clearBoth name cpu0 as writer and both askers are off it, so the kick and the wait ran for each write;
SCI_ENread clear after the disable. The enable's asker is the job's doing: its first thread ended on cpu0 without claiming, its second claimed, and the job saidacpi_release: claimed from the CPU of x2APIC id 2, the kernel's cpu1. The disable's asker is the scheduler's doing; the judge prints it and requires nothing of it.(c)
acpi_server_events:PASS acpi_server_events. Itstestcases-holdboot's line:acpi: ACPI mode: ACPI_ENABLE 0xf0 written to SMI_CMD 0xb2 on cpu0, asked from cpu0; the write held cpu0 2052523ns, its SMI count 4817 before the write and 4818 after; SCI_EN set 12625ns afterThe boot held to its end untouched: one embedded-controller query, 0x4f, no button press and no Shutdown; the server's last word is
28 SCIs; embedded controller queries taken: 0x4f x14. Round 1's red of this row at448107b54was a person's press: the owner was at the machine for another measurement and, through the orchestrator's scheduling error, pressed the power button during that boot (query 0x28, the press served, the supervisor's Shutdown).What an SMI costs, and why cpu0 pays
Tracked readings (
issues/the-t14s-firmware-interrupts-every-cpu-every-2-2-s-under-toyos.md): the enable moved the SMI count on every CPU alike, so the firmware's handler stops all of them whichever CPU writes. Theoutdoes not retire until the handler returns, so the writer sits in it where the write was made: cpu0's kick handler or its own syscall, interrupts closed. Read at289906d5b: the enable'southeld cpu0 2.05 to 2.09 ms on five boots, the disable's 0.15 ms on one (0.22 ms on round 1's one). cpu0 is where device interrupts land, so they wait that long, as they do under every SMI. cpu0 pays because the specification names it and the firmware was written against that.The cost with no round in flight.
serve_hereis called from the kick handler and, throughtlb::poll, on every iteration of every contendedLockspin on every CPU. It is theoweshint: two relaxed loads, a compare and a branch, on every CPU alike. At448107b54it was a load of the CPU id, a test and a branch off cpu0, and those plus the hint on cpu0 (orch/smicmd/kick.dis, where the hint is the two loads,cmpandjbe). This head's kernel was not disassembled; the count is read from the source and that listing. While a round is in flight, about the 2 ms above, a kick or a spin on any CPU also closes interrupts and reads its CPU id inanswer. Thecountersjudge's kick lateness, in (a), reads no cost from it.Which tier reaches what
SMI_CMD. The guest suite shows only that the kick handler, the lock spin and the claim path still work with nothing owed.acpicaseboot's enable by the job's arrangement.Shootdown's, whichkernel/loomalready models.Checks (high-risk: the kernel, a device)
Independent oracle: the T14, above, at
289906d5b.The mutation, booted: red, exit 1. Commit
ca386eedb(branchwt/toyos-smicmd-mutation, never to land):289906d5bplus one hunk, three deletions insmi_cmd.rs, theif percpu::cpu_id() != BOOT { return; }out ofanswer, sowriteanswers its own round on the caller's CPU. The patch isorch/smicmd-r2/mutation.patchand the pull request's comment 6047876901. Itsacpicaseboot, judged byacpi_server_deathat the head: exit 1,FAIL acpi_server_death: SMI_CMD was written on cpu1, not on the boot processor: […] acpi: ACPI mode: ACPI_ENABLE 0xf0 written to SMI_CMD 0xb2 on cpu1, asked from cpu1; the write held cpu1 2050966ns, its SMI count 4822 before the write and 4823 after; SCI_EN set 16558ns afterand its disable reads
on cpu2, asked from cpu2, held 145058 ns. The asker was off cpu0, so the boot is a control: the same job on a kernel without the check writes where it was asked, and the judge refuses it.countersandacpi_server_eventsare not its judges: their asker is cpu0 unless the scheduler says otherwise.Negative control, on a recorded real failure (round 1, at
448107b54): that head's judges over the T14's readbacks of the kernel without the change (922a6b7c7, whoseacpicaseboot wrote the enable from cpu7):acpi_server_exit 1,countersexit 1 (orch/smicmd/negctl-acpi_server.log,negctl-counters.log). Red by the line's shape: that kernel's line names no writer.The judge's number, on a hand-edited line (round 1): the line rewritten naming cpu0 judges green, naming cpu7 red. Synthetic; the edit is the pull request's comment 6047204769.
Gates
host(cargo run -- --ci host)289906d5bguest / suite289906d5btoolchain / build289906d5b… --metal --metal-readback metal-counters counters289906d5borch/smicmd-r2/metal-counters/judge-counters.log… --metal --metal-readback metal-acpi_server acpi_server_289906d5borch/smicmd-r2/metal-acpi_server/judge-acpi_server_.log… --metal --metal-readback mutation/metal-acpi_server acpi_server_death289906d5b, the image fromca386eedborch/smicmd-r2/mutation/metal-acpi_server/judge-acpi_server_death.logcargo run -- --build-only289906d5b'sorch/smicmd-r2/build-wip.logcargo run -- --ci host448107b54orch/smicmd/host.logcargo test(the guest suite: 30 passed, 30 total)448107b54orch/smicmd/guest.logThe run's
headShais289906d5b. The three judges' exits are inorch/t14-748r2.log. Nothing was built or run locally at307bef0f6, by the owner's restriction on this machine's compute: it differs from289906d5binissues/alone, and itshostis the check CI runs on its push. Host load across round 1's guest suite: 15 to 42 (1-minute average).Recorded, not removed
issues/no-t14-row-arranges-an-acpi-disable-asked-off-the-boot-processor.md, three items:289906d5b's boot read itasked from cpu2, so the wait under the dying process's lock has run on the machine once, by the scheduler's doing; of the three boots whose line names the asker, round 1's readasked from cpu0. The judge prints the asker and does not require it: a judge that required a crossing no job arranges would be a flake.asked from cpu1,claimed from the CPU of x2APIC id 2on both). A boot that does reds the row asFAIL acpi_server_death: the job's claim was asked from the boot processor, so no CPU asked it for this write:over an enable lineon cpu0, asked from cpu0, beside the job's line naming a non-zero id; the file says so, so that red is recognised and not re-run.Its exit: a judge that refuses
asked from cpu0on the disable over a job that arranges it, a kernel line that names a spin's answer, and an asker held to its CPU from its read to the kernel's taking of the request.issues/nothing-reads-whether-an-sci-source-is-enabled-when-acpi-disable-is-written.md: the closed issue's one open note.issues/the-acpi-row-is-released-with-an-acpi-disable-the-firmware-has-not-answered.mdstays open and untouched: no tier reaches the handback's expiry.Where this differs from the stage design's K7 proposal
leavefall back past its bound. Here every caller spins and the expiry is a panic: one path, and no request left standing after its caller has gone.answer's own read of the CPU.Unsure of
asked from, so a machine where that is false reds the row and passes nothing.Shootdown::owesis a relaxed hint, as it is for the counters round's kick. A cpu0 that read it stale would ignore its kick and the caller would panic atDEAF_CPU. An interrupt's delivery orders it on x86; nothing models it.🤖 Generated with Claude Code
https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A