Skip to content

Functions arrive and leave: the device set, native PCIe hot-plug, Gone (lifecycle stage 2) - #934

Draft
Japabu wants to merge 41 commits into
mainfrom
wt/toyos-lifecycle-2
Draft

Japabu wants to merge 41 commits into
mainfrom
wt/toyos-lifecycle-2

Conversation

@Japabu

@Japabu Japabu commented Oct 15, 2026 •

Copy link
Copy Markdown
Collaborator

Status: draft, stacked on #935. Head 17e7277dc, built on wt/toyos-aperture-b at f9e44e0fb (main d7f269dbc merged). Until #935 lands this diff carries #935's too. Review round 1's items are answered (round-1 comment). Host CI, both image builds and four QEMU rows are green at the head. Metal at 330a75662: T14 4 passed, AMD 1 passed.

Stage 2 of issues/a-device-function-is-driven-from-its-arrival-to-its-end.md, "Functions arrive and leave". The architecture was ruled on this branch's design round: the kernel owns the slot's interrupt, arrival work runs on the device set's reader, the button means remove, one record per requester, and the _OSC grant rides stage B's declaration. Since #947, "nothing moves a BAR": main's track asked for place_unassigned to become one assignment inside the port's window with no probe, which this round does.

What changes

  • The device set (toyos_abi::devset, object kind 16, SYS_DEVICE_SET 139, SYS_DEVICE_SET_READ 140, toyos::devset; 136 to 138 are left to the open memory PR, and the profile has 142 bins): a level-triggered snapshot of every PCI function the kernel hands out. It is a header (generation, settle time) followed by inventory Pci records in rising arrival order. It is minted from a SysCap carrying DEVICE, with READ and WAIT only, so the supervisor alone holds it. devset::decode refuses by name: no header, a wrong count, a non-PCI record, arrival 0, or a duplicate or out-of-order arrival.
  • Records per requester (kernel/src/pcidev): up to 512 records, each written once and read lock-free, each carrying its live arrival. A returning device is a new arrival on the same record and reuses its emptied domain. Every call a claim of an older arrival makes answers Gone. A departed function's release asks the device nothing (tear_down_departed), and its retirement drops its BARs from Machine::decoded, its reset entry, parked grants and residue.
  • Native PCIe hot-plug (kernel/src/pcidev/slot.rs, toyos_pci::hotplug):
    • A slot is armed from pcidev::declare, once stage B's declaration is held, below a root whose _OSC commit was granted native hot-plug and capability structure control (bits 0 and 4, PCI Firmware 3.3 §4.5.1). Every port says why its slot is or is not armed.
    • acpiserver commits both bits where the root's query offered both (osc::negotiate_root) and declares the grant; q35's default 0x1E mask gets no commit.
    • Native hot-plug fails closed (orchestrator's ruling, holding the owner's "refuse external"; unknown isolation is an error). No firmware signal says a port is internal: ACPI 6.5 defines none, and Microsoft's ExternalFacingPort _DSD property ("Kernel DMA Protection") marks only external ports. So acpiserver commits a root's native hot-plug only where its image passes --hot-plug-ports-internal. Even then it does not commit a root with a port marked external, or one whose _DSD it cannot read. Only the QEMU hot-plug test's image declares it, and a build host test holds that. Without a commit the kernel arms no slot, so no metal slot is armed until claims behind external ports are refused in code (the track's exit).
    • An armed port is the kernel's: the set lists it as kernel-driven, and a claim on it answers KernelDriven.
    • Slots arm from sys_acpi once the declaration has answered, outside the acpi claim's borrow.
    • The handler is bounded. Powering a slot, §6.6.1's 100 ms, and reading the functions below it run on the set's reader, serialised so that two readers never enumerate one arrival twice.
  • An arrived function's BARs (replacing place_unassigned and behind_bridges):
    • Before the function is listed, each BAR it leaves unassigned is sized and given the lowest address inside its port's windows that no other function decodes in (hotplug::Windows, hotplug::assign, host-tested), whole 4 KiB pages, a 32-bit BAR below 4 GiB.
    • The address must lie inside the declared root bridges' memory. It is written once and kept where the register reads it back. Nothing reads through it.
    • The BARs enter Machine::decoded, so bars() and pages::of_bar treat them like firmware's.
  • IOMMU:
    • VT-d binds a function with no context entry yet.
    • SMMUv3's unclaimable refuses only a function that arrived after the unit was programmed, whose IORT route was never read.
    • A fault from a function the fault table does not know is handed to pcidev.
  • toyos-supervisor and the supervisor: as in round 1, and main's drive_where now reads the set, says what joined and left, and decides over the rows asked for.

