SMM Timer Callback: Why 50 Ticks Was Too Late
The PeriodicTimerCallback fires every ~64ms. I set AUTO_TRIGGER_TICK_THRESHOLD to 50, meaning the hypervisor would launch after 50 ticks -- about 3.2 seconds after the SMI timer registered. By then, Windows was already running and HvlPhase0Initialize had already checked for a hypervisor. I was too late. The whole point of launching from SMM was to be there before the kernel looked.
This was the first real architectural problem I hit with SmmInfect. The hypervisor code was working. The VMCB was correct. VMRUN succeeded. But the timing was wrong, and wrong timing in a pre-boot hypervisor means you lose the entire advantage of being pre-boot.
The timer architecture
SmmInfect registers a periodic SMI that fires at a fixed interval. The UEFI spec's EFI_SMM_PERIODIC_TIMER_DISPATCH2_PROTOCOL provides this. I chose the smallest available interval -- SmiTickInterval of roughly 64ms on my Gigabyte B550M board. The callback increments a tick counter and checks whether it's time to act.
// the original timer logic
static UINT32 gTickCount = 0;
static BOOLEAN gVirtualized = FALSE;
// threshold: don't try to virtualize until tick 50
// rationale: need to wait for the kernel to be loaded
#define AUTO_TRIGGER_TICK_THRESHOLD 50
EFI_STATUS EFIAPI PeriodicTimerCallback(
IN EFI_HANDLE DispatchHandle,
IN const VOID *Context,
IN OUT VOID *CommBuffer,
IN OUT UINTN *CommBufferSize
) {
gTickCount++;
if (gVirtualized) return EFI_SUCCESS;
if (gTickCount < AUTO_TRIGGER_TICK_THRESHOLD) return EFI_SUCCESS;
if (DetectWindowsKernel()) {
VirtualizeCore();
gVirtualized = TRUE;
}
return EFI_SUCCESS;
}
The idea was sound: wait for the kernel to load, detect it, virtualize. The problem is the timing assumption. I assumed the kernel would be idle for a while after loading, giving me time to slip in. Wrong.
The HvlPhase0Initialize race
Windows checks for a hypervisor very early in kernel initialization -- before most drivers load, before the I/O manager starts, before the registry is fully mounted. The check happens in HvlPhase0Initialize, which is called from KiSystemStartup. On my hardware, this runs approximately 1.8 seconds after the kernel starts executing.
// timeline on my hardware (Ryzen 7 5800XT, NVMe boot)
// all times relative to power-on
T+0.0s: PeriodicTimerCallback registered
T+1.2s: bootmgfw.efi starts
T+1.8s: winload.efi starts
T+2.4s: ntoskrnl entry point
T+2.6s: HvlPhase0Initialize() // checks for hypervisor HERE
//
T+3.2s: gTickCount == 50 // my threshold fires HERE
//
// problem: HvlPhase0Initialize ran 600ms BEFORE my threshold
HvlPhase0Initialize runs CPUID leaf 0x40000000. If it gets "Microsoft Hv" back, it initializes the Hyper-V enlightenment path. If it gets nothing (no hypervisor present), it skips the entire enlightenment layer and continues. This check happens once during boot. There's no retry, no polling. If I'm not there when it checks, I'm invisible in the worst possible way -- the kernel proceeds without hypervisor support, and late injection causes inconsistencies.
Lowering the threshold doesn't fix it
My first instinct was to lower AUTO_TRIGGER_TICK_THRESHOLD to 30 or even 10. But this doesn't work reliably because the problem isn't the threshold number -- it's the unpredictability of the timeline.
// timing varies wildly across hardware
//
// Gigabyte B550M + NVMe:
// kernel entry at T+2.4s, HvlPhase0Initialize at T+2.6s
//
// ASUS X570 + SATA SSD:
// kernel entry at T+4.1s, HvlPhase0Initialize at T+4.3s
//
// Dell OptiPlex (older BIOS, HDD):
// kernel entry at T+8.0s, HvlPhase0Initialize at T+8.2s
//
// there is NO fixed tick count that works on all hardware
// if I set threshold=10 on fast hardware, I try to virtualize
// during UEFI when there's no kernel to detect
// if I set threshold=50, I'm too late on fast hardware
Different hardware, different BIOS versions, different boot devices -- all shift the timeline. An NVMe boot on a fast board might have the kernel running at T+2.4s. A SATA SSD on an older board might take T+4s. A mechanical hard drive might take T+8s. There is no single tick count that is both early enough to catch the kernel and late enough to ensure the kernel exists.
DetectWindowsKernel() heuristic with no threshold at all -- start checking from tick 1. But during UEFI boot, the CR3/RIP values can temporarily match the heuristic as various DXE drivers load at high addresses. Without a threshold, I'd try to virtualize a UEFI DXE driver thinking it was the Windows kernel. That produces a spectacular crash because the VMCB's saved state doesn't match the actual UEFI context.The real solution: don't use timers
The timer approach is fundamentally flawed. Periodic polling with a fixed threshold can't solve a timing problem where the deadline varies by 5x across hardware configurations. I needed a different trigger mechanism.
The approach that actually works is event-driven, not timer-driven. Instead of polling on a timer, I hook into the boot path itself. There are two viable approaches:
// approach 1: hook GetVariable in the EFI runtime services
// hvax64.exe calls GetVariable to read boot config
// I intercept this call and inject my code at that exact moment
// (this is the parasitic Hyper-V approach from hv_hook.c)
// approach 2: hook ExitBootServices
// the OS calls ExitBootServices exactly once, right before
// transitioning from UEFI to the kernel. I register an
// SMM handler that fires on ExitBootServices and virtualizes
// at that exact transition point.
SmmRegisterProtocolNotify(
&gEfiExitBootServicesGuid,
OnExitBootServices,
&Registration
);
The ExitBootServices hook is the cleanest option. The OS calls EFI_BOOT_SERVICES.ExitBootServices() exactly once, at the transition from UEFI to the kernel. I register an SMM notification for this event. When it fires, I know the kernel is about to start -- I virtualize at that exact moment, before the kernel gets its first instruction. No polling, no thresholds, no timing assumptions.
What I learned about SMM timing
SMM's periodic timer is a blunt instrument. It's good for tasks that don't have a deadline -- monitoring temperatures, updating watchdog counters, logging diagnostic data. It's terrible for tasks that must happen in a specific window, because the window varies by hardware and the timer granularity (64ms) is coarse relative to boot event timing.
The deeper lesson: if I need to intercept a specific moment in the boot process, I should hook that moment directly. Don't poll for it. The boot process has well-defined events -- ExitBootServices, SetVirtualAddressMap, ReadyToBoot -- and SMM can listen for all of them. Hooking the event is deterministic. Polling a timer and hoping to catch it is not.
I moved SmmInfect to the ExitBootServices hook, and the timing problem disappeared. The hypervisor is active before the kernel's first instruction. HvlPhase0Initialize sees my CPUID response on its first check. No race, no threshold, no guessing.