.. /blog
2026-02-22 // 6 min

LBRV: Hardware LBR Auto-Swap on VMRUN

lbrvstealthamd-svm

This is the shortest post in the series because the fix was the simplest. One flag in the VMCB, zero additional code, and it eliminates an entire detection surface. That's the kind of trade-off I love -- maximum impact for minimum complexity.

Last Branch Records (LBR) are a CPU feature that logs the source and destination of recent branch instructions. The CPU maintains a stack of "from" and "to" address pairs -- the last N branches that were taken. Debuggers use it, profilers use it, and anti-cheat software uses it to check what code has been executing recently.

The problem without LBRV

Every time a #VMEXIT fires, the CPU transitions from guest mode to host mode and starts executing my handler code. My handler has branches -- function calls, match statements, if/else chains. Those branches get recorded in the LBR stack. When the CPU transitions back to guest mode via VMRUN, those LBR entries are still there.

So the guest resumes execution and its LBR stack contains addresses from my hypervisor code. If anything reads the LBR MSRs (IA32_DEBUGCTL, LastBranchFromIP, LastBranchToIP, LastIntFromIP, LastIntToIP), it sees addresses in kernel space that don't correspond to any loaded driver or the kernel itself. Those are addresses from my hidden hypervisor pages -- the ones NPT makes invisible to the guest. The guest can't read those pages, but the LBR still has the addresses. Dead giveaway.

// What the LBR stack looks like WITHOUT LBRV
// after a CPUID intercept → #VMEXIT → handler → VMRUN

// LBR entry 0 (most recent):
//   from: 0xFFFFF800`10234A80  ← guest code (ntoskrnl)
//   to:   0xFFFFF800`10234A90  ← guest code (ntoskrnl)

// LBR entry 1:
//   from: 0xFFFF8800`DEAD1234  ← HYPERVISOR handler!
//   to:   0xFFFF8800`DEAD5678  ← HYPERVISOR handler!

// LBR entry 2:
//   from: 0xFFFF8800`DEAD0100  ← HYPERVISOR vmrun_loop!
//   to:   0xFFFF8800`DEAD0200  ← HYPERVISOR vmrun_loop!

// anti-cheat reads LBR → sees addresses in unmapped region
// → detection: branch targets in hidden memory

LBRV -- the one-flag fix

AMD SVM has a feature called LBR Virtualization (LBRV). When enabled, the CPU automatically saves the host's LBR state and restores the guest's LBR state on VMRUN, and does the reverse on #VMEXIT. The guest never sees host branch records, and the host never sees guest branch records. Hardware does all the work.

// Enable LBRV -- one flag in the VMCB control area
// AMD APM Vol 2, Section 15.17: LBR Virtualization
vmcb.control.virt_ext |= 1 << 1; // LBRV enable

// also need DBGCTL intercept for complete isolation
vmcb.control.intercept_msr_debugctl = 1;

That's it. One bit set in virt_ext, one MSR intercept for DEBUGCTL, and the entire LBR detection surface disappears. The CPU handles all the save/restore automatically on every VMRUN and #VMEXIT. No software save, no timing overhead from manually swapping MSRs, no risk of getting the order wrong.

Without LBRV, I would have had to manually save and restore 4 LBR MSRs (LastBranchFromIP, LastBranchToIP, LastIntFromIP, LastIntToIP) plus the DEBUGCTL register on every single #VMEXIT and VMRUN. That's 5 extra RDMSR + 5 extra WRMSR per exit -- about 2000 extra cycles. LBRV does it in zero software cycles because the hardware handles it during the VMRUN/VMEXIT microcode.

The remaining LBR gap

LBRV handles the 4 legacy LBR MSRs perfectly. But there's a catch on newer AMD CPUs: Zen 3 and later have a 16-entry deep LBR stack (accessed via different MSRs). LBRV only saves the 4 legacy entries. Entries 4 through 15 might still contain host branch targets after a VMRUN.

This is a known gap in my detection vector analysis (Tier 3, item 19). Vanguard monitors LBR and could potentially spot host addresses in the deep stack entries. The mitigation would be to manually save/restore the extended LBR entries -- which I haven't done yet because no anti-cheat has actually checked the deep stack entries in practice.

For now, LBRV handles the common case. The legacy 4-entry LBR is what hvdetecc and most public detectors check. The deep stack is a theoretical concern that I'll address when it becomes a practical one. The beauty of LBRV is that the 99% case costs exactly one bit in the VMCB. That's the cleanest fix in the entire hypervisor.