Deleted: pcidev::FUNCTIONS and arrival_of, VT-d bind's "every function was enumerated" assertion, and the supervisor's PCI reading from the inventory.

The guest row and why it needs QEMU

a_function_arrives_and_leaves runs on Profile::HeadlessHotplugPort: q35, an empty pcie-root-port, and acpi-pci-hotplug-with-bridge-support=off, whose _OSC offers 0x1F.

  • It plugs an edu over QMP. The test requires the kernel's line that its BAR 0 was assigned inside the port's window, then a pciholder.
  • device_del presses the button: the claim answers Gone on its interrupt read, config read, BAR map and DMA grant; the holder ends with 0; the supervisor says the arrival left.
  • A second plug is a new, larger arrival.

No bench has a hot-plug port the owner lets a driver claim behind, so physical arrival is QEMU-only. The slot's, the assignment's and the supervisor's decisions are host-tested.

Checks and evidence

ToyOSOrg/evidence pr-934 at 01fa36f6a for the rows through addf8e97b; that commit was published through the GitHub API after the clone was refused, which the owner is deciding on. Logs from 47dc0138f on are kept locally and not published.

check revision result
cargo run -- --ci host 17e7277dc exit 0, 93 steps green
--build-only, x86-64 and aarch64 17e7277dc exit 0, 0
a_function_arrives_and_leaves, a_drives_row_holds_one_process_per_function, virt_claim_lpi, the_log_lands_on_the_boot_stick 17e7277dc exit 0, 4 passed
17 QEMU rows addf8e97b exit 0, 17 passed
controls: assignment dropped; no commit; external port committed anyway; hot-plug image without its declaration c5f97d961, 47dc0138f, 330a75662 each red
metal 330a75662 T14: acpi_tables_loaded, claim_reuses_its_remapping_entry, usbd_serves_the_boot_stick, usbd_drives_the_type_c_controller passed; its root reads hot-plug ports are not declared internal, so native hot-plug is not committed and no port arms. AMD: amdvi_translates_the_stick_controller passed. The QEMU rows above agree since, and the changes after it are ABI numbering, an armed port's bookkeeping, where arming runs, and main's merge, none of which a slot-less metal boot reaches; no re-boot is needed.

Evidence through addf8e97b is ToyOSOrg/evidence pr-934 at 01fa36f6a, published through the GitHub API after the clone was refused, which the owner is deciding on; later logs are kept locally.

Round 1's stand-in-grant.patch evidence is superseded: the row now runs on stage B's real commit.

Risks and open points

  • No machine but QEMU's test arms a hot-plug slot until claims behind external ports are refused in code; the staged T14 acpi_tables_loaded boot should show not declared internal for its root.
  • acpiserver commits before the kernel knows whether a slot can be armed. On a machine whose units remap no interrupt, firmware gives hot-plug up and nothing serves it (recorded in the track).
  • PCI root bridges are acpiserver's declaration: each root's _CRS windows and _OSC offer, held by the kernel; the loader's window reads go (PCI apertures stage B) #935's open decision (a kernel driver's domain before the declaration) also applies here: cargo run -- --arch aarch64 panics on its console.
  • A surprise removal is the handler's path with no guest reaching it; 512 records is a hard bound; a departed claim's release relies on the domain and remapping while the card may still be powered (button case).

🤖 Generated with Claude Code

https://claude.ai/code/session_01PXEk8BBMDT8tXu4AnENB9Q

Japabu and others added 18 commits October 14, 2026 20:54
…clare, pcidev state)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016UzbDnM9m6nQWhbjGi6Lv1
# Conflicts:
#	kernel/src/pcidev/mod.rs
#	lib/acpi/tests/resource.rs
…vers map at identity

Work in progress before review; the PR body states the change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016UzbDnM9m6nQWhbjGi6Lv1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016UzbDnM9m6nQWhbjGi6Lv1
…the _OSC grant

Ruled for lifecycle stage 2, which arms hot-plug ports under a granting
root: acpiserver negotiates each root's _OSC under the PCI host bridge UUID
(PCI Firmware Specification 3.3 section 4.5) in the pass that reads its
_CRS, and the kernel keeps the records and says each grant.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016UzbDnM9m6nQWhbjGi6Lv1
Stage 2 of issues/a-device-function-is-driven-from-its-arrival-to-its-end.md.

