Repository navigation
Stage 6 step 2, built: the longest interrupts-off and preemption-off windows per CPU, and a held window the T14 reads back - #649
Conversation
…, per CPU, beside the census `mask-windows` is a kernel build that reads the counter at every masking transition and every preempt-count crossing of zero, and prints, beside each CPU's IRQ census line, the longest interrupts-off and preemption-off window that CPU closed since the report before (`windows: cpuN irqs_off_ns=… preempt_off_ns=…`). Fed by each architecture's masking primitives and entries: `IrqGuard`, `enable_interrupts`/`disable_interrupts` and the halt on both; on x86-64 the syscall entry, every gate stub (device, TLB, timer in both rings, spurious, unclaimed) and the exception dispatch; on AArch64 the vector dispatch. A return to user mode opens interrupts at `exit_to_user`'s end, which every such return runs last. The preempt count's accessors feed its crossings on both arches, and the x86-64 stubs that raise it inline feed theirs from their Rust halves. A context switch runs inside an `IrqGuard` in this build, so no saved context carries interrupts open through `context_switch`'s flags restore, the one transition no hook can follow. A scheduler pass ends a preemption-off window and starts the next, and the idle halt inside a pass is no window: without both the number is the time since the CPU last returned to user mode, idle time included. Every hook checks the state it finds: a transition no hook saw panics at the next hook on that CPU, and every entry is held to what the hardware says it interrupted (a maskable interrupt only with interrupts open; an exception's frame carries the flag). The load `issues/kernel/a-process-lengthens-an-interrupts-off-walk-by-the-threads-it-parks-on-one-ring.md` names is `test_rs_ring_park_herd`: 256 threads parked in `submit` on one ring, and the main thread completing into it 64 times, each once the roster shows all 256 parked again. `mask_windows` (x86-64, four CPUs) and `virt_mask_windows` (AArch64) boot `boot-actuators,mask-windows,test-actuators` with `windows-staged`, which spins 1 ms inside the first process exit, where both windows are open; a later report of that CPU must carry both at least that long. The `mask_windows` metal row boots the shipping kernel plus `mask-windows` on the T14 and records the boot's longest windows and the herd's. The harness lets a named build that carries the actuators arm one. roster.rs holds 1024 entries and refuses a machine with more, since the herd's 257 threads would not fit its 256. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
…duces Positive control first, then seven refusals: no staged span, a staged span no later report carries, one carried only by a report before it, a census without its windows, a CPU that closed no window, one CPU of two, and a field missing. With the staged check reduced to "some later report exists" it reds: "a staged span no later report carries was accepted". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
…s own A 1 ms span is inside what a TCG guest's own windows can reach — the herd's completion alone walks 256 waiters — so with the spin removed a later report could still carry it and the negative control would stay green. 250 ms stands above that and far inside `time::DEAF_CPU`'s 5 s. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
…host tests `toyos_sched::windows::Windows` is one CPU's record: every transition takes the caller's counter reading (read only when the transition needs it) and answers `Unseen` for one that could not have followed the state it finds. The kernel's `windows.rs` is the glue: the per-CPU statics, the hooks each architecture calls, the report and `windows-staged`. What no guest can red now reds on the host: a pass ends the preemption-off window and starts the next, the idle halt inside a pass is no window, only a crossing of zero opens or closes one (and a raise past one reads no clock), the longest since the last take is what a take answers, and each transition nothing reported before is named. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
…build too At 66530cf both guest arms of the windows' instrument died before a kernel was built. `mask_windows` and `virt_mask_windows` boot `MASK_WINDOWS_KERNEL` (`boot-actuators,mask-windows,test-actuators`) armed with `windows-staged`. `kernel_of` had been widened to let a named build that carries `boot-actuators` arm an actuator. `build_test_parts` still required the exact `TEST_KERNEL` for any parameter outside the kernel's own and the valued ones, so it panicked: "a boot asking for ["windows-staged"] must boot the test kernel, not ["boot-actuators", "mask-windows", "test-actuators"]". Every mutation arm then failed on that same refusal, so none of them measured anything. The two sites now ask one predicate, `build::carries_the_actuators`: the build carries every feature of `TEST_KERNEL`, alone or under an instrument. `kernel_of`'s `boot-actuators` check becomes the same predicate, so it can no longer disagree with the image build. `build_test_parts`' refusal moves into `refuse_an_unarmable_boot` so a host test can ask it directly. The test is `a_named_build_over_the_test_kernel_arms_an_actuator`: the windows' kernel and the test kernel arm `windows-staged`, and the scheduler's check kernel is refused. Negative control: restoring the exact-equality rule at the refusal builds, and the test is red with the guest's own message. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
…eport says each closed once At 772bac7, `virt_mask_windows` was red twice. In both runs the report after the stage carried a preemption-off window longer than the 250 ms spin (271362000 ns and 292094000 ns), and an interrupts-off window shorter than it (65031000 ns and 153183000 ns). The two clocks did not disagree. The spin and the record both read `cpu::counter` (RDTSC on x86-64, CNTVCT_EL0 after an ISB on AArch64) and convert with the one period in `clock`. The cause was where the spin ran. In `virtjobcase` the first process to exit is `preempt`, whose counting thread never enters the kernel. Its main thread's `SYS_EXIT` is not the last out. The killed sibling tears the process down at its exit boundary, and `leave_user_if_due` opens interrupts across that teardown. So the spin ran with preemption off and interrupts open, and the record said exactly that. A QEMU verdict asserts order, completion, content and counts, never how long something took. The guest verdict now reads no duration: - `toyos_sched::windows::Windows::mark` marks the window of each kind open now. A missing open window is refused as `Unseen`. Each mark rides to its window's close, and `take_marked` answers each marked close once. - `windows-staged` marks at the first `SYS_EXIT`, whose entry masked interrupts and raised the preempt count, and it spins no longer. It asserts both from the CPU first, so a stage moved to where a window is shut is refused as that and not as a missed hook. It prints `windows: staged cpuK open`. A later report of cpuK prints `windows: staged cpuK closed irqs_off=… preempt_off=…` for the marked windows that closed since the report before. - `common::irqcensus::windows` holds a stage to this: after the stage, its CPU's reports say each marked window closed exactly once, and no close precedes its stage. The census pairing and the every-CPU-closed-both checks stay. Durations are judged on the T14 alone, which arms no actuator. The record's new mark word costs every close one relaxed load, in the metal build too. The verdicts move above the control-registers doc block they had been inserted into. That block's own misplacement onto `irq_census` is main's, filed as issues/build/the-control-registers-verdicts-doc-comment-sits-on-the-census-verdict.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
Main is 469 commits past the last merge (e754dc8, #588). Six files conflicted, each resolved by accounting for both sides: - kernel/src/actuator.rs: main (#639, #660) deleted every actuator in the region this branch added `windows-staged` to, `handler-post` among them. Main's deletions stand, and `windows-staged` sits after `usb-reset-under-load`. Its doc names `virt_mask_windows`, its one judge now. - kernel/src/scheduler.rs: main deleted `dump-in-blocking-pass`'s lines in `exit_to_user`; this branch's `irqs_unmasking` hook followed them. The hook stays, the actuator's lines go. - src/clippy.rs: main dropped `fpu-save-nothing` and `user-writable-gsbase` from both instrument lists; `mask-windows` is appended to what is left, and the two `MASK_WINDOWS_KERNEL` shapes are this branch's. - src/build.rs: main cut `TEST_SUITE_KERNEL_BUILDS` to the shipping and the test kernel; `MASK_WINDOWS_KERNEL` is the third. `carries_the_actuators` and `refuse_an_unarmable_boot` merged without conflict. - tests/common/qemu.rs: main shortened `DECLARED_KERNEL_BUILDS`' doc and deleted `kernel_of`'s paragraph on `boot_image`; this branch's sentence for the windows' build and `kernel_of`'s rule are kept over main's text. - tests/toyos.rs: main's file (#660: 21 declared guest tests, `Sched` and the shared guest boot gone, `metal::Metal` a struct) taken whole, and this branch's additions put back onto it: `ring_park_herd` on `RUST_SKIP`, `virt_mask_windows` and its judge `mask_windows`, the `mask_windows` metal row with `WINDOWSCASE`, and `windows_on_metal`. **The x86-64 guest test `mask_windows` does not come back.** Main refuses one name in two registries (`registered`), and `mask_windows` is the T14's row; `irq_census_conservation`, the census these lines sit beside, was cut from the guest suite for its row the same way. Its `MACHINE_TESTS` entry, its `CARRIES` row and its `run_machine_test` arm, which booted the shared guest image main deleted, are not carried. What it held is held by: the x86-64 hooks' checks by the metal row, which boots them under the herd; the stage by `virt_mask_windows`; the verdict by `mask_windows_verdict` on the host. The census of raw masking instructions is unchanged by the merge: every word of `cli`, `sti`, `popfq`, `iretq`, `sysretq`, `eret`, `daifset`, `daifclr`, `daif`, `wfi` and `hlt` under `kernel/src`, comments apart, is the same list at 737803d and here but for main's own two moves: the NMI gate's `iretq` from `nmi_gate.rs` to `idt/nmi.rs`, and `IrqGuard::unclosed`, a read of the flags, deleted on both architectures. The clause of the guard's drop hook that named it goes with it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm
Stage 6 step 2's exit compared the two windows at stage 6's first commit (53b1ef7, the instrument applied as a patch) with those at step 1's head. Both arms are behind main by the whole guest-suite cut and its neighbours: their toolchain is in no store, a build at either starts with an LLVM and compiler rebuild, and their `keystore::sweep` panics on today's stores and could sweep stores other worktrees use. The comparison is taken instead as root CLAUDE.md gives a negative control: on one tree, today's with the instrument, with step 1 (#634) as it stands and with it reverted. That reads what the exit is for, whether step 1 lengthened either window, with today's toolchain. The track's step 2 and the stage's rule for steps 2 to 5 say so, and the exit of the issue step 2 holds moves with them. The kernel's lines are still counted against stage 6's first commit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm
|
The base arm: step 1 (#634, diff --git a/kernel/Cargo.toml b/kernel/Cargo.toml
index c4b2f45b0..62b59ce64 100644
--- a/kernel/Cargo.toml
+++ b/kernel/Cargo.toml
@@ -53,10 +53,10 @@ log-commit-release-off = []
# `src/log/registry.rs`'s pointer store and load go `Relaxed`, and
# `log_publish` reds.
shard-publish-relaxed = []
-# `src/pcidev/record.rs`'s read-modify-writes become a load and a store, so a
-# message the ISR records between a reader's two halves is lost, and
-# `device_irq` reds. They *are* the record's design, so turning them off reverts
-# the whole of what the model claims.
+# `src/pcidev/record.rs`'s two read-modify-writes become a load and a store, so
+# a message the ISR records between a reader's two halves is lost and a wake can
+# be owed twice, and `device_irq` reds. That pair *is* the record's design, so
+# turning it off reverts the whole of what the model claims.
device-irq-lossy = []
# `src/sched/dump_request.rs`'s `take` and `end_report` go `Relaxed`, so the next report
# is unordered against the last one's writes, and `dump_request` reds.
diff --git a/kernel/src/arch/x86_64/idt/hda.rs b/kernel/src/arch/x86_64/idt/hda.rs
index d75e99ef7..579bebedf 100644
--- a/kernel/src/arch/x86_64/idt/hda.rs
+++ b/kernel/src/arch/x86_64/idt/hda.rs
@@ -1,6 +1,6 @@
use super::device_irq::device_irq_entry;
-// Heap-free, and takes only interrupts-off locks: may interrupt a CPU holding the controller lock (preemption disabled, not interrupts).
+// Lock-free, heap-free: may interrupt a CPU holding the controller lock (preemption disabled, not interrupts).
extern "sysv64" fn hda_handler() {
crate::arch::percpu::irq_took!(Hda);
crate::drivers::hda::isr_complete();
diff --git a/kernel/src/arch/x86_64/idt/user_dev.rs b/kernel/src/arch/x86_64/idt/user_dev.rs
index 9ad2dccbc..18142f94d 100644
--- a/kernel/src/arch/x86_64/idt/user_dev.rs
+++ b/kernel/src/arch/x86_64/idt/user_dev.rs
@@ -6,14 +6,18 @@
//! wake every user driver in the machine on any of their interrupts, which is
//! one process learning when another's device is busy.
//!
-//! Heap-free like every other device entry here: the record is atomics, and
-//! the claim's watch is posted from the handler.
+//! Lock-free and heap-free like every other device entry here: the record is
+//! atomics and the wake happens on the next scheduler pass.
use super::device_irq::device_irq_entry;
+use crate::irq_ring::IrqSource;
fn took(slot: usize) {
crate::arch::percpu::irq_took!(UserDev);
crate::pcidev::isr(slot);
+ crate::irq_ring::isr_publish(IrqSource::UserDev, crate::clock::nanos_since_boot());
+ // Force resched now, so `drain_irqs` turns the record into a wake before
+ // the next quantum tick rather than after it.
crate::preempt::set_need_resched();
crate::arch::apic::eoi();
}
diff --git a/kernel/src/arch/x86_64/idt/virtio_sound.rs b/kernel/src/arch/x86_64/idt/virtio_sound.rs
index 6e3744d62..98060824d 100644
--- a/kernel/src/arch/x86_64/idt/virtio_sound.rs
+++ b/kernel/src/arch/x86_64/idt/virtio_sound.rs
@@ -1,6 +1,6 @@
use super::device_irq::device_irq_entry;
-// Heap-free, and takes only interrupts-off locks: may interrupt a CPU holding the controller lock, which disables preemption but not interrupts.
+// Lock-free and heap-free: may interrupt a CPU holding the controller lock, which disables preemption but not interrupts.
extern "sysv64" fn virtio_sound_handler() {
crate::arch::percpu::irq_took!(Sound);
crate::drivers::virtio_sound::isr_complete();
diff --git a/kernel/src/arch/x86_64/mod.rs b/kernel/src/arch/x86_64/mod.rs
index d7e401a25..b59cc8303 100644
--- a/kernel/src/arch/x86_64/mod.rs
+++ b/kernel/src/arch/x86_64/mod.rs
@@ -50,8 +50,10 @@ pub const ELF_MACHINE: toyos_elf::Machine = toyos_elf::Machine::X86_64;
/// Interrupts masked on this CPU for as long as the guard lives, and then put
/// back as they were — restored, not enabled — so a guard nests inside a region
-/// that is already masked. The one way this kernel masks and restores. `TF` is
-/// always clear in Ring 0, so the guard leaves it alone.
+/// that is already masked. The one way this kernel masks and restores: the
+/// scheduler's pass, a log record's reservation and publication, and the
+/// console backend each hold one. `TF` is always clear in Ring 0, so the guard
+/// leaves it alone.
///
/// Both edges are compiler barriers (no `nomem`): a memory access written
/// inside the region is emitted inside it.
diff --git a/kernel/src/arch/x86_64/vtd/fault.rs b/kernel/src/arch/x86_64/vtd/fault.rs
index 3de98c0f3..c0188b87c 100644
--- a/kernel/src/arch/x86_64/vtd/fault.rs
+++ b/kernel/src/arch/x86_64/vtd/fault.rs
@@ -1,6 +1,6 @@
//! Vt-d fault interrupt handling: MSI-delivered, never polled.
//!
-//! The handler is bounded, allocates nothing; unit and
+//! The handler is bounded, allocates nothing and takes no lock; unit and
//! function state lives in fixed arrays of atomics, written once before the
//! mask comes off. Whatever the stream, the same things happen first: Bus
//! Master Enable cleared on the function that faulted, the first record latched
@@ -36,7 +36,8 @@ const FSTS_OVERFLOW: u32 = 1 << 0;
// One fault recording register's F bit, in the 32-bit word that carries it.
const RECORD_FAULT: u32 = 1 << 31;
-// Atomics only: each field is written once before the unit's mask comes off.
+// Atomics only: the handler takes no lock; each field is written once before
+// the unit's mask comes off.
struct FaultUnit {
// Physical base of the register window; 0 means this slot is unused.
// vtd::window refuses a base of 0, so no armed unit can collide with the sentinel.
diff --git a/kernel/src/device.rs b/kernel/src/device.rs
index 66935836c..17c7f60d5 100644
--- a/kernel/src/device.rs
+++ b/kernel/src/device.rs
@@ -79,20 +79,7 @@ impl Claim {
impl Drop for Claim {
fn drop(&mut self) {
match self.what {
- Claimed::Class(class) => {
- // Before the flag goes: a poll the next holder registers is not this claim's to answer.
- match class {
- DeviceType::HdaAudio | DeviceType::VirtioSound => {
- crate::drivers::AUDIO_WATCH.cancel_polls()
- }
- DeviceType::Keyboard
- | DeviceType::Mouse
- | DeviceType::Framebuffer
- | DeviceType::PciFunction
- | DeviceType::Partition => {}
- }
- *taken(class).lock() = false;
- }
+ Claimed::Class(class) => *taken(class).lock() = false,
// Bus mastering off, then the domain, then the pages: `release`
// owns that order, and this is where a dying process reaches it.
Claimed::PciFunction(slot) => crate::pcidev::release(slot),
diff --git a/kernel/src/drivers/hda.rs b/kernel/src/drivers/hda.rs
index d86471b81..8e87fcc85 100644
--- a/kernel/src/drivers/hda.rs
+++ b/kernel/src/drivers/hda.rs
@@ -166,7 +166,7 @@ pub fn isr_complete() {
}
ISR.timestamp.store(timestamp, Ordering::Relaxed);
ISR.mask.fetch_or(mask, Ordering::Release);
- super::AUDIO_WATCH.post_in_place();
+ crate::irq_ring::isr_publish(crate::irq_ring::IrqSource::Audio, timestamp);
crate::preempt::set_need_resched();
}
diff --git a/kernel/src/drivers/mod.rs b/kernel/src/drivers/mod.rs
index a4f8cf9f8..d71937ef0 100644
--- a/kernel/src/drivers/mod.rs
+++ b/kernel/src/drivers/mod.rs
@@ -23,4 +23,4 @@ pub use crate::mm::DmaPool;
/// What an audio read and an audio poll wait on: one for both backends, since
/// at most one binds and the claim's reader cannot know which.
-pub static AUDIO_WATCH: crate::watch::IrqWatch = crate::watch::IrqWatch::new();
+pub static AUDIO_WATCH: crate::watch::Watch = crate::watch::Watch::new();
diff --git a/kernel/src/drivers/virtio_sound.rs b/kernel/src/drivers/virtio_sound.rs
index c5cf09e9d..6ad0df502 100644
--- a/kernel/src/drivers/virtio_sound.rs
+++ b/kernel/src/drivers/virtio_sound.rs
@@ -108,7 +108,8 @@ pub fn isr_complete() {
return;
}
isr_push_completion(mask, timestamp);
- super::AUDIO_WATCH.post_in_place();
+ crate::irq_ring::isr_publish(crate::irq_ring::IrqSource::Audio, timestamp);
+ // Force a scheduler entry on IRQ return so the record becomes wakes now, not at next tick.
crate::preempt::set_need_resched();
}
diff --git a/kernel/src/inbox/mod.rs b/kernel/src/inbox/mod.rs
index d2453243f..d7c69cc79 100644
--- a/kernel/src/inbox/mod.rs
+++ b/kernel/src/inbox/mod.rs
@@ -19,18 +19,14 @@
//! process's ring or say something no object said. A post writes at most one
//! entry, and a ring holds at most [`MAX_PENDING_WATCHES`] polls.
//!
-//! **Locks.** What a completion writes, and the page it is written into, sit
-//! behind an [`IrqLock`] of their own, because a device's interrupt handler
-//! posts in place, firing its polls under its list lock; nothing is taken
-//! under it. The rest of a ring, its submissions and its polls, is its
-//! `Lock`'s, which no post reaches. A ring's own watch is an [`IrqWatch`] and
+//! **Locks.** A ring's own lock takes nothing under it and is never taken under
+//! a watch's: a post fires its polls with its list let go. A ring's own watch
//! holds only threads, because no handle names a ring as a thing to watch.
use alloc::sync::Arc;
use alloc::vec::Vec;
use core::sync::atomic::Ordering;
-use toyos_sched::sync::CellLock;
use toyos_sched::task::WaitClass;
use toyos_sched::watch::{Fire, Ring};
@@ -40,7 +36,7 @@ use crate::process::{self, Pid};
use crate::scheduler;
use crate::sync::Lock;
use crate::time::{Deadline, Duration};
-use crate::watch::{IrqLock, IrqWatch};
+use crate::watch::Watch;
use crate::DirectMap;
use toyos_abi::inbox::{
@@ -66,12 +62,7 @@ impl InboxRef {
impl Drop for InboxRef {
fn drop(&mut self) {
- // The page is the completions', so it goes only once no post can reach
- // them. Both halves are taken out under their locks and let go of
- // outside them: the unmap flushes.
- let Some(completions) = self.0.completions.with(Option::take) else {
- unreachable!("an inbox is torn down by its one reference, once");
- };
+ // Taken out under the lock and let go of outside it: the unmap flushes.
let Some(mut state) = self.0.state.lock().take() else {
unreachable!("an inbox is torn down by its one reference, once");
};
@@ -79,7 +70,7 @@ impl Drop for InboxRef {
poll.withdraw();
}
// `Unmapped`'s drop flushes; the `Arc` drop after it frees the pages.
- drop(completions.shm.unmap_from(state.owner_pid));
+ drop(state.shm.unmap_from(state.owner_pid));
}
}
@@ -202,80 +193,68 @@ const MAX_PENDING_WATCHES: usize = 1024;
pub struct Inbox {
/// `None` once the ring's one reference let go of it.
state: Lock<Option<RingState>>,
- /// `None` from the moment that reference starts letting go of it.
- completions: IrqLock<Option<Completions>>,
/// Threads parked in `submit`; never a poll — see the module header.
- watch: IrqWatch,
+ watch: Watch,
}
struct RingState {
- /// The page's address. The page is [`Completions`]'s, which the teardown
- /// lets go of only after it has taken this.
shm_phys: DirectMap,
+ /// A ring's page has no lifetime of its own; it goes with the last handle to the ring.
+ shm: Arc<SharedMemObject>,
submission_size: u32,
+ completion_size: u32,
+ /// The kernel's own copy of the completion tail, the only one it reads.
+ completion_tail: u32,
/// Polls still armed as of the last registration, which sweeps the rest.
pending: Vec<Arc<Poll>>,
owner_pid: Pid,
}
-// No accessor below returns a Rust reference into a ring's page — the process
-// maps it writable, so only atomics or `read_volatile` are sound here.
+impl RingState {
+ // No accessor below returns a Rust reference into this page — the process
+ // maps it writable, so only atomics or `read_volatile` are sound here.
-/// One atomic word of one ring header; never `&RingHeader` — see the block above.
-fn ring_word(page: &DirectMap, ring_off: u64, field_off: usize) -> &core::sync::atomic::AtomicU32 {
- let ptr = page.as_mut_ptr::<u8>();
- // SAFETY: offset is in-bounds and 4-aligned within the 2 MiB page, which outlives both of the ring's halves that name it; `AtomicU32` is sound over memory the process also writes.
- unsafe {
- core::sync::atomic::AtomicU32::from_ptr(
- ptr.add(ring_off as usize + field_off) as *mut u32,
- )
+ /// One atomic word of one ring header; never `&RingHeader` — see the block above.
+ fn ring_word(&self, ring_off: u64, field_off: usize) -> &core::sync::atomic::AtomicU32 {
+ let ptr = self.shm_phys.as_mut_ptr::<u8>();
+ // SAFETY: offset is in-bounds and 4-aligned within the 2 MiB page; `AtomicU32` is sound over memory the process also writes.
+ unsafe {
+ core::sync::atomic::AtomicU32::from_ptr(
+ ptr.add(ring_off as usize + field_off) as *mut u32,
+ )
+ }
}
-}
-impl RingState {
fn submission_head(&self) -> &core::sync::atomic::AtomicU32 {
- ring_word(&self.shm_phys, SUBMISSION_RING_OFF, core::mem::offset_of!(RingHeader, head))
+ self.ring_word(SUBMISSION_RING_OFF, core::mem::offset_of!(RingHeader, head))
}
fn submission_tail(&self) -> &core::sync::atomic::AtomicU32 {
- ring_word(&self.shm_phys, SUBMISSION_RING_OFF, core::mem::offset_of!(RingHeader, tail))
- }
-
- /// One submission entry, copied out by value via `read_volatile` — never a `&Submission`.
- fn submission_at(&self, index: u32) -> Submission {
- let ptr = self.shm_phys.as_mut_ptr::<u8>();
- // SAFETY: `index` is masked by `submission_size` (≤256), keeping the read in-bounds and aligned within the page.
- unsafe { (ptr.add(SUBMISSIONS_OFF as usize + index as usize * core::mem::size_of::<Submission>()) as *const Submission).read_volatile() }
+ self.ring_word(SUBMISSION_RING_OFF, core::mem::offset_of!(RingHeader, tail))
}
-}
-/// What a poll's completion writes, which a post from an interrupt handler
-/// reaches, and the ring's page, which goes only with these.
-struct Completions {
- /// A ring's page has no lifetime of its own; it goes with the last handle to the ring.
- shm: Arc<SharedMemObject>,
- page: DirectMap,
- completion_size: u32,
- /// The kernel's own copy of the completion tail, the only one it reads.
- completion_tail: u32,
-}
-
-impl Completions {
fn completion_head(&self) -> &core::sync::atomic::AtomicU32 {
- ring_word(&self.page, COMPLETION_RING_OFF, core::mem::offset_of!(RingHeader, head))
+ self.ring_word(COMPLETION_RING_OFF, core::mem::offset_of!(RingHeader, head))
}
fn completion_tail_word(&self) -> &core::sync::atomic::AtomicU32 {
- ring_word(&self.page, COMPLETION_RING_OFF, core::mem::offset_of!(RingHeader, tail))
+ self.ring_word(COMPLETION_RING_OFF, core::mem::offset_of!(RingHeader, tail))
}
fn completion_dropped(&self) -> &core::sync::atomic::AtomicU32 {
- ring_word(&self.page, COMPLETION_RING_OFF, core::mem::offset_of!(RingHeader, dropped))
+ self.ring_word(COMPLETION_RING_OFF, core::mem::offset_of!(RingHeader, dropped))
+ }
+
+ /// One submission entry, copied out by value via `read_volatile` — never a `&Submission`.
+ fn submission_at(&self, index: u32) -> Submission {
+ let ptr = self.shm_phys.as_mut_ptr::<u8>();
+ // SAFETY: `index` is masked by `submission_size` (≤256), keeping the read in-bounds and aligned within the page.
+ unsafe { (ptr.add(SUBMISSIONS_OFF as usize + index as usize * core::mem::size_of::<Submission>()) as *const Submission).read_volatile() }
}
/// The address of one completion entry — a pointer, never a `&mut` minted from a shared borrow.
fn completion_at(&self, index: u32) -> *mut Completion {
- let ptr = self.page.as_mut_ptr::<u8>();
+ let ptr = self.shm_phys.as_mut_ptr::<u8>();
// SAFETY: `index` is masked by `completion_size` (≤512), keeping the offset inside the page.
unsafe { ptr.add(COMPLETION_RING_OFF as usize + core::mem::size_of::<RingHeader>() + index as usize * core::mem::size_of::<Completion>()) as *mut Completion }
}
@@ -290,7 +269,7 @@ impl Completions {
return;
}
let idx = tail & (self.completion_size - 1);
- // SAFETY: `idx` is masked to ring size; the completions' lock serializes kernel writers.
+ // SAFETY: `idx` is masked to ring size; the ring's lock serializes kernel writers.
unsafe { self.completion_at(idx).write(Completion { token: user_data, result, flags }) };
self.completion_tail = tail.wrapping_add(1);
self.completion_tail_word().store(tail.wrapping_add(1), Ordering::Release);
@@ -313,23 +292,15 @@ impl Inbox {
/// Post one completion and wake whoever waits in `submit`. A ring already
/// torn down takes nothing and wakes nobody.
fn complete(&self, user_data: u64, result: i32) {
- let posted = self.completions.with(|c| {
- c.as_mut().map(|c| c.post_completion(user_data, result, 0))
- });
- if posted.is_some() {
- // In place: an interrupt handler's post reaches here through the
- // poll it fires.
- self.watch.post_in_place();
+ let posted = self.with_state(|state| state.post_completion(user_data, result, 0));
+ if posted.is_ok() {
+ self.watch.post();
}
}
fn with_state<R>(&self, f: impl FnOnce(&mut RingState) -> R) -> Result<R, SyscallError> {
self.state.lock().as_mut().map(f).ok_or(SyscallError::NotFound)
}
-
- fn with_completions<R>(&self, f: impl FnOnce(&Completions) -> R) -> Result<R, SyscallError> {
- self.completions.with(|c| c.as_ref().map(f)).ok_or(SyscallError::NotFound)
- }
}
/// Largest submission ring a process may ask for.
@@ -386,17 +357,14 @@ pub fn create(depth: u32) -> Result<(InboxRef, u64), SyscallError> {
let inbox = Arc::new(Inbox {
state: Lock::new(Some(RingState {
shm_phys,
- submission_size,
- pending: Vec::new(),
- owner_pid: pid,
- })),
- completions: IrqLock::new(Some(Completions {
shm,
- page: shm_phys,
+ submission_size,
completion_size,
completion_tail: 0,
+ pending: Vec::new(),
+ owner_pid: pid,
})),
- watch: IrqWatch::new(),
+ watch: Watch::new(),
});
Ok((InboxRef(inbox), shm_vaddr))
}
@@ -423,7 +391,7 @@ pub fn submit(
}
loop {
- let (count, dropped) = inbox.with_completions(|c| (c.completion_count(), c.dropped()))?;
+ let (count, dropped) = inbox.with_state(|s| (s.completion_count(), s.dropped()))?;
if count >= min_complete || min_complete == 0 {
return Ok(count);
@@ -450,7 +418,7 @@ pub fn submit(
0,
WaitClass::Io,
deadline,
- || inbox.with_completions(|c| c.completion_count()).map_or(true, |n| n >= min_complete),
+ || inbox.with_state(|s| s.completion_count()).map_or(true, |n| n >= min_complete),
)
.is_err()
{
diff --git a/kernel/src/irq_ring.rs b/kernel/src/irq_ring.rs
index a71e5204c..6d2c72165 100644
--- a/kernel/src/irq_ring.rs
+++ b/kernel/src/irq_ring.rs
@@ -10,23 +10,19 @@ use crate::arch::percpu;
use crate::scheduler::MAX_CPUS;
/// Interrupt sources that drive scheduling; exhaustive, so a new variant requires updating every `match`.
-/// The discriminant is `trace::Kind::IrqDrain`'s top byte, pinned for the reason `Kind` is.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum IrqSource {
- Xhci = 2,
- I8042 = 3,
+ Audio,
+ /// Every vector a claimed PCI function delivers on, coalesced into one
+ /// slot: the record only says a pass is owed, and `pcidev` keeps the
+ /// per-slot flag that says whose.
+ UserDev,
+ Xhci,
+ I8042,
}
impl IrqSource {
- pub const COUNT: usize = 2;
-
- /// This source's record in a CPU's slots.
- const fn slot(self) -> usize {
- match self {
- Self::Xhci => 0,
- Self::I8042 => 1,
- }
- }
+ pub const COUNT: usize = 4;
}
/// 64-byte aligned so two CPUs' slots never share a cache line.
@@ -40,7 +36,7 @@ static SLOTS: [CpuSlots; MAX_CPUS] =
pub fn isr_publish(source: IrqSource, timestamp_nanos: u64) {
// MSI-X vectors are configured after clock calibration, so a real IRQ never stamps 0.
assert!(timestamp_nanos != 0, "irq_ring: zero IRQ timestamp");
- let slot = &SLOTS[percpu::cpu_id() as usize].0[source.slot()];
+ let slot = &SLOTS[percpu::cpu_id() as usize].0[source as usize];
// ISRs run with IF=0, so this load-then-store can't interleave with a same-CPU `take`.
if slot.load(Ordering::Relaxed) == 0 {
slot.store(timestamp_nanos, Ordering::Relaxed);
@@ -49,7 +45,7 @@ pub fn isr_publish(source: IrqSource, timestamp_nanos: u64) {
/// Consumes the current CPU's pending record for `source`, returning its IRQ-time timestamp.
pub fn take(source: IrqSource) -> Option<u64> {
- let slot = &SLOTS[percpu::cpu_id() as usize].0[source.slot()];
+ let slot = &SLOTS[percpu::cpu_id() as usize].0[source as usize];
// Atomic swap: an interrupting ISR sees either the old record or the cleared slot, never a torn value.
match slot.swap(0, Ordering::Relaxed) {
0 => None,
@@ -63,7 +59,7 @@ pub fn take(source: IrqSource) -> Option<u64> {
/// True if `source` has an undrained record on this CPU; non-consuming, unlike [`take`].
pub fn pending(source: IrqSource) -> bool {
- SLOTS[percpu::cpu_id() as usize].0[source.slot()].load(Ordering::Relaxed) != 0
+ SLOTS[percpu::cpu_id() as usize].0[source as usize].load(Ordering::Relaxed) != 0
}
/// True if any IRQ record is undrained on the current CPU; non-consuming.
diff --git a/kernel/src/object/ops.rs b/kernel/src/object/ops.rs
index ac7edc38b..20385f95d 100644
--- a/kernel/src/object/ops.rs
+++ b/kernel/src/object/ops.rs
@@ -17,8 +17,7 @@ use crate::time::Deadline;
use crate::pipe::{self, PipeId};
use crate::process::PipeMap;
use crate::user_ptr::{UserBytes, UserBytesMut};
-use crate::inbox::PollEntry;
-use crate::watch::{IrqWatch, Watch};
+use crate::watch::Watch;
use crate::{device as device_registry, keyboard, mouse};
use super::device::DeviceClaim;
@@ -243,24 +242,14 @@ pub fn pipe_write(object: &KObjectRef) -> Option<(PipeId, WaitClass)> {
pub enum WatchRef {
Static(&'static Watch),
Shared(Arc<Watch>),
- /// A device's, which its interrupt handler posts.
- Irq(&'static IrqWatch),
}
-impl WatchRef {
- pub(crate) fn add_poll(&self, entry: PollEntry) {
+impl core::ops::Deref for WatchRef {
+ type Target = Watch;
+ fn deref(&self) -> &Watch {
match self {
- Self::Static(watch) => watch.add_poll(entry),
- Self::Shared(watch) => watch.add_poll(entry),
- Self::Irq(watch) => watch.add_poll(entry),
- }
- }
-
- pub fn cancel_polls(&self) {
- match self {
- Self::Static(watch) => watch.cancel_polls(),
- Self::Shared(watch) => watch.cancel_polls(),
- Self::Irq(watch) => watch.cancel_polls(),
+ Self::Static(watch) => watch,
+ Self::Shared(watch) => watch,
}
}
}
@@ -277,10 +266,10 @@ pub fn read_watch(object: &KObjectRef) -> Option<WatchRef> {
device_registry::DeviceType::Keyboard => Some(WatchRef::Static(&keyboard::WATCH)),
device_registry::DeviceType::Mouse => Some(WatchRef::Static(&mouse::WATCH)),
device_registry::DeviceType::PciFunction => {
- d.pci_slot().map(|slot| WatchRef::Irq(crate::pcidev::watch(slot)))
+ d.pci_slot().map(|slot| WatchRef::Static(crate::pcidev::watch(slot)))
}
device_registry::DeviceType::HdaAudio | device_registry::DeviceType::VirtioSound => {
- Some(WatchRef::Irq(&crate::drivers::AUDIO_WATCH))
+ Some(WatchRef::Static(&crate::drivers::AUDIO_WATCH))
}
device_registry::DeviceType::Framebuffer => None,
// A partition answers its description and has nothing to wait for.
diff --git a/kernel/src/pcidev/mod.rs b/kernel/src/pcidev/mod.rs
index d87b5cd29..f3efd833b 100644
--- a/kernel/src/pcidev/mod.rs
+++ b/kernel/src/pcidev/mod.rs
@@ -117,7 +117,7 @@ use crate::mm::policy::{CachePolicy, MmioPolicy};
use crate::mm::{align_2m, DirectMap, Mmio, PAGE_2M};
use crate::object::shm::{Region, SharedMemObject};
use crate::sync::Lock;
-use crate::watch::IrqWatch;
+use crate::watch::Watch;
/// How many functions this machine can hand out at once.
///
@@ -280,7 +280,7 @@ static BOUND: [Lock<Option<Bound>>; MAX_FUNCTIONS] =
/// What a claimed function's poll waits on, one per slot: two processes each driving a
/// function must not learn when the other's device is busy.
-static WATCHES: [IrqWatch; MAX_FUNCTIONS] = [const { IrqWatch::new() }; MAX_FUNCTIONS];
+static WATCHES: [Watch; MAX_FUNCTIONS] = [const { Watch::new() }; MAX_FUNCTIONS];
/// Every function this machine enumerated, and the two windows a BAR may be
/// moved into.
@@ -1663,11 +1663,7 @@ pub fn take_record(slot: usize) -> Result<Option<DeviceIrqRecord>, SyscallError>
if IRQ[slot].faulted() {
return Err(SyscallError::Io);
}
- let taken = IRQ[slot].take();
- if taken.is_some() && IRQ[slot].take_unannounced() {
- log!("pcidev: slot {slot} took its first message on vector {:#x}", VECTORS[slot]);
- }
- Ok(taken.map(|count| DeviceIrqRecord { count }))
+ Ok(IRQ[slot].take().map(|count| DeviceIrqRecord { count }))
}
/// Whether a read of the claim answers at once: a message is waiting, or the
@@ -1676,26 +1672,46 @@ pub fn has_irq(slot: usize) -> bool {
IRQ[slot].armed() || IRQ[slot].faulted()
}
-/// Records one message and posts the claim's watch. Called from the vector's
-/// handler, so it allocates nothing; `record.rs` owns the counting, and
-/// `kernel-loom` models it against a concurrent reader.
+/// Records one message. Called from the vector's ISR, so it takes no lock and
+/// allocates nothing; `record.rs` owns the counting, and `kernel-loom` models
+/// it against a concurrent reader.
pub fn isr(slot: usize) {
IRQ[slot].took();
- WATCHES[slot].post_in_place();
+}
+
+/// Turn every message taken since the last pass into a wake.
+///
+/// On the scheduler pass rather than in the ISR, like every other device in
+/// this kernel: a wake takes the inbox lock and an ISR may not.
+pub fn drain_pending() {
+ for (slot, irq) in IRQ.iter().enumerate() {
+ if !irq.take_pending() {
+ continue;
+ }
+ // A fault's wake is no message.
+ if !irq.faulted() && irq.take_unannounced() {
+ log!(
+ "pcidev: slot {slot} took its first message on vector {:#x}",
+ VECTORS[slot]
+ );
+ }
+ WATCHES[slot].post();
+ }
}
/// The unit refused this function an access.
///
-/// Called from the fault handler: every call the claim answers refuses from
-/// here on, its interrupt read included, and the post wakes whoever waits on
-/// the claim to read that refusal, as a message's does.
+/// Called from the fault handler, which takes no lock: every call the claim
+/// answers refuses from here on, its interrupt read included, and this CPU's
+/// next scheduler pass wakes whoever waits on the claim to read that refusal —
+/// the pass a message earns, posted the way its ISR posts it.
pub fn note_fault(slot: usize) {
IRQ[slot].fault();
- WATCHES[slot].post_in_place();
+ crate::irq_ring::isr_publish(crate::irq_ring::IrqSource::UserDev, crate::clock::nanos_since_boot());
crate::preempt::set_need_resched();
}
/// The watch of the function a claim holds at `slot`.
-pub fn watch(slot: usize) -> &'static IrqWatch {
+pub fn watch(slot: usize) -> &'static Watch {
&WATCHES[slot]
}
diff --git a/kernel/src/pcidev/record.rs b/kernel/src/pcidev/record.rs
index 72ff55f33..51f27139a 100644
--- a/kernel/src/pcidev/record.rs
+++ b/kernel/src/pcidev/record.rs
@@ -6,16 +6,20 @@
//! not an ordering, and no guest test in this suite lands on it.
//!
//! **Two parties race**: the ISR, on whichever CPU the unit routed the message
-//! to, and the holder reading its record through a syscall on any CPU.
+//! to, and the holder reading its record through a syscall on any CPU. The
+//! scheduler pass that turns a message into a wake runs on the ISR's own CPU
+//! after it, so those two do not interleave.
//!
-//! **The invariant is that every message is counted exactly once.** The count
-//! is a read-modify-write on both sides and never a load followed by a store:
-//! a reader that loaded a count and then cleared it drops every message the ISR
-//! recorded in between, and a driver that misses one waits for a device that
-//! has already spoken. No ordering carries anything across these words — each
-//! is the whole of what it says — so the orderings here are `Relaxed` and the
-//! model is about the interleaving; the holder reads them after the wake the
-//! claim's watch post owes it, which orders them.
+//! **The invariant is that every message is counted exactly once, and owes
+//! exactly one wake.** Both are read-modify-writes and neither is a load
+//! followed by a store: a reader that loaded a count and then cleared it drops
+//! every message the ISR recorded in between, and a driver that misses one
+//! waits for a device that has already spoken. No ordering carries anything
+//! across these words — each is the whole of what it says — so the orderings
+//! here are `Relaxed` and the model is about the interleaving, **but for one
+//! edge**: a fault arms the same wake a message does, and the pass that takes
+//! that wake has to read the fault, so `pending` is released by [`Interrupt::fault`]
+//! and acquired by [`Interrupt::take_pending`].
#[cfg(not(feature = "loom"))]
use core::sync::atomic::{AtomicBool, AtomicU32, Ordering};
@@ -28,13 +32,16 @@ use loom::sync::atomic::{AtomicBool, AtomicU32, Ordering};
/// control for and `kernel-loom` is the model of.
const ORDER: Ordering = Ordering::Relaxed;
-/// The negative control: the read-modify-writes become a load and a store,
+/// The negative control: the two read-modify-writes become a load and a store,
/// which is the whole of what this record's design is. Never on in a kernel
/// build.
#[cfg(feature = "device-irq-lossy")]
macro_rules! take_word {
- ($word:expr, $empty:expr) => {{
- let held = $word.load(ORDER);
+ ($word:expr, $empty:expr) => {
+ take_word!($word, $empty, ORDER)
+ };
+ ($word:expr, $empty:expr, $order:expr) => {{
+ let held = $word.load($order);
$word.store($empty, ORDER);
held
}};
@@ -42,7 +49,10 @@ macro_rules! take_word {
#[cfg(not(feature = "device-irq-lossy"))]
macro_rules! take_word {
($word:expr, $empty:expr) => {
- $word.swap($empty, ORDER)
+ take_word!($word, $empty, ORDER)
+ };
+ ($word:expr, $empty:expr, $order:expr) => {
+ $word.swap($empty, $order)
};
}
@@ -62,10 +72,12 @@ macro_rules! bump {
/// What the ISR writes and the claim reads back.
///
-/// Atomics only: the handler allocates nothing.
+/// Atomics only: the handler takes no lock and allocates nothing.
pub struct Interrupt {
/// Messages since the holder's last read.
count: AtomicU32,
+ /// Set by the ISR, cleared by the scheduler pass that turns it into a wake.
+ pending: AtomicBool,
/// The unit refused this function an access. Every call the claim answers
/// refuses from here on: its bus mastering is gone, so a driver that kept
/// going would be driving nothing.
@@ -85,6 +97,7 @@ impl Interrupt {
pub const fn new() -> Self {
Self {
count: AtomicU32::new(0),
+ pending: AtomicBool::new(false),
faulted: AtomicBool::new(false),
unannounced: AtomicBool::new(true),
}
@@ -96,6 +109,7 @@ impl Interrupt {
pub fn new() -> Self {
Self {
count: AtomicU32::new(0),
+ pending: AtomicBool::new(false),
faulted: AtomicBool::new(false),
unannounced: AtomicBool::new(true),
}
@@ -108,6 +122,12 @@ impl Interrupt {
/// two of these, and what it took plus what is left has to be what arrived.
pub fn took(&self) {
bump!(self.count);
+ // `swap` and not a store: [`Self::fault`] releases through this same
+ // word, and a plain write landing after that release in `pending`'s
+ // modification order ends the release sequence there — the pass that
+ // later takes the fault's wake would then synchronize with nothing.
+ // An RMW extends the sequence instead, whichever order it lands in.
+ self.pending.swap(true, ORDER);
}
/// The messages since the last read, or `None` for none.
@@ -127,19 +147,36 @@ impl Interrupt {
self.count.load(ORDER) != 0
}
+ /// Whether a wake is owed, taken at most once per message. Answers `true`
+ /// for the pass that owes it and `false` for every pass after.
+ ///
+ /// `swap` for the same reason as [`Self::take`]: two passes that both
+ /// loaded `true` would both wake one message's watchers. `Acquire`, so
+ /// the pass that takes a fault's wake reads [`Self::faulted`] set.
+ pub fn take_pending(&self) -> bool {
+ take_word!(self.pending, false, Ordering::Acquire)
+ }
+
/// Whether this is the first message this slot has taken. Answers `true`
/// once per claim and `false` ever after, so a caller may log on it.
///
- /// `swap` for [`Self::take`]'s reason: two reads that both loaded `true`
- /// would both announce one message.
+ /// `swap` for [`Self::take_pending`]'s reason: two passes that both loaded
+ /// `true` would both announce one message.
pub fn take_unannounced(&self) -> bool {
take_word!(self.unannounced, false)
}
- /// The unit refused this function an access. Called from the fault handler:
- /// every call the claim answers refuses from here on.
+ /// The unit refused this function an access. Called from the fault handler,
+ /// which takes no lock: every call the claim answers refuses from here on,
+ /// and a wake is owed as for a message, because a holder waiting on the
+ /// claim would otherwise wait for a function that can no longer speak.
+ ///
+ /// The wake is a `swap` and not a store: this one races a pass on another
+ /// CPU, and loom 0.7 lets a plain store be lost to a concurrent `swap`, which
+ /// C11 forbids, so a store here is a wake the model cannot show is owed.
pub fn fault(&self) {
self.faulted.store(true, ORDER);
+ self.pending.swap(true, Ordering::Release);
}
pub fn faulted(&self) -> bool {
@@ -150,6 +187,7 @@ impl Interrupt {
/// up. The holder is not running at either point.
pub fn clear(&self) {
self.count.store(0, ORDER);
+ self.pending.store(false, ORDER);
self.faulted.store(false, ORDER);
self.unannounced.store(true, ORDER);
}
diff --git a/kernel/src/sched/driver.rs b/kernel/src/sched/driver.rs
index 800611f60..14a17ad7a 100644
--- a/kernel/src/sched/driver.rs
+++ b/kernel/src/sched/driver.rs
@@ -671,6 +671,18 @@ fn drain_irqs(entered: super::dump::Entered) {
super::dump::serve_if_owed();
// Repaints the panel if whoever owns the screen has drawn over the report.
crate::drivers::panic_console::hold_report();
+
+ if crate::irq_ring::take(crate::irq_ring::IrqSource::UserDev).is_some() {
+ // Which claim it was is the per-slot flag `pcidev` keeps; the record
+ // here says only that a pass is owed, so one function's interrupt does
+ // not wake every user driver in the machine.
+ crate::pcidev::drain_pending();
+ }
+ if crate::irq_ring::take(crate::irq_ring::IrqSource::Audio).is_some() {
+ // Both backends share one watch, so a second would need the parking side
+ // to know which driver bound, which it doesn't.
+ crate::drivers::AUDIO_WATCH.post();
+ }
}
/// Leave the current stack for this CPU's idle stack and never come back.
diff --git a/kernel/src/sched/payload.rs b/kernel/src/sched/payload.rs
index 34d737df6..523e2a6ed 100644
--- a/kernel/src/sched/payload.rs
+++ b/kernel/src/sched/payload.rs
@@ -8,7 +8,7 @@ use core::sync::atomic::{AtomicU32, AtomicU64, Ordering};
use toyos_sched::fair::{FairShare, ShareState};
use toyos_sched::hw::Nanos;
use toyos_sched::msg::Msg;
-use toyos_sched::sync::CellLock;
+use toyos_sched::sync::LeafLock;
use toyos_sched::task::{SchedPayload, TaskAccounting, TaskShared, WaitClass};
use toyos_sched::park::WaitTicket;
@@ -28,7 +28,7 @@ impl<T> KernelLock<T> {
}
}
-impl<T: Send> CellLock<T> for KernelLock<T> {
+impl<T: Send> LeafLock<T> for KernelLock<T> {
fn with<R>(&self, f: impl FnOnce(&mut T) -> R) -> R {
f(&mut self.0.lock())
}
diff --git a/kernel/src/watch.rs b/kernel/src/watch.rs
index 8697bb067..42c8a0b51 100644
--- a/kernel/src/watch.rs
+++ b/kernel/src/watch.rs
@@ -9,10 +9,9 @@
//! record of what was posted — a waiter re-reads the object, never the post.
//!
//! **A post allocates nothing and may be made under any lock but a poll
-//! ring's** (`crate::inbox`). A watch an interrupt handler posts is an
-//! [`IrqWatch`]: its list sits behind an [`IrqLock`], and its post is made in
-//! place and frees nothing. Every other watch's list lock leaves interrupts
-//! open, and no handler takes it. Registration allocates, in the syscall that registers.
+//! ring's** (`crate::inbox`), so a driver posts from its scheduler-pass drain
+//! and never from its ISR, which publishes to `irq_ring` and nothing else.
+//! Registration allocates, in the syscall that registers.
//!
//! A [`Watch`] is a borrowed reference for the whole of a wait: [`Armed`]
//! holds it, so an object cannot be freed under a thread waiting on it, and
@@ -21,7 +20,6 @@
use alloc::sync::Arc;
use toyos_sched::hw::Nanos;
-use toyos_sched::sync::CellLock;
use toyos_sched::task::{Refused, WaitClass, WakeCause, WakeReason};
use toyos_sched::watch::{Poster, Waiters};
@@ -34,64 +32,14 @@ use crate::sched::payload::{KMsg, KShared, KernelLock, TaskHandle};
use crate::scheduler::Parkable;
use crate::time::Deadline;
-type List = Waiters<KMsg, PollEntry>;
+type Inner = toyos_sched::watch::Watch<KMsg, PollEntry, KernelLock<Waiters<KMsg, PollEntry>>>;
-/// What an object holds to be waitable, its list behind `L`.
-pub struct Waitable<L: CellLock<List>>(toyos_sched::watch::Watch<KMsg, PollEntry, L>);
-
-/// A watch no interrupt handler reaches.
-pub type Watch = Waitable<KernelLock<List>>;
-
-/// A watch an interrupt handler posts, and the watch of a ring such a post
-/// completes into. It has no `post`, which frees: [`IrqWatch::post_in_place`]
-/// frees nothing.
-pub type IrqWatch = Waitable<IrqLock<List>>;
-
-/// What an interrupt handler's post takes: an [`IrqWatch`]'s list and a poll
-/// ring's completions. Held with interrupts off, so a handler never finds one
-/// held by the context it interrupted; nothing allocates or frees under it.
-pub struct IrqLock<T>(masked::Masked<T>);
-
-impl<T> IrqLock<T> {
- pub const fn new(value: T) -> Self {
- Self(masked::Masked::new(value))
- }
-}
-
-impl<T: Send> CellLock<T> for IrqLock<T> {
- fn with<R>(&self, f: impl FnOnce(&mut T) -> R) -> R {
- // Raised first, so the lock's own release never reaches depth zero, and
- // a pass, with interrupts masked.
- preempt_off(|_| {
- let irq = crate::arch::IrqGuard::close();
- let mut held = self.0.lock(&irq);
- f(&mut held)
- })
- }
-}
-
-mod masked {
- use crate::arch::IrqGuard;
- use crate::sync::{Lock, LockGuard};
-
- /// A lock taken only through a borrow of a closed [`IrqGuard`]: never
- /// before the mask, and never held past it.
- pub struct Masked<T>(Lock<T>);
-
- impl<T> Masked<T> {
- pub const fn new(value: T) -> Self {
- Self(Lock::new(value))
- }
-
- pub fn lock<'a>(&'a self, _closed: &'a IrqGuard) -> LockGuard<'a, T> {
- self.0.lock()
- }
- }
-}
+/// What an object holds to be waitable.
+pub struct Watch(Inner);
impl Watch {
pub const fn new() -> Self {
- Self(toyos_sched::watch::Watch::new(KernelLock::new(Waiters::new())))
+ Self(Inner::new(KernelLock::new(Waiters::new())))
}
/// Something about the object changed: wake every thread waiting on it and
@@ -134,44 +82,23 @@ impl Watch {
)
})
}
-}
-impl IrqWatch {
- pub const fn new() -> Self {
- Self(toyos_sched::watch::Watch::new(IrqLock::new(Waiters::new())))
- }
-
- /// Something about the object changed: wake every thread waiting on it and
- /// complete every poll where it stands, freeing nothing. A handler makes it
- /// with its CPU's preempt count raised, as `device_irq_entry` holds it, so
- /// this post's own never reaches zero, and a pass, inside the interrupt.
- pub fn post_in_place(&self) {
- preempt_off(|p| {
- let env = Poster { cpus: cpus(), kicker: &HW, preempt: p };
- self.0.post_in_place(WakeCause::new(WakeReason::Woken), &env);
- });
- }
-}
-
-impl<L: CellLock<List>> Waitable<L> {
/// A poll ring's entry, from `inbox`'s registration and nowhere else.
pub(crate) fn add_poll(&self, entry: PollEntry) {
self.0.add_ring(entry);
}
/// Answer every poll registered here as gone: the source it watched ended.
- /// A thread's, since it frees what it answered.
pub fn cancel_polls(&self) {
self.0.cancel_rings();
}
-
}
/// A thread's registration on one watch, held across its wait and ended by
/// its drop.
#[must_use = "a registration must outlive the park it was made for"]
-pub struct Armed<'a, L: CellLock<List> = KernelLock<List>> {
- watch: &'a Waitable<L>,
+pub struct Armed<'a> {
+ watch: &'a Watch,
shared: Arc<KShared>,
task: Arc<TaskHandle>,
/// Wait class for the blocked-time breakdown; the park carries no subject
@@ -179,7 +106,7 @@ pub struct Armed<'a, L: CellLock<List> = KernelLock<List>> {
class: WaitClass,
}
-impl<L: CellLock<List>> Drop for Armed<'_, L> {
+impl Drop for Armed<'_> {
fn drop(&mut self) {
self.watch.0.unregister(&self.shared);
}
@@ -187,11 +114,7 @@ impl<L: CellLock<List>> Drop for Armed<'_, L> {
/// Register the running task on `watch`; `None` when there is no current task.
/// Call before reading the condition the wait is for.
-pub fn arm<L: CellLock<List>>(
- watch: &Waitable<L>,
- token: u64,
- class: WaitClass,
-) -> Option<Armed<'_, L>> {
+pub fn arm(watch: &Watch, token: u64, class: WaitClass) -> Option<Armed<'_>> {
let task = crate::sched::driver::current_handle()?;
let shared = crate::sched::driver::current_shared()?;
watch.0.register(&shared, token);
@@ -221,11 +144,7 @@ fn not_revocable() -> ! {
/// this thread is cancelled. A return is not an answer — the caller re-reads
/// its condition.
#[track_caller]
-pub fn wait<L: CellLock<List>>(
- p: &Parkable,
- armed: &Armed<'_, L>,
- deadline: Deadline,
-) -> Result<(), Cancelled> {
+pub fn wait(p: &Parkable, armed: &Armed<'_>, deadline: Deadline) -> Result<(), Cancelled> {
match wait_inner(p, armed, deadline, Cancel::Answers) {
Ok(()) => Ok(()),
Err(Ended::Cancelled) => Err(Cancelled(())),
@@ -235,7 +154,7 @@ pub fn wait<L: CellLock<List>>(
/// The same as [`wait`], for a wait a kill may not end.
#[track_caller]
-pub fn wait_uncancellable<L: CellLock<List>>(p: &Parkable, armed: &Armed<'_, L>, deadline: Deadline) {
+pub fn wait_uncancellable(p: &Parkable, armed: &Armed<'_>, deadline: Deadline) {
match wait_inner(p, armed, deadline, Cancel::Ignores) {
Ok(()) => {}
Err(Ended::Cancelled) => unreachable!("an uncancellable wait never reports a cancel"),
@@ -249,9 +168,9 @@ pub fn wait_uncancellable<L: CellLock<List>>(p: &Parkable, armed: &Armed<'_, L>,
/// be waited on again safely, and the caller reads its own condition again to
/// tell them apart.
#[track_caller]
-pub fn wait_until<L: CellLock<List>>(
+pub fn wait_until(
p: &Parkable,
- watch: &Waitable<L>,
+ watch: &Watch,
token: u64,
class: WaitClass,
deadline: Deadline,
@@ -297,9 +216,9 @@ pub fn wait_uncancellable_until(p: &Parkable, watch: &Watch, token: u64, ready:
}
#[track_caller]
-fn wait_inner<L: CellLock<List>>(
+fn wait_inner(
_p: &Parkable,
- armed: &Armed<'_, L>,
+ armed: &Armed<'_>,
deadline: Deadline,
cancel: Cancel,
) -> Result<(), Ended> {
diff --git a/toyos-sched/src/cpu.rs b/toyos-sched/src/cpu.rs
index 6700a829a..5be981f47 100644
--- a/toyos-sched/src/cpu.rs
+++ b/toyos-sched/src/cpu.rs
@@ -2401,13 +2401,13 @@ mod tests {
use crate::fair::{FairShare, ShareState};
use crate::hw::{Kicker, Machine};
use crate::mailbox::{mailbox, NoPreempt};
- use crate::sync::CellLock;
+ use crate::sync::LeafLock;
use crate::task::{RtState, TaskAccounting, TaskBuilder};
use std::sync::Mutex;
struct TestLock<T>(Mutex<T>);
- impl<T: Send> CellLock<T> for TestLock<T> {
+ impl<T: Send> LeafLock<T> for TestLock<T> {
fn with<R>(&self, f: impl FnOnce(&mut T) -> R) -> R {
f(&mut self.0.lock().expect("a test never poisons a lock"))
}
diff --git a/toyos-sched/src/fair.rs b/toyos-sched/src/fair.rs
index cbcd46030..95aa1256a 100644
--- a/toyos-sched/src/fair.rs
+++ b/toyos-sched/src/fair.rs
@@ -5,7 +5,7 @@
use core::num::NonZeroU32;
use core::sync::atomic::{AtomicU64, Ordering};
-use crate::sync::CellLock;
+use crate::sync::LeafLock;
/// Clamp for stored lag at the Runnable→NonRunnable transition: how far
/// behind (entitled catch-up) or ahead (throttled on wake) of the frontier a
@@ -206,13 +206,13 @@ fn vrt_from_lag(frontier: u64, lag: i64) -> u64 {
}
/// One process's fair-share pot, reached through any thread that owns it. The
-/// cell is supplied by the environment for the reason stated on [`CellLock`]:
+/// cell is supplied by the environment for the reason stated on [`LeafLock`]:
/// the kernel's is a word-sized spin, the simulator's a mutex.
pub struct FairShare<L> {
state: L,
}
-impl<L: CellLock<ShareState>> FairShare<L> {
+impl<L: LeafLock<ShareState>> FairShare<L> {
pub fn new(state: L) -> Self {
Self { state }
}
diff --git a/toyos-sched/src/sync.rs b/toyos-sched/src/sync.rs
index 1a08ca171..b0e1a9827 100644
--- a/toyos-sched/src/sync.rs
+++ b/toyos-sched/src/sync.rs
@@ -20,12 +20,13 @@ pub use loom::sync::Arc;
/// Interior mutability for a small shared cell, supplied by the environment:
/// a watch's waiter list and the per-process fair share. The kernel's
-/// implementor is never held across a pass or a switch; the simulator and the
-/// loom models supply their own.
+/// implementor is a few-instruction, IRQ-off leaf lock that acquires nothing
+/// beneath it and is never held across a pass or a switch; the simulator and
+/// the loom models supply their own.
///
/// It lives here rather than in one of its users because the core crate may
/// not implement a lock itself — that would need `unsafe`, which only
/// `mailbox.rs` is allowed to write.
-pub trait CellLock<T>: Sync {
+pub trait LeafLock<T>: Sync {
fn with<R>(&self, f: impl FnOnce(&mut T) -> R) -> R;
}
diff --git a/toyos-sched/src/task.rs b/toyos-sched/src/task.rs
index 1c490d24c..24f997990 100644
--- a/toyos-sched/src/task.rs
+++ b/toyos-sched/src/task.rs
@@ -13,7 +13,7 @@ use crate::fair::{FairShare, ShareState, QUANTUM_NS};
use crate::hw::{CpuId, Nanos};
use crate::mailbox::MailboxNode;
use crate::msg::Msg;
-use crate::sync::{Arc, AtomicBool, AtomicU64, CellLock, Ordering};
+use crate::sync::{Arc, AtomicBool, AtomicU64, LeafLock, Ordering};
use crate::park::CommittedTicket;
/// Monotonic, never reused. Stale messages keyed by `TaskKey` are provably
@@ -31,8 +31,8 @@ pub trait SchedPayload: Sized + Send + 'static {
/// The cell the per-process [`FairShare`] lives in. Supplied by the
/// environment because the core crate may not implement a lock itself
- /// (see [`CellLock`]).
- type ShareLock: CellLock<ShareState> + Send;
+ /// (see [`LeafLock`]).
+ type ShareLock: LeafLock<ShareState> + Send;
}
/// Shorthand for the share type a payload implies.
diff --git a/toyos-sched/src/watch.rs b/toyos-sched/src/watch.rs
index 1b9fcb795..6108818e2 100644
--- a/toyos-sched/src/watch.rs
+++ b/toyos-sched/src/watch.rs
@@ -15,22 +15,16 @@
//! waiting side does between its registration and its park can be skipped:
//! `park::prepare` consumes the flag, and the commit consumes the claim.
//!
-//! **Nothing allocates or frees under the list lock**, because an interrupt
-//! handler's post can interrupt the allocator's holder while another CPU holds
-//! the list waiting for the allocator. A registration takes the entries that
-//! can no longer fire out [`FEW`] at a time, grows a full list in a buffer
-//! allocated with the lock let go, and drops both with it let go.
-//! [`Watch::post`] takes every ring entry out and fires and frees them with the
-//! lock let go; [`Watch::post_in_place`], for a handler, which may not free at
-//! all, fires them where they stand, and later registrations sweep the dead
-//! out, since an entry is one-shot.
+//! **A post allocates nothing.** Every ring entry is one-shot, so a post takes
+//! them all out of the list and fires them with the list lock let go; what it
+//! frees is those entries. A registration sweeps the entries a withdrawal left
+//! behind, so the list never holds more than the polls live at its last post or
+//! registration, plus that one.
//!
-//! **Lock order.** A post in place fires its rings under the list lock, so
-//! beneath it are each ring's own lock and the watch that ring's submitters
-//! park on, which holds threads and no ring and so nests nothing. The drop of
-//! every entry the list lets go of runs with it let go, because an entry's last
-//! reference may own another watch. So a post may be made under any lock but a
-//! ring's own.
+//! **Lock order.** The list lock is a leaf the environment supplies:
+//! [`Ring::fire`] is always called with it let go, and so is the drop of every
+//! entry the list lets go of, because an entry's last reference may own another
+//! watch. So a post may be made under any lock but a ring's own.
use alloc::vec::Vec;
use core::marker::PhantomData;
@@ -39,7 +33,7 @@ use crate::cpu::CpuHandles;
use crate::hw::Kicker;
use crate::mailbox::{PreemptGuard, SchedMsg};
use crate::park::{notify, revoke};
-use crate::sync::{fence, Arc, AtomicU32, CellLock, Ordering};
+use crate::sync::{fence, Arc, AtomicU32, LeafLock, Ordering};
use crate::task::{TaskShared, WakeCause};
/// Why a ring entry is posted.
@@ -56,16 +50,14 @@ pub enum Fire {
pub trait Ring {
/// Post this poll's completion. One-shot across every watch the poll is
/// registered on: an entry that already fired, or whose poll was withdrawn,
- /// posts nothing. Called with at most the posting watch's list lock held,
- /// and from an interrupt handler by a post in place: may take only its
- /// ring's own lock and post only the watch its ring's submitters park on,
- /// and allocates and frees nothing.
+ /// posts nothing. Called with no watch's list lock held; may take only its
+ /// ring's own lock and post only the watch its ring's submitters park on.
fn fire(&self, how: Fire);
/// Whether a fire would still post anything. `false` is permanent.
fn live(&self) -> bool;
}
-/// Everything a watch holds.
+/// Everything a watch holds, behind the environment's leaf lock.
pub struct Waiters<M, R> {
/// In registration order, so a bounded post reaches the longest waiter
/// first.
@@ -105,16 +97,12 @@ pub struct Poster<'a, M, K, P> {
pub preempt: &'a P,
}
-/// How many ring entries one section takes out of the list to drop with the
-/// lock let go, on the stack and so allocating nothing.
-pub const FEW: usize = 4;
-
-pub struct Watch<M, R: Ring, L: CellLock<Waiters<M, R>>> {
+pub struct Watch<M, R: Ring, L: LeafLock<Waiters<M, R>>> {
list: L,
_msg: PhantomData<fn() -> (M, R)>,
}
-impl<M, R: Ring, L: CellLock<Waiters<M, R>>> Watch<M, R, L> {
+impl<M, R: Ring, L: LeafLock<Waiters<M, R>>> Watch<M, R, L> {
pub const fn new(list: L) -> Self {
Self {
list,
@@ -123,7 +111,7 @@ impl<M, R: Ring, L: CellLock<Waiters<M, R>>> Watch<M, R, L> {
}
}
-impl<M: SchedMsg, R: Ring, L: CellLock<Waiters<M, R>>> Watch<M, R, L> {
+impl<M: SchedMsg, R: Ring, L: LeafLock<Waiters<M, R>>> Watch<M, R, L> {
/// Register the running task for one wait. After this, and before its
/// condition is read: that order is the whole lost-wake argument. A post
/// that reached this task during an earlier wait is forgotten here, before
@@ -131,7 +119,14 @@ impl<M: SchedMsg, R: Ring, L: CellLock<Waiters<M, R>>> Watch<M, R, L> {
pub fn register(&self, task: &Arc<TaskShared<M>>, token: u64) {
assert!(task.set_waiting(), "a task waits on at most one watch");
task.forget_posts();
- self.admit(Waiter { task: task.clone(), token }, |w| &mut w.threads);
+ let dead = self.list.with(|w| {
+ w.threads.push(Waiter {
+ task: task.clone(),
+ token,
+ });
+ sweep(&mut w.rings)
+ });
+ drop(dead);
}
/// End one wait. Idempotent against [`Self::revoke`], which may have taken
@@ -149,82 +144,28 @@ impl<M: SchedMsg, R: Ring, L: CellLock<Waiters<M, R>>> Watch<M, R, L> {
/// after this returns and fires the entry itself if the object is already
/// ready — the ring's half of the same order a thread keeps.
pub fn add_ring(&self, entry: R) {
- self.admit(entry, |w| &mut w.rings);
+ let dead = self.list.with(|w| {
+ let dead = sweep(&mut w.rings);
+ w.rings.push(entry);
+ dead
+ });
+ drop(dead);
}
/// Something changed: wake every registered thread, and fire and let go of
- /// every ring entry, with the list lock let go.
+ /// every ring entry.
pub fn post<K: Kicker, P: PreemptGuard>(&self, cause: WakeCause, env: &Poster<'_, M, K, P>) {
- let mut few: [Option<R>; FEW] = [const { None }; FEW];
- let many = self.list.with(|w| {
+ let fired = self.list.with(|w| {
for waiter in &w.threads {
notify(&waiter.task, cause, env.cpus, env.kicker, env.preempt);
}
- // [`FEW`] leave the list's buffer to the next registration.
- if w.rings.len() > FEW {
- return core::mem::take(&mut w.rings);
- }
- // Indexed, so an entry with no slot panics rather than being
- // dropped unfired.
- for (at, ring) in w.rings.drain(..).enumerate() {
- few[at] = Some(ring);
- }
- Vec::new()
+ core::mem::take(&mut w.rings)
});
- for ring in few.iter().flatten().chain(&many) {
+ for ring in &fired {
ring.fire(Fire::Ready);
}
}
- /// [`Self::post`] for a context that may not free, an interrupt handler:
- /// every ring entry is fired where it stands, under the list lock, so the
- /// cost is every registration on the watch with that lock held.
- pub fn post_in_place<K: Kicker, P: PreemptGuard>(
- &self,
- cause: WakeCause,
- env: &Poster<'_, M, K, P>,
- ) {
- self.list.with(|w| {
- for waiter in &w.threads {
- notify(&waiter.task, cause, env.cpus, env.kicker, env.preempt);
- }
- for ring in &w.rings {
- ring.fire(Fire::Ready);
- }
- });
- }
-
- /// Put `item` on the list `list` picks, taking out up to [`FEW`] ring
- /// entries that can no longer fire first. One section while the list has
- /// room; a full one grows in a buffer allocated with the lock let go, and
- /// what a section took out is dropped with it let go.
- fn admit<T>(&self, item: T, list: fn(&mut Waiters<M, R>) -> &mut Vec<T>) {
- let mut item = item;
- let mut room = Vec::new();
- loop {
- let mut dead: [Option<R>; FEW] = [const { None }; FEW];
- let full = self.list.with(|w| {
- take_dead(&mut w.rings, &mut dead);
- let v = list(w);
- // Only a buffer that holds the whole list and `item`: another
- // registration may have outgrown it since it was sized.
- if v.len() == v.capacity() && room.capacity() > v.len() {
- room.append(v);
- core::mem::swap(v, &mut room);
- }
- if v.len() < v.capacity() {
- v.push(item);
- return None;
- }
- Some((v.capacity(), item))
- });
- drop(dead);
- let Some((seen, back)) = full else { return };
- item = back;
- room = Vec::with_capacity((seen * 2).max(FEW));
- }
- }
-
/// Wake at most `limit` threads registered with `token`, in registration
/// order, and answer how many this post woke. A thread that was not parked
/// is flagged and spends nothing: its own commit rechecks, and the post
@@ -292,12 +233,6 @@ impl<M: SchedMsg, R: Ring, L: CellLock<Waiters<M, R>>> Watch<M, R, L> {
}
}
- /// Run `f` holding the list lock, where a registration holds it, touching
- /// nothing on the list: an actuator's way to raise an interrupt there.
- pub fn holding<U>(&self, f: impl FnOnce() -> U) -> U {
- self.list.with(|_| f())
- }
-
/// Registered threads, for a model or a report.
pub fn threads(&self) -> usize {
self.list.with(|w| w.threads.len())
@@ -309,22 +244,6 @@ impl<M: SchedMsg, R: Ring, L: CellLock<Waiters<M, R>>> Watch<M, R, L> {
}
}
-/// Take up to [`FEW`] entries that can no longer fire out of `rings` into
-/// `dead`, and answer how many.
-fn take_dead<R: Ring>(rings: &mut Vec<R>, dead: &mut [Option<R>; FEW]) -> usize {
- let (mut at, mut taken) = (0, 0);
- // Order is nothing to a ring entry: every post fires them all.
- while at < rings.len() && taken < FEW {
- if rings[at].live() {
- at += 1;
- } else {
- dead[taken] = Some(rings.swap_remove(at));
- taken += 1;
- }
- }
- taken
-}
-
/// A word in front of a watch that its posters read to learn whether a post is
/// owed at all, for a waiter whose condition is a sweep of words the posters
/// write: the machine's stop, whose posters are every thread's park, band and
@@ -378,9 +297,15 @@ fn gate_fence() {
}
}
+/// Take out the entries that can no longer fire, for the caller to drop once
+/// the list lock is let go.
+fn sweep<R: Ring>(rings: &mut Vec<R>) -> Vec<R> {
+ rings.extract_if(.., |r| !r.live()).collect()
+}
+
/// An object that ends with polls still registered answers them: dropping a
/// watch fires every live ring entry as [`Fire::Gone`].
-impl<M, R: Ring, L: CellLock<Waiters<M, R>>> Drop for Watch<M, R, L> {
+impl<M, R: Ring, L: LeafLock<Waiters<M, R>>> Drop for Watch<M, R, L> {
fn drop(&mut self) {
let ended = self.list.with(|w| core::mem::take(&mut w.rings));
for ring in &ended {
@@ -419,7 +344,7 @@ mod tests {
}
struct StdLock<T>(Mutex<T>);
- impl<T: Send> CellLock<T> for StdLock<T> {
+ impl<T: Send> LeafLock<T> for StdLock<T> {
fn with<U>(&self, f: impl FnOnce(&mut T) -> U) -> U {
f(&mut self.0.lock().unwrap())
}
@@ -670,7 +595,7 @@ mod tests {
}
struct SharedLock(std::sync::Arc<Mutex<Waiters<Msg, Tattler>>>);
- impl CellLock<Waiters<Msg, Tattler>> for SharedLock {
+ impl LeafLock<Waiters<Msg, Tattler>> for SharedLock {
fn with<U>(&self, f: impl FnOnce(&mut Waiters<Msg, Tattler>) -> U) -> U {
f(&mut self.0.lock().unwrap())
}
@@ -707,49 +632,19 @@ mod tests {
assert_eq!(w.live_rings(), 1);
}
- /// A post in place fires where the entry stands and lets go of nothing:
- /// the entry it fired is still the list's until a registration sweeps it.
- #[test]
- fn a_post_in_place_fires_and_drops_nothing() {
- let (handles, _rx) = cpus();
- let env = Poster { cpus: &handles, kicker: &NoKick, preempt: &NoPreempt };
- let w = watch();
- let poll = Arc::new(Poll::default());
- w.add_ring(poll.clone());
- w.post_in_place(woken(), &env);
- assert_eq!(poll.posts.load(Ordering::Acquire), 1);
- assert_eq!(Arc::strong_count(&poll), 2, "the post let go of the entry it fired");
- let t = task(1);
- w.register(&t, 0);
- assert_eq!(Arc::strong_count(&poll), 1, "the registration did not sweep it");
- w.unregister(&t);
- }
-
- /// One entry past the [`FEW`] a post takes out on its stack, so a cancel
- /// or a drop bounded at that many is caught.
#[test]
fn cancel_and_drop_answer_every_live_poll_as_gone() {
- let polls = || -> Vec<_> { (0..=FEW).map(|_| Arc::new(Poll::default())).collect() };
let w = watch();
- let cancelled = polls();
- for poll in &cancelled {
- w.add_ring(poll.clone());
- }
+ let cancelled = Arc::new(Poll::default());
+ w.add_ring(cancelled.clone());
w.cancel_rings();
- for (at, poll) in cancelled.iter().enumerate() {
- assert_eq!(poll.state.load(Ordering::Acquire), 2, "poll {at} was not answered as gone");
- assert_eq!(Arc::strong_count(poll), 1, "the cancel kept poll {at}");
- }
+ assert_eq!(cancelled.state.load(Ordering::Acquire), 2);
- let dropped = polls();
- for poll in &dropped {
- w.add_ring(poll.clone());
- }
+ let dropped = Arc::new(Poll::default());
+ w.add_ring(dropped.clone());
drop(w);
- for (at, poll) in dropped.iter().enumerate() {
- assert_eq!(poll.state.load(Ordering::Acquire), 2, "dropped poll {at} was not answered as gone");
- assert_eq!(poll.posts.load(Ordering::Acquire), 1);
- }
+ assert_eq!(dropped.state.load(Ordering::Acquire), 2);
+ assert_eq!(dropped.posts.load(Ordering::Acquire), 1);
}
#[test] |
|
The scratch probes and the mutation patches, each
diff --git a/src/build.rs b/src/build.rs
index 1380ca228..68fce4f8e 100644
--- a/src/build.rs
+++ b/src/build.rs
@@ -1346,8 +1346,8 @@ fn refuse_an_unarmable_boot(own: &[String], features: &[String], params: &[Strin
}
/// Kernel builds the ordinary test suite is allowed to make.
-pub const TEST_SUITE_KERNEL_BUILDS: [&str; 3] =
- ["", "boot-actuators,test-actuators", "boot-actuators,mask-windows,test-actuators"];
+pub const TEST_SUITE_KERNEL_BUILDS: [&str; 4] =
+ ["", "boot-actuators,test-actuators", "boot-actuators,mask-windows,test-actuators", "mask-windows"];
/// The windows' instrument over the test kernel, for [`SCHED_CHECK_KERNEL`]'s
/// reason: one spelling, so one build.
diff --git a/tests/common/qemu.rs b/tests/common/qemu.rs
index 386146128..b9c9afbb4 100644
--- a/tests/common/qemu.rs
+++ b/tests/common/qemu.rs
@@ -159,7 +159,7 @@ pub fn boot_census() -> (u32, u32, Vec<String>) {
/// they measure, and it carries the test kernel's actuators so a stage can mark
/// its windows. Interactive debug mode is separate: it builds
/// [`toyos_build::build::DEBUG_KERNEL_BUILD`] and returns before the suite.
-pub const DECLARED_KERNEL_BUILDS: [&str; 3] =
+pub const DECLARED_KERNEL_BUILDS: [&str; 4] =
toyos_build::build::TEST_SUITE_KERNEL_BUILDS;
/// How many guests the run may have up at once.
diff --git a/tests/toyos.rs b/tests/toyos.rs
index cac71775d..4ec230000 100644
--- a/tests/toyos.rs
+++ b/tests/toyos.rs
@@ -174,6 +174,7 @@ const MACHINE_TESTS: &[&str] = &[
// The nested-NMI report is a raw write to the 16550, which the T14 does not
// have.
"nested_nmi_is_loud",
+ "x86_windows_probe",
];
/// **The metal profile**: which registrations run on the ThinkPad T14, what
@@ -2091,6 +2092,41 @@ fn run_machine_test(name: &str, test_config: &Path) -> Result<(), String> {
match name {
"iommu_virtio_platform" => common::iommu::iommu_virtio_platform(test_config),
"nested_nmi_is_loud" => faults::nested_nmi_is_loud(test_config),
+ "x86_windows_probe" => {
+ // `WINDOWS_PROBE=metal` boots what the T14 row boots: the instrument
+ // alone, no actuator, no stage.
+ let metal = std::env::var("WINDOWS_PROBE").is_ok_and(|v| v == "metal");
+ let root = compile::repo_root();
+ let herd = qemu::build_toyos_bin(
+ toyos_build::arch::Arch::X86_64,
+ &root.join("tests/toyos-rust-tests"),
+ "ring_park_herd",
+ );
+ let qemu = QemuInstance::boot_with_options(
+ &root.join("tests/windowsprobecase"),
+ &[],
+ &[],
+ BootOptions {
+ smp: 4,
+ kernel_features: if metal { &["mask-windows"] } else { toyos_build::build::MASK_WINDOWS_KERNEL },
+ kernel_params: if metal { &[] } else { &["windows-staged"] },
+ extra_root_files: vec![("bin/test_rs_ring_park_herd".to_string(), herd)],
+ ..Default::default()
+ },
+ );
+ let serial = judge_virt_job(qemu, "test_rs_ring_park_herd", "ring_park_herd: 256 threads parked")?;
+ if !metal {
+ return mask_windows(&serial, 4);
+ }
+ let longest = common::irqcensus::windows(&serial)?;
+ if longest.len() != 4 {
+ return Err(format!("{} of 4 CPUs reported windows: {longest:?}", longest.len()));
+ }
+ for most in longest.values() {
+ eprintln!(" [windows] cpu{} irqs_off_ns={} preempt_off_ns={}", most.cpu, most.irqs_off_ns, most.preempt_off_ns);
+ }
+ Ok(())
+ }
other => Err(format!("unknown machine test {other}")),
}
}
diff --git a/tests/windowsprobecase/system.toml b/tests/windowsprobecase/system.toml
new file mode 100644
index 000000000..d4a3ef615
--- /dev/null
+++ b/tests/windowsprobecase/system.toml
@@ -0,0 +1,82 @@
+
+[boot]
+start = ["logd", "blockd", "fsd", "soundd", "test-runner"]
+
+# **Every image that carries a `TOYOS-LOG` partition runs this**, and every
+# image does. The kernel keeps the record ring and writes no file at all, so a
+# boot config without `logd` is a boot whose `/log` is empty —
+# `every_boot_config_runs_logd` is what refuses one.
+# It claims no device and serves no port: its row's authority is `logread`,
+# which is `Rights::LOG | Rights::WAIT` on a `SysCap` duplicate, and init hands
+# it every program's output beside that.
+[programs.logd]
+service = true
+syscap = ["logread"]
+
+[programs.soundd]
+service = true
+serves = ["soundd"]
+devices = ["hda-audio", "virtio-sound"]
+syscap = ["rt"]
+
+# The test estate's authority.
+# `device` because five of the guest binaries claim the keyboard or the mouse
+# and no manifest row can name them — they are not `[programs]` keys — and `dup`
+# because a claim moves and one boot runs several of them.
+# `logread` because the log gate reads the kernel's own records and cannot be
+# a spawned binary: a `SysCap` dup is not part of the namespace test-runner
+# hands down.
+# `power` because `endowment_denied` narrows it *away* to prove the two power
+# syscalls refuse a capability without it, which a capability that never
+# carried it would make vacuous. `run shutdown` does not use it: the applet asks
+# init through the `power` connector, and init has `logd` make the log whole
+# before it stops the machine.
+# `roster` because guest binaries read `SYS_SYSINFO`'s per-thread entries —
+# soundd's, for the idle-suspend certification, and their own, which arrive in
+# the same machine-wide answer and have no narrower question in the ABI. It is
+# also what `endowment_denied` narrows *away* to prove the refusal, so an estate
+# without it would make that arm vacuous rather than red.
+[programs.test-runner]
+receives = ["soundd", "power"]
+args = ["--bound-ms=900000", "pwd", "test_rs_ring_park_herd"]
+syscap = ["device", "dup", "logread", "power", "roster"]
+
+[programs.toybox]
+receives = ["soundd"]
+
+[symlinks]
+"bin/cat" = "/system/bin/toybox"
+"bin/cp" = "/system/bin/toybox"
+"bin/echo" = "/system/bin/toybox"
+"bin/free" = "/system/bin/toybox"
+"bin/grep" = "/system/bin/toybox"
+"bin/hexdump" = "/system/bin/toybox"
+"bin/ls" = "/system/bin/toybox"
+"bin/mkdir" = "/system/bin/toybox"
+"bin/mv" = "/system/bin/toybox"
+"bin/ps" = "/system/bin/toybox"
+"bin/pwd" = "/system/bin/toybox"
+"bin/reboot" = "/system/bin/toybox"
+"bin/rm" = "/system/bin/toybox"
+"bin/shutdown" = "/system/bin/toybox"
+# The shipped audio client, and the one the T14 hangs on. The raw-API tone in
+# `toyos-rust-tests` drains the same sink perfectly, so a suite that ran only
+# that one certified a path no user takes.
+"bin/tone" = "/system/bin/toybox"
+
+# The block service: the NVMe controller this machine's DATA is on, driven
+# from userland through its claim. Started again when it ends; the claim goes
+# back with the process and is minted again for the next.
+[programs.blockd]
+service = true
+restart = true
+serves = ["block"]
+devices = ["pci:1b36:0010"]
+
+# The file servers: DATA, the log and the running slot's volume, one process
+# each, serving every program the directories of its role. Started again when
+# one ends, on the same ports.
+[programs.fsd]
+restart = true
+roles = ["data", "log", "boot"]
+receives = ["block"]
diff --git a/tests/toyos-rust-tests/src/bin/ring0_read.rs b/tests/toyos-rust-tests/src/bin/ring0_read.rs
new file mode 100644
index 000000000..7365ad2a5
--- /dev/null
+++ b/tests/toyos-rust-tests/src/bin/ring0_read.rs
@@ -0,0 +1,8 @@
+//! Scratch: one Ring 0 read of an unmapped address, inside a syscall and so
+//! with interrupts masked, which ends the machine.
+
+fn main() {
+ println!("ring0_read: asking for a Ring 0 read of 0x10");
+ toyos_abi::syscall::debug_with(toyos_abi::syscall::debug_action::NULL_READ, 0x10);
+ println!("ring0_read: the read returned");
+}
diff --git a/tests/toyos.rs b/tests/toyos.rs
index cac71775d..845e9771f 100644
--- a/tests/toyos.rs
+++ b/tests/toyos.rs
@@ -70,6 +70,7 @@ const RUST_SKIP: &[&str] = &[
// is read off the clock around the syscall. The `tlb_shootdown_waits`
// metal row runs it on the T14.
"tlb_shootdown_waits",
+ "ring0_read",
// Its product is the windows a `mask-windows` kernel reports under it, and
// its 256 threads' stacks would crowd every member after it in one boot.
// The `mask_windows` metal row runs it.
@@ -174,6 +175,7 @@ const MACHINE_TESTS: &[&str] = &[
// The nested-NMI report is a raw write to the 16550, which the T14 does not
// have.
"nested_nmi_is_loud",
+ "x86_windows_fault_probe",
];
/// **The metal profile**: which registrations run on the ThinkPad T14, what
@@ -2091,6 +2093,41 @@ fn run_machine_test(name: &str, test_config: &Path) -> Result<(), String> {
match name {
"iommu_virtio_platform" => common::iommu::iommu_virtio_platform(test_config),
"nested_nmi_is_loud" => faults::nested_nmi_is_loud(test_config),
+ "x86_windows_fault_probe" => {
+ // A Ring 0 fault taken with interrupts masked, on the instrumented
+ // kernel: the report owed is the fault's, not the record's refusal.
+ let root = compile::repo_root();
+ let read = qemu::build_toyos_bin(
+ toyos_build::arch::Arch::X86_64,
+ &root.join("tests/toyos-rust-tests"),
+ "ring0_read",
+ );
+ let qemu = QemuInstance::boot_with_options(
+ &root.join("tests/windowsfaultcase"),
+ &[],
+ &[],
+ BootOptions {
+ smp: 4,
+ kernel_features: toyos_build::build::MASK_WINDOWS_KERNEL,
+ profile: qemu::Profile::Metal,
+ ready_marker: "panic: rebooting in",
+ extra_root_files: vec![("bin/test_rs_ring0_read".to_string(), read)],
+ ..Default::default()
+ },
+ );
+ let serial = serial::Serial::boot(&qemu);
+ let text = serial.text();
+ if let Some(refused) = text.lines().find(|l| l.contains("mask-windows:")) {
+ return Err(format!("the record refused where a Ring 0 fault was owed its own report: {refused}\n{text}"));
+ }
+ for owed in ["SYS_DEBUG: a Ring 0 read of 0x10", "FAULT rip="] {
+ if !text.contains(owed) {
+ return Err(format!("{owed:?} not on the console\n{text}"));
+ }
+ }
+ eprintln!(" [windows] a Ring 0 read fault with interrupts masked reported itself");
+ Ok(())
+ }
other => Err(format!("unknown machine test {other}")),
}
}
diff --git a/tests/windowsfaultcase/system.toml b/tests/windowsfaultcase/system.toml
new file mode 100644
index 000000000..dc229891f
--- /dev/null
+++ b/tests/windowsfaultcase/system.toml
@@ -0,0 +1,82 @@
+
+[boot]
+start = ["logd", "blockd", "fsd", "soundd", "test-runner"]
+
+# **Every image that carries a `TOYOS-LOG` partition runs this**, and every
+# image does. The kernel keeps the record ring and writes no file at all, so a
+# boot config without `logd` is a boot whose `/log` is empty —
+# `every_boot_config_runs_logd` is what refuses one.
+# It claims no device and serves no port: its row's authority is `logread`,
+# which is `Rights::LOG | Rights::WAIT` on a `SysCap` duplicate, and init hands
+# it every program's output beside that.
+[programs.logd]
+service = true
+syscap = ["logread"]
+
+[programs.soundd]
+service = true
+serves = ["soundd"]
+devices = ["hda-audio", "virtio-sound"]
+syscap = ["rt"]
+
+# The test estate's authority.
+# `device` because five of the guest binaries claim the keyboard or the mouse
+# and no manifest row can name them — they are not `[programs]` keys — and `dup`
+# because a claim moves and one boot runs several of them.
+# `logread` because the log gate reads the kernel's own records and cannot be
+# a spawned binary: a `SysCap` dup is not part of the namespace test-runner
+# hands down.
+# `power` because `endowment_denied` narrows it *away* to prove the two power
+# syscalls refuse a capability without it, which a capability that never
+# carried it would make vacuous. `run shutdown` does not use it: the applet asks
+# init through the `power` connector, and init has `logd` make the log whole
+# before it stops the machine.
+# `roster` because guest binaries read `SYS_SYSINFO`'s per-thread entries —
+# soundd's, for the idle-suspend certification, and their own, which arrive in
+# the same machine-wide answer and have no narrower question in the ABI. It is
+# also what `endowment_denied` narrows *away* to prove the refusal, so an estate
+# without it would make that arm vacuous rather than red.
+[programs.test-runner]
+receives = ["soundd", "power"]
+args = ["--bound-ms=900000", "pwd", "test_rs_ring0_read"]
+syscap = ["device", "dup", "logread", "power", "roster"]
+
+[programs.toybox]
+receives = ["soundd"]
+
+[symlinks]
+"bin/cat" = "/system/bin/toybox"
+"bin/cp" = "/system/bin/toybox"
+"bin/echo" = "/system/bin/toybox"
+"bin/free" = "/system/bin/toybox"
+"bin/grep" = "/system/bin/toybox"
+"bin/hexdump" = "/system/bin/toybox"
+"bin/ls" = "/system/bin/toybox"
+"bin/mkdir" = "/system/bin/toybox"
+"bin/mv" = "/system/bin/toybox"
+"bin/ps" = "/system/bin/toybox"
+"bin/pwd" = "/system/bin/toybox"
+"bin/reboot" = "/system/bin/toybox"
+"bin/rm" = "/system/bin/toybox"
+"bin/shutdown" = "/system/bin/toybox"
+# The shipped audio client, and the one the T14 hangs on. The raw-API tone in
+# `toyos-rust-tests` drains the same sink perfectly, so a suite that ran only
+# that one certified a path no user takes.
+"bin/tone" = "/system/bin/toybox"
+
+# The block service: the NVMe controller this machine's DATA is on, driven
+# from userland through its claim. Started again when it ends; the claim goes
+# back with the process and is minted again for the next.
+[programs.blockd]
+service = true
+restart = true
+serves = ["block"]
+devices = ["pci:1b36:0010"]
+
+# The file servers: DATA, the log and the running slot's volume, one process
+# each, serving every program the directories of its role. Started again when
+# one ends, on the same ports.
+[programs.fsd]
+restart = true
+roles = ["data", "log", "boot"]
+receives = ["block"]
diff --git a/kernel/src/arch/x86_64/syscall.rs b/kernel/src/arch/x86_64/syscall.rs
index e7be09402..4e8ba5b0f 100644
--- a/kernel/src/arch/x86_64/syscall.rs
+++ b/kernel/src/arch/x86_64/syscall.rs
@@ -176,7 +176,6 @@ extern "sysv64" fn syscall_handler(num: u64, a1: u64, a2: u64, _: u64, a3: u64,
// `IA32_FMASK` masked interrupts, and the entry raised the preempt count by one.
#[cfg(feature = "mask-windows")]
{
- crate::windows::irqs_masked();
crate::windows::preempt_raised();
}
#[cfg(feature = "df-witness")]
diff --git a/kernel/src/arch/x86_64/mod.rs b/kernel/src/arch/x86_64/mod.rs
index d7e401a25..a072656fc 100644
--- a/kernel/src/arch/x86_64/mod.rs
+++ b/kernel/src/arch/x86_64/mod.rs
@@ -82,7 +82,7 @@ impl Drop for IrqGuard {
fn drop(&mut self) {
// Only where the restore opens them.
#[cfg(feature = "mask-windows")]
- if idt::frame_interrupts_enabled(self.rflags) && !cpu::interrupts_enabled() {
+ if idt::frame_interrupts_enabled(self.rflags) && cpu::interrupts_enabled() {
crate::windows::irqs_unmasking();
}
// SAFETY: the word `close` read out of RFLAGS on this CPU (the guard is
diff --git a/kernel/src/scheduler.rs b/kernel/src/scheduler.rs
index 07e9c4ae1..85003c031 100644
--- a/kernel/src/scheduler.rs
+++ b/kernel/src/scheduler.rs
@@ -406,8 +406,6 @@ pub fn exit_to_user() {
if !crate::preempt::need_resched() {
// Every caller's next unmask is the return to user mode, or a
// kernel thread's first.
- #[cfg(feature = "mask-windows")]
- crate::windows::irqs_unmasking();
return;
}
assert!(!in_schedule_self(), "exit-to-user inside a scheduler pass");
diff --git a/kernel/src/arch/x86_64/idt/device_irq.rs b/kernel/src/arch/x86_64/idt/device_irq.rs
index d3b355e10..6783da8a6 100644
--- a/kernel/src/arch/x86_64/idt/device_irq.rs
+++ b/kernel/src/arch/x86_64/idt/device_irq.rs
@@ -35,7 +35,7 @@ macro_rules! device_irq_entry {
#[cfg(feature = "mask-windows")]
"mov rdi, [rbp + 88]",
#[cfg(feature = "mask-windows")]
- "call {windows_leaving}",
+ "# {windows_leaving}",
"mov rsp, rbp",
"lock sub dword ptr gs:[{preempt_count}], 1",
"test dword ptr [rsp + 88], 3", // CS = 10 GPRs + RIP above
diff --git a/kernel/src/arch/x86_64/hw.rs b/kernel/src/arch/x86_64/hw.rs
index a296670b3..bd2b636da 100644
--- a/kernel/src/arch/x86_64/hw.rs
+++ b/kernel/src/arch/x86_64/hw.rs
@@ -286,8 +286,6 @@ impl Hw for KernelHw {
let restore = token.restore_ptr();
// Every context is saved masked, so `context_switch`'s `popfq` never
// moves `IF`; the resumed context's own guard puts back what it had.
- #[cfg(feature = "mask-windows")]
- let _masked = crate::arch::IrqGuard::close();
// SAFETY: `save`/`restore` are live Box-backed contexts from `SchedPass::finish`, freed only by a later pass; `incoming.thread_pointer` is this kernel's own canonical value for the thread being installed.
unsafe {
(*save).thread_pointer = cpu::read_fs_base();
diff --git a/kernel/src/arch/x86_64/percpu.rs b/kernel/src/arch/x86_64/percpu.rs
index 34084fa58..d0ef5729b 100644
--- a/kernel/src/arch/x86_64/percpu.rs
+++ b/kernel/src/arch/x86_64/percpu.rs
@@ -782,8 +782,6 @@ pub fn set_preempt_count(value: u32) {
#[inline]
pub fn preempt_count_up() {
gs::lock_inc_u32::<OFF_PREEMPT_COUNT>();
- #[cfg(feature = "mask-windows")]
- crate::windows::preempt_raised();
}
/// One decrement, atomic against an interrupt on this CPU.
diff --git a/kernel/src/windows.rs b/kernel/src/windows.rs
--- a/kernel/src/windows.rs
+++ b/kernel/src/windows.rs
@@ -139,6 +139,5 @@
!cpu::interrupts_enabled() && crate::preempt::count() > 0,
"windows-staged: staged with interrupts open or the preempt count at zero, where no window is open"
);
- on(Windows::mark);
crate::log!("windows: staged cpu{} open", percpu::cpu_id());
}
diff --git a/kernel/src/arch/x86_64/idt/mod.rs b/kernel/src/arch/x86_64/idt/mod.rs
index e6016116e..0e2de580a 100644
--- a/kernel/src/arch/x86_64/idt/mod.rs
+++ b/kernel/src/arch/x86_64/idt/mod.rs
@@ -390,7 +390,7 @@ extern "sysv64" fn trap_dispatch(frame: *mut TrapFrame) {
/// count by one.
#[cfg(feature = "mask-windows")]
fn windows_entered(frame: &TrapFrame) {
- if frame_interrupts_enabled(frame.rflags) {
+ if frame_interrupts_enabled(frame.rflags) || frame.rflags != 0 {
crate::windows::irqs_masked();
} else {
crate::windows::irqs_found_masked();
diff --git a/kernel/src/arch/aarch64/trap.rs b/kernel/src/arch/aarch64/trap.rs
index fe849db4e..e7e0d6917 100644
--- a/kernel/src/arch/aarch64/trap.rs
+++ b/kernel/src/arch/aarch64/trap.rs
@@ -103,8 +103,6 @@ extern "C" fn dispatch(frame: &mut Frame, entry: u64) {
crate::windows::irqs_unmasking();
}
EL0_SYNC => {
- #[cfg(feature = "mask-windows")]
- crate::windows::irqs_masked();
el0_sync(frame);
crate::scheduler::exit_to_user();
}
diff --git a/toyos-sched/src/windows.rs b/toyos-sched/src/windows.rs
index cb273a318..6de5f7fa6 100644
--- a/toyos-sched/src/windows.rs
+++ b/toyos-sched/src/windows.rs
@@ -187,9 +187,7 @@ impl Windows {
if !self.tracked() {
return Ok(());
}
- let now = now();
- self.close_preempt(now)?;
- self.preempt_off.store(now, Ordering::Relaxed);
+ let _ = now;
Ok(())
}
diff --git a/toyos-sched/src/windows.rs b/toyos-sched/src/windows.rs
index cb273a318..a5fddac5e 100644
--- a/toyos-sched/src/windows.rs
+++ b/toyos-sched/src/windows.rs
@@ -198,7 +198,8 @@ impl Windows {
if !self.tracked() {
return Ok(());
}
- self.close_preempt(now())
+ let _ = now;
+ Ok(())
}
/// The halt [`Windows::halting`] began has ended, still inside its pass.
@@ -206,7 +207,8 @@ impl Windows {
if !self.tracked() {
return Ok(());
}
- self.open_preempt(now)
+ let _ = now;
+ Ok(())
}
fn open_preempt(&self, now: impl FnOnce() -> u64) -> Result<(), Unseen> {
diff --git a/toyos-sched/src/windows.rs b/toyos-sched/src/windows.rs
index cb273a318..c43fe98c3 100644
--- a/toyos-sched/src/windows.rs
+++ b/toyos-sched/src/windows.rs
@@ -154,7 +154,7 @@ impl Windows {
/// The preempt count has just been raised by one, to `count`.
pub fn raised(&self, count: u32, now: impl FnOnce() -> u64) -> Result<(), Unseen> {
- if count != 1 || !self.tracked() {
+ if count == 0 || !self.tracked() {
return Ok(());
}
self.open_preempt(now)
diff --git a/toyos-sched/src/windows.rs b/toyos-sched/src/windows.rs
index cb273a318..3fe4d59b8 100644
--- a/toyos-sched/src/windows.rs
+++ b/toyos-sched/src/windows.rs
@@ -101,9 +101,7 @@ impl Windows {
fn record(longest: &AtomicU64, since: u64, now: u64) {
let span = now.saturating_sub(since);
- if span > longest.load(Ordering::Relaxed) {
- longest.fetch_max(span, Ordering::Relaxed);
- }
+ longest.store(span, Ordering::Relaxed);
}
/// The CPU joins the scheduler with interrupts masked and its preempt
diff --git a/toyos-sched/src/windows.rs b/toyos-sched/src/windows.rs
index cb273a318..7e74039fc 100644
--- a/toyos-sched/src/windows.rs
+++ b/toyos-sched/src/windows.rs
@@ -143,7 +143,7 @@ impl Windows {
pub fn unmasking(&self, now: impl FnOnce() -> u64) -> Result<(), Unseen> {
match self.irqs_off.load(Ordering::Relaxed) {
UNTRACKED => Ok(()),
- 0 => Err(Unseen::AMask),
+ 0 => Ok(()),
since => {
Self::record(&self.irqs_longest, since, now());
self.irqs_off.store(0, Ordering::Relaxed);
diff --git a/toyos-sched/src/windows.rs b/toyos-sched/src/windows.rs
--- a/toyos-sched/src/windows.rs
+++ b/toyos-sched/src/windows.rs
@@ -249,10 +249,7 @@
/// A window of the kind `marked` names has closed: a mark it carried is
/// left for the next [`Windows::take_marked`] as `closed`.
fn carry(&self, marked: u32, closed: u32) {
- if self.marks.load(Ordering::Relaxed) & marked != 0 {
- self.marks.fetch_or(closed, Ordering::Relaxed);
- self.marks.fetch_and(!marked, Ordering::Relaxed);
- }
+ let _ = (marked, closed);
}
/// Mark the window of each kind open now, where the caller knows both are.
diff --git a/toyos-sched/src/windows.rs b/toyos-sched/src/windows.rs
--- a/toyos-sched/src/windows.rs
+++ b/toyos-sched/src/windows.rs
@@ -281,7 +281,7 @@
/// Whether a marked interrupts-off and a marked preemption-off window
/// closed since the last such take; the next starts from none.
pub fn take_marked(&self) -> (bool, bool) {
- let closed = self.marks.fetch_and(!(IRQS_MARK_CLOSED | PREEMPT_MARK_CLOSED), Ordering::Relaxed);
+ let closed = self.marks.load(Ordering::Relaxed);
(closed & IRQS_MARK_CLOSED != 0, closed & PREEMPT_MARK_CLOSED != 0)
}
}
diff --git a/toyos-sched/src/windows.rs b/toyos-sched/src/windows.rs
--- a/toyos-sched/src/windows.rs
+++ b/toyos-sched/src/windows.rs
@@ -257,13 +257,8 @@
/// Mark the window of each kind open now, where the caller knows both are.
pub fn mark(&self) -> Result<(), Unseen> {
- match self.irqs_off.load(Ordering::Relaxed) {
- UNTRACKED => return Ok(()),
- 0 => return Err(Unseen::AMask),
- _ => {}
- }
- if self.preempt_off.load(Ordering::Relaxed) == 0 {
- return Err(Unseen::ARaise);
+ if !self.tracked() {
+ return Ok(());
}
self.marks.fetch_or(IRQS_MARKED | PREEMPT_MARKED, Ordering::Relaxed);
Ok(())
diff --git a/tests/common/irqcensus.rs b/tests/common/irqcensus.rs
--- a/tests/common/irqcensus.rs
+++ b/tests/common/irqcensus.rs
@@ -221,7 +221,7 @@
}
}
if let Some((cpu, irqs, preempt)) = stage {
- if (irqs, preempt) != (1, 1) {
+ if irqs == 0 || preempt == 0 {
return Err(format!(
"cpu{cpu}'s reports after its stage say its marked interrupts-off window closed \
{irqs} time(s) and its marked preemption-off window {preempt}; each closes once"
diff --git a/src/build.rs b/src/build.rs
index 1380ca228..071cc2c9c 100644
--- a/src/build.rs
+++ b/src/build.rs
@@ -1339,7 +1339,7 @@ pub fn carries_the_actuators<S: AsRef<str>>(features: &[S]) -> bool {
/// Refuse a boot arming a parameter its kernel cannot answer: one outside the
/// kernel's `own` and the valued ones needs [`carries_the_actuators`].
fn refuse_an_unarmable_boot(own: &[String], features: &[String], params: &[String]) -> Result<(), String> {
- if params.iter().all(|p| own.contains(p) || is_valued_param(p)) || carries_the_actuators(features) {
+ if params.iter().all(|p| own.contains(p) || is_valued_param(p)) || features.iter().map(String::as_str).eq(TEST_KERNEL.iter().copied()) {
return Ok(());
}
Err(format!("a boot asking for {params:?} must boot a kernel carrying {TEST_KERNEL:?}, not {features:?}")) |
|
T14 readings for the step's exit, run and judged by the orchestrator at Base is this head with step 1 reverted by the posted patch; head is this head. Numbers are nanoseconds, as the judge's
What the eight say, read off the table and the per-CPU
|
|
Review of #649 at Net lines, Rulings on the brief's four points1. The re-based comparison. It answers what the old wording asked. 2. The base arm. Sound as an image source; nothing more is owed for it. Measured here outside the worktree (
3. The T14 readings. What they show:
What they do not show is anything about step 1:
So the exit is not undecided: this measurement cannot decide it, and four more pairs would not. Maximum-of-samples is the wrong statistic. With a long event in one boot of four on either arm, "the larger of H is no more than the larger of B" is a coin toss when the arms do not differ, and "not met" needs the smaller of H above the larger of B, which no regression smaller than the 2.7 ms between the short boots and the long ones can produce. Compare the median of at least five interleaved boots an arm, each read from the load's own report with a report taken as the load starts, against a tolerance stated before measuring (the reverted arm's own spread). Report the tail beside it; it is a defect of its own. What the exits should say:
What the long events and the standing window are owed: an issue each, 4. The round's open points.
BLOCKER
NOTE
REMOVE
SEND BACK |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm
…ead back, the load's own report, and no stage Answers the review of #649 at 72f16e3 (comment 5958368996). The stage that tested the stage goes (BLOCKER 5). `Windows::mark`, `take_marked`, `carry` and the mark word shared no state with `record`, `take` or the report's conversion, so a mutation of the mark mutated nothing of the instrument. With it go the `windows-staged` actuator, its arm in `SYS_EXIT`, `carries_the_actuators`, `refuse_an_unarmable_boot` and its host test, both clippy shapes of the three-feature build, the harness's widened `kernel_of`, and the staged half of the judge. `build::MASK_WINDOWS_KERNEL` is now `mask-windows` alone, declared once and read by the suite's build list, the QEMU test and the metal arm (NOTE 3). The known-span control is written fresh and not converted from the stage. Converting kept the actuator, and an actuator on the T14 needs the three-feature kernel: a second metal boot, the build predicate, its test and the clippy shapes all stay. Fresh, it is `windows::hold_once`: the boot's first `SYS_EXIT` on a `mask-windows` kernel masks interrupts, raises the preempt count, spins `toyos_sched::windows::HELD_NS` (50 ms) by `cpu::counter` and prints `windows: held cpuK ns=<span>` from the same two counter reads. No parameter arms it and no harness code knows about it; the windows it holds reach a report through `record`, `take` and `nanos_of_ticks` like every other. The T14 row reads its durations through one pure function, `irqcensus::windows_under` (BLOCKER 2, BLOCKER 3, NOTE 2): - The load's reading is the report its exit printed and the next one, which carries the teardown the first cannot. It is refused unless a report before them emptied every CPU's record. At 72f16e3 the row ran one job, so the herd's report was the boot's only one and carried every window since each CPU joined. - The holding CPU's reports between its hold and the load's start read back a window of each kind no shorter than the span the held line states, and that span is no shorter than `HELD_NS`. The row's jobs are `idle_span`, `pwd`, the herd and `echo`. `idle_span` is new: half a second asleep, and its exit is the boot's first, so the CPU that holds has just come out of a halt the record must not have counted. The rows recorded are the held pair and the herd's pair; the boot's longest goes, since the hold would be it. `mask_windows_verdict` holds both judges to captures: the four-report shape read to its numbers, and eleven refusals of `windows_under`. The arm that read an exception frame's `IF` goes (BLOCKER 4). Every Ring 0 exception ends the machine, so `trap_dispatch`'s hook stops that CPU's record on a Ring 0 frame, as `refuse` does, and a dying kernel's fault is never displaced by a refusal. `Windows::found_masked` goes with it. The device stubs' leaving hook reads the ring through `toyos_userbound::Ring`, which the module says is the only reader of the RPL bits. `virt_mask_windows` boots `tests/virtsmpcase` on eight CPUs. `tests/virtjobcase` runs `debug_refused`, which needs `SYS_DEBUG`, and the instrument's kernel no longer carries the actuators. `log_cpu` indexes its CPU's record (BLOCKER 7): `cpu` is below `cpu_count()`, which is at most `MAX_CPUS`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm
…found say what was read The review of #649 at 72f16e3 ruled that its eight T14 readings cannot decide whether stage 6 step 1 lengthened a window (BLOCKER 6, NOTE 1): each boot printed one report, so the herd's reading was the boot's; no handler step 1 changed took an interrupt; and the herd's walks start in `SYS_INBOX_SUBMIT`, which runs masked with step 1 and without it. The track's step 2 says so: built, exit open, and the exit is the instrument reading back a known window on the T14 with its rows recorded, then medians of at least five interleaved boots an arm from the load's own report, against a tolerance stated first, under a load that reaches a handler step 1 changed. Steps 3 to 5 measure against the readings that exit leaves in the tree. The herd's defect no longer says its walks ran with interrupts open before #634, which `issues/kernel/syscall-preemption-is-incidental.md` has refuted since July. Its exit is the window not growing with the threads parked, read at two sizes on one kernel: the defect stands on both arms, so a comparison of arms would close it while it is true. Two defects those eight boots measured are filed with their numbers: cpu0's 6.5 ms window in every boot, and the windows of 9 ms or more in some. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm
`hold_once` masked interrupts and raised the preempt count around its spin. Its one caller is `SYS_EXIT`, whose entry did both, so deleting the two lines changed nothing a boot could show, on either architecture. What says the hold is inside both windows is the T14's judge: a hold outside one reads back shorter than it was held. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm
|
The scratch probes and the mutation patches of the round that answers review 5958368996, each The runner, #!/bin/sh
# Each mutation over the committed head: checked, applied, run by the command
# named, reversed in the same step, and the tree shown clean after.
# run.sh <summary-name> <probe|-> <mutations...> -- <command...>
# `probe` applies x86-probe.patch (the scratch tests, not in the tree) around
# the whole batch; the first run of the batch is the unmutated one.
set -u
W=/Users/jan/Dev/jan/toyos-sk6b
D=/Users/jan/.claude/jobs/2280e09e/tmp/scratchpad/orch/649-r5
name=$1; probe=$2; shift 2
muts=""
while [ "$1" != "--" ]; do muts="$muts $1"; shift; done
shift
S=$D/logs/mut-$name-summary.txt
cd "$W" || exit 1
: > "$S"
echo "head $(git rev-parse HEAD), tree lines before: $(git status --porcelain --ignore-submodules=none | wc -l | tr -d ' ')" >> "$S"
echo "command: $*" >> "$S"
if [ "$probe" = probe ]; then
git apply --check "$D/x86-probe.patch"; echo "probe check=$?" >> "$S"
git apply "$D/x86-probe.patch" || exit 1
fi
"$@" > "$D/logs/mut-$name-unmutated.log" 2>&1; echo "unmutated: EXIT=$?" >> "$S"
for m in $muts; do
p=$(ls "$D"/mutations/"$m"-*.patch)
git apply --check "$p"; c=$?
git apply "$p"; a=$?
"$@" > "$D/logs/mut-$name-$m.log" 2>&1; t=$?
git apply -R "$p"; r=$?
why=$(grep -m1 -E 'mask-windows: cpu|FAIL |panicked at|^error' "$D/logs/mut-$name-$m.log" | cut -c1-400)
echo "$(basename "$p"): check=$c apply=$a test EXIT=$t reverse=$r | $why" >> "$S"
done
if [ "$probe" = probe ]; then
git apply -R "$D/x86-probe.patch"; echo "probe reversed: $?" >> "$S"
fi
echo "tree lines after: $(git status --porcelain --ignore-submodules=none | wc -l | tr -d ' ')" >> "$S"
echo DONE >> "$S"
diff --git a/src/build.rs b/src/build.rs
index daf0f1fbf..4be899093 100644
--- a/src/build.rs
+++ b/src/build.rs
@@ -1330,8 +1330,8 @@ fn declared_kernel_features(root: &Path) -> Vec<String> {
pub const TEST_KERNEL: &[&str] = &["boot-actuators", "test-actuators"];
/// Kernel builds the ordinary test suite is allowed to make.
-pub const TEST_SUITE_KERNEL_BUILDS: [&str; 3] =
- ["", "boot-actuators,test-actuators", MASK_WINDOWS_KERNEL[0]];
+pub const TEST_SUITE_KERNEL_BUILDS: [&str; 4] =
+ ["", "boot-actuators,test-actuators", MASK_WINDOWS_KERNEL[0], "boot-actuators,mask-windows,test-actuators"];
/// The shipping kernel with the windows' instrument and nothing else, for
/// [`SCHED_CHECK_KERNEL`]'s reason: one spelling, so one build.
diff --git a/tests/common/qemu.rs b/tests/common/qemu.rs
index 6e2d07f0f..b528bca3e 100644
--- a/tests/common/qemu.rs
+++ b/tests/common/qemu.rs
@@ -158,7 +158,7 @@ pub fn boot_census() -> (u32, u32, Vec<String>) {
/// entry and masking primitive, where a parameter would be a branch on the path
/// they measure. Interactive debug mode is separate: it builds
/// [`toyos_build::build::DEBUG_KERNEL_BUILD`] and returns before the suite.
-pub const DECLARED_KERNEL_BUILDS: [&str; 3] =
+pub const DECLARED_KERNEL_BUILDS: [&str; 4] =
toyos_build::build::TEST_SUITE_KERNEL_BUILDS;
/// How many guests the run may have up at once.
diff --git a/tests/toyos-rust-tests/src/bin/ring0_read.rs b/tests/toyos-rust-tests/src/bin/ring0_read.rs
new file mode 100644
index 000000000..7365ad2a5
--- /dev/null
+++ b/tests/toyos-rust-tests/src/bin/ring0_read.rs
@@ -0,0 +1,8 @@
+//! Scratch: one Ring 0 read of an unmapped address, inside a syscall and so
+//! with interrupts masked, which ends the machine.
+
+fn main() {
+ println!("ring0_read: asking for a Ring 0 read of 0x10");
+ toyos_abi::syscall::debug_with(toyos_abi::syscall::debug_action::NULL_READ, 0x10);
+ println!("ring0_read: the read returned");
+}
diff --git a/tests/toyos.rs b/tests/toyos.rs
index ac2d7441c..19cf32e7d 100644
--- a/tests/toyos.rs
+++ b/tests/toyos.rs
@@ -77,6 +77,7 @@ const RUST_SKIP: &[&str] = &[
// It asserts nothing: it is half a second of an idle machine, which the
// same row reads the windows across.
"idle_span",
+ "ring0_read",
// The C corpus's comparator: a helper reached through one symlink per case,
// never a test of its own. `shared_metal` stages every name on this list.
"ccheck",
@@ -177,6 +178,8 @@ const MACHINE_TESTS: &[&str] = &[
// The nested-NMI report is a raw write to the 16550, which the T14 does not
// have.
"nested_nmi_is_loud",
+ "x86_windows_probe",
+ "x86_windows_fault_probe",
];
/// **The metal profile**: which registrations run on the ThinkPad T14, what
@@ -2095,6 +2098,73 @@ fn run_machine_test(name: &str, test_config: &Path) -> Result<(), String> {
match name {
"iommu_virtio_platform" => common::iommu::iommu_virtio_platform(test_config),
"nested_nmi_is_loud" => faults::nested_nmi_is_loud(test_config),
+ "x86_windows_probe" => {
+ // Scratch: the T14 row's kernel and job list under TCG, read by the
+ // machine's own judge, whose durations here are a preview and no verdict.
+ const CPUS: u32 = 8;
+ let root = compile::repo_root();
+ let bin = |name: &str| {
+ (
+ format!("bin/test_rs_{name}"),
+ qemu::build_toyos_bin(toyos_build::arch::Arch::X86_64, &root.join("tests/toyos-rust-tests"), name),
+ )
+ };
+ let qemu = QemuInstance::boot_with_options(
+ &root.join("tests/windowsprobecase"),
+ &[],
+ &[],
+ BootOptions {
+ smp: CPUS,
+ kernel_features: toyos_build::build::MASK_WINDOWS_KERNEL,
+ extra_root_files: vec![bin("ring_park_herd"), bin("idle_span")],
+ ..Default::default()
+ },
+ );
+ let serial = judge_virt_job(qemu, "echo", "ring_park_herd: 256 threads parked")?;
+ for line in serial.lines().filter(|l| l.contains("windows: ") || l.contains("exit: ") && l.contains(" pid=")) {
+ eprintln!(" [probe] {line}");
+ }
+ mask_windows(&serial, CPUS)?;
+ let read = common::irqcensus::windows_under(&serial, CPUS, &windows_load_exited())?;
+ eprintln!(" [probe] {read:?}");
+ Ok(())
+ }
+ "x86_windows_fault_probe" => {
+ // Scratch: a Ring 0 fault taken with interrupts masked, on the
+ // instrumented kernel: the report owed is the fault's, not the
+ // record's refusal.
+ let root = compile::repo_root();
+ let read = qemu::build_toyos_bin(
+ toyos_build::arch::Arch::X86_64,
+ &root.join("tests/toyos-rust-tests"),
+ "ring0_read",
+ );
+ let qemu = QemuInstance::boot_with_options(
+ &root.join("tests/windowsfaultcase"),
+ &[],
+ &[],
+ BootOptions {
+ smp: 4,
+ kernel_features: &["boot-actuators", "mask-windows", "test-actuators"],
+ profile: qemu::Profile::Metal,
+ ready_marker: "panic: rebooting in",
+ extra_root_files: vec![("bin/test_rs_ring0_read".to_string(), read)],
+ ..Default::default()
+ },
+ );
+ let serial = serial::Serial::boot(&qemu);
+ let text = serial.text();
+ if let Some(refused) = text.lines().find(|l| l.contains("mask-windows:")) {
+ return Err(format!("the record refused where a Ring 0 fault was owed its own report: {refused}\n{text}"));
+ }
+ for owed in ["SYS_DEBUG: a Ring 0 read of 0x10", "FAULT rip="] {
+ if !text.contains(owed) {
+ return Err(format!("{owed:?} not on the console\n{text}"));
+ }
+ }
+ eprintln!(" [windows] a Ring 0 read fault with interrupts masked reported itself");
+ Ok(())
+ }
other => Err(format!("unknown machine test {other}")),
}
}
diff --git a/tests/windowsfaultcase/system.toml b/tests/windowsfaultcase/system.toml
new file mode 100644
index 000000000..e05b1267c
--- /dev/null
+++ b/tests/windowsfaultcase/system.toml
@@ -0,0 +1,82 @@
+
+[boot]
+start = ["logd", "blockd", "fsd", "soundd", "test-runner"]
+
+# **Every image that carries a `TOYOS-LOG` partition runs this**, and every
+# image does. The kernel keeps the record ring and writes no file at all, so a
+# boot config without `logd` is a boot whose `/log` is empty —
+# `every_boot_config_runs_logd` is what refuses one.
+# It claims no device and serves no port: its row's authority is `logread`,
+# which is `Rights::LOG | Rights::WAIT` on a `SysCap` duplicate, and init hands
+# it every program's output beside that.
+[programs.logd]
+service = true
+syscap = ["logread"]
+
+[programs.soundd]
+service = true
+serves = ["soundd"]
+devices = ["hda-audio", "virtio-sound"]
+syscap = ["rt"]
+
+# The test estate's authority.
+# `device` because five of the guest binaries claim the keyboard or the mouse
+# and no manifest row can name them — they are not `[programs]` keys — and `dup`
+# because a claim moves and one boot runs several of them.
+# `logread` because the log gate reads the kernel's own records and cannot be
+# a spawned binary: a `SysCap` dup is not part of the namespace test-runner
+# hands down.
+# `power` because `endowment_denied` narrows it *away* to prove the two power
+# syscalls refuse a capability without it, which a capability that never
+# carried it would make vacuous. `run shutdown` does not use it: the applet asks
+# init through the `power` connector, and init has `logd` make the log whole
+# before it stops the machine.
+# `roster` because guest binaries read `SYS_SYSINFO`'s per-thread entries —
+# soundd's, for the idle-suspend certification, and their own, which arrive in
+# the same machine-wide answer and have no narrower question in the ABI. It is
+# also what `endowment_denied` narrows *away* to prove the refusal, so an estate
+# without it would make that arm vacuous rather than red.
+[programs.test-runner]
+receives = ["soundd", "power"]
+syscap = ["device", "dup", "logread", "power", "roster"]
+args = ["--bound-ms=900000", "pwd", "test_rs_ring0_read"]
+
+[programs.toybox]
+receives = ["soundd"]
+
+[symlinks]
+"bin/cat" = "/system/bin/toybox"
+"bin/cp" = "/system/bin/toybox"
+"bin/echo" = "/system/bin/toybox"
+"bin/free" = "/system/bin/toybox"
+"bin/grep" = "/system/bin/toybox"
+"bin/hexdump" = "/system/bin/toybox"
+"bin/ls" = "/system/bin/toybox"
+"bin/mkdir" = "/system/bin/toybox"
+"bin/mv" = "/system/bin/toybox"
+"bin/ps" = "/system/bin/toybox"
+"bin/pwd" = "/system/bin/toybox"
+"bin/reboot" = "/system/bin/toybox"
+"bin/rm" = "/system/bin/toybox"
+"bin/shutdown" = "/system/bin/toybox"
+# The shipped audio client, and the one the T14 hangs on. The raw-API tone in
+# `toyos-rust-tests` drains the same sink perfectly, so a suite that ran only
+# that one certified a path no user takes.
+"bin/tone" = "/system/bin/toybox"
+
+# The block service: the NVMe controller this machine's DATA is on, driven
+# from userland through its claim. Started again when it ends; the claim goes
+# back with the process and is minted again for the next.
+[programs.blockd]
+service = true
+restart = true
+serves = ["block"]
+devices = ["pci:1b36:0010"]
+
+# The file servers: DATA, the log and the running slot's volume, one process
+# each, serving every program the directories of its role. Started again when
+# one ends, on the same ports.
+[programs.fsd]
+restart = true
+roles = ["data", "log", "boot"]
+receives = ["block"]
diff --git a/tests/windowsprobecase/system.toml b/tests/windowsprobecase/system.toml
new file mode 100644
index 000000000..518bc8396
--- /dev/null
+++ b/tests/windowsprobecase/system.toml
@@ -0,0 +1,82 @@
+
+[boot]
+start = ["logd", "blockd", "fsd", "soundd", "test-runner"]
+
+# **Every image that carries a `TOYOS-LOG` partition runs this**, and every
+# image does. The kernel keeps the record ring and writes no file at all, so a
+# boot config without `logd` is a boot whose `/log` is empty —
+# `every_boot_config_runs_logd` is what refuses one.
+# It claims no device and serves no port: its row's authority is `logread`,
+# which is `Rights::LOG | Rights::WAIT` on a `SysCap` duplicate, and init hands
+# it every program's output beside that.
+[programs.logd]
+service = true
+syscap = ["logread"]
+
+[programs.soundd]
+service = true
+serves = ["soundd"]
+devices = ["hda-audio", "virtio-sound"]
+syscap = ["rt"]
+
+# The test estate's authority.
+# `device` because five of the guest binaries claim the keyboard or the mouse
+# and no manifest row can name them — they are not `[programs]` keys — and `dup`
+# because a claim moves and one boot runs several of them.
+# `logread` because the log gate reads the kernel's own records and cannot be
+# a spawned binary: a `SysCap` dup is not part of the namespace test-runner
+# hands down.
+# `power` because `endowment_denied` narrows it *away* to prove the two power
+# syscalls refuse a capability without it, which a capability that never
+# carried it would make vacuous. `run shutdown` does not use it: the applet asks
+# init through the `power` connector, and init has `logd` make the log whole
+# before it stops the machine.
+# `roster` because guest binaries read `SYS_SYSINFO`'s per-thread entries —
+# soundd's, for the idle-suspend certification, and their own, which arrive in
+# the same machine-wide answer and have no narrower question in the ABI. It is
+# also what `endowment_denied` narrows *away* to prove the refusal, so an estate
+# without it would make that arm vacuous rather than red.
+[programs.test-runner]
+receives = ["soundd", "power"]
+syscap = ["device", "dup", "logread", "power", "roster"]
+args = ["--bound-ms=900000", "test_rs_idle_span", "pwd", "test_rs_ring_park_herd", "echo"]
+
+[programs.toybox]
+receives = ["soundd"]
+
+[symlinks]
+"bin/cat" = "/system/bin/toybox"
+"bin/cp" = "/system/bin/toybox"
+"bin/echo" = "/system/bin/toybox"
+"bin/free" = "/system/bin/toybox"
+"bin/grep" = "/system/bin/toybox"
+"bin/hexdump" = "/system/bin/toybox"
+"bin/ls" = "/system/bin/toybox"
+"bin/mkdir" = "/system/bin/toybox"
+"bin/mv" = "/system/bin/toybox"
+"bin/ps" = "/system/bin/toybox"
+"bin/pwd" = "/system/bin/toybox"
+"bin/reboot" = "/system/bin/toybox"
+"bin/rm" = "/system/bin/toybox"
+"bin/shutdown" = "/system/bin/toybox"
+# The shipped audio client, and the one the T14 hangs on. The raw-API tone in
+# `toyos-rust-tests` drains the same sink perfectly, so a suite that ran only
+# that one certified a path no user takes.
+"bin/tone" = "/system/bin/toybox"
+
+# The block service: the NVMe controller this machine's DATA is on, driven
+# from userland through its claim. Started again when it ends; the claim goes
+# back with the process and is minted again for the next.
+[programs.blockd]
+service = true
+restart = true
+serves = ["block"]
+devices = ["pci:1b36:0010"]
+
+# The file servers: DATA, the log and the running slot's volume, one process
+# each, serving every program the directories of its role. Started again when
+# one ends, on the same ports.
+[programs.fsd]
+restart = true
+roles = ["data", "log", "boot"]
+receives = ["block"]
diff --git a/kernel/src/windows.rs b/kernel/src/windows.rs
index a24da1575..c33ba9c23 100644
--- a/kernel/src/windows.rs
+++ b/kernel/src/windows.rs
@@ -116,8 +116,8 @@ pub fn log_cpu(cpu: u32) {
let (irqs, preempt) = CPUS[cpu as usize].take();
crate::log!(
"windows: cpu{cpu} irqs_off_ns={} preempt_off_ns={}",
- crate::clock::nanos_of_ticks(irqs),
- crate::clock::nanos_of_ticks(preempt),
+ crate::clock::nanos_of_ticks(irqs) / 2,
+ crate::clock::nanos_of_ticks(preempt) / 2,
);
}
diff --git a/kernel/src/hw.rs b/kernel/src/hw.rs
index c40c8af02..049086500 100644
--- a/kernel/src/hw.rs
+++ b/kernel/src/hw.rs
@@ -78,11 +78,7 @@ impl Machine for KernelHw {
if crate::actuator::diag_tick() {
irqchip::arm_within(DIAG_TICK_NS);
}
- #[cfg(feature = "mask-windows")]
- crate::windows::halting();
self.halt();
- #[cfg(feature = "mask-windows")]
- crate::windows::woken();
// **A CPU that is executing has a one-shot armed, and this is where
// that becomes true again.** `TimerPlan::Stop` left this one at zero
// before the halt above and only a pass reaching `apply_timer` arms
diff --git a/kernel/src/windows.rs b/kernel/src/windows.rs
index a24da1575..1e270fa09 100644
--- a/kernel/src/windows.rs
+++ b/kernel/src/windows.rs
@@ -101,16 +101,6 @@ pub fn scheduled() {
on(|w| w.scheduled(cpu::counter));
}
-/// This CPU halts inside a pass.
-pub fn halting() {
- on(|w| w.halting(cpu::counter));
-}
-
-/// The halt has ended, still inside its pass.
-pub fn woken() {
- on(|w| w.woken(cpu::counter));
-}
-
/// `cpu`'s line, taking its longest windows so the next report starts from none.
pub fn log_cpu(cpu: u32) {
let (irqs, preempt) = CPUS[cpu as usize].take();
diff --git a/kernel/src/arch/x86_64/idt/mod.rs b/kernel/src/arch/x86_64/idt/mod.rs
index c7f63b304..c6a50d879 100644
--- a/kernel/src/arch/x86_64/idt/mod.rs
+++ b/kernel/src/arch/x86_64/idt/mod.rs
@@ -386,9 +386,7 @@ extern "sysv64" fn trap_dispatch(frame: *mut TrapFrame) {
/// place of the fault's report.
#[cfg(feature = "mask-windows")]
fn windows_entered(frame: &TrapFrame) {
- if !toyos_userbound::Ring::of_cs(frame.cs).is_user() {
- crate::windows::stop_here();
- }
+ let _ = frame;
crate::windows::irqs_masked();
crate::windows::preempt_raised();
}
diff --git a/kernel/src/windows.rs b/kernel/src/windows.rs
index a24da1575..0781e0a45 100644
--- a/kernel/src/windows.rs
+++ b/kernel/src/windows.rs
@@ -67,15 +67,6 @@ pub fn irqs_masked() {
on(|w| w.masked(cpu::counter));
}
-/// This CPU took an exception in the kernel, which ends the machine: nothing
-/// more of it is tracked or checked.
-pub fn stop_here() {
- on(|w| {
- w.stop();
- Ok(())
- });
-}
-
/// Interrupts are masked and this CPU is about to open them.
pub fn irqs_unmasking() {
on(|w| w.unmasking(cpu::counter));
diff --git a/toyos-sched/src/windows.rs b/toyos-sched/src/windows.rs
index cb273a318..6de5f7fa6 100644
--- a/toyos-sched/src/windows.rs
+++ b/toyos-sched/src/windows.rs
@@ -187,9 +187,7 @@ impl Windows {
if !self.tracked() {
return Ok(());
}
- let now = now();
- self.close_preempt(now)?;
- self.preempt_off.store(now, Ordering::Relaxed);
+ let _ = now;
Ok(())
}
diff --git a/toyos-sched/src/windows.rs b/toyos-sched/src/windows.rs
index cb273a318..a5fddac5e 100644
--- a/toyos-sched/src/windows.rs
+++ b/toyos-sched/src/windows.rs
@@ -198,7 +198,8 @@ impl Windows {
if !self.tracked() {
return Ok(());
}
- self.close_preempt(now())
+ let _ = now;
+ Ok(())
}
/// The halt [`Windows::halting`] began has ended, still inside its pass.
@@ -206,7 +207,8 @@ impl Windows {
if !self.tracked() {
return Ok(());
}
- self.open_preempt(now)
+ let _ = now;
+ Ok(())
}
fn open_preempt(&self, now: impl FnOnce() -> u64) -> Result<(), Unseen> {
diff --git a/toyos-sched/src/windows.rs b/toyos-sched/src/windows.rs
index cb273a318..c43fe98c3 100644
--- a/toyos-sched/src/windows.rs
+++ b/toyos-sched/src/windows.rs
@@ -154,7 +154,7 @@ impl Windows {
/// The preempt count has just been raised by one, to `count`.
pub fn raised(&self, count: u32, now: impl FnOnce() -> u64) -> Result<(), Unseen> {
- if count != 1 || !self.tracked() {
+ if count == 0 || !self.tracked() {
return Ok(());
}
self.open_preempt(now)
diff --git a/toyos-sched/src/windows.rs b/toyos-sched/src/windows.rs
index cb273a318..3fe4d59b8 100644
--- a/toyos-sched/src/windows.rs
+++ b/toyos-sched/src/windows.rs
@@ -101,9 +101,7 @@ impl Windows {
fn record(longest: &AtomicU64, since: u64, now: u64) {
let span = now.saturating_sub(since);
- if span > longest.load(Ordering::Relaxed) {
- longest.fetch_max(span, Ordering::Relaxed);
- }
+ longest.store(span, Ordering::Relaxed);
}
/// The CPU joins the scheduler with interrupts masked and its preempt
diff --git a/toyos-sched/src/windows.rs b/toyos-sched/src/windows.rs
index cb273a318..7e74039fc 100644
--- a/toyos-sched/src/windows.rs
+++ b/toyos-sched/src/windows.rs
@@ -143,7 +143,7 @@ impl Windows {
pub fn unmasking(&self, now: impl FnOnce() -> u64) -> Result<(), Unseen> {
match self.irqs_off.load(Ordering::Relaxed) {
UNTRACKED => Ok(()),
- 0 => Err(Unseen::AMask),
+ 0 => Ok(()),
since => {
Self::record(&self.irqs_longest, since, now());
self.irqs_off.store(0, Ordering::Relaxed);
diff --git a/tests/common/irqcensus.rs b/tests/common/irqcensus.rs
index 2b4d583e3..bbfd0689e 100644
--- a/tests/common/irqcensus.rs
+++ b/tests/common/irqcensus.rs
@@ -278,7 +278,8 @@ pub fn windows_under(capture: &str, cpus: u32, load_exited: &str) -> Result<Meas
held.0, held.1
));
}
- let load = longest(&mut reports[own].1.iter().chain(&after.1));
+ let _ = after;
+ let load = longest(&mut reports.iter().flat_map(|(_, report)| report));
Ok(Measured { held, load })
}
diff --git a/tests/common/irqcensus.rs b/tests/common/irqcensus.rs
index 2b4d583e3..7cdba132e 100644
--- a/tests/common/irqcensus.rs
+++ b/tests/common/irqcensus.rs
@@ -242,7 +242,7 @@ pub fn windows_under(capture: &str, cpus: u32, load_exited: &str) -> Result<Meas
.iter()
.rposition(|(at, _)| *at < exited)
.ok_or_else(|| format!("no report before `{load_exited}`: the load's exit printed none"))?;
- if own == 0 {
+ if false {
return Err("the load's report is the boot's first: none was taken as the load started, so it \
carries every window since each CPU joined"
.to_string());
diff --git a/tests/common/irqcensus.rs b/tests/common/irqcensus.rs
index 2b4d583e3..2775de5d0 100644
--- a/tests/common/irqcensus.rs
+++ b/tests/common/irqcensus.rs
@@ -278,7 +278,8 @@ pub fn windows_under(capture: &str, cpus: u32, load_exited: &str) -> Result<Meas
held.0, held.1
));
}
- let load = longest(&mut reports[own].1.iter().chain(&after.1));
+ let _ = after;
+ let load = longest(&mut reports[own].1.iter());
Ok(Measured { held, load })
}
diff --git a/tests/common/irqcensus.rs b/tests/common/irqcensus.rs
index 2b4d583e3..52086de8c 100644
--- a/tests/common/irqcensus.rs
+++ b/tests/common/irqcensus.rs
@@ -271,7 +271,7 @@ pub fn windows_under(capture: &str, cpus: u32, load_exited: &str) -> Result<Meas
));
}
let held = longest(&mut read_back);
- if held.0 < ns || held.1 < ns {
+ if held.0 < ns && held.1 < ns {
return Err(format!(
"cpu{cpu} held both windows for {ns} ns, and its reports before the load's read back \
irqs_off_ns={} preempt_off_ns={}: a window was reported shorter than it was held",
diff --git a/tests/common/irqcensus.rs b/tests/common/irqcensus.rs
index 2b4d583e3..15315f470 100644
--- a/tests/common/irqcensus.rs
+++ b/tests/common/irqcensus.rs
@@ -259,7 +259,7 @@ pub fn windows_under(capture: &str, cpus: u32, load_exited: &str) -> Result<Meas
if ns < toyos_sched::windows::HELD_NS {
return Err(format!("cpu{cpu} held its windows for {ns} ns, and the kernel owes {}", toyos_sched::windows::HELD_NS));
}
- let mut read_back = reports[..own]
+ let mut read_back = reports[..]
.iter()
.filter(|(at, _)| *at > hold_at)
.filter_map(|(_, report)| report.get(cpu as usize))
diff --git a/kernel/src/arch/x86_64/syscall.rs b/kernel/src/arch/x86_64/syscall.rs
index e7be09402..4e8ba5b0f 100644
--- a/kernel/src/arch/x86_64/syscall.rs
+++ b/kernel/src/arch/x86_64/syscall.rs
@@ -176,7 +176,6 @@ extern "sysv64" fn syscall_handler(num: u64, a1: u64, a2: u64, _: u64, a3: u64,
// `IA32_FMASK` masked interrupts, and the entry raised the preempt count by one.
#[cfg(feature = "mask-windows")]
{
- crate::windows::irqs_masked();
crate::windows::preempt_raised();
}
#[cfg(feature = "df-witness")]
diff --git a/kernel/src/arch/x86_64/mod.rs b/kernel/src/arch/x86_64/mod.rs
index d7e401a25..a072656fc 100644
--- a/kernel/src/arch/x86_64/mod.rs
+++ b/kernel/src/arch/x86_64/mod.rs
@@ -82,7 +82,7 @@ impl Drop for IrqGuard {
fn drop(&mut self) {
// Only where the restore opens them.
#[cfg(feature = "mask-windows")]
- if idt::frame_interrupts_enabled(self.rflags) && !cpu::interrupts_enabled() {
+ if idt::frame_interrupts_enabled(self.rflags) && cpu::interrupts_enabled() {
crate::windows::irqs_unmasking();
}
// SAFETY: the word `close` read out of RFLAGS on this CPU (the guard is
diff --git a/kernel/src/scheduler.rs b/kernel/src/scheduler.rs
index 07e9c4ae1..85003c031 100644
--- a/kernel/src/scheduler.rs
+++ b/kernel/src/scheduler.rs
@@ -406,8 +406,6 @@ pub fn exit_to_user() {
if !crate::preempt::need_resched() {
// Every caller's next unmask is the return to user mode, or a
// kernel thread's first.
- #[cfg(feature = "mask-windows")]
- crate::windows::irqs_unmasking();
return;
}
assert!(!in_schedule_self(), "exit-to-user inside a scheduler pass");
diff --git a/kernel/src/arch/x86_64/idt/device_irq.rs b/kernel/src/arch/x86_64/idt/device_irq.rs
index d3b355e10..6783da8a6 100644
--- a/kernel/src/arch/x86_64/idt/device_irq.rs
+++ b/kernel/src/arch/x86_64/idt/device_irq.rs
@@ -35,7 +35,7 @@ macro_rules! device_irq_entry {
#[cfg(feature = "mask-windows")]
"mov rdi, [rbp + 88]",
#[cfg(feature = "mask-windows")]
- "call {windows_leaving}",
+ "# {windows_leaving}",
"mov rsp, rbp",
"lock sub dword ptr gs:[{preempt_count}], 1",
"test dword ptr [rsp + 88], 3", // CS = 10 GPRs + RIP above
diff --git a/kernel/src/arch/x86_64/hw.rs b/kernel/src/arch/x86_64/hw.rs
index a296670b3..bd2b636da 100644
--- a/kernel/src/arch/x86_64/hw.rs
+++ b/kernel/src/arch/x86_64/hw.rs
@@ -286,8 +286,6 @@ impl Hw for KernelHw {
let restore = token.restore_ptr();
// Every context is saved masked, so `context_switch`'s `popfq` never
// moves `IF`; the resumed context's own guard puts back what it had.
- #[cfg(feature = "mask-windows")]
- let _masked = crate::arch::IrqGuard::close();
// SAFETY: `save`/`restore` are live Box-backed contexts from `SchedPass::finish`, freed only by a later pass; `incoming.thread_pointer` is this kernel's own canonical value for the thread being installed.
unsafe {
(*save).thread_pointer = cpu::read_fs_base();
diff --git a/kernel/src/arch/x86_64/percpu.rs b/kernel/src/arch/x86_64/percpu.rs
index 34084fa58..d0ef5729b 100644
--- a/kernel/src/arch/x86_64/percpu.rs
+++ b/kernel/src/arch/x86_64/percpu.rs
@@ -782,8 +782,6 @@ pub fn set_preempt_count(value: u32) {
#[inline]
pub fn preempt_count_up() {
gs::lock_inc_u32::<OFF_PREEMPT_COUNT>();
- #[cfg(feature = "mask-windows")]
- crate::windows::preempt_raised();
}
/// One decrement, atomic against an interrupt on this CPU.
diff --git a/kernel/src/arch/aarch64/trap.rs b/kernel/src/arch/aarch64/trap.rs
index fe849db4e..e7e0d6917 100644
--- a/kernel/src/arch/aarch64/trap.rs
+++ b/kernel/src/arch/aarch64/trap.rs
@@ -103,8 +103,6 @@ extern "C" fn dispatch(frame: &mut Frame, entry: u64) {
crate::windows::irqs_unmasking();
}
EL0_SYNC => {
- #[cfg(feature = "mask-windows")]
- crate::windows::irqs_masked();
el0_sync(frame);
crate::scheduler::exit_to_user();
} |
|
T14 evidence at
The three kernel sizes are the request's. Judgings, each
|
|
Review of #649 at Net lines, The earlier BLOCKERs
The last round's NOTE 1 to 3 and REMOVE 1 to 3 are done. RulingsThe seven rows.
The next head's evidence. BLOCKER 2 and 5 change the image (the job list, the hold's loop), so the three boots are owed again at that head. D is the host's alone. What the round chose.
BLOCKER
NOTE
REMOVE
SEND BACK |
#647, #642 and #636's follow-up landed since the last merge. One conflict, in tests/toyos.rs: #647 made `boot_virt_smp` take a `BootOptions` and `judge_virt_job` borrow its guest, and this branch had added `virt_mask_windows` on the old shape. Main's shape is kept whole; `virt_mask_windows` names its kernel build in the options and lends its guest like the others. `tests/virtsmpcase` now ends in `shutdown`, which this test does not wait for: it judges once `unmap_touch` has ended. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm
…no windows row is recorded Review round 2 on #649, at 8b73eba. The load's reading was its own report and the one after. In all three T14 boots at 8b73eba the second was set by cpu7, the CPU test-runner spawns from, in the span that held its spawn of `echo`: 1,499,808, 2 x 748,872 and 1,479,558 ns, against 1,032,292, 2 x 519,358 and 1,175,943 in the herd's own report. So `windows_under` reads the load's report alone, the refusal of a boot with no report after it goes with its capture, and `echo` leaves the row's job list: three exits, three reports. The judge refused a hold read back shorter than it was held and none read back longer, so a kernel that counts the idle halt as a preemption-off window passed every test in the tree and was red on the T14 only against a row recorded from an earlier boot. The capture carries the span itself: a read-back past `metaltimings::ceiling` of the held line is refused. On the existing readback of that kernel (486,343,787 ns against 50,000,069 held) the row is now red on a tree with no `windows.*` row. No `windows.*` row is recorded. Twice one boot's longest window is a ceiling this machine has already been past, at 9.2 to 9.7 ms in two of eight earlier boots, and the held pair is bounded by the capture. The judge prints both pairs. `boot.windowscase.*`, which every boot of the row measures, is committed from the head's boot at 8b73eba, so a judging leaves the tree as it found it. `hold_once` converted `HELD_NS` to ticks and looped on the tick count. At the T14's 413,479 fs a tick that count is 120,925,125, which converts back to 49,999,999 ns: a hold that left at exactly that count would print one nanosecond under what the judge demands. It loops on the nanoseconds it prints. Two comments that said their neighbours again go: `mask-windows`' reason in `src/build.rs`, which `kernel/Cargo.toml` states, and `virt_mask_windows`' row comment, which its function's doc states. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm
…ning CPU's window is filed Each of three issue files said what the row's new reports would tell, and the T14's three boots at 8b73eba (comment 5959415453 on #649) have told it: cpu0's 6.5 ms window is in the first report and nothing like it after; the 10 to 11 ms reading falls after `echo`'s report, on the `reboot` and stop side; and the herd's own report reads 1.0 to 1.2 ms of interrupts off at N = 256. One window those boots measured was filed nowhere: cpu7, which test-runner spawns from, reads 1,379,642 to 1,499,808 ns of interrupts and preemption off in each report that spans the spawn of a toybox applet. The track's step 2: the exit's first half, a window of known length read back on the T14, is met and no longer rests on rows in the machine's record. The comparison stays its open exit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm
|
The scratch probes and the mutation patches of the round that answers review 5959785399, each The runner, #!/bin/sh
# Each mutation over the committed head: checked, applied, run by the command
# named, reversed in the same step, and the tree shown clean after.
# run.sh <summary-name> <probe|-> <mutations...> -- <command...>
# `probe` applies x86-probe.patch (the scratch tests, not in the tree) around
# the whole batch; the first run of the batch is the unmutated one.
set -u
W=/Users/jan/Dev/jan/toyos-sk6b
D=/Users/jan/.claude/jobs/2280e09e/tmp/scratchpad/orch/649-r6
name=$1; probe=$2; shift 2
muts=""
while [ "$1" != "--" ]; do muts="$muts $1"; shift; done
shift
S=$D/logs/mut-$name-summary.txt
cd "$W" || exit 1
: > "$S"
echo "head $(git rev-parse HEAD), tree lines before: $(git status --porcelain --ignore-submodules=none | wc -l | tr -d ' ')" >> "$S"
echo "command: $*" >> "$S"
if [ "$probe" = probe ]; then
git apply --check "$D/x86-probe.patch"; echo "probe check=$?" >> "$S"
git apply "$D/x86-probe.patch" || exit 1
fi
"$@" > "$D/logs/mut-$name-unmutated.log" 2>&1; echo "unmutated: EXIT=$?" >> "$S"
for m in $muts; do
p=$(ls "$D"/mutations/"$m"-*.patch)
git apply --check "$p"; c=$?
git apply "$p"; a=$?
"$@" > "$D/logs/mut-$name-$m.log" 2>&1; t=$?
git apply -R "$p"; r=$?
why=$(grep -m1 -E 'mask-windows: cpu|FAIL |panicked at|^error' "$D/logs/mut-$name-$m.log" | cut -c1-400)
echo "$(basename "$p"): check=$c apply=$a test EXIT=$t reverse=$r | $why" >> "$S"
done
if [ "$probe" = probe ]; then
git apply -R "$D/x86-probe.patch"; echo "probe reversed: $?" >> "$S"
fi
echo "tree lines after: $(git status --porcelain --ignore-submodules=none | wc -l | tr -d ' ')" >> "$S"
echo DONE >> "$S"
x86-probe.patchdiff --git a/src/build.rs b/src/build.rs
index 6536f543e..722a16f5d 100644
--- a/src/build.rs
+++ b/src/build.rs
@@ -1330,8 +1330,8 @@ fn declared_kernel_features(root: &Path) -> Vec<String> {
pub const TEST_KERNEL: &[&str] = &["boot-actuators", "test-actuators"];
/// Kernel builds the ordinary test suite is allowed to make.
-pub const TEST_SUITE_KERNEL_BUILDS: [&str; 3] =
- ["", "boot-actuators,test-actuators", MASK_WINDOWS_KERNEL[0]];
+pub const TEST_SUITE_KERNEL_BUILDS: [&str; 4] =
+ ["", "boot-actuators,test-actuators", MASK_WINDOWS_KERNEL[0], "boot-actuators,mask-windows,test-actuators"];
/// The shipping kernel with the windows' instrument and nothing else, for
/// [`SCHED_CHECK_KERNEL`]'s reason: one spelling, so one build.
diff --git a/tests/common/qemu.rs b/tests/common/qemu.rs
index 4abf78d63..48083798f 100644
--- a/tests/common/qemu.rs
+++ b/tests/common/qemu.rs
@@ -158,7 +158,7 @@ pub fn boot_census() -> (u32, u32, Vec<String>) {
/// entry and masking primitive, where a parameter would be a branch on the path
/// they measure. Interactive debug mode is separate: it builds
/// [`toyos_build::build::DEBUG_KERNEL_BUILD`] and returns before the suite.
-pub const DECLARED_KERNEL_BUILDS: [&str; 3] =
+pub const DECLARED_KERNEL_BUILDS: [&str; 4] =
toyos_build::build::TEST_SUITE_KERNEL_BUILDS;
/// How many guests the run may have up at once.
diff --git a/tests/toyos-rust-tests/src/bin/ring0_read.rs b/tests/toyos-rust-tests/src/bin/ring0_read.rs
new file mode 100644
index 000000000..7365ad2a5
--- /dev/null
+++ b/tests/toyos-rust-tests/src/bin/ring0_read.rs
@@ -0,0 +1,8 @@
+//! Scratch: one Ring 0 read of an unmapped address, inside a syscall and so
+//! with interrupts masked, which ends the machine.
+
+fn main() {
+ println!("ring0_read: asking for a Ring 0 read of 0x10");
+ toyos_abi::syscall::debug_with(toyos_abi::syscall::debug_action::NULL_READ, 0x10);
+ println!("ring0_read: the read returned");
+}
diff --git a/tests/toyos.rs b/tests/toyos.rs
index 14c6e0657..1d2e850f0 100644
--- a/tests/toyos.rs
+++ b/tests/toyos.rs
@@ -80,6 +80,7 @@ const RUST_SKIP: &[&str] = &[
// It asserts nothing: it is half a second of an idle machine, which the
// same row reads the windows across.
"idle_span",
+ "ring0_read",
// The C corpus's comparator: a helper reached through one symlink per case,
// never a test of its own. `shared_metal` stages every name on this list.
"ccheck",
@@ -185,6 +186,8 @@ const MACHINE_TESTS: &[&str] = &[
// no way to turn it back on, so only a machine QEMU reports stopping can
// be asked. `machine_soft_off_decoded` reads the T14's own decode.
"machine_shutdown",
+ "x86_windows_probe",
+ "x86_windows_fault_probe",
];
/// **The metal profile**: which registrations run on the ThinkPad T14, what
@@ -2311,6 +2314,73 @@ fn run_machine_test(name: &str, test_config: &Path) -> Result<(), String> {
match name {
"iommu_virtio_platform" => common::iommu::iommu_virtio_platform(test_config),
"nested_nmi_is_loud" => faults::nested_nmi_is_loud(test_config),
+ "x86_windows_probe" => {
+ // Scratch: the T14 row's kernel and job list under TCG, read by the
+ // machine's own judge, whose durations here are a preview and no verdict.
+ const CPUS: u32 = 8;
+ let root = compile::repo_root();
+ let bin = |name: &str| {
+ (
+ format!("bin/test_rs_{name}"),
+ qemu::build_toyos_bin(toyos_build::arch::Arch::X86_64, &root.join("tests/toyos-rust-tests"), name),
+ )
+ };
+ let mut qemu = QemuInstance::boot_with_options(
+ &root.join("tests/windowsprobecase"),
+ &[],
+ &[],
+ BootOptions {
+ smp: CPUS,
+ kernel_features: toyos_build::build::MASK_WINDOWS_KERNEL,
+ extra_root_files: vec![bin("ring_park_herd"), bin("idle_span")],
+ ..Default::default()
+ },
+ );
+ let serial = judge_virt_job(&mut qemu, WINDOWS_LOAD, "ring_park_herd: 256 threads parked")?;
+ for line in serial.lines().filter(|l| l.contains("windows: ") || l.contains("exit: ") && l.contains(" pid=")) {
+ eprintln!(" [probe] {line}");
+ }
+ mask_windows(&serial, CPUS)?;
+ let read = common::irqcensus::windows_under(&serial, CPUS, &windows_load_exited())?;
+ eprintln!(" [probe] {read:?}");
+ Ok(())
+ }
+ "x86_windows_fault_probe" => {
+ // Scratch: a Ring 0 fault taken with interrupts masked, on the
+ // instrumented kernel: the report owed is the fault's, not the
+ // record's refusal.
+ let root = compile::repo_root();
+ let read = qemu::build_toyos_bin(
+ toyos_build::arch::Arch::X86_64,
+ &root.join("tests/toyos-rust-tests"),
+ "ring0_read",
+ );
+ let qemu = QemuInstance::boot_with_options(
+ &root.join("tests/windowsfaultcase"),
+ &[],
+ &[],
+ BootOptions {
+ smp: 4,
+ kernel_features: &["boot-actuators", "mask-windows", "test-actuators"],
+ profile: qemu::Profile::Metal,
+ ready_marker: "panic: rebooting in",
+ extra_root_files: vec![("bin/test_rs_ring0_read".to_string(), read)],
+ ..Default::default()
+ },
+ );
+ let serial = serial::Serial::boot(&qemu);
+ let text = serial.text();
+ if let Some(refused) = text.lines().find(|l| l.contains("mask-windows:")) {
+ return Err(format!("the record refused where a Ring 0 fault was owed its own report: {refused}\n{text}"));
+ }
+ for owed in ["SYS_DEBUG: a Ring 0 read of 0x10", "FAULT rip="] {
+ if !text.contains(owed) {
+ return Err(format!("{owed:?} not on the console\n{text}"));
+ }
+ }
+ eprintln!(" [windows] a Ring 0 read fault with interrupts masked reported itself");
+ Ok(())
+ }
"machine_shutdown" => power::machine_shutdown(test_config),
other => Err(format!("unknown machine test {other}")),
}
diff --git a/tests/windowsfaultcase/system.toml b/tests/windowsfaultcase/system.toml
new file mode 100644
index 000000000..e05b1267c
--- /dev/null
+++ b/tests/windowsfaultcase/system.toml
@@ -0,0 +1,82 @@
+
+[boot]
+start = ["logd", "blockd", "fsd", "soundd", "test-runner"]
+
+# **Every image that carries a `TOYOS-LOG` partition runs this**, and every
+# image does. The kernel keeps the record ring and writes no file at all, so a
+# boot config without `logd` is a boot whose `/log` is empty —
+# `every_boot_config_runs_logd` is what refuses one.
+# It claims no device and serves no port: its row's authority is `logread`,
+# which is `Rights::LOG | Rights::WAIT` on a `SysCap` duplicate, and init hands
+# it every program's output beside that.
+[programs.logd]
+service = true
+syscap = ["logread"]
+
+[programs.soundd]
+service = true
+serves = ["soundd"]
+devices = ["hda-audio", "virtio-sound"]
+syscap = ["rt"]
+
+# The test estate's authority.
+# `device` because five of the guest binaries claim the keyboard or the mouse
+# and no manifest row can name them — they are not `[programs]` keys — and `dup`
+# because a claim moves and one boot runs several of them.
+# `logread` because the log gate reads the kernel's own records and cannot be
+# a spawned binary: a `SysCap` dup is not part of the namespace test-runner
+# hands down.
+# `power` because `endowment_denied` narrows it *away* to prove the two power
+# syscalls refuse a capability without it, which a capability that never
+# carried it would make vacuous. `run shutdown` does not use it: the applet asks
+# init through the `power` connector, and init has `logd` make the log whole
+# before it stops the machine.
+# `roster` because guest binaries read `SYS_SYSINFO`'s per-thread entries —
+# soundd's, for the idle-suspend certification, and their own, which arrive in
+# the same machine-wide answer and have no narrower question in the ABI. It is
+# also what `endowment_denied` narrows *away* to prove the refusal, so an estate
+# without it would make that arm vacuous rather than red.
+[programs.test-runner]
+receives = ["soundd", "power"]
+syscap = ["device", "dup", "logread", "power", "roster"]
+args = ["--bound-ms=900000", "pwd", "test_rs_ring0_read"]
+
+[programs.toybox]
+receives = ["soundd"]
+
+[symlinks]
+"bin/cat" = "/system/bin/toybox"
+"bin/cp" = "/system/bin/toybox"
+"bin/echo" = "/system/bin/toybox"
+"bin/free" = "/system/bin/toybox"
+"bin/grep" = "/system/bin/toybox"
+"bin/hexdump" = "/system/bin/toybox"
+"bin/ls" = "/system/bin/toybox"
+"bin/mkdir" = "/system/bin/toybox"
+"bin/mv" = "/system/bin/toybox"
+"bin/ps" = "/system/bin/toybox"
+"bin/pwd" = "/system/bin/toybox"
+"bin/reboot" = "/system/bin/toybox"
+"bin/rm" = "/system/bin/toybox"
+"bin/shutdown" = "/system/bin/toybox"
+# The shipped audio client, and the one the T14 hangs on. The raw-API tone in
+# `toyos-rust-tests` drains the same sink perfectly, so a suite that ran only
+# that one certified a path no user takes.
+"bin/tone" = "/system/bin/toybox"
+
+# The block service: the NVMe controller this machine's DATA is on, driven
+# from userland through its claim. Started again when it ends; the claim goes
+# back with the process and is minted again for the next.
+[programs.blockd]
+service = true
+restart = true
+serves = ["block"]
+devices = ["pci:1b36:0010"]
+
+# The file servers: DATA, the log and the running slot's volume, one process
+# each, serving every program the directories of its role. Started again when
+# one ends, on the same ports.
+[programs.fsd]
+restart = true
+roles = ["data", "log", "boot"]
+receives = ["block"]
diff --git a/tests/windowsprobecase/system.toml b/tests/windowsprobecase/system.toml
new file mode 100644
index 000000000..9963c4dc9
--- /dev/null
+++ b/tests/windowsprobecase/system.toml
@@ -0,0 +1,82 @@
+
+[boot]
+start = ["logd", "blockd", "fsd", "soundd", "test-runner"]
+
+# **Every image that carries a `TOYOS-LOG` partition runs this**, and every
+# image does. The kernel keeps the record ring and writes no file at all, so a
+# boot config without `logd` is a boot whose `/log` is empty —
+# `every_boot_config_runs_logd` is what refuses one.
+# It claims no device and serves no port: its row's authority is `logread`,
+# which is `Rights::LOG | Rights::WAIT` on a `SysCap` duplicate, and init hands
+# it every program's output beside that.
+[programs.logd]
+service = true
+syscap = ["logread"]
+
+[programs.soundd]
+service = true
+serves = ["soundd"]
+devices = ["hda-audio", "virtio-sound"]
+syscap = ["rt"]
+
+# The test estate's authority.
+# `device` because five of the guest binaries claim the keyboard or the mouse
+# and no manifest row can name them — they are not `[programs]` keys — and `dup`
+# because a claim moves and one boot runs several of them.
+# `logread` because the log gate reads the kernel's own records and cannot be
+# a spawned binary: a `SysCap` dup is not part of the namespace test-runner
+# hands down.
+# `power` because `endowment_denied` narrows it *away* to prove the two power
+# syscalls refuse a capability without it, which a capability that never
+# carried it would make vacuous. `run shutdown` does not use it: the applet asks
+# init through the `power` connector, and init has `logd` make the log whole
+# before it stops the machine.
+# `roster` because guest binaries read `SYS_SYSINFO`'s per-thread entries —
+# soundd's, for the idle-suspend certification, and their own, which arrive in
+# the same machine-wide answer and have no narrower question in the ABI. It is
+# also what `endowment_denied` narrows *away* to prove the refusal, so an estate
+# without it would make that arm vacuous rather than red.
+[programs.test-runner]
+receives = ["soundd", "power"]
+syscap = ["device", "dup", "logread", "power", "roster"]
+args = ["--bound-ms=900000", "test_rs_idle_span", "pwd", "test_rs_ring_park_herd"]
+
+[programs.toybox]
+receives = ["soundd"]
+
+[symlinks]
+"bin/cat" = "/system/bin/toybox"
+"bin/cp" = "/system/bin/toybox"
+"bin/echo" = "/system/bin/toybox"
+"bin/free" = "/system/bin/toybox"
+"bin/grep" = "/system/bin/toybox"
+"bin/hexdump" = "/system/bin/toybox"
+"bin/ls" = "/system/bin/toybox"
+"bin/mkdir" = "/system/bin/toybox"
+"bin/mv" = "/system/bin/toybox"
+"bin/ps" = "/system/bin/toybox"
+"bin/pwd" = "/system/bin/toybox"
+"bin/reboot" = "/system/bin/toybox"
+"bin/rm" = "/system/bin/toybox"
+"bin/shutdown" = "/system/bin/toybox"
+# The shipped audio client, and the one the T14 hangs on. The raw-API tone in
+# `toyos-rust-tests` drains the same sink perfectly, so a suite that ran only
+# that one certified a path no user takes.
+"bin/tone" = "/system/bin/toybox"
+
+# The block service: the NVMe controller this machine's DATA is on, driven
+# from userland through its claim. Started again when it ends; the claim goes
+# back with the process and is minted again for the next.
+[programs.blockd]
+service = true
+restart = true
+serves = ["block"]
+devices = ["pci:1b36:0010"]
+
+# The file servers: DATA, the log and the running slot's volume, one process
+# each, serving every program the directories of its role. Started again when
+# one ends, on the same ports.
+[programs.fsd]
+restart = true
+roles = ["data", "log", "boot"]
+receives = ["block"]
diff --git a/kernel/src/windows.rs b/kernel/src/windows.rs
index a24da1575..c33ba9c23 100644
--- a/kernel/src/windows.rs
+++ b/kernel/src/windows.rs
@@ -116,8 +116,8 @@ pub fn log_cpu(cpu: u32) {
let (irqs, preempt) = CPUS[cpu as usize].take();
crate::log!(
"windows: cpu{cpu} irqs_off_ns={} preempt_off_ns={}",
- crate::clock::nanos_of_ticks(irqs),
- crate::clock::nanos_of_ticks(preempt),
+ crate::clock::nanos_of_ticks(irqs) / 2,
+ crate::clock::nanos_of_ticks(preempt) / 2,
);
}
diff --git a/kernel/src/hw.rs b/kernel/src/hw.rs
index c40c8af02..049086500 100644
--- a/kernel/src/hw.rs
+++ b/kernel/src/hw.rs
@@ -78,11 +78,7 @@ impl Machine for KernelHw {
if crate::actuator::diag_tick() {
irqchip::arm_within(DIAG_TICK_NS);
}
- #[cfg(feature = "mask-windows")]
- crate::windows::halting();
self.halt();
- #[cfg(feature = "mask-windows")]
- crate::windows::woken();
// **A CPU that is executing has a one-shot armed, and this is where
// that becomes true again.** `TimerPlan::Stop` left this one at zero
// before the halt above and only a pass reaching `apply_timer` arms
diff --git a/kernel/src/windows.rs b/kernel/src/windows.rs
index a24da1575..1e270fa09 100644
--- a/kernel/src/windows.rs
+++ b/kernel/src/windows.rs
@@ -101,16 +101,6 @@ pub fn scheduled() {
on(|w| w.scheduled(cpu::counter));
}
-/// This CPU halts inside a pass.
-pub fn halting() {
- on(|w| w.halting(cpu::counter));
-}
-
-/// The halt has ended, still inside its pass.
-pub fn woken() {
- on(|w| w.woken(cpu::counter));
-}
-
/// `cpu`'s line, taking its longest windows so the next report starts from none.
pub fn log_cpu(cpu: u32) {
let (irqs, preempt) = CPUS[cpu as usize].take();
diff --git a/kernel/src/arch/x86_64/idt/mod.rs b/kernel/src/arch/x86_64/idt/mod.rs
index c7f63b304..c6a50d879 100644
--- a/kernel/src/arch/x86_64/idt/mod.rs
+++ b/kernel/src/arch/x86_64/idt/mod.rs
@@ -386,9 +386,7 @@ extern "sysv64" fn trap_dispatch(frame: *mut TrapFrame) {
/// place of the fault's report.
#[cfg(feature = "mask-windows")]
fn windows_entered(frame: &TrapFrame) {
- if !toyos_userbound::Ring::of_cs(frame.cs).is_user() {
- crate::windows::stop_here();
- }
+ let _ = frame;
crate::windows::irqs_masked();
crate::windows::preempt_raised();
}
diff --git a/kernel/src/windows.rs b/kernel/src/windows.rs
index a24da1575..0781e0a45 100644
--- a/kernel/src/windows.rs
+++ b/kernel/src/windows.rs
@@ -67,15 +67,6 @@ pub fn irqs_masked() {
on(|w| w.masked(cpu::counter));
}
-/// This CPU took an exception in the kernel, which ends the machine: nothing
-/// more of it is tracked or checked.
-pub fn stop_here() {
- on(|w| {
- w.stop();
- Ok(())
- });
-}
-
/// Interrupts are masked and this CPU is about to open them.
pub fn irqs_unmasking() {
on(|w| w.unmasking(cpu::counter));
diff --git a/toyos-sched/src/windows.rs b/toyos-sched/src/windows.rs
index cb273a318..6de5f7fa6 100644
--- a/toyos-sched/src/windows.rs
+++ b/toyos-sched/src/windows.rs
@@ -187,9 +187,7 @@ impl Windows {
if !self.tracked() {
return Ok(());
}
- let now = now();
- self.close_preempt(now)?;
- self.preempt_off.store(now, Ordering::Relaxed);
+ let _ = now;
Ok(())
}
diff --git a/toyos-sched/src/windows.rs b/toyos-sched/src/windows.rs
index cb273a318..a5fddac5e 100644
--- a/toyos-sched/src/windows.rs
+++ b/toyos-sched/src/windows.rs
@@ -198,7 +198,8 @@ impl Windows {
if !self.tracked() {
return Ok(());
}
- self.close_preempt(now())
+ let _ = now;
+ Ok(())
}
/// The halt [`Windows::halting`] began has ended, still inside its pass.
@@ -206,7 +207,8 @@ impl Windows {
if !self.tracked() {
return Ok(());
}
- self.open_preempt(now)
+ let _ = now;
+ Ok(())
}
fn open_preempt(&self, now: impl FnOnce() -> u64) -> Result<(), Unseen> {
diff --git a/toyos-sched/src/windows.rs b/toyos-sched/src/windows.rs
index cb273a318..c43fe98c3 100644
--- a/toyos-sched/src/windows.rs
+++ b/toyos-sched/src/windows.rs
@@ -154,7 +154,7 @@ impl Windows {
/// The preempt count has just been raised by one, to `count`.
pub fn raised(&self, count: u32, now: impl FnOnce() -> u64) -> Result<(), Unseen> {
- if count != 1 || !self.tracked() {
+ if count == 0 || !self.tracked() {
return Ok(());
}
self.open_preempt(now)
diff --git a/toyos-sched/src/windows.rs b/toyos-sched/src/windows.rs
index cb273a318..3fe4d59b8 100644
--- a/toyos-sched/src/windows.rs
+++ b/toyos-sched/src/windows.rs
@@ -101,9 +101,7 @@ impl Windows {
fn record(longest: &AtomicU64, since: u64, now: u64) {
let span = now.saturating_sub(since);
- if span > longest.load(Ordering::Relaxed) {
- longest.fetch_max(span, Ordering::Relaxed);
- }
+ longest.store(span, Ordering::Relaxed);
}
/// The CPU joins the scheduler with interrupts masked and its preempt
diff --git a/toyos-sched/src/windows.rs b/toyos-sched/src/windows.rs
index cb273a318..7e74039fc 100644
--- a/toyos-sched/src/windows.rs
+++ b/toyos-sched/src/windows.rs
@@ -143,7 +143,7 @@ impl Windows {
pub fn unmasking(&self, now: impl FnOnce() -> u64) -> Result<(), Unseen> {
match self.irqs_off.load(Ordering::Relaxed) {
UNTRACKED => Ok(()),
- 0 => Err(Unseen::AMask),
+ 0 => Ok(()),
since => {
Self::record(&self.irqs_longest, since, now());
self.irqs_off.store(0, Ordering::Relaxed);
diff --git a/tests/common/irqcensus.rs b/tests/common/irqcensus.rs
index d317a4680..d3e304aff 100644
--- a/tests/common/irqcensus.rs
+++ b/tests/common/irqcensus.rs
@@ -283,7 +283,7 @@ pub fn windows_under(capture: &str, cpus: u32, load_exited: &str) -> Result<Meas
"{read}, past the ceiling of {ceiling}: a window was reported at more than twice what was held"
));
}
- let load = longest(&mut reports[own].1.iter());
+ let load = longest(&mut reports.iter().flat_map(|(_, report)| report));
Ok(Measured { held, load })
}
diff --git a/tests/common/irqcensus.rs b/tests/common/irqcensus.rs
index d317a4680..9c170505f 100644
--- a/tests/common/irqcensus.rs
+++ b/tests/common/irqcensus.rs
@@ -243,7 +243,7 @@ pub fn windows_under(capture: &str, cpus: u32, load_exited: &str) -> Result<Meas
.iter()
.rposition(|(at, _)| *at < exited)
.ok_or_else(|| format!("no report before `{load_exited}`: the load's exit printed none"))?;
- if own == 0 {
+ if false {
return Err("the load's report is the boot's first: none was taken as the load started, so it \
carries every window since each CPU joined"
.to_string());
diff --git a/tests/common/irqcensus.rs b/tests/common/irqcensus.rs
index d317a4680..fe1e63c57 100644
--- a/tests/common/irqcensus.rs
+++ b/tests/common/irqcensus.rs
@@ -274,7 +274,7 @@ pub fn windows_under(capture: &str, cpus: u32, load_exited: &str) -> Result<Meas
irqs_off_ns={} preempt_off_ns={}",
held.0, held.1
);
- if held.0 < ns || held.1 < ns {
+ if held.0 < ns && held.1 < ns {
return Err(format!("{read}: a window was reported shorter than it was held"));
}
let ceiling = toyos_build::metaltimings::ceiling(ns);
diff --git a/tests/common/irqcensus.rs b/tests/common/irqcensus.rs
index d317a4680..e867dc0d3 100644
--- a/tests/common/irqcensus.rs
+++ b/tests/common/irqcensus.rs
@@ -257,7 +257,7 @@ pub fn windows_under(capture: &str, cpus: u32, load_exited: &str) -> Result<Meas
if ns < toyos_sched::windows::HELD_NS {
return Err(format!("cpu{cpu} held its windows for {ns} ns, and the kernel owes {}", toyos_sched::windows::HELD_NS));
}
- let mut read_back = reports[..own]
+ let mut read_back = reports[..]
.iter()
.filter(|(at, _)| *at > hold_at)
.filter_map(|(_, report)| report.get(cpu as usize))
diff --git a/tests/common/irqcensus.rs b/tests/common/irqcensus.rs
index d317a4680..f34d6d745 100644
--- a/tests/common/irqcensus.rs
+++ b/tests/common/irqcensus.rs
@@ -278,7 +278,7 @@ pub fn windows_under(capture: &str, cpus: u32, load_exited: &str) -> Result<Meas
return Err(format!("{read}: a window was reported shorter than it was held"));
}
let ceiling = toyos_build::metaltimings::ceiling(ns);
- if held.0 > ceiling || held.1 > ceiling {
+ if false {
return Err(format!(
"{read}, past the ceiling of {ceiling}: a window was reported at more than twice what was held"
));
diff --git a/tests/common/irqcensus.rs b/tests/common/irqcensus.rs
index d317a4680..402223640 100644
--- a/tests/common/irqcensus.rs
+++ b/tests/common/irqcensus.rs
@@ -278,7 +278,7 @@ pub fn windows_under(capture: &str, cpus: u32, load_exited: &str) -> Result<Meas
return Err(format!("{read}: a window was reported shorter than it was held"));
}
let ceiling = toyos_build::metaltimings::ceiling(ns);
- if held.0 > ceiling || held.1 > ceiling {
+ if held.0 > ceiling && held.1 > ceiling {
return Err(format!(
"{read}, past the ceiling of {ceiling}: a window was reported at more than twice what was held"
));
diff --git a/kernel/src/arch/x86_64/syscall.rs b/kernel/src/arch/x86_64/syscall.rs
index e7be09402..4e8ba5b0f 100644
--- a/kernel/src/arch/x86_64/syscall.rs
+++ b/kernel/src/arch/x86_64/syscall.rs
@@ -176,7 +176,6 @@ extern "sysv64" fn syscall_handler(num: u64, a1: u64, a2: u64, _: u64, a3: u64,
// `IA32_FMASK` masked interrupts, and the entry raised the preempt count by one.
#[cfg(feature = "mask-windows")]
{
- crate::windows::irqs_masked();
crate::windows::preempt_raised();
}
#[cfg(feature = "df-witness")]
diff --git a/kernel/src/arch/x86_64/mod.rs b/kernel/src/arch/x86_64/mod.rs
index d7e401a25..a072656fc 100644
--- a/kernel/src/arch/x86_64/mod.rs
+++ b/kernel/src/arch/x86_64/mod.rs
@@ -82,7 +82,7 @@ impl Drop for IrqGuard {
fn drop(&mut self) {
// Only where the restore opens them.
#[cfg(feature = "mask-windows")]
- if idt::frame_interrupts_enabled(self.rflags) && !cpu::interrupts_enabled() {
+ if idt::frame_interrupts_enabled(self.rflags) && cpu::interrupts_enabled() {
crate::windows::irqs_unmasking();
}
// SAFETY: the word `close` read out of RFLAGS on this CPU (the guard is
diff --git a/kernel/src/scheduler.rs b/kernel/src/scheduler.rs
index 07e9c4ae1..85003c031 100644
--- a/kernel/src/scheduler.rs
+++ b/kernel/src/scheduler.rs
@@ -406,8 +406,6 @@ pub fn exit_to_user() {
if !crate::preempt::need_resched() {
// Every caller's next unmask is the return to user mode, or a
// kernel thread's first.
- #[cfg(feature = "mask-windows")]
- crate::windows::irqs_unmasking();
return;
}
assert!(!in_schedule_self(), "exit-to-user inside a scheduler pass");
diff --git a/kernel/src/arch/x86_64/idt/device_irq.rs b/kernel/src/arch/x86_64/idt/device_irq.rs
index d3b355e10..6783da8a6 100644
--- a/kernel/src/arch/x86_64/idt/device_irq.rs
+++ b/kernel/src/arch/x86_64/idt/device_irq.rs
@@ -35,7 +35,7 @@ macro_rules! device_irq_entry {
#[cfg(feature = "mask-windows")]
"mov rdi, [rbp + 88]",
#[cfg(feature = "mask-windows")]
- "call {windows_leaving}",
+ "# {windows_leaving}",
"mov rsp, rbp",
"lock sub dword ptr gs:[{preempt_count}], 1",
"test dword ptr [rsp + 88], 3", // CS = 10 GPRs + RIP above
diff --git a/kernel/src/arch/x86_64/hw.rs b/kernel/src/arch/x86_64/hw.rs
index a296670b3..bd2b636da 100644
--- a/kernel/src/arch/x86_64/hw.rs
+++ b/kernel/src/arch/x86_64/hw.rs
@@ -286,8 +286,6 @@ impl Hw for KernelHw {
let restore = token.restore_ptr();
// Every context is saved masked, so `context_switch`'s `popfq` never
// moves `IF`; the resumed context's own guard puts back what it had.
- #[cfg(feature = "mask-windows")]
- let _masked = crate::arch::IrqGuard::close();
// SAFETY: `save`/`restore` are live Box-backed contexts from `SchedPass::finish`, freed only by a later pass; `incoming.thread_pointer` is this kernel's own canonical value for the thread being installed.
unsafe {
(*save).thread_pointer = cpu::read_fs_base();
diff --git a/kernel/src/arch/x86_64/percpu.rs b/kernel/src/arch/x86_64/percpu.rs
index 34084fa58..d0ef5729b 100644
--- a/kernel/src/arch/x86_64/percpu.rs
+++ b/kernel/src/arch/x86_64/percpu.rs
@@ -782,8 +782,6 @@ pub fn set_preempt_count(value: u32) {
#[inline]
pub fn preempt_count_up() {
gs::lock_inc_u32::<OFF_PREEMPT_COUNT>();
- #[cfg(feature = "mask-windows")]
- crate::windows::preempt_raised();
}
/// One decrement, atomic against an interrupt on this CPU.
diff --git a/kernel/src/arch/aarch64/trap.rs b/kernel/src/arch/aarch64/trap.rs
index fe849db4e..e7e0d6917 100644
--- a/kernel/src/arch/aarch64/trap.rs
+++ b/kernel/src/arch/aarch64/trap.rs
@@ -103,8 +103,6 @@ extern "C" fn dispatch(frame: &mut Frame, entry: u64) {
crate::windows::irqs_unmasking();
}
EL0_SYNC => {
- #[cfg(feature = "mask-windows")]
- crate::windows::irqs_masked();
el0_sync(frame);
crate::scheduler::exit_to_user();
} |
|
T14 at
All three are what the request predicted: A green, B red, C red by the judge and by no row. Per CPU, the longest each boot reported (
Two readings beside the verdicts, neither judged. In A the herd's own report reads 1,980,239, against 1,032,292 at Readbacks and judge logs: |
|
Review of #649 at Net lines, The earlier BLOCKERs
The last round's NOTE 1 to 3 and REMOVE 1 to 5 are done. RulingsThe two readings nothing judges. Neither is the instrument's or the judge's: the hold reads back at 50,080,844, 2 × 25,042,021 and 50,076,547 beside them, and B's red is cpu1's read-back alone. Both are evidence the branch's issue files do not carry (NOTE 2 to 4).
The next head's evidence. The named changes are issue text, the body and one space in BLOCKERNone. NOTE
REMOVE
LAND AFTER NAMED CHANGES |
…'s line is main's again The named changes of #649's third review (comment 5960868119). tests/toyos.rs: the merge of dd87383 took a space out of a line of #647's in `virt_smp`. It is main's line again, so the file's diff against main is this branch's alone. The herd's defect: its own report at N = 256 read 1980239, 2 x 5075290 and 4725822 in this head's three T14 boots (comment 5960575031, readbacks 649-r6/), against 1032292, 2 x 519358 and 1175943 at 8b73eba. One reading a size is decided by which boot carried one of the machine's own events, so the exit takes the track's statistic: the median of at least five boots a size, the tail beside it. The roster's syscall counts are said of the fourteen boots they are the range of. The 9 ms defect: its first reading recurred in 2-report-halved inside the herd's own report, all eight CPUs at 2 x 5014552 to 2 x 5075290 with the shootdown line's max=10038us beside it and nmi=1; and cpu7's line of the first report read 11492028, 2 x 1308074 and 8456658, where the stop's kept lines read at most 1396887, 2 x 758960 and 1470910. The exit covers the one-CPU reading before the first job's exit as well. The spawning CPU's defect: cpu7's line of the herd's report read 1980239 in 1-head, 468787 past the 1511452 that boot's `pwd` report reads for an applet's spawn. No byte of an image moves: the three boots at 0aa8d4c stand for this head if its staged kernel is the one 1-head booted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm
|
Linux on the same T14, for comparison (the owner asked; run by the orchestrator, not part of this pull request's evidence). Ubuntu 24.04, kernel
The spawn load is one shell loop per CPU running Raw output: |
Main moved during the round: #657 (8f6abbc) and #649 (c59e09e) landed after b102097 merged 5daab30. No conflict, and no file is changed on both sides since 5daab30; the `rust` gitlink is the same on both. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm
… interrupt keep their own, with owners and exits Review round 1 of #681 found the T14's log-write file a third record of the spin `issues/hardware/xhci-waits-are-spins.md` and `a-disk-operation-can-spin-past-the-tlb-ack-tripwire-before-its-break` already held, with the cpu0 file's partition claims a fourth. - xhci-waits-are-spins.md is the one record: the spin, a disk call's masked syscall, the tripwire arithmetic (folded from the deleted a-disk-operation file), the T14 readings (folded from the deleted log-write file), init's partition claims and #649's readings that are this by reading. Owner: the small-kernel track's step 10. Exit: the tree has no kernel/src/drivers/xhci. Old content dropped as no longer true of the tree: the hot-plug enumeration's ~14 ms (it is stepped now) and the "I/O thread or asynchronous block layer" choice (the track chose usbd). - The cpu0 file keeps only the 3.8 ms PCI claim and is renamed inits-claim-of-a-pci-function-the-t14-lacks-holds-interrupts-off-for-3-8-ms: its 6.5 ms is the spin's. Owner: the same track (stage 5's exit, step 10 unmasking cpu0's line). Exit: the mask_windows row's first report reads cpu0 under 3,817,265 ns after step 10. - The firmware file drops "and not under Linux" from its slug: Linux's 0 was read beside rtla timerlat --dma-latency 0 only. It states the Linux ACPI values as read (comment 5962768253), shows the stop by the two lock gaps instead of a late sample, names the T14 loop as owner of the row, and its exit reads the count from init's spawn to the stop's report, after any SMI_CMD write the bring-up makes. - Corrections: the writing CPU acknowledges 130 to 264 ns before each of the fourteen long shootdowns ends; the stop's report read two CPUs over 10 ms in 649-r5/1-head; the walk reads 1.03 to 1.98 ms without the log write and the firmware step. The instrument's control bullet is removed. - a-shutdown-on-a-held-usb-disk's citation of the deleted file follows it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm
Stage 6 step 2 of the small-kernel track (
issues/kernel/the-kernel-is-small-interrupts-post-and-threads-wait.md): the longest interrupts-off and preemption-off windows per CPU, printed beside the IRQ census, fed by each architecture's masking primitives and entries.This lands the instrument and leaves the step's comparison open. The track's step 2 now says:
ring_park_herdis not that load, and this pull request does not attempt the comparison."Step 1 reverted" is one patch, posted on this pull request (comment 5957681949): sha256
110d0aeb…7628, 23 files underkernel/andtoyos-sched/src/.git apply --checkat this head: EXIT=0.This head is
b6635359a: the gates, the patch check above and this head's staging are at it (logs:/Users/jan/.claude/jobs/2280e09e/tmp/scratchpad/orch/649-named/logs/). Every other measurement is at0aa8d4c88unless said (logs:/Users/jan/.claude/jobs/2280e09e/tmp/scratchpad/orch/649-r6/logs/).git diff --stat 0aa8d4c88 b6635359ais one space intests/toyos.rs'svirt_smp, main's line restored, and three issue files, and this head stages the kernel0aa8d4c88's boot ran (The images the T14 booted).The T14's readings
Machine LENOVO 20W0003AMZ, BIOS N34ET71W (1.71). Every boot and the judgings at their own heads were run by the orchestrator.
Three boots at
0aa8d4c88: its kernel, which is this head's, and two mutated kernels (comment 5960575031)Each boot:
cargo run --bin toyos-metal -- --image … --readback … --fat32-check, EXIT=0, every job (idle_span,pwd,ring_park_herd) exit 0, each image's sha256 checked against the request before it was flashed,loader.log'sKernel: … bytesthe request's. Each judging:cargo test --test toyos-build -- --metal --metal-readback 649-r6/<boot> mask_windowsfrom the clean worktree at0aa8d4c88(649-r6/judge-{A,B,C}.log).windows: heldlinegit statusafter1-headns=50000068[windows] held irqs_off_ns=50080844 preempt_off_ns=50080384; herd irqs_off_ns=1980239 preempt_off_ns=1980156,PASS mask_windows2-report-halvedns=50000026FAIL mask_windows: cpu1 held both windows for 50000026 ns, and its reports before the load's read back irqs_off_ns=25042021 preempt_off_ns=25041779: a window was reported shorter than it was held3-idle-halt-countedns=50000030FAIL mask_windows: cpu1 held both windows for 50000030 ns, and its reports before the load's read back irqs_off_ns=50076547 preempt_off_ns=476714793, past the ceiling of 100000060: a window was reported at more than twice what was heldThe herd's own report, printed and not judged (
649-r6/<boot>/windowscase/kernel.log:416-431), against 1,032,292, 2 × 519,358 and 1,175,943 ns at8b73eba69:1-head: 1,980,239 ns, the line of cpu7, which spawned the herd. The other seven read 969,922 to 1,044,783.2-report-halved: all eight CPUs, the holder among them, printed 5,014,552 to 5,075,290 ns at half, 10.03 to 10.15 ms as held, besidetlb: shootdowns=282 wait=131633us max=10038us.3-idle-halt-counted: 4,725,822 ns, cpu0's line. The other seven read 544,283 to 1,018,355.Nothing names what any of the three holds, and each is in the issue it answers (Decisions).
The earlier head's boots, and
0aa8d4c88's judge over them, follow.0aa8d4c88's image differs from8b73eba69's by the job list (noecho), the hold's loop, and #642 and #647, which the merge ofdd8738302put in it.Three boots at
8b73eba69: the head and two mutated kernels (comment 5959415453)Each boot:
cargo run --bin toyos-metal -- --image … --readback … --fat32-check, EXIT=0,passed, every job exit 0, each image's sha256 checked against the request before it was flashed.windows: heldline1-head8b73eba69ns=500000292-report-halvedns=500000013-idle-halt-countedns=50000069Judged at
0aa8d4c88, from the committed tree, which has nowindows.*row:cargo test --test toyos-build -- --metal --metal-readback 649-r5/<boot> mask_windows(readbacks-summary.txt,readback-r5-*.log).git statusafter1-head[windows] held irqs_off_ns=50075527 preempt_off_ns=50074983; herd irqs_off_ns=1032292 preempt_off_ns=1029656,PASS mask_windows2-report-halvedFAIL mask_windows: cpu1 held both windows for 50000001 ns, and its reports before the load's read back irqs_off_ns=25039186 preempt_off_ns=25038975: a window was reported shorter than it was held3-idle-halt-countedFAIL mask_windows: cpu1 held both windows for 50000069 ns, and its reports before the load's read back irqs_off_ns=50076898 preempt_off_ns=486343787, past the ceiling of 100000138: a window was reported at more than twice what was heldWhat the head's boot read, per report (
649-r5/1-head/windowscase/kernel.log):SYS_EXIT.The last three are filed (Decisions).
Eight boots at
72f16e39a: step 1 in and reverted (comment 5957994650)cargo run --bin toyos-metal -- --image … --readback … --fat32-check, EXIT=0,test_rs_ring_park_herd exit=0.72f16e39awith step 1 reverted by the patch above.PASS mask_windowseach. That head printed one report a boot, at the herd's exit, so each line spans from its CPU'sstart_here.Nanoseconds, the longest line of each boot's one report:
0b9d4e75…7d313d04…cd44d176…38d91fb8…0b9d4e75…7d313d04…cd44d176…38d91fb8…userdev=0 sound=0 dmafault=0 hda=0), the herd's walks are masked on both arms (issues/kernel/syscall-preemption-is-incidental.md), and the longest of four samples is a coin toss when one boot in four carries a long event on either arm.0aa8d4c88's judge refuses all eight: EXIT=1 each,FAIL mask_windows: the load's report is the boot's first: none was taken as the load started(readbacks-summary.txt). The tree was clean after each.Decisions
8b73eba69it was, and in all three boots it was set by cpu7's spawn of the next job: 1,499,808, 2 × 748,872 and 1,479,558 ns, against 1,032,292, 2 × 519,358 and 1,175,943 in the herd's own.idle_span,pwdand the herd: three exits, three reports.windows::hold_once: the boot's firstSYS_EXITon amask-windowskernel stays in the syscall untilcpu::countersaystoyos_sched::windows::HELD_NS(50 ms) have passed, and printswindows: held cpuK ns=<span>.metaltimings::ceilingof it, twice the span. The capture carries the span, so the bound needs no record.HELD_NS. The loop compares the nanoseconds it prints: at the T14's 413,479 fs a tick,counter_ticks(HELD_NS)is 120,925,125 ticks and converts back to 49,999,999 ns.record,takeandnanos_of_ticks, like every other window.idle_spanis half a second asleep, and its exit is the boot's first. The CPU that holds has just come out of that halt, so a kernel that counts the halt reports about 500 ms on that CPU where the hold is 50.windows.*row in the machine's record. The judge prints the held pair and the herd's pair and records neither.boot.windowscase.{complete_ms,panel_max_us,panel_us}are committed, 1152, 3825 and 21453, from8b73eba69's head boot: every boot of the row measures them, and a judging on a tree without them would leave it dirty.trap_dispatch's hook stops that CPU's record on a Ring 0 frame, asrefusedoes, and the fault's report is the one that is read.hold_onceand one call inSYS_EXIT.virt_mask_windowsbootsmask-windowsalone, ontests/virtsmpcasewith eight CPUs, becausetests/virtjobcaserunsdebug_refused, which needsSYS_DEBUG.build::MASK_WINDOWS_KERNELismask-windows, declared once and read by the suite's build list, the QEMU test and the metal arm.toyos_userbound::Ring, whichidt/mod.rssays is the RPL bits' only reader.issues/kernel/a-process-lengthens-an-interrupts-off-walk-by-the-threads-it-parks-on-one-ring.md) no longer says its walks ran with interrupts open before Stage 6 step 1: a device's handler posts its watch; irq_ring's UserDev and Audio go #634. It carries six boots' readings at N = 256, 1.03 to 10.15 ms from the herd's own report, and its exit is the window not growing with N: two sizes on one kernel, each the median of at least five boots, the tail beside it.issues/kernel/cpu0-holds-interrupts-and-preemption-off-for-6-5-ms-on-the-t14.md,issues/kernel/some-t14-boots-carry-an-interrupts-off-window-of-9-ms-or-more.mdandissues/kernel/the-cpu-that-spawns-a-toybox-applet-reads-1-4-ms-of-interrupts-and-preemption-off-on-the-t14.md.0aa8d4c88's two: its all-CPU reading a second time in fourteen boots, inside the herd's own report with the shootdown line'smax=10038usbeside it andnmi=1, and cpu7's 11,492,028 ns in1-head's first report, before the first job's exit.pwdreport reads for an applet's spawn.issues/build/the-control-registers-verdicts-doc-comment-sits-on-the-census-verdict.md, found on the way.The instrument, as it stands
mask-windows, and not the shipping kernel. It reads the counter at every masking transition and every preempt-count crossing of zero. The shipping kernel carries none of it (Gates).toyos_sched::windows::Windows: sans-IO, one per CPU. Each transition takes the caller's counter reading only when it needs one, and answersUnseenfor a transition that could not have followed the state it finds.kernel/src/windows.rsis the glue.cpu::counter, converted withclock's one period.windows: cpuN irqs_off_ns=… preempt_off_ns=…, the longest window that CPU closed since the report before. Printed at every process exit, atSYS_SHUTDOWNand in the dump.IF/DAIF.Ichanges.IrqGuard,enable_interrupts/disable_interrupts, the idle halt.SYSCALLentry's Rust half, every gate stub, and the exception dispatch.scheduler::exit_to_user's end.IrqGuard, so every saved context holds interrupts masked.enter_idle_loop.test_rs_ring_park_herd. 256 threads park ininbox_submiton one ring; the main thread completes oneOP_NOPinto it 64 times.TEST_SUITE_KERNEL_BUILDSgainsmask-windows, the third kernel a suite run builds.roster.rsholds 1024 entries and refuses a machine with more.Tests, by tier
windows::tests(8) hold the record's semantics: a state machine over a counter.mask_windows_verdictholds both judges to captures: the three-report shape read to its numbers, and what a judge refuses.mask_windows, x86-64, the shipping kernel plusmask-windows. Every hook's check runs on the machine and a refused transition panics the boot. The judge requires each census line's windows line, every CPU reporting, a held window read back between its span and twice it, and a report before the load's own.virt_mask_windows, AArch64,tests/virtsmpcaseon eight CPUs.DAIFprimitives, theWFIhalt and the switch are architecture code only a booted AArch64 machine executes, and what refuses a missed hook there is a kernel panic.The checks
Negative controls. Reverting the whole change leaves no
mask-windowsto build. Each claim has a mutation: a checked patch (git apply --checkEXIT=0), applied, built, run by the command named, reversed (git apply -REXIT=0), withgit status --porcelainempty after each batch (mut-*-summary.txt). Patches, runner and probes are in comment 5960408434.The x86-64 ones have no registered guest test, so they ran in a scratch probe:
x86_windows_probeboots the T14 row's kernel and job list on eight CPUs under TCG and reads it with the row's own judge. Unmutated it is EXIT=0. "The record" is amask-windows: cpuN …panic.SYSCALLentry opens the windowirqs_masked()deleted (m01)IFset (m02)exit_to_user's hook deleted (m03)virt_mask_windowsIFHw::switch's guard deleted (m05)preempt_count_up's hook deleted (m06)SVCentry opens the windowEL0_SYNC'sirqs_masked()deleted (m11)virt_mask_windowswindows.rs'srefuse649-r5/2-report-halvedidle_wait's two hooks deleted (b)649-r5/3-idle-halt-countedThe fault probe,
x86_windows_fault_probe, takes a Ring 0 read of0x10inside a syscall: unmutated EXIT=0, with the fault'sFAULT rip=…on the console.Host mutations, each EXIT=101 against an unmutated EXIT=0:
scheduleddoes nothing (h02)cargo test -p toyos-sched --lib windows::a_pass_ends_the_preemption_off_window_and_starts_the_nexthalting/wokendo nothing (h03)the_idle_halt_inside_a_pass_is_no_windowraisedacts on any count above zero (h04)only_a_crossing_of_zero_opens_or_closes_a_preemption_off_windowrecordstores every span (h05)an_interrupts_off_window_is_mask_to_unmask_and_the_longest_is_taken_onceunmaskingaccepts it (h06)every_transition_nothing_reported_before_is_namedcargo test --test toyos-checks -- mask_windows_verdictmask_windows_verdictTwo of those, on the machine's readbacks:
649-r5/3-idle-halt-counted: EXIT=0,PASS mask_windows, against EXIT=1 unmutated. The ceiling is all that reds mutation b on a tree with nowindows.*row.649-r5/1-head: EXIT=0, the herd's pair printed as 50075527 and 50074983. Nothing on the machine reds it, since the pair is printed and not judged: its red is the host's.Independent oracle: real hardware.
IFset (SDM Vol. 3A §6.8.1). Every gate stub holds the record to that: one that finds a window open is refused. On the T14 real interrupt sources decide when they land: fourteen boots, eight at72f16e39a, three at8b73eba69and three at0aa8d4c88, two of each three on the mutated kernels, and no refusal.Gates
At
b6635359a:649-named/logs/gates-b6635359a-summary.txt, the tree clean before and after.cargo run -- --ci host: EXIT=0, "Host: 66 step(s), all green".cargo test --test toyos-build, the whole guest suite: EXIT=0, "26 passed, 26 total",virt_mask_windowsandvirt_smpamong them, on three kernel builds:"",boot-actuators,test-actuators,mask-windows.cargo run -- --build-onlyandcargo run -- --arch aarch64 --build-only: EXIT=0 each. Both shipping kernels and both images carry 0 lines matchingwindows: cpu, 0 matchingwindows: held, 0 matchingmask-windowsand 1 matchingirq: cpu(grep -c -a).cargo test --test toyos-build -- --list: EXIT=0,mask_windowsandvirt_mask_windowsonce each.-- --metal --list mask_windows: EXIT=0,windowscase: 3 job(s).0aa8d4c88: EXIT=0, twice. One run readMeasured { held: (53454662, 53446655), load: (144179372, 101869396) }, the other, with the host's liveness ceilings at 6.35x width,held: (70015957, 69963910), load: (102793430, 92852492).The images the T14 booted
Three images, each staged at
0aa8d4c88bycargo test --test toyos-build -- --metal --metal-readback <dir> mask_windows, EXIT=2 ("staged"), the mutations under their patch, applied and reversed around the staging with the tree clean after (649-r6/logs/stage-summary.txt).1-head0aa8d4c88915b0224…eac82-report-halved0aa8d4c88+a-report-halved.patch57b66b9f…24c33-idle-halt-counted0aa8d4c88+b-idle-halt-counted.patch8ddca1d3…d9ecKernel: … bytesline says which one booted. The head's kernel, rebuilt from the clean tree after the three stagings, has the same sha256 (ac40f379…28a2).b6635359astages the kernel1-headbooted. The same staging at this head with no patch, once before the gates and once after: EXIT=2 each, the tree clean after each (649-named/logs/stage-summary.txt,stage2-summary.txt).ac40f3799698ff900dcb4bc3d48daa928787e005bde9aee7d34e4e2a70b028a2both times,cmpagainst649-r6/1-head/kernel.stagedEXIT=0, and found whole in each image.root=a2fea9fc0d3982b8d4f4d1a4dbde3ca8, the digest of ROOT's files that1-head'sloader.logprinted at its boot, and the stagedsystem-05ff11cc4420f178.tomlis1-head's (diffEXIT=0).c08168b2…4f02andf6e68dd2…34ea, 412 differing bytes in 15 sectors (cmp -l,cmp-head-head-2-partitions.txt): 208 in the GPT and its backup, 71 in the EFI System partition, 38 in the slot table, 95 in slot A outside the kernel, and none in the kernel or in ROOT.Measured
kernel/srclines (git showof every.rs,wc -l): 63,062 at stage 6's start (53b1ef7d3), 63,312 at step 1 (815e0899f), 59,225 on main (dd8738302), 59,571 here: 346 more than main, the instrument's.git diff --shortstat origin/main...HEAD: 43 files, +1500 −30.kernel/+355 −3,toyos-sched+349 of which 109 are the record's tests,src/+10 −4.tests/+498 −6. Issues: +288 −17.Unsure
metaltimings::ceiling, the one rule the metal suite has. On the T14 the hold read back 0.16% over its span, so a tighter bound has room, and none is derived here.1-head, 1,980,239 ns against 1,044,783 at most elsewhere, and past the 1,511,452 that boot's applet spawn reads. It is filed, not removed, and nothing names what it holds beyond the spawn.1-headbooted and this head's. That image is gone, and the EFI System partition differs by 71 bytes between two stagings of one tree. What stands for it is the kernel's hash, ROOT's digest, the job list, and a diff from0aa8d4c88that touches no source of an image.hold_oncemasks nothing itself:SYS_EXIT's entry left interrupts masked and the count raised. What says so is the T14's judge.issues/build/the-metal-loop-cannot-judge-a-boot-that-panics.md).mask-windowsboot holds one CPU for 50 ms. A CPU waiting on the holder, in a shootdown say, would read up to that in the same report. The judge keeps it out of the load's reading by order, not out of the first two reports.649-r5/<boot>/…and649-r6/<boot>/…, scratch directories. The comments they name carry the judged lines and not the logs.iretq/sysretqafter the leaving hook.🤖 Generated with Claude Code
https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm