diff --git a/README.md b/README.md index a388a1eff6f..76b3e91d0f7 100644 --- a/README.md +++ b/README.md @@ -192,9 +192,8 @@ attempt. ![ToyOS booting on a ThinkPad T14](first-boot.jpg) The screenshot is the laptop's own panel. The machine has no serial port, so -the kernel renders its log and its panics to the framebuffer, and pages them -with PageUp and PageDown polled straight off the keyboard controller after -every CPU has halted. It is reporting a real bug: the page cache sized an index +the kernel renders its log and its panics to the framebuffer. It is reporting a +real bug: the page cache sized an index from the disk's block count, which fit in QEMU's test image and wanted 238 MB on a 244 GB drive. diff --git a/issues/build/one-checkout-of-one-tree-builds-two-kernels.md b/issues/build/one-checkout-of-one-tree-builds-two-kernels.md new file mode 100644 index 00000000000..67e4dc2a1d8 --- /dev/null +++ b/issues/build/one-checkout-of-one-tree-builds-two-kernels.md @@ -0,0 +1,57 @@ +--- +status: open +kind: defect +opened: 2026-10-03 +--- + +# One checkout of one tree builds two kernels + +A kernel's bytes depend on what its target directory built before. Measured on +`wt/toyos-ps2`, the tree clean each time, of the x86-64 kernel with +`boot-actuators,test-actuators`, which is built from the same sources at both +heads: + +- At `66798c7ed`, read back out of a staged metal image (`toyos/kernel.elf` on + slot A's volume): 3514896 bytes and sha256 + `2593c71c4c86f3c03815f08afc6a89eb9832c351e99376ffb4aab7858c8f83c2` when the + build before it had one expression of `kernel/src/arch/x86_64/pio.rs` + patched, since restored; and + `d0c3cbd81729d01f20c772c08203c0bd4a4818b5f3d77d789cc4193104c4dfdd` after + eleven such builds in turn, each of a patch to one of `pio.rs`, + `toyos-userbound/src/port.rs`, `isa.rs`, `percpu.rs`, `panic_reboot.rs`, + `idt/exceptions.rs` with `paging.rs`, and `object/ops.rs`, each restored + before the next. Pull request #592 carries the patches and their order. +- At `b00d9a589`, built into two empty target directories by the cargo line + `src/build.rs` runs for it: 3514808 bytes and sha256 + `cb95d0d34ac99148584083e21cbb09edc299e5a94168e8ca71a9d7ef3db8a37b` from + each, where the kernel the worktree's own `kernel/target` had last built was + still `2593c71c…`. + +The empty directories' kernel and `2593c71c…` differ in `.symtab` and +`.strtab` and in no other section: 121 of 6419 +symbol names, each in its `.llvm.` suffix alone, ten of the 261 suffixes +the kernel carries. It is the suffix +`issues/build/two-checkouts-of-one-tree-build-different-guest-bytes.md` +measures across checkout paths; that issue's exit builds two clean checkouts +and would not see this one. + +What moves a suffix is not measured. One of the eleven patches, `pio.rs`'s, +built in a once-empty directory and reversed, left that directory building the +empty one's kernel. `[profile.toyos]` builds with `incremental` on (root +`Cargo.toml`): the suspect, and no build with it off was compared. + +What it costs: an image's signed header names the kernel file, symbol table +and all, so a reading taken on a machine names the bytes of one build, and the +next build of the same sources may carry others: a metal reading reaches a +later head by its bytes only where the builds that preceded the booted image +are replayed. `issues/build/toyos-builds-itself.md`'s M4 compares a guest +build against "the host build of the same commit", and of this kernel a host +has built three. + +Exit: a target directory builds from a tree the kernel an empty target +directory builds from it, whatever the first built before. A gate reads it: it +builds a kernel into an empty directory and compares it with the one the +checkout's own target directory builds. What that checkout has built is its +"before", so no patch is its input, and on this worktree as it stands it is +red, `2593c71c…` against `cb95d0d3…`. A gate that builds an edit, reverses it +and compares is green on this defect, by the `pio.rs` measurement. diff --git a/issues/build/the-guest-suite-runs-only-what-no-cheaper-tier-reaches.md b/issues/build/the-guest-suite-runs-only-what-no-cheaper-tier-reaches.md index 2eed1c81742..45e79cd0810 100644 --- a/issues/build/the-guest-suite-runs-only-what-no-cheaper-tier-reaches.md +++ b/issues/build/the-guest-suite-runs-only-what-no-cheaper-tier-reaches.md @@ -69,8 +69,6 @@ console's renderer. Exit: as stage A. - metal `watchdog_resets`: the TCO ends the machine. - metal `quiesce_refuses_a_second_shutdown`: the machine has one shutdown, and a second caller is refused while the first holds it. -- metal `screen_pager_keys`: PageUp pages a halted report. -- host `screen_paged_scrollback`: a report longer than the panel pages with no input. - host `screen_early_panel`: the panel repaints after each committed record. - host `screen_log_absent`: a `/log` that did not mount is said on the panel. - host `screen_blocked_dump`: Ctrl+Alt+D's summary tells the three states apart. diff --git a/issues/build/timing-verdicts-ruled-off-qemu-have-no-metal-arm.md b/issues/build/timing-verdicts-ruled-off-qemu-have-no-metal-arm.md index 8a68a77dbab..9e76cb85a81 100644 --- a/issues/build/timing-verdicts-ruled-off-qemu-have-no-metal-arm.md +++ b/issues/build/timing-verdicts-ruled-off-qemu-have-no-metal-arm.md @@ -38,8 +38,7 @@ their clocks, and no `METAL` row judges them. - **Input.** A boot with no i8042 is no slower than one with it (`i8042_absent`). The i8042 counters repeat at most once per 10 s and only when the pin asserted (`i8042_health_cadence`), and the idle loop does not - spin on the controller (`i8042_health`'s idle trips). The fatal path's panel - holds while a key is held (`panic_key_holds`). + spin on the controller (`i8042_health`'s idle trips). - **USB.** The connect settle ends on the device appearing and not at `EMPTY_BUS_NS` (`xhci_slow_connect`). A disk call ends inside `toyos_xhci::call::AFTER_BREAK`, a staged break skips its data-phase wait, the diff --git a/issues/hardware/a-frozen-toyos-waits-for-a-hand-on-the-power-button.md b/issues/hardware/a-frozen-toyos-waits-for-a-hand-on-the-power-button.md index aeda14821ef..9aa423dd0d3 100644 --- a/issues/hardware/a-frozen-toyos-waits-for-a-hand-on-the-power-button.md +++ b/issues/hardware/a-frozen-toyos-waits-for-a-hand-on-the-power-button.md @@ -17,7 +17,7 @@ kernel arms the chipset's TCO only on the `watchdog` parameter - A userland `watchdogd` feeds the watchdog on every machine, with no new syscall; the detector is armed on every boot; nothing ships for tests alone. - **A panic's panel holds as it does today** and feeds the watchdog while it - holds, after a key too (`panic_console::hold_the_panel`). + holds (`panic_console::hold_the_panel`). - **`watchdogd` reaches the chipset's timer through the general port claim #592 brings, its `isa` claim, as the ACPI server will**, and not through a device class of its own: one way of doing it and not two (2026-10-03). That diff --git a/issues/hardware/a-metal-session-runs-a-pre-flash-gate-first.md b/issues/hardware/a-metal-session-runs-a-pre-flash-gate-first.md index 9fb38ea8273..c8aa3d198aa 100644 --- a/issues/hardware/a-metal-session-runs-a-pre-flash-gate-first.md +++ b/issues/hardware/a-metal-session-runs-a-pre-flash-gate-first.md @@ -39,9 +39,7 @@ anything touching the boot path. these to be read-verified only: TCG reports every CPU feature present, so the missing-bit path cannot run. 4. **The on-screen console. If this fails, do not flash.** Every screen test. - Confirm the muted profile actually removes the UART, and that the paging test - is driven by a timer rather than a keypress — input may be dead on the - machine. *False pass:* the late-panic gate passes with the capture routine's + Confirm the muted profile actually removes the UART. *False pass:* the late-panic gate passes with the capture routine's body replaced by a bare return, so these cover rendering, not capture. 5. **Input, which is the milestone and which a verdict once omitted entirely.** The 2026-08-01 verdict recorded GO over six sections and a seventeen-row diff --git a/issues/hardware/gop-is-not-the-path-every-boot-takes.md b/issues/hardware/gop-is-not-the-path-every-boot-takes.md index 69eb85df184..97c1deb7d8c 100644 --- a/issues/hardware/gop-is-not-the-path-every-boot-takes.md +++ b/issues/hardware/gop-is-not-the-path-every-boot-takes.md @@ -8,8 +8,8 @@ opened: 2026-09-03 Until `06ce633` no configuration in this tree produced a UEFI GOP at all: `kernel/src/drivers/gop.rs` had never executed and `kernel_args.gop_framebuffer` -was zero everywhere. `cargo run --gop` and `BootOptions { profile: Profile::Gop }` -(`-vga std`) fixed that and the path works. +was zero everywhere. `cargo run --gop` (`-vga std`) fixed that and the path +works. **The owner ruled on 2026-09-01: GOP is the floor, and the mode is the firmware's.** The mode half has landed. This file is the other half, and it is diff --git a/issues/isolation/the-i8042s-holder-holds-the-machines-reset-line.md b/issues/isolation/the-i8042s-holder-holds-the-machines-reset-line.md new file mode 100644 index 00000000000..e5743697288 --- /dev/null +++ b/issues/isolation/the-i8042s-holder-holds-the-machines-reset-line.md @@ -0,0 +1,30 @@ +--- +status: open +kind: defect +opened: 2026-09-28 +--- + +# The i8042's holder holds the machine's reset line + +An `isa` claim on the i8042 (`kernel/src/arch/x86_64/pio.rs`'s `GRANTABLE`) +opens ports 0x60 and 0x64 to the process that binds it, through the TSS I/O +permission bitmap, and the bitmap grants a port or refuses it: it cannot see +the value written. The controller's command port takes `0xFE`, which pulses the +CPU's reset line, and `0xD1`, which writes its output port, where the same +line lives (IBM PC AT Technical Reference, the 8042's commands). So whichever +process drives the keyboard can reset the machine whenever it likes: no memory +is exposed and the kernel does not crash, but a userland bug in that one +program ends every other one without a word in the log. + +Neither way out is free: filtering the command byte means the kernel decoding +the controller's protocol, a syscall or a trapped instruction per access in +place of the bitmap; keeping 0x64 in the kernel means the keyboard driver is +not wholly userland, which the owner ruled it must be. + +A recorded weakness of the power broker's track +(`issues/isolation/the-power-broker-authority-with-a-human-in-the-loop.md`), +which owns it (owner ruling). + +**Exit**: resetting the machine is the power broker's decision alone: the +keyboard's holder reaches the controller's reset line only through the broker, +or not at all. diff --git a/issues/isolation/the-power-broker-authority-with-a-human-in-the-loop.md b/issues/isolation/the-power-broker-authority-with-a-human-in-the-loop.md index c8d0702143c..e7947487d0a 100644 --- a/issues/isolation/the-power-broker-authority-with-a-human-in-the-loop.md +++ b/issues/isolation/the-power-broker-authority-with-a-human-in-the-loop.md @@ -31,3 +31,6 @@ so the native shape is smaller: Unstaffed until the owner opens it; sequenced naturally with the userland/ product era. What must not happen meanwhile is the accident this track exists to prevent: `POWER` spreading to more manifest rows. + +Its recorded weakness: the i8042's holder resets the machine without it +(`issues/isolation/the-i8042s-holder-holds-the-machines-reset-line.md`). diff --git a/issues/kernel/deferred-release-outlives-its-syscall.md b/issues/kernel/deferred-release-outlives-its-syscall.md index fd487e787f9..68c6640db85 100644 --- a/issues/kernel/deferred-release-outlives-its-syscall.md +++ b/issues/kernel/deferred-release-outlives-its-syscall.md @@ -293,6 +293,16 @@ swap's author. Exit condition: this issue closes — the kernel publishes a process's end only once its deferred releases have run — and `CLAIM_RETURN` is deleted with it. +**A second loop waits the same release out.** +`tests/toyos-rust-tests/src/isa_row.rs`'s `claim_after` asks for the `isa` row +again while the claim is refused as `AlreadyExists`, for at most five seconds, +wherever `isa_grant` or `isa_lines` claims the row after a holder has exited. +No run was red without it: it rests on the `Device` release measured in this +section. Owner: the `isa` claim's author. Exit condition: the same, and +`claim_after` is deleted with `CLAIM_RETURN` — once the release is no longer +deferred, a claim refused after its holder's exit is a defect that loop would +hide for five seconds. + **Its tests are deleted**: `38a5064b6` took `handle_basic`, `handle_transfer` and `kill_while_blocked` out, and `009db6db3` retired `SYS_DEBUG` actions 17 and 18, which only `handle_transfer` read. `02c35a85d` then moved `FILL`, which diff --git a/issues/kernel/every-driver-is-still-in-the-kernel.md b/issues/kernel/every-driver-is-still-in-the-kernel.md index ac4f86ca87a..9552fffcca2 100644 --- a/issues/kernel/every-driver-is-still-in-the-kernel.md +++ b/issues/kernel/every-driver-is-still-in-the-kernel.md @@ -43,6 +43,8 @@ What is left of the staged work: 3. Done: **the capability itself** is `DeviceType::PciFunction` plus `SYS_DEVICE_BAR_MAP` and `SYS_DEVICE_DMA_ALLOC`, with config space readable and unwritable and the interrupt delivered as a record on the claim. +4. **The i8042 (PS/2)**: staged as stage 7 of + `issues/kernel/the-kernel-is-small-interrupts-post-and-threads-wait.md`. - **USB HID cannot move to userspace without moving the boot block device or splitting the controller.** It shares the controller, the event ring and the diff --git a/issues/kernel/the-isa-port-switchs-cost-is-unmeasured.md b/issues/kernel/the-isa-port-switchs-cost-is-unmeasured.md new file mode 100644 index 00000000000..bc1b340d6ad --- /dev/null +++ b/issues/kernel/the-isa-port-switchs-cost-is-unmeasured.md @@ -0,0 +1,18 @@ +--- +status: open +kind: tooling +opened: 2026-09-29 +--- + +# The `isa` port switch's cost on the scheduler hot path is unmeasured + +`arch::pio::switch_to` (`kernel/src/arch/x86_64/pio.rs`) runs on every context +switch, from `KernelHw::switch`: per `GRANTABLE` row one `BOUND` load, one +bitmap byte read, and on a change one bitmap write per port. On x86-64 that is +one row on every switch of every CPU, whether or not any process holds a claim. +Nothing has measured what it adds to a switch, and a timing verdict comes only +from metal. + +**Exit**: the T14's switch cost with and without the call, from one metal run, +recorded in the commit that closes this; or the switch skips the rows entirely +while no row is bound, with the skip's cost measured the same way. diff --git a/issues/kernel/the-kernel-is-small-interrupts-post-and-threads-wait.md b/issues/kernel/the-kernel-is-small-interrupts-post-and-threads-wait.md index 303fbbf64df..9ebf9e99773 100644 --- a/issues/kernel/the-kernel-is-small-interrupts-post-and-threads-wait.md +++ b/issues/kernel/the-kernel-is-small-interrupts-post-and-threads-wait.md @@ -126,8 +126,7 @@ times: 10. **usbd**, stage 5's second half: the whole xHCI moves, HID to the keyboard claim and mass storage over `toyos-blockring`, and the kernel USB bridge is deleted. **What must work with no userland stays off - USB**: the panic console pages its report by itself and needs no - keyboard, and steers only from the i8042 it polls; the kernel's one + USB**: the kernel's one hotkey, Ctrl+Alt+D (`kernel/src/keyboard.rs`, the blocked-task dump), is recognised on the i8042's transitions and no longer on a USB keyboard's, which from here reach the kernel only as usbd's keyboard @@ -150,7 +149,7 @@ times: Each step measures the kernel's lines against stage 6's first commit, and the longest interrupts-off and preemption-off windows against the readings step 2's exit puts in the tree, by that exit's rule. - Steps 3 and 4 do not land alone: they land with #592's i8042 stage and + Steps 3 and 4 do not land alone: they land with stage 7.2 and with usbd. 1. **Interrupts post.** A post is legal in a handler: the watches a handler posts, and the completions of a ring they complete into, sit behind @@ -159,7 +158,7 @@ times: IOMMU's refusal and both audio backends post from the handler, and `irq_ring`'s `UserDev` and `Audio` and their arms in `drain_irqs` go. The thread is the holder's: netd's, blockd's, soundd's mix thread, and - the `isa` claim's when #592 lands. **Exit**: `handler_post_without_a_pass`, + an `isa` claim's holder. **Exit**: `handler_post_without_a_pass`, a vector taken on a CPU holding preemption off, inside a post of its own watch, inside a completion into a ring polling it, or inside that ring's own watch, posting once that section lets go and before any pass, red on @@ -184,7 +183,7 @@ times: second bullet). `ring_park_herd` is not that load: its walks start in `SYS_INBOX_SUBMIT`, which runs with interrupts masked on both arms, and it reaches no handler step 1 changed. - 3. **The i8042's thread is ps2server's** (#592's i8042 stage): `irq_ring`'s + 3. **The i8042's thread is ps2server's** (stage 7.2): `irq_ring`'s `I8042`, `keyboard_controller::service` and the idle loop's `verdict_due` go with the kernel's driver. **Exit**: that stage's. 4. **xHCI's thread is usbd's** (step 10 above): `Xhci`, `poll_if_pending` @@ -201,6 +200,18 @@ times: and `issues/kernel/nothing-fails-when-a-devices-release-or-close-stops-answering-its-polls.md` are met. +7. **The i8042 leaves the kernel.** Owner ruling, 2026-09-28: drivers are + userland, and a dead kernel takes no input, with no emergency way. + 1. **The panic console takes no input, and an `isa` claim grants a process + exact ports through the TSS I/O permission bitmap and its ISA lines as + records** (`kernel/src/isa.rs`). **Done** (#592). + 2. **ps2server**, the server over that claim, feeding the kernel's keyboard + and mouse streams so Ctrl+Alt+D and the merge with USB HID stay where + they are; the kernel's driver, its vector, its actuators and the + `keyboard_controller` seam deleted. A keyboard claim is refused while no + source exists, which init's order of endowment then decides. **Exit**: + no i8042 code in the kernel, and typing resumes after ps2server is killed + and restarted. ## Standing diff --git a/issues/panic-path/a-fatal-event-stands-down-both-bounds-and-may-leave-nothing-to-end-the-machine.md b/issues/panic-path/a-fatal-event-stands-down-both-bounds-and-may-leave-nothing-to-end-the-machine.md index dc6c2cff938..1d1c7c8c1ae 100644 --- a/issues/panic-path/a-fatal-event-stands-down-both-bounds-and-may-leave-nothing-to-end-the-machine.md +++ b/issues/panic-path/a-fatal-event-stands-down-both-bounds-and-may-leave-nothing-to-end-the-machine.md @@ -23,9 +23,7 @@ with no fallback by design (`kernel/src/drivers/acpi.rs:277-283`). Where the table named none, `arm` returns `Bound::Held` (`kernel/src/panic_reboot.rs:134-145`) and `hold_the_panel` loops `while bound.is_armed()` (`kernel/src/drivers/panic_console/mod.rs:647-650`), -which never ends. A keypress also retires the bound, which is right for a -machine with somebody in front of it and is exactly wrong for one running -unattended. +which never ends. **On the T14 that last bound is the one thing already known not to work.** `issues/hardware/an-armed-tco-has-never-reset-the-t14.md` records that no claim @@ -61,5 +59,5 @@ prints it. **Exit condition**: a boot that takes a fatal event on the T14 with nobody in front of it either ends itself and leaves a record naming the fault, or the run -shows which of `Bound::Held` and the retired-by-a-keypress path held it — read -off the machine, not argued from the tree. +shows that `Bound::Held` held it — read off the machine, not argued from the +tree. diff --git a/issues/panic-path/a-fatal-path-entered-outside-a-panic-holds-the-panel-with-interrupts-open.md b/issues/panic-path/a-fatal-path-entered-outside-a-panic-holds-the-panel-with-interrupts-open.md new file mode 100644 index 00000000000..7076b3620b0 --- /dev/null +++ b/issues/panic-path/a-fatal-path-entered-outside-a-panic-holds-the-panel-with-interrupts-open.md @@ -0,0 +1,32 @@ +--- +status: open +kind: defect +opened: 2026-10-03 +--- + +# A fatal path entered outside a panic holds the panel with interrupts open + +`panic::halt_all_cpus` says the machine holds its report "with interrupts +masked" and masks nothing itself: the panic handler and the exception entries +mask before they reach it. Two callers do not, both in the test kernel alone: +the `panel-painter-stalls` actuator, from a scheduler pass +(`kernel/src/drivers/panic_console/mod.rs`'s `stall`), and `SYS_DEBUG`'s +`FATAL_HALT` (`kernel/src/syscall/dispatch.rs`). The CPU that then holds the +panel spins on the reset bound with `IF` set, and every device handler pinned +to it still runs on a kernel that has declared itself dead. + +**Evidence**: `screen_fatal_behind_a_painter` on `Profile::Metal` with two +CPUs, QEMU's `info registers -a` read once the report is on the panel: in six +boots of six the spinning CPU's `RFL` has bit 9 set (`0x293`, `0x287`, `0x297`) +and the halted one's has it clear. With `hold_the_panel` patched to poll port +0x60 and a key pressed inside the bound, six boots: the patched poll read the +key in four, each with the panel's CPU cpu1, and never saw it in two. The one +of those two whose console was kept had the panel on cpu0, where the i8042's +handler is pinned. + +So that test stages a fatal path no shipping kernel takes. Not measured: that +the handler read the byte in the two boots the poll missed it; the open `IF` +on the handler's own CPU is what would let it. + +**Exit**: `halt_all_cpus` masks interrupts itself, or its two callers outside +a fault do, and the same reading shows `IF` clear on the CPU holding the panel. diff --git a/issues/panic-path/a-storage-phase-panic-reads-its-key-off-an-unconfigured-i8042.md b/issues/panic-path/a-storage-phase-panic-reads-its-key-off-an-unconfigured-i8042.md deleted file mode 100644 index 3c9c27ed479..00000000000 --- a/issues/panic-path/a-storage-phase-panic-reads-its-key-off-an-unconfigured-i8042.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-09-27 ---- - -# A panic in the storage phase reads its key off an i8042 the kernel has not configured - -The panic panel's reset bound is retired by a key press -(`kernel/src/drivers/panic_console/mod.rs`, `read_key`), which polls port -`0x60` through `keyboard_controller::poll_byte`. `i8042::init` -(`kernel/src/arch/x86_64/i8042/mod.rs`) is what turns translation on, stops -and restarts scanning and enables the port-1 clock. It now runs first in the -device phase (`arch::boot::platform_devices` in `kernel/src/main.rs`), after -storage, so its lines stay on a panel that shows the log's tail. - -So a panic anywhere in the storage phase — NVMe or xHCI init, -`rootfs::hold_source`, the DATA and FAT mounts — meets the controller as the -firmware left it. Whether a key press then reaches `read_key` as a set-1 make -code depends on the firmware: with translation off or scanning off it does -not, and the panel resets at its bound however many keys are pressed. That -ordering is the one the tree had before storage moved behind init's spawn, so -it has shipped before; it is still a weakness. No boot exercises a panic in -the storage phase with the firmware's controller state left unconfigured. - -## Exit condition - -A key press retires the panel's bound for a panic anywhere after the i8042 -probe could have run — the controller configured before the first phase that -can panic on a device, without moving its lines off the panel's tail — and a -boot that panics in the storage phase on the metal-sim shape shows the press -retiring it. diff --git a/issues/panic-path/no-console-between-boot-and-terminal.md b/issues/panic-path/no-console-between-boot-and-terminal.md index 1d5e63a97d9..1cc33c45b14 100644 --- a/issues/panic-path/no-console-between-boot-and-terminal.md +++ b/issues/panic-path/no-console-between-boot-and-terminal.md @@ -36,8 +36,7 @@ it by saying where the log will be, and cannot put a line in it. (`kernel/src/drivers/panic_console/mod.rs:639`), and a compositor claiming the framebuffer sets it. So on `bootable.img` the last kernel screenful ever painted is the one at `Boot: complete`, the desktop overwrites it a few tens of -milliseconds later, and no key pauses it: `page_forever` is reached only from -`halt_all_cpus`, so a *successful* boot never pages. +milliseconds later, and no key pauses it. **The durable answer landed and is not this.** "A log sink that survives userland" is `/system/bin/logd`: the kernel keeps the record ring and the console and diff --git a/kernel/src/actuator.rs b/kernel/src/actuator.rs index 389919103ef..31a902a95b8 100644 --- a/kernel/src/actuator.rs +++ b/kernel/src/actuator.rs @@ -52,6 +52,11 @@ actuators! { /// AMD `SYSRET` SS-attributes workaround's only guest-observable proof. sysret_ss_probe = "sysret-ss-probe"; + /// Leave the i8042 unprobed, so this kernel drives no controller and an + /// `isa` claim on it is granted. Judged by `isa_ports_are_the_binders_alone` + /// and `isa_lines_reach_their_holder`. + i8042_withheld = "i8042-withheld"; + /// Script the input core directly at end of boot. test_input_merge = "test-input-merge"; @@ -154,6 +159,10 @@ actuators! { /// Run the revoked-backing controls after mount. revoked_backing_selftest = "revoked-backing-selftest"; + /// Shorten the panicked kernel's own reboot bound from a minute to seconds, + /// so a guest reaches the reset. Judged by `screen_fatal_behind_a_painter`. + panic_reboot_fast = "panic-reboot-fast"; + /// Have Ctrl+Alt+D's report painter go fatal holding the panel's latch: a /// fatal path meeting a painter that will never let go. panel_painter_stalls = "panel-painter-stalls"; diff --git a/kernel/src/arch/aarch64/keyboard_controller.rs b/kernel/src/arch/aarch64/keyboard_controller.rs index e04e19a2e3c..cebd8f04265 100644 --- a/kernel/src/arch/aarch64/keyboard_controller.rs +++ b/kernel/src/arch/aarch64/keyboard_controller.rs @@ -1,13 +1,5 @@ //! The platform's own keyboard controller. An Arm machine has none: its -//! keyboards are USB, and the panic panel's key poll reads nothing here. - -/// Whether the panic path reads a key here. -pub const PANIC_KEYS: bool = false; - -/// No byte ever waits. -pub fn poll_byte() -> Option<(u8, bool)> { - None -} +//! keyboards are USB. /// Nothing to decide about a controller that is not there. pub fn verdict_due() -> bool { diff --git a/kernel/src/arch/aarch64/mod.rs b/kernel/src/arch/aarch64/mod.rs index 824f53c8550..b07ea6171a1 100644 --- a/kernel/src/arch/aarch64/mod.rs +++ b/kernel/src/arch/aarch64/mod.rs @@ -26,6 +26,7 @@ pub mod irqchip; pub mod keyboard_controller; pub mod paging; pub mod percpu; +pub mod pio; pub mod pmu; pub mod power; pub mod psci; diff --git a/kernel/src/arch/aarch64/pio.rs b/kernel/src/arch/aarch64/pio.rs new file mode 100644 index 00000000000..3c14e7456b2 --- /dev/null +++ b/kernel/src/arch/aarch64/pio.rs @@ -0,0 +1,25 @@ +//! The I/O port space: AArch64 has none, and so no ISA function to grant. + +use core::convert::Infallible; + +use crate::process::Pid; + +/// Nothing: every `isa` claim is refused as naming no function. +pub const GRANTABLE: &[crate::isa::Grantable] = &[]; + +/// No line is routed where no row exists. +pub type Line = Infallible; + +/// Never called: [`GRANTABLE`] has no row to route. +pub fn route(_row: usize, _irq: u8) -> Result { + unreachable!("AArch64 has no ISA bus") +} + +pub fn set_masked(line: Line, _masked: bool) { + match line {} +} + +/// Never called: nothing is bound where nothing can be claimed. +pub fn switch_to(_pid: Option) { + unreachable!("AArch64 has no I/O permission bitmap") +} diff --git a/kernel/src/arch/x86_64/hw.rs b/kernel/src/arch/x86_64/hw.rs index 50ed0a04970..e1479a01660 100644 --- a/kernel/src/arch/x86_64/hw.rs +++ b/kernel/src/arch/x86_64/hw.rs @@ -303,6 +303,7 @@ impl Hw for KernelHw { crate::preempt::set_count(incoming.preempt); percpu::set_current_tid(incoming.id.map(|id| id.1)); percpu::set_current_pid(incoming.id.map(|id| id.0)); + super::pio::switch_to(incoming.id.map(|id| id.0)); match incoming.id { Some(_) => { percpu::set_kernel_stack(incoming.kernel_stack_top); diff --git a/kernel/src/arch/x86_64/i8042/mod.rs b/kernel/src/arch/x86_64/i8042/mod.rs index 5ff7fc7ba9f..70adb4ba85b 100644 --- a/kernel/src/arch/x86_64/i8042/mod.rs +++ b/kernel/src/arch/x86_64/i8042/mod.rs @@ -135,6 +135,13 @@ const HEALTH_MUTE_BLIND: u8 = 7; static HEALTH: AtomicU8 = AtomicU8::new(HEALTH_OFF); static ARMED_NS: AtomicU64 = AtomicU64::new(0); +/// Whether this driver armed the controller this boot, which refuses an `isa` +/// claim on it from then on: [`HEALTH`] never returns to [`HEALTH_OFF`], so a +/// quarantined controller is still this driver's. +pub fn drives() -> bool { + HEALTH.load(Ordering::Relaxed) != HEALTH_OFF +} + // Repeats, but only when the pin has asserted since the last line — so past // the first repeat, silence means no interrupt, not a driver that stopped. fn health_period_ns() -> u64 { @@ -711,8 +718,8 @@ fn quarantine() { } // Each read below is done as its section's sole reader: init before the -// vector is armed, the aux re-enable on `IRQ_CPU` under `IrqGuard::close`, -// and the panic pager with every CPU halted — so no ISR ever races them. +// vector is armed, and the aux re-enable on `IRQ_CPU` under `IrqGuard::close` +// — so no ISR ever races them. fn deadline(millis: u64) -> u64 { crate::clock::nanos_since_boot() + millis * 1_000_000 @@ -960,6 +967,10 @@ fn firmware_claim(rsdp_addr: u64) -> Result<(u8, u16), crate::drivers::acpi::Tab } pub fn init(rsdp_addr: u64) { + if crate::actuator::i8042_withheld() { + log!("i8042: withheld, left unprobed for a claim"); + return; + } // Logged, never obeyed: bit 1 is one summary bit, while the handshake // below is three direct observations of the machine in front of us. match firmware_claim(rsdp_addr) { @@ -1245,19 +1256,6 @@ pub fn init(rsdp_addr: u64) { } -/// Whether the panic path reads a key here: [`poll_byte`] is its poll. -pub const PANIC_KEYS: bool = true; - -/// One byte from the controller if it has one; never waits. Only legal once -/// every CPU is halted — port 0x60's sole reader is otherwise the ISR. -pub fn poll_byte() -> Option<(u8, bool)> { - let status = inb(STATUS); - if status & OBF == 0 { - return None; - } - Some((inb(DATA), status & AUXB != 0)) -} - /// The handler's drain loop, without the EOI. Runs with interrupts off on /// `IRQ_CPU`, keeping `push_isr`'s producer single. Publishes the same /// record the ISR does — a silent push here would manufacture a lost edge. diff --git a/kernel/src/arch/x86_64/idt/exceptions.rs b/kernel/src/arch/x86_64/idt/exceptions.rs index e394f2f401f..f5cf3986d0d 100644 --- a/kernel/src/arch/x86_64/idt/exceptions.rs +++ b/kernel/src/arch/x86_64/idt/exceptions.rs @@ -10,12 +10,19 @@ use super::{Vector, TrapFrame, PF_PRESENT, PF_WRITE, PF_INSTRUCTION_FETCH}; /// Walk RBP chain for user backtrace through page tables. Takes no pid: this /// always backtraces the process running on this CPU. -fn user_backtrace(start_rbp: u64, pml4: *const u64, max_frames: usize) { +fn user_backtrace(start_rbp: u64, max_frames: usize) { let mut rbp = start_rbp; for _ in 0..max_frames { - if rbp == 0 || !rbp.is_multiple_of(8) { break; } - let Some(saved_rbp) = safe_read_u64(rbp, pml4) else { break }; - let Some(return_addr) = safe_read_u64(rbp + 8, pml4) else { break }; + if rbp == 0 { break; } + let saved_rbp = match read_user_u64(rbp) { + Ok(word) => word, + Err(Unread::Refused) => { + log!(" rbp {:#x} refused: no user address", rbp); + break; + } + Err(Unread::Absent) => break, + }; + let Ok(return_addr) = read_user_u64(rbp + 8) else { break }; if return_addr == 0 { break; } process::resolve_user_symbol_return(return_addr).log_bare(return_addr); rbp = saved_rbp; @@ -46,48 +53,42 @@ fn safe_read_kernel(addr: u64) -> Option { Some(unsafe { core::ptr::read_volatile(addr as *const u64) }) } -/// Reads a u64; for a user address, walks page tables by hand to avoid -/// demand-paging faults inside an exception handler. -fn safe_read_u64(addr: u64, user_pml4: *const u64) -> Option { - if !addr.is_multiple_of(8) || addr == 0 { - return None; +/// Why the crash report read no word at an address. +#[derive(Clone, Copy)] +enum Unread { + /// Not a user address: a crash report is never how a process reads the kernel. + Refused, + /// Misaligned, or nothing mapped there. + Absent, +} + +/// A word of the faulting process's memory, read through the tables this CPU +/// runs under: the crash path may neither take the address space's lock nor +/// demand-page. +fn read_user_u64(addr: u64) -> Result { + if !toyos_userbound::is_user_addr(addr) { + return Err(Unread::Refused); } - if !user_pml4.is_null() { - let pml4_idx = ((addr >> 39) & 0x1FF) as usize; - let pdpt_idx = ((addr >> 30) & 0x1FF) as usize; - let pd_idx = ((addr >> 21) & 0x1FF) as usize; - // SAFETY: each read is guarded by the present bit of the entry before - // it; `user_pml4` is a direct-map pointer to the live PML4 from `CR3`; - // every index is masked to nine bits, so `add` stays inside the - // 512-entry table, and each next-level pointer and the final - // `page_phys + offset` stay inside the direct map by the same walk. - // - // Hand-rolled instead of `mm::paging`: a faulted CPU may not take the - // address space lock and may not demand-page. - // - // Not `read_volatile` either: see `kernel_backtrace`. - let pml4e = unsafe { *user_pml4.add(pml4_idx) }; - if pml4e & 1 == 0 { return None; } - let pdpt = crate::DirectMap::from_phys(pml4e & 0x000F_FFFF_FFFF_F000).as_ptr::(); - // SAFETY: the walk's argument, one level down. - let pdpte = unsafe { *pdpt.add(pdpt_idx) }; - if pdpte & 1 == 0 { return None; } - let pd = crate::DirectMap::from_phys(pdpte & 0x000F_FFFF_FFFF_F000).as_ptr::(); - // SAFETY: the walk's argument, one level down again. - let pde = unsafe { *pd.add(pd_idx) }; - if pde & 1 == 0 { return None; } - let page_phys = pde & 0x000F_FFFF_FFE0_0000; - let offset = addr & (mm::PAGE_2M - 1); - // SAFETY: the walk's argument — a direct-map read of a byte inside the - // present 2 MiB leaf the three entries above resolved. - Some(unsafe { *crate::DirectMap::from_phys(page_phys + offset).as_ptr::() }) - } else if mm::is_kernel_addr(addr) { - // SAFETY: `addr` is 8-aligned (checked at the top) and a kernel address - // (checked in this arm), so it is inside the direct map. - Some(unsafe { *(addr as *const u64) }) - } else { - None + if !addr.is_multiple_of(8) { + return Err(Unread::Absent); } + let at = mm::paging::translate_in_current_tables(addr).ok_or(Unread::Absent)?; + // SAFETY: a direct-map address of 8 aligned bytes inside the present leaf + // the current tables resolved. Not `read_volatile`: see `kernel_backtrace`. + Ok(unsafe { *at.as_ptr::() }) +} + +/// The `in` or `out` at `rip`, read through the page tables as the rest of the +/// report reads user memory; `None` for any other instruction or an unreadable one. +fn port_access_at(rip: u64, rdx: u64) -> Option { + let (at, shift) = (rip & !7, rip & 7); + let lo = read_user_u64(at).ok()?; + // The next word only where the four bytes run into it: an `in` that ends + // just before an unmapped page is still named. + let hi = if shift > 4 { read_user_u64(at + 8).ok()? } else { 0 }; + // Shifted rather than indexed: nothing on this path may panic. + let code = ((u128::from(lo) | u128::from(hi) << 64) >> (8 * shift)) as u32; + toyos_userbound::port_access(code.to_le_bytes(), rdx as u16) } pub(crate) struct ExceptionContext<'a> { @@ -157,7 +158,6 @@ fn crash_report_exception(ctx: &ExceptionContext) { let ring3 = ctx.ring().is_user(); let tid = percpu::current_tid().unwrap_or(crate::process::Tid(0)); let pid = percpu::current_pid(); - let pml4 = if ring3 { crate::DirectMap::from_phys(crate::mm::paging::Cr3::current().phys()).as_ptr::() } else { core::ptr::null() }; let (pf_action, pf_cause) = if ctx.vector() == Vector::PageFault { let action = if ctx.frame.error_code & PF_INSTRUCTION_FETCH != 0 { "execute" } @@ -180,7 +180,24 @@ fn crash_report_exception(ctx: &ExceptionContext) { log!("SIGFPE tid={}: {}", tid, name) } Vector::GeneralProtection | Vector::StackSegment | Vector::AlignmentCheck => { - log!("SIGBUS tid={}: {} (error_code={:#x})", tid, name, ctx.frame.error_code) + log!("SIGBUS tid={}: {} (error_code={:#x})", tid, name, ctx.frame.error_code); + // At CPL 3 an `in` or `out` faults only on a port the bitmap refuses. + if let Some(access) = (ctx.vector() == Vector::GeneralProtection) + .then(|| port_access_at(ctx.frame.rip, ctx.frame.rdx)) + .flatten() + { + match super::super::pio::refused_port(access) { + Some(port) if port == access.port => { + log!(" {access}, which this process holds no grant for") + } + Some(port) => log!( + " {access}, reaching port {port:#06x}, which this process holds no grant for" + ), + // The bitmap opens the span whole, so the #GP is not + // the port's: a string form's non-canonical `rsi`/`rdi`, say. + None => log!(" {access}, a #GP this decode cannot attribute to a port"), + } + } } _ => log!("FATAL tid={}: {}", tid, name), } @@ -203,7 +220,12 @@ fn crash_report_exception(ctx: &ExceptionContext) { } if ctx.vector() == Vector::PageFault { - crate::mm::paging::debug_page_walk(ctx.cr2); + // A user fault walks its own half: the kernel's tables are no process's to read. + if ring3 && !toyos_userbound::is_user_addr(ctx.cr2) { + log!(" Page walk for {:#x} refused: no user address", ctx.cr2); + } else { + crate::mm::paging::debug_page_walk(ctx.cr2); + } } log!(" Registers:"); @@ -232,7 +254,7 @@ fn crash_report_exception(ctx: &ExceptionContext) { log!(" Backtrace:"); if ring3 { if pid.is_some() { - user_backtrace(ctx.frame.rbp, pml4, 32); + user_backtrace(ctx.frame.rbp, 32); } } else { kernel_backtrace(ctx.frame.rbp, 32); @@ -246,17 +268,23 @@ fn crash_report_exception(ctx: &ExceptionContext) { percpu::syscall_num(), user_rip, percpu::user_rsp()); log!(" User backtrace:"); process::resolve_user_symbol(user_rip).log_bare(user_rip); - let pml4 = crate::DirectMap::from_phys(crate::mm::paging::Cr3::current().phys()).as_ptr::(); - user_backtrace(percpu::syscall_rbp(), pml4, 20); + user_backtrace(percpu::syscall_rbp(), 20); } } - if safe_read_u64(ctx.frame.rsp, pml4).is_some() { - log!(" Stack (from RSP):"); - for i in 0..8u64 { - let addr = ctx.frame.rsp + i * 8; - let Some(val) = safe_read_u64(addr, pml4) else { break }; - log!(" [{:#x}] = {:#018x}", addr, val); + // A user fault's stack is the process's own, and a stack pointer it aimed + // at the kernel reads nothing. + let read = |addr: u64| if ring3 { read_user_u64(addr) } else { safe_read_kernel(addr).ok_or(Unread::Absent) }; + match read(ctx.frame.rsp) { + Err(Unread::Refused) => log!(" Stack (from RSP): {:#x} refused: no user address", ctx.frame.rsp), + Err(Unread::Absent) => {} + Ok(_) => { + log!(" Stack (from RSP):"); + for i in 0..8u64 { + let addr = ctx.frame.rsp.wrapping_add(i * 8); + let Ok(val) = read(addr) else { break }; + log!(" [{:#x}] = {:#018x}", addr, val); + } } } @@ -298,8 +326,7 @@ fn crash_report_panic(info: &core::panic::PanicInfo, rbp: u64) { percpu::syscall_num(), user_rip, percpu::user_rsp()); log!(" User backtrace:"); process::resolve_user_symbol(user_rip).log_bare(user_rip); - let pml4 = crate::DirectMap::from_phys(crate::mm::paging::Cr3::current().phys()).as_ptr::(); - user_backtrace(percpu::syscall_rbp(), pml4, 20); + user_backtrace(percpu::syscall_rbp(), 20); } } } @@ -362,11 +389,10 @@ pub(super) fn double_fault_handler(frame: &TrapFrame) -> ! { log!(" User context (pid={:?} tid={:?}):", pid, tid); log!(" rip={:#018x} rsp={:#018x} rbp={:#018x}", maybe_rip, maybe_rsp, user_rbp); - let pml4 = crate::DirectMap::from_phys(crate::mm::paging::Cr3::current().phys()).as_ptr::(); log!(" User backtrace:"); if pid.is_some() { process::resolve_user_symbol(maybe_rip).log_bare(maybe_rip); - user_backtrace(user_rbp, pml4, 20); + user_backtrace(user_rbp, 20); } else { log!(" {:#x}", maybe_rip); } diff --git a/kernel/src/arch/x86_64/idt/isa.rs b/kernel/src/arch/x86_64/idt/isa.rs new file mode 100644 index 00000000000..61eac4b1e46 --- /dev/null +++ b/kernel/src/arch/x86_64/idt/isa.rs @@ -0,0 +1,17 @@ +//! The vector a claimed ISA function's lines deliver on: a count into the +//! row's record and a post of its claim's watch, as a claimed PCI function's +//! message is. + +use super::device_irq::device_irq_entry; + +extern "sysv64" fn isa0_handler() { + crate::arch::percpu::irq_took!(UserDev); + crate::isa::isr(0); + crate::preempt::set_need_resched(); + crate::arch::apic::eoi(); +} + +device_irq_entry! { + /// `pio::GRANTABLE`'s row 0. + pub(super) fn isa0_entry => isa0_handler +} diff --git a/kernel/src/arch/x86_64/idt/mod.rs b/kernel/src/arch/x86_64/idt/mod.rs index c7f63b30495..f5ed027ec4f 100644 --- a/kernel/src/arch/x86_64/idt/mod.rs +++ b/kernel/src/arch/x86_64/idt/mod.rs @@ -3,6 +3,7 @@ mod device_irq; mod dma_fault; mod hda; mod i8042; +mod isa; mod nmi; pub(crate) mod spurious; mod timer; @@ -30,6 +31,11 @@ const PIC2_DATA: u16 = 0xA1; /// The vector both PS/2 lines are routed to. pub const I8042_VECTOR: u8 = Vector::I8042 as u8; +/// The vector each `pio::GRANTABLE` row's lines are routed to, by row: the +/// vector is how the kernel knows whose record an interrupt belongs to. +pub const ISA_VECTORS: [u8; 1] = [Vector::Isa0 as u8]; +const _: () = assert!(ISA_VECTORS.len() == super::pio::GRANTABLE.len(), "a row with no vector"); + /// The vector an IOMMU writes into its own `FEDATA`. pub const DMA_FAULT_VECTOR: u8 = Vector::DmaFault as u8; @@ -261,6 +267,7 @@ idt_vectors! { ring3 UserDev1 = 0x29, user_dev::user_dev1_entry; ring3 UserDev2 = 0x2A, user_dev::user_dev2_entry; ring3 UserDev3 = 0x2B, user_dev::user_dev3_entry; + ring3 Isa0 = 0x2C, isa::isa0_entry; // Ring 0 because it never returns: `cli; hlt` forever. ring0 HaltAll = 0xFD, stub_halt_all; ring3 TlbFlush = 0xFE, tlb::tlb_flush_entry; diff --git a/kernel/src/arch/x86_64/mod.rs b/kernel/src/arch/x86_64/mod.rs index 5d9a48a9a47..f9f59e888a8 100644 --- a/kernel/src/arch/x86_64/mod.rs +++ b/kernel/src/arch/x86_64/mod.rs @@ -28,6 +28,7 @@ pub mod mtrr; pub mod paging; pub mod pat; pub mod percpu; +pub mod pio; pub mod pmu; pub mod power; pub mod rtc; diff --git a/kernel/src/arch/x86_64/paging.rs b/kernel/src/arch/x86_64/paging.rs index 1ad2ef7bad4..b8d0d70e859 100644 --- a/kernel/src/arch/x86_64/paging.rs +++ b/kernel/src/arch/x86_64/paging.rs @@ -948,23 +948,37 @@ fn has(entry: u64, flag: u64) -> u8 { /// space); lock-free and silent, for the panic path to prove a mapping /// before writing through it. pub fn present_in_current_tables(addr: u64) -> bool { + translate_in_current_tables(addr).is_some() +} + +/// Where `addr` lands in the tables this CPU runs under now, whatever the +/// leaf's size; lock-free, for a crash path that may take no lock. Rights are +/// not asked: a caller reading on a process's behalf refuses a non-user +/// address itself. +pub fn translate_in_current_tables(addr: u64) -> Option { // SAFETY: `Cr3::current().phys()` names the table this CPU runs under // right now, so it can't be freed meanwhile. No lock: each entry read is // one aligned `u64`, atomic at the hardware level, so no read is torn. let mut table = unsafe { PageTablePage::from_phys(Cr3::current().phys()) }; for level in 0..3 { - let entry = table[((addr >> (39 - level * 9)) & 0x1FF) as usize]; + let shift = 39 - level * 9; + let entry = table[((addr >> shift) & 0x1FF) as usize]; if entry & PAGE_PRESENT == 0 { - return false; + return None; } if level > 0 && entry & PAGE_SIZE_BIT != 0 { - return true; + let span = 1u64 << shift; + return Some(crate::mm::DirectMap::from_phys( + (entry & ADDR_MASK & !(span - 1)) | (addr & (span - 1)), + )); } // SAFETY: PRESENT just checked; same argument as above the loop — a // present entry under the current CR3 names a live table. table = unsafe { PageTablePage::from_phys(entry & ADDR_MASK) }; } - table[((addr >> 12) & 0x1FF) as usize] & PAGE_PRESENT != 0 + let pte = table[((addr >> 12) & 0x1FF) as usize]; + (pte & PAGE_PRESENT != 0) + .then(|| crate::mm::DirectMap::from_phys((pte & ADDR_MASK) | (addr & 0xFFF))) } /// Give every 2 MiB leaf covering `[phys, phys + size)` the write-combining diff --git a/kernel/src/arch/x86_64/percpu.rs b/kernel/src/arch/x86_64/percpu.rs index 3cd57d6ddcf..07cc73c15a7 100644 --- a/kernel/src/arch/x86_64/percpu.rs +++ b/kernel/src/arch/x86_64/percpu.rs @@ -4,6 +4,8 @@ use core::sync::atomic::{AtomicU32, AtomicU64, AtomicU8}; use alloc::alloc::alloc_zeroed; use core::alloc::Layout; +use toyos_userbound::IoBitmap; + use super::cpu; use crate::log; @@ -22,7 +24,7 @@ pub const STAR_SYSRET_BASE: u16 = USER_DS - 8; const _: () = assert!(STAR_SYSRET_BASE + 8 == USER_DS); const _: () = assert!(STAR_SYSRET_BASE + 16 == USER_CS); -/// 64-bit TSS (104 bytes). +/// The 104-byte 64-bit TSS, then the I/O permission bitmap. #[repr(C, packed)] pub struct Tss { reserved0: u32, @@ -34,6 +36,9 @@ pub struct Tss { reserved2: u64, reserved3: u16, iopb_offset: u16, + /// What Ring 3 may `in` and `out` on this CPU; `pio::switch_to` opens a + /// port only while the process holding it runs. + io_bitmap: IoBitmap, } impl Tss { @@ -47,11 +52,18 @@ impl Tss { ist: [0; 7], reserved2: 0, reserved3: 0, - iopb_offset: size_of::() as u16, + iopb_offset: offset_of!(Tss, io_bitmap) as u16, + io_bitmap: IoBitmap::refusing(), } } } +const _: () = assert!(offset_of!(Tss, io_bitmap) == 104, "the bitmap follows the architectural TSS"); +const _: () = assert!( + offset_of!(Tss, io_bitmap) + size_of::() == size_of::(), + "the TSS limit ends at the bitmap's refusing byte" +); + /// Per-CPU fault state machine for the escalation policy on nested faults. #[repr(u8)] #[derive(Clone, Copy, PartialEq, Eq, Debug)] @@ -550,6 +562,19 @@ pub unsafe fn set_kernel_stack(rsp: u64) { core::ptr::write_unaligned(&raw mut (*percpu).tss.rsp0, rsp); } +/// This CPU's I/O permission bitmap. Called with preemption held, by a +/// scheduler pass's own hold or by closed interrupts, so the caller cannot +/// leave the CPU whose bitmap this reaches while `f` runs. +pub fn io_bitmap(f: impl FnOnce(&mut IoBitmap) -> R) -> R { + let percpu = gs::read_u64::() as *mut PerCpu; + // SAFETY: this CPU's own `PerCpu`, read from `gs:[0]`, and the only Rust + // reference into its bitmap while `f` runs: the caller stays on this CPU, + // and the one handler that reaches the bitmap is a Ring 3 #GP's, which + // interrupts no kernel code; `IoBitmap` has alignment 1, which no packed + // struct breaks. + f(unsafe { &mut (*percpu).tss.io_bitmap }) +} + /// The two words [`set_kernel_stack`] writes: `kernel_rsp` (syscall entry) and `tss.rsp0` (Ring 3 interrupt entry); read only by an instrument. /// # Safety: must be called from the CPU whose GS base points to the relevant PerCpu. #[cfg(feature = "stack-witness")] diff --git a/kernel/src/arch/x86_64/pio.rs b/kernel/src/arch/x86_64/pio.rs new file mode 100644 index 00000000000..97ae5784949 --- /dev/null +++ b/kernel/src/arch/x86_64/pio.rs @@ -0,0 +1,60 @@ +//! The I/O port space as a process reaches it: the ISA functions it can be +//! handed, their lines, and this CPU's I/O permission bitmap, whose decisions +//! are `toyos_userbound::port`'s. + +use alloc::format; +use alloc::string::String; + +use toyos_userbound::PortAccess; + +use super::ioapic::{self, Gsi}; +use crate::isa::Grantable; +use crate::process::Pid; + +/// The functions a process may be handed: the i8042's data and command ports, +/// its keyboard line and its aux line. +pub const GRANTABLE: &[Grantable] = &[Grantable { + name: "the i8042", + ports: &[0x60, 0x64], + irqs: &[1, 12], + kernel_drives: super::i8042::drives, +}]; + +/// A routed line: the I/O APIC input it arrives on. +pub type Line = Gsi; + +/// Point ISA line `irq` at the row's vector on the CPU every device interrupt +/// targets, masked. +pub fn route(row: usize, irq: u8) -> Result { + let line = ioapic::gsi_for_isa_irq(irq).ok_or_else(|| String::from("no I/O APIC"))?; + ioapic::route( + line.gsi, + super::idt::ISA_VECTORS[row], + crate::drivers::pci::MSG_DEST, + line.trigger, + line.polarity, + ) + .map_err(|why| format!("{why:?}"))?; + Ok(line.gsi) +} + +pub fn set_masked(line: Line, masked: bool) { + ioapic::set_masked(line, masked).expect("a line `route` placed has a unit"); +} + +/// Open every row's ports if `pid` holds that row and close them otherwise, on +/// this CPU: at every switch, with `pid` the incoming task's process, under the +/// pass's preemption hold; and at a bind, with interrupts closed. +pub fn switch_to(pid: Option) { + let rows = GRANTABLE + .iter() + .enumerate() + .map(|(row, grantable)| (grantable.ports, pid.is_some_and(|pid| crate::isa::bound_to(row, pid)))); + super::percpu::io_bitmap(|bitmap| bitmap.switch_to(rows)); +} + +/// The port `access` faulted on, by this CPU's bitmap: the process that +/// faulted is still this CPU's, and its #GP's handler runs with interrupts off. +pub fn refused_port(access: PortAccess) -> Option { + super::percpu::io_bitmap(|bitmap| bitmap.refused(access)) +} diff --git a/kernel/src/device.rs b/kernel/src/device.rs index 66935836c20..20e9e15f0a7 100644 --- a/kernel/src/device.rs +++ b/kernel/src/device.rs @@ -42,11 +42,13 @@ pub struct Claim { /// What one claim holds. A class is at most one device on this machine and a /// per-class flag says whether it is taken; a PCI function is one of several, /// so what it gives back is its `pcidev` slot; a partition's exclusivity is its -/// view's own hold on the blocks (`block::Partition::of`). +/// view's own hold on the blocks (`block::Partition::of`); an ISA function's is +/// its `isa` row's. enum Claimed { Class(DeviceType), PciFunction(usize), Partition(crate::block::Partition), + Isa(usize), } impl Claim { @@ -71,7 +73,7 @@ impl Claim { pub(crate) fn partition(&self) -> Option<&crate::block::Partition> { match &self.what { Claimed::Partition(view) => Some(view), - Claimed::Class(_) | Claimed::PciFunction(_) => None, + Claimed::Class(_) | Claimed::PciFunction(_) | Claimed::Isa(_) => None, } } } @@ -89,7 +91,8 @@ impl Drop for Claim { | DeviceType::Mouse | DeviceType::Framebuffer | DeviceType::PciFunction - | DeviceType::Partition => {} + | DeviceType::Partition + | DeviceType::Isa => {} } *taken(class).lock() = false; } @@ -98,6 +101,7 @@ impl Drop for Claim { Claimed::PciFunction(slot) => crate::pcidev::release(slot), // The view drops with this, and its hold with the last clone of it. Claimed::Partition(_) => {} + Claimed::Isa(row) => crate::isa::release(row), } } } @@ -187,6 +191,13 @@ pub fn try_claim(class: DeviceType, selector: [u64; 2]) -> Result { + let set = toyos_abi::syscall::IsaId::from_wire(selector).ok_or(ClaimError::Absent)?; + // The row's own guard, taken inside as a PCI slot's is. + let row = crate::isa::claim(set)?; + let claim = Claim { what: Claimed::Isa(row) }; + Ok(DeviceClaim::new(class, DeviceInfo::Isa(set, row), claim)) + } DeviceType::HdaAudio => { let (info, pcm) = crate::drivers::hda::info().ok_or(ClaimError::Absent)?; let claim = Claim::acquire(class)?; diff --git a/kernel/src/drivers/panic_console/mod.rs b/kernel/src/drivers/panic_console/mod.rs index b7fb8373dd0..2228210d661 100644 --- a/kernel/src/drivers/panic_console/mod.rs +++ b/kernel/src/drivers/panic_console/mod.rs @@ -6,9 +6,9 @@ //! [`render`] paints it inside `halt_all_cpus`, before `panic_flush`. virtio-gpu //! is unsupported: its scanout needs the unbounded-poll wedge this module avoids. //! -//! The two holds this module ends a panic in — [`page_forever`] and -//! [`hold_the_panel`] — are also where `crate::panic_reboot`'s bound is -//! watched, because the keyboard poll that retires it is here. +//! The hold this module ends a panic in, [`hold_the_panel`], is also where +//! `crate::panic_reboot`'s bound is watched. It reads no input and scrolls +//! nothing: a dead kernel takes none, and its report shows what fits, once. mod access; mod latch; @@ -18,7 +18,6 @@ use core::cell::UnsafeCell; use core::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, AtomicUsize, Ordering}; use toyos_abi::boot::{KernelArgs, MemoryMapEntry}; -use toyos_ps2::{KeyDecoder, KeyOutcome}; use crate::log; use crate::panic_reboot::Bound; @@ -44,12 +43,6 @@ const _: () = assert!(SNAPSHOT_CAP >= MAX_ROWS * MAX_COLS); /// One bit per byte `text` can hold — worst case is a message of nothing but newlines, one line per byte. const ALERT_WORDS: usize = SNAPSHOT_CAP.div_ceil(64); -/// How long each page stays up; a [`Cadence`], not a deadline — nothing expires. -const PAGE_HOLD: Cadence = Cadence::every( - Duration::from_secs(3), - "a five-page report cycles inside a 15-second video", -); - /// How long Ctrl+Alt+D's report keeps the panel. const REPORT_HOLD: Budget = Budget::of( Duration::from_secs(15), @@ -586,8 +579,7 @@ fn live_tail() -> View<'static> { // already partly consumed. /// What a fatal path paints: the captured report, or the live ring for a -/// path that reached `halt_all_cpus` without the panic handler. Idempotent -/// for the captured case, which [`page_forever`] walks repeatedly. +/// path that reached `halt_all_cpus` without the panic handler. fn fatal_text() -> View<'static> { if CAPTURE_ACCESS.read() { // SAFETY: `read` changed the state to `READING` before access; every writer refuses that state. @@ -637,8 +629,8 @@ fn fatal_claimed() -> bool { } /// Paint the newest page of the captured report, fatal paths only; returns -/// whether this call claimed the screen, entitling [`page_forever`] — which it -/// does whatever any painter that is not a fatal path holds. +/// whether this call claimed the screen, entitling [`hold_the_panel`] — which +/// it does whatever any painter that is not a fatal path holds. pub fn render() -> bool { if !seize() { return false; @@ -648,7 +640,7 @@ pub fn render() -> bool { // Before the paint, from the same view the panel gets: a fault inside the // painter then costs the screen and not the copy the next boot reads. crate::blackbox::record_panic(text.text); - paint(Fill::Fatal, text, Page::Last, Watch::No, || false); + paint(Fill::Fatal, text, Watch::No, || false); true } @@ -670,114 +662,19 @@ pub fn seal_wedge(said: core::fmt::Arguments) { crate::blackbox::record_wedge(format_args!("{said}{Census}\n"), live_tail().text); } -/// Cycle the report across the screen until the machine is switched off, or -/// until `bound` returns it to firmware. Reached only from `halt_all_cpus`, -/// after `panic_flush`, on the CPU whose [`render`] claimed [`FATAL`]; the -/// handler's other two exits reach [`hold_the_panel`] the same way, and every -/// one of the three is the last call its CPU makes. -pub fn page_forever(mut bound: Bound) -> ! { - if !crate::clock::calibrated() { - hold_the_panel(bound); +/// Keep the panel as it is until `bound` resets the machine: the panic path's +/// terminal hold, and the last call its CPU makes. +pub fn hold_the_panel(bound: Bound) -> ! { + if !bound.is_armed() { + // Nothing to wait for, so this CPU costs the machine no power. + crate::arch::cpu::halt() } - let text = fatal_text(); - let Some(fb) = snapshot() else { hold_the_panel(bound) }; - let Some((cols, grid_rows)) = geometry(&fb) else { hold_the_panel(bound) }; - let (_, pages, _) = pagination(text.text, cols, grid_rows); - if pages < 2 { - hold_the_panel(bound); - } - // `None` is the screenful [`render`] already painted, not a numbered page, so the first key reaches either end. - let mut shown: Option = None; - let mut keys = KeyDecoder::new(); - // Once steered, the cycle is his: there is no way back to automatic paging. - let mut steered = false; - // Spins rather than `hlt`: nothing would wake it, and re-arming the LAPIC timer would dispatch the scheduler mid-panic. + // Spins rather than `hlt`: nothing would wake it, and re-arming the LAPIC + // timer would dispatch the scheduler mid-panic. loop { - let step = hold((!steered).then_some(PAGE_HOLD.nanos()), &mut keys, &mut bound); - steered |= step.is_some(); - let next = match (shown, step.unwrap_or(PageKey::Down)) { - (None, PageKey::Down) => 0, - (None, PageKey::Up) => pages - 1, - (Some(page), PageKey::Down) => (page + 1) % pages, - (Some(page), PageKey::Up) => (page + pages - 1) % pages, - }; - paint(Fill::Fatal, text, Page::Nth(next), Watch::No, || false); - shown = Some(next); - } -} - -/// Keep the panel as it is until `bound` resets the machine, or for good once a -/// key has retired it. The panic path's terminal hold wherever there is no -/// second page to cycle — and the whole of it on a machine with no panel at all. -/// -/// One poller: every caller is the CPU that claimed [`FATAL`], because two CPUs -/// reading port 0x60 would each see half of every scancode. -pub fn hold_the_panel(mut bound: Bound) -> ! { - let mut keys = KeyDecoder::new(); - while bound.is_armed() { - read_key(&mut keys, &mut bound); bound.check(); core::hint::spin_loop(); } - // Nothing left to wait for, so this CPU costs the machine no power. - crate::arch::cpu::halt() -} - -/// One byte off the controller, folded into `keys`; a key **press** retires -/// `bound`, since pressing one is how the person reading the panel says he is -/// there. `None` is a poll that found nothing, which is not the decoder's -/// `Pending`. -/// -/// The pointer shares the port; its packet bytes look like scancodes to -/// anything that does not skip them. [`i8042::poll_byte`] is an `inb` — no -/// lock, no MMIO. -/// -/// [`i8042::poll_byte`]: crate::arch::keyboard_controller::poll_byte -fn read_key(keys: &mut KeyDecoder, bound: &mut Bound) -> Option { - let (byte, false) = crate::arch::keyboard_controller::poll_byte()? else { - return None; - }; - let outcome = keys.feed(byte); - // A make code and nothing else. A break code is the release of a key - // pressed before this panel existed, and a controller's own byte — an ACK, - // a self-test result — is the hardware answering itself; neither is a - // person saying he is here to read it. - if matches!(outcome, KeyOutcome::Key { pressed: true, .. }) { - bound.retire(); - } - Some(outcome) -} - -/// Which way the next paint moves. -#[derive(Clone, Copy)] -enum PageKey { - Up, - Down, -} - -/// HID usages: what the wire decoder emits, not scancodes. -const HID_PAGE_UP: u8 = 0x4B; -const HID_PAGE_DOWN: u8 = 0x4E; - -/// Wait for a page key, giving up after `nanos`; `None` means the deadline -/// expired. `bound` is retired by any key and resets the machine at its own -/// expiry, so an unattended panel pages until it is over and no longer. -fn hold(nanos: Option, keys: &mut KeyDecoder, bound: &mut Bound) -> Option { - let target = nanos.map(|n| crate::clock::nanos_since_boot().saturating_add(n)); - while target.is_none_or(|t| crate::clock::nanos_since_boot() < t) { - match read_key(keys, bound) { - Some(KeyOutcome::Key { usage: HID_PAGE_UP, pressed: true }) => { - return Some(PageKey::Up); - } - Some(KeyOutcome::Key { usage: HID_PAGE_DOWN, pressed: true }) => { - return Some(PageKey::Down); - } - _ => {} - } - bound.check(); - core::hint::spin_loop(); - } - None } /// Repaint at a boot phase boundary, so a machine that wedges later still shows how far it got. @@ -815,7 +712,7 @@ fn repaint() { if SCREEN_OWNED_BY_USERLAND.load(Ordering::Relaxed) || !take_for_boot() { return; } - paint(Fill::Boot, live_tail(), Page::Last, Watch::No, fatal_claimed); + paint(Fill::Boot, live_tail(), Watch::No, fatal_claimed); PAINTING.store(false, Ordering::SeqCst); } @@ -851,7 +748,7 @@ fn paint_held_report() { #[cfg(feature = "boot-actuators")] stall::inside_the_latch(); forget_the_glass(); - paint(Fill::Boot, report_text(), Page::Last, Watch::Yes, fatal_claimed); + paint(Fill::Boot, report_text(), Watch::Yes, fatal_claimed); PAINTING.store(false, Ordering::SeqCst); } @@ -891,14 +788,6 @@ pub fn hold_report() { paint_held_report(); } -/// Which slice of the text a paint shows. -#[derive(Clone, Copy)] -enum Page { - /// The newest screenful: a page-aligned last page would leave the bottom blank when rows divide badly. - Last, - Nth(usize), -} - /// Carries "halted" vs "still booting" at zero cost, and proves the console ran this boot. #[derive(Clone, Copy)] enum Fill { @@ -1144,7 +1033,7 @@ fn spent(began: u64, pixels: u64) { /// `stop` is asked before every row and every scanline of a fill: a paint it /// answers yes to leaves at once, and the grid is forgotten, not believed. -fn paint(fill: Fill, view: View, page: Page, watch: Watch, stop: impl Fn() -> bool) { +fn paint(fill: Fill, view: View, watch: Watch, stop: impl Fn() -> bool) { let Some(fb) = snapshot() else { return }; if !mapped(&fb) { return; @@ -1154,14 +1043,9 @@ fn paint(fill: Fill, view: View, page: Page, watch: Watch, stop: impl Fn() -> bo let mut pixels = 0u64; let text = view.text; let (total, pages, per) = pagination(text, cols, grid_rows); - // `Last` is the newest `per` rows, not page `pages - 1`, so both an even - // and uneven division fill the screen; the footer's page number therefore - // can't be derived from `first` and `shown` carries it separately. - let newest = total.saturating_sub(per); - let (first, shown) = match page { - Page::Last => (newest, pages), - Page::Nth(n) => (n.saturating_mul(per).min(newest), (n + 1).min(pages)), - }; + // The newest `per` rows, not page `pages - 1`: a page-aligned last page + // would leave the bottom blank when rows divide badly. + let first = total.saturating_sub(per); let ground = match fill { Fill::Fatal => rgb(&fb, 0x60, 0x00, 0x00), @@ -1215,7 +1099,7 @@ fn paint(fill: Fill, view: View, page: Page, watch: Watch, stop: impl Fn() -> bo } } if pages > 1 && r == grid_rows - 1 { - footer_cells(shown, pages, want); + footer_cells(pages, want); } let glassed = glass.row(r, cols); @@ -1280,15 +1164,15 @@ fn flush_stores() { crate::arch::barrier::scanout_flush(); } -/// `[page 2/4]` into the bottom row's cells; not decoration — the pager advances on a timer with no key to press. -fn footer_cells(page: usize, pages: usize, row: &mut [Cell]) { +/// `[page 4/4]` into the bottom row's cells: the screen is the last page of more. +fn footer_cells(pages: usize, row: &mut [Cell]) { let mut buf = [0u8; 24]; let mut n = 0; for &b in b"[page " { buf[n] = b; n += 1; } - n += write_num(&mut buf[n..], page); + n += write_num(&mut buf[n..], pages); buf[n] = b'/'; n += 1; n += write_num(&mut buf[n..], pages); diff --git a/kernel/src/drivers/pci.rs b/kernel/src/drivers/pci.rs index 31b12e1397a..1e033aedbe8 100644 --- a/kernel/src/drivers/pci.rs +++ b/kernel/src/drivers/pci.rs @@ -27,7 +27,7 @@ pub const MSIX_ENTRY: u16 = 0; // Every device interrupt in this kernel targets cpu0, named as a destination for // the message and for the unit to put in an entry. -const MSG_DEST: u32 = 0; +pub(crate) const MSG_DEST: u32 = 0; /// No requester id: a bus/device/function is sixteen bits, so this is none of them. pub(crate) const NO_FUNCTION: u32 = u32::MAX; diff --git a/kernel/src/irq_census.rs b/kernel/src/irq_census.rs index be637d91d3b..4b8df43a05e 100644 --- a/kernel/src/irq_census.rs +++ b/kernel/src/irq_census.rs @@ -19,9 +19,10 @@ pub enum Source { Timer, /// Vector 0x21, xHCI MSI-X (or MSI). Xhci, - /// Vectors 0x28-0x2B, the MSI-X of a PCI function a process drives. One - /// count for all four: which claim a message belonged to is the claim's own - /// record, and the census is about this machine's interrupt routing. + /// Vectors 0x28-0x2C: the MSI-X of a PCI function a process drives, and + /// the lines of an ISA function one does. One count for all five: which + /// claim an interrupt belonged to is the claim's own record, and the census + /// is about this machine's interrupt routing. UserDev, /// Vector 0x23, virtio-sound MSI-X. Sound, diff --git a/kernel/src/isa.rs b/kernel/src/isa.rs new file mode 100644 index 00000000000..dedf26c0a0a --- /dev/null +++ b/kernel/src/isa.rs @@ -0,0 +1,180 @@ +//! A legacy ISA function driven by a process: the I/O ports it decodes, +//! opened in the CPU's I/O permission bitmap, and the lines it raises, +//! answered as interrupt records on the claim. +//! +//! **What can be claimed is the architecture's [`GRANTABLE`] table, matched +//! exactly.** A port no row names is never opened, a set that is not a whole +//! row is refused rather than trimmed to one, and a row this kernel drives +//! itself is refused by name. +//! +//! **The ports belong to a process, not to the handle.** The first read of the +//! claim binds them to the process that reads it ([`bind`]); from then until +//! that process's teardown ([`process_ends`]) every switch onto one of its +//! threads opens them and every other switch closes them +//! (`arch::pio::switch_to`). A handle moved on after that answers nothing but +//! refusals, and the row is claimable again only once both the claim and the +//! process are gone. A binding reaches the process's other threads at their +//! next switch, and its end needs no switch at all: teardown runs once every +//! thread has left, and no thread that has left returns to Ring 3. +//! +//! **A line is routed once per boot and masked while no claim holds it**, since +//! an interrupt-remapping entry is never given back. Its handler counts into +//! the record a claimed PCI function's does and posts the claim's watch, and +//! the holder reads the record back the same way. + +use alloc::vec::Vec; +use core::sync::atomic::{AtomicU32, Ordering}; + +use toyos_abi::pci::DeviceIrqRecord; +use toyos_abi::syscall::IsaId; + +use crate::arch::pio::{self, GRANTABLE}; +use crate::device::ClaimError; +use crate::pcidev::record::Interrupt; +use crate::process::Pid; +use crate::sync::Lock; +use crate::watch::IrqWatch; + +/// One function a process may be handed whole. +pub struct Grantable { + /// What the log calls it. + pub name: &'static str, + /// Ascending, as [`IsaId`] spells them, and each one the I/O permission + /// bitmap names: a port past it is no `u8`. + pub ports: &'static [u8], + pub irqs: &'static [u8], + /// Whether this kernel drives the function itself, which no claim shares. + pub kernel_drives: fn() -> bool, +} + +/// How many rows any architecture's table has; a static array per row below. +const MAX_ROWS: usize = 1; +const _: () = assert!(GRANTABLE.len() <= MAX_ROWS, "every grantable row needs its state"); + +/// No process holds the row's ports. +const NOBODY: u32 = Pid::MAX.0; + +struct Row { + /// A claim on the row exists. + minted: bool, + /// The row's lines, routed by its first claim and kept; `Err` is a line + /// this machine could not route, which refuses every claim after it too. + lines: Option, ()>>, +} + +static ROWS: [Lock; MAX_ROWS] = + [const { Lock::new(Row { minted: false, lines: None }) }; MAX_ROWS]; + +/// The pid whose threads the row's ports are open for, or [`NOBODY`]; read by +/// every context switch, which takes no lock. +static BOUND: [AtomicU32; MAX_ROWS] = [const { AtomicU32::new(NOBODY) }; MAX_ROWS]; + +static IRQ: [Interrupt; MAX_ROWS] = [const { Interrupt::new() }; MAX_ROWS]; + +/// What a claim's poll waits on, one per row. +static WATCHES: [IrqWatch; MAX_ROWS] = [const { IrqWatch::new() }; MAX_ROWS]; + +/// Mint the claim on the row `set` names, lines routed and unmasked. +pub fn claim(set: IsaId) -> Result { + let row = GRANTABLE + .iter() + .position(|g| { + set.ports().eq(g.ports.iter().map(|&port| u16::from(port))) + && set.irqs().eq(g.irqs.iter().copied()) + }) + .ok_or(ClaimError::Absent)?; + let grantable = &GRANTABLE[row]; + if (grantable.kernel_drives)() { + return Err(ClaimError::KernelDriven); + } + let mut state = ROWS[row].lock(); + if state.minted || BOUND[row].load(Ordering::Acquire) != NOBODY { + return Err(ClaimError::Owned); + } + let lines = state.lines.get_or_insert_with(|| { + grantable + .irqs + .iter() + .map(|&irq| { + pio::route(row, irq).map_err(|why| { + log!("isa: {} line {irq} not routable: {why}", grantable.name); + }) + }) + .collect() + }); + let Ok(lines) = lines else { return Err(ClaimError::Unusable) }; + for &line in lines.iter() { + pio::set_masked(line, false); + } + state.minted = true; + Ok(row) +} + +/// The claim's last handle went: its lines masked, its record emptied and its +/// polls answered. The ports stay with the process that bound them until that +/// process ends. +pub fn release(row: usize) { + // Not held across the watch's answer; the row stays minted until that is + // made, so no next claim's poll is among the ones answered. + if let Some(Ok(lines)) = &ROWS[row].lock().lines { + for &line in lines { + pio::set_masked(line, true); + } + } + IRQ[row].clear(); + // The claim is gone, so a poll on it is answered rather than left for the + // next holder's interrupts. + WATCHES[row].cancel_polls(); + ROWS[row].lock().minted = false; +} + +/// Open the row's ports to `pid` for the rest of its life. Called once per +/// claim, by the claim's first read, and never twice for a row: [`claim`] +/// mints none while a process holds its ports. +pub fn bind(row: usize, pid: Pid) { + match BOUND[row].compare_exchange(NOBODY, pid.raw(), Ordering::AcqRel, Ordering::Acquire) { + Ok(_) => log!("isa: {}'s ports are pid {pid}'s", GRANTABLE[row].name), + Err(held) => assert!(held == pid.raw(), "isa: row {row} is pid {held}'s, and pid {pid} bound it"), + } + // This thread is already running, so no switch opens them for it. + let _irq = crate::arch::IrqGuard::close(); + pio::switch_to(Some(pid)); +} + +/// Whether `pid` holds the row's ports. +pub fn bound_to(row: usize, pid: Pid) -> bool { + BOUND[row].load(Ordering::Acquire) == pid.raw() +} + +/// Called from the process teardown, once every thread has left. +pub fn process_ends(pid: Pid) { + for (row, bound) in BOUND.iter().enumerate().take(GRANTABLE.len()) { + if bound.compare_exchange(pid.raw(), NOBODY, Ordering::AcqRel, Ordering::Relaxed).is_ok() { + log!("isa: {}'s ports went back with pid {pid}", GRANTABLE[row].name); + } + } +} + +/// The interrupts since the last read, or `None` for none. +pub fn take_record(row: usize) -> Option { + let taken = IRQ[row].take(); + if taken.is_some() && IRQ[row].take_unannounced() { + log!("isa: {} took its first interrupt", GRANTABLE[row].name); + } + taken.map(|count| DeviceIrqRecord { count }) +} + +pub fn has_irq(row: usize) -> bool { + IRQ[row].armed() +} + +/// Records one interrupt and posts the claim's watch. Called from the row's +/// handler, so it takes no lock but the watch's own and allocates nothing. +pub fn isr(row: usize) { + IRQ[row].took(); + WATCHES[row].post_in_place(); +} + +pub fn watch(row: usize) -> &'static IrqWatch { + &WATCHES[row] +} diff --git a/kernel/src/main.rs b/kernel/src/main.rs index 91566f5c40a..66957505f97 100644 --- a/kernel/src/main.rs +++ b/kernel/src/main.rs @@ -85,6 +85,7 @@ mod pipe; mod device; mod pcidev; +mod isa; mod gpu; mod user_ptr; mod vma; @@ -126,7 +127,6 @@ fn panic(info: &core::panic::PanicInfo) -> ! { let bound = panic_reboot::arm(false); // No capture(): the outer panic's snapshot is the one worth showing. // render() is safe by construction here: a fault inside the renderer itself would find the fatal panel already claimed, and return without touching a pixel. - // Only the CPU that took the panel watches the bound; a reentry inside the pager finds it held and halts. if drivers::panic_console::render() { drivers::panic_console::hold_the_panel(bound); } diff --git a/kernel/src/object/device.rs b/kernel/src/object/device.rs index 2694474e484..fa65bbb2ad0 100644 --- a/kernel/src/object/device.rs +++ b/kernel/src/object/device.rs @@ -28,6 +28,9 @@ pub enum DeviceInfo { /// Which partition, how long, and both its GUIDs; the view it moves blocks /// through is the claim's own (`device::Claim::partition`). Partition(toyos_abi::part::PartitionInfo), + /// The ports and lines granted, as the selector named them, and the `isa` + /// row they are. + Isa(toyos_abi::syscall::IsaId, usize), } /// The two scanout buffers and the cursor plane. @@ -71,6 +74,7 @@ impl DeviceInfo { // is what `SYS_DEVICE_DMA_ALLOC` answers later. Self::PciFunction(info, _) => info.as_bytes().into(), Self::Partition(info) => info.as_bytes().into(), + Self::Isa(set, _) => set.wire().iter().flat_map(|word| word.to_ne_bytes()).collect(), Self::Hda(info, pcm) => { let mut info = *info; info.pcm = install_buffers(table, &[pcm])?[0]; @@ -93,6 +97,8 @@ pub struct DeviceClaim { /// a poll's readiness check and a `close` are both places that must not /// take it. pci_slot: Option, + /// The `isa` row for a claim on one, read without that lock for the same reasons. + isa_row: Option, // No Rights::DUP: at most one handle exists, so info_read needs no per-handle state. info_read: AtomicBool, described: crate::sync::Lock, @@ -112,10 +118,15 @@ impl DeviceClaim { DeviceInfo::PciFunction(_, slot) => Some(*slot), _ => None, }; + let isa_row = match &info { + DeviceInfo::Isa(_, row) => Some(*row), + _ => None, + }; Arc::new(Self { core: Self::new_core(), class, pci_slot, + isa_row, info_read: AtomicBool::new(false), described: crate::sync::Lock::new(Described { info, bytes: None }), reference: Held::new(claim), @@ -134,6 +145,11 @@ impl DeviceClaim { self.pci_slot.map(usize::from) } + /// Which `isa` row this claim holds, for a claim on an ISA function. + pub fn isa_row(&self) -> Option { + self.isa_row + } + /// The view a partition claim transfers through: `None` for a claim on /// anything else, and once the last handle has let the partition go. /// diff --git a/kernel/src/object/ops.rs b/kernel/src/object/ops.rs index d734e59602f..43b39373cc5 100644 --- a/kernel/src/object/ops.rs +++ b/kernel/src/object/ops.rs @@ -280,6 +280,9 @@ pub fn read_watch(object: &KObjectRef) -> Option { device_registry::DeviceType::PciFunction => { d.pci_slot().map(|slot| WatchRef::Irq(crate::pcidev::watch(slot))) } + device_registry::DeviceType::Isa => { + d.isa_row().map(|row| WatchRef::Irq(crate::isa::watch(row))) + } device_registry::DeviceType::HdaAudio | device_registry::DeviceType::VirtioSound => { Some(WatchRef::Irq(&crate::drivers::AUDIO_WATCH)) } @@ -324,6 +327,7 @@ fn close_ends_polls(object: &KObjectRef) -> bool { device_registry::DeviceType::Keyboard => false, device_registry::DeviceType::Mouse | device_registry::DeviceType::PciFunction + | device_registry::DeviceType::Isa | device_registry::DeviceType::HdaAudio | device_registry::DeviceType::VirtioSound | device_registry::DeviceType::Framebuffer @@ -430,6 +434,25 @@ pub fn read_device( buf.write_at(0, record_bytes(&record)); Some(toyos_abi::pci::DeviceIrqRecord::SIZE as u64) } + // The PCI shape, but the description is what binds the ports to the + // reader, and nothing after it answers any other process. + device_registry::DeviceType::Isa => { + let row = claim.isa_row().expect("an ISA claim knows its row"); + let pid = crate::process::current_process(); + if !claim.info_read() { + crate::isa::bind(row, pid); + return Some(claim.describe(table, buf)); + } + if !crate::isa::bound_to(row, pid) { + return Some(SyscallError::PermissionDenied.to_u64()); + } + if buf.len() < toyos_abi::pci::DeviceIrqRecord::SIZE { + return Some(SyscallError::InvalidArgument.to_u64()); + } + let record = crate::isa::take_record(row)?; + buf.write_at(0, record_bytes(&record)); + Some(toyos_abi::pci::DeviceIrqRecord::SIZE as u64) + } device_registry::DeviceType::HdaAudio => { if !claim.info_read() { return Some(claim.describe(table, buf)); @@ -621,7 +644,9 @@ pub fn fstat(object: &KObjectRef) -> Stat { device_registry::DeviceType::Keyboard => FileType::Keyboard, device_registry::DeviceType::Mouse => FileType::Mouse, device_registry::DeviceType::Framebuffer => FileType::Framebuffer, - device_registry::DeviceType::PciFunction => FileType::Unknown, + device_registry::DeviceType::PciFunction | device_registry::DeviceType::Isa => { + FileType::Unknown + } device_registry::DeviceType::HdaAudio | device_registry::DeviceType::VirtioSound => FileType::Unknown, device_registry::DeviceType::Partition => FileType::Unknown, @@ -701,7 +726,8 @@ fn partition_fsync(claim: &DeviceClaim) -> u64 { | device_registry::DeviceType::Framebuffer | device_registry::DeviceType::HdaAudio | device_registry::DeviceType::VirtioSound - | device_registry::DeviceType::PciFunction => { + | device_registry::DeviceType::PciFunction + | device_registry::DeviceType::Isa => { return SyscallError::PermissionDenied.to_u64(); } } @@ -767,6 +793,9 @@ pub fn has_data(object: &KObjectRef) -> bool { device_registry::DeviceType::PciFunction => { !d.info_read() || d.pci_slot().is_some_and(crate::pcidev::has_irq) } + device_registry::DeviceType::Isa => { + !d.info_read() || d.isa_row().is_some_and(crate::isa::has_irq) + } device_registry::DeviceType::Framebuffer => true, device_registry::DeviceType::Partition => true, device_registry::DeviceType::HdaAudio => { diff --git a/kernel/src/panic.rs b/kernel/src/panic.rs index 5481315d3db..89dfbe79673 100644 --- a/kernel/src/panic.rs +++ b/kernel/src/panic.rs @@ -342,8 +342,8 @@ pub fn last_words( } /// Halt all CPUs: stop the others, flush pending log output, then hold this -/// machine's panel until a key retires the reboot bound or the bound returns it -/// to firmware. Every fatal path's one funnel. +/// machine's panel until the reboot bound returns it to firmware. Every fatal +/// path's one funnel. // panic_flush bypasses the log-ring and serial locks — once the others are stopped a wedged holder never releases them, so taking them normally could deadlock. pub fn halt_all_cpus() -> ! { // Before the halt and the panel: from here this machine holds a report for @@ -372,10 +372,10 @@ pub fn halt_all_cpus() -> ! { unsafe { serial::panic_flush(); } // Must follow the flush — it's the deepest stack this path reaches. crate::arch::trap::report_fault_stack(); - // page_forever runs strictly after the flush: it is an unbounded loop and may only run once the serial report is out. - // Only the CPU that painted watches the bound; the rest halt below, since two CPUs polling one keyboard would split every key. + // hold_the_panel runs strictly after the flush: it is an unbounded loop and may only run once the serial report is out. + // Only the CPU that painted watches the bound; the rest halt below. if painted { - crate::drivers::panic_console::page_forever(bound); + crate::drivers::panic_console::hold_the_panel(bound); } cpu::halt(); } diff --git a/kernel/src/panic_reboot.rs b/kernel/src/panic_reboot.rs index 1aa2cace678..88d9fbf238a 100644 --- a/kernel/src/panic_reboot.rs +++ b/kernel/src/panic_reboot.rs @@ -1,8 +1,6 @@ //! What a panicked kernel does with the machine once its report is on the //! panel: it holds the panel for [`toyos_tco::PANIC_BOUND_MS`] and then returns -//! the machine to firmware. A key press retires the bound for good — a key is -//! how a person at the machine says the panel is being read — and nobody -//! pressing one inside the bound means nobody is there to read it. +//! the machine to firmware. //! //! **The bound is carried in counter ticks, not nanoseconds** (`cpu::counter`: //! the TSC, the generic timer's count). A panic may land before `clock::init`, @@ -12,7 +10,7 @@ //! that states no frequency and has no calibrated clock cannot time anything, //! and the arm line says so instead of resetting on a guess. -use crate::arch::{cpu, keyboard_controller}; +use crate::arch::cpu; use crate::drivers::serial; use crate::time::{Budget, Duration}; @@ -22,22 +20,24 @@ const PANIC_BOUND: Budget = Budget::of( "the machine returns itself to firmware instead of holding a panel nobody is reading", ); +/// `panic-reboot-fast`'s bound: a judge cannot spend the shipped minute per boot. +#[cfg(feature = "boot-actuators")] +const FAST_BOUND: Budget = Budget::of( + Duration::from_secs(5), + "a guest reaches the reset inside one test", +); + /// Whether a reboot is armed on this panic, and when. #[derive(Clone, Copy)] pub enum Bound { /// Reset the machine at this `cpu::counter` reading. At(u64), - /// Hold the panel: somebody is reading it, or nothing here could time a - /// wait, or this machine has no reset this kernel performs. + /// Hold the panel: nothing here could time a wait, or this machine has no + /// reset this kernel performs. Held, } impl Bound { - /// A key arrived: the machine is that person's from here on. - pub fn retire(&mut self) { - *self = Self::Held; - } - /// Reset the machine if the bound has passed; every wait on the panic path /// calls this, and it is the only place that decides the reset has come due. pub fn check(self) { @@ -93,25 +93,20 @@ fn deadline(bound: Budget) -> Option<(u64, Source)> { /// and on the console; false is for the reentry guard, whose suspect is the log /// path itself, and there the line goes to the UART raw and the panel carries none. pub fn arm(on_the_record: bool) -> Bound { + #[cfg(feature = "boot-actuators")] + let budget = if crate::actuator::panic_reboot_fast() { FAST_BOUND } else { PANIC_BOUND }; + #[cfg(not(feature = "boot-actuators"))] let budget = PANIC_BOUND; // ASCII only, here and in every line below: the panel's font renders // anything outside 0x20..=0x7E as a dot. let secs = budget.duration().millis() / 1_000; - // A key retires the bound only where the panic path reads one. - let unless = if keyboard_controller::PANIC_KEYS { " unless a key is pressed" } else { "" }; match (deadline(budget), crate::power::can_reboot()) { (Some((cycles, source)), true) => { if on_the_record { - alert!( - "{ARMED} in {secs} s{unless}, timed by {}", - source.named() - ); + alert!("{ARMED} in {secs} s, timed by {}", source.named()); } else { - let mut uart = serial::panic_registers(); - uart.write(b"panic: rebooting"); - uart.write(unless.as_bytes()); - uart.write(b"\n"); + serial::panic_registers().write(b"panic: rebooting\n"); } Bound::At(cycles) } @@ -144,11 +139,8 @@ pub fn arm(on_the_record: bool) -> Bound { /// Return the machine to firmware. The second of this path's two lines, and it /// goes out raw: the log has already been flushed and drained by here. pub fn reboot_now() -> ! { - serial::panic_registers().write(if keyboard_controller::PANIC_KEYS { - b"\npanic: no key inside the bound, so nobody is here: returning this machine to firmware\n" - } else { - b"\npanic: the bound is over: returning this machine to firmware\n" - }); + serial::panic_registers() + .write(b"\npanic: the bound is over: returning this machine to firmware\n"); // Not `power::reboot`: its flush waits on the console wire, and a CPU this // panic stopped may be holding it. crate::power::reset_now() diff --git a/kernel/src/pcidev/mod.rs b/kernel/src/pcidev/mod.rs index d87b5cd294d..055419ae1c4 100644 --- a/kernel/src/pcidev/mod.rs +++ b/kernel/src/pcidev/mod.rs @@ -94,7 +94,7 @@ /// No `crate::` reference, so `kernel-loom` compiles it and models the /// interleaving no guest test lands on. -mod record; +pub(crate) mod record; use alloc::string::String; use alloc::sync::Arc; diff --git a/kernel/src/process.rs b/kernel/src/process.rs index 54ef3ee91e4..7a6811c359e 100644 --- a/kernel/src/process.rs +++ b/kernel/src/process.rs @@ -1059,6 +1059,8 @@ fn teardown_resources( crate::arch::trap::log_unclaimed(); ops::close_all(&mut data.handles); + // Every thread has left, and none returns to Ring 3 to use them. + crate::isa::process_ends(pid); data.elf.elf_alloc.take(); data.elf.loaded_libs.clear(); data.mmap_regions.clear(); diff --git a/kernel/src/syscall/device.rs b/kernel/src/syscall/device.rs index b084c9b9439..85c5fd3202a 100644 --- a/kernel/src/syscall/device.rs +++ b/kernel/src/syscall/device.rs @@ -131,7 +131,7 @@ pub(super) fn sys_device_claim(syscap: RawHandle, class: u64, selector: [u64; 2] | device::DeviceType::HdaAudio | device::DeviceType::VirtioSound => 0, device::DeviceType::PciFunction => 1, - device::DeviceType::Partition => 2, + device::DeviceType::Partition | device::DeviceType::Isa => 2, }; if selector[read..].iter().any(|&word| word != 0) { return SyscallError::InvalidArgument.to_u64(); @@ -397,7 +397,8 @@ pub(super) fn sys_partition_transfer( | device::DeviceType::Framebuffer | device::DeviceType::HdaAudio | device::DeviceType::VirtioSound - | device::DeviceType::PciFunction => { + | device::DeviceType::PciFunction + | device::DeviceType::Isa => { drop(claim); return crate::object::HandleError::WrongType { held: class.class_name(), diff --git a/src/metal.rs b/src/metal.rs index 5686eff1d0e..74e5ffe901b 100644 --- a/src/metal.rs +++ b/src/metal.rs @@ -803,6 +803,12 @@ pub const FLASHABLE: &[&str] = &[ // writes no firmware state; the CPU rejoins, and the boot goes on to // userland and ends the way an unarmed one does. "dump-deaf-cpu", + // The kernel leaves the i8042 unprobed and a test process drives it through + // an `isa` claim: it reads the configuration byte, has the keyboard + // acknowledge `0xF4` and writes the byte back, each a command this kernel's + // own driver sends the controller on every boot. It reaches no firmware + // state, and firmware programs the controller again at the next power-on. + "i8042-withheld", ]; /// The arm that stops the machine, named once: [`FLASHABLE`] rules on it and diff --git a/tests/checks.rs b/tests/checks.rs index 1ff598bfb2d..675103f936d 100644 --- a/tests/checks.rs +++ b/tests/checks.rs @@ -292,7 +292,7 @@ mod checks { [nmi] rip=0xffffffff8012d3a0 rsp=0xffff80000017df50\n\ [nmi] the outer handler's frame is gone; the machine stops here.\n\ ining scheduler\n\ - [kernel 0.390 cpu0] panic: rebooting in 60 s unless a key is pressed\n"; + [kernel 0.390 cpu0] panic: rebooting in 60 s, timed by the calibrated clock\n"; const SPLICED: &str = "[[kenrnmel i0.38]5 cpNu1E] CSPUT 1E: Djo inNiMngI s choednule r\n\ c[pkeurn el0 0:.3 85a cp u1s] sechcedo: ncpud=1 rNeaMdyI=0 deyinngt=0e srtoeppded= 0 wpahrkield=0e c urrIentS=NTon2e trwipas=s1\n \ stil[lke rnieln 0 .3u87s cep.u1\n\ diff --git a/tests/common/faults.rs b/tests/common/faults.rs index bf0b63cb70c..4e83b9b2861 100644 --- a/tests/common/faults.rs +++ b/tests/common/faults.rs @@ -136,3 +136,32 @@ pub fn dump_nmi_probe_on_metal(kernel: &Serial) -> Result<(), String> { } Ok(()) } + +/// Where `test_rs_fault_gate_child`'s kernel arms aim: the direct map's first +/// words, which no process may name. +const KERNEL_RSP: &str = "0xffff800000000000"; +const KERNEL_RBP: &str = "0xffff800000000010"; +const KERNEL_READ: &str = "0xffff800000000008"; + +/// **A crash report reads a faulting process's memory only at user +/// addresses.** One of `test_rs_fault_gates`' children dies with its stack and +/// frame pointers aimed at the kernel's direct map, another reading there; the +/// kernel's records name each refusal, and carry neither a word from behind +/// them nor the kernel's page walk for the read. +pub fn crash_report_reads_no_kernel_memory(kernel: &Serial) -> Result<(), String> { + for refused in [ + format!("Stack (from RSP): {KERNEL_RSP} refused: no user address"), + format!("rbp {KERNEL_RBP} refused: no user address"), + format!("Page walk for {KERNEL_READ} refused: no user address"), + ] { + kernel.must_say(&refused)?; + } + // A stack word the report read is `[address] = value`, and the walk's + // header is `Page walk for address [PML4=…`. + let walk = format!("Page walk for {KERNEL_READ} ["); + let leaked = |l: &&str| (l.contains("[0xffff8") && l.contains("] = ")) || l.contains(&walk); + match kernel.text().lines().find(leaked) { + Some(line) => Err(format!("the kernel's records carry kernel memory a crash report read: {line:?}")), + None => Ok(()), + } +} diff --git a/tests/common/isa.rs b/tests/common/isa.rs new file mode 100644 index 00000000000..b433593b95e --- /dev/null +++ b/tests/common/isa.rs @@ -0,0 +1,47 @@ +//! The `isa` claim's rows on the T14: what the kernel said beside each job. +//! Every judge reads the kernel's own records, so the same predicate holds a +//! log however it reached the host. + +use super::serial::Serial; + +/// The kernel's line under `i8042-withheld`: the premise of every row that +/// needs the claim granted. +const WITHHELD: &str = "i8042: withheld, left unprobed for a claim"; + +/// How often `needle` is in `log`, which must be `want`. +fn said(log: &Serial, needle: &str, want: usize) -> Result<(), String> { + match log.text().matches(needle).count() { + seen if seen == want => Ok(()), + seen => Err(format!("the kernel said {needle:?} {seen} time(s), want {want}:\n{}", log.text())), + } +} + +/// `test_rs_isa_grant` on a boot whose i8042 the kernel left alone: every +/// access its children make that the kernel must refuse, named by the kernel +/// with the port and whose grant it is not. The unbound holder and the moved +/// claim both die at 0x64, the bound children one port past what they hold by +/// an `in` and by an `out`, the wide one on the port its access spans past the +/// grant, and the process holding nothing at the data port. +pub fn ports(kernel: &Serial) -> Result<(), String> { + kernel.must_say(WITHHELD)?; + const NAMED: [(&str, usize); 5] = [ + ("in of 1 byte(s) from port 0x0064", 2), + ("in of 1 byte(s) from port 0x0061", 1), + ("out of 1 byte(s) to port 0x0061", 1), + ("in of 1 byte(s) from port 0x0060", 1), + ("in of 2 byte(s) from port 0x0060, reaching port 0x0061", 1), + ]; + for (access, times) in NAMED { + said(kernel, &format!("{access}, which this process holds no grant for"), times)?; + } + kernel.must_say("isa: the i8042's ports are pid")?; + kernel.must_say("isa: the i8042's ports went back with pid")?; + Ok(()) +} + +/// `test_rs_isa_lines` on the same boot: one holder read a first interrupt off +/// its claim, and no later claim did. +pub fn lines(kernel: &Serial) -> Result<(), String> { + kernel.must_say(WITHHELD)?; + said(kernel, "isa: the i8042 took its first interrupt", 1) +} diff --git a/tests/common/mod.rs b/tests/common/mod.rs index d6c4b15489d..1e861455f0d 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -7,6 +7,8 @@ pub mod devices; pub mod faults; pub mod iommu; pub mod irqcensus; +/// The `isa` claim's rows on the T14. +pub mod isa; /// The cable: netd's address, and the T14 answering the host on it. pub mod lan; pub mod logstream; diff --git a/tests/common/qemu.rs b/tests/common/qemu.rs index 0b7917e87d4..8abc9cda9b0 100644 --- a/tests/common/qemu.rs +++ b/tests/common/qemu.rs @@ -661,10 +661,10 @@ fn words(monitor: &mut QmpMonitor, at: u64) -> Vec { /// /// Not a display setting: each variant is a whole machine. `Headless` is the /// historical test config -- no VGA and no GPU device at all, so firmware -/// publishes no GOP and `kernel_args.gop_framebuffer` is zero. `Gop` swaps in -/// `-vga std` so firmware publishes a linear framebuffer, which is the path a -/// laptop takes and the only one in which the on-screen panic console renders -/// anything. `Metal` goes the whole way to the target laptop's shape. +/// publishes no GOP and `kernel_args.gop_framebuffer` is zero. `Metal` is the +/// target laptop's shape, with `-vga std` so firmware publishes a linear +/// framebuffer, the only path in which the on-screen panic console renders +/// anything. #[derive(Clone, Copy, PartialEq)] pub enum Profile { Headless, @@ -674,7 +674,6 @@ pub enum Profile { /// `iommu_platform=on`, and the harness sets that only where a unit exists, /// so the guest's own negotiation comes out the other way here. HeadlessNoIommu, - Gop, /// M1 metal-sim: GOP, NVMe, xHCI with the boot stick on it, i8042 from /// q35, and nothing else -- no virtio device and no USB HID. This is the /// machine shape that gets flashed, so it is the one the input tests run @@ -711,7 +710,6 @@ impl Profile { Self::Virt | Self::VirtEl2 | Self::VirtEl2NoVhe | Self::VirtTcg => Arch::Aarch64, Self::Headless | Self::HeadlessNoIommu - | Self::Gop | Self::Metal => Arch::X86_64, } } @@ -870,16 +868,6 @@ impl Profile { nvme_bytes: NVME_SMALL, iommu: Some(IOMMU_DEFAULT), }, - Self::Gop => Shape { - vga: "std", - panel: None, - virtio: Virtio::Present, - nic: Nic::Virtio, - xhci: &[XHCI_DEFAULT], - usb: &["usb-kbd,bus=xhci.0"], - nvme_bytes: NVME_SMALL, - iommu: Some(IOMMU_DEFAULT), - }, Self::Metal => Shape { vga: "std", // The T14's panel, advertised the way the laptop's is: 240x67 @@ -1337,13 +1325,8 @@ impl QemuInstance { /// Screendump until the decoded screen carries `needle`, or the timeout. /// - /// Every fatal path needs this, for one of two reasons. The panic - /// handler's own path paints after the drain that emits the report, so a - /// marker on serial does not yet prove a paint. The halt_all_cpus paths - /// are the other way round and once *did* need only a single dump — but a - /// report too long for one screen now pages, so the screen a marker - /// proves is only the first of several and any given dump may hold a - /// different one. + /// The panic handler's own path paints after the drain that emits the + /// report, so a marker on serial does not yet prove a paint. pub fn screendump_until(&mut self, needle: &str, timeout: Duration) -> super::screen::Ppm { self.screendump_while(timeout, Duration::from_millis(100), |dump| { dump.text().contains(needle) diff --git a/tests/metal/lenovo-20w0003amz.toml b/tests/metal/lenovo-20w0003amz.toml index b57b3a97eda..21efc0c463b 100644 --- a/tests/metal/lenovo-20w0003amz.toml +++ b/tests/metal/lenovo-20w0003amz.toml @@ -13,6 +13,9 @@ bios = "N34ET71W (1.71 )" "boot.hardlockup.complete_ms" = 1164 "boot.hardlockup.panel_max_us" = 3949 "boot.hardlockup.panel_us" = 21772 +"boot.isa-withheld.complete_ms" = 780 +"boot.isa-withheld.panel_max_us" = 3888 +"boot.isa-withheld.panel_us" = 19251 "boot.jobcase.complete_ms" = 1164 "boot.jobcase.panel_max_us" = 3873 "boot.jobcase.panel_us" = 21418 diff --git a/tests/toyos-rust-tests/src/arch/port.rs b/tests/toyos-rust-tests/src/arch/port.rs new file mode 100644 index 00000000000..a1bad6c1c6c --- /dev/null +++ b/tests/toyos-rust-tests/src/arch/port.rs @@ -0,0 +1,31 @@ +//! `in` and `out` from Ring 3, on the one architecture with a port space. + +#[cfg(target_arch = "x86_64")] +pub use x86_64::*; + +#[cfg(target_arch = "x86_64")] +mod x86_64 { + /// `in al, dx`, or `in ax, dx` when `wide`, which spans `port` and the port + /// after it. A port this process holds no grant for faults it. + pub fn port_in(port: u16, wide: bool) -> u16 { + let value: u16; + // SAFETY: an `in` has no memory effect. + unsafe { + if wide { + core::arch::asm!("in ax, dx", in("dx") port, out("ax") value, options(nomem, nostack)); + } else { + core::arch::asm!("in al, dx", in("dx") port, out("ax") value, options(nomem, nostack)); + } + } + if wide { value } else { value & 0xFF } + } + + /// `out dx, al`. A port this process holds no grant for faults it. + pub fn port_out(port: u16, value: u8) { + // SAFETY: an `out` has no memory effect; what the device does with the + // byte is the caller's. + unsafe { + core::arch::asm!("out dx, al", in("dx") port, in("al") value, options(nomem, nostack)); + } + } +} diff --git a/tests/toyos-rust-tests/src/arch/stack.rs b/tests/toyos-rust-tests/src/arch/stack.rs new file mode 100644 index 00000000000..dce07fb2e57 --- /dev/null +++ b/tests/toyos-rust-tests/src/arch/stack.rs @@ -0,0 +1,22 @@ +//! A fault taken with the stack registers aimed where the process chose. + +#[cfg(target_arch = "x86_64")] +pub use x86_64::*; + +#[cfg(target_arch = "x86_64")] +mod x86_64 { + /// #UD with `rsp` and `rbp` set first, so the fault's report meets both. + pub fn undefined_with_stack_at(rsp: u64, rbp: u64) -> ! { + // SAFETY: none — the fault is the point, and it ends this process. + unsafe { + core::arch::asm!( + "mov rsp, {s}", + "mov rbp, {f}", + "ud2", + s = in(reg) rsp, + f = in(reg) rbp, + options(noreturn), + ); + } + } +} diff --git a/tests/toyos-rust-tests/src/bin/fault_gate_child.rs b/tests/toyos-rust-tests/src/bin/fault_gate_child.rs index c1c9c10cb6e..ac93be52820 100644 --- a/tests/toyos-rust-tests/src/bin/fault_gate_child.rs +++ b/tests/toyos-rust-tests/src/bin/fault_gate_child.rs @@ -8,6 +8,9 @@ //! so a surviving arm is evidence rather than a shrug: the CPU either never //! flagged the condition, or flagged it and declined to trap. +#[path = "../arch/stack.rs"] +mod stack; + fn main() { let kind = std::env::args().nth(1).expect("usage: fault_gate_child "); match kind.as_str() { @@ -19,6 +22,8 @@ fn main() { "xm" => simd_exception(), "ac" => alignment_check(), "pf" => read_null(), + "kernel_stack" => kernel_stack(), + "kernel_page" => kernel_page(), other => panic!("unknown fault kind {other}"), } println!("survived {kind}"); @@ -177,3 +182,20 @@ fn alignment_check() { } println!(" RFLAGS.AC readback after a misaligned load: {}", (flags >> 18) & 1); } + +/// The kernel's direct map starts here (`kernel/src/mm/mod.rs`'s +/// `PHYS_OFFSET`): the first byte of physical memory, and no process's. +const DIRECT_MAP: u64 = 0xFFFF_8000_0000_0000; + +/// #UD (6) with the stack and frame pointers aimed at the direct map, so a +/// crash report that followed either would print physical memory. +fn kernel_stack() { + stack::undefined_with_stack_at(DIRECT_MAP, DIRECT_MAP + 0x10) +} + +/// #PF (14) at a direct-map address, whose page walk is the kernel's tables. +#[inline(never)] +fn kernel_page() { + // SAFETY: none — the fault is the point, and it ends this process. + unsafe { core::ptr::read_volatile((DIRECT_MAP + 8) as *const u64) }; +} diff --git a/tests/toyos-rust-tests/src/bin/fault_gates.rs b/tests/toyos-rust-tests/src/bin/fault_gates.rs index 2d8d885e3fe..4720b7973ef 100644 --- a/tests/toyos-rust-tests/src/bin/fault_gates.rs +++ b/tests/toyos-rust-tests/src/bin/fault_gates.rs @@ -42,6 +42,11 @@ const ARMS: &[(&str, Expect)] = &[ // on any machine this kernel runs on — emulated or not. ("ac", Expect::MachineLives), ("pf", Expect::Killed), + // Each dies with the kernel's direct map in a register or an operand its + // crash report would follow; `crash_report_reads_no_kernel_memory` reads + // what the report said. + ("kernel_stack", Expect::Killed), + ("kernel_page", Expect::Killed), ]; fn main() { diff --git a/tests/toyos-rust-tests/src/bin/isa_claim_refused.rs b/tests/toyos-rust-tests/src/bin/isa_claim_refused.rs new file mode 100644 index 00000000000..77546a24f04 --- /dev/null +++ b/tests/toyos-rust-tests/src/bin/isa_claim_refused.rs @@ -0,0 +1,23 @@ +//! Where the kernel drives the i8042, an `isa` claim on it is refused: two +//! drivers on one controller is not something a claim may create. + +use toyos::endow::Endowments; +use toyos::syscap::SysCap; +use toyos::Device; +use toyos_abi::syscall::{IsaId, SyscallError, SYSCAP_LABEL}; + +fn main() { + let cap: SysCap = Endowments::get() + .take(SYSCAP_LABEL) + .expect("the test estate is endowed a device-minting capability"); + let set = IsaId::parse("0060,0064:1,12").expect("the i8042's set"); + match cap.claim_isa::(set) { + Err(SyscallError::PermissionDenied) => {} + other => panic!( + "a claim on the i8042 answered {:?}, not PermissionDenied: the kernel does not drive \ + the controller here, or let a claim share it", + other.map(|_| ()) + ), + } + println!("the claim on a controller the kernel drives was refused PermissionDenied"); +} diff --git a/tests/toyos-rust-tests/src/bin/isa_grant.rs b/tests/toyos-rust-tests/src/bin/isa_grant.rs new file mode 100644 index 00000000000..309bdb76b2e --- /dev/null +++ b/tests/toyos-rust-tests/src/bin/isa_grant.rs @@ -0,0 +1,232 @@ +//! The `isa` claim's ports: the i8042's two, opened in the I/O permission +//! bitmap for the one process that bound them and for no other. +//! +//! Run where the kernel drives no i8042 (`i8042-withheld`), so the claim is +//! granted. Every port access that must be refused is made in a child of its +//! own, which prints a marker, makes the one access and must never print again; +//! the kernel's record of each kill is the harness's to read. +//! +//! The children: `unbound` holds the claim and never read it, `bound` read it +//! and steps one port past what it was granted, `wide` read it and makes a +//! two-byte access at its data port, `out` read it and writes one port past +//! the grant, `unclaimed` holds nothing, and `moved` was handed a claim its +//! parent had already bound and watches from its own ring. + +use std::os::toyos::process::CommandExt; +use std::process::{Command, Stdio}; +use std::time::Duration; + +use toyos::endow::Endowments; +use toyos::poller::{Poller, READABLE}; +use toyos::syscap::SysCap; +use toyos::Device; +use toyos_abi::pci::DeviceIrqRecord; +use toyos_abi::syscall::{IsaId, SyscallError, SYSCAP_LABEL}; + +#[path = "../isa_row.rs"] +mod isa_row; +#[path = "../arch/port.rs"] +mod port; + +use port::{port_in, port_out}; + +const SELF_PATH: &str = "/system/bin/test_rs_isa_grant"; +const CLAIM_LABEL: &str = "isa-claim"; + +/// The i8042's data and command ports, keyboard and aux lines. +const I8042: &str = "0060,0064:1,12"; +const DATA: u16 = 0x60; +const STATUS: u16 = 0x64; +/// The port just past the data port, which no row names. +const PAST: u16 = 0x61; + +/// The ceiling on a watch being answered once its claim has ended. +const ANSWER: Duration = Duration::from_secs(5); +/// More records than a controller nothing here asks anything of raises. +const STALE_RECORDS: usize = 8; + +fn main() { + match std::env::args().nth(1).as_deref() { + None => grant(&syscap()), + Some("unbound") => unbound(), + Some("bound") => bound(), + Some("wide") => wide(), + Some("out") => out(), + Some("unclaimed") => unclaimed(), + Some("moved") => moved(), + other => panic!("isa_grant: unknown role {other:?}"), + } +} + +fn syscap() -> SysCap { + Endowments::get() + .take(SYSCAP_LABEL) + .expect("the test estate is endowed a device-minting capability") +} + +fn set(text: &str) -> IsaId { + IsaId::parse(text).unwrap_or_else(|| panic!("{text:?} is no ISA set")) +} + +fn claim(cap: &SysCap, text: &str) -> Result { + cap.claim_isa(set(text)) +} + +/// Read the claim's description, which binds its ports to this process, and +/// check it names what was claimed. +fn bind(claim: &Device) { + let mut words = [0u8; 16]; + let n = claim.read(&mut words).expect("isa: the claim's first read is its description"); + assert_eq!(n, words.len(), "isa: a description of {n} bytes"); + let wire = [ + u64::from_ne_bytes(words[..8].try_into().expect("eight bytes")), + u64::from_ne_bytes(words[8..].try_into().expect("eight bytes")), + ]; + assert_eq!(IsaId::from_wire(wire), Some(set(I8042)), "isa: the description names another set"); +} + +/// Spawn `role` holding `claim`, or nothing; answers its stdout and whether it +/// exited cleanly. +fn child(role: &str, claim: Option) -> (String, bool) { + let mut command = Command::new(SELF_PATH); + command.arg(role).stdout(Stdio::piped()); + if let Some(claim) = claim { + command.endow(CLAIM_LABEL, claim.into_raw().0); + } + let out = command.output().expect("isa: spawn a child role"); + (String::from_utf8_lossy(&out.stdout).into_owned(), out.status.success()) +} + +/// A child that must have printed `marker` and then died at the access after it. +fn killed_after(role: &str, (said, clean): (String, bool), marker: &str) { + assert!(said.contains(marker), "isa: {role} never reached its access: {said:?}"); + assert!(!clean && !said.contains("survived"), "isa: {role} survived its access: {said:?}"); + println!("isa: {role} was ended at {marker:?}"); +} + +fn grant(cap: &SysCap) { + // Nothing but a whole row is a grant: a subset, a missing line and a + // neighbouring port are each no function this machine can hand out. + for part in ["0060:1", "0060,0064:1", "0061,0064:1,12", "0064:12"] { + match claim(cap, part) { + Err(SyscallError::NotFound) => {} + other => panic!("isa: {part:?} answered {:?}, want NotFound", other.map(|_| ())), + } + } + println!("isa: a subset, a missing line and a neighbouring port were each refused NotFound"); + + let held = claim(cap, I8042).expect("isa: the i8042 row is free where the kernel drives none"); + match claim(cap, I8042) { + Err(SyscallError::AlreadyExists) => {} + other => panic!("isa: a second claim answered {:?}, want AlreadyExists", other.map(|_| ())), + } + killed_after("unbound", child("unbound", Some(held)), "unbound: in from 0x64"); + + // The claim died with its unbound holder, so the row comes back; each + // bound child's ports go back with it. + let mut holder = "unbound"; + for (role, marker) in [ + ("bound", "bound: in from 0x61"), + ("wide", "wide: in of two bytes from 0x60"), + ("out", "out: out to 0x61"), + ] { + let held = isa_row::claim_after(cap, set(I8042), holder); + killed_after(role, child(role, Some(held)), marker); + holder = role; + } + killed_after("unclaimed", child("unclaimed", None), "unclaimed: in from 0x60"); + + let held = isa_row::claim_after(cap, set(I8042), holder); + bind(&held); + let status = port_in(STATUS, false); + println!("isa: bound here, status reads {status:#04x}"); + let poller = Poller::new(1); + watch(&poller, &held); + killed_after("moved", child("moved", Some(held)), "moved: in from 0x64"); + // The claim ended with the child that held it, which answers this ring's + // watch on it: no later holder's interrupt is this process's to hear of. + let mut answered = false; + poller.wait(1, ANSWER.as_nanos() as u64, |_| answered = true); + assert!(answered, "isa: a watch on a claim that ended in another process was never answered"); + // The claim is gone with the child, and the ports are still this process's. + match claim(cap, I8042) { + Err(SyscallError::AlreadyExists) => {} + other => panic!( + "isa: a claim while this process holds the bound ports answered {:?}", + other.map(|_| ()) + ), + } + let _ = port_in(STATUS, false); + println!("isa: a moved claim carries nothing, and the ports stay with the process that bound them"); +} + +/// Arm `poller`'s watch on `claim`, which this process has bound, with no +/// record behind it: one a stray byte raised is read away and the watch armed +/// again. +fn watch(poller: &Poller, claim: &Device) { + for _ in 0..STALE_RECORDS { + poller.watch(claim, READABLE, 0); + let mut ready = false; + poller.wait(0, 0, |_| ready = true); + if !ready { + return; + } + let mut record = [0u8; DeviceIrqRecord::SIZE]; + claim.read(&mut record).expect("isa: a readable claim reads its record"); + } + panic!("isa: the claim was readable {STALE_RECORDS} times with nothing asked of the controller"); +} + +fn taken() -> Device { + Endowments::get().take(CLAIM_LABEL).expect("isa: this role is endowed the claim") +} + +fn unbound() { + let _claim = taken(); + println!("unbound: in from 0x64"); + let _ = port_in(STATUS, false); + println!("unbound: survived"); +} + +fn bound() { + let claim = taken(); + bind(&claim); + println!("bound: 0x64 read {:#04x}", port_in(STATUS, false)); + println!("bound: in from 0x61"); + let _ = port_in(PAST, false); + println!("bound: survived"); +} + +fn wide() { + let claim = taken(); + bind(&claim); + println!("wide: in of two bytes from 0x60"); + let _ = port_in(DATA, true); + println!("wide: survived"); +} + +fn out() { + let claim = taken(); + bind(&claim); + println!("out: out to 0x61"); + port_out(PAST, 0); + println!("out: survived"); +} + +fn unclaimed() { + println!("unclaimed: in from 0x60"); + let _ = port_in(DATA, false); + println!("unclaimed: survived"); +} + +fn moved() { + let claim = taken(); + let mut buf = [0u8; 16]; + match claim.read(&mut buf) { + Err(SyscallError::PermissionDenied) => println!("moved: its read was refused"), + other => panic!("moved: a claim bound to another process answered {other:?}"), + } + println!("moved: in from 0x64"); + let _ = port_in(STATUS, false); + println!("moved: survived"); +} diff --git a/tests/toyos-rust-tests/src/bin/isa_lines.rs b/tests/toyos-rust-tests/src/bin/isa_lines.rs new file mode 100644 index 00000000000..aa8497c5838 --- /dev/null +++ b/tests/toyos-rust-tests/src/bin/isa_lines.rs @@ -0,0 +1,239 @@ +//! The `isa` claim's lines: an interrupt the i8042 raises reaches the claim's +//! holder as a record, and one it raises once the claim is gone reaches nobody. +//! +//! Run where the kernel drives no i8042 (`i8042-withheld`), with no key to +//! press: the keyboard is told to enable scanning (`0xF4`) and its +//! acknowledgement (`0xFA`) is the byte that raises the line. +//! +//! - `device` holds the claim: it drives the controller until the +//! acknowledgement arrives as a record and a byte, gives the claim up, and +//! has the keyboard acknowledge once more, on a line then masked. +//! - `after` holds the next claim on the row, and finds no record on it. + +use std::os::toyos::process::CommandExt; +use std::process::Command; +use std::time::{Duration, Instant}; + +use toyos::endow::Endowments; +use toyos::poller::{Poller, READABLE}; +use toyos::syscap::SysCap; +use toyos::{AsHandle, Device}; +use toyos_abi::pci::DeviceIrqRecord; +use toyos_abi::syscall::{self, IsaId, SyscallError, SYSCAP_LABEL}; + +#[path = "../isa_row.rs"] +mod isa_row; +#[path = "../arch/port.rs"] +mod port; + +use port::{port_in, port_out}; + +const SELF_PATH: &str = "/system/bin/test_rs_isa_lines"; +const CLAIM_LABEL: &str = "isa-claim"; + +/// The i8042's data and command ports, keyboard and aux lines. +const I8042: &str = "0060,0064:1,12"; +const DATA: u16 = 0x60; +const STATUS: u16 = 0x64; + +const OBF: u8 = 1 << 0; +const IBF: u8 = 1 << 1; + +/// The controller's configuration byte: its keyboard interrupt, and the bits +/// that stop the keyboard's clock and the aux port's. +const CFG_KEYBOARD_IRQ: u8 = 1 << 0; +const CFG_KEYBOARD_CLOCK_OFF: u8 = 1 << 4; +const CFG_AUX_CLOCK_OFF: u8 = 1 << 5; + +/// The keyboard's enable-scanning command, and its acknowledgement. +const ENABLE_SCANNING: u8 = 0xF4; +const ACK: u8 = 0xFA; + +/// A controller answers a command, and a keyboard acknowledges one, in +/// milliseconds; this is the liveness ceiling on one that does not, and it +/// panics by name. +const CONTROLLER: Duration = Duration::from_secs(1); +/// The ceiling on the record behind an acknowledgement. +const RECORD: Duration = Duration::from_secs(5); +/// More records than the holder's own setup can raise before it asks the +/// keyboard anything: one byte answers the configuration read. +const STALE_RECORDS: usize = 8; + +fn main() { + match std::env::args().nth(1).as_deref() { + None => lines(&syscap()), + Some("device") => device(taken()), + Some("after") => after(taken()), + other => panic!("isa_lines: unknown role {other:?}"), + } +} + +fn syscap() -> SysCap { + Endowments::get() + .take(SYSCAP_LABEL) + .expect("the test estate is endowed a device-minting capability") +} + +fn taken() -> Device { + Endowments::get().take(CLAIM_LABEL).expect("isa: this role is endowed the claim") +} + +fn set() -> IsaId { + IsaId::parse(I8042).expect("the i8042's set") +} + +/// Run `role` holding `claim`; it must end cleanly. +fn role(role: &str, claim: Device) { + let status = Command::new(SELF_PATH) + .arg(role) + .endow(CLAIM_LABEL, claim.into_raw().0) + .status() + .expect("isa: spawn a role"); + assert!(status.success(), "isa lines: {role} ended {:?}", status.code()); +} + +fn lines(cap: &SysCap) { + role("device", isa_row::claim_after(cap, set(), "the job before this one")); + // The row comes back with `device`, whose keyboard acknowledged once more + // after its claim was gone. + role("after", isa_row::claim_after(cap, set(), "device")); + println!("isa lines: the holder's interrupt reached it, and the one after its claim reached nobody"); +} + +fn inb(port: u16) -> u8 { + port_in(port, false) as u8 +} + +/// Read the claim's description, which binds its ports to this process. +fn bind(claim: &Device) { + let mut words = [0u8; 16]; + let n = claim.read(&mut words).expect("isa: the claim's first read is its description"); + assert_eq!(n, words.len(), "isa: a description of {n} bytes"); +} + +/// Spin on the status register until `ready`, or panic naming `what`. +fn wait_status(what: &str, ready: impl Fn(u8) -> bool) { + let by = Instant::now() + CONTROLLER; + while !ready(inb(STATUS)) { + assert!(Instant::now() < by, "isa device: the controller never {what} in {CONTROLLER:?}"); + std::hint::spin_loop(); + } +} + +fn command(byte: u8) { + wait_status("took a command", |s| s & IBF == 0); + port_out(STATUS, byte); +} + +fn data(byte: u8) { + wait_status("took a data byte", |s| s & IBF == 0); + port_out(DATA, byte); +} + +/// Whatever the output buffer holds, read away. +fn drain() { + for _ in 0..32 { + if inb(STATUS) & OBF == 0 { + return; + } + let _ = inb(DATA); + } + panic!("isa device: the output buffer never drained"); +} + +/// Have the keyboard acknowledge, and read the acknowledgement once it is in +/// the output buffer. +fn acknowledged(when: &str) { + data(ENABLE_SCANNING); + wait_status("had the keyboard's acknowledgement", |s| s & OBF != 0); + let byte = inb(DATA); + assert_eq!(byte, ACK, "isa device: {when}, the keyboard answered {byte:#04x}, not its acknowledgement"); +} + +/// The record on `claim` if there is one, without waiting. +fn record(claim: &Device) -> Option { + let mut record = [0u8; DeviceIrqRecord::SIZE]; + match syscall::read_nonblock(claim.as_handle(), &mut record) { + Ok(n) => { + assert_eq!(n, DeviceIrqRecord::SIZE, "isa: a record of {n} bytes"); + Some(u32::from_ne_bytes(record)) + } + Err(SyscallError::WouldBlock) => None, + Err(other) => panic!("isa: a bound claim's read answered {other:?}"), + } +} + +/// Arm `poller`'s watch on `claim` with no record behind it. Where the +/// controller came with its keyboard interrupt on, the configuration's own +/// answer raised the line: a record of that is read away and the watch armed +/// again. +fn arm(poller: &Poller, claim: &Device) { + for _ in 0..STALE_RECORDS { + let _ = record(claim); + poller.watch(claim, READABLE, 0); + let mut ready = false; + poller.wait(0, 0, |_| ready = true); + if !ready { + return; + } + } + panic!("isa device: the claim was readable {STALE_RECORDS} times with nothing asked of the keyboard"); +} + +/// Drive the controller through `claim` until the keyboard's acknowledgement +/// arrives as a record and a byte, then give the claim up and have it +/// acknowledge again. +fn device(claim: Device) { + bind(&claim); + drain(); + // Read the configuration byte, then write it back with the keyboard's line + // on and its clock running, and the aux port's clock stopped so that every + // byte from here on is the keyboard's (i8042: 0x20 reads it, 0x60 writes + // it, 0xAE enables the keyboard's port). + command(0x20); + wait_status("answered its configuration", |s| s & OBF != 0); + let config = inb(DATA); + command(0x60); + data((config | CFG_KEYBOARD_IRQ | CFG_AUX_CLOCK_OFF) & !CFG_KEYBOARD_CLOCK_OFF); + command(0xAE); + drain(); + println!("isa device: config {config:#04x}, keyboard line on"); + + // Armed before the keyboard is asked anything, so the watch is completed + // by a post from the line's handler and by nothing this thread reads. + let poller = Poller::new(1); + arm(&poller, &claim); + data(ENABLE_SCANNING); + let mut woke = false; + poller.wait(1, RECORD.as_nanos() as u64, |_| woke = true); + assert!(woke, "isa device: no record in {RECORD:?} of the keyboard's acknowledgement"); + let count = record(&claim).expect("isa device: a readable claim reads a record"); + assert!(count >= 1, "isa device: a record counting {count}"); + // The byte is the holder's: nothing else may have read it from under the record. + wait_status("had the byte behind its interrupt", |s| s & OBF != 0); + let byte = inb(DATA); + assert_eq!(byte, ACK, "isa device: the interrupt carried {byte:#04x}, not the acknowledgement"); + println!("isa device: {count} interrupt(s), the keyboard's acknowledgement behind them"); + + // The claim goes and the ports stay this process's: the keyboard's next + // acknowledgement raises a line no claim holds. + drop(claim); + acknowledged("with its claim gone"); + // The configuration the controller came with. + command(0x60); + data(config); + println!("isa device: the keyboard acknowledged once more after the claim went"); +} + +/// The claim after `device`'s: nothing raised the line under it, so it holds +/// no record. +fn after(claim: Device) { + bind(&claim); + if let Some(count) = record(&claim) { + panic!( + "isa after: a claim nothing raised a line under carries a record of {count}: the line \ + was open while no claim held it" + ); + } + println!("isa after: no record on a claim nothing raised a line under"); +} diff --git a/tests/toyos-rust-tests/src/isa_row.rs b/tests/toyos-rust-tests/src/isa_row.rs new file mode 100644 index 00000000000..26b3235a18f --- /dev/null +++ b/tests/toyos-rust-tests/src/isa_row.rs @@ -0,0 +1,29 @@ +//! The `isa` row once its last holder has given it back. +//! +//! A holder's exit is not its claim's release having run: the release is a +//! deferred hook another CPU can still be inside when `wait` returns +//! (`issues/kernel/deferred-release-outlives-its-syscall.md`). So the next +//! claim is asked for until it is granted. + +use std::time::{Duration, Instant}; + +use toyos::syscap::SysCap; +use toyos::Device; +use toyos_abi::syscall::{IsaId, SyscallError}; + +/// The liveness ceiling on a release in flight, and it panics by name. +const RELEASED: Duration = Duration::from_secs(5); + +/// The claim on `set`, once `holder`, which held the row before, has let it go. +pub fn claim_after(cap: &SysCap, set: IsaId, holder: &str) -> Device { + let by = Instant::now() + RELEASED; + loop { + match cap.claim_isa(set) { + Ok(claim) => return claim, + Err(SyscallError::AlreadyExists) => {} + Err(other) => panic!("isa: the claim after {holder} answered {other:?}"), + } + assert!(Instant::now() < by, "isa: the row never came back from {holder} in {RELEASED:?}"); + std::thread::yield_now(); + } +} diff --git a/tests/toyos.rs b/tests/toyos.rs index 7ccdc6930f9..df7324f8e70 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -14,7 +14,7 @@ use common::qemu::{ self, await_guest, await_marker, BootOptions, QemuInstance, STALLED, TIMED_OUT, }; -use common::{audio, compile, devices, faults, lan, metal, power, screen, serial, usb}; +use common::{audio, compile, devices, faults, isa, lan, metal, power, screen, serial, usb}; use toyos_build::bootlog::{self}; use toyos_build::testargs::{self, SUITE}; @@ -88,6 +88,11 @@ const RUST_SKIP: &[&str] = &[ "ccheck", "disk_backtrace_child", "fault_gate_child", + // Each needs a boot whose i8042 the kernel does not drive: the + // `isa_ports_are_the_binders_alone` and `isa_lines_reach_their_holder` + // metal rows run them. + "isa_grant", + "isa_lines", // It takes the machine down; `virt_fatal_halts_the_others_first` runs it. "panic_halts_first", // Needs a launcher and a declared `cat` and shell, which `tests/testcases` @@ -129,6 +134,9 @@ const DRIVEN_AND_SHARED: &[&str] = &[ // Its shared run is the x86-64 verdict; `virt_readonly_copyout` builds it // for AArch64 and runs it on that architecture's job case. "abuse_readonly_copyout", + // Its shared run asserts every arm's kill; `crash_report_reads_no_kernel_memory` + // reads what the kernel said of two of them. + "fault_gates", "sched_stress", "std_alloc", ]; @@ -146,8 +154,9 @@ const SCREEN_TESTS: &[(&str, qemu::Profile)] = &[ ("screen_panic_muted", qemu::Profile::Metal), // The same fatal path from inside Ctrl+Alt+D's report painter, holding the // panel's latch it will never give back: the report has to take the screen - // anyway, and its CPU has to go on to watch the reset bound. - ("screen_fatal_behind_a_painter", qemu::Profile::Gop), + // anyway, and its CPU has to go on to watch the reset bound. The profile + // whose 16550 is the console, where the fatal path writes its last line raw. + ("screen_fatal_behind_a_painter", qemu::Profile::Metal), // The same fatal path with a compositor holding the panel, which is the // only configuration the owner's laptop is ever in. ("screen_fatal_halt_composited", qemu::Profile::Metal), @@ -630,8 +639,53 @@ const METAL: &[(&str, metal::Metal)] = &[ judge: |b| operation_nesting_log(b[0].kernel().text()), }, ), + // ---- the `isa` claim: one image whose i8042 the kernel leaves alone ---- + ( + // The I/O permission bitmap on the machine's own processor: the ports + // open to the process that bound them, and every other access killed + // by name. + "isa_ports_are_the_binders_alone", + metal::Metal { + arms: ISA_WITHHELD, + judge: |b| { + b[0].job_passed("test_rs_isa_grant")?; + isa::ports(&b[0].kernel()) + }, + }, + ), + ( + // The machine's own controller raises its line through the I/O APIC to + // the claim's holder, and to nobody once the claim is gone. + "isa_lines_reach_their_holder", + metal::Metal { + arms: ISA_WITHHELD, + judge: |b| { + b[0].job_passed("test_rs_isa_lines")?; + isa::lines(&b[0].kernel()) + }, + }, + ), + ( + "crash_report_reads_no_kernel_memory", + metal::Metal { + arms: &[metal::once("testcases", "tests/testcases", &[], &["test_rs_fault_gates"])], + judge: |b| { + b[0].job_passed("test_rs_fault_gates")?; + faults::crash_report_reads_no_kernel_memory(&b[0].kernel()) + }, + }, + ), ]; +/// A boot whose kernel leaves the i8042 unprobed, so the one grantable row is +/// free: the ports' job first, since its last holder keeps them until it ends. +const ISA_WITHHELD: &[metal::Arm] = &[metal::once( + "isa-withheld", + "tests/testcases", + &["i8042-withheld"], + &["test_rs_isa_grant", "test_rs_isa_lines"], +)]; + /// The boot most of the first tranche rides: the plain `tests/testcases` shape /// with a job list that ends it. const TESTCASES: &[metal::Arm] = &[metal::once( @@ -1816,12 +1870,11 @@ fn run_screen_test(name: &str, profile: qemu::Profile, test_config: &Path) -> Re print_screen(name, &text); // The arm line is here and nowhere else: this is the machine whose // panel is its only account, so it is the only one whose capture - // `halt_all_cpus` refreshes to carry it. Newest record, so it sits - // at the foot of the same `Page::Last` the two lines above are on. + // `halt_all_cpus` refreshes to carry it. // The bound is derived: a panel promising a minute while the kernel // counts something else is the failure this line exists to catch. let armed = format!( - "panic: rebooting in {} s unless a key is pressed", + "panic: rebooting in {} s, timed by ", toyos_tco::PANIC_BOUND_MS / 1_000 ); for want in ["PANIC:", "test-late-panic: on-screen console check", &armed] { @@ -2039,8 +2092,11 @@ fn run_screen_test(name: &str, profile: qemu::Profile, test_config: &Path) -> Re // go fatal once it holds the latch, so the fatal path meets a // holder beneath itself; the report must take the screen // regardless, and its CPU must go on to watch the reset bound, - // which is what the paging proves. + // which is what the reset proves. const HELD: &str = "panel: a painter holding the panel went fatal"; + /// `panic_reboot::reboot_now`'s line, raw on the 16550, which is + /// this profile's console. + const BOUND_OVER: &str = "panic: the bound is over"; let mut qemu = QemuInstance::boot_with_options( test_config, &[], @@ -2048,7 +2104,7 @@ fn run_screen_test(name: &str, profile: qemu::Profile, test_config: &Path) -> Re BootOptions { profile, qmp: true, - kernel_params: &["panel-painter-stalls"], + kernel_params: &["panel-painter-stalls", "panic-reboot-fast"], ..Default::default() }, ); @@ -2073,15 +2129,17 @@ fn run_screen_test(name: &str, profile: qemu::Profile, test_config: &Path) -> Re dump.fill() )); } - // The pager runs only on the CPU that claimed the panel, and it is - // the loop that watches the reset bound: a second page is its proof. - let paged = qemu.screendump_while(Duration::from_secs(87), Duration::from_millis(200), |d| { - d.rows().iter().any(|r| r.contains("[page ")) && d.text() != text - }); - if paged.text() == text { + // Only the CPU that claimed the panel watches the reset bound, so + // the machine resetting at it is that CPU's proof: QEMU exits on a + // reset, and the fatal path's last line says whose reset it was. + let by = qemu.budget(Duration::from_secs(15)); + let said = qemu.await_exit(by).map_err(|why| { + format!("no CPU is watching the reset bound: {why}\ndecoded screen:\n{text}") + })?; + if !said.contains(BOUND_OVER) { return Err(format!( - "the report never paged, so no CPU is watching the reset bound\ndecoded \ - screen:\n{text}" + "QEMU exited without {BOUND_OVER:?} on the console, so this was not the panic \ + path's reset\n{said}" )); } Ok(()) @@ -2124,8 +2182,7 @@ fn run_screen_test(name: &str, profile: qemu::Profile, test_config: &Path) -> Re return Err("the compositor never took the screen".to_string()); } - // The probe fires 5 s after the claim; the poll is for that plus - // the pager cycling pages. + // The probe fires 5 s after the claim; the poll is for that. const MARKER: &str = "metal-panic-probe"; let dump = qemu.screendump_while( Duration::from_secs(40), diff --git a/toyos-abi/src/syscall.rs b/toyos-abi/src/syscall.rs index a11bbad0d5b..e970422f24e 100644 --- a/toyos-abi/src/syscall.rs +++ b/toyos-abi/src/syscall.rs @@ -1229,6 +1229,14 @@ device_classes! { /// Like `pci`, a class whose name is not the whole of the entry — /// `part:` — and [`DeviceRequest`] is the one parser. Partition = 8 => "part", + /// One legacy ISA function, driven by whoever holds the claim: the process + /// that first reads the claim may `in` and `out` the function's ports for + /// the rest of its life and no other process may, and the function's lines + /// are answered as interrupt records on the claim ([`IsaId`]). + /// + /// Like `pci`, a class whose name is not the whole of the entry — + /// `isa:,…:,…` — and [`DeviceRequest`] is the one parser. + Isa = 9 => "isa", } /// A PCI function named by what identifies the *card*, not the slot firmware @@ -1285,9 +1293,134 @@ fn hex16(text: &str) -> Option { Some(value) } +/// A legacy ISA function named by everything it decodes and raises: +/// `isa:0060,0064:1,12` is the i8042's two ports and its keyboard and aux +/// lines. +/// +/// **The name is the grant**, so it is exact: the kernel opens these ports and +/// routes these lines, and a set that is not one function it can hand out +/// whole is refused rather than trimmed to one it can. One spelling per set — +/// ports as four lowercase hex digits, lines in decimal with no leading zero, +/// each list strictly ascending — because every reader compares the string. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct IsaId { + /// Ascending and nonzero, then zeros: port 0 is the DMA controller's and + /// never a function a process drives, which is what lets 0 mean no port. + ports: [u16; IsaId::MAX_PORTS], + /// One bit per ISA line. + irqs: u16, +} + +impl IsaId { + pub const MAX_PORTS: usize = 4; + /// Bounded so the longest name fits [`DeviceRequest::MAX_NAME`]. + pub const MAX_IRQS: usize = 4; + /// The ISA bus has sixteen lines. + const LINES: u8 = 16; + + /// `None` for a set with no port or no line, a port 0, a line past 15, a + /// list longer than its bound, or a list not strictly ascending. + pub fn new(ports: &[u16], irqs: &[u8]) -> Option { + if ports.is_empty() || ports.len() > Self::MAX_PORTS || ports[0] == 0 || !ascending(ports) { + return None; + } + if irqs.is_empty() || irqs.len() > Self::MAX_IRQS || !ascending(irqs) { + return None; + } + let mut set = Self { ports: [0; Self::MAX_PORTS], irqs: 0 }; + set.ports[..ports.len()].copy_from_slice(ports); + for &irq in irqs { + if irq >= Self::LINES { + return None; + } + set.irqs |= 1 << irq; + } + Some(set) + } + + pub fn ports(&self) -> impl Iterator + '_ { + self.ports.iter().copied().take_while(|&port| port != 0) + } + + pub fn irqs(&self) -> impl Iterator + '_ { + (0..Self::LINES).filter(|line| self.irqs & (1 << line) != 0) + } + + /// `",…:,…"`, the tail of an `isa:` entry. + pub fn parse(text: &str) -> Option { + let (ports, irqs) = text.split_once(':')?; + let mut port_list = [0u16; Self::MAX_PORTS]; + let mut n_ports = 0; + for port in ports.split(',') { + *port_list.get_mut(n_ports)? = hex16(port)?; + n_ports += 1; + } + let mut irq_list = [0u8; Self::MAX_IRQS]; + let mut n_irqs = 0; + for irq in irqs.split(',') { + *irq_list.get_mut(n_irqs)? = decimal_line(irq)?; + n_irqs += 1; + } + Self::new(&port_list[..n_ports], &irq_list[..n_irqs]) + } + + /// The two selector words [`device_claim`] carries: the ports in 16-bit + /// lanes from the low end, and the lines as a mask. + pub fn wire(self) -> [u64; 2] { + let ports = self + .ports + .iter() + .enumerate() + .fold(0u64, |word, (lane, &port)| word | (port as u64) << (16 * lane)); + [ports, self.irqs as u64] + } + + /// The selector words decoded, refusing whatever [`Self::new`] refuses. + pub fn from_wire([ports, irqs]: [u64; 2]) -> Option { + let lanes: [u16; Self::MAX_PORTS] = + core::array::from_fn(|lane| (ports >> (16 * lane)) as u16); + let named = lanes.iter().take_while(|&&port| port != 0).count(); + if lanes[named..].iter().any(|&port| port != 0) || irqs > u16::MAX as u64 { + return None; + } + let mut lines = [0u8; Self::LINES as usize]; + let mut n = 0; + for line in 0..Self::LINES { + if irqs & (1 << line) != 0 { + lines[n] = line; + n += 1; + } + } + if n > Self::MAX_IRQS { + return None; + } + Self::new(&lanes[..named], &lines[..n]) + } +} + +fn ascending(list: &[T]) -> bool { + list.windows(2).all(|pair| pair[0] < pair[1]) +} + +/// One ISA line in decimal, `0` to `15`, with no sign and no leading zero. +fn decimal_line(text: &str) -> Option { + let bytes = text.as_bytes(); + if bytes.is_empty() || bytes.len() > 2 || (bytes.len() == 2 && bytes[0] == b'0') { + return None; + } + let mut value = 0u8; + for &byte in bytes { + if !byte.is_ascii_digit() { + return None; + } + value = value * 10 + (byte - b'0'); + } + (value < IsaId::LINES).then_some(value) +} + /// What one `devices` entry asks for: a class, and for -/// [`DeviceType::PciFunction`] which function, and for a partition which -/// partition. +/// [`DeviceType::PciFunction`] which function, for a partition which +/// partition, and for an ISA function which ports and lines. /// /// One parser, because four places read the same spelling — the build system's /// gate, `/system/bin/init`'s mint, the kernel's claim and the claimant's own @@ -1298,6 +1431,7 @@ pub enum DeviceRequest { Class(DeviceType), Pci(PciId), Partition(crate::part::PartGuid), + Isa(IsaId), } impl DeviceRequest { @@ -1307,6 +1441,9 @@ impl DeviceRequest { /// The spelling that carries a unique partition GUID. pub const PART_PREFIX: &'static str = "part:"; + /// The spelling that carries an ISA function's ports and lines. + pub const ISA_PREFIX: &'static str = "isa:"; + /// The longest a `devices` entry, and so a `dev:` label's tail, can be: /// `part:` and a GUID, rounded up to a multiple of eight. pub const MAX_NAME: usize = 48; @@ -1318,11 +1455,14 @@ impl DeviceRequest { if let Some(guid) = name.strip_prefix(Self::PART_PREFIX) { return crate::part::PartGuid::parse(guid).map(Self::Partition); } + if let Some(set) = name.strip_prefix(Self::ISA_PREFIX) { + return IsaId::parse(set).map(Self::Isa); + } let class = DeviceType::from_class_name(name)?; match class { - // A bare `pci` or `part` names no device, and would otherwise - // leave the selector at zero. - DeviceType::PciFunction | DeviceType::Partition => None, + // A bare `pci`, `part` or `isa` names no device, and would + // otherwise leave the selector at zero. + DeviceType::PciFunction | DeviceType::Partition | DeviceType::Isa => None, _ => Some(Self::Class(class)), } } @@ -1332,6 +1472,7 @@ impl DeviceRequest { Self::Class(class) => class, Self::Pci(_) => DeviceType::PciFunction, Self::Partition(_) => DeviceType::Partition, + Self::Isa(_) => DeviceType::Isa, } } @@ -1342,6 +1483,7 @@ impl DeviceRequest { Self::Class(_) => [0, 0], Self::Pci(id) => [id.wire(), 0], Self::Partition(guid) => guid.wire(), + Self::Isa(set) => set.wire(), } } @@ -1365,6 +1507,35 @@ impl DeviceRequest { return core::str::from_utf8(&buf[..at + GUID_TEXT_LEN]) .expect("a prefix and a GUID's text are ASCII"); } + Self::Isa(set) => { + let mut at = Self::ISA_PREFIX.len(); + buf[..at].copy_from_slice(Self::ISA_PREFIX.as_bytes()); + for (i, port) in set.ports().enumerate() { + if i > 0 { + buf[at] = b','; + at += 1; + } + for shift in [12, 8, 4, 0] { + buf[at] = HEX[((port >> shift) & 0xF) as usize]; + at += 1; + } + } + buf[at] = b':'; + at += 1; + for (i, line) in set.irqs().enumerate() { + if i > 0 { + buf[at] = b','; + at += 1; + } + if line >= 10 { + buf[at] = b'1'; + at += 1; + } + buf[at] = b'0' + line % 10; + at += 1; + } + return core::str::from_utf8(&buf[..at]).expect("hex, digits and separators are ASCII"); + } Self::Pci(id) => id, }; let prefix = Self::PCI_PREFIX.as_bytes(); @@ -2411,6 +2582,21 @@ mod tests { "part:c12a7328-f81f-11d2-ba4b-00a0c93ec93b", // one spelling: uppercase // A partition is named by its unique GUID and nothing else. "part-type:C12A7328-F81F-11D2-BA4B-00A0C93EC93B", + "isa", // a bare ISA class names no ports + "isa:", + "isa:0060,0064", // a function with no line + "isa:0060,0064:", + "isa::1", + "isa:60,64:1,12", // one spelling: four hex digits + "isa:0064,0060:1,12", // and ascending + "isa:0060,0060:1", + "isa:0060:12,1", + "isa:0060:01", + "isa:0060:16", // the bus has sixteen lines + "isa:0000:1", // port 0 means no port + "isa:0060,0061,0062,0063,0064:1", // more ports than a selector word holds + "isa:0060:1,2,3,4,5", + "isa:0060:+1", "", ] { assert_eq!(DeviceRequest::parse(bad), None, "{bad:?} parsed"); @@ -2430,6 +2616,10 @@ mod tests { "mouse", // The longest entry there is, which is what `MAX_NAME` is for. "part:0FC63DAF-8483-4772-8E79-3D69D8477DE4", + "isa:0060,0064:1,12", + "isa:0001:0", + // The longest an ISA entry can be. + "isa:fff0,fff1,fff2,fffe:12,13,14,15", ] { let request = DeviceRequest::parse(name).expect("a name this table has"); let mut buf = [0u8; DeviceRequest::MAX_NAME]; @@ -2449,6 +2639,31 @@ mod tests { assert_eq!(request.map(DeviceRequest::selector), Some(guid.wire())); } + /// An `isa:` entry is its ports in the first selector word and its lines in + /// the second, and the kernel decodes nothing a config could not have + /// written: a port lane after a gap, or a line past the bus, is no set. + #[test] + fn an_isa_entry_survives_the_wire_and_nothing_else_decodes() { + let request = DeviceRequest::parse("isa:0060,0064:1,12").expect("the i8042's set"); + let DeviceRequest::Isa(set) = request else { panic!("{request:?} is not an ISA set") }; + assert_eq!(request.class(), DeviceType::Isa); + assert!(set.ports().eq([0x60, 0x64])); + assert!(set.irqs().eq([1, 12])); + assert_eq!(request.selector(), [0x0064_0060, (1 << 1) | (1 << 12)]); + assert_eq!(IsaId::from_wire(set.wire()), Some(set)); + for bad in [ + [0, 1 << 1], // no port + [0x0060, 0], // no line + [0x0060_0000, 1 << 1], // a port after a gap + [0x0060_0064, 1 << 1], // not ascending + [0x0060, 1 << 16], // past the bus + [0x0060, 0b1_1111], // more lines than a name holds + [0x0060_0060, 1 << 1], // one port twice + ] { + assert_eq!(IsaId::from_wire(bad), None, "{bad:x?} decoded"); + } + } + /// The selector is one word on the wire and the kernel decodes it back; /// a vendor lost to a shift would claim a different card. #[test] diff --git a/toyos-ps2/tests/decode.rs b/toyos-ps2/tests/decode.rs index 979750c76ab..aef9b850401 100644 --- a/toyos-ps2/tests/decode.rs +++ b/toyos-ps2/tests/decode.rs @@ -87,16 +87,14 @@ fn make_and_break_and_the_e0_prefix() { assert_eq!(press(&mut d, &[0x4B]), [(0x5C, true)]); } -/// The panic console's pager reads these two straight off a halted controller -/// with its own decoder, and its whole input vocabulary is what this asserts. #[test] -fn the_pager_keys_decode_to_their_hid_usages() { +fn page_up_and_down_decode_to_their_hid_usages() { let mut d = KeyDecoder::new(); assert_eq!(press(&mut d, &[0xE0, 0x49]), [(0x4B, true)], "PageUp make"); assert_eq!(press(&mut d, &[0xE0, 0xC9]), [(0x4B, false)], "PageUp break"); assert_eq!(press(&mut d, &[0xE0, 0x51]), [(0x4E, true)], "PageDown make"); assert_eq!(press(&mut d, &[0xE0, 0xD1]), [(0x4E, false)], "PageDown break"); - // Unprefixed they are the keypad's 9 and 3, which a pager must not answer. + // Unprefixed they are the keypad's 9 and 3. assert_eq!(press(&mut d, &[0x49]), [(0x61, true)]); assert_eq!(press(&mut d, &[0x51]), [(0x5B, true)]); } diff --git a/toyos-tco/src/lib.rs b/toyos-tco/src/lib.rs index eb7ceb1938e..1d60e1f8af7 100644 --- a/toyos-tco/src/lib.rs +++ b/toyos-tco/src/lib.rs @@ -125,10 +125,8 @@ pub const C_MEMBER_MS: u64 = 260; pub const LEASE_BOUND_MS: u64 = 20_000; /// The bound a panicked kernel holds its panel for before it returns the -/// machine to firmware itself, in milliseconds. Nothing feeds this one: a key -/// press retires it, because a key means somebody is reading the panel, and -/// nobody pressing one inside the bound means nobody is there to. A minute is -/// this project's bound for every watchdog. +/// machine to firmware itself, in milliseconds. A minute is this project's +/// bound for every watchdog. pub const PANIC_BOUND_MS: u64 = 60_000; /// The bound the *kernel* gives a whole boot, in milliseconds, before it seals diff --git a/toyos-userbound/src/lib.rs b/toyos-userbound/src/lib.rs index c7fdc63f4f5..bf114b2a683 100644 --- a/toyos-userbound/src/lib.rs +++ b/toyos-userbound/src/lib.rs @@ -6,17 +6,18 @@ //! hold a user window, which of them does each piece of the copy land in, and //! is every one of them pinned? **Before a placement**: can a length userland //! asked for be placed at all, and where does it go? **After a trap**: which -//! side did the frame come from? +//! side did the frame come from? **Before an `in` or `out`**: which ports does +//! this CPU open to the process running on it? //! -//! [`span`] answers the first, [`segment`] the second, [`place`] the third and -//! [`fault`] the fourth. +//! [`span`] answers the first, [`segment`] the second, [`place`] the third, +//! [`fault`] the fourth and [`port`] the fifth. //! //! Pure. No I/O, no allocation, no `unsafe`, nothing read from a device and //! nothing named outside this crate. The kernel is the only caller — -//! `user_ptr.rs`, `mm/`, `syscall/`, `loader/` and -//! `arch/x86_64/idt/exceptions.rs` — and this is a crate rather than files inside it so -//! that the boundary table below runs on the host in milliseconds instead of in -//! a boot. +//! `user_ptr.rs`, `mm/`, `syscall/`, `loader/`, `arch/x86_64/percpu.rs`, +//! `arch/x86_64/pio.rs` and `arch/x86_64/idt/exceptions.rs` — and this is a +//! crate rather than files inside it so that the boundary table below runs on +//! the host in milliseconds instead of in a boot. //! //! The numbers are x86-64's: [`USER_TOP`] is the canonical split at 48-bit //! linear addresses, [`PAGE_2M`] is the kernel's one user page size, and a @@ -28,11 +29,13 @@ pub mod fault; pub mod place; +pub mod port; pub mod segment; pub mod span; pub use fault::Ring; pub use place::{PageSpan, Window}; +pub use port::{port_access, IoBitmap, PortAccess, IO_PORTS}; pub use segment::{pieces, segments, Pinned, Pins, Segment}; pub use span::{ align_2m_checked, in_user_half, is_user_addr, is_user_object, rebase_base, Access, PAGE_2M, diff --git a/toyos-userbound/src/port.rs b/toyos-userbound/src/port.rs new file mode 100644 index 00000000000..1001b0818e9 --- /dev/null +++ b/toyos-userbound/src/port.rs @@ -0,0 +1,201 @@ +//! Which I/O ports the CPU a process runs on opens to Ring 3, and which port +//! a refused `in` or `out` named. +//! +//! **A process reaches a port only through its CPU's I/O permission bitmap, +//! with IOPL left at 0** (Intel SDM Vol. 1 §19.5.2, AMD APM Vol. 2 §12.2.4): +//! the TSS carries one bit per port below [`IO_PORTS`], set unless the process +//! running there holds a grant naming that port, and every port at or past it +//! is past the TSS limit, which the processor refuses by itself. + +/// Ports the bitmap names. A port the bitmap can open is therefore a `u8`. +pub const IO_PORTS: usize = 0x100; + +/// The bitmap as the processor reads it at the end of a TSS: one bit per port, +/// set to refuse, then the all-ones byte past the last (the processor reads two +/// bytes for every check, and the second must refuse). +#[repr(C)] +pub struct IoBitmap { + refused: [u8; IO_PORTS / 8], + end: u8, +} + +const _: () = assert!(size_of::() == IO_PORTS / 8 + 1 && align_of::() == 1); + +impl IoBitmap { + /// Every port refused. + pub const fn refusing() -> Self { + Self { refused: [0xFF; IO_PORTS / 8], end: 0xFF } + } + + /// Whether Ring 3 may access `port`. + pub const fn opens(&self, port: u16) -> bool { + (port as usize) < IO_PORTS && self.refused[port as usize / 8] & (1 << (port % 8)) == 0 + } + + fn set(&mut self, port: u8, open: bool) { + let (at, bit) = (&mut self.refused[port as usize / 8], 1u8 << (port % 8)); + *at = if open { *at & !bit } else { *at | bit }; + } + + /// Open each row's ports if the process switching in holds the row, and + /// close them otherwise: `rows` is every grantable row's ports, with + /// whether the incoming process holds it. + pub fn switch_to<'a>(&mut self, rows: impl IntoIterator) { + for (ports, held) in rows { + // A row's ports open and close together, so its first bit is its state. + let Some(&first) = ports.first() else { continue }; + if self.opens(first as u16) == held { + continue; + } + for &port in ports { + self.set(port, held); + } + } + } + + /// The first port of `access` this bitmap refuses, which is the port its + /// #GP faulted on. `None` if every port in the span is open: the fault was + /// not the port's, and its report must not guess one. + pub fn refused(&self, access: PortAccess) -> Option { + (0..access.bytes as u16).map(|i| access.port.wrapping_add(i)).find(|&port| !self.opens(port)) + } +} + +/// An `in` or `out` as decoded from the bytes at a faulting instruction. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct PortAccess { + pub out: bool, + pub port: u16, + /// How many ports from `port` on the access spans, every one of which the + /// bitmap must open. + pub bytes: u8, +} + +impl core::fmt::Display for PortAccess { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + let (verb, way) = if self.out { ("out", "to") } else { ("in", "from") }; + write!(f, "{verb} of {} byte(s) {way} port {:#06x}", self.bytes, self.port) + } +} + +/// The port access `code` begins with, or `None` for any other instruction. +/// What a Ring 3 #GP names, since its error code (0) says nothing; `dx` is +/// what the forms that take their port from DX read. +/// +/// Up to two prefixes of those an `in`/`out` can carry: operand size (which +/// halves a 4-byte access), a REP for the string forms, and REX. +pub const fn port_access(code: [u8; 4], dx: u16) -> Option { + const fn prefix(byte: u8) -> bool { + matches!(byte, 0x66 | 0xF2 | 0xF3 | 0x40..=0x4F) + } + // Destructured rather than indexed: the crash report calls this, and + // nothing on that path may panic. + let (half, op, imm) = match code { + [a, b, op, imm] if prefix(a) && prefix(b) => (a == 0x66 || b == 0x66, op, imm), + [a, op, imm, _] if prefix(a) => (a == 0x66, op, imm), + [op, imm, _, _] => (false, op, imm), + }; + let wide = if half { 2 } else { 4 }; + let (out, port, bytes) = match op { + 0xE4 => (false, imm as u16, 1), + 0xE5 => (false, imm as u16, wide), + 0xE6 => (true, imm as u16, 1), + 0xE7 => (true, imm as u16, wide), + 0xEC | 0x6C => (false, dx, 1), + 0xED | 0x6D => (false, dx, wide), + 0xEE | 0x6E => (true, dx, 1), + 0xEF | 0x6F => (true, dx, wide), + _ => return None, + }; + Some(PortAccess { out, port, bytes }) +} + +const _: () = { + const fn is(access: Option, out: bool, port: u16, bytes: u8) -> bool { + match access { + Some(a) => a.out == out && a.port == port && a.bytes == bytes, + None => false, + } + } + // `in al, 0x61`, `out 0x64, al`, `in eax, dx`, `in ax, dx` and `rep outsb`. + assert!(is(port_access([0xE4, 0x61, 0, 0], 0), false, 0x61, 1)); + assert!(is(port_access([0xE6, 0x64, 0, 0], 0), true, 0x64, 1)); + assert!(is(port_access([0xED, 0, 0, 0], 0x60), false, 0x60, 4)); + assert!(is(port_access([0x66, 0xED, 0, 0], 0x60), false, 0x60, 2)); + assert!(is(port_access([0xF3, 0x6E, 0, 0], 0x3F8), true, 0x3F8, 1)); + // `mov eax, 0x61` (B8) and `hlt` are no port access. + assert!(port_access([0xB8, 0x61, 0, 0], 0).is_none()); + assert!(port_access([0xF4, 0, 0, 0], 0).is_none()); +}; + +#[cfg(test)] +mod tests { + extern crate std; + + use std::vec::Vec; + + use super::*; + + /// The i8042's row, and a second one beside it. + const I8042: &[u8] = &[0x60, 0x64]; + const OTHER: &[u8] = &[0x70, 0x71]; + + fn open_ports(bitmap: &IoBitmap) -> Vec { + (0..=u16::MAX).filter(|&port| bitmap.opens(port)).collect() + } + + #[test] + fn a_fresh_bitmap_refuses_every_port_and_ends_in_ones() { + let bitmap = IoBitmap::refusing(); + assert_eq!(open_ports(&bitmap), [] as [u16; 0]); + assert_eq!(bitmap.end, 0xFF); + } + + #[test] + fn a_held_row_opens_its_ports_and_no_other() { + let mut bitmap = IoBitmap::refusing(); + bitmap.switch_to([(I8042, true), (OTHER, false)]); + assert_eq!(open_ports(&bitmap), [0x60, 0x64], "a port beside a granted one opened with it"); + // The byte the processor reads past the bitmap still refuses. + assert_eq!(bitmap.end, 0xFF); + } + + #[test] + fn a_switch_to_a_process_holding_nothing_closes_the_row() { + let mut bitmap = IoBitmap::refusing(); + bitmap.switch_to([(I8042, true), (OTHER, true)]); + assert_eq!(open_ports(&bitmap), [0x60, 0x64, 0x70, 0x71]); + bitmap.switch_to([(I8042, false), (OTHER, true)]); + assert_eq!(open_ports(&bitmap), [0x70, 0x71], "the next process kept its predecessor's ports"); + bitmap.switch_to([(I8042, false), (OTHER, false)]); + assert_eq!(open_ports(&bitmap), [] as [u16; 0]); + bitmap.switch_to([(I8042, true), (OTHER, false)]); + assert_eq!(open_ports(&bitmap), [0x60, 0x64], "the holder's return did not open its row again"); + } + + #[test] + fn the_last_port_of_the_bitmap_opens_without_touching_the_end_byte() { + let mut bitmap = IoBitmap::refusing(); + bitmap.switch_to([(&[0xFF][..], true)]); + assert_eq!(open_ports(&bitmap), [0xFF]); + assert_eq!(bitmap.end, 0xFF); + } + + #[test] + fn a_refused_access_names_the_first_port_it_may_not_touch() { + let mut bitmap = IoBitmap::refusing(); + bitmap.switch_to([(I8042, true), (&[0xFF][..], true)]); + let access = |port, bytes| PortAccess { out: false, port, bytes }; + // A granted port, alone: the fault is not the port's. + assert_eq!(bitmap.refused(access(0x60, 1)), None); + assert_eq!(bitmap.refused(access(0x64, 1)), None); + // One past the grant, and a wider access that runs into it. + assert_eq!(bitmap.refused(access(0x61, 1)), Some(0x61)); + assert_eq!(bitmap.refused(access(0x60, 2)), Some(0x61)); + assert_eq!(bitmap.refused(access(0x60, 4)), Some(0x61)); + // Past the bitmap is past the TSS limit, and the span wraps as ports do. + assert_eq!(bitmap.refused(access(0xFF, 2)), Some(0x100)); + assert_eq!(bitmap.refused(access(0x3F8, 1)), Some(0x3F8)); + assert_eq!(bitmap.refused(access(0xFFFF, 2)), Some(0xFFFF)); + } +} diff --git a/toyos/src/syscap.rs b/toyos/src/syscap.rs index 1501c2f5834..f7cdc16c9ca 100644 --- a/toyos/src/syscap.rs +++ b/toyos/src/syscap.rs @@ -53,6 +53,18 @@ impl SysCap { self.mint(DeviceRequest::Partition(guid)) } + /// Mint the claim for one legacy ISA function, by exactly its ports and + /// lines. Apart from [`Self::claim`] for the reason [`Self::claim_pci`] is. + /// + /// `NotFound` is a set that is not one function this machine can hand out + /// whole, and `PermissionDenied` one the kernel drives itself. + pub fn claim_isa( + &self, + set: toyos_abi::syscall::IsaId, + ) -> Result { + self.mint(DeviceRequest::Isa(set)) + } + fn mint(&self, request: DeviceRequest) -> Result { let raw = syscall::device_claim(self.0.raw(), request)?; // SAFETY: the kernel installed this handle in this process's table for diff --git a/userland/init/src/main.rs b/userland/init/src/main.rs index 1901f662fde..93e4ad0f841 100644 --- a/userland/init/src/main.rs +++ b/userland/init/src/main.rs @@ -1955,6 +1955,7 @@ fn start<'a>( DeviceRequest::Class(class) => syscap.claim::(class), DeviceRequest::Pci(id) => syscap.claim_pci::(id), DeviceRequest::Partition(name) => syscap.claim_partition::(name), + DeviceRequest::Isa(set) => syscap.claim_isa::(set), }; let asked = Instant::now(); let mut held_still = 0u32;