- The device set (toyos_abi::devset, SYS_DEVICE_SET/SYS_DEVICE_SET_READ):
  a level-triggered snapshot of every PCI function the kernel hands out,
  minted from DEVICE with READ and WAIT only, so the supervisor alone
  holds it. Its header carries the time by which a slot may change with
  no interrupt (PCIe 6.0 §6.6.1's wait, a command's §6.7.3.2 timeout).
- pcidev keeps one record per requester, published one at a time, each
  carrying its live arrival: a returning device is a new arrival on the
  same record, so its domain id is reused. A claim of an older arrival
  answers Gone on every call; a departed function's release asks the
  device nothing, and its windows, firmware BARs, parked grants and
  residue are retired.
- Native PCIe hot-plug on a port below a host bridge whose _OSC granted
  native hot-plug and capability structure control. The port is the
  kernel's, its MSI-X entry armed through the claim path's remapped
  message; the handler reads and clears Slot Status, takes a lost or
  button-removed slot's functions out of the set, issues at most one
  Slot Control write and posts the set's watch. Powering a slot, the
  100 ms wait and enumeration run on the set's reader. The last release
  behind a removed slot powers it off.
- An unassigned BAR behind a bridge is placed by
  placement::behind_bridges and accepted when the register reads the
  written address back with its flag bits intact.
- toyos-supervisor: a departed arrival is never started again and is
  forgotten once nothing runs on it; the restart budget is the address's.
- The supervisor reads the set, says what joined and left, and waits on
  its watch and its settle time.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016UzbDnM9m6nQWhbjGi6Lv1
# Conflicts:
#	kernel/src/arch/aarch64/smmu/domain.rs
#	kernel/src/arch/x86_64/amdvi/domain.rs
#	kernel/src/arch/x86_64/amdvi/mod.rs
#	kernel/src/pcidev/mod.rs
#	tests/toyos.rs
…to nothing is no BAR

A slot's handler posts the device set's watch, which the scheduler must
exist for: the boot-time call is made after scheduler::init (a real
declaration arrives from acpiserver later still). An unassigned register
behind a bridge is placed only where its size probe answers a size:
QEMU's edu's BAR 1 reads 0 and sizes to nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016UzbDnM9m6nQWhbjGi6Lv1
…clared bus-mastering site

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016UzbDnM9m6nQWhbjGi6Lv1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016UzbDnM9m6nQWhbjGi6Lv1
…eclaration fails every metal boot

The AMD bench's root has more memory windows than the per-root bound of
8, so at 4f5142f its declaration was refused, every claim on it would
have been, and no row saw it: its boot claims nothing and its row judges
kernel-driven domains. The windows are now one list of 64 across every
root; acpiserver says each root's count and its _CRS bytes under OWN,
so the bench's next boot records them. Every metal boot now fails on the
kernel's refused-declaration record, and the AMD row requires the
declaration. The host bridge _OSC tests gain QEMU q35's masked shape.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016UzbDnM9m6nQWhbjGi6Lv1
…ried only, the no-acpi-row loss recorded

- A declaration whose records the ABI does not name (a count or run past
  its array, an unnamed _OSC word, a reversed bus range) or whose roots
  share a bus on one segment is refused through Held like any other
  refusal: logged, posted, and the answer (toyos_pci::aperture::records).
- The root _OSC is a query alone and its offer is recorded; the hot-plug
  service that acts on native hot-plug control (#934) commits it, so the
  osc module's condition holds.
- issues/an-x86-machine-with-no-acpi-row-hands-over-no-pci-claim.md.
- window.rs: Addresses::map's contract stated per arm.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016UzbDnM9m6nQWhbjGi6Lv1
Owner ruling: the kernel stops driving PCI functions (step 10) before this
lands, so no kernel driver needs a domain before the declaration. The
window rule is the one rule again, for every domain: above the CPU where
the unit has room, and otherwise clear of the declared windows, with no
domain where nothing is declared. A claim passes its declaration; a
kernel driver, which comes up before any declaration, passes none.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016UzbDnM9m6nQWhbjGi6Lv1
Japabu and others added 11 commits October 15, 2026 07:49
So #934 commits through this ABI unchanged: a root's record holds what
its query offered and what a commit (Query clear) was granted, none in
stage B. A commit beside no offer, or of bits not offered, is a record
the ABI does not name, refused by name with the declaration.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016UzbDnM9m6nQWhbjGi6Lv1
Conflicts resolved against main's changes since 3b77b9b:

- #947 deleted kernel/pci/src/placement.rs and
  issues/no-boot-reaches-the-retry-that-undoes-a-placement-nothing-answered.md;
  the branch's hunks there (a doc comment and an import, and the issue's
  exit) were about the placement that no longer exists, so both stay
  deleted. issues/an-aarch64-cargo-run-boot-hands-over-no-pci-claim.md
  stays deleted: main's one-line edit restated how its windows are read,
  which this branch replaces.
