Skip to content

The panic console takes no input, and an isa claim grants a process exact ports - #592

Open
Japabu wants to merge 4 commits into
mainfrom
wt/toyos-ps2
Open

Japabu wants to merge 4 commits into
mainfrom
wt/toyos-ps2

Conversation

@Japabu

@Japabu Japabu commented Sep 28, 2026 •

Copy link
Copy Markdown
Collaborator

The first stage of taking the i8042 (PS/2) out of the kernel, on the owner's ruling that drivers are userland and a dead kernel takes no input, with no emergency way. Stage 7.1 of issues/kernel/the-kernel-is-small-interrupts-post-and-threads-wait.md; 7.2, the userland server and the driver's deletion, is recorded there.

Why a stage and not the whole move

The whole move is not one reviewable change. The driver is 1,496 lines (i8042/mod.rs, tally.rs, its IDT entry), a dozen guest tests measure its internals through nine kernel actuators, and the harness paces every line it types at a windowed shell on the kernel's i8042: drain bytes= trace (shell_type_once, every i8042-trace boot), so moving the driver moves that pacing and puts the server in every boot config that types. This stage lands what the server stands on and deletes nothing half way: there is no second path to the controller, and the kernel refuses a claim on a controller it drives.

The panic console takes no input

  • The pager cycles on its own and is never steered: page_forever waits PAGE_HOLD and paints the next page, hold_the_panel spins on the bound or halts.
  • Bound::retire is gone: the reboot bound fires whoever is at the machine. The arm line is now panic: rebooting in N s, timed by … and the raw last line panic: the bound is spent: returning this machine to firmware.
  • keyboard_controller::poll_byte, the one read of port 0x60 outside the driver's ISR, is deleted on both architectures, and the kernel's panic path no longer uses toyos-ps2.
  • Tests: screen_pager_keys is deleted with the keys it pressed. panic_key_holds (and its release-only arm) becomes panic_ignores_keys: the same panicked guest with a pressed inside the bound, which must reset anyway. screen_paged_scrollback stays: it presses nothing and judges the automatic cycle, which survives. The panic_armed()/PANIC_REBOOTING strings in tests/common/power.rs and screen_panic_muted's arm line follow the kernel's.
  • issues/panic-path/a-storage-phase-panic-reads-its-key-off-an-unconfigured-i8042.md is closed: the key it was about is gone. A stale clause in a-fatal-event-stands-down-both-bounds… and the README's PageUp/PageDown sentence are deleted.

