Skip to content

Groundwork for user drivers: thread-safe allocation and mapping, MSI-X teardown, DMA barriers - #3257

Closed
valentinbreiz wants to merge 9 commits into
gen3from
feature/driver-kit-groundwork
Closed

valentinbreiz wants to merge 9 commits into
gen3from
feature/driver-kit-groundwork

Conversation

@valentinbreiz

Copy link
Copy Markdown
Member

First step towards letting a user kernel (such as examples/DevKernel) register its own PCI and USB drivers. This PR adds no driver API. It fixes the Core and PCI paths that a driver would hit if it bound late, from a preemptible thread, and could be unbound. No PublicAPI.*.txt file changes.

Problem

Today every driver binds during HAL init, one after another, and none ever unbinds. Several paths only work because of that:

  • Page allocation. PageAllocator.AllocPages scanned the RAT for an empty run, then marked it, with no lock. A preemption in between let two threads claim the same pages. Free(uint) also read the byte past the end of the RAT when a run ended on the last page.
  • Small heap. SmallHeap.Free called InternalCpu.EnableInterrupts() on its common path. It runs inside Heap.Free's interrupt-masked scope, so every free of a small object unmasked interrupts in the middle of that scope and of any critical section around it.
  • MMIO mapping. DeviceMapper.EnsureMapped walked and edited the page tables with no lock.
    • On x64, two threads mapping under the same empty PML4 or PDPT slot could each link a new table, and the mappings made through the first one were lost.
    • On ARM64, two splits could both fill the single spare L2 table for different 1 GiB blocks.
    • It returned nothing, so a caller could not tell a failed mapping from a good one before dereferencing the alias.
  • x64 PCI config space. Mechanism Rewrote codeplex startpage in markdown #1 is a write to 0xCF8 followed by an access at 0xCFC. A thread preempted between the two resumed against whatever register another thread had selected.
  • MSI-X. It could be enabled and programmed, but never undone:
    • a vector or LPI stayed allocated with its handler registered;
    • a second MsiX.Enable on the same function issued another MAPD over a mapped ITS DeviceID, orphaning the first ITT and every LPI translated through it;
    • SetEntry unmasks at once, so an entry can fire before its driver is ready;
    • entries were rewritten while unmasked, which PCI 3.0 §6.8.3.5 leaves undefined.
  • Serial output with interrupts masked. ReadyThread, BlockThread, CheckSleepingThreads and ConditionVariable wrote a line to the polled UART on every wake, block and signal. They run with interrupts masked, often from an ISR, so each byte stalled every IRQ. The timer ISR also logged every 50th tick for the whole uptime.
  • DMA ordering in an ISR. The NVMe completion handler ordered its reads through PlatformHAL.Initializer?.DmaBarrier(). That is an interface call inside an ISR, and a full barrier where a load-to-load ordering is enough.

Fix

Locks. Three IRQ-safe spinlocks (Scheduler.SpinLock.AcquireIrqSafe):

Lock Guards Notes
PageAllocator.s_ratLock RAT scan and mark in AllocPages, Free(uint), FreePageCount Held over RAT reads and writes only. Anything under it that allocates or throws would re-enter AllocPages with interrupts masked and spin on itself.
DeviceMapper.s_lock (x64 and ARM64) page-table walk and edit May take the PageAllocator lock beneath it (table allocation), never the reverse. Not reentrant.
PciDevice.s_configLock each 0xCF8 write with its 0xCFC access x64 only. An ECAM access is a single load or store and needs no lock.
  • Pages are zeroed after s_ratLock is released: the run is already marked, and clearing a large run with interrupts masked would hold off every IRQ.
    • Rejected: clearing under the lock.
  • Free(void*) looks up the run's first page outside the lock. It only reads entries of the caller's own run, and it can throw.
  • SmallHeap no longer touches the interrupt flag at all. Heap.Free's scope owns it.

Mapping result. DeviceMapper.EnsureMapped and IPlatformInitializer.EnsureMmioMapped now return whether the 2 MiB block is mapped on return (true when it already was). Existing drivers ignore the result for now. The ARM64 mapper no longer logs every walk step and every already-mapped block, only new mappings and failures.

