Skip to content

The kernel's random bytes come from a ChaCha20 generator keyed from the loader's seed and the CPU's sources, and 17 of the AArch64 guest tests run under HVF - #802

Merged
Japabu merged 6 commits into
mainfrom
wt/toyos-entropy
Oct 9, 2026

Conversation

@Japabu

@Japabu Japabu commented Oct 9, 2026 •

Copy link
Copy Markdown
Collaborator

The owner's wish: "i want the guest suite on this laptop to run arm." 19 of the 21 virt_ tests ran emulated only because an HVF guest has no RNDR and the AArch64 kernel had no other entropy. This is the entropy stage: 17 of what are now 24 virt_ tests run under HVF on an Apple host.

At the top: one HVF defect found, not fixed, four tests left emulated

A boot can power off without its last word on the console. virt_el1_smp, virt_mask_windows and virt_off_names_the_cpus_left_on were red under HVF six times, each STALLED: waiting for the boot's last word. A capture of one shows QEMU traced seven CPU_OFF and one SYSTEM_OFF from the guest: it powered off, its console's last line the supervisor's stop request, with klogd about 100 ms of guest clock behind. By reading, quiesce's drain after the last word (log::console::drain_inline) is a try-lock on the wire that declines while klogd holds it, and the power-off does not wait.

  • It is the console's and the stop's (kernel/src/syscall/machine.rs, kernel/src/log/console.rs), outside this stage's fence, so it is filed and not fixed: issues/the-boots-last-word-can-miss-the-console-when-klogd-holds-the-wire.md, with the counts, the capture, what separates it from the counters-read silences, and an exit.
  • Those three tests keep their emulated profiles, and so does virt_reboot, which waits for Rebooting. through the same stop and was never measured under HVF; it is also the one test of SYSTEM_RESET through the SMC conduit. Profile::VirtTcg stays for virt_el1_smp. 13 of the scout's 17 moved.
  • Measured at 7522ec61e, before the three went back. Rate: 5 reds in 131 side-by-side runs of the five SMP tests under HVF (393 last-word guests) at host load 28 to 46, and one more in a whole suite; none in 50 runs (150 guests) of the same tests emulated at EL1 in the same session. Those counts alone do not separate the two (Fisher, p = 0.33); the capture is what names the step.
  • Nothing else was red under HVF.

What changed, per decision

toyos-random, a new pure crate. The ChaCha20 block function of RFC 8439, and three types.

  • Seed: 32 bytes Seed::judge did not refuse. Refused: any other length, and bytes whose four 8-byte words are one value, which covers all zeros, all ones and a source stuck on one draw. Seed::take judges a seed another program handed over as a byte array and a length, and zeroes both whatever the judgment.
  • Generator: its one constructor takes a Seed, so an unkeyed generator is unrepresentable. mix XORs a further seed into the key and replaces the key with a ChaCha20 block of the result: mixed in, never substituted.
  • Stream: one draw. Generator::stream is fast key erasure: one block under the key, whose first half replaces the key and whose second keys the stream. The stream's block counter is 64 bits.
  • Why a crate of its own and not the kernel's library: the loader judges firmware's bytes with the same Seed::judge, so two programs share it, and it is the boundary where bytes from outside the kernel's trust are bounded by their form. Why written here: the brief's decision, a trust-boundary primitive in the kernel takes no community crate. No dependency.

The loader (bootloader/src/seed.rs) reads 32 bytes from EFI_RNG_PROTOCOL before ExitBootServices, opened GET_PROTOCOL so no driver is stopped, straight into KernelArgs. One line says whether it got them and never a byte. No protocol, a failed GetRNG or refused bytes hand the kernel none; none is an error. Both architectures.

KernelArgs gains loader_seed: [u8; 32] and loader_seed_len: u64 at the end; LAYOUT folds the size in, so an old loader is refused by the existing check. It no longer derives Debug, so no {:?} of it on either side can print the seed.

The kernel (kernel/src/random.rs, new). random::key runs once, after the boot's own lines and before hasher::seed:

  • takes the loader's seed through Seed::take from both the loader's own KernelArgs and the kernel's copy (kernel_main now takes &mut KernelArgs), which zeroes both, and mixes the copy's;
  • mixes each CPU source the architecture declares: RDSEED and RDRAND on x86-64, RNDR on AArch64, four draws each;
  • says each source mixed or not mixed, with the reason, and panics by name where nothing was mixed.
  • hasher::seed and SYS_RANDOM draw from it on both architectures. SYS_RANDOM no longer fails: SyscallError::Io for a dry RDRAND is gone.

Decided and stated:

  • Reseeding: out of scope. The key descends from boot alone. issues/the-kernels-generator-is-keyed-once-and-never-reseeded.md (renamed from every-random-byte-is-one-rdrand, whose slug the tree now refutes) stays open for reseeding, a jitter source, a statistical health test and the stack residue below.
  • Fork and clone safety: one lock, no per-process state. The generator is one Lock<Option<Generator>>. A draw holds it for one ChaCha20 block and expands its stream with the lock given back, on the calling thread, so no lock is held across the user copy and the work under the lock is bounded. No process holds generator state, so there is nothing a clone could duplicate. No kernel thread.
  • What a process learns of another's bytes: nothing. Each draw's bytes are ChaCha20 keystream under a key no other draw has, derived from a key replaced before the draw returns. Learning anything of another draw from them is distinguishing ChaCha20 from random.
  • A later memory disclosure does not give earlier draws: the key a draw was made under is overwritten before it begins. It does give every later draw until reboot: that is the reseeding owed.
  • RDSEED is new on x86-64, beside RDRAND. qemu64 under TCG has none, so on this host that path is the "not mixed" arm; CI's KVM guest and the T14 run the other. +rdseed was not added to the guest CPU: src/arch.rs's CPU string is outside the fence.