The isa claim

  • The interface (the orchestrator's): DeviceType::Isa = 9, spelled isa:0060,0064:1,12 in a devices entry, one spelling per set (four lowercase hex digits per port, decimal lines, both ascending). The selector carries the ports in 16-bit lanes and the lines as a mask; IsaId in toyos-abi is the one parser, SysCap::claim_isa the SDK's mint, and init mints it like any other entry. No syscall was added.
  • Exact rows only. arch::pio::GRANTABLE is the whole of what can be claimed: on x86-64 one row, the i8042's 0x60, 0x64 and lines 1, 12; on AArch64 none. A subset, a missing line or a neighbouring port is NotFound; a row the kernel drives (i8042::drives, its ACTIVE) is PermissionDenied, which is every machine whose i8042 the kernel armed, so in this stage the claim is live only where the kernel's probe gave up or found nothing.
  • The ports belong to the process that first reads the claim. That read binds them (and answers the set back as the description); every context switch then opens the row's ports in the incoming CPU's I/O permission bitmap for that process and closes them for every other (pio::switch_to, from KernelHw::switch). The ports stay bound until the process's teardown, which runs after every thread has left and none returns to Ring 3, so no revocation ever has to reach a thread running on another CPU. A handle moved on after the bind answers PermissionDenied, and the row is claimable again once both the claim and the process are gone. IOPL stays 0.
  • The bitmap. Each CPU's TSS grows by 33 bytes: one bit per port below 0x100 and the all-ones byte the processor reads past the last (Intel SDM Vol. 1 §19.5.2). The descriptor's limit ends at that byte, so every port from 0x100 up is refused by the limit with no bit at all. Compile-time asserts pin the bitmap's offset and the limit.
  • The lines are routed once per boot to vector 0x2c (an interrupt-remapping entry is never given back), unmasked while a claim exists and masked when it goes, and counted into pcidev's own Interrupt record (now pub(crate)), posted through the UserDev IRQ-ring slot, and read the way a PCI claim's are.
  • Killed by name. A Ring 3 #GP at an in/out now adds in of 1 byte(s) from port 0x0061, which this process holds no grant for to the kill record; the decode is a const fn with compile-time cases. The report reads the faulting bytes through safe_read_u64, the crash path's hand walk of the user page tables, which now goes down to a 4 KiB leaf when the PDE is not one: an image whose first 2 MiB mixes text, rodata and data is mapped by map_window as a table of 4 KiB leaves, and the walk used to read that table's PDE as a 2 MiB page. That is why isa_ports_are_the_binders_alone was red at e72cf7f4: all four refused ins were killed as a bare #GP, and none named its port (isa_grant's LOADs are r--, r-x and rw- below 0x9f000, and the bytes at its faulting rips are 66 ba NN 00 ec: mov dx, NN; in al, dx). The stack is one uniform window, which is why the backtrace was right all along.
  • Declared, not fixed: the i8042's holder can reset the machine (0xFE to 0x64), which a bitmap cannot filter — filed for the owner as issues/isolation/the-i8042s-holder-holds-the-machines-reset-line.md.

kernel/src: 64,918 lines on origin/main (3d902477), 65,313 on this head (find kernel/src -name '*.rs' -print0 | xargs -0 cat | wc -l): the mechanism lands before the driver it replaces is deleted.

Gates on the head

All on 7c13193f, each its own exit code, with the tree clean after:

  • cargo run -- --ci host: EXIT=0 (54 steps)
  • cargo run -- --build-only: EXIT=0

Merged with origin/main (3d902477)

Main rewrote screen_pager_keys and deleted panic_key_holds (a QEMU test measures no time); this branch deletes the first with the pager keys and turns the second into panic_ignores_keys, which waits on the reset, not on a clock. The branch's side is kept in all four tests/toyos.rs hunks. Main had moved PANIC_REBOOTING to tests/common/qemu.rs; it now holds the kernel's new raw line, panic: the bound is spent, and power.rs's copy is gone. issues/build/timing-verdicts-ruled-off-qemu-have-no-metal-arm.md loses the sentence about the panel holding while a key is held, since the panel now holds for no key.

Nothing on the panic path reads input: no poll_byte, no KeyDecoder, no page keys in drivers/panic_console, panic.rs or panic_reboot.rs. The one movement left is page_forever's automatic page cycle, which no key steers.

Tests, controls and the oracle

New guest tests: isa_ports_are_the_binders_alone (Fast; MetalNoUsb, no i8042: the claim, its refusals, and one child per access that must die — holding the claim unbound, one port past the grant, holding nothing, holding a claim its parent bound — each named by the kernel), isa_claim_refused_where_the_kernel_drives (Nightly), isa_lines_reach_their_holder (Nightly; i8042-budget-expired makes the kernel give the real controller up, and the test drives it through the claim to a keystroke's record and its byte, 0x1e).

Negative controls, each a checked patch in the job scratchpad. m1–m4 and m6 are in /private/tmp/claude-502/-Users-jan-Dev-jan-toyos/2280e09e-428b-4b81-bc00-1ede594b7247/scratchpad/ps2-r2/, regenerated on 7c13193f, then applied, built (cargo run -- --build-only EXIT=0 each, the kernel recompiled each time) and reverted to a clean tree. m5 is still the earlier …/scratchpad/ps2/m5-keys-retire-the-bound-again.patch: git apply --check passes on this head (EXIT=0), but it was built only on 36518879. The orchestrator runs the guest verdicts:

patch what it breaks must red
m1-every-process-gets-the-ports.patch the switch opens the row for every process isa_ports_are_the_binders_alone (unbound survived its access)
m2-a-port-grants-its-neighbour.patch opening a port opens the next one too isa_ports_are_the_binders_alone (bound survived at 0x61)
m3-a-subset-is-a-row.patch a subset of a row's ports is a grant isa_ports_are_the_binders_alone ("0060:1" minted)
m4-no-bitmap-in-the-limit.patch the bitmap's offset back past the limit isa_ports_are_the_binders_alone (the bound child dies before 0x61), isa_lines_reach_their_holder
m5-keys-retire-the-bound-again.patch the base's key-retired bound and pager keys, with this branch's lines panic_ignores_keys
m6-the-report-reads-a-split-window-as-a-2mib-leaf.patch the whole of 7c13193f reverted: the crash path's walk reads every PDE as a 2 MiB leaf again isa_ports_are_the_binders_alone (no port named; measured red at e72cf7f4, which is this arm)

Independent oracles: the Intel SDM Vol. 1 §19.5.2 and AMD APM Vol. 2 §12.2.4 for the bitmap, its end byte and the limit; QEMU's TCG check_io is a second implementation of that check the local guests run on, and CI's KVM shards run it on silicon. For the controller, the IBM PC AT Technical Reference's 8042 commands (0x20, 0x60, 0xAE), status bits and set-1 0x1e, which QEMU's pckbd/ps2 model implements independently.

Unsure

  • No guest run of this head. At e72cf7f4 the orchestrator measured isa_ports_are_the_binders_alone red for the reason fixed above, with every access refused where it should be. isa_claim_refused_where_the_kernel_drives, isa_lines_reach_their_holder, panic_ignores_keys, panic_reboots, screen_paged_scrollback, screen_panic_muted, screen_late_panic and klogd_fault_halts were green there, before the merge.
  • port_access_at reads the 8 bytes after the faulting rip's aligned word as well. An in that ends right before an unmapped page is decoded as no port access, and the kill record then carries only the bare #GP.
  • The switch now writes the bitmap bits of every grantable port on every context switch — a load, a compare and a few byte writes — and that cost is not measured.
  • Binding at the first read rather than at the claim's arrival is my decision; the server in 7.2 reads its claim first, as netd reads its PCI claim.
  • isa_lines_reach_their_holder rests on QEMU leaving the keyboard scanning after the kernel's aborted probe, which is QEMU's reset state and not something the probe promises.
  • On a machine with no aux device line 12 is still routed and unmasked while a claim exists.

Guest runs to queue

One per run:

  • cargo test --test toyos-build -- isa_ports_are_the_binders_alone
  • cargo test --test toyos-build -- --nightly isa_claim_refused_where_the_kernel_drives
  • cargo test --test toyos-build -- --nightly isa_lines_reach_their_holder
  • cargo test --test toyos-build -- --weekly panic_ignores_keys
  • cargo test --test toyos-build -- --nightly panic_reboots
  • cargo test --test toyos-build -- --weekly screen_paged_scrollback
  • cargo test --test toyos-build -- --weekly screen_panic_muted
  • cargo test --test toyos-build -- screen_late_panic
  • cargo test --test toyos-build -- --nightly klogd_fault_halts
  • cargo test --test toyos-build (the Fast tier, this head merged with main)
  • each mutation above with the test its row names

🤖 Generated with Claude Code

Japabu and others added 2 commits September 29, 2026 00:28
…xact ports

The first stage of taking the i8042 out of the kernel (owner ruling: drivers
are userland, and a dead kernel takes no input, with no emergency way). The
whole move does not fit one reviewable change: the driver is 1,496 lines, a
dozen guest tests measure its internals through kernel actuators, and the
harness paces every typed line on its drain trace. This stage lands what the
server will stand on and deletes nothing half way; ps2d and the driver's
deletion are stage 7.2 of the small-kernel track.

The panic console takes no input. The pager cycles on its own and is never
steered, the reboot bound has no retire and fires whoever is at the machine,
and poll_byte, the one read of port 0x60 outside the driver's ISR, is gone
from both architectures. screen_pager_keys is deleted with the keys it
pressed; panic_key_holds becomes panic_ignores_keys, the same guest with a key
pressed inside the bound that must now reset anyway. screen_paged_scrollback
stays: it presses nothing and judges the automatic cycle, which survives. The
panic-path issue about reading the key off an unconfigured i8042 closes with
the key.

The isa claim, DeviceType::Isa ("isa:0060,0064:1,12"), names an ISA function
by exactly its ports and lines, and the kernel grants only a whole row of its
architecture's GRANTABLE table: on x86-64 the i8042, on AArch64 nothing. The
first read of the claim binds the ports to the reading process and answers
the set back; each CPU's TSS now carries an I/O permission bitmap for ports
below 0x100 and its refusing end byte (Intel SDM Vol. 1 19.5.2), and every
context switch opens a row's ports for the process holding it and closes them
for every other. IOPL stays 0. The ports stay with that process until its
teardown, after every thread has left, so no revocation ever races a running
thread; a handle moved on after the bind answers PermissionDenied. The lines
are routed once per boot to vector 0x2c, unmasked while a claim exists, and
counted into the same record a claimed PCI function's are. A claim on a
function the kernel drives is refused by name, which is every machine whose
i8042 the kernel armed. A Ring 3 #GP at an in or out now names the port and
the direction in the kill record. No syscall was added.

Tests: toyos-abi's parser and wire round trip; isa_grant in three roles on
three machines (refused where the kernel drives; granted, and refused one port
over and to every other process, on a machine with no i8042; a real
controller the kernel gave up on driven by the claim to a keystroke's record
and byte). The holder can reset the machine through 0x64's 0xFE, which the
bitmap cannot filter: filed as a question for the owner.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The kernel-drives refusal is one boot for one syscall word, so it runs with
the nightly tier; the binding test, the security boundary itself, stays in
the fast one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Japabu
Japabu marked this pull request as ready for review September 28, 2026 22:35
Japabu and others added 2 commits September 29, 2026 03:58
Conflicts, each hunk accounted for:

- tests/toyos.rs: main rewrote screen_pager_keys (PageUp one page back per
  key) and deleted panic_key_holds; this branch deletes screen_pager_keys with
  the pager keys and turns panic_key_holds into panic_ignores_keys. Kept the
  branch's side of all four hunks: the pager takes no key, so main's rewrite
  has no subject left.
- tests/common/power.rs: main moved PANIC_REBOOTING to tests/common/qemu.rs
  and reworded two doc comments. The branch's local constant goes; qemu.rs's
  takes the kernel's new line, "panic: the bound is spent". The doc on
  panic_reboots keeps the branch's wording (the bound fires whoever is at the
  machine); PANIC_FAST_SECS takes main's.
- issues/build/timing-verdicts-ruled-off-qemu-have-no-metal-arm.md: the
  sentence recording panic_key_holds' property as having no metal arm is
  deleted, since the panel no longer holds for a key at all.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
isa_ports_are_the_binders_alone was red: every refused `in` was killed as a
bare #GP and none named its port. The decode was reached and its bytes were
wrong. `safe_read_u64` walked a user address to the PDE and read it as a 2 MiB
leaf always, but an image whose first 2 MiB holds text beside rodata and data
(`isa_grant`: r--, r-x and rw- LOADs under 0x9f000) is mapped by
`map_window` as a page table of 4 KiB leaves. The PDE then names that table,
and the report read the table's 2 MiB-aligned neighbourhood instead of the
`ec` at the faulting rip. The stack, one uniform window, read correctly, which
is why the backtrace never showed it.

The walk now goes one level down when the PDE's PS bit is clear, as
`paging::debug_page_walk` does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant