Skip to content

iommu: lowest-first device addresses below each claim's stated limit; a claim masters only when its holder starts it - #974

Merged
Japabu merged 6 commits into
mainfrom
wt/toyos-dma-limit
Oct 21, 2026
Merged

Japabu merged 6 commits into
mainfrom
wt/toyos-dma-limit

Conversation

@Japabu

@Japabu Japabu commented Oct 21, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

A domain's device addresses started a quarter of the way up the unit's range, above all memory. The AMD bench's HDMI audio function 03:00.1 (1002:1637) sets GCAP 64OK, but it issues only address bits 0–39 in both of its DMA engines (probe evidence below). So it faulted on every grant.

Two other problems sat under that:

  • A grant above a function's real width is issued truncated, and can land on another grant of the same domain.
  • A claimed function started mastering at its first grant, whatever state firmware or a previous holder left it in. On the AMD bench, firmware-held 03:00.3 faulted against usbd for exactly this reason (usbd drives every xHCI controller; the kernel drives no USB #971).

What this does (owner decisions K1–K3, 2026-10-21)

  • Lowest-first allocation, below a stated limit (K1). The allocator is kernel/pure/iova.rs, pure and host-tested.

    • A domain's allowed addresses are the unit's range less: the first leaf, the root bridges' windows, firmware's IVMD and RMRR regions, the x86 interrupt range (VT-d 5.0 §3.15; AMD 48882 §2.2.9), AMD-Vi's special range FD_0000_0000..2^40 (48882 §2.1.2, Table 3), and the SMMU's ITS doorbell page.
    • Addresses are handed out lowest first and each exactly once. The lend-window and residue rules are unchanged.
    • The new syscall device_dma_limit(claim, bits) takes 32–64 bits. It is stated once per claim, before any grant or lend, and every grant ends at or below that limit. Running out is ResourceExhausted.
    • Deleted: the above-RAM rule, the quarter floor, the CPU-width branch (physical_address_bits) and WindowBelowMemory. Without a declaration of the root-bridge windows there is no domain, on every backend.
  • DMA held off until the holder starts the function (K2).

    • At hand-over, the kernel clears and drains the function's Bus Master Enable before attaching its domain.
    • The function masters only once its holder calls the new syscall device_dma_start, after the driver's own stop or reset.
    • The kernel treats that call as no proof of safety: the function still reaches only its domain.
  • Each driver's stated width:

    Driver Width Basis
    virtio, via negotiate 64 virtio 1.2 §2.7.5
    nvme 64 NVMe addresses are 64-bit
    i219 64 82574 §7.1.3, §7.2.10.1
    usbd 64 it already requires AC64
    hda 64OK → 64, otherwise 32 HDA 1.0a §3.3.2
    kernel xHCI AC64 → 64, otherwise 32 xHCI §5.3.6
    kernel virtio-console 64 virtio 1.2 §2.7.5
  • 1002:1637 unavailable by name (K3). hda refuses it before any grant, because its 64OK is contradicted and no independent source gives its width. The 40-bit measurement stays as scoped evidence in issues/the-hdmi-audio-function-1002-1637-is-driven-at-no-address-width.md, which replaces a-function-that-truncates-….

  • A device that bypasses translation is refused (fd95b3af0). Virtio 1.2 §6.1: a virtio function that does not offer VIRTIO_F_ACCESS_PLATFORM uses physical addresses, whatever unit sits on the bus. With low addresses, such a function would write wherever an honest holder's addresses land in RAM.

    • The supervisor mints every claim. It now reads the function's offered features through the claim before the claim moves (offered_features writes only the feature select, so the device is not reset).
    • A function that does not offer the bit, or whose offer cannot be read, is refused by name, and the holder starts without it.
    • The kernel still names no virtio (kernel/src/pcidev, kernel/src/iommu).
    • This closes issues/a-virtio-function-that-offers-no-access-platform-is-handed-over-as-translated.md.
  • Judge. domain_ends_below_the_host_bridges becomes domains_map_clear_of_the_host_bridges, judged against every maps … at record.

Not added: a generic reset control for holders, because nothing calls one yet. The design is FLR, AF FLR or PM D3hot, with mastering off; a secondary bus reset only where the bridge's bus holds that function alone.

Checks

  • Host CI: cargo run -- --ci host exit 0 at the tree committed as 6cb27ce9f. The only later edit is a comment in kernel/pci/src/express.rs. 88 steps green, including the control iova-ignores-limit, which reds a_function_that_drops_its_upper_bits_reaches_only_its_own_grant, every_grant_is_the_lowest_allowed_run_below_its_limit and the_amd_benchs_layout.
  • QEMU (cargo test --test toyos-build -- …): all pass at the final tree.
    • q35 VT-d and AMD-Vi: amdvi_selftest, amdvi_firmware_left, bar_map_again, bar_map_behind_a_bridge, hda_plays_to_the_controller_that_reaches_an_output, iommu_virtio_platform, lent_device_memory_refused, nvme_disk_boots, the_compositor_drives_the_virtio_gpu, usbd_drives_the_spare, usbd_refused_without_amdvi_remapping, usbd_refused_without_smi_filter.
    • aarch64 virt: virt_smmu, virt_claim_lpi.
  • What the QEMU runs found:
    • usbd_drives_the_spare reds unless usbd starts the controller before naming any address to it. QEMU's xHCI reads its segment table when ERSTBA is written. The fix moves the start in usbd, and it passes.
    • amdvi_selftest assumed a domain's addresses are memory the identity domain does not map, which is no longer true. It now aims past the memory top.
  • Metal: staged, not yet run (requests in the PR thread). Expected on the AMD bench: the HDMI function is refused by name, the analogue HDA plays, and amdvi_translates_the_stick_controller has no fault.

Checks after fd95b3af0

  • cargo run -- --ci host: exit 0.
  • New row a_virtio_function_without_access_platform_is_refused, on q35 with a virtio-net created without iommu_platform=on: it reads the supervisor's refusal and netstack: no NIC on this machine. QEMU is the only machine that can create such a function.
  • Negative control for that row: with the check deleted, the row reds, because netstack negotiated the NIC with access_platform=n.
  • Green alongside it: iommu_virtio_platform, the_compositor_drives_the_virtio_gpu, hda_plays_…, nvme_disk_boots, bar_map_again, bar_map_behind_a_bridge.
  • Evidence: https://github.com/ToyOSOrg/evidence/tree/f19627f2d931c5e4aff099b66eb8bb18f35bef3e

Metal (run by the coordinator)

  • AMD bench, at 6cb27ce9f: 6/6 PASS.
    • hda_tone, hda_tone_resampled, domains_map_clear_of_the_host_bridges, amdvi_remaps_the_stick_controller, amdvi_usbd_drives_its_own_entry.
    • amdvi_translates_the_stick_controller, now green; it was red before this change.
    • The HDMI function is refused by name, and the analogue HDA plays.
  • T14, at 6cb27ce9f: 5/6 PASS.
  • Not on metal: fd95b3af0 (the supervisor's virtio refusal) and 94968aca2 (an output line). Neither machine has a virtio function, so the refusal only passes through the non-virtio path there; QEMU covers it, with its control.

Review r1 (f8d86c93e)

  • The kernel's virtio console. One that offers no ACCESS_PLATFORM is now given physical addresses (virtio 1.2 §6.1) and stays on the identity domain.
    • New row the_kernel_tells_a_bypassing_console_physical_addresses, on q35 with VT-d and the console created without iommu_platform=on: green.
    • With the fix reverted, the boot never reaches its ready marker.
  • The hand-over drain (completed in r2, 96e929bd1). After BME is cleared and the posted writes are flushed (PCIe Base §2.4.1), the kernel waits for Transactions Pending to clear before attaching the domain.
    • Which bit it waits on: Device Status' (§7.5.3.5) on an Express function. On a conventional function that reports it, the Advanced Features capability's (PCI Advanced Capabilities ECN), which the PCH's integrated functions publish.
    • The wait is bounded at 100 ms, twice the default Completion Timeout range's top (§2.8, Device Control 2 §7.5.3.16). On conventional PCI, which has no Completion Timeout, the same figure serves as a hang ceiling. A function still transacting past it is refused by name (StillTransacting).
    • A truncated capability list is refused (CapsTruncated).
    • A function with neither capability has no pending signal. Its posted writes are flushed, and a read it still had in flight may fault in its new domain against its holder. Isolation holds, because that domain maps only the function's own memory.
  • Doc comment. bring_up's doc is corrected.
  • No kernel panic on a device's width. DmaPool::alloc_in is now fallible on a device's width. The kernel's xHCI and the console refuse such a device by name instead of panicking.
  • Suggestions.
    • Taken: the syscall wrappers moved beside the other device_dma_* ones, and the per-boot address budget is recorded in issues/a-narrow-functions-device-addresses-are-spent-once-per-boot.md.
    • Declined: claiming as PciDev at the mint sites, because the devices path shares one generic claim for every claim kind.
  • Checks.
  • Metal at f8d86c93e: AMD 6/6, T14 6/6 PASS.
    • The Express wait cleared immediately on both benches.
    • No StillTransacting and no hand-over fault on the T14's 00:0d.0, 00:1f.3 and 00:1f.6.

Review r2 (96e929bd1)

  • The Advanced Features Transactions Pending wait and the CapsTruncated refusal described above. af::reports_pending has a host test.
  • The console declines its function when the pool is refused.
  • QUIESCE_NANOS wording and citation.
  • Checks:
  • Metal: the T14 testcases boot is re-staged, so its PCH functions exercise the Advanced Features path. AMD is not re-run: its claimed functions are Express endpoints, and their path differs only by a read-only walk for the Advanced Features capability and the truncation check, which the MSI-X arming before it already refuses.

Risks

  • Residue left by an earlier holder that stated a wider limit than the function really has. This needs three things together:

    1. an earlier driver that stated the width wrongly;
    2. no reset between the two holders;
    3. one function's domain handing out every allowed address below its true width within one boot.

    Only (3) lets an address at or above that width be handed out at all, because allocation is lowest-first. On the AMD bench (3) is 1,704 leaves below 4 GiB, or 9,129 below 2^40. One claim may hold at most 32 MiB of grants (16 leaves), so it takes over 100 claims of that function in one boot, every one of them under the wrong statement.

    If all three hold, a stale transfer truncates onto an address the domain already handed out: a residue placement (fresh pages the new holder was placed at, which is the existing residue rule) or the lend window. New grants are never placed at a residue above the new holder's limit.

  • The ECAM hole is allocatable. That rests on the IOMMU seeing requests before address decode: reasoning, not a citation.

Probe evidence: https://github.com/ToyOSOrg/evidence/tree/30bf1f4f70c2ce4771f0e7393fcc9a64d1c08cd1/probe
Host and QEMU logs: https://github.com/ToyOSOrg/evidence/tree/47debd4a123d06c83f4381d24929ea76f2e10ae4

🤖 Generated with Claude Code

Japabu and others added 2 commits October 21, 2026 11:23
…ted limit, and a claim masters the bus only when its holder starts it

A domain's addresses used to start a quarter of the way up the unit's range,
above all memory. The AMD bench's HDMI audio function 1002:1637 sets 64OK
yet issues only 40 address bits, so every grant it got was out of its reach.
Worse, a grant above a function's real width is issued truncated, and can
then land on another grant of the same domain.

Allocation (owner decision K1): `kernel/pure/iova.rs`, host-tested.
- A domain's allowed addresses are the unit's range less the first leaf,
  the root bridges' windows, firmware's IVMD and RMRR regions, the x86
  interrupt range (VT-d 5.0 §3.15, 48882 §2.2.9), AMD-Vi's special range
  FD_0000_0000..2^40 (48882 Table 3) and the SMMU's ITS doorbell page.
- Addresses are handed out lowest first and each exactly once.
- No grant ends above the limit its holder states. The stated limit is a
  new syscall, `device_dma_limit`, made once per claim before any grant or
  lend.
- The above-RAM rule, the quarter floor, the CPU-width branch and
  `physical_address_bits` are deleted.
- The control `iova-ignores-limit` reds the truncation, model and
  AMD-layout tests.

Hand-over (owner decision K2):
- A claimed function's Bus Master Enable is cleared and drained before
  its domain is attached. Firmware's stale DMA no longer faults against
  the holder.
- A function now masters only once its holder calls the new syscall
  `device_dma_start`, not at its first grant.
- Each driver states its width and starts the function after its own stop
  or reset:
  - virtio, in `negotiate`: 64 bits;
  - nvme and i219: 64 bits;
  - usbd: 64 bits, since it requires AC64;
  - hda: 64OK gives 64 bits, otherwise 32;
  - the kernel's xHCI: AC64 decides, 64 or 32 bits.

Unavailable while investigating (K3): hda refuses 1002:1637 by name. Its
64OK is contradicted and no independent source gives its width; see
issues/the-hdmi-audio-function-1002-1637-is-driven-at-no-address-width.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ed to every holder

Virtio 1.2 §6.1: a device that does not offer VIRTIO_F_ACCESS_PLATFORM
uses physical addresses whatever translates the bus. QEMU keeps such a
function on the machine's own address space, the unit bypassed. Since
domains now hand out their lowest addresses first, an honest holder's
device addresses are also physical addresses of memory, and such a function
would write into memory nothing granted it.

The supervisor mints every claim and is trusted, so it reads the function's
offered features through the claim before the claim moves to its holder.
It reads them without resetting the device: `offered_features` writes only
the feature select. A virtio function that offers no ACCESS_PLATFORM, or
whose offer cannot be read, is refused by name, and its holder starts
without it. Neither `kernel/src/pcidev` nor `kernel/src/iommu` names
virtio.

The QEMU profile HeadlessNicUntranslated creates virtio-net without
iommu_platform=on. The row
a_virtio_function_without_access_platform_is_refused reads the refusal and
`netstack: no NIC on this machine`. With the check deleted, netstack
negotiated the NIC with access_platform=n and the row stalled red.
Closes the issue a-virtio-function-that-offers-no-access-platform-is-
handed-over-as-translated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Japabu and others added 2 commits October 21, 2026 12:02
The traffic line says how many frames the driver received, but not
whether the wire carried any. A quiet network and a receive the driver
never got both read as no frame received. This adds the MAC's own counts
(TPR, GPRC, MPC, CRCERRS), which it already reads, to the output.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Recorded from the T14 comparison of #974 against main, where both runs
failed with the MAC counting no frame seen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Japabu

Japabu commented Oct 21, 2026

Copy link
Copy Markdown
Collaborator Author

Review r1: #974 at 85436374b (base b580e0078)

Architecture

The change fits K1–K3. There is one pure allocator, kernel/pure/iova.rs, and it is host-tested against a model, with a negative control registered in build/ci.rs (so build/clippy.rs picks the feature up through $CONTROLS). It replaces iommu/window.rs together with its above-RAM rule, its quarter floor and physical_address_bits on both architectures. Each backend owns its reservations, and they stay in architecture modules: the x86 interrupt range, AMD-Vi's special range and the SMMU's doorbell page. Hand-over now clears BME before attach. The holder starts mastering through a generic device_dma_start, and the kernel names no device. The 1002:1637 refusal lives in the hda driver (K3), and the virtio refusal lives in the supervisor, as the closed issue's ruling required. I found no superseded path left behind. Nothing names window.rs, "first grant starts mastering", WindowBelowMemory or the old judge name outside one historical issue sentence.

Approximate size: production code grows by about 60 lines net (the allocator replaces window.rs; there are two syscalls and per-driver statements). Tests grow by about 220 lines (the iova model test, the hda width unit, the supervisor unit and one QEMU row).

Allocator correctness:

  • Off-by-one at the limit: stop > limit.end() admits a grant ending exactly at 1 << bits, and WIDEST = u64::MAX is sound because translatable < 64.
  • Ordering and exhaustion: the refusal returns as soon as the lowest fitting run crosses the limit, and that is correct because ranges ascend. Overflow is checked.
  • Placement: handed_out requires the run to lie inside one allowed range.
  • Residue: residue reuse is filtered by the limit.
  • Lend window: the lend window sits at the domain's first 256 MiB, so the dma_limit assert is a real invariant.

I found no defect in iova.rs.

Findings

BLOCKER 1: kernel/src/drivers/virtio_console.rs:236-243. The kernel console's DMA lands in low physical RAM when its function offers no ACCESS_PLATFORM.

  • The kernel's own virtio driver still accepts a function without the bit (virtio.rs:744 only adds the bit where it is offered) and gives it an Own domain.
  • With lowest-first allocation, the pool's device address is now LEAF (0x200000) or just above it.
  • Virtio 1.2 §6: a device without the bit uses physical addresses whatever the IOMMU. So the console reads and writes physical RAM at about 2 MiB.
  • Before this change the address was at least 2^46 and reached no RAM. This is a regression from "the console doesn't work" to "the kernel's memory is overwritten", on any hypervisor that exposes a vIOMMU with a default (iommu_platform=off) or transitional virtio-serial. issues/the-virtio-drivers-match-only-a-non-transitional-device-id.md lists "the kernel's console on every machine" as a path to exactly such functions.
  • The harness hides this because it always sets iommu_platform=on behind a unit.
  • Fix: where offered.features lacks the bit, give the console DeviceSpace::Untranslated, which is what §6.1 requires (a driver MUST pass physical addresses), or refuse it. kernel/src/drivers is not covered by the "pcidev/iommu name no virtio" constraint.
  • Cheapest check: inspection, plus one QEMU boot of an existing profile with the console's {platform} dropped. That boot also serves as the negative control.

BLOCKER 2: kernel/src/pcidev/mod.rs:855-862. The hand-over "drain" drains posted writes only, not in-flight DMA reads (K2).

  • drain_messages is a single config read. A completion may not pass the function's earlier posted writes (PCIe Base §2.4.1, RO clear), but it must be allowed to pass Non-Posted Requests.
  • So a DMA read that firmware or a previous holder issued before BME was cleared can reach the unit after space.attach. It is then translated in the new domain and faults.
  • with_bound turns that fault into Io for the new holder. That is the usbd drives every xHCI controller; the kernel drives no USB #971 symptom the comment says is prevented ("what it had in flight lands … so no fault of firmware's is the holder's"), and K2 explicitly requires in-flight DMA drained before attach.
  • Fix: after clearing BME, poll Device Status Transactions Pending (PCIe Base §7.5.3.5; express.rs already reads that capability) with a failure timeout, before attach. Refuse the hand-over by name if it never clears. Without an Express capability, the posted-write flush is all there is; say that.
  • Alternative: narrow the comment and the PR to "posted writes", and take the remaining gap back to the owner as a change to K2. I don't recommend that, because the fix is small.
  • Evidence needed: none beyond inspection for the logic. The AMD bench's amdvi_translates_the_stick_controller already exercises the hand-over.

BLOCKER 3: kernel/src/pcidev/mod.rs:817-821. bring_up's doc is false.

  • It says it will "arm the interrupt, and only then let it master the bus". bring_up no longer lets the function master at all.
  • The security paragraph under it argues the old order.
  • Fix: restate it as "...arm the interrupt, stop and drain its mastering, and attach the domain; its holder starts it", or delete the second paragraph, which the module header already covers.

BLOCKER 4: kernel/src/mm/dma.rs:219-222 with kernel/src/drivers/xhci/wait/boot.rs:344-345. A device's width now reaches a kernel panic, and the comment that justifies the panic is false.

  • DeviceSpace::Untranslated now refuses pages ending above the limit (iommu/mod.rs). alloc_in panics on any map error, under the comment "a kernel bug, not a device's answer".
  • A kernel xHCI without AC64 that lands in Untranslated will panic the kernel at boot whenever alloc_contiguous returns pages above 4 GiB. Untranslated is reached on a machine without a unit, or now on any machine whose firmware declares no root-bridge windows (Undeclared).
  • The refusal is right; the panic and the comment are not. A hardware capability is an external input.
  • Fix: make alloc_in fallible for that case, so init_one, which already returns Option, logs and refuses the controller by name. Alternatively, have the pool allocate below the limit when the space is untranslated.
  • Cheapest check: a type change and inspection.

SUGGESTION 1: system/supervisor/src/main.rs:2531-2545.

  • translated makes two unsafe from_handle round trips between Device and PciDev.
  • Claiming as PciDev at both mint sites (claim_function::<PciDev>) and converting once, at endowment, would remove one unsafe block and the "moving back out" dance.

SUGGESTION 2: hand-out-once now gives a 32-bit function a per-boot budget.

  • The budget is about 1,700 leaves on the AMD bench. Every claim of a resettable narrow function spends its grant leaves for good, because retired addresses are never reused.
  • That ends in a clean ResourceExhausted after about 100 or more restarts, not corruption. Still, it is a new limit no record names.
  • Options: a sentence in an existing issue, or later, reuse of addresses whose reset has completed.

SUGGESTION 3: sdk/abi/src/syscall.rs:114-136. device_dma_limit and device_dma_start sit between SYS_ACCEPT and the TLS call, about 2,200 lines from the other device_dma_* wrappers. Move them next to device_dma_alloc.

Evidence

  • Host: CI at 6cb27ce9f and after fd95b3af0. The iova-ignores-limit control reds the three allocator tests.
  • QEMU: rows on q35 under VT-d and AMD-Vi, and on aarch64 virt.
  • Metal: AMD 6/6 at 6cb27ce9f.

That evidence covers the allocator and the AMD layout.

Virtio refusal (fd95b3af0):

  • The QEMU row on q35 with an untranslated NIC, plus its control (with the check deleted, netstack negotiates access_platform=n), meets the closed issue's exit.
  • offered_features' "no reset" claim has a host test.
  • The Ok(true) path runs in every other virtio row.
  • The Err path and the non-virtio pass-through are three lines each, established by inspection.
  • Neither bench has a virtio function, so metal adds nothing. Reusing the 6cb27ce9f metal evidence for the later commits is sound: they touch only the supervisor's virtio branch, a test output line and an issue.

T14 i219_msi: I accept the independence. Both images ran back to back in one session and read zero MAC-counted frames, and the record is filed.

Blocker fixes: B1 needs one QEMU boot with an untranslated console, plus a control. B2 needs the AMD bench row again once the poll lands, because it changes the hand-over on metal. B3 and B4 need a compile only.

SEND BACK

- The kernel's virtio console. A console that offers no
  VIRTIO_F_ACCESS_PLATFORM uses physical addresses whatever translates the
  bus (virtio 1.2 §6.1). With lowest-first domains its pool sat at about
  2 MiB of somebody else's memory. Such a console is now told physical
  addresses: an untranslated space, kept on the identity domain.
  The QEMU row the_kernel_tells_a_bypassing_console_physical_addresses (on
  q35 with VT-d and the console created without iommu_platform=on) boots
  over it. With the fix reverted, the boot never reaches its ready marker.
- The hand-over drain. A config read flushes the function's posted writes
  (PCIe Base §2.4.1) but orders none of its DMA reads. After clearing Bus
  Master Enable, the kernel now waits for Device Status' Transactions
  Pending (§7.5.3.5) to clear before attaching the domain. The wait is
  bounded at twice the top of the Completion Timeout's default range
  (§2.8). A function still transacting past that is refused by name. A
  function with no Express capability has only the posted-write flush.
- bring_up's doc comment no longer argues the order in which the old code
  started mastering.
- DmaPool::alloc_in is fallible. A device whose width its pages or its
  domain cannot meet refuses that device by name (the kernel's xHCI, the
  console) instead of panicking the kernel. Every other mapping refusal
  is still a kernel bug.

Suggestions taken: the two syscall wrappers now sit beside the other
`device_dma_*` wrappers, and the per-boot address budget of a narrow
function is recorded in issues/. Declined: claiming as PciDev at the
supervisor's mint sites, which would special-case the one generic path
every claim kind shares.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Japabu

Japabu commented Oct 21, 2026

Copy link
Copy Markdown
Collaborator Author

Review r2: #974 at f8d86c93e (r1 at 85436374b)

Scope: this round checks the four r1 blockers and the code their fixes added. The allocator, the architecture and the i219 independence were settled in r1 and are not reopened here.

Assessment

Three of the four fixes are correct as written: B1 (the console), B3 (the bring_up doc) and B4 (alloc_in). B2, the drain, uses the right mechanism for an Express function. It also polls a status bit against a failure deadline, so it is an event wait, not a fixed delay.

What remains is in the new quiesce, at kernel/src/pcidev/mod.rs:1153-1165. Its non-Express branch skips a Transactions Pending bit that this kernel already reads elsewhere, and it treats a truncated capability list as if the capability were absent.

B1, console without ACCESS_PLATFORM: resolved

  • virtio_console.rs:235-247 selects DeviceSpace::Untranslated when the bit is not offered. That is what virtio 1.2 §6.1 requires.
  • The QEMU row the_kernel_tells_a_bypassing_console_physical_addresses passes (logs/r1-guest.log).
  • With the if true || mutation the same row goes red: the boot times out with nothing on the console (logs/console-control.log, exit 1). That is a real negative control.
  • Profile and Shape plumbing: the new profile only removes {platform} from the console argument. Correct.

B3, the bring_up doc: resolved

pcidev/mod.rs:826-828 now matches the code: stop, drain, attach, and the holder starts mastering. The paragraph about the old order is gone.

B4, DmaPool::alloc_in: resolved

  • AddressesExhausted is the variant that DeviceSpace::map returns for an untranslated page above the limit (iommu/mod.rs:200) and for domain exhaustion (domains.rs:85). Both now return Err. Every other map error still panics, which is right for the kernel's own 2 MiB pages.
  • On refusal, the pages vector drops and PhysPage::drop frees the pages.
  • xHCI refuses the controller by name through refuse, before attach. The cap_selftest expect holds, because an untranslated space with the WIDEST limit cannot refuse.

B2, the drain: right mechanism, two gaps in the new code

Citations. No copy of PCIe Base is reachable from here (no local copy, and the spec is member-only), so I checked these against my own knowledge of r4.0–r6.0, not a document:

  • §2.4.1, Transaction Ordering Rules. Correct. The row for Completions against the Posted Request column forbids a Completion with RO clear from passing a Posted Request. The same row allows a Completion to pass Non-Posted Requests, which is why the flush alone was insufficient.
  • §7.5.3.5, Device Status Register, bit 5 Transactions Pending. Correct. It agrees with the file's existing §7.5.3.3, §7.5.3.4 and §7.5.3.16 citations of 6.0. The spec's definition (cleared only once every outstanding Non-Posted Request has completed or been terminated by Completion Timeout) matches the comment at express.rs:37-38.
  • §2.8, Completion Timeout Mechanism. The section is right. The 50 ms figure is the top of the default range, "50 µs to 50 ms". §2.8 requires that range of a function that does not implement the programming mechanism. The encoding 0000b, meaning "Default range: 50 µs to 50 ms", is defined in the Completion Timeout Value field of Device Control 2 (§7.5.3.16).
    • The site would be exact as "the default range's top, 50 ms (§2.8, Device Control 2 §7.5.3.16)". This is optional: the current text is not false.
    • Status: verified from recall, not against a copy. If the orchestrator wants it settled, it is one lookup in a 6.0 copy.

100 ms as a failure timeout. Justified.

  • In practice the bit clears within microseconds. Only a request whose completer never answers keeps it set, and then the timeout mechanism is what ends it.
  • Doubling the default range's top covers every function left at the default. A function programmed into Ranges B–D (up to 64 s), or with the timeout disabled, can outlast 100 ms only if a request is actually stuck, and refusing that function by name is correct under K2. Waiting for the programmed value, up to 64 s, in a claim syscall would be worse.
  • The wait runs on the claimer's thread with no lock held: MACHINE's guard is dropped before bring_up, and held is an atomic. Its cost is bounded the same way as settle_after_reset.

BLOCKER 1: kernel/src/pcidev/mod.rs:1149-1156. A non-Express function's Transactions Pending is not polled, and the comment says no such bit exists.

  • kernel/pci/src/af.rs already models the Advanced Features capability (PCI Advanced Capabilities for Conventional PCI ECN). AF Capabilities bit 0 (TP_CAPABLE) says the function implements AF Status bit 0, Transactions Pending.
  • af.rs's own header says the PCH's integrated functions publish AF where an Express function would use its Express capability. reset_how already reads that status at mod.rs:1092-1096.
  • So on such a function, the hand-over has the same architectural signal the Express path uses, and it ignores it.
  • The comment "A function with no Express capability has no such bit, and the write flush is all that is known of it" is false for every function that publishes AF with TP.
  • The T14's integrated functions are the likely beneficiaries. I219 parts publish AF; that is inference, since I did not see the bench's capability list.
  • Fix: when there is no Express capability but AF reports TP_CAPABLE, poll af::pending(AF_STATUS) on the same deadline and refusal. That needs one small af::reports_pending(caps) helper with a host test like resets.
    • The comment then says that a function with neither capability gets only the posted-write flush.
    • Name the consequence plainly: a read it still had in flight may reach the new domain and fault there.
    • The 100 ms bound needs its own justification for AF, because conventional PCI has no Completion Timeout, or a sentence saying the Express figure is reused as a hang ceiling.
  • Evidence: inspection, plus a host unit for the helper. The T14 testcases boot, which hands over 00:1f.3, 00:1f.6 and 00:0d.0, re-run at the fix's head would exercise it.

BLOCKER 2: kernel/src/pcidev/mod.rs:1156. A truncated capability list skips the wait silently.

  • let Ok(cap) = pci.capability(express::CAP_ID) else { return Ok(()) } treats NoCapability::Truncated as "not Express".
  • bring_up refuses truncation by name elsewhere (Refusal::CapsTruncated, at mod.rs:856). It must refuse it here too, rather than weaken K2's drain on a malformed list.
  • MSI-X or MSI being found earlier does not prove that the list walked to its end.
  • Fix: match Absent to the AF/flush path above, and Truncated to Err(Refusal::CapsTruncated). The same applies to the AF lookup.
  • Evidence: inspection.

On the K2 question. The non-Express weaker drain is acceptable once B1 is fixed. Refusing conventional functions would be wrong:

  • Isolation does not rest on the drain. Posted writes are flushed before attach. A read request that reaches the unit after attach is translated in the new domain, and at that moment the domain holds only this function's own lend window and residue. So nothing leaves the function's isolation.
  • The gap is attribution, not safety. What remains is a possible spurious fault charged to the holder: the usbd drives every xHCI controller; the kernel drives no USB #971 symptom, not an isolation breach. K2's own text says isolation holds regardless.
  • Refusing would remove capability. Refusing every function with neither capability would deny integrated endpoints that offer no architectural pending signal. Those include probably the T14's TCSS xHCI, which usbd_drives_the_type_c_controller passes on (inference; its capability list was not seen).
  • The honest statement needed. For a function with neither capability, the comment and the PR body should say: "no pending signal exists; posted writes are flushed and an in-flight read may fault in the new domain."

Optional

  • virtio_console.rs:252-256. The new refusal returns without offered.decline(), so the function keeps ACKNOWLEDGE|DRIVER. The MULTIPORT path a few lines above declines. With Limit::WIDEST the branch cannot be reached today, so this only matters for consistency.
  • express.rs:41-44. "One programmed longer, or with it disabled, outlasts this and is refused" reads as if such a function is always refused. It is refused only when a request is actually outstanding. Suggested wording: "...; one whose request outlasts this is refused."

Evidence

  • Metal at f8d86c93e. AMD and T14 pass 6/6 each (metal-amd-r3, metal-t14-r3, verdict.txt passed ×4).
    • The images were built at 12:38, after the 12:35 commit, from a clean worktree.
    • The AMD DMA faults on 03:00.4 are the remap row's deliberate probes (aimed at 0x50400000, a vector not its own), not hand-over faults.
    • On T14, the hand-overs of 00:0d.0, 00:1f.3 and 00:1f.6 produced no fault and no StillTransacting.
    • This covers the Express wait, which clears immediately on both benches. It does not cover B1's AF path, which does not exist yet.
  • QEMU. r1-guest.log: 11/11, plus the console control (red). Host CI: ci-host4.log, exit 0.
  • PR body. Lines 82-84 need the drain statement corrected after B1. Line 94 still says metal is "not yet run". Both are for the orchestrator.

SEND BACK

…nd refuses a truncated capability list

Review r2.
- A conventional function that publishes Advanced Features with
  Transactions Pending (PCI Advanced Capabilities ECN; the PCH's integrated
  functions do) now has that bit polled before the domain attaches. It
  uses the same deadline and the same `StillTransacting` refusal as the
  Express bit. Conventional PCI has no Completion Timeout, so the bound is
  a hang ceiling there. `af::reports_pending` is new, with a host test.
- A function with neither capability has no pending signal. Its posted
  writes are flushed, and a read still in flight may fault in its new
  domain against its holder; isolation holds. The comment at the site now
  says so.
- A truncated capability list refuses the hand-over (`CapsTruncated`)
  rather than skipping the wait.
- Optional points taken: the console declines the function when its pool
  is refused, and QUIESCE_NANOS cites Device Control 2 §7.5.3.16 and says
  only an outstanding request outlasting it is refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Japabu

Japabu commented Oct 21, 2026

Copy link
Copy Markdown
Collaborator Author

Metal, T14 (ThinkPad T14 Gen 2), head 96e929bd1, testcases boot, run by the orchestrator: rc 0, judge 5 passed / 0 failed (hda_tone, hda_tone_resampled, claim_reuses_its_remapping_entry, i219_messages_on_its_msi_vector, domains_map_clear_of_the_host_bridges).

The kernel log has no StillTransacting or CapsTruncated refusal. The log does not record which drain path each hand-over took, so this shows only that the Advanced Features path refused nothing.

The AMD result at f8d86c93e (6/6) is reused: the implementer judges the change has no effect on its Express endpoints.

@Japabu

Japabu commented Oct 21, 2026

Copy link
Copy Markdown
Collaborator Author

r3: #974 at 96e929bd1 (r2 at f8d86c93e)

Scope: only the two r2 blockers and the code their fixes added.

r2 BLOCKER 1, Advanced Features Transactions Pending: resolved.

  • af::reports_pending tests TP_CAPABLE (AF Capabilities bit 0) alone. That is the condition under which AF Status bit 0 means anything. Its host test checks the bit and its neighbours.

  • quiesce (kernel/src/pcidev/mod.rs) prefers the Express Device Status. When that capability is absent, it polls AF Status if TP is reported. Otherwise it falls through to the flush.

  • The comment now states the facts that matter:

    • the AF wait reuses the bound only as a hang ceiling, because conventional PCI has no Completion Timeout;
    • a function with neither capability gets the flush;
    • such a function may fault against its holder;
    • isolation still holds.

    It is long for a doc comment. Each clause carries a constraint, though, so I accept it.

r2 BLOCKER 2, truncated list: resolved.

  • NoCapability has exactly Absent and Truncated. The match is exhaustive and has no catch-all.
  • A truncation in either lookup refuses with CapsTruncated before any wait.
  • Absent Express with absent AF, or AF without TP, reaches Ok(()).

Optionals: both were taken and are correct. The console now calls offered.decline() on refusal. The QUIESCE_NANOS citation now reads "§2.8, Device Control 2 §7.5.3.16".

SUGGESTION: the Box<dyn Fn() -> bool> heap-allocates per hand-over only to choose between two register reads. A two-variant enum, or a (Capability, offset, bit) read, would say the same thing without dynamic dispatch. This is optional.

Evidence:

  • Host CI: exit 0, 88 steps (ci-host5.log), including the new af test. QEMU claim rows: 9/9 (r2-guest.log).
  • Both logs finished at 13:02 and 13:04 local, just before the 13:04:22 commit. I accept them as the committed tree; the worktree is clean.
  • The AF path is metal-only behaviour on the T14's PCH functions. The orchestrator's testcases rerun at 96e929bd1 is the check for it. Its result should show 00:1f.3, 00:1f.6 and 00:0d.0 handed over, with no StillTransacting.
  • Not re-running AMD is sound only if every claimed AMD function has an Express capability. That is an inference: their branch is the f8d86c9 code, unchanged.

LAND, conditional on the T14 rerun passing as stated.

@Japabu
Japabu marked this pull request as ready for review October 21, 2026 11:10
@Japabu
Japabu added this pull request to the merge queue Oct 21, 2026
Merged via the queue into main with commit d571391 Oct 21, 2026
8 checks passed
@Japabu
Japabu deleted the wt/toyos-dma-limit branch October 21, 2026 11:33
Japabu added a commit that referenced this pull request Oct 21, 2026
#974 changed the kernel xHCI driver and the old usbd binary (system/usbd/src/hc.rs),
both of which this branch deletes. They stay deleted, and their new limit and
start calls are carried into the new usbd in the next commit. dispatch.rs
takes #974's two DMA syscalls, and SYS_PARTITION_* stays deleted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Japabu added a commit that referenced this pull request Oct 21, 2026
…troller firmware keeps

On #974's hand-over contract. A claim's function masters nothing until its
holder starts it (SYS_DEVICE_DMA_START), and every grant and lend is placed
below the limit the holder states (SYS_DEVICE_DMA_LIMIT).

- After the firmware handoff and the controller's halt and reset, usbd
  states its limit. That is 64 bits, or 32 where HCCPARAMS1.AC64 is clear
  (xHCI 1.2 §5.3.6). It then takes its grant and starts mastering before it
  writes any address to the controller. A controller without AC64 is no
  longer refused: #974 places its grants below 4 GiB.
- A controller firmware keeps (HC BIOS Owned still set after HANDOFF_NS,
  §4.22.1) is now taken, by name, with its SMIs off. Until usbd starts it,
  its bus mastering is off, so the halt and reset cut no transfer of
  firmware's that could reach memory (PCIe Base 6.0 §7.5.1.1.3). That is
  the AMD bench's 03:00.3. The previous commit's refusal stood in for this
  contract until it landed.
- A refused limit or start ends usbd by name, as every other refusal does.
- Test: a_controller_firmware_keeps_is_taken_by_name_with_its_smis_off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Japabu added a commit that referenced this pull request Oct 21, 2026
…Us offer

The row demanded x2APIC of every bench, which is the T14's alone. The
kernel takes x2APIC exactly where CPUID.01H:ECX[21] offers it, and the
AMD bench's Ryzen 7 4800U does not: every CPU its scout read reports
leaf 1 ECX = 0x76d8320b, bit 21 clear, against the T14's 0x77fafbbf.
So the AMD bench declared xAPIC as it should, and the row failed on the
expectation alone; main's own AMD readback for #974 says the same. The
row now takes the mode from the bench, told by its signature as the
topology and panel rows are.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Japabu added a commit that referenced this pull request Oct 21, 2026
…Us offer

The row demanded x2APIC of every bench, which is the T14's alone. The
kernel takes x2APIC exactly where CPUID.01H:ECX[21] offers it, and the
AMD bench's Ryzen 7 4800U does not: every CPU its scout read reports
leaf 1 ECX = 0x76d8320b, bit 21 clear, against the T14's 0x77fafbbf.
So the AMD bench declared xAPIC as it should, and the row failed on the
expectation alone; main's own AMD readback for #974 says the same. The
row now takes the mode from the bench, told by its signature as the
topology and panel rows are.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Japabu added a commit that referenced this pull request Oct 21, 2026
…Us offer

The row demanded x2APIC of every bench, which is the T14's alone. The
kernel takes x2APIC exactly where CPUID.01H:ECX[21] offers it, and the
AMD bench's Ryzen 7 4800U does not: every CPU its scout read reports
leaf 1 ECX = 0x76d8320b, bit 21 clear, against the T14's 0x77fafbbf.
So the AMD bench declared xAPIC as it should, and the row failed on the
expectation alone; main's own AMD readback for #974 says the same. The
row now takes the mode from the bench, told by its signature as the
topology and panel rows are.

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