The harness.

  • -device virtio-rng-pci on the virt shape (a new Shape::rng), and on cargo run's virt. It is firmware's device: edk2's driver, read once. Measured: with iommu_platform=on QEMU refuses to start the transitional device, so it is passed plain.
  • x86-64 gets no virtio-rng. Measured with the scout's probe app on this host's QEMU: OVMF answers EFI_RNG_PROTOCOL on qemu64,+rdrand without the device. Whether CI's OVMF does under KVM is not measured; the kernel has RDRAND there either way and the loader's line says.
  • RDRAND keyed is asserted on x86-64: iommu_virtio_platform's netcase boots, Headless and HeadlessNoIommu, must say random: RDRAND is mixed into the generator's key. Firmware's seed alone keys the generator on this host, so before this no x86-64 test saw a CPU source go in. The CPU is qemu64,+rdrand under TCG and the host's under KVM, which has RDRAND too.
  • 13 tests move to Profile::Virt. The seven job-case boots are HVF boots; virt_jobs_at_el2 is one EL2 boot of the job case judging every job, so the track's stage-4 claim (timer and FP under the EL2 profile) stays held.
  • EL2 coverage the move gives up, accepted in the track's stage 4: virt_user_mode, virt_irq_storm, virt_timer_floor, virt_failed_ap_leaves_no_hole and virt_fatal_halts_the_others_first no longer boot entered at EL2. The drop from EL2 stays judged by virt_el2_drop, virt_smp and virt_jobs_at_el2, and PSCI through SMC by virt_smp (CPU_ON, CPU_OFF, SYSTEM_OFF) and virt_reboot (SYSTEM_RESET).
  • The PSCI-trace assertion stays TCG's, as on main: no traced test runs under HVF at this head. The last-word issue's exit says to widen it when its four tests move.
  • Profile::VirtNoRng: virt with no virtio-rng on a CPU with no RNDR (the host's under HVF, cortex-a72 emulated). Measured: cortex-a72 under TCG without the device has no protocol, and with it has.
  • random_draws on virt runs in the job case, which boots one CPU: its eight threads never put two CPUs on GENERATOR there. Draws from two CPUs at once are measured on the T14's shared boot alone: no guest test here or in CI runs random_draws on more than one CPU.

Trust

  • The seed is trusted for being secret and unpredictable, and that is assumed, not checked. It is length-checked and never parsed; only bytes whose four 8-byte words are one value are refused. A fixed 32-byte seed repeated on every boot cannot be detected at boot: under HVF it would key every boot alike, and only virt_random_differs would see it.
  • Firmware and the hypervisor already load the kernel and can read all of a guest's memory, so trusting their bytes adds nothing to the trusted base.
  • Under HVF the seed is the only source: ToyOS there is as random as the host's generator behind QEMU's virtio-rng.
  • Where the CPU has a source it is mixed with the seed. The mix is sound while the sources are independent; a source that could see the key and choose its bytes could cancel it, and such a CPU or firmware already holds the machine.
  • Not wiped: what the compiler spilled of a key to a stack frame or a register, the copy a Generator leaves when it is moved into its static, and whatever firmware keeps. The first two are in the reseeding issue.
  • The seed's zeroing is Seed::take's, held by a host test on every arm. That the kernel takes the loader's own copy as well as its own is one line held by reading: after mm::init drops the identity map, no oracle can read the loader's stack, and the line's deletion stays green.

Checks (high-risk: a security boundary and the boot ABI)

Head eb9b2bd25, which merges main at 558283168. Logs are this round's r6-*.log, kept outside the tree.

Gate Exit
cargo run -- --ci host (clippy with it) 0
cargo run -- --build-only 0
cargo run -- --build-only --arch aarch64 0
cargo test, the whole guest suite: 41 passed, 41 total (49.3s; workers: 175s building, 327s testing), load 15.65 at the start, 28.62 at the end 0
cargo test --test toyos-build -- --metal --metal-readback <dir> boot:testcases boot:shared: three images staged at this head, no machine touched 2, the readback's own
Mutation Test it must red Result
a rotation of the quarter round the RFC vectors red, exit 101
eight double rounds for ten the RFC vectors red, exit 101
a draw keeps its key the_key_a_draw_was_made_under_is_gone_when_it_returns, a_draw_is_the_blocks_the_module_states red, exit 101
mix substitutes the seed for the key every_seed_mixed_moves_the_draw_and_none_replaces_another, a_draw_is_the_blocks_the_module_states red, exit 101
a constant is a seed bytes_a_failed_source_leaves_are_no_seed red, exit 101
the stream's counter is 32 bits a_stream_does_not_repeat_where_32_bits_of_counter_end red, exit 101
a take leaves the seed where it was handed a_taken_seed_leaves_zeros_where_it_was_handed red, exit 101
x86-64 declares no CPU source iommu_virtio_platform red, exit 1: keyed from 1 source, the loader's
RDRAND's carry check inverted iommu_virtio_platform red, exit 1: RDRAND is not mixed: it had no data to give
the kernel keys from a constant in place of the loader's seed virt_random_differs (HVF) red, exit 1: two boots printed one draw
the kernel does not mix the loader's seed virt_user_mode (HVF) red, exit 1
the loader hands length 0 virt_user_mode (HVF) red, exit 1
the virt shape has no virtio-rng virt_user_mode red, exit 1
a draw writes nothing to its window virt_random_differs, by random_draws' own asserts red, exit 1
no source keys from a constant instead of panicking virt_no_seed_refused red, exit 1

The mutation "kernel keys from a constant" is red only where the CPU has no source of its own: under HVF. On a host that emulates virt with -cpu max, RNDR still differs per boot.

Why the new guest tests need QEMU

  • virt_random_differs: two boots of one image print different draws. Its subject is a guest whose only source is firmware's seed through edk2's virtio-rng driver, which exists only under QEMU with HVF; a host test boots nothing, and the T14 has RDRAND and one boot per row.
  • virt_no_seed_refused: a machine with no firmware protocol and no CPU source. No such metal exists here; the refusal's decision is two lines over a type that cannot be unkeyed, and the test holds that the kernel says each reason and stops before its first hash container.
  • virt_jobs_at_el2: no new behaviour. It is what seven EL2 boots held, in one.
  • random_draws (a shared Rust test, so a metal row on the T14 too): what one boot's draws owe, in the guest: exact window, no two of 8000 concurrent draws alike, no gross bias.
  • The RDRAND line in iommu_virtio_platform adds no boot: it reads a boot that test already makes. The T14 reads the same line, but nothing on metal runs where firmware's seed alone keys the generator and a CPU source can be lost unseen beside it, which is this host's TCG.

The measurement the owner asked for

Suite wall time and the workers' split, from the suite's own summary line, with uptime's 1-minute load. The suite is build-bound, and a test's time includes waiting on a shared image build, so the first run of an arm measures the build.

Arm Run Tests Wall Workers building Workers testing Load at start
main at 1621281ae first 37 passed 154.6 s 1215 s 531 s 34.50
main at 1621281ae second 37 passed 42.7 s 201 s 232 s 38.42
this branch at 7c05a8dd4 first 40 passed 154.5 s 1193 s 600 s 31.59
this branch at 7c05a8dd4 second 40 passed 41.9 s 167 s 279 s 47.36

One session, the four runs back to back in one worktree, each exit 0. With three more tests and 18 boots under HVF where 2 were, the suite takes the time it took. At this head, with virt_reboot emulated again, 17 boots run under HVF.

Single boots alone, images built, same session, load 36 to 63: virt_timer_preempts under HVF 3 s of testing against virt_jobs_at_el2 emulated 4 s; virt_el1_smp under HVF 3 s against virt_smp emulated 3 s. A boot is two to four seconds either way: HVF does not make this suite measurably faster, it makes it run on the host's own CPU.

Not measured

  • The T14 at this head: staged, not run (testcases, shared, which carries random_draws, and testcases-watchdog).
  • CI: whether its OVMF answers the protocol under KVM.
  • cargo run --arch aarch64: the device was added to its shape and probed with an EFI app on that machine (virt, SMMUv3, HVF: the protocol answers), and the dev loop itself was not launched.
  • Of the track's owed cache and TLB items: programs are mapped executable through cache::make_executable on every HVF boot with no red, which is no proof; which tests replace a live entry is not measured, and no test reclaims an ASID. Stage 4 now names the guest test that closes each.

Records

  • issues/toyos-runs-on-arm64.md: the owner's ruling of 2026-10-09 on the default architecture, with its stage-7 exit item; stage 4 brought to what this stage did, the EL2 entries it gave up accepted, and each owed cache and TLB step given the test that closes it; stage 5's AP clean likewise; stage 6 records SMCCC TRNG absent and the rng done without a ToyOS driver.
  • issues/the-boots-last-word-can-miss-the-console-when-klogd-holds-the-wire.md: new, as above, virt_reboot in its list and its exit.
  • issues/edk2-stable202502-to-202608-hangs-at-exitbootservices-under-hvf.md: a QEMU upgrade bundling an affected edk2 now reds most virt_ tests, not two.

Net against main at 558283168: 28 files, +1224 −167. Production (loader, kernel, toyos-abi, toyos-random, the dev loop's shape, manifests, Cargo.lock): +536 −73, which is the generator and its keying where there was one instruction per architecture. Tests: +513 −61. Issues: +175 −33.

🤖 Generated with Claude Code

https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C

Japabu and others added 4 commits October 9, 2026 12:21
…he loader's seed and the CPU's sources, and the AArch64 guest tests run under HVF

An HVF guest has no RNDR, and the AArch64 kernel had no other entropy: its
hash seed panicked there and sys_random executed RNDR unconditionally, so 19
of the 21 virt_ tests ran emulated.

toyos-random is the generator, pure: the ChaCha20 block function of RFC 8439,
held to its test vectors; a Seed that 32 bytes become only when their four
words are not one value; a Generator that exists only keyed, mixes every
further seed by XOR into the key and a block, and replaces its key at every
draw.

The loader reads 32 bytes from EFI_RNG_PROTOCOL before ExitBootServices,
judges them with the kernel's own judgment and hands them in KernelArgs,
saying in one line whether it got them. The kernel mixes them with RDSEED and
RDRAND, or RNDR, zeroes the field in the loader's copy and its own, and
refuses by name a machine where nothing was mixed. hasher::seed and
SYS_RANDOM draw from it on both architectures.

The virt shape gains a virtio-rng, which edk2 drives for the protocol. 17
tests move to Profile::Virt, VirtTcg goes, the PSCI trace is taken under HVF
too, and one boot of the job case stays at EL2 with every job judged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
… entropy stage

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
…VF it can miss the console

virt_el1_smp, virt_mask_windows and virt_off_names_the_cpus_left_on were red
under HVF six times in this stage's runs, each stalled waiting for
"Shutting down.". A capture of one shows QEMU traced seven CPU_OFF and one
SYSTEM_OFF from the guest: it powered off with its console's last line the
supervisor's stop request and klogd about 100 ms behind. quiesce's drain
after the last word is a try-lock on the wire that declines while klogd holds
it, and the power-off does not wait.

That is the console's and the stop's, outside this stage's fence, and is
filed with the evidence. The three keep their emulated profiles, and
Profile::VirtTcg stays for virt_el1_smp.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
…he AArch64 ROOT, into the entropy stage

The track's owner rulings are main's, his words and no more; what the
default architecture is in the tree as it stands moves out of them into a
section of its own, and stage 7's exit keeps the flip beside main's frames.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
@Japabu

Japabu commented Oct 9, 2026

Copy link
Copy Markdown
Collaborator Author

Mutation patches, the negative control and the probes behind this pull request's body, each as applied with git apply. Run by mutations/run.sh's shape: git apply --check, apply, the named command, its exit, git apply -R, tree clean.

Negative control, against the base 5055dc4aa (cargo test virt_user_mode, exit 1):

--- a/tests/toyos.rs
+++ b/tests/toyos.rs
@@ -230,7 +230,7 @@
     ("virt_early_panic", qemu::Profile::Virt),
     ("virt_early_fault", qemu::Profile::Virt),
     ("virt_el2_drop", qemu::Profile::VirtEl2NoVhe),
-    ("virt_user_mode", qemu::Profile::VirtEl2),
+    ("virt_user_mode", qemu::Profile::Virt),
     ("virt_timer_preempts", qemu::Profile::VirtEl2),
     ("virt_irq_storm", qemu::Profile::VirtEl2),
     ("virt_timer_floor", qemu::Profile::VirtEl2),

h1-rotation (cargo test -p toyos-random, red, exit 101):

--- a/toyos-random/src/chacha.rs
+++ b/toyos-random/src/chacha.rs
@@ -15,7 +15,7 @@
     s[a] = s[a].wrapping_add(s[b]);
     s[d] = (s[d] ^ s[a]).rotate_left(8);
     s[c] = s[c].wrapping_add(s[d]);
-    s[b] = (s[b] ^ s[c]).rotate_left(7);
+    s[b] = (s[b] ^ s[c]).rotate_left(9);
 }
 
 /// The 64-byte block `key`, `counter` and `nonce` give, into `out`. Both

h2-rounds (cargo test -p toyos-random, red, exit 101):

--- a/toyos-random/src/chacha.rs
+++ b/toyos-random/src/chacha.rs
@@ -32,7 +32,7 @@
     }
 
     let mut state = initial;
-    for _ in 0..10 {
+    for _ in 0..8 {
         quarter_round(&mut state, 0, 4, 8, 12);
         quarter_round(&mut state, 1, 5, 9, 13);
         quarter_round(&mut state, 2, 6, 10, 14);

h3-draw-keeps-its-key (cargo test -p toyos-random, red, exit 101):

--- a/toyos-random/src/lib.rs
+++ b/toyos-random/src/lib.rs
@@ -125,7 +125,6 @@
     pub fn stream(&mut self) -> Stream {
         let mut block = [0u8; 64];
         chacha::block(&self.key, 0, &DRAW, &mut block);
-        self.key.copy_from_slice(&block[..SEED_LEN]);
         let mut stream = Stream { key: [0; SEED_LEN], next: 0 };
         stream.key.copy_from_slice(&block[SEED_LEN..]);
         wipe(&mut block);

h4-mix-substitutes (cargo test -p toyos-random, red, exit 101):

--- a/toyos-random/src/lib.rs
+++ b/toyos-random/src/lib.rs
@@ -112,7 +112,7 @@
 
     pub fn mix(&mut self, seed: Seed) {
         for (key, seed) in self.key.iter_mut().zip(&seed.0) {
-            *key ^= seed;
+            *key = *seed;
         }
         let mut block = [0u8; 64];
         chacha::block(&self.key, 0, &MIX, &mut block);

h5-constant-is-a-seed (cargo test -p toyos-random, red, exit 101):

--- a/toyos-random/src/lib.rs
+++ b/toyos-random/src/lib.rs
@@ -75,10 +75,6 @@
         let Ok(bytes) = <&[u8; SEED_LEN]>::try_from(bytes) else {
             return Err(Refusal::Length(bytes.len()));
         };
-        let (words, _) = bytes.as_chunks::<8>();
-        if words.iter().all(|word| *word == words[0]) {
-            return Err(Refusal::Constant);
-        }
         Ok(Seed(*bytes))
     }
 }

h6-counter-is-32-bits (cargo test -p toyos-random, red, exit 101):

--- a/toyos-random/src/lib.rs
+++ b/toyos-random/src/lib.rs
@@ -152,7 +152,6 @@
     pub fn fill(&mut self, out: &mut [u8]) {
         for chunk in out.chunks_mut(64) {
             let mut nonce = [0u8; 12];
-            nonce[..4].copy_from_slice(&((self.next >> 32) as u32).to_le_bytes());
             let mut block = [0u8; 64];
             chacha::block(&self.key, self.next as u32, &nonce, &mut block);
             self.next += 1;

g1-kernel-keys-from-a-constant (cargo test --test toyos-build -- virt_random_differs, red, exit 1):

--- a/kernel/src/random.rs
+++ b/kernel/src/random.rs
@@ -61,7 +61,7 @@
 
     match usize::try_from(copy.loader_seed_len).ok().and_then(|len| copy.loader_seed.get(..len)) {
         Some([]) => log!("random: {LOADER} is not mixed: the loader handed none"),
-        Some(bytes) => mix(LOADER, Seed::judge(bytes)),
+        Some(_) => mix(LOADER, Seed::judge(b"a seed all boots of an image had")),
         None => mix(LOADER, Err(Refusal::Length(copy.loader_seed_len as usize))),
     }
     for args in [loader, copy] {

g2-kernel-ignores-the-loaders-seed (cargo test --test toyos-build -- virt_user_mode, red, exit 1):

--- a/kernel/src/random.rs
+++ b/kernel/src/random.rs
@@ -61,7 +61,7 @@
 
     match usize::try_from(copy.loader_seed_len).ok().and_then(|len| copy.loader_seed.get(..len)) {
         Some([]) => log!("random: {LOADER} is not mixed: the loader handed none"),
-        Some(bytes) => mix(LOADER, Seed::judge(bytes)),
+        Some(_) => log!("random: {LOADER} is not mixed: mutated"),
         None => mix(LOADER, Err(Refusal::Length(copy.loader_seed_len as usize))),
     }
     for args in [loader, copy] {

g3-loader-hands-no-seed (cargo test --test toyos-build -- virt_user_mode, red, exit 1):

--- a/bootloader/src/main.rs
+++ b/bootloader/src/main.rs
@@ -632,7 +632,7 @@
         loader_seed: [0; toyos_abi::boot::SEED_LEN],
         loader_seed_len: 0,
     };
-    kernel_args.loader_seed_len = seed::read(&system_table, &mut kernel_args.loader_seed);
+    let _ = seed::read(&system_table, &mut kernel_args.loader_seed);
     report_reach(
         "Kernel arguments",
         &kernel_args as *const KernelArgs as u64,

g4-virt-has-no-rng (cargo test --test toyos-build -- virt_user_mode, red, exit 1):

--- a/tests/common/qemu.rs
+++ b/tests/common/qemu.rs
@@ -877,7 +877,7 @@
                 usb: &[],
                 nvme_bytes: 0,
                 iommu: None,
-                rng: true,
+                rng: false,
             },
             Self::Headless => Shape {
                 vga: "none",

g5-a-draw-writes-nothing (cargo test --test toyos-build -- virt_random_differs, red, exit 1):

--- a/kernel/src/random.rs
+++ b/kernel/src/random.rs
@@ -116,7 +116,6 @@
     while at < out.len() {
         let n = (out.len() - at).min(chunk.len());
         stream.fill(&mut chunk[..n]);
-        out.write_at(at, &chunk[..n]);
         at += n;
     }
     wipe(&mut chunk);

g6-no-source-keys-from-a-constant (cargo test --test toyos-build -- virt_no_seed_refused, red, exit 1):

--- a/kernel/src/random.rs
+++ b/kernel/src/random.rs
@@ -86,12 +86,7 @@
         wipe(&mut bytes);
     }
 
-    let Some(generator) = generator else {
-        panic!(
-            "random: nothing keyed the generator: the loader handed no seed and this CPU has no random \
-             source this kernel draws from, so no byte it gave out would be random"
-        )
-    };
+    let generator = generator.unwrap_or_else(|| Generator::keyed(Seed::judge(b"a seed all boots of an image had").ok().expect("a seed")));
     log!("random: the generator is keyed from {mixed} source(s), and every random byte is its ChaCha20");
     assert!(GENERATOR.lock().replace(generator).is_none(), "random: key() ran twice in one boot");
 }

The emulated arm of the SMP loop at 7522ec61e (Profile::Virt under TCG at EL1; 50 runs of the five SMP tests, no red):

--- a/tests/common/qemu.rs
+++ b/tests/common/qemu.rs
@@ -729,7 +729,7 @@
     /// How this host provides the machine.
     pub fn accel(self) -> Accel {
         match self {
-            Self::VirtEl2 | Self::VirtEl2NoVhe => Accel::Tcg,
+            Self::Virt | Self::VirtEl2 | Self::VirtEl2NoVhe => Accel::Tcg,
             _ => self.arch().accel(),
         }
     }

debug-shutdown-regs.patch, the probe that caught the powered-off guest with no last word, against 7522ec61e. A debug instrument, not for landing:

--- a/tests/toyos.rs
+++ b/tests/toyos.rs
@@ -2054,17 +2054,37 @@
 fn virt_mask_windows(profile: qemu::Profile) -> Result<(), String> {
     let mut qemu = boot_virt_smp(BootOptions {
         profile,
+        psci_trace: Some(common::lane::dir().join("virt_mask_windows.psci")),
         smp: VIRT_CPUS,
         kernel_features: toyos_build::build::MASK_WINDOWS_KERNEL,
         ..Default::default()
     });
     let mut serial = virt_console(&qemu);
     judge_virt_job(&mut qemu, &mut serial, "unmap_touch", UNMAP_TOUCH_SAID)?;
-    // To the boot's last word, said after every report: the drain that took the job's end can stop inside one.
-    await_marker(&mut qemu, &mut serial, power::SHUTTING_DOWN, "the boot's last word")?;
+    if let Err(e) = await_marker(&mut qemu, &mut serial, power::SHUTTING_DOWN, "the boot's last word") {
+        let said = serial.clone();
+        return Err(debug_capture(&mut qemu, &e, &said, &common::lane::dir().join("virt_mask_windows.psci")));
+    }
     mask_windows(&serial, VIRT_CPUS)
 }
 
+/// What a guest whose last word never came said from the read on, what it said late, and what QEMU
+/// traced of its power-off.
+fn debug_capture(qemu: &mut QemuInstance, e: &str, said: &str, trace: &Path) -> String {
+    let late = qemu.drain_serial(Duration::from_millis(500));
+    let from = said.find("===TEST_START test_rs_counters_read").unwrap_or(0);
+    let tail: Vec<&str> = said[from..].lines().filter(|l| !l.contains("kernel tid=")).collect();
+    let traced = fs::read_to_string(trace).unwrap_or_default();
+    let count = |function: &str| traced.lines().filter(|l| l.contains(function)).count();
+    format!(
+        "{e}\nDEBUG-CONSOLE\n{}\nDEBUG-LATE\n{late}\nDEBUG-PSCI {} lines traced: {} CPU_OFF, {} SYSTEM_OFF\nDEBUG-END",
+        tail.join("\n"),
+        traced.lines().count(),
+        count("x0=0x0000000084000002"),
+        count("x0=0x0000000084000008"),
+    )
+}
+
 /// Boot `tests/virtsmpcase` as `options` say.
 fn boot_virt_smp(options: BootOptions) -> QemuInstance {
     let config = compile::repo_root().join("tests/virtsmpcase/system.toml");
@@ -2138,8 +2158,13 @@
     eprintln!("  [virt] {VIRT_CPUS} CPUs entered at EL{el}, started through {conduit}, and scheduling");
     judge_virt_job(&mut qemu, &mut serial, "test_rs_counters_read", COUNTERS_READ_SAID)?;
     judge_virt_job(&mut qemu, &mut serial, "test_rs_trace_read", TRACE_READ_SAID)?;
-    let (console, calls) =
-        ended_through_psci(&mut qemu, &mut stop, serial, power::SHUTTING_DOWN, "guest-shutdown", &trace, |_| Vec::new())?;
+    let said = serial.clone();
+    let ended =
+        ended_through_psci(&mut qemu, &mut stop, serial, power::SHUTTING_DOWN, "guest-shutdown", &trace, |_| Vec::new());
+    let (console, calls) = match ended {
+        Ok(ended) => ended,
+        Err(e) => return Err(debug_capture(&mut qemu, &e, &said, &trace)),
+    };
     let record = console
         .lines()
         .find_map(toyos_quiesce::Record::parse)
@@ -2328,7 +2353,11 @@
             .map(|cpu| format!("power: cpu{cpu} is not off by PSCI's answer inside the budget; SYSTEM_OFF regardless"))
             .collect()
     };
-    let (_, calls) = ended_through_psci(&mut qemu, &mut stop, serial, power::SHUTTING_DOWN, "guest-shutdown", &trace, named)?;
+    let said = serial.clone();
+    let (_, calls) = match ended_through_psci(&mut qemu, &mut stop, serial, power::SHUTTING_DOWN, "guest-shutdown", &trace, named) {
+        Ok(ended) => ended,
+        Err(e) => return Err(debug_capture(&mut qemu, &e, &said, &trace)),
+    };
     let left_on = spared(&calls);
     let last = psci_powered_off(&calls, VIRT_CPUS, &left_on)?;
     eprintln!(

@Japabu

Japabu commented Oct 9, 2026

Copy link
Copy Markdown
Collaborator Author

Review, round 1, head 863d73ed3 against main at 1621281ae.

Net: 27 files, +1157 −170. Production +522 −72 (the body says +524; Cargo.lock counts as production here), tests +486 −64, issues +149 −34. The production growth replaces one instruction per architecture with a generator, its keying and the loader's seed, and I accept that reason. I checked the RFC 8439 tests against the RFC's text as fetched: §2.1.1, §2.2.1, §2.3.2 and all five appendix A.1 blocks are the RFC's own bytes, not constants the code produced. I checked eight of the twelve mutations (h1, h3, h4, h5, h6, g1, g3, g6) and each is red for the stated reason at this head. The negative control's log shows the RNDR panic.

BLOCKER

  • kernel/src/random.rs:67 — The claim that the seed is zeroed in both copies has no test that can fail on it. Run this patch: delete the loop for args in [loader, copy] { wipe(&mut args.loader_seed); args.loader_seed_len = 0; }. Every host and guest test stays green. The body says as much ("held by reading alone"). On a security boundary that is a claim no test can fail on. A host test is cheap: move the take-judge-wipe of a (&mut [u8; 32], &mut u64) pair into toyos-random (for example a Seed::take), have the kernel call it for both copies, and add a host test asserting both fields are zero after the take, on the accepted arm and on the refused arm. The patch above must then turn that test red.
  • kernel/src/arch/x86_64/entropy.rs:12 — No test anywhere sees an x86-64 CPU source mixed. Run this patch: pub const SOURCES: &[Source] = &[];. On this host's TCG, OVMF answers EFI_RNG_PROTOCOL (measured, per the body), so the loader's seed keys the generator and every x86-64 test stays green. No x86-64 boot asserts random: RDRAND is mixed into the generator's key. The same blind spot covers an inverted setc check in rdseed: the source would read as "had no data", the boot would go on, and nothing would turn red. The test the patch must turn red: an existing x86-64 guest test (the cheapest is a check on the boot log of one Headless boot, whose CPU is qemu64,+rdrand) asserting random: RDRAND is mixed. The T14 reading must also show RDSEED mixed (see below).
  • tests/toyos.rs:261 — virt_reboot stays under HVF on a guess. It ends through the same quiesce → drain_inline (a try-lock on the wire) → reset path that the filed issue names. Under HVF that path lost the last word on three sibling tests, and the stop's own records (the census, the stop record, the last word itself) wake klogd inside that window whatever the case reported before. The PR moved three tests back after measuring them and left this one where it is unmeasured. Do one of these two, in this diff:
    1. Move it to Profile::VirtEl2 and name it in the issue's "Until it is fixed" list.
    2. Measure it the way the other three were measured: at least 131 side-by-side runs under Profile::Virt at load 30 or above, none red, posted with command, exit code and log.
  • PR body, "Negative control" — The control was run on the wrong base. It ran on 5055dc4aa. The green arm was measured at 863d73ed3, whose base is 1621281ae. Root CLAUDE.md asks for the whole change reverted onto the base the green arm was measured on. Re-run cargo test virt_user_mode with only that test's profile moved to Profile::Virt on 1621281ae, and post its exit code and the refusal line.
  • The T14 reading is missing. The change runs RDSEED for the first time and reads EFI_RNG_PROTOCOL on real firmware, so it targets hardware, and QEMU is not the hardware. The three images are staged only. What the reading has to show is set out under "The T14" below.

NOTE

  • tests/toyos.rs:244-263 — Moving these tests to EL1 under HVF loses EL2 and SMC coverage the body does not account for.
    • SYSTEM_RESET through the SMC conduit is now judged by no test. virt_reboot was its only test and now runs on HVC.
    • virt_irq_storm, virt_timer_floor, virt_failed_ap_leaves_no_hole, virt_fatal_halts_the_others_first and virt_user_mode no longer boot entered at EL2. virt_jobs_at_el2 covers the timer and FP jobs, not those.
    • Either name these as accepted in the track's stage 4, or keep the reset's SMC arm (for example as one more EL2 row).
  • tests/toyos-rust-tests/src/bin/random_draws.rs:45 — On virt the job case boots with smp: 1, so "8 threads drawing at once" never puts two CPUs on GENERATOR there. Draws from two CPUs at once are measured only by the T14's shared row and CI's x86-64 guest. The body should say so.
  • issues/toyos-runs-on-arm64.md:302-312 — Stage 4's owed items no longer have an exit. These are cache::make_executable, break-before-make and the ASID flush. The sentence "the first HVF run … is their exit" is deleted, and the new text says that run "is no proof". Nothing names what will close them now. A stage item needs an exit something can read.
  • issues/the-boots-last-word-can-miss-the-console-when-klogd-holds-the-wire.md, "Not known" — The issue can separate itself from the counters-read silences, and does not:
    • The three and four silences in issues/a-counters-read-under-host-load-can-go-silent-for-15-s.md are a different step. Each console stops at spawn: …test_rs_counters_read, with no TEST_END of the read and no supervisor stop line. In this defect's capture, every queued userland line through power: the machine stops … reached the wire, because drain_for_the_stop waits for the wire and drains the queue. Only the kernel's records went missing.
    • The two console-less virt_mask_windows stalls stay undecidable. Neither had a PSCI trace.
    • Say this, and name the discriminator for the next capture: whether TEST_END test_rs_counters_read and the supervisor's stop line are present, plus the PSCI trace.
  • Same issue — "The probe that captured it is a patch on the entropy stage's pull request" points at nothing a reader can resolve. Name the comment's URL.
  • Same issue, exit — The exit's "300 side-by-side runs" should also cover virt_reboot if the BLOCKER above keeps it under HVF.
  • toyos-abi/src/boot.rs:2 — KernelArgs derives Debug, so one {:?} of the struct on either side prints the seed. Today nothing does, and this is held by reading. Either give the seed field a newtype whose Debug prints its length only, or drop the derive.
  • PR body, "Trust" — "a constant is refused" is false as written. Only bytes whose four 8-byte words are equal are refused. A fixed 32-byte seed that repeats on every boot cannot be detected at boot. Under HVF it would key every boot alike, and only virt_random_differs would see it. The trust statement is therefore: secret and unpredictable is assumed, not checked.
  • PR body, evidence — r5-chain.txt records the metal readback step as EXIT=1 for boot:shared-2. The kept r5-metal-readback.log is a later run with boot:shared, exit 2. The staging is fine; the body's citation is to an overwritten log.

What collides

The T14

What the reading must show, from the staged testcases, shared and testcases-watchdog boots:

  • Rows: every row green, test_rs_random_draws in shared among them.
  • Loader: exactly one Seed: line per boot, of one of three kinds:
    • 32 bytes from EFI_RNG_PROTOCOL …
    • firmware has no EFI_RNG_PROTOCOL … — fine, record it.
    • a GetRNG failed or refused line — not a send-back by itself, but recorded in the tracker with the status.
  • Kernel:
    • random: RDSEED is mixed into the generator's key and random: RDRAND is mixed into the generator's key.
    • The loader's seed mixed or "handed none", consistent with the Seed: line.
    • random: the generator is keyed from N source(s), with N = 3, or 2 with no protocol.
  • Nothing secret: no seed byte, no key, no line from seed.rs or random.rs carrying hex. The one draw random_draws prints is throwaway output and is fine on the console, but it is not posted.

What sends it back:

  • any row red or any panic;
  • RDSEED or RDRAND "not mixed" on this CPU (it has both), which is a carry-flag or retry defect;
  • more than one Seed: line, or a source line missing.

No further boot is owed: random_draws already makes 8000 draws from eight threads across the T14's CPUs.

What the landing head's checks must show

host, toolchain / build (the toyos-abi change rebuilds the toolchain) and guest / suite must all be green at the head that lands. Under KVM on the Linux runners, guest / suite runs x86-64 with the runner's RDRAND and RDSEED. Read its logs for both random: source lines, since until the BLOCKER above lands no test asserts them. The virt_ tests there run under TCG at EL1 on -cpu max, where RNDR and the loader's seed are both mixed. That leaves these measured only on this Mac:

  • a generator keyed from the loader's seed alone;
  • the two-boot difference that mutation g1 turns red;
  • the PSCI trace under HVF;
  • every program mapped executable under HVF.

The orchestrator may land on reading those checks together with the T14 reading, once the BLOCKERs above are closed.

SEND BACK

@Japabu

Japabu commented Oct 9, 2026

Copy link
Copy Markdown
Collaborator Author

The T14 at 863d73ed3 (the orchestrator's reading; worktree clean before and after, each image's sha256 checked against the request in the command that flashed it). Three boots (testcases, shared, testcases-watchdog), each toyos-metal exit 0; judged with --metal --metal-readback <dir> boot:testcases boot:shared: exit 0, 112 passed, 0 failed, 3 boots, test_rs_random_draws among them.

On both testcases and shared, the loader and the kernel said, in order:

  • Seed: 32 bytes from EFI_RNG_PROTOCOL for the kernel's generator (one such line per boot): the T14's firmware has the protocol.
  • random: the loader's seed is mixed into the generator's key
  • random: RDSEED is mixed into the generator's key
  • random: RDRAND is mixed into the generator's key
  • random: the generator is keyed from 3 source(s), and every random byte is its ChaCha20

No line in any kernel or loader log carries a seed, a key or a drawn byte. RDSEED ran on this CPU for the first time and keyed the generator.

This is the hardware half of round 1's T14 item; the round's other BLOCKERs change source, so the head that lands is booted again.

Japabu and others added 2 commits October 9, 2026 14:44
…796), into the entropy stage

No hunk conflicted. The Headless shape main brings (one NVMe disk, no
stick) already carries `rng: false`, and no Shape literal it adds lacks
the field.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C
…d on x86-64; virt_reboot stays emulated at EL2

Round 1's review of the entropy stage, finding by finding:

- `Seed::take` judges a handed seed and zeroes the bytes and length it
  was handed in on every arm: accepted, refused for its bytes, refused
  for its length, and none handed. The kernel takes both copies through
  it, the loader's own arguments and its copy of them. A host test
  holds the zeros on each arm, so deleting the wipe is now red.
- `iommu_virtio_platform`'s netcase boot, a Headless machine whose CPU
  is `qemu64,+rdrand` under TCG and the host's under KVM, says
  `random: RDRAND is mixed into the generator's key`. On this host
  firmware answers EFI_RNG_PROTOCOL, so its seed alone would key the
  generator and no x86-64 test saw a CPU source go in.
- `virt_reboot` goes back to `Profile::VirtEl2`: it ends through the
  same stop whose last word HVF lost on three sibling tests, it was
  moved unmeasured, and it is the one test of SYSTEM_RESET through the
  SMC conduit. The last-word issue names it in its list, and its exit
  covers it.
- With no PSCI-traced test left under HVF, the trace's assertion is
  TCG's again, as on main; the last-word issue's exit says to widen it
  when the four move.
- `KernelArgs` derives no `Debug`: a `{:?}` of it would print the seed.
- The last-word issue is told apart from the counters-read silences,
  names what the next capture has to show, and cites the probe's
  comment. The track's stage 4 accepts the EL2 entries the move to HVF
  gave up and names the test that closes each owed cache and TLB step;
  stage 5's AP clean gets the same.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C
@Japabu

Japabu commented Oct 9, 2026

Copy link
Copy Markdown
Collaborator Author

Round 2's mutation patches and negative control, at head eb9b2bd25, each as applied with git apply: git apply --check, apply, the named command, its exit, git apply -R, tree clean after each.

Negative control, against main at 558283168 (the base of this head; cargo test --test toyos-build -- virt_user_mode, exit 1, EARLY PANIC: panicked at kernel/src/hasher.rs:35:9: kernel hasher: ID_AA64ISAR0_EL1.RNDR is zero, so this CPU has no RNDR, and virtio-rng is the port's stage 6, and the seed has no other source):

--- a/tests/toyos.rs
+++ b/tests/toyos.rs
@@ -235,7 +235,7 @@
     ("virt_early_panic", qemu::Profile::Virt),
     ("virt_early_fault", qemu::Profile::Virt),
     ("virt_el2_drop", qemu::Profile::VirtEl2NoVhe),
-    ("virt_user_mode", qemu::Profile::VirtEl2),
+    ("virt_user_mode", qemu::Profile::Virt),
     ("virt_timer_preempts", qemu::Profile::VirtEl2),
     ("virt_irq_storm", qemu::Profile::VirtEl2),
     ("virt_timer_floor", qemu::Profile::VirtEl2),

h1-rotation (cargo test -p toyos-random, red, exit 101):

--- a/toyos-random/src/chacha.rs
+++ b/toyos-random/src/chacha.rs
@@ -15,7 +15,7 @@
     s[a] = s[a].wrapping_add(s[b]);
     s[d] = (s[d] ^ s[a]).rotate_left(8);
     s[c] = s[c].wrapping_add(s[d]);
-    s[b] = (s[b] ^ s[c]).rotate_left(7);
+    s[b] = (s[b] ^ s[c]).rotate_left(9);
 }
 
 /// The 64-byte block `key`, `counter` and `nonce` give, into `out`. Both

h2-rounds (cargo test -p toyos-random, red, exit 101):

--- a/toyos-random/src/chacha.rs
+++ b/toyos-random/src/chacha.rs
@@ -32,7 +32,7 @@
     }
 
     let mut state = initial;
-    for _ in 0..10 {
+    for _ in 0..8 {
         quarter_round(&mut state, 0, 4, 8, 12);
         quarter_round(&mut state, 1, 5, 9, 13);
         quarter_round(&mut state, 2, 6, 10, 14);

h3-draw-keeps-its-key (cargo test -p toyos-random, red, exit 101):

--- a/toyos-random/src/lib.rs
+++ b/toyos-random/src/lib.rs
@@ -140,7 +140,6 @@
     pub fn stream(&mut self) -> Stream {
         let mut block = [0u8; 64];
         chacha::block(&self.key, 0, &DRAW, &mut block);
-        self.key.copy_from_slice(&block[..SEED_LEN]);
         let mut stream = Stream { key: [0; SEED_LEN], next: 0 };
         stream.key.copy_from_slice(&block[SEED_LEN..]);
         wipe(&mut block);

h4-mix-substitutes (cargo test -p toyos-random, red, exit 101):

--- a/toyos-random/src/lib.rs
+++ b/toyos-random/src/lib.rs
@@ -127,7 +127,7 @@
 
     pub fn mix(&mut self, seed: Seed) {
         for (key, seed) in self.key.iter_mut().zip(&seed.0) {
-            *key ^= seed;
+            *key = *seed;
         }
         let mut block = [0u8; 64];
         chacha::block(&self.key, 0, &MIX, &mut block);

h5-constant-is-a-seed (cargo test -p toyos-random, red, exit 101):

--- a/toyos-random/src/lib.rs
+++ b/toyos-random/src/lib.rs
@@ -75,10 +75,6 @@
         let Ok(bytes) = <&[u8; SEED_LEN]>::try_from(bytes) else {
             return Err(Refusal::Length(bytes.len()));
         };
-        let (words, _) = bytes.as_chunks::<8>();
-        if words.iter().all(|word| *word == words[0]) {
-            return Err(Refusal::Constant);
-        }
         Ok(Seed(*bytes))
     }
 

h6-counter-is-32-bits (cargo test -p toyos-random, red, exit 101):

--- a/toyos-random/src/lib.rs
+++ b/toyos-random/src/lib.rs
@@ -167,7 +167,6 @@
     pub fn fill(&mut self, out: &mut [u8]) {
         for chunk in out.chunks_mut(64) {
             let mut nonce = [0u8; 12];
-            nonce[..4].copy_from_slice(&((self.next >> 32) as u32).to_le_bytes());
             let mut block = [0u8; 64];
             chacha::block(&self.key, self.next as u32, &nonce, &mut block);
             self.next += 1;

h7-a-take-leaves-the-seed (cargo test -p toyos-random, red, exit 101):

--- a/toyos-random/src/lib.rs
+++ b/toyos-random/src/lib.rs
@@ -92,8 +92,6 @@
             Some(handed) => Some(Seed::judge(handed)),
             None => Some(Err(Refusal::Length(handed))),
         };
-        wipe(bytes);
-        wipe(core::slice::from_mut(len));
         taken
     }
 }

x1-x86-has-no-cpu-source (cargo test --test toyos-build -- iommu_virtio_platform, red, exit 1):

--- a/kernel/src/arch/x86_64/entropy.rs
+++ b/kernel/src/arch/x86_64/entropy.rs
@@ -3,16 +3,16 @@
 //! `RDRAND`, the DRBG it seeds (SDM Vol. 1, "Random Number Generator
 //! Instructions").
 
+#![allow(dead_code)]
+
 use core::arch::asm;
 
 use super::cpu;
 
 pub use crate::random::Source;
 
-pub const SOURCES: &[Source] = &[
-    Source { name: "RDSEED", available: has_rdseed, draw: rdseed },
-    Source { name: "RDRAND", available: has_rdrand, draw: cpu::rdrand },
-];
+pub const SOURCES: &[Source] = &[];
+const _: fn() -> Option<u64> = cpu::rdrand;
 
 fn has_rdrand() -> Result<(), &'static str> {
     if cpu::has_rdrand() {

x2-rdrand-carry-inverted (cargo test --test toyos-build -- iommu_virtio_platform, red, exit 1):

--- a/kernel/src/arch/x86_64/cpu.rs
+++ b/kernel/src/arch/x86_64/cpu.rs
@@ -79,7 +79,7 @@
                 options(nomem, nostack),
             );
         }
-        if ok != 0 {
+        if ok == 0 {
             return Some(val);
         }
     }

g1-kernel-keys-from-a-constant (cargo test --test toyos-build -- virt_random_differs, red, exit 1):

--- a/kernel/src/random.rs
+++ b/kernel/src/random.rs
@@ -63,7 +63,7 @@
     drop(Seed::take(&mut loader.loader_seed, &mut loader.loader_seed_len));
     match Seed::take(&mut copy.loader_seed, &mut copy.loader_seed_len) {
         None => log!("random: {LOADER} is not mixed: the loader handed none"),
-        Some(seed) => mix(LOADER, seed),
+        Some(_) => mix(LOADER, Seed::judge(b"a seed all boots of an image had")),
     }
 
     for source in entropy::SOURCES {

g2-kernel-ignores-the-loaders-seed (cargo test --test toyos-build -- virt_user_mode, red, exit 1):

--- a/kernel/src/random.rs
+++ b/kernel/src/random.rs
@@ -63,7 +63,7 @@
     drop(Seed::take(&mut loader.loader_seed, &mut loader.loader_seed_len));
     match Seed::take(&mut copy.loader_seed, &mut copy.loader_seed_len) {
         None => log!("random: {LOADER} is not mixed: the loader handed none"),
-        Some(seed) => mix(LOADER, seed),
+        Some(_) => log!("random: {LOADER} is not mixed: mutated"),
     }
 
     for source in entropy::SOURCES {

g3-loader-hands-no-seed (cargo test --test toyos-build -- virt_user_mode, red, exit 1):

--- a/bootloader/src/main.rs
+++ b/bootloader/src/main.rs
@@ -632,7 +632,7 @@
         loader_seed: [0; toyos_abi::boot::SEED_LEN],
         loader_seed_len: 0,
     };
-    kernel_args.loader_seed_len = seed::read(&system_table, &mut kernel_args.loader_seed);
+    let _ = seed::read(&system_table, &mut kernel_args.loader_seed);
     report_reach(
         "Kernel arguments",
         &kernel_args as *const KernelArgs as u64,

g4-virt-has-no-rng (cargo test --test toyos-build -- virt_user_mode, red, exit 1):

--- a/tests/common/qemu.rs
+++ b/tests/common/qemu.rs
@@ -908,7 +908,7 @@
                 usb: &[],
                 storage: Storage::Stick { nvme_bytes: 0 },
                 iommu: None,
-                rng: true,
+                rng: false,
             },
             Self::Headless => Shape {
                 vga: "none",

g5-a-draw-writes-nothing (cargo test --test toyos-build -- virt_random_differs, red, exit 1):

--- a/kernel/src/random.rs
+++ b/kernel/src/random.rs
@@ -115,7 +115,6 @@
     while at < out.len() {
         let n = (out.len() - at).min(chunk.len());
         stream.fill(&mut chunk[..n]);
-        out.write_at(at, &chunk[..n]);
         at += n;
     }
     wipe(&mut chunk);

g6-no-source-keys-from-a-constant (cargo test --test toyos-build -- virt_no_seed_refused, red, exit 1):

--- a/kernel/src/random.rs
+++ b/kernel/src/random.rs
@@ -85,12 +85,7 @@
         wipe(&mut bytes);
     }
 
-    let Some(generator) = generator else {
-        panic!(
-            "random: nothing keyed the generator: the loader handed no seed and this CPU has no random \
-             source this kernel draws from, so no byte it gave out would be random"
-        )
-    };
+    let generator = generator.unwrap_or_else(|| Generator::keyed(Seed::judge(b"a seed all boots of an image had").ok().expect("a seed")));
     log!("random: the generator is keyed from {mixed} source(s), and every random byte is its ChaCha20");
     assert!(GENERATOR.lock().replace(generator).is_none(), "random: key() ran twice in one boot");
 }

@Japabu Japabu changed the title The kernel's random bytes come from a ChaCha20 generator keyed from the loader's seed and the CPU's sources, and 18 of the AArch64 guest tests run under HVF The kernel's random bytes come from a ChaCha20 generator keyed from the loader's seed and the CPU's sources, and 17 of the AArch64 guest tests run under HVF Oct 9, 2026
@Japabu

Japabu commented Oct 9, 2026

Copy link
Copy Markdown
Collaborator Author

Round 1's findings at head eb9b2bd25 (merges main at 558283168). Patches: #802 (comment).

BLOCKER

  • The seed's zeroing had no test. Fixed. toyos_random::Seed::take(&mut [u8; 32], &mut u64) judges a handed seed and zeroes both fields whatever the judgment; the kernel takes both copies through it, the loader's own arguments and its copy. a_taken_seed_leaves_zeros_where_it_was_handed asserts both fields zero on five arms: accepted, refused as constant, refused at length 16 and 33, and none handed. The review's patch now lives in take: deleting its two wipe calls (h7-a-take-leaves-the-seed) is red, exit 101. Deleting the kernel's take of the loader's own copy stays green, and I did not run it as a mutation: once mm::init drops the identity map no oracle can read the loader's stack. It is one line, held by reading, and the body says so.
  • No x86-64 test saw a CPU source mixed. Fixed. iommu_virtio_platform's netcase boots (Headless, HeadlessNoIommu) must say random: RDRAND is mixed into the generator's key. SOURCES = &[] (x1) is red, exit 1, with the boot keyed from 1 source, the loader's. RDRAND's carry check inverted (x2) is red, exit 1, with RDRAND is not mixed: it had no data to give. qemu64 has no RDSEED, so RDSEED's carry check is seen only on metal. There the T14 reading at 863d73ed3 shows RDSEED is mixed and RDRAND is mixed (The kernel's random bytes come from a ChaCha20 generator keyed from the loader's seed and the CPU's sources, and 17 of the AArch64 guest tests run under HVF #802 (comment)).
  • virt_reboot under HVF on a guess. Your option 1. It is back on Profile::VirtEl2, and the last-word issue names it in "Until it is fixed" and in its exit. No PSCI-traced test runs under HVF now, so the trace's assertion is TCG's again, as on main, and the branch's widening is deleted. The issue's exit says to widen it when the four tests move.
  • The control ran on the wrong base. Re-run on main at 558283168, the base of this head after the merge, with virt_user_mode alone moved to Profile::Virt: cargo test --test toyos-build -- virt_user_mode exits 1 with EARLY PANIC: panicked at kernel/src/hasher.rs:35:9: kernel hasher: ID_AA64ISAR0_EL1.RNDR is zero, so this CPU has no RNDR, and virtio-rng is the port's stage 6, and the seed has no other source.
  • The T14 reading. The orchestrator's reading at 863d73ed3 is above: 112 passed, the protocol present, keyed from 3 sources. This head changes the kernel, so testcases, shared and testcases-watchdog are staged again at eb9b2bd25, each image's sha256 in the request, for a fresh reading.

NOTE

  • EL2 and SMC coverage. SYSTEM_RESET through SMC is kept: virt_reboot is EL2 again. The other five tests' lost EL2 entries are stated as accepted in the track's stage 4, beside what still judges the drop from EL2 (virt_el2_drop, virt_smp, virt_jobs_at_el2) and PSCI through SMC (virt_smp, virt_reboot).
  • random_draws on virt runs on one CPU. Said in the body. One correction to the review: CI's guest suite is the same set of 41 tests and runs random_draws on no x86-64 boot, so the T14's shared boot is the only place two CPUs draw at once.
  • Stage 4's owed items had no exit. Each now closes on a guest test under HVF that is red with its step deleted, and stays owed with that test until one is. The tests are named: code rewritten at one address for make_executable, on a host whose CTR_EL0.DIC the test reads clear; a page's live entry replaced under a reader on another CPU for break-before-make; ASIDs bounded to two by an actuator, with three processes, for the reclaim flush. Stage 5's AP clean gets the same: red with the clean deleted.
  • The last word against the counters-read silences. The issue now separates them as you laid out. Those silences stop at the read's spawn: record, with no TEST_END and no stop line; this defect's capture had every userland line through power: the machine stops …. The two console-less virt_mask_windows stalls are recorded as undecidable. The next capture's discriminator is named: ===TEST_END test_rs_counters_read, the supervisor's stop line, and QEMU's PSCI trace.
  • The probe's citation. It is now debug-shutdown-regs.patch in The kernel's random bytes come from a ChaCha20 generator keyed from the loader's seed and the CPU's sources, and 17 of the AArch64 guest tests run under HVF #802 (comment).
  • The exit's 300 runs. They cover four tests, virt_reboot among them.
  • KernelArgs' Debug. The derive is dropped. Both architectures build without it, so nothing used it.
  • Trust. The body now says the seed's secrecy and unpredictability are assumed, not checked, and that a fixed seed repeated every boot cannot be detected at boot.
  • The metal citation. The body cites the T14 reading's comment and this round's staging log, r6-metal-readback.log: exit 2, boot:testcases boot:shared, three images.

Gates at eb9b2bd25

Gate Exit
cargo run -- --ci host (clippy with it) 0
cargo run -- --build-only 0
cargo run -- --build-only --arch aarch64 0
cargo test: 41 passed, 41 total (49.3s; workers: 175s building, 327s testing), load 15.65 at the start and 28.62 at the end 0
15 mutations, each red: h1 to h7 exit 101, x1, x2 and g1 to g6 exit 1 as stated
the negative control on 558283168 1
T14 staging, --metal-readback 2, the readback's own

One red on the way: the first --ci host at the unamended commit exited 1 on clippy's type_complexity in the new test's array type. That is fixed by a type alias, and every gate above was re-run at this head.

@Japabu

Japabu commented Oct 9, 2026

Copy link
Copy Markdown
Collaborator Author

Review, round 2, head eb9b2bd25 against main at 558283168. I judged 863d73ed3..eb9b2bd25 without the merge of main, and I read the implementer's kept r6 logs.

Net against main: 28 files, +1224 −167. That is production +536 −73, tests +513 −61 and issues +175 −33, which matches the body when I recount it from --numstat. This round's own commit adds 15 production lines (Seed::take) and removes 3 net in kernel/src/random.rs. I accept both.

Round 1's BLOCKERs

  • Seed zeroing: CLOSED.
    • h7-a-take-leaves-the-seed is red, exit 101. It panics at toyos-random/src/tests.rs:183, the zero assertion, and the test covers all five arms.
    • Where the two copies live:
      • The kernel's copy is the kernel_args local in kernel_main, on the boot stack inside the kernel image's reserved region.
      • The loader's copy is the kernel_args local of the loader's main (bootloader/src/main.rs:596), on the stack firmware gave the loader. That stack is Boot Services memory, and toyos_bootmap::is_usable_type gives it to the pmm after the handoff.
    • So that page can reach a later process. It reaches one only through pmm::alloc_page or alloc_contiguous, and both zero the whole 2 MiB frame first (kernel/src/mm/pmm.rs:171-176 and :220-225). claim() hands a frame over as it is only to the kernel heap (mm/alloc.rs:560), and safe code cannot read heap bytes it has not written.
    • The line drop(Seed::take(&mut loader…)) is therefore not the only barrier between the seed and userland. It guards against a kernel disclosure of uninitialised heap. Firmware's own RNG and virtio-rng buffers fall in that same class, and the body already lists them as not wiped. Holding it by reading is acceptable. It owes no test and no further measurement.
  • The x86-64 source seen by a test: CLOSED.
    • x1 is red, exit 1, with "random: RDRAND is mixed…" never reached the boot console and keyed from 1 source(s).
    • x2 is red, exit 1, with RDRAND is not mixed: it had no data to give.
    • The CPU string is +rdrand on both TCG and KVM (src/arch.rs:166-167), so CI and nightly both run the assertion.
    • Leaving RDSEED's carry check to the T14 is acceptable. No profile in the fence gives a guest RDSEED under TCG here, and the T14 reading at 863d73ed3 saw RDSEED mixed. The reading at this head has to show it again.
  • virt_reboot: CLOSED.
    • It is back on Profile::VirtEl2 (tests/toyos.rs:263) and passed in the suite. The issue names it under "Until it is fixed" and in the exit.
    • The PSCI-trace assertion is byte-for-byte main's again. Every traced test runs under TCG: virt_smp, virt_reboot and virt_off_names_the_cpus_left_on on VirtEl2, and virt_el1_smp on VirtTcg.
    • This loses no measurement. The HVF trace is a capture, and that capture stays in the issue and its probe comment. The exit asks for the assertion to be widened again when the four tests move.
  • The negative control: CLOSED. The control log at 558283168 has virt_user_mode alone on Profile::Virt. It exits 1 with the kernel/src/hasher.rs:35:9 RNDR panic, at load 25.5 to 37.1, and the tree was back on the branch and clean afterwards.
  • The T14: OPEN. This head changes the kernel (random::key now takes both copies through Seed::take), toyos-abi, and through the merge of main the image (A machine's image is installed on its one NVMe disk beside its DATA: the Headless guests boot off it with no stick, and /log and /home are read back across a reboot through diskserver #797, toyos-virtio: the virtio 1.2 PCI transport and split virtqueue as one pure crate, with netstack's virtio-net its first client; toyos-device-memory: the one boundary toyos-i219 and toyos-virtio are written against #796). The reading at 863d73ed3 does not stand for this head, and none is on the pull request yet.

Round 1's NOTEs

All closed:

  • The EL2 entries are accepted in stage 4, with what still judges each.
  • Every owed stage-4 item names a test that a deletion turns red. Stage 5's AP clean does too.
  • The issue separates itself from the counters-read silences and names the next capture's discriminator.
  • The probe citation resolves.
  • The exit covers four tests.
  • The trust sentence is now true.
  • Debug is dropped. A {:?} of KernelArgs, or of a struct holding it, no longer compiles. Nothing prints the seed field, and the loader's seed.rs prints no byte.

The implementer's correction holds:

  • shared_metal is reached only from the --metal branch of main().
  • None of the suite's 41 guest tests is a shared member.
  • The virt job case boots one CPU.

So only the T14's shared boot puts two CPUs on GENERATOR. No guest or host test of that is owed:

  • Generator is &mut self behind the kernel's Lock, so the type refuses unsynchronised access.
  • The lock's scope is the one expression in stream().
  • Two host threads over the pure crate would test std's Mutex, not the kernel's Lock.

BLOCKER

  • T14 — No reading at eb9b2bd25. Here is what it has to show, from testcases and shared as the request states:

    • Every row green, with test_rs_random_draws among shared's members.
    • Each of the five lines exactly once and in order: the loader's Seed: 32 bytes from EFI_RNG_PROTOCOL…, then the loader's seed, RDSEED and RDRAND each mixed, then keyed from 3 source(s).
    • No panic, and no line carrying a seed, key or drawn byte.

    Any red, any not mixed, a missing or repeated line, or a Seed: line without the protocol sends it back.

NOTE

  • PR body, Trust, last bullet — The body holds the loader-copy line by reading because "no oracle can read the loader's stack". The reason that holding is safe is that the loader's stack is Boot Services memory the pmm takes over, and that the pmm zeroes every frame it hands a process (pmm.rs:171-176, :220-225). Say that.
  • PR body, mutations table, x1 — The chain's own x1 run was red on a compile error: cpu::rdrand never used, under -D warnings. The stated reason comes from a second run with the patch that is posted. Say so.
  • PR body — The kept logs hold only this round's runs. These claims at earlier heads now stand on no kept log, and the body does not say so:
    • the owner's timing table (1621281ae, 7c05a8dd4) and the single-boot timings;
    • the 131-run and 50-run counts at 7522ec61e;
    • the iommu_platform=on refusal;
    • the OVMF and cortex-a72 protocol probes;
    • the AArch64 EFI-app probe.

What the landing head must show

The pull request is a draft, and ci.yml's host skips a draft, so it is marked ready first. At the head that lands:

The orchestrator may land on those three checks together with a T14 reading at eb9b2bd25 that shows the above, with no further round, provided the head does not move. If the head moves, re-stage and boot the T14 again from the new head. That includes a merge of main carrying #790, because its compiler rebuilds the kernel and the loader. A merge-queue build of #790 alongside an unmoved head needs no new boot.

SEND BACK

@Japabu

Japabu commented Oct 9, 2026

Copy link
Copy Markdown
Collaborator Author

The T14 at eb9b2bd25 (the orchestrator's reading; worktree clean before and after, each image's sha256 checked against the request in the command that flashed it). Three boots (shared, testcases-watchdog, testcases), each toyos-metal exit 0; judged with --metal --metal-readback <dir> boot:testcases boot:shared: exit 0, 112 passed, 0 failed, 3 boots, test_rs_random_draws among shared's members.

On testcases and on shared, each line once, in this order: Seed: 32 bytes from EFI_RNG_PROTOCOL for the kernel's generator; random: the loader's seed is mixed into the generator's key; random: RDSEED is mixed into the generator's key; random: RDRAND is mixed into the generator's key; random: the generator is keyed from 3 source(s), and every random byte is its ChaCha20. No panic; no line carries a seed, a key or a drawn byte.

This closes round 2's one BLOCKER. Marked ready; it lands on host, toolchain / build and guest / suite at this head (the body records, from guest / suite's log, whether the KVM runner's boots say RDSEED is mixed and which Seed: line CI's OVMF gives).

@Japabu
Japabu marked this pull request as ready for review October 9, 2026 13:35
@Japabu

Japabu commented Oct 9, 2026

Copy link
Copy Markdown
Collaborator Author

CI at eb9b2bd25, read from each job's own log (the orchestrator). host: [ci] Host: 79 step(s), all green. toolchain / build: LLVM, compiler and freestanding restored; the sysroot fcaf4949f5310fa6 built (the toyos-abi change). guest / suite: test result: ok. 41 passed, 41 total, iommu_virtio_platform PASS (its boots assert random: RDRAND is mixed), [ci] Guest: 5 step(s), all green, no FAIL line.

Not readable from CI's logs, so not recorded: whether the KVM runner's boots say RDSEED is mixed, and which Seed: line CI's OVMF gives. The suite's log keeps no guest console of a passing test, so neither line is in it; both stay "not measured" in the body. On the T14 both were read: RDSEED mixed, Seed: 32 bytes from EFI_RNG_PROTOCOL.

With the T14 reading at this head above. git merge-tree against main at a944746d5 exits 0 with no conflict.

@Japabu
Japabu added this pull request to the merge queue Oct 9, 2026
Merged via the queue into main with commit 8dbccd3 Oct 9, 2026
6 checks passed
@Japabu
Japabu deleted the wt/toyos-entropy branch October 9, 2026 14:23
Japabu added a commit that referenced this pull request Oct 9, 2026
…m end's order (#803) and the .local name's re-probe (#800), into the app grants

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C
Japabu added a commit that referenced this pull request Oct 9, 2026
…this defect, into the stop's hold of the console wire

The one conflict is the virt screen-test table: main moved
`virt_reboot_refused_without_psci` to HVF and added its EL2 and random rows;
this branch's two staged reboot rows stay emulated beside them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C
Japabu added a commit that referenced this pull request Oct 9, 2026
…nd's order (#803) and the .local name's probing (#800), into the HTTPS client

ring_kat's line in DRIVEN_AND_SHARED met random_draws' there; the merge keeps
main's, and the next commit decides ring_kat's place.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C
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