MSI-X teardown.

Member What it does
MsiX.SetEntryMasked Programs an entry and leaves it masked. A message the device raises meanwhile waits in its Pending Bit Array until the entry is unmasked.
MsiX.Disable Sets Function Mask, masks every entry, clears MSI-X Enable, reads Message Control back, then hands every slot and the device's routing state back to the binder.
IMsiBinder.UnbindEntry x64: frees the entry's IDT vector. ARM64: DISCARD + SYNC, then frees the LPI.
IMsiBinder.ReleaseDevice Unbinds what is still bound. ARM64 then issues MAPD V=0 + SYNC and frees the ITT.
  • Every entry is masked before its address and data are written, in both SetEntry and SetEntryMasked.
  • Disable reads Message Control back before freeing anything: an ECAM store can retire before it reaches the function, and the read cannot complete ahead of it.
  • Disable writes the per-entry masks only when memory decode is on. Function Mask already covers every entry, and a table write the function does not decode is an unclaimed posted write: dropped on x64, an asynchronous abort on some ARM64 root complexes.
  • Disable leaves the Command register alone, so INTx stays disabled as Enable left it. Enable does not record what that bit held before, so restoring it is the owner's job.
  • Each binder now keeps a context per function, keyed by bus/slot/function (x64 records each entry's vector, ARM64 its LPI and the ITT address).
    • PrepareDevice on a function whose earlier context was never released tears that context down first and logs [MSI] function 0x.. still had an unreleased context, releasing it first (x64) or [MSI-ITS] DeviceID 0x.. was still mapped, ... (ARM64).
    • Releasing the old context later does nothing, so it cannot free a vector that another device holds by then.
  • IMsiBinder and MsiRouting now document that every member runs in thread context only: the allocators take plain spinlocks.

Logging. The per-wake, per-block and per-signal lines are gone, and the tick log stops after the first 10 ticks.

DMA barriers.

  • New Core.Memory.DmaOrdering.ReadBarrier and WriteBarrier, backed by _native_dma_rmb and _native_dma_wmb:
    • ARM64 emits dmb oshld and dmb oshst. DMB rather than DSB, because only the order of accesses matters, not their completion.
    • x64 is TSO and needs no instruction. Each barrier stays an opaque native call anyway, so the compiler cannot move a DMA-memory access across it.
  • The NVMe completion handler uses DmaOrdering.ReadBarrier.

Public surface. Every changed member is internal, except DeviceMapper.EnsureMapped (void to bool) and GICv3Its.MapDevice (void to ulong, the ITT address). Those live in the arch Core assemblies, which have no PublicAPI files.

Verification

Build. make setup: 0 errors. The only warnings in the touched files are on lines this PR does not change.

Test suites, QEMU (JUnit tests / failed / skipped):

Suite x64 arm64
HelloWorld 3 / 0 / 0 3 / 0 / 0
Memory 71 / 0 / 0 71 / 0 / 0
GarbageCollector 46 / 0 / 0 46 / 0 / 0
Threading 74 / 0 / 0 74 / 0 / 0
Timer 26 / 0 / 0 not run
Interrupts 17 / 0 / 0 51 / 0 / 23
Pci 6 / 0 / 0 6 / 0 / 0
Virtio 10 / 0 / 0 20 / 0 / 3
Storage (all profiles) 438 / 0 / 42 1314 / 0 / 736
Network 48 / 0 / 0 48 / 0 / 0
Graphic 69 / 0 / 26 not run
  • Totals match the gen3 CI baseline (run 35973349716), except Interrupts, which gains the 5 new cells. Every skip is a profile gate.
  • After the review fixes: Interrupts, Threading, Virtio, Storage and Network re-ran on both architectures, plus Timer on x64. Same totals.
  • After the final cleanup: Interrupts (17/17) and Memory (71/71) on x64.

New Interrupts cells (12 to 17 per cell):

Test Checks
MsiX_SetEntryMasked_LeavesEntryMasked On a scratch table, the entry is written but stays masked, and SetEntry still unmasks.
MsiRouting_UnbindEntry_ReturnsSlot Binds until the allocator runs dry (172 vectors on x64), then checks that UnbindEntry frees a slot and ReleaseDevice frees all 172 again.
MsiRouting_PrepareDevice_RemapDoesNotLeak Re-prepares one function repeatedly. The routing slot count is unchanged; free pages go 127465, then 127459 after the take-overs, then 127459 after the stale releases (32-page slack).
MsiX_Disable_ClearsEnableAndMasksEntries On an MSI-X function no driver enabled (e1000e on x64): sets Function Mask, unmasks every entry, then checks that Disable clears MSI-X Enable, leaves Function Mask set and re-masks every entry.
MsiX_EnableDisableEnable_SameFunction After Disable, the first routing context refuses to bind. A second Enable sets MSI-X Enable, clears Function Mask and programs a zeroed entry again.
  • The first three skip without MSI routing (ARM64 with GICv2).
  • The last two also need an MSI-X function no driver owns.

Other suites.

  • Storage, nvme and nvme+acpi-off: Mmio_HighBar_RemappedOnDemand now also asserts that EnsureMmioMapped reports the relocated BAR as mapped ([DeviceMapper] Mapping MMIO phys 0x110000000).
  • Profile_NvmeInterruptModeMatches passes with NVMe on MSI-X, on the new read barrier.

Real hardware. The runs above are QEMU only.

Known gaps.

  • On ARM64, MsiX.Disable never runs against a live function. virtio-net owns the only MSI-X function there, so the last two cells skip. A driver test suite in a later PR will cover it.
  • No driver acts on EnsureMmioMapped's result yet (MsiX.Enable, NVMe, AHCI, xHCI, virtio). The driver model will turn a failed mapping into a declined bind.
  • An owner whose context was taken over can still call MsiX.Disable on its function. The binder side is safe, but the Message Control write would disable the new owner's MSI-X. This goes away once each function has a single owner, which is the next step.
  • If the 0xCF8 write in PciDevice.SelectConfigAddress throws, the lock stays held with interrupts masked. Only a null PortIO can do that, which is already fatal at boot.
  • Found but not changed here: CachedInterfaceDispatch calls SmallHeap.Alloc without masking interrupts.

SmallHeap.Free called InternalCpu.EnableInterrupts on its common path
and on its panic paths, and SmallHeap.Alloc did the same before its
out-of-pages panic. Heap.Free already runs them in a scope that masks
interrupts and restores the flag on exit, so each call unmasked
interrupts in the middle of that scope and of any critical section the
caller had nested it in. The calls are gone, along with the
commented-out serial traces.
AllocPages scanned the RAT for an empty run and then marked it, with
nothing to stop a preemption in between, so two threads could claim the
same pages. The scan and the mark, Free(uint) and every FreePageCount
update now run under an IRQ-safe spinlock. Pages are zeroed after the
lock is released, so clearing a large run does not hold off interrupts.

Free(uint) also read the byte past the end of the RAT when a run ended
on the last page; the bound is now checked before the read. The serial
line AllocPages wrote on every call is gone.
ReadyThread, BlockThread, CheckSleepingThreads and ConditionVariable
wrote a line to the polled UART on every wake, block and signal. They
run with interrupts masked, often from an ISR, so each byte stalled
every IRQ. Those lines are gone. The timer tick is logged for the first
10 ticks only, to show at boot that it runs, instead of every 50 ticks
for the whole uptime.
DeviceMapper.EnsureMapped walked and edited the page tables with no
lock. On x64, two threads mapping under the same empty PML4 or PDPT
slot could each link a new table, and the mappings made through the
first one were lost. On ARM64, two splits could both fill the single
spare L2 table. Both mappers now hold an IRQ-safe spinlock, which may
take the PageAllocator lock beneath it but never the reverse.

EnsureMapped and IPlatformInitializer.EnsureMmioMapped now return
whether the block is mapped, so a caller can refuse to touch an alias
that would fault. Existing drivers ignore the result for now; the
Storage suite's high-BAR test checks it. The ARM64 mapper no longer
logs every walk step and every already-mapped block, only new mappings
and failures.
On x64 a config access is a CONFIG_ADDRESS write to 0xCF8 followed by a
CONFIG_DATA access at 0xCFC. A thread preempted between the two resumed
against whatever register another thread had selected meanwhile. Each
pair now runs under one IRQ-safe spinlock. ECAM accesses are single
loads or stores and stay unlocked.
DmaOrdering.ReadBarrier and WriteBarrier order CPU accesses to coherent
DMA memory the way a device observes them: dmb oshld and dmb oshst on
ARM64, in the Outer Shareable domain where DMA masters sit. x64 is TSO
and needs no instruction, but each barrier stays an opaque native call
so the compiler cannot move a DMA-memory access across it. Neither
allocates, blocks or dispatches through an interface, so an ISR can
use both.

The NVMe completion handler now uses the read barrier instead of the
interface call PlatformHAL.Initializer.DmaBarrier().
MSI-X could be enabled and programmed but never undone. A vector or LPI
stayed allocated with its handler registered, and a second MsiX.Enable
on the same function issued another MAPD over a mapped ITS DeviceID,
orphaning the first ITT and every LPI translated through it.

- MsiX.SetEntryMasked programs an entry and leaves it masked, so a
  driver still starting up receives nothing; the device holds the
  message in its Pending Bit Array until the entry is unmasked. Every
  entry is now masked before its address and data are rewritten.
- MsiX.Disable sets Function Mask, masks every entry, clears MSI-X
  Enable and reads the register back, so the function has stopped
  signalling before any vector or LPI goes back to the allocator.
- IMsiBinder gains UnbindEntry and ReleaseDevice. x64 records the
  vector each entry holds and frees it. ARM64 issues DISCARD and SYNC
  before freeing each LPI, then MAPD V=0 and SYNC before freeing the
  ITT.
- PrepareDevice on a function whose earlier context was never released
  tears that context down first and logs it. Releasing the old context
  later does nothing, so it cannot free what the new owner holds.
Five new cells in the Interrupts suite (12 -> 17 per cell):

- MsiX_SetEntryMasked_LeavesEntryMasked: on a scratch table, the entry
  is written but stays masked, and SetEntry still unmasks.
- MsiRouting_UnbindEntry_ReturnsSlot: binds entries until the vector or
  LPI allocator runs dry, then checks that UnbindEntry frees a slot and
  that ReleaseDevice frees every slot the function still held.
- MsiRouting_PrepareDevice_RemapDoesNotLeak: re-prepares one function
  repeatedly; no ITT page or routing slot leaks, and releasing a
  context that was taken over frees nothing twice.
- MsiX_Disable_ClearsEnableAndMasksEntries: on an MSI-X function no
  driver has enabled, Disable clears MSI-X Enable, leaves Function Mask
  set and re-masks entries that were unmasked.
- MsiX_EnableDisableEnable_SameFunction: after Disable the first
  routing context refuses to bind, and a second Enable on the same
  function programs its entries again.

The first three skip without MSI routing (ARM64 GICv2). The last two
also need an MSI-X function no driver owns, which ARM64 does not have
(virtio-net owns the only one), so they run on x64 only (e1000e).
@github-actions

github-actions Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

🧪 Fat Tests

Cell x64 arm64
all ✅ 42/42 ✅ 42/42

📎 Artifacts

Architecture Test Results UART Log Kernel ISO
x64 XML Log ISO
arm64 XML Log ISO

📋 View full test summary | 📄 Kernel.cs

@github-actions

github-actions Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

🧪 File Tests

Cell x64 arm64
all ✅ 37/37 ✅ 37/37

📎 Artifacts

Architecture Test Results UART Log Kernel ISO
x64 XML Log ISO
arm64 XML Log ISO

📋 View full test summary | 📄 Kernel.cs

@github-actions

Copy link
Copy Markdown

🧪 Graphic Tests

Cell x64 arm64
all ✅ 49/75, 26 skip ⚠️ 24/50, 26 skip
bare ⚠️ 12/25, 13 skip ⚠️ 12/25, 13 skip
virtio-gpu ⚠️ 12/25, 13 skip ⚠️ 12/25, 13 skip
vmware-svga ✅ 25/25 n/a

Skipped

  • x64: 26 skipped: VMware SVGA II adapter not present, needs the vmware-svga profile (26)
  • arm64: 26 skipped: VMware SVGA II adapter not present, needs the vmware-svga profile (26)

📎 Artifacts

Architecture Test Results UART Log Kernel ISO
x64 XML Log ISO
arm64 XML Log ISO

📋 View full test summary | 📄 Kernel.cs

@github-actions

github-actions Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

🧪 Interrupts Tests

Cell x64 arm64
all ✅ 17/17 ✅ 28/51, 23 skip
bare ✅ 17/17 ⚠️ 7/17, 10 skip
bare+gicv2 n/a ✅ 9/17, 8 skip
bare+gicv3 n/a ✅ 12/17, 5 skip

Skipped

  • arm64: 23 skipped: no MSI routing in this cell (GICv2 has no ITS) (6), needs MSI routing and an MSI-X function no driver has enabled (arm64: virtio-net owns the only one) (6), self-IPI harness is x64-only (3), LAPIC MSI address contract is x64-only (3), LAPIC timer vector is x64-only (3), gic-version not pinned by this cell (2)

📎 Artifacts

Architecture Test Results UART Log Kernel ISO
x64 XML Log ISO
arm64 XML Log ISO

📋 View full test summary | 📄 Kernel.cs

@github-actions

github-actions Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

🧪 Memory Tests

Cell x64 arm64
all ✅ 71/71 ✅ 71/71

📎 Artifacts

Architecture Test Results UART Log Kernel ISO
x64 XML Log ISO
arm64 XML Log ISO

📋 View full test summary | 📄 Kernel.cs

@github-actions

github-actions Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

🧪 Network Tests

Cell x64 arm64
all ✅ 48/48 ✅ 48/48
e1000e ✅ 24/24 n/a
virtio-net-mmio n/a ✅ 24/24
virtio-net-pci ✅ 24/24 ✅ 24/24

📎 Artifacts

Architecture Test Results UART Log Kernel ISO
x64 XML Log ISO
arm64 XML Log ISO

📋 View full test summary | 📄 Kernel.cs

@github-actions

Copy link
Copy Markdown

🧪 Pci Tests

Cell x64 arm64
all ✅ 6/6 ✅ 6/6

📎 Artifacts

Architecture Test Results UART Log Kernel ISO
x64 XML Log ISO
arm64 XML Log ISO

📋 View full test summary | 📄 Kernel.cs

@github-actions

Copy link
Copy Markdown

🧪 Power Tests

Cell x64 arm64
all ✅ 4/4 ✅ 4/4

📎 Artifacts

Architecture Test Results UART Log Kernel ISO
x64 XML Log ISO
arm64 XML Log ISO

📋 View full test summary | 📄 Kernel.cs

@github-actions

Copy link
Copy Markdown

🧪 Runtime Tests

Cell x64 arm64
all ✅ 103/103 ✅ 103/103

📎 Artifacts

Architecture Test Results UART Log Kernel ISO
x64 XML Log ISO
arm64 XML Log ISO

📋 View full test summary | 📄 Kernel.cs

@github-actions

github-actions Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

🧪 Storage Tests

Cell x64 arm64
all ✅ 426/468, 42 skip ⚠️ 668/1404, 736 skip
ahci ✅ 69/78, 9 skip ✅ 69/78, 9 skip
ahci+acpi-off ✅ 69/78, 9 skip ⚠️ 3/78, 75 skip
ahci+gicv2 n/a ✅ 69/78, 9 skip
ahci+gicv2+acpi-off n/a ⚠️ 3/78, 75 skip
ahci+gicv3 n/a ✅ 69/78, 9 skip
ahci+gicv3+acpi-off n/a ⚠️ 3/78, 75 skip
nvme ✅ 73/78, 5 skip ✅ 70/78, 8 skip
nvme+acpi-off ✅ 72/78, 6 skip ⚠️ 3/78, 75 skip
nvme+gicv2 n/a ✅ 71/78, 7 skip
nvme+gicv2+acpi-off n/a ⚠️ 3/78, 75 skip
nvme+gicv3 n/a ✅ 71/78, 7 skip
nvme+gicv3+acpi-off n/a ⚠️ 3/78, 75 skip
usb ✅ 74/78, 4 skip ✅ 74/78, 4 skip
usb+acpi-off ✅ 69/78, 9 skip ⚠️ 3/78, 75 skip
usb+gicv2 n/a ✅ 74/78, 4 skip
usb+gicv2+acpi-off n/a ⚠️ 3/78, 75 skip
usb+gicv3 n/a ✅ 74/78, 4 skip
usb+gicv3+acpi-off n/a ⚠️ 3/78, 75 skip

Skipped

  • x64: 42 skipped: not a USB profile (20), 64-bit BAR relocation probe is nvme-profile only (8), USB hot-plug thread not running (scheduler timer not ticking) (5), not an NVMe profile (4), NVMe controller API is nvme-profile only (4), acpi-off has no MSI routing to pin (1)
  • arm64: 736 skipped: no block device bound for partition-table tests (441), no block device bound for this profile (171), not a USB profile (60), x64 mapper cell; arm64 installs Device mappings via DeviceMapper (18), BAR relocation probe is x64-only (same harness as the mapper cell) (18), NVMe controller API is nvme-profile only (15), not an NVMe profile (12), interrupt mode not pinned by this cell (1)

📎 Artifacts

Architecture Test Results UART Log Kernel ISO
x64 XML Log ISO
arm64 XML Log ISO

📋 View full test summary | 📄 Kernel.cs

@github-actions

github-actions Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

🧪 Threading Tests

Cell x64 arm64
all ✅ 76/76 ✅ 76/76

📎 Artifacts

Architecture Test Results UART Log Kernel ISO
x64 XML Log ISO
arm64 XML Log ISO

📋 View full test summary | 📄 Kernel.cs

@github-actions

github-actions Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

🧪 Timer Tests

Cell x64 arm64
all ✅ 26/26 ✅ 20/20

📎 Artifacts

Architecture Test Results UART Log Kernel ISO
x64 XML Log ISO
arm64 XML Log ISO

📋 View full test summary | 📄 Kernel.cs

@github-actions

github-actions Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

🧪 Virtio Tests

Cell x64 arm64
all ✅ 10/10 ✅ 17/20, 3 skip
virtio-mmio n/a ✅ 7/10, 3 skip
virtio-pci ✅ 10/10 ✅ 10/10

Skipped

  • arm64: 3 skipped: this cell presents virtio over MMIO (3)

📎 Artifacts

Architecture Test Results UART Log Kernel ISO
x64 XML Log ISO
arm64 XML Log ISO

📋 View full test summary | 📄 Kernel.cs

@github-actions

Copy link
Copy Markdown

🧪 HelloWorld Tests

Cell x64 arm64
all ✅ 3/3 ✅ 3/3

📎 Artifacts

Architecture Test Results UART Log Kernel ISO
x64 XML Log ISO
arm64 XML Log ISO

📋 View full test summary | 📄 Kernel.cs

@github-actions

Copy link
Copy Markdown

🧪 Math Tests

Cell x64 arm64
all ✅ 30/30 ✅ 30/30

📎 Artifacts

Architecture Test Results UART Log Kernel ISO
x64 XML Log ISO
arm64 XML Log ISO

📋 View full test summary | 📄 Kernel.cs

@github-actions

Copy link
Copy Markdown

🧪 TypeCasting Tests

Cell x64 arm64
all ✅ 17/17 ✅ 17/17

📎 Artifacts

Architecture Test Results UART Log Kernel ISO
x64 XML Log ISO
arm64 XML Log ISO

📋 View full test summary | 📄 Kernel.cs

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