Repository navigation
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
Conversation
…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
|
Mutation patches, the negative control and the probes behind this pull request's body, each as applied with Negative control, against the base --- 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),
--- 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
--- 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);
--- 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);
--- 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);
--- 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))
}
}
--- 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;
--- 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] {
--- 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] {
--- 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,
--- 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",
--- 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);
--- 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 --- 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(),
}
}
--- 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!( |
|
Review, round 1, head 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
NOTE
What collides
The T14What the reading must show, from the staged
What sends it back:
No further boot is owed: What the landing head's checks must show
The orchestrator may land on reading those checks together with the T14 reading, once the BLOCKERs above are closed. SEND BACK |
|
The T14 at On both
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. |
…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
|
Round 2's mutation patches and negative control, at head Negative control, against --- 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),
--- 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
--- 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);
--- 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);
--- 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);
--- 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))
}
--- 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;
--- 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
}
}
--- 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() {
--- 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);
}
}
--- 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 {
--- 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 {
--- 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,
--- 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",
--- 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);
--- 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");
} |
|
Round 1's findings at head BLOCKER
NOTE
Gates at
|
| 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.
|
Review, round 2, head Net against Round 1's BLOCKERs
Round 1's NOTEsAll closed:
The implementer's correction holds:
So only the T14's
BLOCKER
NOTE
What the landing head must showThe pull request is a draft, and ci.yml's
The orchestrator may land on those three checks together with a T14 reading at SEND BACK |
|
The T14 at On This closes round 2's one BLOCKER. Marked ready; it lands on |
|
CI at Not readable from CI's logs, so not recorded: whether the KVM runner's boots say With the T14 reading at this head above. |
…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
…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
…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
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 24virt_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_windowsandvirt_off_names_the_cpus_left_onwere red under HVF six times, eachSTALLED: waiting for the boot's last word. A capture of one shows QEMU traced sevenCPU_OFFand oneSYSTEM_OFFfrom the guest: it powered off, its console's last line the supervisor's stop request, withklogdabout 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 whileklogdholds it, and the power-off does not wait.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.virt_reboot, which waits forRebooting.through the same stop and was never measured under HVF; it is also the one test ofSYSTEM_RESETthrough the SMC conduit.Profile::VirtTcgstays forvirt_el1_smp. 13 of the scout's 17 moved.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.What changed, per decision
toyos-random, a new pure crate. The ChaCha20 block function of RFC 8439, and three types.Seed: 32 bytesSeed::judgedid 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::takejudges a seed another program handed over as a byte array and a length, and zeroes both whatever the judgment.Generator: its one constructor takes aSeed, so an unkeyed generator is unrepresentable.mixXORs 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::streamis 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.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 fromEFI_RNG_PROTOCOLbeforeExitBootServices, openedGET_PROTOCOLso no driver is stopped, straight intoKernelArgs. One line says whether it got them and never a byte. No protocol, a failedGetRNGor refused bytes hand the kernel none; none is an error. Both architectures.KernelArgsgainsloader_seed: [u8; 32]andloader_seed_len: u64at the end;LAYOUTfolds the size in, so an old loader is refused by the existing check. It no longer derivesDebug, so no{:?}of it on either side can print the seed.The kernel (
kernel/src/random.rs, new).random::keyruns once, after the boot's own lines and beforehasher::seed:Seed::takefrom both the loader's ownKernelArgsand the kernel's copy (kernel_mainnow takes&mut KernelArgs), which zeroes both, and mixes the copy's;RDSEEDandRDRANDon x86-64,RNDRon AArch64, four draws each;hasher::seedandSYS_RANDOMdraw from it on both architectures.SYS_RANDOMno longer fails:SyscallError::Iofor a dry RDRAND is gone.Decided and stated:
issues/the-kernels-generator-is-keyed-once-and-never-reseeded.md(renamed fromevery-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.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.qemu64under TCG has none, so on this host that path is the "not mixed" arm; CI's KVM guest and the T14 run the other.+rdseedwas not added to the guest CPU:src/arch.rs's CPU string is outside the fence.The harness.
-device virtio-rng-pcion thevirtshape (a newShape::rng), and oncargo run'svirt. It is firmware's device: edk2's driver, read once. Measured: withiommu_platform=onQEMU refuses to start the transitional device, so it is passed plain.EFI_RNG_PROTOCOLonqemu64,+rdrandwithout 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.iommu_virtio_platform's netcase boots,HeadlessandHeadlessNoIommu, must sayrandom: 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 isqemu64,+rdrandunder TCG and the host's under KVM, which has RDRAND too.Profile::Virt. The seven job-case boots are HVF boots;virt_jobs_at_el2is 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.virt_user_mode,virt_irq_storm,virt_timer_floor,virt_failed_ap_leaves_no_holeandvirt_fatal_halts_the_others_firstno longer boot entered at EL2. The drop from EL2 stays judged byvirt_el2_drop,virt_smpandvirt_jobs_at_el2, and PSCI through SMC byvirt_smp(CPU_ON,CPU_OFF,SYSTEM_OFF) andvirt_reboot(SYSTEM_RESET).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:virtwith no virtio-rng on a CPU with no RNDR (the host's under HVF,cortex-a72emulated). Measured:cortex-a72under TCG without the device has no protocol, and with it has.random_drawsonvirtruns in the job case, which boots one CPU: its eight threads never put two CPUs onGENERATORthere. Draws from two CPUs at once are measured on the T14'ssharedboot alone: no guest test here or in CI runsrandom_drawson more than one CPU.Trust
virt_random_differswould see it.Generatorleaves when it is moved into its static, and whatever firmware keeps. The first two are in the reseeding issue.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: aftermm::initdrops 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 mergesmainat558283168. Logs are this round'sr6-*.log, kept outside the tree.cargo run -- --ci host(clippy with it)cargo run -- --build-onlycargo run -- --build-only --arch aarch64cargo 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 endcargo test --test toyos-build -- --metal --metal-readback <dir> boot:testcases boot:shared: three images staged at this head, no machine touchedtoyos-random's tests hold the quarter round to §2.1.1 and §2.2.1 and the block function to §2.3.2 and all five vectors of appendix A.1, copied from the RFC's text as fetched from rfc-editor.org.863d73ed3(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)): 112 rows passed, 0 failed, over three boots,test_rs_random_drawsamong them. Ontestcasesandsharedthe loader saidSeed: 32 bytes from EFI_RNG_PROTOCOL for the kernel's generator, and the kernel said the loader's seed, RDSEED and RDRAND each mixed andkeyed from 3 source(s); no loader or kernel line carried a seed, a key or a drawn byte. RDSEED's carry check is seen only there:qemu64has no RDSEED. This head changes the kernel, so its images are staged again for a reading.mainat558283168, this head's base, withvirt_user_mode's profile alone moved toProfile::Virt:cargo test --test toyos-build -- virt_user_modeexits 1 withEARLY 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_key_a_draw_was_made_under_is_gone_when_it_returns,a_draw_is_the_blocks_the_module_statesmixsubstitutes the seed for the keyevery_seed_mixed_moves_the_draw_and_none_replaces_another,a_draw_is_the_blocks_the_module_statesbytes_a_failed_source_leaves_are_no_seeda_stream_does_not_repeat_where_32_bits_of_counter_enda_taken_seed_leaves_zeros_where_it_was_handediommu_virtio_platformiommu_virtio_platformRDRAND is not mixed: it had no data to givevirt_random_differs(HVF)virt_user_mode(HVF)virt_user_mode(HVF)virtshape has no virtio-rngvirt_user_modevirt_random_differs, byrandom_draws' own assertsvirt_no_seed_refusedThe 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
virtwith-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.iommu_virtio_platformadds 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.mainat1621281aemainat1621281ae7c05a8dd47c05a8dd4One 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_rebootemulated again, 17 boots run under HVF.Single boots alone, images built, same session, load 36 to 63:
virt_timer_preemptsunder HVF 3 s of testing againstvirt_jobs_at_el2emulated 4 s;virt_el1_smpunder HVF 3 s againstvirt_smpemulated 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
testcases,shared, which carriesrandom_draws, andtestcases-watchdog).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.cache::make_executableon 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_rebootin 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 mostvirt_tests, not two.Net against
mainat558283168: 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