Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
e19b6b7
multikernel: simplify and harden duplex IPI transport
nickolaev Sep 12, 2026
2038320
multikernel: carry assigned PCI resource metadata
nickolaev Aug 11, 2026
e355d71
x86/multikernel: build PCI roots for assigned functions
nickolaev Aug 11, 2026
6c6d1d2
x86/multikernel: filter raw PCI config operations
nickolaev Aug 4, 2026
4c197fe
pci/multikernel: add exclusive SR-IOV VF leases
nickolaev Jul 30, 2026
327193c
multikernel: make resource reservation atomic
nickolaev Jul 30, 2026
6981130
pci/multikernel: isolate VF DMA with a host IOMMU domain
nickolaev Jul 30, 2026
05f183b
pci/multikernel: quiesce VFs before releasing leases
nickolaev Jul 30, 2026
b2b989f
pci/multikernel: reset assigned VFs before respawn
nickolaev Aug 3, 2026
3b80473
multikernel: serialize CPU ownership transfers
nickolaev Aug 5, 2026
27750f2
multikernel: make IPI publication ordered and recoverable
nickolaev Aug 12, 2026
2c9ad68
multikernel: pin control routes across CPU ownership changes
nickolaev Aug 11, 2026
f73c64e
multikernel: mediate PCI config with direct replies
nickolaev Aug 4, 2026
f5cde37
multikernel: move assigned MSI control to process context
nickolaev Aug 11, 2026
69f1e81
multikernel: add lossless pending IRQ mailboxes
nickolaev Aug 11, 2026
516eddc
multikernel: expose transport reliability stats
nickolaev Aug 11, 2026
5973597
multikernel: make IPI timeout recovery bounded
nickolaev Aug 17, 2026
eb1d539
pci/multikernel: fix disabled-feature builds
nickolaev Sep 13, 2026
da2c40b
multikernel: make control route delivery atomic-safe
nickolaev Sep 14, 2026
9f303cc
tty: make multikernel host receive locks IRQ-safe
nickolaev Sep 14, 2026
7338170
multikernel: initialize resource state without PCI
nickolaev Sep 15, 2026
61865d2
tty: defer multikernel host wakeups from hard IRQ
nickolaev Sep 15, 2026
6b47d14
multikernel: defer message completions from hard IRQ
nickolaev Sep 15, 2026
d0dd880
multikernel: preallocate IPI control work
nickolaev Sep 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions Documentation/ABI/testing/multikernel
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
What: /sys/fs/multikernel/instances/<name>/stats
Date: August 2026
Contact: Nikolay Nikolaev <nicknickolaev@gmail.com>
Description:
Read-only reliability snapshot for one multikernel instance. The
format is one ``key value`` pair per line. The first line is always
``stats_version 1``. Versioned formats are append-only; readers must
ignore unknown keys.

The snapshot is observational, not atomic with concurrent producers.
Cumulative transport counters are unsigned 32-bit event counts and
wrap modulo 2^32. They reset when the shared buffer is initialized
for a new ``spawn_epoch`` and cannot be reset through this file.
Compare deltas only between samples with the same nonzero epoch.
Gauges are unsigned point-in-time counts and have no reset operation.

Metadata keys (class; unit; meaning):

``stats_version`` (metadata; version; this file format, currently 1),
``abi_version`` (metadata; version; exact shared transport ABI),
``snapshot_atomic`` (metadata; boolean; always 0),
``transport_counter_bits`` (metadata; bits; always 32),
``transport_available`` (gauge; boolean; shared buffer is mapped), and
``spawn_epoch`` (metadata; launch identifier; changes on each start).

``abi_version`` identifies the exact private kernel-to-kernel transport
contract used by the running instance. Exposing it here supports
diagnostics; it is not a stable userspace ABI or a compatibility promise
between transport versions.

Ordered IPI keys (class; unit; increment or sampled condition; reset):

The duplex transport serializes each direction with a local spinlock and
publishes a single shared READY bit. Consequently
``ipi.producer_contention``, ``ipi.invalid_state``,
``ipi.cancelled_writes``, ``ipi.gate_busy``, ``ipi.slot_writing``,
``ipi.slot_consuming``, and ``ipi.slot_cancelled`` remain zero. They are
retained in version 1 for reader compatibility.

``ipi.producer_contention`` (compatibility counter; always zero),
``ipi.full_failures`` (counter; sends; either direction's head slot
prevented publication; new epoch),
``ipi.invalid_state`` and ``ipi.cancelled_writes`` (compatibility
counters; always zero), ``ipi.gate_busy`` and ``ipi.slot_writing``
(compatibility gauges; always zero), ``ipi.slot_ready`` (gauge; READY
slots summed across both directions; sampled), and
``ipi.slot_consuming`` and ``ipi.slot_cancelled`` (compatibility gauges;
always zero).

Direct reply keys (class; unit; increment or sampled condition; reset):

``reply.late_replies`` (counter; replies; stale exact-token claim or
completion was rejected; new epoch), ``reply.cancelled_slots``
(counter; slots; timeout
cancelled RESERVED/WRITING; new epoch), ``reply.atomic_timeouts``
(counter; waits; bounded atomic wait expired in cancellable
RESERVED/WRITING or indeterminate EXECUTING state; new epoch),
``reply.indeterminate_timeouts`` (counter; waits; subset where
EXECUTING won and changed to COMMITTED; new epoch),
``reply.occupied_failures`` (counter;
reservations; no FREE slot; new epoch), ``reply.slot_busy`` (gauge;
slots; state is not FREE; sampled), ``reply.slot_reserved`` (gauge;
slots; RESERVED; sampled), ``reply.slot_writing`` (gauge; slots;
WRITING; sampled), ``reply.slot_executing`` (gauge; slots; EXECUTING;
sampled), ``reply.slot_committed`` (gauge; slots; COMMITTED; sampled),
``reply.slot_ready`` (gauge; slots; READY; sampled), and
``reply.slot_abandoned`` (gauge; slots; ABANDONED; sampled).

Pending IRQ keys (class; unit; increment or sampled condition; reset):

``irq.recorded`` (counter; IRQs; pending count recorded; new epoch),
``irq.coalesced`` (counter; IRQs; recorded onto an already-pending
slot; new epoch), ``irq.masked_deferred`` (counter; IRQs; delivery
deferred while logically masked; new epoch), ``irq.stale`` (counter;
observations; epoch/generation/route validation rejected work; new
epoch), ``irq.dispatch_failed`` (counter; IRQs; validated local dispatch
failed; new epoch), ``irq.saturated`` (counter; IRQs; pending count hit
its representable limit; new epoch), ``irq.slot_active`` (gauge; slots;
nonzero generation; sampled), ``irq.slot_pending`` (gauge; slots;
nonzero pending count; sampled), ``irq.slot_masked`` (gauge; slots;
MASKED set; sampled), ``irq.slot_consuming`` (gauge; slots; CONSUMING
set; sampled), and ``irq.pending_total`` (gauge; IRQs; sum of pending
counts across active slots; sampled).
Users: multikernel management and diagnostic tools
44 changes: 44 additions & 0 deletions Documentation/multikernel/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,20 @@ Workflow
``device-remove``, and destroy the instance with ``instance-remove``
once it is stopped.

SR-IOV Assignment Boundary
===========================

SR-IOV assignment is intended for cooperative spawned kernels. Filtering
configuration-space access and enumerating only assigned BDFs prevents
accidental access; it is not a security boundary against a privileged kernel
that deliberately issues configuration cycles or maps host physical windows.

The host keeps exclusive leases for assigned VFs. Assignment fails unless the
device is an SR-IOV VF whose identity and ownership can be validated. Host
drivers remain bound while a VF is merely in the pool and are detached only
while committing a lease. Nested kernels cannot establish this host-owned
lifecycle and therefore cannot lease PCI devices to their children.

Instance States
===============

Expand All @@ -75,6 +89,31 @@ Instance States
- **active**: the kernel is running
- **failed**: an error occurred; check ``dmesg``

Transport Reliability Statistics
================================

