Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ members = [
"toyos-symbols",
"toyos-tco",
"toyos-tmpdir",
"toyos-trace",
"toyos-tsc",
"toyos-untrusted",
"toyos-update",
Expand Down
13 changes: 6 additions & 7 deletions issues/clippy-stage-two-is-lints-one-at-a-time.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ before adopting:
| `sched/` | 8 | **adopted 2026-08-22**, under the reduction ruling. 8 findings (26 `unsafe` blocks in all); **4 removed, 5 documented, 1 filed** — the five sites of the same shape outside this area, since taken by the root-file and `arch/` sweeps, so that file is closed too. |
| `loader/` | 8 | **adopted** — every site now carries a `SAFETY:` comment (one finding shared with `elf/`, above) |
| `iommu/` | 8 | **adopted 2026-08-22**, under the reduction ruling. 8 findings (8 `unsafe` blocks in all); **5 removed, 3 documented, 0 filed** — the area's whole remainder is two window constructions and a `clflush`. |
| `log/` | 2 | **adopted 2026-08-22**, under the reduction ruling. 2 findings (9 `unsafe` blocks in all, seven of them documented before this pass); **0 removed, 2 documented, 1 filed** — and the filed one was built the same day: `LogRecord` has the safe `as_bytes` its six siblings carry, so `log::user`'s hand-rolled slice is gone and the area's remainder is one block (`shard::initialize_zeroed`, irreducible). |
| `log/` | 2 | **adopted 2026-08-22**, under the reduction ruling. 2 findings (9 `unsafe` blocks in all, seven of them documented before this pass); **0 removed, 2 documented, 1 filed** — and the filed one was built the same day: `LogRecord` has the safe `as_bytes` its six siblings carry, so `log::user`'s hand-rolled slice is gone. The block left, `shard::initialize_zeroed`, went once a zeroed shard was an empty one. |
| `object/` | 1 | **adopted** — the one site is `object::shm::Pages`'s `Send`/`Sync` pair, part of the finding filed above |
| `completion` (since deleted) | 0 undocumented (3 unsafe sites, all already carrying a `SAFETY:`/`Safety:` comment predating this pass) | already documented |
| **adopted** | **389** — the whole kernel (`mm` 35 + `elf` 23 + `loader` 8 + `object` 1 + root files 76 + `drivers` 121 + `arch` 107 + `sched` 8 + `iommu` 8 + `log` 2) | gated at the source, because the kernel is one crate with no `-p` scoping to hang a lint on. Every area sweep opened its entry module (`mm/mod.rs`, `object/mod.rs`, `elf/mod.rs`, `loader/mod.rs`, `drivers/mod.rs`, `arch/mod.rs`, `sched/mod.rs`, `iommu/mod.rs`, `log/mod.rs`) with `#![warn(clippy::undocumented_unsafe_blocks)]`; the root-file sweep could not — there is no entry module above them but the crate root — so it **inverted the form**: `main.rs` carries one crate-level `#![warn(...)]`, and an `#[allow(...)]` on a `mod` line is the form a tree not yet swept would take. Both compose with `host-tests.yml`'s existing `-D warnings` on the two kernel invocations, so no command line changed. The module attributes are redundant under the crate one and are left where they are — each still records its own area's status. |
Expand Down Expand Up @@ -480,14 +480,13 @@ What stayed: `table::flush`'s `clflush`/`mfence`, which has no safe spelling
and no narrower one — the instruction takes an address and no length, so the
bound has to be on the caller's side, and it now is.

**`log/` removed nothing in the sweep itself, and both reasons are named at the
site.** `shard::initialize_zeroed` writes one word in place through a
caller-supplied pointer because the alternative is materialising a 512 KiB
`Shard` on the BSP's 16 KiB stack — irreducible. `log::user`'s record-to-bytes
**`log/` removed nothing in the sweep itself.** `log::user`'s record-to-bytes
slice was reducible and needed a sysroot claim to reduce, so the sweep filed it
and the next pull request that was claiming the sysroot anyway took it:
`LogRecord::as_bytes` exists, `log::user`'s block is gone, and the area is down
to the one irreducible site.
`LogRecord::as_bytes` exists and `log::user`'s block is gone.
`shard::initialize_zeroed`, which wrote a shard's one nonzero word in place
rather than build 512 KiB on the BSP's 16 KiB stack, went once a zeroed shard
was an empty one.

**What was reducible and was declined, with the reason.** The kernel stack
canary's two accesses (`write_stack_canary`, `check_stack_canary`) can go
Expand Down
21 changes: 21 additions & 0 deletions issues/no-test-runs-the-shipped-trace-reader.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
status: open
kind: defect
opened: 2026-10-04
---

# No test runs the shipped `/system/bin/trace`

`userland/trace` ships on the `trace` right `system.toml` grants it, and no
test runs it: not its lines, and none of its refusals, each exit 2 with a
reason on stderr — no system capability, a capability without `trace`, a
record that does not decode. `test_rs_trace_read` reaches the syscall and the
decoder underneath it, not the program. A test of the program needs a boot
whose image carries it, which `tests/virtsmpcase` does not (its jobs are the
suite binaries the harness puts on ROOT).

Owned by step 2 of
`issues/the-diary-computes-no-lateness-and-records-no-slow-system-call.md`,
whose lateness reader reads the same rings. **Exit**: a test runs the program
with its right and reads its lines back, and runs it with no capability and
with one lacking `trace`, and reads exit 2 and the reason for each.
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@ kind: track
opened: 2026-07-30
---

# Nothing inside the machine can read the trace ring, and nothing samples RIP
# The diary computes no lateness, and records no window, shootdown or slow system call

`kernel/src/trace.rs` is a per-CPU ring of `RING_CAPACITY` 24-byte `repr(C)`
records, enabled from `main.rs` at boot and written by the timer, the
scheduler, `irq_ring` and every `kernel::sched::hw::TraceEvent` the core emits.
Its one reader is LLDB: `p &TRACE_RINGS` and then `memory read`, which is why
the discriminants are fixed by hand and held there by const assertions. A
booted machine can ask itself nothing: no syscall, no tool, no gate.
`kernel/src/trace.rs` is the diary: a ring per CPU of 32-byte
`toyos_abi::trace` records, written always through the log's slot protocol
and stamped with the CPU's raw counter, by the timer, the scheduler,
`irq_ring` and every `kernel::sched::hw::TraceEvent` the core emits.
`SYS_TRACE_READ` reads it on the `trace` right, `toyos-trace` decodes it, and
`/system/bin/trace` prints it. Nothing reads a lateness off it, and nothing
samples RIP.

Part of `issues/toyos-explains-itself.md`.

Expand All @@ -24,12 +25,24 @@ anything more is built on it.
needs, and a decoder crate. **Exit**: host tests on the decoder; a guest
test in which a spawned child's wake precedes its pick, a second cursor is
undisturbed, loss is counted after a flood, and a process without the right
is ended; and on the T14, the ticks a record costs, measured by a boot that
writes a million records.
is refused and lives; and on the T14, the ticks a record costs, measured by
a boot that writes a million records. The refusal clause is the owner's
ruling (2026-10-04), **"Refuse, like others"**: "Keep one rule for every
permission: the request is refused and the program lives. The track's exit
is reworded to that. If you want missing permissions to end programs, that
becomes one change for all of them." Built: a capability without `trace`
is refused with `PermissionDenied` and its caller lives, the LLDB reading
path and its pinned numbers are gone, and a record costs 53.15 counter
ticks on the T14 (`trace_record_cost` at `70c1fbf56`: 1000000 records in
53145868 ticks).
2. **Timer and thread lateness**, computed by a reader from the arm, fire and
pick events, with no tracer in the kernel. **Exit**, on the T14: the 50 ms
for which `hold_once` keeps both windows open once a boot
(`kernel/src/windows.rs`) reads back as at least that much lateness.
A deadline's wake is recorded as a `timer-fire` naming no task
(`fire_deadlines` in `kernel/pure/sched/cpu.rs`), so a reader cannot say
which sleeper a fire woke; and a `wake` is the owning CPU's handling of a
post, so the span from a post on another CPU to it is in no record.
3. **Windows over a threshold, shootdown records, and slow system calls.**
A system call over the threshold is recorded with its number and its
program, in the shipped kernel: **"Always on"** (owner, 2026-10-03).
Expand Down
4 changes: 2 additions & 2 deletions issues/toyos-beats-linuxs-latency-on-the-t14.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ rows. The tail delays are only worked on once the diary shows their cause."
The step his text names is step 2 here,
`issues/syscall-preemption-is-incidental.md`, whose exit the
`mask_windows` row reads; the diary is
`issues/nothing-in-the-machine-can-read-the-trace-ring.md`. That
`issues/the-diary-computes-no-lateness-and-records-no-slow-system-call.md`. That
mapping is the orchestrator's, not his.

**Exit**: each step's exit is met, in the file the step names, and on the T14,
Expand All @@ -51,7 +51,7 @@ and of the thread it wakes under Linux's loaded longest on those seven CPUs,
503 µs. Nothing takes that reading of ToyOS today:
`latency_wake` reads one thread's p99 and `mask_windows` each CPU's longest
masked windows. Two files owe it: step 2 of
`issues/nothing-in-the-machine-can-read-the-trace-ring.md`, a
`issues/the-diary-computes-no-lateness-and-records-no-slow-system-call.md`, a
reader that computes timer and thread lateness from the ring, and
`issues/no-program-measures-toyos-against-linux-on-one-machine.md`,
one program that reads lateness past a 1 ms timer under both systems.
4 changes: 2 additions & 2 deletions issues/toyos-explains-itself.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ opened: 2026-10-04
ToyOS answers what it is doing, what it did and what it is made of from
inside itself, with programs it ships. Today the answers are scattered: the
log carries numbers in prose (`irq:`, `tlb:`, `PMM:`, `sched:`, `syscalls:`),
the trace ring has LLDB as its only reader, nothing reads RAPL or C-state
the diary computes no lateness, nothing reads RAPL or C-state
residency, and a process's memory is a byte sum that reads 0 under
contention.
Each pillar's own track carries its steps and its exits; this track holds the
Expand Down Expand Up @@ -55,7 +55,7 @@ rest were not put to him.
| Pillar | Its track |
|---|---|
| Log | `issues/logging-records-from-every-producer-and-a-kernel-that-waits-on-nobody.md`; the sinks half of `issues/redesign-the-log-subsystem.md` |
| Diary | `issues/nothing-in-the-machine-can-read-the-trace-ring.md` |
| Diary | `issues/the-diary-computes-no-lateness-and-records-no-slow-system-call.md` |
| Counters, sizes | this file |
| Profile | behind the diary's judgement; `issues/a-frozen-toyos-waits-for-a-hand-on-the-power-button.md` |
| Accounting | `issues/nothing-charges-kernel-memory-to-a-process.md`, `issues/a-processs-memory-is-a-byte-total-that-reads-zero-under-contention.md`, `issues/blocked-time-is-invisible-while-the-park-lasts.md` |
Expand Down
19 changes: 3 additions & 16 deletions kernel/loom/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -193,24 +193,11 @@ pub mod log_registry;
#[path = "../../src/log/read.rs"]
pub mod log_read;

// The shards `read.rs` walks: in the kernel cpu0's and every AP's published
// one, here whichever the calling test thread installed.
#[cfg(not(feature = "loom"))]
std::thread_local! {
static SHARDS: core::cell::Cell<[Option<&'static shard::Shard>; toyos_abi::log::MAX_LOG_SHARDS]> =
const { core::cell::Cell::new([None; toyos_abi::log::MAX_LOG_SHARDS]) };
}

/// What `read.rs` names as `super::shards()`.
/// What `read.rs`'s log-only walks name as `super::shards()`: none, since no
/// test here drives them; the walks a test drives take their shards.
#[cfg(not(feature = "loom"))]
pub fn shards() -> [Option<&'static shard::Shard>; toyos_abi::log::MAX_LOG_SHARDS] {
SHARDS.with(core::cell::Cell::get)
}

/// Install the shards this thread's walks see. No kernel counterpart.
#[cfg(not(feature = "loom"))]
pub fn install_shards(shards: [Option<&'static shard::Shard>; toyos_abi::log::MAX_LOG_SHARDS]) {
SHARDS.with(|cell| cell.set(shards));
[None; toyos_abi::log::MAX_LOG_SHARDS]
}

#[path = "../../src/sched/reap_gate.rs"]
Expand Down
2 changes: 1 addition & 1 deletion kernel/loom/tests/log_body_words.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ fn round_trip(len: usize) {
assert_eq!(got.message(), want.message(), "the message");
// And the key the merge orders by is the one the copy carries, read through
// the other reader over the same words.
assert_eq!(shard.at_ns(seq), Some(want.at_ns), "at_ns through the key reader");
assert_eq!(shard.stamp(seq), Some(want.at_ns), "at_ns through the key reader");
}

/// Every field, at the three lengths the word arithmetic can disagree about:
Expand Down
17 changes: 6 additions & 11 deletions kernel/loom/tests/log_cursor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,13 @@ use kernel_loom::log_read::{drain_ordered, Cursor, RecordSink};
use kernel_loom::log_shard::{Shard, FIRST_SEQ, SHARD_RECORDS};
use toyos_abi::log::{LogCursor, LogRecord, MAX_LOG_SHARDS};

/// A shard for the life of the test binary, which a walk names as `'static`.
/// A shard for the life of the test binary, which a walk names as `'static`: zeroed, as the kernel allocates an AP's.
fn shard() -> &'static Shard {
// SAFETY: a `Shard` is not zero-sized; the block is never freed.
let ptr = unsafe { alloc_zeroed(Layout::new::<Shard>()) }.cast::<Shard>();
assert!(!ptr.is_null());
// SAFETY: fresh, zeroed, aligned and private to this test until it returns.
unsafe {
Shard::initialize_zeroed(ptr);
&*ptr
}
// SAFETY: fresh, zeroed, aligned and never freed; zeroed is an empty shard.
unsafe { &*ptr }
}

/// Commits `count` records to `shard`, each stamped with its own number.
Expand Down Expand Up @@ -66,11 +63,10 @@ fn a_cursors_loss_claim_is_not_read() {
emit(lapped, SHARD_RECORDS as u64 + OVERWRITTEN);
let mut shards = [None; MAX_LOG_SHARDS];
shards[0] = Some(lapped);
kernel_loom::install_shards(shards);

let mut cursor = LogCursor { lost: u64::MAX, ..LogCursor::new() };
let mut walk = Cursor::from_reader(&cursor).expect("a cursor at every shard's start is taken");
drain_ordered(&mut walk, &mut Full);
let mut walk = Cursor::from_reader(&cursor, &shards).expect("a cursor at every shard's start is taken");
drain_ordered(&shards, &mut walk, &mut Full);
walk.write_into(&mut cursor);
assert_eq!(cursor.lost, OVERWRITTEN);
}
Expand All @@ -84,12 +80,11 @@ fn a_cursor_ahead_of_a_shard_is_refused() {
emit(published, RECORDS);
let mut shards = [None; MAX_LOG_SHARDS];
shards[0] = Some(published);
kernel_loom::install_shards(shards);

let taken = |shard: usize, next: u64| {
let mut cursor = LogCursor::new();
cursor.next[shard] = next;
Cursor::from_reader(&cursor).is_some()
Cursor::from_reader(&cursor, &shards).is_some()
};
let head = FIRST_SEQ + RECORDS;
assert!(taken(0, head), "a cursor caught up with a published shard");
Expand Down
8 changes: 4 additions & 4 deletions kernel/loom/tests/log_record.rs
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ fn a_reader_racing_a_recycle_gets_nothing_rather_than_a_mixture() {
assert_whole(&got, FIRST_SEQ);
}
// Its key is the same question asked of the second reader.
if let Some(at_ns) = shard.at_ns(FIRST_SEQ) {
if let Some(at_ns) = shard.stamp(FIRST_SEQ) {
assert_eq!(
at_ns,
record(FIRST_SEQ).at_ns,
Expand Down Expand Up @@ -351,7 +351,7 @@ fn a_key_and_the_record_it_names_come_from_one_generation() {
});

// Racing the recycle: nothing, or this generation's own key.
if let Some(at_ns) = shard.at_ns(target) {
if let Some(at_ns) = shard.stamp(target) {
assert_eq!(
at_ns,
record(target).at_ns,
Expand All @@ -360,10 +360,10 @@ fn a_key_and_the_record_it_names_come_from_one_generation() {
}

w.join().unwrap();
assert_eq!(shard.at_ns(target), Some(record(target).at_ns));
assert_eq!(shard.stamp(target), Some(record(target).at_ns));
// And the key the merge orders by is the one the copy it then makes
// carries, which is the property the two readers exist to share.
let got = shard.read(target).expect("committed once the writer has joined");
assert_eq!(shard.at_ns(target), Some(got.at_ns));
assert_eq!(shard.stamp(target), Some(got.at_ns));
});
}
2 changes: 1 addition & 1 deletion kernel/loom/tests/log_wake.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ fn a_commit_and_an_arm_cannot_both_miss() {

// `klogd`'s park decision, and nothing else of its body: `true` is "do
// not park".
let dont_park = arm_waiter(&m.waiter, || m.shard.at_ns(FIRST_SEQ).is_some());
let dont_park = arm_waiter(&m.waiter, || m.shard.stamp(FIRST_SEQ).is_some());

p.join().unwrap();

Expand Down
27 changes: 8 additions & 19 deletions kernel/loom/tests/log_zeroed_init.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
//! Host-fast regression for the AP shard's allocation path.
//!
//! Loom atomics are deliberately not byte-zeroable, so this test runs with
//! `--no-default-features` and exercises the real `core` layout and the real
//! in-place constructor used after `alloc_zeroed`.
//! `--no-default-features` and exercises the real `core` layout of a shard
//! that `alloc_zeroed` made and nothing constructed.
//!
//! **The gate below therefore runs under exactly one of this crate's two
//! invocations, and the other one runs nothing from this file.** That is the
//! shape, not an accident — but it was silent until 2026-08-14, when CI ran
//! only the default invocation and this file's `running 0 tests` looked
//! identical to a pass. `cargo run -- --ci host` runs both commands
//! now, and every other `loom::model` file is gated `cfg(feature = "loom")`,
//! so a bare `--no-default-features` run at the crate root exercises none of
//! them either — the second command names this target explicitly instead.
//! shape, not an accident. `cargo run -- --ci host` runs both commands, and
//! every other `loom::model` file is gated `cfg(feature = "loom")`, so a bare
//! `--no-default-features` run at the crate root exercises none of them either
//! — the second command names this target explicitly instead.

#![cfg(not(feature = "loom"))]

Expand All @@ -24,17 +22,8 @@ fn a_zero_allocated_ap_shard_issues_first_seq_first() {
let layout = Layout::new::<Shard>();
let ptr = unsafe { alloc_zeroed(layout) }.cast::<Shard>();
assert!(!ptr.is_null());

// Negative witness: allocation alone leaves the first field — `head` by
// the layout assertion in `shard.rs` — at zero. Publishing that pointer
// directly is the reviewed defect and would issue the empty-state number.
let zero_head = unsafe { ptr.cast::<u64>().read() };
assert_eq!(zero_head, 0);
assert_ne!(zero_head, FIRST_SEQ);

// SAFETY: `ptr` is a fresh, aligned, zeroed allocation and remains private
// to this test until initialization completes.
unsafe { Shard::initialize_zeroed(ptr) };
// SAFETY: a fresh, aligned, zeroed allocation, which is an empty shard,
// private to this test.
let shard = unsafe { &*ptr };

assert_eq!(shard.head(), FIRST_SEQ);
Expand Down
4 changes: 4 additions & 0 deletions kernel/loom/tests/loom_retire.rs
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,10 @@ struct Payload;
impl SchedPayload for Payload {
type Ctx = ();
type ShareLock = LoomLock<ShareState>;

fn name(&self) -> u64 {
0
}
}

/// The full message set (`crate::msg::Msg`), as opposed to the reduced [`Msg`]
Expand Down
Loading
Loading