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
34 changes: 27 additions & 7 deletions build/disk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,14 +91,10 @@ impl Node {
format!("/dev/{}", self.part_name(index))
}

/// Where `/sys` answers for the disk, and for one of its partitions.
/// Where `/sys` answers for the disk.
pub fn sysfs(&self) -> String {
format!("/sys/class/block/{}", self.name())
}

pub fn part_sysfs(&self, index: u32) -> String {
format!("{}/{}", self.sysfs(), self.part_name(index))
}
}

/// A whole disk's image, opened: whole sectors, `EFI PART` in the *final*
Expand Down Expand Up @@ -143,10 +139,34 @@ mod tests {
}
assert_eq!(Node::parse("/dev/sda").unwrap().whole(), "/dev/sda");
assert_eq!(Node::parse("/dev/sdb").unwrap().partition(3), "/dev/sdb3");
assert_eq!(Node::parse("/dev/sda").unwrap().part_sysfs(1), "/sys/class/block/sda/sda1");
let nvme = Node::parse("/dev/nvme0n1").unwrap();
assert_eq!(nvme.whole(), "/dev/nvme0n1");
assert_eq!(nvme.partition(3), "/dev/nvme0n1p3");
assert_eq!(nvme.part_sysfs(1), "/sys/class/block/nvme0n1/nvme0n1p1");
}

