Back to work

Custom AMD SVM hypervisor

A minimal Type-1 hypervisor loaded as a UEFI application. When it launches, the guest is the caller itself, so the firmware and every OS boot afterwards runs under our control without knowing it. In active development.

Role Sole engineer · Stack C, MSVC, EDK II · Arch AMD-V, SVM, NPT · Status In development, private

Not a driver. A firmware image.

Most educational hypervisors ship as a Windows kernel driver: the OS boots normally, then a signed .sys loads and virtualises the running system. That works, but the driver shows up in the loaded module list and it means everything before your driver ran on bare metal.

This one is a UEFI application built against EDK II. It runs before the OS. Once it hands control back, the firmware, the boot manager, the kernel, and everything in userspace runs as a guest of the hypervisor from cycle zero.

The guest is the caller itself.

VMRUN is set up so that on entry, the VMCB's guest state is a snapshot of the exact CPU state at the moment the C code called into the launch stub. When VMRUN returns, execution continues in what looks like the next C statement, only now the CPU is running that code as a guest under our control. No separate guest image, no "start Windows now" step. The caller keeps going, virtualised.

This is the cleanest bring-up model for a UEFI-resident hypervisor. It also makes it trivial to catch the first surprising VM-exit: whatever the firmware does next, that's your first stress test.

// Vcpu.c - VMCB guest state is populated from the live CPU,
// so VMRUN "returns" as the guest at the next instruction.
static void VcpuFillGuestStateFromHost (HV_VCPU *Vcpu) {
    VMCB *Vmcb = Vcpu->Vmcb;

    // Selectors, bases, limits, access rights - populated via VMSAVE
    __svm_vmsave ((size_t)Vmcb);

    // Control registers and RIP/RSP/RFLAGS captured at call site
    Vmcb->Cr0    = __readcr0 ();
    Vmcb->Cr3    = __readcr3 ();
    Vmcb->Cr4    = __readcr4 ();
    Vmcb->Rip    = Vcpu->LaunchRip;     // = &guest_entry_label
    Vmcb->Rsp    = Vcpu->LaunchRsp;
    Vmcb->Rflags = Vcpu->LaunchRflags;
    Vmcb->GuestAsid = 1;
}

Identity-mapped NPT across 512 GiB.

Nested Page Tables give the hypervisor a second translation layer the guest cannot see. The initial NPT is a plain identity map: guest physical address equals host physical address, RWX everywhere. That keeps early bring-up simple, and once the guest is running, individual pages can be re-mapped, hidden, or hooked without touching the guest's own paging.

The NPT is built with 2 MiB large pages to keep the walk short and the TLB pressure low. Everything above 512 GiB is left unmapped so a stray access from a broken exit handler faults cleanly instead of scribbling on host state.

One dispatcher, small and legible.

VM-exits are expensive, so the exit handler only intercepts what actually needs handling: CPUID (to hide the hypervisor from feature queries), VMMCALL (the hypercall channel), a small MSR window, and the mandatory ones the architecture requires. Everything else runs on the metal without a round trip.

The dispatcher is a switch on Vmcb->ExitCode, one function per reason, no clever indirection. That's a deliberate choice: hypervisor bugs are catastrophic and the exit path is where you spend all your debugger time, so it stays flat and readable.

"The rule is: minimise interception. Every trap is a debugger session waiting to happen."

Multi-processor, then stealth.

Current focus is per-CPU launch across every logical processor, using UEFI's MP protocol. After that, the CPUID and MSR windows get widened to hide the hypervisor's presence from software probes, and the NPT gains a hook API so specific guest pages can be swapped for shadow copies on execute.

None of this is unusual for a research hypervisor; the value here is that the codebase is small, honest, and stays that way. If you have a research need in this space, source and design notes are available on request.

navigate · selectGlobality