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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
---
status: open
kind: defect
opened: 2026-10-08
---

# A class claim's call in flight acts after the claim's release

A claim on a class this kernel still drives — the framebuffer, HDA audio,
virtio-sound — is a per-class flag (`device::TAKEN`), and a call on one checks
the handle and then acts with nothing held across the two:

- `SYS_DEVICE_REG_READ` and `SYS_DEVICE_REG_WRITE` resolve the claim to
`RegTarget::Hda` or `RegTarget::VirtioSound` under the process-data lock
(`sys_device_reg`, `kernel/src/syscall/device.rs`) and reach
`drivers::hda::reg_write` or `drivers::virtio_sound::reg_write` after it;
- `SYS_GPU_PRESENT`, `SYS_GPU_SET_CURSOR`, `SYS_GPU_MOVE_CURSOR` and
`SYS_GPU_SET_RESOLUTION` ask `holds_claim` and then call `crate::gpu`
(`kernel/src/syscall/dispatch.rs`).

The flag clears when the claim's last handle goes (`Claim`'s drop,
`kernel/src/device.rs`) and the next claim sets it again. So a call whose
claim a sibling thread closed after the check, and whose class another process
claimed before the act, writes a register of the audio controller, or presents
on the display, that the second process now holds.

A claim on a PCI function, an ISA function or the ACPI fixed hardware does not
have this: its calls borrow what names the device from the claim under the
lock its release takes it with (`DeviceClaim::pci`, `DeviceClaim::isa`). That
borrow is no fix to copy here as it stands: `SYS_GPU_SET_RESOLUTION` allocates,
and the claim's lock is a spinlock.

**A poll has the same shape on these classes.** The watch a poll on an audio
claim or the mouse's registers on is the class's one `static`
(`drivers::AUDIO_WATCH`, `mouse::WATCH`, by `ops::read_watch`), and
`inbox::arm` registers on it with nothing held. A sibling thread's close
between the resolve and the registration lets the poll land after the close
answered the watch's polls (`ops::close`, and for audio `Claim`'s drop in
`kernel/src/device.rs` too), and the class's next holder's first interrupt
fires it into the old process's ring: one bit of another process's device
activity. A claim on a PCI
function, an ISA function or the ACPI fixed hardware registers with what the
claim lends (`DeviceClaim::add_poll`) and does not have it.

**Read from the code, not run.** A guest cannot order a close and a second
claim between the two steps of one syscall.

**Exit condition**: no call on a released class claim reaches the device, and
no poll registered on one stays on its watch, once the class's next claim is
minted; or the class is gone from the kernel, as
`issues/every-driver-is-still-in-the-kernel.md` retires each of them.

**Owner**: none. That track is open and nobody holds it.
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
status: open
kind: finding
opened: 2026-10-08
---

# A handler tells a slot it read before the slot changed hands

Two interrupt handlers name a `pcidev` slot by a number and write that slot's
record a moment later, with nothing excluding a release in between:

- the remapping fault handler reads the faulting stream's slot
(`owner_of`, `kernel/src/arch/x86_64/vtd/fault.rs`) and then calls
`pcidev::note_fault(slot)`;
- a claimed function's vector handler is its slot (`idt/user_dev.rs`) and
calls `pcidev::isr(slot)`.

`pcidev::tear_down` clears the stream's slot and takes the remapping entry
away before the slot is free, so a handler that *starts* after a release names
nothing. One already past its read on another CPU still holds the number: if
the slot is released and bound to a second function before its write, that
function's claim is told a fault — every call on it refuses `Io` from then
on — or counts one interrupt its device did not raise.

The interval is two adjacent statements of a handler running with interrupts
off, and a second function's bring-up in between is config-space writes and,
after a reset, a wait of at least the reset's settle time; no authority moves
either way. Nothing in the tree can hold a handler there, so this is read and
not run.

**Exit condition**: the slot's record says which binding it is for and a
handler's write is refused by a record that has moved on, or this is folded
into `kernel/src/pcidev/mod.rs`'s header as the bound it is.

**Owner**: none. No track covers a slot's record.

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@ caches, waits for the log to be durable, seals `DONE` and calls
`pcidev::release` (`kernel/src/pcidev/mod.rs:782`) calls `tear_down` (`:796`),
which disables bus mastering (`:797`), masks the function's MSI-X entry
(`:798`), empties its IOMMU domain (`:799`) and only then resets the function
(`:809`). The one caller of `pcidev::release` in the kernel
(`rg -n 'pcidev::release' kernel/src`, one hit) is `kernel/src/device.rs:75`,
inside `Claim`'s `Drop` — a dying process's handle table.
(`:809`). `release` is private to `pcidev` and its one caller is
`pcidev::Binding`'s `Drop`; a binding is held by a `device::Claim` and dropped
with it — a dying process's handle table.

**So on every one of the five paths, a function whose holder is alive is left
as its holder last wrote it when the register is written.** No process is
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
status: open
kind: tooling
opened: 2026-10-08
---

# `lan_talk` has not been read on the T14 since a device call began to borrow its claim's binding

#763 makes every `pcidev` call act on a `&Binding` its claim lends, and a poll
register through the claim. The I219 under traffic is the one holder that
drives those calls on real hardware at rate: its interrupt records
(`take_record`, `has_irq`), its polls and its DMA grants. The `lan_talk` metal
row is what reads it, and it was staged from #763's head and not read: the
bench's wired path between the T14 and the development machine is down.

