KVM NX huge page recovery burns a core per guest on kernel 7.0.x

waynej

Member
Aug 28, 2023
5
2
8
Putting this here in case anyone else is affected by this issue.

We've been having an issues with VM performance after upgrading our cluster to PVE9 on kernel 7.0.2-6-pve. This has been happening for a few of months and we finally managed to pin it down.

Symptoms on the guest varied, it wasn't noticeable on some VMs, other VMs saw significant CPU usage increases. I was mainly noticeable on when the VM was under load. On the worst affected VMs we would notice CPU usage increase to 100% for 2-3s every 60s in top/htop. When checking on the host we noticed that this correlated when kvm-nx-lpage-re thread started processing.

Code:
top -H -b -p $(cat /var/run/qemu-server/<VMID>.pid)

top - 12:53:36 up 3 days, 14:29,  1 user,  load average: 22.37, 21.69, 22.33
Threads:  15 total,   5 running,  10 sleeping,   0 stopped,   0 zombie
%Cpu(s): 20.5 us, 26.0 sy,  0.0 ni, 52.7 id,  0.4 wa,  0.0 hi,  0.5 si,  0.0 st
MiB Mem : 772546.6 total, 390061.2 free, 219608.5 used, 169506.2 buff/cache     
MiB Swap:   1022.0 total,   1022.0 free,      0.0 used. 552938.1 avail Mem

    PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND                                                                                                                                                                                                 
  48875 root      20   0   20.8g  15.5g  11288 R  99.9   2.1      6,10 CPU 2/KVM                                                                                                                                                                                               
  48876 root      20   0   20.8g  15.5g  11288 R  99.9   2.1      6,16 CPU 3/KVM                                                                                                                                                                                               
  48874 root      20   0   20.8g  15.5g  11288 R  99.7   2.1      6,10 CPU 1/KVM                                                                                                                                                                                               
  49077 root      20   0   20.8g  15.5g  11288 R  99.0   2.1  15:43.10 kvm-nx-lpage-re                                                                                                                                                                                         
  48873 root      20   0   20.8g  15.5g  11288 R  56.8   2.1 222:55.52 CPU 0/KVM                                                                                                                                                                                               
  48872 root      20   0   20.8g  15.5g  11288 S  17.6   2.1 177:53.83 vhost-48832                                                                                                                                                                                             
  48832 root      20   0   20.8g  15.5g  11288 S   0.0   2.1   1:16.95 kvm                                                                                                                                                                                                     
  48833 root      20   0   20.8g  15.5g  11288 S   0.0   2.1   0:00.00 call_rcu                                                                                                                                                                                               
  48879 root      20   0   20.8g  15.5g  11288 S   0.0   2.1   0:00.00 vnc_worker                                                                                                                                                                                             
 974602 root      20   0   20.8g  15.5g  11288 S   0.0   2.1   0:00.00 worker                                                                                                                                                                                                 
 975530 root      20   0   20.8g  15.5g  11288 S   0.0   2.1   0:00.00 worker                                                                                                                                                                                                 
 975531 root      20   0   20.8g  15.5g  11288 S   0.0   2.1   0:00.00 worker                                                                                                                                                                                                 
 975532 root      20   0   20.8g  15.5g  11288 S   0.0   2.1   0:00.00 worker                                                                                                                                                                                                 
 975533 root      20   0   20.8g  15.5g  11288 S   0.0   2.1   0:00.00 worker                                                                                                                                                                                                 
 975534 root      20   0   20.8g  15.5g  11288 S   0.0   2.1   0:00.00 worker

The workaround was to disable nx_huge_pages_recovery_ratio.

Code:
echo 0 > /sys/module/kvm/parameters/nx_huge_pages_recovery_ratio

We have not seen any issues since implementing the workaround. The fix is to roll back to kernel 6.17.x, but we have not tested this. It does not look like this bug has been logged upstream yet.

Analysis of the kernel code by Claude points at this as a bug in the kernel introduced in 6.19+.

The defect: kvm_recover_nx_huge_pages() sizes its loop from a per-MMU-type counter, kvm->arch.possible_nx_huge_pages[mmu_type].nr_pages. The unaccount path does not honour the type:

static void unaccount_nx_huge_page(struct kvm *kvm, struct kvm_mmu_page *sp)
{
sp->nx_huge_page_disallowed = false;
untrack_possible_nx_huge_page(kvm, sp, KVM_SHADOW_MMU); /* hardcoded */
}

It takes no mmu_type and always passes KVM_SHADOW_MMU. So on a TDP MMU guest, which is every guest on current Intel and AMD hardware, recovering a page decrements the shadow counter instead of the TDP one. The corrective decrement in tdp_mmu_unlink_sp() never
runs, because unaccount_nx_huge_page() cleared sp->nx_huge_page_disallowed on the line above and it returns early.

Code:
  ┌───────────────────────────┬─────────────────────────────┬───────────┐
  │          counter          │           actual            │ intended  │
  ├───────────────────────────┼─────────────────────────────┼───────────┤
  │ kvm->stat.nx_lpage_splits │ -1                          │ -1        │
  ├───────────────────────────┼─────────────────────────────┼───────────┤
  │ nr_pages[KVM_TDP_MMU]     │ unchanged                   │ -1        │
  ├───────────────────────────┼─────────────────────────────┼───────────┤
  │ nr_pages[KVM_SHADOW_MMU]  │ -1 (underflows toward 2^64) │ unchanged │
  └───────────────────────────┴─────────────────────────────┴───────────┘