Each instance exposes a read-only ``stats`` file. Version 1 is an
append-only key/value interface covering the duplex IPI transport, direct
reply slots, and pending IRQ mailbox. Readers must ignore unknown keys.

The snapshot is observational rather than atomic. ``transport_available``
reports whether the instance currently has shared transport state, and
``spawn_epoch`` identifies the launch whose counters are shown. Transport
counters are unsigned 32-bit values which may wrap and are reset when a new
epoch is initialized; compare them as modulo-32-bit deltas only between
samples with the same nonzero epoch.

``ipi.*`` reports producer failures and current per-direction slot occupancy,
``reply.*`` reports timeout/recovery counters and reply-slot states, and
``irq.*`` reports recorded, coalesced, deferred, stale, failed, saturated,
active, pending, masked, and consuming mailbox state. Gauge values describe
only the instant at which each field was sampled.

The duplex ready-bit protocol has no shared producer gate or intermediate
WRITING, CONSUMING, CANCELLED, or invalid states. The corresponding legacy
version-1 keys remain present and report zero; ``ipi.slot_ready`` and
``ipi.full_failures`` are summed across both link directions.

Restrictions
============

Expand All @@ -83,3 +122,8 @@ Restrictions
- Instance files are read-only; an instance's resources change through
overlays targeting ``/instances/<name>``.
- Rollback (``rmdir`` on a transaction) cannot destroy a running instance.
- Logical CPU 0 handles assigned-device MSI forwarding and must remain online
while those devices are active.
- Assigned MSI events are recorded in per-instance shared mailboxes before a
doorbell is sent. Masked events remain pending and lost doorbells are retried;
each spawn drains only its own parent-link mailbox.
8 changes: 7 additions & 1 deletion arch/x86/boot/header.S
Original file line number Diff line number Diff line change
Expand Up @@ -379,7 +379,13 @@ xloadflags:
#define XLF56 0
#endif

.word XLF0 | XLF1 | XLF23 | XLF4 | XLF56
#ifdef CONFIG_MULTIKERNEL
# define XLF_MK XLF_MULTIKERNEL_IPI
#else
# define XLF_MK 0
#endif

.word XLF0 | XLF1 | XLF23 | XLF4 | XLF56 | XLF_MK

cmdline_size: .long COMMAND_LINE_SIZE-1 #length of the command line,
#added with boot protocol
Expand Down
11 changes: 9 additions & 2 deletions arch/x86/include/asm/multikernel.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#ifndef __ASSEMBLY__