- K1's IOVA allocator (6cb27ce): a domain's allowed addresses are still the
  unit's range less every reserved range, but the root bridges' windows now
  come per domain from the declaration (`Domains::prepare`), beside the
  backend's own reserved ranges (RMRRs, IVMDs, the interrupt range, AMD-Vi's
  special range, the ITS doorbell), which `avoid` keeps. No declaration, no
  domain. `OwnSpace::create` takes the declared windows; the IOMMU
  selftests, which run before any declaration on QEMU's root bus, pass none.
- pcidev: main's `publish` without the loader's list, and the declaration,
  its wait and its refusals as on the branch.
- KernelArgs: main's `slot_volume_guid` follows `microcode_len` at 456;
  472 bytes.
- The supervisor (27b75c3): the `acpi` claim's holder starts before the
  disk drivers, whose claims wait for its declaration.
- acpicase: main's nvme and usbd rows claim PCI functions on a boot whose
  only `acpi` holder is a job, so it keeps no storage rows; the probe's
  held-function arm takes the console function the kernel drives, the only
  function a kernel driver still holds.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXEk8BBMDT8tXu4AnENB9Q
Main's benchcase and hostedclangcase came with nvme and usbd rows and no
`acpi` claim holder, so on this branch their first claim would wait out its
deadline and every claim of the boot would be refused. Both start
acpiserver now, and a build host test holds the rule for every config:
one whose rows claim a PCI function starts a row holding the `acpi` claim,
undeclaredcase alone excepted, since it is booted to judge that refusal.

Issues directly affected by the loader's window reads going:
- the GCD map's held MMIO descriptors: deleted, since the loader reads no
  GCD map any more;
- the hypervisor and ACPI tracks and the BarUnrouted tooling issue name
  `_CRS` where they named the loader's reads;
- the x86 no-acpi-row issue names the disk drivers, which main's block
  service split made the claimants;
- new: the ACPI server's death has no readback on the T14, since its
  boot's first `acpi` holder is a job and no row declares before usbd's
  claim.

Issues that named closed drafts as owners: the four scheduler findings
#872's S2 and S3 were to settle are the placement track's, which owns the
push, the victim choice and wake placement; the speed track's line naming
#865 as the CPU cap's owner goes, since #976 sized every per-CPU table for
the CPUs firmware names; the ACPI track's `_CST` line keeps its issue and
drops #870.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXEk8BBMDT8tXu4AnENB9Q
The merge of main left the kernel's virtio console with no domain before
the declaration: main's IOVA allocator builds every domain clear of the
root bridges' windows, and the console comes up before acpiserver declares
them. On VT-d the console keeps the identity domain and works; on virt's
SMMUv3 its stream aborts, so `cargo run -- --arch aarch64` panics in
`wait_used` before userland. The issue this branch had closed stays open
with that cause, owned by this track, since how a kernel driver gets a
domain before any declaration is the owner's to decide.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXEk8BBMDT8tXu4AnENB9Q
benchcase: main's osbench round 2 beside the ACPI server this branch
starts there.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXEk8BBMDT8tXu4AnENB9Q
…2d60) into wt/toyos-lifecycle-2

Lifecycle stage 2 rebuilt on main and stage B.

- pcidev: stage B's module is the base. The per-requester records, the
  device set's generation and snapshot, `Gone` on an older arrival's claim,
  the departed release and retirement, and the slot's interrupt are this
  branch's, over main's BAR records (`Decoded`); `Machine::windows` and
  the placement state are gone with #947.
- An arrived function's BARs: `place_unassigned` and `behind_bridges` are
  gone. Each BAR the device leaves unassigned is given, before the
  function is listed, the lowest address inside its port's windows that no
  other function decodes in and that the declared root bridges' memory
  holds (`toyos_pci::hotplug::{Windows, assign}`, host-tested), written
  once and kept where the register reads it back, with no probe through
  it. Its BARs enter `Machine::decoded` and leave it on retirement.
  `bar::placement` stays for that one write, refusing only an I/O BAR and
  asserting the caller's own choice.