Consequence: nr_pages[KVM_TDP_MMU] is incremented on every split and never decremented, growing monotonically for the life of the guest. The loop bound derived from it grows without limit, so the pass exits only when the list empties, while vCPUs re-fault and re-split pages onto it as it runs. Measured on vm: 305,168 calls to kvm_tdp_mmu_zap_possible_nx_huge_page in 130 s where the design intends 4, a factor of 76,292.

nx_lpage_splits is maintained correctly, so the debugfs gauge looks sane while the real driver is a hidden counter not exposed anywhere. Cost therefore runs inverse to the visible split count and tracks guest age instead. The worker is a vhost_task inside the QEMU thread group, so its time bills to the guest as proxmox.qemu.cpu, and it is bursty (a few seconds at 99% of a core, then ~50 s asleep), which cumulative sampling misses entirely.

Fix: roll back to proxmox-kernel-6.17 (6.17.13-21, pve-enterprise), which predates the per-MMU-type array and has mitigation parity with 7.0.14. Needs a maintenance window.
 
  • Like
Reactions: aderumier
Hi @waynej,

thanks for posting on the forum!
Fix: roll back to proxmox-kernel-6.17 (6.17.13-21, pve-enterprise), which predates the per-MMU-type array and has mitigation parity with 7.0.14. Needs a maintenance window.
For clarification is 7.0.14 working properly or is this version still affected?
Since your topic states 7.0.X i assume this would still be affected but want to ask before i look into it in more detail.

Best regards
Jonas
 
Hi @waynej,

thanks for posting on the forum!

For clarification is 7.0.14 working properly or is this version still affected?
Since your topic states 7.0.X i assume this would still be affected but want to ask before i look into it in more detail.

Best regards
Jonas
Anything after 6.19 is affected. 6.17.13-21 has the same CPU vulnerability mitigations as 7.0.14, as opposed to 6.14.x kernels which are missing some mitigations that 6.17.x and 7.0.x cover.
 
windows or linux guests? does guest OS / workload matter?
It affected Windows and Linux guests.

You can check by monitoring top for a VM with:
Code:
top -H -b -p $(cat /var/run/qemu-server/<VMID>.pid)

If you see CPU usage for kvm-nx-lpage-re spike around every minute if it is affected. I wakes up every 60s by default, so you might see some usage, but it shouldn't burn a whole core for a couple of seconds. On our worst affected VMs this would spike to 99%.
 
I am on 7.0.14-12.
I checked VMs ( ~ 10 with highest cpu usage of cluster) and all have TIME+ of 00:00:00 for "kvm-nx-lpage-re".

So no "cpu burning" here as it would increase TIME+ like in your console output with 15:43.
 
I am on 7.0.14-12.
I checked VMs ( ~ 10 with highest cpu usage of cluster) and all have TIME+ of 00:00:00 for "kvm-nx-lpage-re".

So no "cpu burning" here as it would increase TIME+ like in your console output with 15:43.

That is possible, AFAIK, it should only affect systems when iTLB multihit (CVE-2018-12207) mitigation is enabled. If the CPU is not vulnerable, the mitigation has been turned off or if nx_huge_pages_recovery_ratio=0 you wouldn't any CPU time for that process.

To check: cat /sys/devices/system/cpu/vulnerabilities/itlb_multihit
If it is KVM: Mitigation: Split huge pages then the mitigation is active. If it is KVM: Vulnerable the vulnerability is present, but mitigation has been turned off.

The VM nx_lpage_splits is also an indicator. Run find /sys/kernel/debug/kvm -name nx_lpage_splits -exec sh -c 'echo -n "$1: "; cat "$1"' _ {} \; If they are not zero, then the mitigation is active.

The mitigation can be disabled entirely with echo N > /sys/module/kvm/parameters/nx_huge_pages . This does leave the system vulnerable. Not recommended if there is untrusted software/workloads or multi-tenant. On single-tenant environments or if the workloads are trusted, it shouldn't be an issue.
 
Thanks for clarification.
So basically you should not be affected if your CPU desgin is 2019 or newer as CVE is dated 2018.

My rather new AMD Epyc are not affected.

Code:
root@xxxxxx ~ # cat /sys/devices/system/cpu/vulnerabilities/itlb_multihit
Not affected
 
Thanks for clarifying at all!

Will try to verify it now on an "old" Xeon E5-2620 v4 which should still be affected.

Best regards
Jonas
 
So tried it very quickly for now with the aforementioned E5-2620 v4:
Code:
# uname -r
7.0.14-14-pve

# cat /sys/devices/system/cpu/vulnerabilities/itlb_multihit
KVM: Mitigation: Split huge pages

# cat /sys/module/kvm/parameters/nx_huge_pages_recovery_ratio
60

Used a Debian 13 VM with x86_64_v3 CPU type and stress-ng --vm 1 as stressor inside the guest.
Yielded no CPU spikes and no time spent in kvm-nx-lpage-re
Code:
   2482 root      20   0   10.0g   1.2g   7892 S   0.0   1.9   0:00.00 kvm-nx-lpage-re

Could you @waynej share a little more details on the used VM configuration or if you already see something i overlooked, please tell me what i should do differently.

Best regards
Jonas