Kore ABI
Virtualization
How guests are built on Kore: three capabilities, vCPUs as ordinary tasks, one page of exit and control rings per vCPU, and what the hypervisor service offers programs today.
Source: guest/wire.rsA guest is three capabilities and some tasks
Kore's hypervisor has two halves. In the kernel, the world switch runs each virtual CPU as an ordinary scheduled task. In user space, a virtual machine monitor decides what the guest's devices are and services everything the guest does that the hardware can't. The contract between them is a handful of capabilities and one page of rings per vCPU.
| Capability | What it is |
|---|---|
Hypervisor | The machine-virtualization facility. Its holder can create guest address spaces in the hardware format chosen at boot. |
GuestSpace | One guest-physical address space, projected at once into the CPU's second-stage tables (Intel EPT, AMD NPT) and the IOMMU's, so the guest's CPUs and its devices see the same memory under the same rules. |
GuestRegion | One lease-backed window installed in a guest space. The lease stays the physical owner; the region names the guest-visible mapping. |
A vCPU is a Task founded into a guest space. Its handle carries all four rights, and like a founded process it is dormant until started.
Who can build a guest
The Hypervisor capability reaches a task only by founding, under role::HYPERVISOR. In Cosmos, the hypervisor service is the one task founded with it, together with a bounded memory supply (HYPERVISOR_SUPPLY_BYTES, 8 MiB), so a guest that doesn't fit fails its allocation cleanly instead of drawing on kernel memory. Programs use guests through that service rather than holding the facility themselves.
The verbs below are therefore what a monitor uses. They are public, because they are part of the contract, and they refuse without the capability.
Building one: the monitor's side
kore-rt carries the verbs, reachable from libkore as kore::runtime::cap::verbs:
| Verb | Does |
|---|---|
claim_hypervisor(slot) | Takes the founded Hypervisor grant as a typed token |
create_guest_space(&hypervisor) | Creates one owning GuestSpace |
share_stashed_into_guest(&lease, &space, gpa, bytes, access) | Installs a lease carved from the monitor's supply at a guest-physical address, returning a GuestRegion |
share_into_guest(&lease, &space, gpa, bytes, access) | The same, for a lease delivered at founding |
found_guest_vcpu(&space, config, inputs) | Founds one dormant vCPU task |
found_guest_cohort(&space, inputs) | Founds a bounded multi-CPU cohort in one founding tail, every vCPU dormant |
A region's access must include read, and may not include both write and execute: second-stage mappings are W^X like everything else. Its length must equal the lease's contiguous span. On a refusal before commit, found_guest_vcpu hands back every input it was given, so no half of the vCPU's plumbing is lost.
use kore::abi::guest::GuestVcpuConfig;
use kore::abi::session::{SHARE_ACCESS_R, SHARE_ACCESS_W, SHARE_ACCESS_X};
use kore::runtime::cap::verbs::{self, GuestVcpuInputs};
use kore::runtime::cap::{
GuestRegionToken, GuestSpaceToken, HypervisorToken, StashedLease, TaskStartOutcome,
TaskToken,
};
/// RAM the monitor already carved from its supply for one guest.
pub struct GuestRam<'a> {
pub code: &'a StashedLease,
pub code_bytes: u64,
pub data: &'a StashedLease,
pub data_gpa: u64,
pub data_bytes: u64,
}
/// One running guest, and the handles that own it.
pub struct Guest {
pub space: GuestSpaceToken,
pub code: GuestRegionToken,
pub data: GuestRegionToken,
pub vcpu: TaskToken,
}
pub fn build_guest(
hypervisor: &HypervisorToken,
ram: GuestRam<'_>,
first_entry: GuestVcpuConfig,
window: GuestVcpuInputs,
) -> Option<Guest> {
// One guest-physical address space, seen identically by the guest's
// CPUs and by DMA.
let space = verbs::create_guest_space(hypervisor).ok()?;
// Stage-2 W^X: code is readable and executable, data readable and
// writable. A region asking for both W and X is refused.
let code = verbs::share_stashed_into_guest(
ram.code,
&space,
0,
ram.code_bytes,
SHARE_ACCESS_R | SHARE_ACCESS_X,
)
.ok()?;
let data = verbs::share_stashed_into_guest(
ram.data,
&space,
ram.data_gpa,
ram.data_bytes,
SHARE_ACCESS_R | SHARE_ACCESS_W,
)
.ok()?;
// The vCPU is an ordinary task, founded dormant: the window and both
// doorbell endpoints move into it only when the founding commits.
let vcpu = verbs::found_guest_vcpu(&space, first_entry, window).ok()?;
match vcpu.start() {
TaskStartOutcome::Active => Some(Guest { space, code, data, vcpu }),
_ => None,
}
}GuestVcpuConfig is the architecture-neutral first entry: entry, stack and page_table_root, the guest-physical root of the guest's own page tables. GuestVcpuInputs carries the three things that move into the vCPU: the lease holding its formatted window page, the channel end it waits on to resume (resume_recv), and the one it kicks the monitor's doorbell with (exit_send).
On the wire these are session derives: GuestShareReq (kind = GuestRegion) for a region, and DERIVE_KIND_GUEST_FOUND with GuestFoundReq for a vCPU. See sessions.
The vCPU window
Each vCPU has one 4096-byte page holding two single-producer rings of 512-byte records:
| Ring | Direction | Capacity | Why that capacity |
|---|---|---|---|
| Exit | Kernel → monitor | 2 | By proof: the vCPU parks after every publish, so at most one ordinary exit and the terminal record can be outstanding. A full exit ring is a protocol violation, and the kernel retires the poisoned window instead of dropping or waiting. |
| Control | Monitor → kernel | 4 | The largest sane batch (write state, inject, resume, with headroom), drained to empty at the vCPU's safe point. A full control ring is ordinary backpressure. |
Records are bytes and indices only: no capability ever rides a guest ring, and every field decodes from arbitrary bytes, because the peer is untrusted. Each record carries the guest's register image as 40 quadwords in the executing architecture's own wire order. The protocol names registers only by semantic role (state_index::IO_DATUM, state_index::IP), so a consumer never carries a per-architecture register map.
| Exit kind | Value | Meaning |
|---|---|---|
MMIO | 0x4701 | Reserved for a kernel-classified device access; today such faults arrive as STAGE_FAULT |
PORT_IO | 0x4702 | A port in/out trapped (x86) |
CPUID | 0x4703 | CPUID or an intercepted ID-register read |
MSR | 0x4704 | A model-specific register access |
HLT | 0x4705 | The guest idled; only an injected event wakes it |
STAGE_FAULT | 0x4706 | A second-stage fault at a guest-physical address, with the decode-assist tail |
ARCH | 0x4707 | An architecture-specific exit the shared taxonomy doesn't name |
RETIRED | 0x4708 | Terminal: the last record a vCPU publishes, with its reason (shutdown, ordered, poisoned or revoked) |
OP_REFUSED | 0x4709 | A control record was refused, with which one and why |
| Control kind | Value | Meaning |
|---|---|---|
WRITE_STATE | 0x4801 | Install the carried register image before the next entry |
INJECT | 0x4802 | Inject an event into the guest |
RESUME | 0x4803 | Resume the vCPU |
RETIRE | 0x4804 | Retire the vCPU |
A second-stage fault names an address, not an instruction. To complete an MMIO access without a second exit, the record's decode-assist tail carries whatever the hardware knew: the raw instruction bytes (assist::INSN, up to 15), an already-decoded access (assist::ACCESS, as Arm and RISC-V report it), or an honest assist::NONE.
Doorbell notices (NOTICE_COOKIE_GUEST_EXIT, NOTICE_COOKIE_GUEST_CONTROL) only wake the other side. The rings are the source of truth, and a woken consumer drains its ring completely.
Using one: the client's side
Programs talk to the hypervisor service over the ordinary service protocol: HypervisorReqWire and HypervisorRepWire in kore::abi::svc. A guest's console doesn't ride the verbs. It has its own planes in the pair's window: an event ring of output from the guest, and an input ring from the client.
Today the protocol's verbs start the service's built-in guests (an echo console, a halting guest, compiled C, a multi-segment image, MicroPython, an MMIO-console guest and a multi-CPU guest), attach and detach the console, report status and stop the guest, with one live guest at a time. They exist to prove the path end to end, and Cosmos's shell uses them to run MicroPython.