- The slots are armed by stage B's declaration, from `pcidev::declare`,
  below a root whose `_OSC` commit was granted native hot-plug and the
  capability structure. acpiserver commits both where its query offered
  both (`osc::negotiate_root`); the declaration carries the grant. The
  boot-time arm with no bridges is gone.
- A slot's port reads its own bus numbers and windows: main deleted
  `toyos_pci::bridge` and `PciDevice::bridge`. `PciDevice::header_type`
  tells a bridge below a slot.
- Two readers of the set never enumerate one arrival twice (`REFRESHING`).
- ABI: the device set's object kind is 15 and its syscalls 135 and 136,
  past main's; the static `IrqWatch` poll returns for its watch.
- supervisor: main's `drive_where` reads the device set, says what joined
  and left, and decides over the rows asked for.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXEk8BBMDT8tXu4AnENB9Q
Stage B's reopened aarch64 issue and main's osbench round 2.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXEk8BBMDT8tXu4AnENB9Q
- slot: an armed or refused slot names its root bridge's `_OSC` commit,
  which is what arms it now.
- devset: the malformed-snapshot test's non-function record is a `Loaded`
  one; main retired the inventory's `Block` record.
- bar: clippy's checked conversion in `placement`'s 32-bit assert.
- iommu: a stray blank line goes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXEk8BBMDT8tXu4AnENB9Q
`unclaimable` refused every function the IORT does not route through the
unit, which pre-empted the message refusal `virt_claim_lpi` judges for an
e1000e the IORT routes straight to the ITS. It now refuses only a function
that arrived after the unit was programmed, whose route was never read:
the unit keeps the functions enumerated then.

`a_function_arrives_and_leaves` requires the kernel's line that the
plugged `edu`'s BAR 0 was assigned inside the port's window, and reads the
holder's end as the supervisor now words it (`ended, code=0`).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXEk8BBMDT8tXu4AnENB9Q
Kernel clippy's type-complexity and argument-count lints.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXEk8BBMDT8tXu4AnENB9Q
The deferred-callback issue they pointed at went with the kernel's xHCI;
the owner is the lifecycle track again, and the exit is its own.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXEk8BBMDT8tXu4AnENB9Q
Japabu and others added 5 commits October 22, 2026 12:20
…ng port

The orchestrator's ruling, holding the owner's "refuse external": until a
claim behind an external port is refused in code, no such port's slot is
armed. The `_OSC` commit is the root's, so a root with a port whose `_DSD`
carries Microsoft's `ExternalFacingPort` property (the Kernel DMA
Protection one Thunderbolt root ports carry), or whose port's `_DSD` cannot
be read, is queried and not committed, and the kernel arms no slot below
it. A firmware that marks no port reads as all-internal; the track records
that and the exit.

Host-tested: the `_DSD` reading (external, not, other UUIDs, malformed
packages refused) and a root's commit with an external port, an internal
one and an unreadable one.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXEk8BBMDT8tXu4AnENB9Q
… its ports internal

The orchestrator's ruling under root CLAUDE.md: unknown isolation is an
error, and the owner's "refuse external" stands. No firmware signal says a
port is internal. ACPI 6.5 defines none, and Microsoft's
`ExternalFacingPort` `_DSD` property marks only the external ports, so a
port without it is unknown. acpiserver therefore commits a root's native
hot-plug only where its image passes `--hot-plug-ports-internal`. Even
then it does not commit a root with a port marked external, or one whose
`_DSD` it cannot read. Without a commit the kernel arms no slot.

Only the QEMU hot-plug test's image declares it, for QEMU's own root
port, and a build host test holds that.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXEk8BBMDT8tXu4AnENB9Q
Its job must make the boot's first acpi claim, so no row declares the root
bridges' memory, and usbd's stick claim, which carries the log, waits out
the deadline. The T14 boot at 4e32df6 ran the job and stopped cleanly,
and its kernel.log came back empty. A row that cannot produce a readback is
a defective registration, so it goes with its judge and the helper only it
used. No QEMU row takes its place: QEMU's firmware hands over in ACPI mode,
so the release has no legacy mode to return to. `acpi_release` stays,
run by nothing. The issue's exit is the row registered again on a boot that
has a log, and the issues and the ACPI track that read the row point there.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXEk8BBMDT8tXu4AnENB9Q
- KernelArgs: main's boot images (#994) without the loader's root bridge
  windows; 504 bytes, root_image at 184.
- loader: main's payload module, no rootbridge.
- supervisor: start_service takes main's spawn (the image's source, the
  boot images) and skips SYSTEM, which starts before the manifest.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXEk8BBMDT8tXu4AnENB9Q
