Re: [PATCH v3 0/4] KVM: TDX: Validate directly configurable CPUID bits
From: Artem Bityutskiy
Date: Tue Sep 08 2026 - 16:38:00 EST
**Disclaimer**: I am new to KVM and TDX, still learning, let me know if
some of my comments are off.
It took me several days digging through docs and code to understand what is
going on here, so let me summarize my understanding below and please correct
me where I am wrong.
On Thu, 2026-08-27 at 11:18 +0800, Binbin Wu wrote:
> Hi,
>
> The purpose of this patch series is to prevent userspace from enabling
> host state clobbering features that KVM does not support for TDX. A host
> state clobbering feature exposed on a new TDX module/platform can corrupt
> host state if KVM does not explicitly save and restore the related MSR(s)
> across host/guest transitions. If such a feature is blindly exposed to
> and used by a TD, the host will behave unexpectedly.
>
> Except for a few fixed-1 bits required for basic TDX support, host state
> clobbering features are either directly configurable or gated by TD
> ATTRIBUTES/XFAM. So an allowlist covering only the directly configurable
> CPUID bits, plus the corresponding filtering and validation, is sufficient
> to serve the purpose while keeping the code footprint small.
So the general principle is:
- KVM is responsible for protecting the host state from being clobbered
by the TD. Different approaches can be taken here.
- Saving and restoring the host state within KVM itself.
- For a subset of registers, relying on the TDX module to save and
restore the host state.
- Disallowing certain TD features altogether if they pose a risk to
host state.
- TDX module is responsible for saving and restoring TD state.
We have 2 boundaries: VMM <-> TDX module and TDX module <-> TD. Only the
first one is relevant to host state clobbering.
Approach
========
Your patch-set adds an explicit allowlist to KVM: a TD may only use a
feature if every register it needs is either saved/restored by KVM
itself, or is preserved across TDH.VP.ENTER by the TDX module or HW.
I think this approach is safe and sound: each feature is checked before
it is added to the allowlist. No surprises.
A CPU feature is associated with a group of registers, e.g. WAITPKG is
the IA32_UMWAIT_CONTROL MSR. So instead of trying to control individual
register access by TD, KVM controls which features are exposed to the
TD.
CPUID is the actual mechanism for turning CPU features on/off for a TD.
This patch works because the TDX module does not let a TD access the
registers behind a CPU feature unless that feature is exposed via the
virtual CPUID. An attempt to do so results in #UD or #GP.
Not every configurable CPUID field gates some registers, though. Some
are pure enumeration, e.g. family/model/stepping and cache parameters.
So the approach is for KVM to look at what features are enabled in the
virtualized CPUID, and reject the unsafe ones.
Details
=======
TDX module enables TD features only at TD build time, via `TD_PARAMS`
structure of the `TDH.MNG.INIT` seamcall. This happens in the
KVM_TDX_INIT_VM ioctl, after which the feature set cannot be widened.
Migration cannot change them either: the `TDH.IMPORT.STATE.IMMUTABLE`
seamcall imports the source TD's configuration state as-is.
The virtual CPUID values are built based on what the TDX module
supports, what the host supports, and what userspace passed in via the
KVM_TDX_INIT_VM ioctl.
The ioctl carries 'ATTRIBUTES', 'XFAM', and 'CPUID_CONFIG', all fields
of 'TD_PARAMS'. ATTRIBUTES and XFAM turn features on/off and shape
CPUID, but KVM only allows bits it already knows about, so there is no
clobbering risk there.
CPUID_CONFIG is different: it lets userspace set CPUID leaf values
directly (but not for any CPUID, only for pieces of it the TDX module
allows, and KVM enumerates them via the KVM_TDX_CAPABILITIES ioctl).
That is the problematic part. KVM does not check its contents, beyond a
denylist that only filters out TSX and WAITPKG features. Everything
else is allowed.
So your patch-set basically kicks out the current denylist mechanism
and replaces it with an allowlist of directly configurable CPUID leaves
and bits.
There is a long list of CPUID pieces you allowed. I wanted to
acknowledge that it must have been quite an effort to go through all of
them and determine which ones are safe to allow.
Did I understand your work correctly?
> Expected host state clobbering behavior for TDX
> ===============================================
> We also want to call for discussions about the expected host state
> clobbering behavior for TDX here for future features.
>
> For a normal VMX guest, VM entry/exit behavior for a given piece of CPU
> state is architecturally defined: state is either switched by hardware via
> VMCS host/guest fields, or left as the guest value on VM exit and managed
> by KVM in software.
>
> For TDs, the host/guest transition goes through TDH.VP.ENTER, and what the
> TDX module does with a given piece of host state is defined by the TDX
> module ABI rather than by the x86 architecture.
I think I found this contract: the TDH.VP.ENTER definition in the ABI
spec, section "CPU State Preservation Following a Successful TD Entry
and a TD Exit". It refers to a table that lists the MSRs whose value
may not be preserved across TD entry and exit, with the condition for
each, e.g.
IA32_PL0_SSP Init(XFAM[11] | XFAM[12])
IA32_UMWAIT_CONTROL Init(virt. CPUID(7,0).ECX[5])
This is from an older version of the TDX ABI specification. I could not
find 'msr_preservation.pdf' published. But I assume it is published.
Anyway, seems to be a clear contract to me.
>
> What we would like to align on is the expected baseline behavior of
> TDH.VP.ENTER for future features. The proposal is to have TDX simply
> match VMX behavior, i.e. on return from TDH.VP.ENTER, state that VMX would
> restore from the VMCS host fields is restored, and state that VMX would
> leave as the guest value is clobbered. That keeps a single model for VMM,
> and means enabling a new feature for TDs requires the same work flow as
> enabling it for VMX.
Let me restate this to check I follow.
For a VMX guest, it is the VMCS host-state area that is used for
restoring host state. KVM writes it in advance, hardware restores it
upon VM exit.
Most of it is loaded unconditionally: control registers, RSP, RIP,
SYSENTER MSRs and some more. But there are some MSRs that
are restored only if KVM configures the corresponding control bits.
Example: IA32_PAT, IA32_EFER. If the control is clear, the MSR keeps
the guest value upon VM exit.
In KVM some of those controls are set once, e.g. for IA32_PAT. Others
are toggled at run time, e.g. "load IA32_EFER" and "load
IA32_PERF_GLOBAL_CTRL". So there is dynamicity there.
So the proposal is that TDH.VP.ENTER should draw the same line: whatever
VMX would reload from the host-state area is preserved, whatever VMX
leaves as the guest value is clobbered and KVM handles it. Is that
right?
So with TDX module there is a twist. First of all, my understanding is
that the SEAM VMCS (which controls VMM <-> TDX module state
save/restore) is configured when the TDX module is loaded and stays
fixed after that. So no dynamicity there.
Second, the SDM says SEAMCALL behaves like an SMM VM exit and SEAMRET
like a VM entry returning from SMM (SDM 35.1.1, 35.1.2), and SMM VM
exits save state into the guest-state area (SDM 34.15.2.2).
For reference: An SMM VM exit is a VM exit that begins outside SMM
and that ends in SMM.
So if I understand correctly, for VMM <-> TDX module switches, the
hardware uses the VMCS guest-state area, not the host-state area.
Not like in VMX guest case. On TDH.VP.ENTER it saves the VMM state
to the guest-state area, and on return it restores the VMM state.
IOW, looks like there may be significant differences between the VMM
<-> TDX module state handling and VMX VM entries and exits.
So maybe the way to go is to just follow 'msr_preservation.pdf' and
adjust the allowlist? I find this approach safe and acceptable.
Artem.