Repository navigation
pcidev: one address window per function, every claim address below 4 GiB, and a released function's fault is never the kernel's - #1188
Conversation
…ed function's fault is never the kernel's Two ways a claim's holder could harm the kernel, both in `pcidev`'s ownership of a function. **Grants spent addresses for good.** Every `dma_alloc` grant was placed at a fresh address of its function's domain (`Domains::map`, over `kernel::iova::Window::reserve`, which hands each address out once per boot). A holder that claims, allocates and ends again and again, such as a service its supervisor restarts, spent up to `MAX_GRANT_TOTAL` per claim and the remapping tables under it, never given back. The slot then refused every later grant for the rest of the boot. Before that, a table the machine had no frame for panicked in `Tables::alloc`. Now every address a claim is given is in one window reserved with its function's domain: `MAX_GRANT_TOTAL` for grants, then `LEND_WINDOW` for lends, as the lend window already was. `kernel::claimspace` places both in it: - A grant goes at the lowest room in the grant part that no live grant touches. Since that part is `MAX_GRANT_TOTAL` long and holds only grants, this is also the per-claim bound, and the separate sum is gone. - A grant may be placed over the function's residue, the addresses a function no reset quiets may still be aimed at. It is fresh pages, which is what residue needs, and the residue it covers is carved off. A lend still never goes over residue. - Across claims, a reset function's retired grants are unmapped before its next attach, so the next claim reuses the same addresses. The tables under a function are therefore bounded by its window. The domain hands nothing out after it is made, so `Domains::map`, each backend's `map` and `map_at` (VT-d, AMD-Vi, SMMUv3), the x86 dispatch of both, `OwnSpace::map` and `IommuError::AddressesExhausted` are deleted. `OwnSpace::map_at` and `map_again` place and write the grant record that `tests/common/claims.rs`'s `clear_of_host_bridges` reads, in the same form as before. **A late write from a released function halted the machine.** `release` clears `Function::held` at its end, but a transfer the function had already issued can fault after that, and before a reset has quieted it. `note_fault` read such a function as one no process drives, `owner=kernel`, and `iommu::fault::conclude` halted the machine. A function a claim has held now carries `Function::claimed`, which is set by `claim` and never cleared. Its faults are handed to its last holder: bus mastering cleared, the line written as `owner=process`, and the machine goes on. A function only this kernel drives, or one never claimed, still halts. Host tests in `kernel::claimspace`: `a_function_no_reset_quiets_serves_every_holder_whatever_the_last_one_laid_out` runs 2000 claims of random layouts over one window, each followed by a release that leaves everything as residue. Every grant a claim may hold is served, inside the grant part, no lend lands on residue, and the residue stays inside the window and disjoint. Checked mutations: grants that avoid residue, which was the old rule, red it at the first refused grant; a carve that removes nothing reds its disjointness check and `carving_keeps_what_a_grant_does_not_cover`. The late-write fix has no test that makes the write. A function with Bus Master Enable clear issues no request (PCI 3.0 §6.2.2), and the arm64 track records that no function QEMU offers writes once mastering is off except `iommu-testdev`, which no claim can drive. No bench function can be made to write in the window between release and quiet either. Closes issues/a-claim-spends-device-addresses-its-slot-never-gets-back.md, issues/a-narrow-functions-device-addresses-are-spent-once-per-boot.md, issues/a-domains-addresses-are-never-given-back.md and issues/a-late-write-from-a-released-function-halts-the-machine.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…DEVICE_DMA_LIMIT goes The window this branch reserves per function lies below 4 GiB, and nothing else is handed out. Every PCI function issues those addresses whole, so the holder's address width, stated through `SYS_DEVICE_DMA_LIMIT`, no longer decided anything. By the orchestrator's ruling it goes with every consumer, in this landing: - The kernel: the syscall and its dispatch, `pcidev::dma_limit`, `Bound::limit`, and the "stated first" refusals in `dma_alloc` and `dma_map`. - `kernel::iova`: `Limit`. `Window::new` clips every domain to 4 GiB, which now states the invariant the limit used to: a function that drops its upper address bits cannot issue one address as another. Its control, `iova-ignores-limit`, becomes `iova-past-four-gib`, and `nothing_is_handed_out_at_or_above_four_gib` is the test that reds under it. - `toyos-abi`: `SYS_DEVICE_DMA_LIMIT` (131) and `device_dma_limit`. `toyos`: `PciDev::dma_limit`. `toyos-pci-claim`: `limit`, its virtio bring-up's call, and `id`, whose one user was hda's width. - Drivers: nvme, usbd, i219, hda, and the `usbd_spare` and `i219_msi` guest jobs. usbd's `Reset::address_bits` existed only to be stated, and goes. A controller without AC64 is still driven, as its stub test holds. - hda's `address_bits` and its refusal of AMD's `1002:1637`, which drops every address bit from 40 up. No address it is given has one, so it is driven like any other, and issues/the-hdmi-audio-function-1002-1637-is-driven-at-no-address-width.md is closed. The AMD bench's `testcases` boot, staged for this branch, walks its codec. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review of #1188 at f6dc7d0ArchitectureThis is the right ownership. Each function's record already kept one domain for the whole boot. Now that domain reserves one fixed window, and I checked these points and found them sound:
Required
Landing conditions (evidence, no code change)
Suggestions
Sighting (pre-existing, not introduced here)
SEND BACK |
…cks gone, a lend's stale-transfer gap filed - The grant bound is stated as it is: first fit in the claim's 32 MiB grant part, which can refuse below that total once grants are taken back out of order (`dma_alloc`, `dma_undo`, `toyos_abi::syscall::device_dma_alloc`). - The two records that cited the deleted `Bound::lend_room` cite `kernel::claimspace::Window::lend_room`. - hda's `Beyond32Bits` refusal, `Gcap::wide` and their two tests are gone: no device address the kernel gives a claim is at or above 4 GiB. Link::open keeps its `&Reset` as the witness that the controller was brought up. - usbd's stub loses `ac64` and the test that could no longer fail, since nothing reads AC64. - Shorter docs on the `claimspace` test and `note_fault`. - Filed: issues/a-lend-taken-back-before-a-release-is-not-residue.md (security), the reviewer's sighting. Main has it too. Not fixed here: the fix changes placement. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… a wake and a time A record's count is of interrupts the kernel took, not of messages the device sent: the local APIC folds a message that arrives while its vector is pending (Intel SDM Vol. 3A, 325384-093, §13.8.4), an LPI folds while pending, and the claim's mask drops what lands past it. virtio-sound inferred periods from that count, so a record of one interrupt over two periods left a notification owed that never landed, and no later record clocked a period again: the mix loop's DLL stopped being corrected. That stall predates #1193. Now the used ring alone says which periods played, on every read, and the record's newest time anchors the clock only where it can be no other period's: one interrupt taken and one period read. Any other read anchors nothing, and its periods carry into the DLL's next update, which it already does. The notification debt and option (b)'s Notified type are gone. a_record_folded_over_two_periods_does_not_stop_the_clock feeds a count of 1 over two periods and then three steady reads: [0, 1, 1, 1]. #1188's accounting, restored as the control, reads [1, 0, 0, 0]. Review r2 of #1193 besides: the ABI says what the count is; i219_msi's storm comment names the second interrupt as the one that masks; take_record's doc, record.rs's word constants and the issue's account of who pays a read's invalidation wait. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review r2 of #1188 at 83ddb42 (changes since f6dc7d0)ArchitectureThe architecture is unchanged since r1, which accepted it. This round only fixes text and deletes code: 10 files, +59/−73. Across the whole PR the diff is now +478/−739. r1's required findings
The deletionsAll of them remove dead code only:
The new record
|
… a wake and a time A record's count is of interrupts the kernel took, not of messages the device sent: the local APIC folds a message that arrives while its vector is pending (Intel SDM Vol. 3A, 325384-093, §13.8.4), an LPI folds while pending, and the claim's mask drops what lands past it. virtio-sound inferred periods from that count, so a record of one interrupt over two periods left a notification owed that never landed, and no later record clocked a period again: the mix loop's DLL stopped being corrected. That stall predates #1193. Now the used ring alone says which periods played, on every read, and the record's newest time anchors the clock only where it can be no other period's: one interrupt taken and one period read. Any other read anchors nothing, and its periods carry into the DLL's next update, which it already does. The notification debt and option (b)'s Notified type are gone. a_record_folded_over_two_periods_does_not_stop_the_clock feeds a count of 1 over two periods and then three steady reads: [0, 1, 1, 1]. #1188's accounting, restored as the control, reads [1, 0, 0, 0]. Review r2 of #1193 besides: the ABI says what the count is; i219_msi's storm comment names the second interrupt as the one that masks; take_record's doc, record.rs's word constants and the issue's account of who pays a read's invalidation wait. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem
Two ways a claim's holder could harm the kernel, both in
pcidev's ownership of a function:SYS_DEVICE_DMA_ALLOCgrant went at a fresh address of the function's domain (
Domains::map,over
iova::Window::reserve, which hands each address out once per boot).A holder that claims, allocates and ends again and again (a supervisor
restarting a service does exactly this) spent up to 32 MiB a claim, plus
the remapping tables under it, and none of it came back. The slot then
refused every grant for the rest of the boot. If physical memory ran out
first,
Tables::allocpanicked.releaseclears
Function::heldat its end, before a reset has quieted thefunction. A transfer already in flight that faults after that point was
read as a fault from a function no process drives (
owner=kernel), andiommu::fault::concludehalted the machine.Change
One window per function. Every device address a claim is given now lies
in one window reserved with its function's domain:
MAX_GRANT_TOTAL(32 MiB) for grants, then
LEND_WINDOW(256 MiB) for lends, below 4 GiB.The lend window already worked this way.
kernel::claimspace(new, pure) places both. A grant goes first fit inthe 32 MiB grant part, at the lowest room no live grant touches. The window
replaces the per-claim byte sum: a claim is refused where no run of the
grant's length is free, which can happen below 32 MiB once grants are
taken back out of order (see Not done).
still be aimed at) because it is fresh pages, and the residue it covers is
carved off. A lend never goes over residue.
every claim reuses the same addresses.
allocates a table outside the window's span. The tables still come from
Tables::alloc'sexpect, which is now reachable only through thekernel's own superframes running out, not through a holder's churn.
Domains::map,mapandmap_atin every backend (VT-d,AMD-Vi, SMMUv3), x86's dispatch of both,
OwnSpace::mapandIommuError::AddressesExhausted.OwnSpace::map_atandOwnSpace::map_againplace a grant and write thesame
iommu: domainN maps … at …[ again]record as before.domains_map_clear_of_the_host_bridgesreads that record.A released function's fault is its last holder's.
Function::claimedisset by
claimand never cleared.note_faulthands such a function's faultto its last holder: bus mastering cleared, the line written as
owner=process, and the machine goes on. A function only this kerneldrives, or one never claimed, still halts the machine. The issue's exit
proposed clearing ownership only once the reset is quiet. That needs a
quiet event the kernel does not have (a reset's quiet is a deadline read at
the next claim), and a function no reset quiets is never quiet. "Once
claimed, never the kernel's" answers both cases with one flag.
No driver states an address width (round 2, by the orchestrator's
ruling). Every claim address is in its window, below 4 GiB, and every PCI
function issues those whole, so
SYS_DEVICE_DMA_LIMITdecided nothing.pcidev::dma_limitand its"stated first" refusals.
toyos_abi::syscall::device_dma_limit(number 131),
PciDev::dma_limit,toyos_pci_claim::limit, andtoyos_pci_claim::id, whose only user was hda's width.bring-up, plus usbd's
Reset::address_bits. Theusbd_spareandi219_msiguest jobs lose theirs too.kernel::iova::Window::newnow clips every domain to 4 GiB, which is therule
Limitused to enforce. Its controliova-ignores-limitbecomesiova-past-four-gib.address_bits, and its refusal of AMD's1002:1637, go. Thatfunction drops address bits 40 and up, and no address it is given now has
one, so it is driven like any other function.
Closes:
issues/a-claim-spends-device-addresses-its-slot-never-gets-back.mdissues/a-late-write-from-a-released-function-halts-the-machine.mdissues/a-narrow-functions-device-addresses-are-spent-once-per-boot.md(every claim address is now below 4 GiB and reused)
issues/a-domains-addresses-are-never-given-back.md(no domain hands outan address after it is made)
issues/the-hdmi-audio-function-1002-1637-is-driven-at-no-address-width.md(no width is needed: its exit's boot is the AMD
testcasesboot stagedbelow, whose log should show
03:00.1's codec walked with noDMA FAULT)Checks
Head 83ddb42 (round 3: review r1's text fixes and deletions;
--ci hostexit 0, log). Round 3 evidence: b69b43a8c/pr-1188/r3. Round 2 evidence: ce6f9f175/pr-1188/r2.Round 1: 190bc99a4/pr-1188/r1.
cargo run -- --ci hostat f6dc7d0: exit 0 (log).This includes the
iova-past-four-gibcontrol reddening its named tests,and kernel clippy on x86_64 and aarch64. The run before it was red because
Gcap::widehad been removed with hda's width(log), and it is restored.
cargo run -- --build-only, exit 0 (log), and--build-only --arch aarch64, exit 0 (log).kernel::claimspacetests and their two controls are round 1's, ata0d5cd8. The file has changed since only in a doc comment, so those results still hold.
a_function_no_reset_quiets_serves_every_holder_whatever_the_last_one_laid_outruns 2000 claims with random grant layouts and lends, each ending in a
release that leaves everything as residue. Every grant is served inside
the grant part, no lend lands on residue, and the residue stays inside
the window and disjoint.
carve that removes nothing reds it and
carving_keeps_what_a_grant_does_not_cover; both exit 101(controls).
usbd_drives_the_spare,nvme_disk_bootsandvirt_claim_lpiat f6dc7d0 (run 42023736614): success. They cover a VT-d claim with
grants and a fault on a held claim, an nvme claim's grants and lends, and
an SMMUv3 claim.
testcasesboot (judges):claim_reuses_its_remapping_entry,domains_map_clear_of_the_host_bridges("4 mapping(s), none reaching aroot-bridge window") and
usbd_drives_the_type_c_controller.i219_messages_on_its_msi_vector, at its last step only, "noframe arrived on a message under traffic". Every forced cause was
delivered on the claim's MSI, and 65 frames were sent through the new
window's grants. The MAC's own pre-DMA receive counters saw 0 frames on
the wire, and the boot has no
DMA FAULT(excerpt). This is the known independent failure recorded in
issues/i219-msi-passes-only-when-the-network-sends-it-a-frame.md(main showed it on 2026-10-21 and 10-24); the MAC counts frames before any DMA, so placement cannot cause a zero count.testcasesboot (judge):amdvi_translates_the_boot_controllerpasses.03:00.1[1002:1637] ishanded over in domain3, which ends at 0x100000000. Its grant is at
0x200000. hda walks codec0 (1002:aa01), ends with "plays nothing", and
the function is released with a D3hot reset. The log has 0
DMA FAULTlines (excerpt). That meets the closed
1002:1637 record's exit.
Not done, and why
issues no request (PCI 3.0 §6.2.2), and
issues/toyos-runs-on-arm64.mdrecords that no QEMU function writes once mastering is off except
iommu-testdev, which no claim can drive. No bench function can be made towrite between release and quiet either. The change is a flag and two
branches in
note_fault, so this rests on reading the code.of order and the free runs are 2 MiB apart, so a 4 MiB grant is refused
while the claim holds 16 MiB. Before, the byte sum alone decided. Current
drivers grant once at start and never take one back except through
dma_undo's failure path, so none meets it.(
issues/a-lend-taken-back-before-a-release-is-not-residue.md, security,filed from the review's sighting). Main has the same gap, and this branch
neither causes nor fixes it: the fix changes placement.
quiets, because lends avoid residue and nothing carves it there.
issues/a-restarted-display-holder-lends-around-its-predecessors-scanouts.mdalready tracks this, and this change does not alter it. One small
capability is lost: before, a grant of exactly a lent range's size could
reuse that lend-window residue. Grants now stay in the grant part.
🤖 Generated with Claude Code