The device set moves past main's numbers and every open PR's: object kind
16 (main's Notice is 15), and SYS_DEVICE_SET 139 and SYS_DEVICE_SET_READ
140 (main's SYS_PROCESS_QUIT is 135, and an open memory PR claims 136 to
138).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXEk8BBMDT8tXu4AnENB9Q
@Japabu

Japabu commented Oct 22, 2026

Copy link
Copy Markdown
Collaborator Author

Review of #934 (lifecycle stage 2): pushed 330a75662, plus local c65ba5dd5

I reviewed the pushed head 330a75662 and the local merge c65ba5dd5, which is not pushed yet. The merge takes #935's 3d6257080 (main d7f269dbc) and renumbers the ABI. Over #935's coming head the diff is 39 files, +2510/−270. About 600 of the added lines are tests:

  • toyos_pci::hotplug, about 170;
  • toyos-supervisor's set diff, about 80;
  • toyos_abi::devset, about 65;
  • acpiserver's _OSC and _DSD cases, about 90;
  • the QEMU row and its profile, about 120.

The new kernel files are kernel/pci/src/hotplug.rs (pure, host-tested) and kernel/src/pcidev/slot.rs (about 500 lines). The deletions are pcidev::FUNCTIONS, arrival_of, VT-d bind's enumerated-only assertion and the supervisor's reading of the inventory for PCI.

Architecture

The code follows the ruled design in issues/a-device-function-is-driven-from-its-arrival-to-its-end.md:

  • The device set is a level-triggered snapshot. It is minted from DEVICE with READ and WAIT only, and devset::decode validates it at the reader's boundary.
  • Slot work is the reader's. The port's interrupt is the kernel's and is bounded: it reads and clears Slot Status, departs the slot's functions, makes at most one Slot Control write and posts. Powering, §6.6.1's wait and enumeration run in pcidev::snapshot on the set's reader, and the wait is a settle time in the header, not a spin.
  • One record per requester, with its live arrival. Every Binding call checks here() and answers Gone. A departed release asks the device nothing, and retire drops its BARs, reset entry, retired grants and residue.
  • place_unassigned is replaced as main's track asked. hotplug::assign finds the lowest page-clean address in the port's windows. place then requires the result to lie inside a declared root window. The BAR is written once and kept only if it reads back. Nothing is probed through it, and no firmware-assigned BAR moves.
  • Slots are armed only from pcidev::declare, below a root whose _OSC commit was granted bits 0 and 4. acpiserver commits only where the query offered both. It needs the image's --hot-plug-ports-internal, and even then refuses a root with an ExternalFacingPort port or an unreadable _DSD.
  • The fail-closed rule is attributed honestly: the orchestrator's, holding the owner's "Refuse external" (issues/the-iommu-refuses-nothing-yet.md). The T14 readback at 330a75662 shows it working: \_SB_.PC00's hot-plug ports are not declared internal, so native hot-plug is not committed, then each of 00:07.0, 00:07.2 and 00:1c.0 reads not armed: its root bridge's _OSC committed nothing.
  • IOMMU. The SMMU's unclaimable refuses only streams missing from Live::enumerated. VT-d builds a context table for a late arrival, and its not-present entry stays blocked until a claim attaches it. AMD-Vi's device table is all-blocked by default (amdvi::build), so an unclaimed arrival reaches nothing on any unit.

Required

  1. ABI numbers at the pushed head collide with main.

    • 330a75662 defines SYS_DEVICE_SET = 135 and ObjectKind::DeviceSet = 15. Main (kernel: a process is asked to quit, and one that never listens is killed #995) already has SYS_PROCESS_QUIT = 135 and ObjectKind::Notice = 15.
    • The local c65ba5dd5 moves the set to 139/140 and kind 16. It skips 136-138 for the open memory PR and bumps SYSCALL_PROFILE_BINS to 141. That is coherent against main, and ObjectKind::ALL's word-order assertion holds.
    • Push it. The PR body's "SYS_DEVICE_SET 135, SYS_DEVICE_SET_READ 136" and the handoff's "renumbered past main" are false at the pushed head, and correcting them is the orchestrator's job.
  2. kernel/src/pcidev/slot.rs:128 with pcidev/mod.rs:555-570: an armed hot-plug port is reported as claimed by a process.

    • arm_one takes the port's held for the kernel and never records it in Machine::kernel_driven.
    • So inventory(), and with it the device set, lists the port as Driven::Claimed, which the ABI defines as "a process holds a claim on it". No process does.
    • A claim on the port also answers Owned instead of KernelDriven.
    • Fix: record the port as kernel-driven where it is armed. note_kernel_driver's meaning, "a claim on one of them would be two drivers on one device", is exactly this case.
    • Cheapest proof: extend a_function_arrives_and_leaves to read the port's driven in the supervisor's set, or say it in the slot's armed line.
  3. Evidence at the head that will land.

Suggestions

  1. slot.rs:290-305: a slot can stall silently.
    • A powered, occupied slot whose link never reports up answers Next::Nothing with no settle time. Nothing logs it and nothing asks again, unless a Data Link Layer State Changed interrupt arrives. QEMU's port does not keep Link Changed Enable (the track's own measurement).
    • Root CLAUDE.md asks that waits expose stalled progress.
    • Fix: give such a slot a settle deadline and log the stall once it passes, with the bound cited from the specification.
  2. pcidev/mod.rs:1465 against :1908: a contradicted doc.
    • The doc says "a function that left the device set is never asked anything".
    • note_fault now clears Bus Master Enable through the config space of a departed function that still faults. That is a sound defence for a button-removed card that is still powered.
    • The doc should say so. Note also that note_fault now clears BME for held boot functions too, which the fault table already does.
  3. kernel/src/syscall/device.rs:101 with mod.rs:784: slot::arm runs inside the acpi claim's borrow.
    • It makes MSI-X entry claims, IOMMU message claims and config writes for every port, all inside claim.isa(..). object/device.rs:95-97 states the contract for that closure: "f does not wait".
    • Either confirm that messages::claim and isolates never wait on a unit's invalidation, or arm after the borrow returns, still on the declarer's thread.
  4. slot.rs:51: the refresh lock is held across enumeration.
    • REFRESHING is a ticket spinlock held across a whole enumeration: up to 8 functions' BAR sizing, MACHINE and log!. That is bounded, but a second reader spins for it.
    • The supervisor's pci_records mints and reads a second set handle, so two readers in one process can meet.
    • Fine as it is. Worth a line in the lock's doc if it is kept.
  5. build/build.rs:3106: a gate for a rule a reviewer can see. only_the_hot_plug_test_declares_its_ports_internal checks something visible in review, which root CLAUDE.md prefers to keep in instructions. I would keep it, because it holds a security ruling cheaply, but that is the orchestrator's call.
  6. §6.6.1 readiness. The 100 ms after the link comes up is cited at READY_NANOS. Readiness Notifications (§6.23: DRS and FRS) remove it on ports that support them. That capability is unused and unrecorded; a line in the track's gaps would cover it.

Evidence checked

SEND BACK on items 1 to 3. #935's open owner decision also applies here, as the PR body says.

Japabu and others added 7 commits October 22, 2026 14:34
- iommu::DeviceSpace::create's doc and the virtio console's comment say
  what the code does today: a driver in this kernel comes up before the
  declaration and keeps the machine's addresses. The owner's decision on
  that changes the code later.
- acpi_release goes with its row and judge. Nothing ran it, and the
  issue's exit restores all three.
- The aarch64 issue says the console loses the domain main gives it.
- The two new issues are owned by the ACPI track. The PR they were named
  after closes on landing.
- Docs: MAX_WINDOWS states the AMD bench's measured 16, `Osc::Offered`
  names no stage, and the config test says which claims it does not see.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXEk8BBMDT8tXu4AnENB9Q
#935's review round 2.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXEk8BBMDT8tXu4AnENB9Q
…he claim's borrow

- arm records an armed port in `kernel_driven`. The device set lists it as
  the kernel's, not as held by a process that does not exist, and a claim
  on it answers `KernelDriven`. Its armed line says so, and the hot-plug
  row waits for that line.
- Slots arm from `sys_acpi` once the declaration has answered, outside
  `claim.isa`'s borrow, whose closure must not wait. Arming claims a
  message from the units and writes each port's configuration.
- Docs: a departed function's only configuration access is `note_fault`
  clearing its bus mastering; `REFRESHING` says what a second reader waits
  out.
- The track records two gaps: a slot whose link never comes up stalls
  unsaid, and Readiness Notifications are not read.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXEk8BBMDT8tXu4AnENB9Q
…r it is listed

Since #994, SYSTEM is the first service in the list, started before the
manifest was read. The holder count taken from the list's front therefore
found none, the disk drivers' claims were made before acpiserver ran, and
they waited out the declaration's deadline. `the_log_lands_on_the_boot_stick`
went red on it after the merge of main. The declaring rows are now
partitioned out of the whole list and started first, and the session home
step makes every declarer's home.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXEk8BBMDT8tXu4AnENB9Q
The supervisor starts every acpi holder before the disk drivers.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXEk8BBMDT8tXu4AnENB9Q
SYS_DEVICE_SET_READ is 140, so the bins are 142: one for every number
up to it, and SYSCALL_PROFILE_OTHER after them. 141 failed the const
assertion that holds that.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXEk8BBMDT8tXu4AnENB9Q
Main's `watched_readable` (#995) matches every object kind, and the
device set's arm was missing after the merge.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXEk8BBMDT8tXu4AnENB9Q
@Japabu

Japabu commented Oct 22, 2026

Copy link
Copy Markdown
Collaborator Author

Round 1 at 17e7277dc (#935's f9e44e0fb and main d7f269dbc merged).

  1. ABI numbering: object kind 16, SYS_DEVICE_SET 139, SYS_DEVICE_SET_READ 140. The numbers 136 to 138 stay free for the open memory PR (mm: the vm_* lifecycle calls, std and libc on them, and the hot paths the T14 measured (memory stage 3a) #1008).
  2. An armed port is the kernel's. arm records it in kernel_driven, so the set lists it as kernel-driven and a claim on it answers KernelDriven. Its armed line says so, and the hot-plug row waits for that line.
  3. Evidence at the head:

Suggestions:

  • Taken:
    • 3: slots now arm in sys_acpi after claim.isa returns, on the declarer's thread. declare itself only locks and posts.
    • 2 and 4: docs corrected.
    • 5: the gate stays, as the orchestrator ruled.
    • 1 and 6 are recorded in the track's gaps: a slot whose link never comes up stalls silently, and Readiness Notifications are not read. Each has an exit.
  • Not taken: 1's deadline itself. The bound needs a citation I have not read in the specification, so the gap's exit asks for it.

@Japabu

Japabu commented Oct 22, 2026

Copy link
Copy Markdown
Collaborator Author

Review of #934, round 2, at 17e7277dc

17e7277dc is pushed. It merges #935's f9e44e0fb and merges cleanly with origin/main c2fa26319.

Round 1 items

  1. ABI numbers: resolved.
    • Object kind is 16 (main's Notice is 15), and the syscalls are 139 and 140 (main ends at SYS_PROCESS_QUIT 135; 136–138 are left to the open memory PR).
    • SYSCALL_PROFILE_BINS is 142, so SYSCALL_PROFILE_OTHER is 141. 1f8edda28 was needed: at c65ba5dd5 the bins were 141, which made OTHER 140 and failed the const assertion SYS_DEVICE_SET_READ < SYSCALL_PROFILE_OTHER. That head did not compile.
    • ObjectKind::ALL's word-order assertion holds.
    • The watched_readable arm for DeviceSet (17e7277dc) is required by main's exhaustive match from kernel: a process is asked to quit, and one that never listens is killed #995. A device set has no listener notice, so the empty arm is right.
    • The PR body's "135/136" still needs correcting (the orchestrator's job).
  2. Armed port reported as claimed: resolved.
    • slot::arm records the port's requester in Machine::kernel_driven under the same MACHINE lock that marks it armed.
    • So inventory(), and with it the device set, now lists the port as Driven::Kernel, and a claim on it answers KernelDriven.
    • The armed line says the port is the kernel's, and a_function_arrives_and_leaves now waits for that line. The driven value itself is established by inspection of inventory(), which is sufficient.
  3. Evidence at the head that will land: resolved for host and QEMU. The logs carry no revision line, but their timestamps (15:23–15:35) all fall after 17e7277dc (15:22:06):

Suggestions

  • Arming outside the claim's borrow (suggestion 3): taken.
    • sys_acpi now calls pcidev::arm_slots() after the borrow returns, only when DECLARE answered Ok, and still on the declarer's thread.
    • Only one declaration can answer Ok, and arm skips ports already armed, so arming cannot run twice.
  • The silent link stall and Readiness Notifications (suggestions 1 and 6): recorded as gaps in the track, each with its exit.
  • The note_fault doc (suggestion 2): corrected. release's doc now names the one config access a departed function still gets.
  • The refresh spinlock (suggestion 4): documented. REFRESHING's doc states its bound.

Verdict

No review blocker remains. #934 is stacked on #935, so it lands after #935 and after the same metal re-run. #935's item 53 applies here too.

LAND on the review's findings, after #935.

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