/// **A whole disk's image is whole sectors with `EFI PART` in its final
/// one**: a ragged or empty file, and one whose signature is only in the
/// primary header, the false pass a healthy front hides, are refused.
#[test]
fn a_whole_disk_is_whole_sectors_ending_in_the_backup_header() {
let dir = toyos_tmpdir::TempDir::new("whole-disk");
let write = |name: &str, bytes: &[u8]| {
let at = dir.join(name);
std::fs::write(&at, bytes).expect("write");
at
};
let refused = |name: &str, bytes: &[u8]| whole_disk(&write(name, bytes)).map(|(_, n)| n).unwrap_err();
assert_eq!(refused("ragged.img", &[0u8; 1000]), Unfit::Sectors { bytes: 1000 });
assert_eq!(refused("empty.img", &[]), Unfit::Sectors { bytes: 0 });
assert_eq!(
refused("headless.img", &[0u8; 1024]),
Unfit::BackupHeader { at: 512, saw: "\0\0\0\0\0\0\0\0".to_string() }
);
let mut front = vec![0u8; 1536];
front[512..520].copy_from_slice(b"EFI PART");
assert!(matches!(refused("front-only.img", &front), Unfit::BackupHeader { at: 1024, .. }));
let mut whole = front.clone();
whole[1024..1032].copy_from_slice(b"EFI PART");
assert_eq!(whole_disk(&write("whole.img", &whole)).map(|(_, n)| n), Ok(1536));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -106,14 +106,15 @@ resets every connected port and its controller before the kernel's stop
(`toyos_xhci::hc::Hc::stop`). The boot deadline, the hard-lockup detector and
a panic reset with no USB register touched, as #901 §6 accepted, and so does
a stop whose usbd died, its function reset by its claim's release. The
guard for that path is `stick_survives_a_reset_under_load`: `stick_load`
writes the stick through usbd until the boot deadline resets the machine,
judged on `boot.usbload.stick_secs` against the bench's record. It has not
run yet.
guard for that path was `stick_survives_a_reset_under_load`'s
`boot.usbload.stick_secs`, `stick_load` writing the stick through usbd until
the boot deadline reset the machine. Since the benches boot off their NVMe it
writes the NVMe, and no row writes a stick on a bench
(`issues/no-bench-row-drives-usb-storage-since-the-benches-boot-off-nvme.md`).

## Exit condition

A reproduction of the hang itself — not of a reset — that leaves evidence off
the stick, and an account of what the controller and the device were in when it
happened. The regression guard is `stick_survives_a_reset_under_load`'s
`boot.usbload.stick_secs`.
happened. The regression guard is a reset under a write to a stick a bench
row drives, which that issue's exit owes.
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,11 @@ before `:1489`. The next invocation opens with `flash` again. The previous
boot's partition therefore survives to be read exactly when something outside
the loop reads it between the two invocations — and nothing in the loop does.

**And the loop cannot read a wedged stick at all.** `read_log` `?`s on the
`mkdir` and the mount (`:1131-1132`) and `read_mounted` on the listing and
every `cat` (`:1152-1162`); `raw_log` (`:1173`) refuses a short read by name
(`Refusal::Landed`, `:1184-1190`). Both are reached only after
`wait_for_the_stick` has seen the block node for up to `STICK_SECS` (`:68`,
30 s) — which is not tolerance of an unreachable stick but the reason neither
is asked to tolerate one. A pre-flash call to either, as written, would refuse
every run whose predecessor left the stick unenumerable (run 49's mode, below)
instead of recording that it did.
**Since the benches boot off their NVMe nothing stops the loop reading it
first.** The log volume is the install area's partition 7
(`ToyOS-internal-log`), on the disk Ubuntu boots from, so it is there whenever
the loop is: the stick that could come back unenumerable (run 49's mode,
below) is in no boot's path. The line numbers above are of the stick loop.

## What was and was not lost

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,13 @@ much to the stick while a tone plays is the same stimulus.

Since the small-kernel track's step 10 the stick is usbd's: the interrupts are
its claim's and the waits its thread's, and the kernel's two-second transport
break is gone. Whether the starvation remains is unmeasured.
break is gone. Whether the starvation remains is unmeasured. A bench's `/log`
is on its NVMe now, so `tone_beside_a_log_write` writes no stick and is no
evidence for this issue
(`issues/no-bench-row-drives-usb-storage-since-the-benches-boot-off-nvme.md`).

## Exit condition

On the T14, a `METAL` row plays `hda_tone`'s tone while a program writes a
mebibyte to `/log`, and soundserver reports `underruns=0` over the tone's window
across repeated boots. Owner: the metal suite (`tests/toyos.rs`'s `METAL`).
mebibyte to a USB stick through usbd, and soundserver reports `underruns=0`
over the tone's window across repeated boots. Owner: the metal suite (`tests/toyos.rs`'s `METAL`).
5 changes: 3 additions & 2 deletions issues/machines-anyone-can-lend-to-toyos.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@ could boot ToyOS for the suite with no one standing next to it.

**What exists.**
- `--metal-host` targets any bench.
- `toyos-metal --record` captures a machine and its stick into the host store
- `toyos-metal --record` captures a machine into the host store
(`timings::dir()`), never into the tree.
- The loop wipes and writes the stick over `ssh` and sets a one-shot `BootNext`.
- The loop writes the image to the machine's internal install area over
`ssh` and sets a one-shot `BootNext`.
- The machine comes back to its own OS, and the loop reads the log partition
back.

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
---
status: open
kind: tooling
opened: 2026-10-28
---

# No bench row drives USB storage since the benches boot off their NVMe

`toyos-metal` writes each image to the bench's internal install area
(partitions 5–11 after the bench's four, `tests/metal/src/internal.rs`) and
boots it once through `BootNext`, so no stick is in a bench boot's path. The
supervisor's `@boot` is then the NVMe controller, a test image's usbd row
drives no controller on a bench, and every metal judgement whose subject was
the boot stick lost it:

- `usbd_serves_the_boot_stick` is deleted: a bench's boot controller is no
xHCI.
- `stick_survives_a_reset_under_load` is `the_disk_survives_a_reset_under_load`:
the deadline reset under a write to the bench's NVMe. Whether a reset
leaves a USB stick its next host can enumerate, `boot.usbload.stick_secs`
and the guard `a-cut-bulk-only-command-does-not-brick-the-benchs-stick.md`
names, is judged by no row.
- `tone_beside_a_stick_write` and `mask_windows` under `stick_write` are
`tone_beside_a_log_write` and `mask_windows` under `log_write`: they write
`/log`, the NVMe now, so the USB load
`a-megabyte-written-to-the-stick-starves-a-tone-beside-it.md` asks about is
not reproduced on a bench. The windows' records are
`boot.windowscase.log_write.*`, recorded afresh under the NVMe load; the
`stick_write` rows of each bench's record were taken under the USB load.
- `bench_data_designated` no longer reads the T14 stick's short last sector.
- `amdvi_translates_the_boot_controller` and `amdvi_remaps_the_boot_controller`
judge the NVMe controller where they judged the stick's xHCI.

QEMU's machines still boot off a stick (`the_log_lands_on_the_boot_stick`).

**Exit**: a metal row drives a USB stick that is not the boot disk — the
scratch disk (`issues/the-scratch-disk-adopt-scratch-makes-is-not-served.md`)
— and judges usbd serving it, a reset under a write to it, and a tone beside
that write.

**Owner**: the orchestrator, which runs metal.
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ no volume of ours and no designation stamp is never written
(`a_host_disk_beside_the_stick_is_left_alone`, on QEMU's controller under
Intel's id).

No bench has run it. The controller's bring-up, its geometry (`MDTS` of at
Every bench boot now runs on it: the benches boot their images off their
internal NVMe, so the log and boot volumes are served through it. No
readback has judged it yet. The controller's bring-up, its geometry (`MDTS` of at
least 32 blocks, 512- or 4096-byte sectors), its shutdown at the stop, and
the host booting again off a controller ToyOS drove are each unmeasured on
both benches.
Expand Down
10 changes: 6 additions & 4 deletions issues/the-guest-suite-runs-only-what-no-cheaper-tier-reaches.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,10 +135,12 @@ The kernel drives no USB; usbd does (the small-kernel track's step 10). Host:
and `toyos-usbhid`'s and `toyos-scsi`'s decoders. Metal: the usbd rows.
Exit: as stage A, with what is still owed:

- metal: the rewritten `usbload` row, a stick written continuously through
usbd and the machine reset under it by the boot deadline, judged on
`boot.usbload.stick_secs` (#901 section 9).
- metal: `tone_beside_a_stick_write`
- metal: a stick written continuously through usbd and the machine reset
under it by the boot deadline (#901 section 9), which no bench row writes
since the benches boot off their NVMe
(`issues/no-bench-row-drives-usb-storage-since-the-benches-boot-off-nvme.md`).
- metal: a tone beside a stick write, which no bench row makes since the
benches boot off their NVMe (`tone_beside_a_log_write` writes the NVMe)
(`issues/a-megabyte-written-to-the-stick-starves-a-tone-beside-it.md`).
- metal `late_storage_connect`: a disk that connects after bring-up is
served, and `/boot` and `/log` mount off it — on the stub, a device plugged
Expand Down
21 changes: 11 additions & 10 deletions issues/the-kernel-is-small-interrupts-post-and-threads-wait.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,10 @@ kernel's part of a restart, and named module tests hold each program's
where firmware put it, handed over in 4 KiB pages with its MSI-X table's
page, under the owner's Q2 ruling. The orchestrator ruled on #900 that
this route serves usbd's claim (step 10).
4. **Step 10's metal rows still owed** (step 10, below): the rewritten
`usbload` row, `tone_beside_a_stick_write` and `mask_windows` under a stick
write.
4. **Step 10's metal rows still owed** (step 10, below): a reset, a tone and
`mask_windows` under a write to a stick through usbd, which no bench row
makes since the benches boot off their NVMe
(`issues/no-bench-row-drives-usb-storage-since-the-benches-boot-off-nvme.md`).
5. **Stage 6 step 5, then stage 5's windows.**

Beside that order, each waiting on something outside it:
Expand Down Expand Up @@ -203,8 +204,7 @@ Beside that order, each waiting on something outside it:
port and its controller (`Hc::stop`) before the kernel's, as the
kernel's xHCI stop did. **The exit's evidence**: the kernel's
part by `usbd_drives_the_spare` on the default `qemu-xhci` and
`usbd_drives_the_type_c_controller`, and `usbd_serves_the_boot_stick`
for the boot controller; the kernel's input streams gone, with no
`usbd_drives_the_type_c_controller`; the kernel's input streams gone, with no
`Keyboard` or `Mouse` in `sdk/abi`'s `DeviceType` or `FileType`; the
owner of input's by `toyos-input`'s
`a_connections_end_releases_every_device_it_carried_and_a_gone_only_its_own_source`;
Expand All @@ -223,12 +223,13 @@ Beside that order, each waiting on something outside it:
metal rows that asked for that machine (`iommu-no-remap`) are deleted,
and the refusal is QEMU's to hold, by
`usbd_refused_without_amdvi_remapping` and `amdvi_firmware_left`.
**Left**: the metal rows, which have not run: `usbd_serves_the_boot_stick`
and #901 section 9's three that judge with a userland load,
`stick_survives_a_reset_under_load` (a deadline reset under
`stick_load`), `tone_beside_a_stick_write`
**Left**: #901 section 9's three metal judgements under a userland
load on a stick — a deadline reset, a tone
(`issues/a-megabyte-written-to-the-stick-starves-a-tone-beside-it.md`'s
exit) and `mask_windows` under `stick_write`. #971's first metal boots
exit) and `mask_windows` — which no bench row makes since the benches
boot off their NVMe; `the_disk_survives_a_reset_under_load`,
`tone_beside_a_log_write` and `mask_windows` under `log_write` judge the
same under a write to the NVMe. #971's first metal boots
came back with no `logkeeper` file on the stick, which every metal
row reads; QEMU's stick boot (`the_log_lands_on_the_boot_stick`) has
it. And the client test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,7 @@ Each stage lands on its own, in this order.
- The tested pass's `loader.log` is kept as `loader-previous.log` by a
rename (`SetInfo`), not by #539's copy.
- It runs over the cable that
`issues/the-t14-answers-only-through-a-usb-stick.md` owns, and
`issues/the-t14-answers-only-through-what-ubuntu-reads-off-its-disk.md` owns, and
waits on
`issues/the-t14-hung-after-rebooting-with-its-i219-faulted.md`,
`issues/a-fatal-event-stands-down-both-bounds-and-may-leave-nothing-to-end-the-machine.md`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ kind: track
opened: 2026-09-07
---

# The T14 answers only through a USB stick
# The T14 answers only through what Ubuntu reads off its disk

Every boot of the bench is flashed to a stick under Ubuntu and judged by what
is read off it after a reboot into Ubuntu. The laptop is on a cable on the same
Every boot of the bench is flashed to its internal NVMe under Ubuntu and
judged by what is read off it after a reboot into Ubuntu. The laptop is on a cable on the same
LAN as the development Mac and its NIC is the onboard Intel I219 at `00:1f.6`,
`8086:15fc`. The track is to make that cable the answer path. The boot on
which the Mac read the log, ran a command and handed the machine back over it
Expand All @@ -16,8 +16,8 @@ is deleted: the host reaches no T14 that runs ToyOS

What is left is the harness running userland tests over ssh through a russh
client, which `issues/the-t14-reboots-through-ubuntu-for-every-test.md`
stages, and a netboot spike in which the firmware fetches the loader over HTTP
so the stick leaves the boot path.
stages, and a netboot spike in which the firmware fetches the loader over
HTTP.

Constraints a reader would otherwise pay to re-derive:

Expand Down Expand Up @@ -50,6 +50,6 @@ Constraints a reader would otherwise pay to re-derive:
the image named the Mac and nothing on the Mac listened. The serving half and
the Mac's reader are deleted
(`issues/the-host-cannot-reach-the-t14-while-it-runs-toyos.md`); a boot
that dies before netd leases needed the stick either way.
that dies before netd leases needed the disk Ubuntu reads either way.
- The metal loop is `toyos-metal` (`tests/metal/src/lib.rs`), and the T14 is run by the
orchestrator alone.
7 changes: 5 additions & 2 deletions issues/toyos-manages-its-own-partitions.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,8 @@ a signed test image's job holds it.
`Guid::TOYOS_SCRATCH` replaces `toyos-metal --adopt-scratch`
(`issues/the-scratch-disk-adopt-scratch-makes-is-not-served.md`).
4. **The installer** (`issues/storage-is-layers-and-a-role-is-a-filesystem.md`,
stage 3) lays its layout through stage 2's library and authorization, and
`toyos-metal --install-internal`'s `sgdisk` and `partx` go with it.
stage 3) lays its layout through stage 2's library and authorization.
`toyos-metal`'s `sgdisk` and `partx` are not this track's to replace: they
run under a bench's Ubuntu before ToyOS boots, making the install area once
and giving partitions 7–11 the image's GUIDs on every flash, and they leave
with Ubuntu (`issues/the-build-runs-host-tools-outside-rust-and-qemu.md`).
16 changes: 16 additions & 0 deletions loader/src/bootnext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,22 @@ pub fn point_at_us(handle: Handle, system_table: &SystemTable) {
}
}

/// Whether the machine's default boot, `BootOrder`'s first entry, is another
/// entry than `BootCurrent`, the one that started this loader (UEFI 2.10
/// §3.3): a `BootNext` bought this boot, so a reset hands the machine to that
/// default and not back here. `false` where either variable does not read.
pub fn default_is_another(system_table: &SystemTable) -> bool {
let rt = system_table.runtime_services();
let read = |name: &CStr16| rt.get_variable(name, &GLOBAL_VARIABLE).ok().map(|(bytes, _)| bytes);
let (Some(current), Some(order)) = (read(cstr16!("BootCurrent")), read(cstr16!("BootOrder"))) else {
return false;
};
match (current.get(..2), order.get(..2)) {
(Some(current), Some(first)) => current != first,
_ => false,
}
}

/// The GPT partition GUID of the volume firmware loaded this image from.
fn our_partition(handle: Handle, system_table: &SystemTable) -> Option<[u8; 16]> {
let bs = system_table.boot_services();
Expand Down
24 changes: 18 additions & 6 deletions loader/src/efi/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -538,21 +538,33 @@ unsafe impl core::alloc::GlobalAlloc for Pool {
}
}

/// Set by the first panic, so a panic while saying one goes straight to the
/// power-off.
/// Set by the first panic, so a panic while saying one goes straight to
/// `ResetSystem`.
static PANICKED: AtomicBool = AtomicBool::new(false);

/// Say the panic on firmware's console and power the machine off; from the
/// first `ExitBootServices` call the console is not written, and once boot
/// services are gone neither is there, and the CPU spins.
/// Whether a panic resets the machine rather than powering it off
/// ([`reset_on_panic`]).
static PANIC_RESETS: AtomicBool = AtomicBool::new(false);

/// Have a panic from here on reset the machine instead of powering it off:
/// for a machine whose next boot after a reset is not this loader's.
pub fn reset_on_panic() {
PANIC_RESETS.store(true, Ordering::Relaxed);
}

/// Say the panic on firmware's console, then reset the machine or power it
/// off as [`reset_on_panic`] chose; from the first `ExitBootServices` call the
/// console is not written, and once boot services are gone neither is there,
/// and the CPU spins.
#[panic_handler]
fn panic(info: &core::panic::PanicInfo) -> ! {
if let Some(raw) = live() {
if console().is_some() && !PANICKED.swap(true, Ordering::Relaxed) {
print(format_args!("[PANIC]: {info}\n"));
}
let kind = if PANIC_RESETS.load(Ordering::Relaxed) { ResetType::WARM } else { ResetType::SHUTDOWN };
// SAFETY: runtime services live as long as the table.
unsafe { &*raw.runtime_services }.reset(ResetType::SHUTDOWN, Status::ABORTED)
unsafe { &*raw.runtime_services }.reset(kind, Status::ABORTED)
}
loop {
core::hint::spin_loop();
Expand Down
Loading