#include <linux/build_bug.h>
#include <linux/init.h>
#include <linux/stddef.h>
#include <linux/types.h>
#include <linux/cpumask.h>
Expand Down Expand Up @@ -127,7 +128,7 @@ struct mk_spawn_context {
u32 target_apic_id; /* Target CPU's APIC ID */
u32 flags; /* MK_SPAWN_F_* flags */
u32 ready; /* Signal flag */
u32 reserved; /* Padding for alignment */
u32 abi_magic; /* Host/spawn generation marker */
/* Keep all existing context offsets unchanged. */
struct boot_params bp; /* Standard x86 boot params */
/* Optional boot data belongs after boot_params, in the zeroed tail. */
Expand Down Expand Up @@ -198,7 +199,8 @@ int mk_spawn_cpu(struct mk_instance *instance, int cpu,
/* The pool park set (park page, slot, page tables) as base,size pairs */
int mk_pool_park_regions(u64 *pairs, int max);

/* Initialize boot context tracking in spawn kernel */
/* Validate and initialize boot context tracking in spawn kernel */
struct mk_spawn_context *mk_validate_boot_context(phys_addr_t ctx_phys);
void mk_init_boot_context(phys_addr_t ctx_phys);


Expand All @@ -221,6 +223,11 @@ int multikernel_wakeup_secondary_cpu_64(u32 apicid, unsigned long start_eip,
int multikernel_restore_ap(unsigned int cpu, unsigned long cr3,
unsigned long gs_base, unsigned long stack,
unsigned long entry);
#if defined(CONFIG_MULTIKERNEL) && defined(CONFIG_PCI)
void __init x86_multikernel_pci_platform_init(void);
#else
static inline void x86_multikernel_pci_platform_init(void) { }
#endif

/* NMI on an offline pool CPU: honor a pending force halt */
#ifdef CONFIG_MULTIKERNEL
Expand Down
1 change: 1 addition & 0 deletions arch/x86/include/uapi/asm/bootparam.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
#define XLF_5LEVEL (1<<5)
#define XLF_5LEVEL_ENABLED (1<<6)
#define XLF_MEM_ENCRYPTION (1<<7)
#define XLF_MULTIKERNEL_IPI 0x0100

#ifndef __ASSEMBLER__

Expand Down
24 changes: 24 additions & 0 deletions arch/x86/kernel/kexec-bzimage64.c
Original file line number Diff line number Diff line change
Expand Up @@ -554,6 +554,11 @@ static void *bzImage64_load(struct kimage *image, char *kernel,
.buf_max = ULONG_MAX, .top_down = true };

header = (struct setup_header *)(kernel + setup_hdr_offset);
if (image->type == KEXEC_TYPE_MULTIKERNEL &&
!(header->xloadflags & XLF_MULTIKERNEL_IPI)) {
pr_err("Loaded kernel lacks the required shared transport layout\n");
return ERR_PTR(-EPROTONOSUPPORT);
}
setup_sects = header->setup_sects;
if (setup_sects == 0)
setup_sects = 4;
Expand Down Expand Up @@ -747,10 +752,29 @@ static void *bzImage64_load(struct kimage *image, char *kernel,

/* For multikernel, setup custom e820 map */
if (image->type == KEXEC_TYPE_MULTIKERNEL) {
#ifdef CONFIG_MULTIKERNEL
image->arch.mk_boot_params = bootparam_load_addr;

/*
* setup_boot_parameters() copies the host subarchitecture. A
* spawn kernel must take the multikernel platform path instead.
*/
params->hdr.hardware_subarch = X86_SUBARCH_MULTIKERNEL;

/*
* The spawn trampoline enters the compressed kernel directly,
* bypassing purgatory. The x86 boot protocol's 64-bit entry is
* 0x200 bytes from the start of the protected-mode payload.
*/
image->arch.mk_kernel_entry = kernel_load_addr + 0x200;

ret = mk_e820_fill(image->mk_instance, params);
if (ret)
goto out_free_params;
#else
ret = -EOPNOTSUPP;
goto out_free_params;
#endif
}

/* Allocate loader specific data */
Expand Down
68 changes: 50 additions & 18 deletions arch/x86/kernel/kexec-vmlinux.c
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,17 @@ struct elf_kernel_info {

/*
* Find multikernel entry point from PT_NOTE section.
* Looks for note with name "Linux" and type 0x4d4b ('MK').
* The note type carries the generation; the descriptor remains one u64.
*/
static unsigned long find_multikernel_entry_note(const void *buf, size_t len,
const Elf64_Ehdr *ehdr)
#define MK_VMLINUX_LEGACY_NOTE_TYPE 0x4d4b
#define MK_VMLINUX_PREVIOUS_NOTE_TYPE 0x4d4b0002

static int find_multikernel_entry_note(const void *buf, size_t len,
const Elf64_Ehdr *ehdr,
unsigned long *entry)
{
const Elf64_Phdr *phdrs = buf + ehdr->e_phoff;
bool legacy = false;
int i;

for (i = 0; i < ehdr->e_phnum; i++) {
Expand All @@ -91,25 +96,35 @@ static unsigned long find_multikernel_entry_note(const void *buf, size_t len,
if (ptr + note_size > end)
break;

if (nhdr->n_type == 0x4d4b &&
nhdr->n_namesz == 6 &&
if (nhdr->n_namesz == 6 &&
nhdr->n_descsz == sizeof(u64) &&
!memcmp(ptr + sizeof(*nhdr), "Linux", 6)) {
u64 entry = *(u64 *)(ptr + sizeof(*nhdr) +
ALIGN(nhdr->n_namesz, 4));
pr_info("multikernel: entry=0x%llx\n", entry);
return entry;
const u64 *note_entry;

note_entry = ptr + sizeof(*nhdr) +
ALIGN(nhdr->n_namesz, 4);
if (nhdr->n_type == MK_VMLINUX_NOTE_TYPE) {
*entry = *note_entry;
pr_info("multikernel: entry=0x%llx\n",
*note_entry);
return 0;
}
if (nhdr->n_type == MK_VMLINUX_LEGACY_NOTE_TYPE ||
nhdr->n_type == MK_VMLINUX_PREVIOUS_NOTE_TYPE)
legacy = true;
}
ptr += note_size;
}
}
return 0;
return legacy ? -EPROTONOSUPPORT : -ENOENT;
}

/*
* Parse ELF kernel and extract key information
*/
static int kexec_parse_elf_kernel(const void *kernel_buf, unsigned long kernel_len,
static int kexec_parse_elf_kernel(const void *kernel_buf,
unsigned long kernel_len,
bool multikernel,
struct elf_kernel_info *info)
{
const Elf64_Ehdr *ehdr;
Expand Down Expand Up @@ -159,14 +174,25 @@ static int kexec_parse_elf_kernel(const void *kernel_buf, unsigned long kernel_l
* PT_NOTE contains physical offset from load base, not virtual address.
* This is the canonical way and survives symbol stripping.
*/
info->multikernel_entry = find_multikernel_entry_note(kernel_buf, kernel_len, ehdr);
if (!info->multikernel_entry) {
pr_err("multikernel_startup_64 entry offset not found in PT_NOTE\n");
return -ENOEXEC;
info->multikernel_entry = 0;
if (multikernel) {
int ret;

ret = find_multikernel_entry_note(kernel_buf, kernel_len, ehdr,
&info->multikernel_entry);
if (ret == -EPROTONOSUPPORT)
pr_err("legacy vmlinux note type 0x%x is incompatible; expected 0x%x\n",
MK_VMLINUX_LEGACY_NOTE_TYPE, MK_VMLINUX_NOTE_TYPE);
else if (ret)
pr_err("multikernel ABI note type 0x%x not found\n",
MK_VMLINUX_NOTE_TYPE);
if (ret)
return ret == -ENOENT ? -ENOEXEC : ret;

pr_info("Multikernel entry offset: 0x%lx\n",
info->multikernel_entry);
}

pr_info("Multikernel entry offset: 0x%lx\n", info->multikernel_entry);

/* Find lowest load address and calculate total memory needed */
phdr = (const Elf64_Phdr *)(kernel_buf + ehdr->e_phoff);
for (i = 0; i < ehdr->e_phnum; i++) {
Expand Down Expand Up @@ -326,12 +352,13 @@ static void *vmlinux_load(struct kimage *image, char *kernel,
.top_down = true };
struct kexec_buf pbuf = { .image = image, .buf_min = MIN_PURGATORY_ADDR,
.buf_max = ULONG_MAX, .top_down = true };
bool multikernel = image->type == KEXEC_TYPE_MULTIKERNEL;
int ret;

pr_info("Loading ELF vmlinux (type=%d)\n", image->type);

/* Parse ELF headers */
ret = kexec_parse_elf_kernel(kernel, kernel_len, &elf_info);
ret = kexec_parse_elf_kernel(kernel, kernel_len, multikernel, &elf_info);
if (ret) {
pr_err("Failed to parse ELF kernel: %d\n", ret);
return ERR_PTR(ret);
Expand Down Expand Up @@ -531,12 +558,17 @@ static void *vmlinux_load(struct kimage *image, char *kernel,

/* For multikernel, setup custom e820 map */
if (image->type == KEXEC_TYPE_MULTIKERNEL) {
#ifdef CONFIG_MULTIKERNEL
ret = mk_e820_fill(image->mk_instance, params);
if (ret) {
kvfree(ldata->kernel_buf);
kfree(ldata);
goto out_free_params;
}
#else
ret = -EOPNOTSUPP;
goto out_free_params;
#endif
}

ldata->bootparams_buf = params;
Expand Down
Loading