The row cannot tell #763 from `main` while the path is down: staged from
`main` at `6f87cdb9c` and booted the same day, it is red the same way, `the
stream never opened` (`src/metaltalk.rs`), which is the host failing to reach
the machine and not the machine's answer.

What stands in for it meanwhile: under QEMU, `iommu_virtio_platform` and the
suite's network tests drive the same `pcidev` functions through virtio-net's
claim; on the T14, `claim_reuses_its_remapping_entry`, `isa_` and `acpi_`. None
is the I219.

Owner: the orchestrator, which holds the T14 and the bench.

**Exit**: `lan_talk` green from the T14 on a head that contains #763.
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
---
status: open
kind: defect
opened: 2026-10-08
---

# The power-off logs the Global Lock's give-back after the console's last drain, and `acpi_mediated_access` waits for that line

`acpi_mediated_access` (`tests/toyos.rs`) ends its wait on the kernel's line
`acpi: the Global Lock given back for a holder that left it taken (the machine
is stopping)`. Twice it never arrived, and the harness ended the guest after
`GUEST_QUIET` (15 s) of silence:

```
FAIL acpi_mediated_access: STALLED: waiting for the probe's power-off to give the lock back — it went quiet
```

| where | head | instrument | load | result |
|---|---|---|---|---|
| `main`'s nightly, run 37758546165, `tcg / suite` | `b6bcb9691` | TCG on a hosted 4-core x86-64 runner, 31 tests one wide | the suite alone | STALL after 18 s |
| the development machine, a whole-suite run of #763 | `252d19247` | TCG, x86-64 guest on an AArch64 host, 14 cores, 12 wide | load average 45 to 67: five agents compiling beside the suite | STALL after 35 s |
| `main`'s merge queue, six runs, #749 to #746 (37753990562, 37755528529, 37757410477, 37758981663, 37759626406, 37760845496), `guest / suite` | `6f87cdb9c` to `1084ddc9a` | KVM on hosted 4-core runners, 31 tests one wide | the suite alone | PASS, 3 s each |
| the development machine, `acpi_` by name | `252d19247` | TCG, 2 tests | run alone | PASS, 4 s |

The nightly's head has #749, which added the test, and nothing of #763: the
red is `main`'s. KVM on a hosted runner is not TCG on a loaded host, so the
six green runs say only that the test passes where the guest is fast.

## What was read

`power::shutdown` (`kernel/src/power.rs`) calls `serial::flush_final()` under
the comment "Last chance: nothing drains the log ring after this point", and
then `arch::power::off`. `off` (`kernel/src/arch/x86_64/power.rs`) calls
`acpi_mode::settle`, whose `give_back` logs the line the test waits for, then
`acpi_mode::quiet`, then writes `SLP_TYP` and `SLP_EN`. So the line is
committed after the stop's last drain. Past boot the console's one writer is
`klogd` (`kernel/src/log/console.rs`, `Drain::Thread`), woken at the commit: the
line reaches the host only if `klogd`, on the other CPU, puts it on the wire
before this CPU has made one port read, `quiet`'s writes and the two writes
that end the machine. Nothing waits for it.

That is a reading, and no run confirms it: neither red kept what the guest
said after boot. The test passes `await_guest`'s error up without its capture,
and the `uart-*.log` the nightly kept (artifact `serial-suite`, `uart-6.log`,
the `i8042-withheld` boot) ends where every boot's does, at the hand-over to
the virtio console. It fits both reds: the nightly's 18 s is the 3 s the test
takes under KVM and the 15 s of `GUEST_QUIET`. A probe whose arm failed would
have ended the wait by `===TEST_END`, and a kernel panic by its own line.

The branch that saw it locally moves nothing here: `settle`, `give_back`,
`off`, `shutdown` and the console are `main`'s bytes in #763.

Owner: the `acpi` claim's author (#749).

**Exit**: `acpi_mediated_access` green in the nightly's `tcg / suite`, with the
give-back's line on the wire before `SLP_EN` by construction and not by
`klogd` winning; and a stall of this test carrying what the guest said.
10 changes: 5 additions & 5 deletions kernel/src/arch/aarch64/acpi_mode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,25 @@ use toyos_abi::acpi::AcpiInfo;

use crate::device::ClaimError;

pub fn claim() -> Result<(usize, AcpiInfo), ClaimError> {
pub fn claim() -> Result<(crate::isa::Row, AcpiInfo), ClaimError> {
Err(ClaimError::Absent)
}

/// Never called: nothing is released where nothing can be claimed.
pub fn release(_row: usize) {
pub fn release() {
unreachable!("AArch64 has no ACPI row")
}

/// Never called, as [`release`]: the three are reached only with a claim.
pub fn access(_row: usize, _request: &mut toyos_abi::acpi::Access) -> Result<(), toyos_abi::syscall::SyscallError> {
pub fn access(_row: &crate::isa::Row, _request: &mut toyos_abi::acpi::Access) -> Result<(), toyos_abi::syscall::SyscallError> {
unreachable!("AArch64 has no ACPI row")
}

pub fn lock_take() -> Result<bool, toyos_abi::syscall::SyscallError> {
pub fn lock_take(_row: &crate::isa::Row) -> Result<bool, toyos_abi::syscall::SyscallError> {
unreachable!("AArch64 has no ACPI row")
}

pub fn lock_release() -> Result<(), toyos_abi::syscall::SyscallError> {
pub fn lock_release(_row: &crate::isa::Row) -> Result<(), toyos_abi::syscall::SyscallError> {
unreachable!("AArch64 has no ACPI row")
}

Expand Down
Loading
Loading