multikernel: add isolated SR-IOV VF assignment - #4
Conversation
b715339 to
0ff04f5
Compare
|
Hi @nickolaev Nice work, thanks for the PR! I finally have time to look into it. A few high-level comments:
|
|
@congwang-mk thanks for the high-level review. Indeed, this is a naive implementation of "let's make ICMP pass"; It is very fragile as it is. The questions you asked are setting a proper direction. I have some ideas; let me try to clean things up and will ping you once I have a better patch series. |
8d8960b to
4e05ab7
Compare
|
@congwang-mk I have updated the commit history with the revised work and the PR message reflects the new state. |
b2d1b55 to
64608bc
Compare
Thank you for doing it. I will take a look during the weekend, since I am still working on the refactoring to prepare for ARM/RISCV (which is also why you got merge conflicts). |
64608bc to
5efa61c
Compare
|
Thanks for the update! The layering is a clear improvement: synthetic roots instead of physical bridges, manifest-owned BAR/identity metadata,
|
|
Separate from the item list above, a thought about the shape rather than the bugs. SR-IOV VFs are built assuming something mediates. VF config space is impoverished by design because a hypervisor is expected to synthesize the missing parts from the PF's SR-IOV capability. Multikernel's premise is that there is no mediator, so the series meets a mediator-shaped hole and fills it two ways: the manifest for BARs and identity, which works because those are static, and a config-op filter for everything else, which does not, because the filter lives inside the kernel it is meant to constrain. The interrupt gap is the same mismatch surfacing where it cannot be papered over. But multikernel already has the right mechanism: a host that owns global state and a message ring to reach it. That suggests splitting along the axis that actually exists:
Keep the synthetic roots and the IOMMU domain either way. The identity-mapped domain in particular fits well: with no second-level translation it range-restricts a device rather than virtualizing an address space, which is simpler than what VFIO needs. The obvious cost is latency on mediated config access, and I have not measured what that does to something like igbvf at probe time. Worth a quick experiment before committing. |
5efa61c to
75e1053
Compare
Thanks for the review @congwang-mk |
1e25d2f to
a3db37b
Compare
|
OK, so the host-mediated PCI config is now in place and IRQ ownership is resolved. |
congwang-mk
left a comment
There was a problem hiding this comment.
Code review of the SR-IOV VF assignment series. 8 findings inline: 3 that look like real breakage (link failure, atomic-context deadlock, use-after-free), 4 medium, 1 cleanup.
| return ret; | ||
| } | ||
|
|
||
| bool mk_pci_msi_controlled(struct pci_dev *dev) |
There was a problem hiding this comment.
Link failure when CONFIG_PCI_MMCONFIG=n. The five mk_pci_msi_* functions (230, 238, 271, 285, 306) are inside the #ifdef CONFIG_PCI_MMCONFIG block; the #else arm only re-provides mk_arch_snapshot_pci_host_bridges and the x86_init.pci hooks. multikernel.h declares them unconditionally under MULTIKERNEL && X86 && PCI, and drivers/pci/msi/msi.c:244 + irqdomain.c:15,19,31,34,42 call them unconditionally.
PCI_MMCONFIG depends on ACPI || JAILHOUSE_GUEST, so a spawn kernel with CONFIG_ACPI=n (what this series targets) fails to link vmlinux with five undefined references.
| goto out; | ||
|
|
||
| /* Pairs with the response handler's publication of status and value. */ | ||
| while (!smp_load_acquire(&pending.done)) { |
There was a problem hiding this comment.
Synchronous RPC re-dispatches the whole IPI ring from atomic context. mk_pci_remote_config() and mk_pci_send_irq_request() (:214) busy-wait up to 1s in mk_poll_ipi_messages(), which drains the ring and runs every handler, including mk_pci_irq_forward_handler() -> generic_handle_irq_safe() -> the assigned device's ISR.
Both callers run IRQs-off under a raw spinlock: config accesses come via pci_bus_read/write_config_* holding pci_lock; mk_pci_msi_bind() is reached from __pci_write_msi_msg() under desc->lock with IRQs off during irq_startup().
Deadlock: driver probe does pci_read_config_word(), the poll dispatches a forwarded MSI, that ISR issues a config read -> self-deadlock on the non-recursive pci_lock, same CPU. Even without re-entry, spinning 1s IRQs-off trips the hard-lockup watchdog.
| pr_info("Releasing multikernel instance %d (%s), returning resources to root\n", | ||
| instance->id, instance->name); | ||
| ret = mk_instance_release_resources(instance); | ||
| WARN_ON_ONCE(ret); |
There was a problem hiding this comment.
Instance freed after failed resource release -> use-after-free. This only WARN_ON_ONCE()s the error, then kfree(instance) two lines down.
On failure mk_pci_release_assignments() (kernel/multikernel/pci.c:1697-1717) breaks out of its loop, leaving the mk_pci_assignment linked on both instance->pci_assignments (now freed) and the global active list, with assignment->instance dangling. A VF whose FLR times out during teardown gets there; the next BUS_NOTIFY_UNBOUND_DRIVER runs mk_pci_assignment_failure_work(), which dereferences assignment->instance->state and ->name.
mk_instance_destroy() and mk_create_instance_from_dtb() do honor this error; the kref path is the outlier.
| tail = atomic_read(&ring->tail); | ||
|
|
||
| slot = &root_instance->ipi_data->ring.entries[tail]; | ||
| for (scanned = 0; scanned < MK_IPI_RING_SIZE; scanned++) { |
There was a problem hiding this comment.
Ring consumption is no longer FIFO. The new drain scans all MK_IPI_RING_SIZE slots from tail and consumes anything READY, skipping WRITING. Slot order no longer equals delivery order: A claims slot 5, B claims 6, B publishes first -> 6 is delivered before 5.
mk_vsock_ipi_handler() turns each payload into an skb on a byte-stream socket, so two concurrent senders now corrupt the stream rather than just delaying it. The goal here (an interrupted producer must not block others) is reachable without breaking ordering in the common case.
| { | ||
| struct pci_dev *dev = msi_desc_to_pci_dev(entry); | ||
|
|
||
| if (mk_pci_msi_write_msg(dev, entry->msi_index, entry->irq, |
There was a problem hiding this comment.
MSI bind failure is swallowed, leaving a silently dead vector. mk_pci_msi_write_msg() (arch/x86/multikernel/pci.c:271) logs the mk_pci_msi_bind() error with pr_err_ratelimited() and then returns true unconditionally, so __pci_write_msi_msg() stores entry->msg and returns as if programmed.
On RPC timeout or -ESTALE/-ENODEV, pci_alloc_irq_vectors() and request_irq() both succeed while the host never enabled the corresponding vector. The VF driver then waits forever for an interrupt that never arrives, with no error visible to it.
| instance = mk_instance_find(irq_work->request.sender_instance_id); | ||
| if (!instance) | ||
| goto out; | ||
| mk_cpu_ownership_lock(); |
There was a problem hiding this comment.
CPU-ownership mutex held across sleeping MSI work. This holds mk_cpu_ownership_lock() across mk_pci_irq_access(), which can msleep(MK_PCI_FLR_SETTLE_MS + 1), pci_alloc_irq_vectors(), request_irq() and free_irq() (synchronize_irq). mk_pci_cfg_work_fn() (:505) does the same.
Every mk_instance_transfer_cpus() / mk_send_cpu_add() / mk_send_cpu_remove() on any instance stalls behind an unrelated instance's MSI setup, and the requester's own 1s deadline can expire while the host is still in the FLR sleep -> spurious -ETIMEDOUT. Only the mk_pci_request_route_stale check actually needs the ownership lock.
| pr_info("Forwarding host IRQ %u as instance IRQ %u for %s vector %u\n", | ||
| irq, local_irq, pci_name(assignment->vf), | ||
| payload.vector); | ||
| if (mk_send_message_to_instance(assignment->instance, MK_MSG_IO, |
There was a problem hiding this comment.
Forwarded MSI silently dropped on allocation/backpressure failure. This hardirq handler forwards via mk_send_message_to_instance() -> __mk_send_message(), which does a kzalloc(GFP_ATOMIC) per interrupt and returns -ENOSPC from mk_ipi_ring_claim_slot() when the 64-slot ring is full. The handler only pr_warn_ratelimited()s and returns IRQ_HANDLED.
Edge-triggered MSI never re-asserts, so under memory pressure or ring backpressure the spawn kernel permanently loses that completion (e.g. an igbvf TX/RX cleanup interrupt) and the queue hangs. No retry, coalescing, or pending-interrupt fallback.
| return 0; | ||
| } | ||
|
|
||
| raw_pci_ops = &pci_mmcfg; |
There was a problem hiding this comment.
Dead stores; the "saved backend" does not exist. raw_pci_ops/raw_pci_ext_ops are set to &pci_mmcfg and immediately overwritten by the filtered ops, and nothing captures pci_mmcfg anywhere.
The commit message says accepted accesses are "forwarded to the saved backend", but mk_pci_raw_read/write always go out as a host RPC. The ECAM windows registered by pci_mmconfig_add() and mapped by pci_mmcfg_arch_init() just above are therefore mapped and never used. Either wire the local backend up or drop the ECAM mapping and these two stores, since as written it reads like there is a local fast path.
| unsigned long boot_lps; /* Host delay loops per second */ | ||
| unsigned long boot_cpu_khz; /* Host CPU frequency calibration */ | ||
| unsigned long boot_tsc_khz; /* Host TSC frequency calibration */ | ||
| unsigned long boot_apic_hz; /* Host local APIC timer frequency */ |
There was a problem hiding this comment.
I am wondering why you have to handle clock in this PR? If anything is wrong with clock, please separate it out
9cd7a06 to
5916a11
Compare
Spawn kernels cannot calibrate against host-owned PIT, PIC, or IO-APIC resources. Carry the host loops-per-jiffy, CPU and TSC frequencies, and local APIC timer calibration in the spawn boot context. Install fixed calibration callbacks before x86 timer initialization. Keep explicit command-line calibration authoritative. Signed-off-by: Nikolay Nikolaev <nicknickolaev@gmail.com>
5916a11 to
4ff65f4
Compare
4ff65f4 to
20902d6
Compare
Describe only the PCI functions assigned to a spawn and retain the BAR shape discovered by the host. Validate exact BDF syntax, reject duplicate functions, and preserve the inventory across the baseline and instance device trees. Keep PCI support behind CONFIG_PCI so the control plane remains buildable without PCI. Signed-off-by: Nikolay Nikolaev <nicknickolaev@gmail.com>
Create one synthetic root bus for each assigned domain and bus, then scan only the explicitly assigned devfns. Supply an exact bus-number resource when x86 root resources do not provide one. Spawn kernels do not inherit or map host ECAM windows. Configuration is mediated by the filtered backend introduced with the assigned roots. Signed-off-by: Nikolay Nikolaev <nicknickolaev@gmail.com>
Synthetic root filtering alone leaves raw x86 configuration backends able to reach functions outside the spawn inventory. Apply the assigned-BDF filter to both raw configuration entry points. Identity reads come from validated metadata while other accesses use the selected backend only for an assigned function. Signed-off-by: Nikolay Nikolaev <nicknickolaev@gmail.com>
Reserve assigned VFs transactionally under a host-wide lease lock. Detach the host driver, mark the device assigned, and publish ownership only when the complete request succeeds. Propagate host-driver restoration failures and retain failed instance state when cleanup cannot be completed safely. Signed-off-by: Nikolay Nikolaev <nicknickolaev@gmail.com>
Instance creation reserves memory, CPUs, PCI devices, host-bridge metadata, and platform devices. Returning an error after any one of those transfers can otherwise expose a partially populated instance and leak resources from the root. Validate configuration counts and lists before transfer, acquire each resource class in a fixed order, and unwind every completed step in reverse order. Centralize release so create failure, instance deletion, and final reference teardown share the same all-or-nothing semantics. Balance references acquired for remote memory add and remove operations on every success and error path so resource hotplug cannot pin a deleted instance. Signed-off-by: Nikolay Nikolaev <nicknickolaev@gmail.com>
Exclusive VF ownership does not constrain DMA. Allocate a host-owned domain for each assignment and map only the memory owned by the instance. Update mappings with memory hotplug under the lease lifetime. Detach and destroy the domain before returning the VF to the host. Signed-off-by: Nikolay Nikolaev <nicknickolaev@gmail.com>
Returning a VF while it can still issue DMA races the IOMMU teardown and host-driver reprobe. Unexpected PF or VF removal must also stop an active instance instead of silently losing its assigned device. Clear bus mastering, wait for pending transactions, and issue function-level reset while the assignment domain is still attached. Then detach and free the domain, restore the saved driver override and host driver, and only afterwards return the inventory to the root. Lease-loss notifications force an active instance to halt and mark it failed. Rollback entries that were prepared but never committed skip device quiesce and driver restoration, releasing only their prepared IOMMU resources. Signed-off-by: Nikolay Nikolaev <nicknickolaev@gmail.com>
A stopped or force-halted instance can leave bus mastering enabled and DMA in flight while its next boot rewrites the same memory. Lease teardown resets the VF, but a respawn retains the lease and previously skipped that protection. Require assigned VFs to support FLR. After confirming that all instance CPUs are parked, clear bus mastering, drain pending transactions, and reset every leased VF while its restrictive IOMMU domain remains attached. Abort the restart if any device cannot be made safe. Signed-off-by: Nikolay Nikolaev <nicknickolaev@gmail.com>
Protect CPU membership and pool ownership with a dedicated mutex nested inside an operation-wide transaction lock. Keep add and remove transactions serialized across reservation, acknowledgment, and repark so a CPU cannot be transferred twice. Signed-off-by: Nikolay Nikolaev <nicknickolaev@gmail.com>
Serialize shared-ring producers with a bounded owner-aware gate. Preserve FIFO publication and recover a gate only after its producer CPU is known to be parked. The gate and slot-state protocol change the private shared transport layout. Add an exact pre-launch layout and protocol check at this boundary, then require a transport initialization acknowledgment after both rings have been validated and before marking the instance active. Fail invalid manifests and missing acknowledgments closed. A spawn started by a host without the pre-launch check validates the boot-context anchor before using any shifted field and enters a local interrupt-disabled halt loop on mismatch without trusting shared park state or touching reset and APIC hardware. Signed-off-by: Nikolay Nikolaev <nicknickolaev@gmail.com>
Cache the IRQ forwarding CPU for hardirq-safe routing. Serialize route, ownership, reload, halt, and teardown mutations while active users hold a route reference. Drain assignment IRQ producers before publishing a replacement route or reparking the old CPU. Signed-off-by: Nikolay Nikolaev <nicknickolaev@gmail.com>
Proxy spawn PCI configuration through the host so only a live VF lease selected by an assigned BDF can access hardware. Return results through preallocated generation-tagged reply slots. Atomic callers wait only on their slot with a bounded deadline and never drain the general ring. Bump the exact transport ABI to version 4. Signed-off-by: Nikolay Nikolaev <nicknickolaev@gmail.com>
Keep MSI and MSI-X programming out of irqchip callbacks. Setup, bind, activation, restore, and teardown use the process-context PCI control path; write_msg only updates the cached message. Route controlled VF FLR through a separate process-context direct reply, with an independent spawn epoch and serial generation. Reject raw config-space FLR writes so reset cannot run inside the bounded atomic config path. Reject stale operations without side effects and fail closed on incomplete activation or reset. Pre-mask controlled MSI-X tables before host activation. Bump the exact transport ABI to version 6. Signed-off-by: Nikolay Nikolaev <nicknickolaev@gmail.com>
A bounded shared ring cannot guarantee delivery when a host interrupt arrives in hardirq context. Record assigned interrupts in preallocated per-instance pending slots and use the IPI only as a doorbell. Protect each slot with spawn epoch, lifecycle generation, and an atomic pending, masked, and consuming token. Coalesce while masked and retry lost doorbells until the guest drains the slot. Bump the exact transport ABI to version 7. Signed-off-by: Nikolay Nikolaev <nicknickolaev@gmail.com>
Expose a versioned per-instance snapshot for the ordered IPI ring, direct reply table, and pending IRQ mailbox. Document every counter, gauge, reset boundary, and the non-atomic modulo-u32 snapshot semantics. Signed-off-by: Nikolay Nikolaev <nicknickolaev@gmail.com>
20902d6 to
29c6e31
Compare
congwang-mk
left a comment
There was a problem hiding this comment.
I read through the full series. The design is sound: the host keeps device ownership, the IOMMU domain is the real isolation boundary rather than config filtering, and the docs say that plainly instead of overclaiming. Generation and epoch tagging is applied consistently, and encoding owner CPU plus slot in the producer gate (so a force-halt NMI cannot strand it permanently) is a genuinely good idea.
Below are correctness findings, then a request about how the series is packaged.
Findings
1. Refcount-0 resurrection in mk_instance_release() (kernel/multikernel/core.c)
The new early return in the kref release callback:
ret = mk_instance_release_resources(instance);
if (WARN_ON_ONCE(ret)) {
pr_crit("Retaining multikernel instance %d ...");
return; /* not freed, refcount is already 0 */
}kref_put() has already driven the count to zero, but the instance is still in mk_instance_idr and on mk_instance_list, because mk_instance_destroy() only unlinks after a successful release. The next mk_instance_find() does idr_find() followed by kref_get() on a zero refcount, which trips refcount_warn_saturate("addition on 0"), and the following put frees an object other callers still hold.
This is reachable whenever mk_pci_release_assignments() returns -ETIMEDOUT or -EBUSY, which the series deliberately makes possible (mailbox quiesce timeout, VF still driver-bound). Suggested fix: re-init the reference before returning, or unlink from the idr and list first so the object is unreachable.
2. mk_ipi_ring_publish() spins on a cross-kernel gate with interrupts disabled (kernel/multikernel/ipi.c)
preempt_disable(); local_irq_save(flags);
for (retry = 0; retry < MK_IPI_PRODUCER_RETRIES; retry++) { /* 10000 */
old = atomic64_cmpxchg_acquire(&ring->producer_gate, 0, token);
...
cpu_relax();
}producer_gate lives in shared memory, so the owner may be a CPU belonging to a different kernel. A host CPU can therefore spin 10000 cpu_relax() iterations with local interrupts off waiting on a spawn that is stalled, descheduled, or NMI-parked. Console output reaches this path through vsock, as the old comment in this function noted.
This converts a lock-free MPSC ring that admitted up to 63 concurrent producers into a single global gate, and the failure mode is a dropped message (-EAGAIN) rather than backpressure. The recoverability argument for the gate design is convincing; holding it with IRQs off and spinning on a remote kernel is the part I would push back on.
3. mk_reply_cancel() can permanently strand reply slots (kernel/multikernel/ipi.c)
In the cancel loop, any state that is neither WRITING nor EXECUTING (notably COMMITTED, which an earlier timeout installs) falls out of the loop into the cancelled: label, bumps cancelled_slots, and returns 0, so the caller reports -ETIMEDOUT while the slot is never returned to FREE.
A COMMITTED slot is only reclaimed by mk_reply_publish(). If the host worker never reaches publish (instance force-halted between mk_reply_begin_execute() and publish), that slot is gone for the rest of the epoch. There are only MK_REPLY_SLOTS == 16 and every PCI config access consumes one, so a small number of these wedges config access with -ENOSPC until respawn. occupied_failures will grow with no recovery path.
4. mk_do_cpu_add() can leave a CPU online but untracked (kernel/multikernel/hotplug.c)
After add_cpu() succeeds:
mk_cpu_ownership_lock();
if (root_instance->cpus) {
ret = mk_cpu_set_add(root_instance->cpus, cpu_id);
if (ret)
pr_warn(...);
}
mk_cpu_ownership_unlock();
if (ret)
goto unlock_transaction;Two issues. On mk_cpu_set_add() failure the function returns an error with the CPU already online and the rollback mk_hotplug_op never recorded, so nothing will undo it. And when root_instance->cpus is NULL, the if (ret) tests the leftover value from the CPU bring-up call rather than a fresh result. The previous code warned and continued, which was at least self-consistent.
The pre-reserve added just above is the right idea; it just is not carried through to the failure path.
5. XLF_MULTIKERNEL_IPI_V2 through V7 claim six bits of xloadflags
arch/x86/include/uapi/asm/bootparam.h now takes 0x0100 through 0x2000 in the documented x86 boot protocol UAPI word for a private host/spawn handshake, with five of the six values unused. struct mk_shared_data already carries abi_magic and abi_version, and the ELF note now carries ipi_abi_version, so the boot-protocol bit is only needed as a coarse "this bzImage speaks the protocol at all" gate. One bit would do, and I would expect the x86 maintainers to say the same when this eventually goes upstream.
Smaller notes
mk_pci_msi_prepare()acceptsMK_PCI_MSI_FAILEDas a valid starting state alongsideIDLE, so it re-prepares after a teardown that is known to have failed. The host-sideSETUPdoes retrymk_pci_release_irqs()first, but "failed teardown" is exactly the case where that retry is least likely to succeed.x86_multikernel_pci_init()callspanic()whenpci_scan_single_device()returns NULL. Fail-closed is right for assignment generally, but a transient enumeration miss now kills the spawn rather than failing the instance.mk_instance_confirm_parked()gained an earlyif (!instance->cpus_on_slot) return 0;ahead of the newmk_ipi_ring_recover_halted()call, so that path reports success without recovering a gate a dead producer may still own.DECLARE_PCI_FIXUP_EARLY(PCI_ANY_ID, PCI_ANY_ID, mk_pci_restore_resources)runs on every device on everyCONFIG_PCIkernel. It is inert on the host, but a global ANY_ID fixup for a subarch-specific feature would be better gated onX86_SUBARCH_MULTIKERNEL.- Two counters exist but are unreachable: the spawn's config-access latency (
mk_pci_cfg_count/total_ns/max_ns) is only printed once viapr_noticeduring enumeration, andmk_pci_control_pool_exhaustedis incremented and never read. Both belong in the newstatsfile, especially the latency one given that config access is the hot path. kernel/multikernel/pci.cat ~2600 lines covers leases, IOMMU domains, IRQ forwarding, the control-plane message handler, config access, and the transaction machinery. Splitting it along those lines would make it much easier to review. The host/spawn split againstarch/x86/multikernel/pci.cis already clean.
Request: split the IPI transport rework out
The commit split within the PR is good, but three commits are standalone infrastructure that happens to ride along with the feature:
multikernel: make resource reservation atomicmultikernel: serialize CPU ownership transfersmultikernel: make IPI publication ordered and recoverable
I would like at least the IPI transport work to land as its own series. It rewrites the shared ring, adds the reply table, bumps the shared ABI to v7, burns a boot-protocol flag bit, and breaks every existing DTB through MK_FDT_COMPATIBLE. That is a larger and riskier change than the SR-IOV feature it is supporting, it affects every multikernel user rather than only those assigning VFs, and findings 2 and 3 are both in it. Reviewed on its own it would get the attention it deserves, and SR-IOV could then be reviewed as a feature rather than as a feature plus a transport rewrite.
The CPU ownership serialization is a similar case on a smaller scale. The mk_cpu_set locking rework in particular (adding the raw_spinlock_t, out-of-lining the three inline accessors, rewriting reserve/add/copy as retry loops) changes an API used throughout the subsystem and stands on its own merits. Worth at least its own commit, separate from the route-pinning changes it enables.
To be clear on why the CPU work is here at all: mk_pci_request_route_stale() makes CPU ownership a security-relevant fact, which does require the mk_cpu_set locking, the route migration in mk_send_cpu_remove(), and the CPU 0 exclusion. That chain is sound. But mk_do_cpu_add() and mk_do_cpu_remove() operate on root_instance->cpus, which the route validation never reads, so the transaction and ownership locking there is consistency work rather than a prerequisite. That is also where finding 4 crept in.
Depends on #5.
Summary
This 15-commit series adds host-controlled SR-IOV VF assignment to multikernel instances while keeping device ownership and privileged hardware programming in the host kernel.
The lifecycle fails closed when assignment, DMA isolation, reset, interrupt programming, or teardown cannot be completed safely.
Validation
!MULTIKERNEL,PCI_MSI=n,ACPI=n/PCI_MMCONFIG=n, andPCI=nbuild lanes.