요약·해설과 원문, 전문 번역을 서로 분리했습니다. API 이름, symbol, source path는 원문 표기를 사용합니다.
1. 요약·해설
원문의 핵심 논리와 kernel programming 관점의 보충 설명입니다. 아래의 전문 번역과는 별도로 작성했습니다.
2. 영어 원문 전체
번역 기준이 된 Linux v6.18.37 원문입니다. 줄 번호는 이 버전의 파일 좌표입니다.
원문 전체 펼치기
.. SPDX-License-Identifier: GPL-2.0
=====================================
Intel Trust Domain Extensions (TDX)
=====================================
Intel's Trust Domain Extensions (TDX) protect confidential guest VMs from
the host and physical attacks by isolating the guest register state and by
encrypting the guest memory. In TDX, a special module running in a special
mode sits between the host and the guest and manages the guest/host
separation.
TDX Host Kernel Support
=======================
TDX introduces a new CPU mode called Secure Arbitration Mode (SEAM) and
a new isolated range pointed by the SEAM Ranger Register (SEAMRR). A
CPU-attested software module called 'the TDX module' runs inside the new
isolated range to provide the functionalities to manage and run protected
VMs.
TDX also leverages Intel Multi-Key Total Memory Encryption (MKTME) to
provide crypto-protection to the VMs. TDX reserves part of MKTME KeyIDs
as TDX private KeyIDs, which are only accessible within the SEAM mode.
BIOS is responsible for partitioning legacy MKTME KeyIDs and TDX KeyIDs.
Before the TDX module can be used to create and run protected VMs, it
must be loaded into the isolated range and properly initialized. The TDX
architecture doesn't require the BIOS to load the TDX module, but the
kernel assumes it is loaded by the BIOS.
TDX boot-time detection
-----------------------
The kernel detects TDX by detecting TDX private KeyIDs during kernel
boot. Below dmesg shows when TDX is enabled by BIOS::
[..] virt/tdx: BIOS enabled: private KeyID range: [16, 64)
TDX module initialization
---------------------------------------
The kernel talks to the TDX module via the new SEAMCALL instruction. The
TDX module implements SEAMCALL leaf functions to allow the kernel to
initialize it.
If the TDX module isn't loaded, the SEAMCALL instruction fails with a
special error. In this case the kernel fails the module initialization
and reports the module isn't loaded::
[..] virt/tdx: module not loaded
Initializing the TDX module consumes roughly ~1/256th system RAM size to
use it as 'metadata' for the TDX memory. It also takes additional CPU
time to initialize those metadata along with the TDX module itself. Both
are not trivial. The kernel initializes the TDX module at runtime on
demand.
Besides initializing the TDX module, a per-cpu initialization SEAMCALL
must be done on one cpu before any other SEAMCALLs can be made on that
cpu.
The kernel provides two functions, tdx_enable() and tdx_cpu_enable() to
allow the user of TDX to enable the TDX module and enable TDX on local
cpu respectively.
Making SEAMCALL requires VMXON has been done on that CPU. Currently only
KVM implements VMXON. For now both tdx_enable() and tdx_cpu_enable()
don't do VMXON internally (not trivial), but depends on the caller to
guarantee that.
To enable TDX, the caller of TDX should: 1) temporarily disable CPU
hotplug; 2) do VMXON and tdx_enable_cpu() on all online cpus; 3) call
tdx_enable(). For example::
cpus_read_lock();
on_each_cpu(vmxon_and_tdx_cpu_enable());
ret = tdx_enable();
cpus_read_unlock();
if (ret)
goto no_tdx;
// TDX is ready to use
And the caller of TDX must guarantee the tdx_cpu_enable() has been
successfully done on any cpu before it wants to run any other SEAMCALL.
A typical usage is do both VMXON and tdx_cpu_enable() in CPU hotplug
online callback, and refuse to online if tdx_cpu_enable() fails.
User can consult dmesg to see whether the TDX module has been initialized.
If the TDX module is initialized successfully, dmesg shows something
like below::
[..] virt/tdx: 262668 KBs allocated for PAMT
[..] virt/tdx: module initialized
If the TDX module failed to initialize, dmesg also shows it failed to
initialize::
[..] virt/tdx: module initialization failed ...
TDX Interaction to Other Kernel Components
------------------------------------------
TDX Memory Policy
~~~~~~~~~~~~~~~~~
TDX reports a list of "Convertible Memory Region" (CMR) to tell the
kernel which memory is TDX compatible. The kernel needs to build a list
of memory regions (out of CMRs) as "TDX-usable" memory and pass those
regions to the TDX module. Once this is done, those "TDX-usable" memory
regions are fixed during module's lifetime.
To keep things simple, currently the kernel simply guarantees all pages
in the page allocator are TDX memory. Specifically, the kernel uses all
system memory in the core-mm "at the time of TDX module initialization"
as TDX memory, and in the meantime, refuses to online any non-TDX-memory
in the memory hotplug.
Physical Memory Hotplug
~~~~~~~~~~~~~~~~~~~~~~~
Note TDX assumes convertible memory is always physically present during
machine's runtime. A non-buggy BIOS should never support hot-removal of
any convertible memory. This implementation doesn't handle ACPI memory
removal but depends on the BIOS to behave correctly.
CPU Hotplug
~~~~~~~~~~~
TDX module requires the per-cpu initialization SEAMCALL must be done on
one cpu before any other SEAMCALLs can be made on that cpu. The kernel
provides tdx_cpu_enable() to let the user of TDX to do it when the user
wants to use a new cpu for TDX task.
TDX doesn't support physical (ACPI) CPU hotplug. During machine boot,
TDX verifies all boot-time present logical CPUs are TDX compatible before
enabling TDX. A non-buggy BIOS should never support hot-add/removal of
physical CPU. Currently the kernel doesn't handle physical CPU hotplug,
but depends on the BIOS to behave correctly.
Note TDX works with CPU logical online/offline, thus the kernel still
allows to offline logical CPU and online it again.
Erratum
~~~~~~~
The first few generations of TDX hardware have an erratum. A partial
write to a TDX private memory cacheline will silently "poison" the
line. Subsequent reads will consume the poison and generate a machine
check.
A partial write is a memory write where a write transaction of less than
cacheline lands at the memory controller. The CPU does these via
non-temporal write instructions (like MOVNTI), or through UC/WC memory
mappings. Devices can also do partial writes via DMA.
Theoretically, a kernel bug could do partial write to TDX private memory
and trigger unexpected machine check. What's more, the machine check
code will present these as "Hardware error" when they were, in fact, a
software-triggered issue. But in the end, this issue is hard to trigger.
If the platform has such erratum, the kernel prints additional message in
machine check handler to tell user the machine check may be caused by
kernel bug on TDX private memory.
Kexec
~~~~~~~
Currently kexec doesn't work on the TDX platforms with the aforementioned
erratum. It fails when loading the kexec kernel image. Otherwise it
works normally.
Interaction vs S3 and deeper states
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
TDX cannot survive from S3 and deeper states. The hardware resets and
disables TDX completely when platform goes to S3 and deeper. Both TDX
guests and the TDX module get destroyed permanently.
The kernel uses S3 for suspend-to-ram, and use S4 and deeper states for
hibernation. Currently, for simplicity, the kernel chooses to make TDX
mutually exclusive with S3 and hibernation.
The kernel disables TDX during early boot when hibernation support is
available::
[..] virt/tdx: initialization failed: Hibernation support is enabled
Add 'nohibernate' kernel command line to disable hibernation in order to
use TDX.
ACPI S3 is disabled during kernel early boot if TDX is enabled. The user
needs to turn off TDX in the BIOS in order to use S3.
TDX Guest Support
=================
Since the host cannot directly access guest registers or memory, much
normal functionality of a hypervisor must be moved into the guest. This is
implemented using a Virtualization Exception (#VE) that is handled by the
guest kernel. A #VE is handled entirely inside the guest kernel, but some
require the hypervisor to be consulted.
TDX includes new hypercall-like mechanisms for communicating from the
guest to the hypervisor or the TDX module.
New TDX Exceptions
------------------
TDX guests behave differently from bare-metal and traditional VMX guests.
In TDX guests, otherwise normal instructions or memory accesses can cause
#VE or #GP exceptions.
Instructions marked with an '*' conditionally cause exceptions. The
details for these instructions are discussed below.
Instruction-based #VE
~~~~~~~~~~~~~~~~~~~~~
- Port I/O (INS, OUTS, IN, OUT)
- HLT
- MONITOR, MWAIT
- WBINVD, INVD
- VMCALL
- RDMSR*,WRMSR*
- CPUID*
Instruction-based #GP
~~~~~~~~~~~~~~~~~~~~~
- All VMX instructions: INVEPT, INVVPID, VMCLEAR, VMFUNC, VMLAUNCH,
VMPTRLD, VMPTRST, VMREAD, VMRESUME, VMWRITE, VMXOFF, VMXON
- ENCLS, ENCLU
- GETSEC
- RSM
- ENQCMD
- RDMSR*,WRMSR*
RDMSR/WRMSR Behavior
~~~~~~~~~~~~~~~~~~~~
MSR access behavior falls into three categories:
- #GP generated
- #VE generated
- "Just works"
In general, the #GP MSRs should not be used in guests. Their use likely
indicates a bug in the guest. The guest may try to handle the #GP with a
hypercall but it is unlikely to succeed.
The #VE MSRs are typically able to be handled by the hypervisor. Guests
can make a hypercall to the hypervisor to handle the #VE.
The "just works" MSRs do not need any special guest handling. They might
be implemented by directly passing through the MSR to the hardware or by
trapping and handling in the TDX module. Other than possibly being slow,
these MSRs appear to function just as they would on bare metal.
CPUID Behavior
~~~~~~~~~~~~~~
For some CPUID leaves and sub-leaves, the virtualized bit fields of CPUID
return values (in guest EAX/EBX/ECX/EDX) are configurable by the
hypervisor. For such cases, the Intel TDX module architecture defines two
virtualization types:
- Bit fields for which the hypervisor controls the value seen by the guest
TD.
- Bit fields for which the hypervisor configures the value such that the
guest TD either sees their native value or a value of 0. For these bit
fields, the hypervisor can mask off the native values, but it can not
turn *on* values.
A #VE is generated for CPUID leaves and sub-leaves that the TDX module does
not know how to handle. The guest kernel may ask the hypervisor for the
value with a hypercall.
#VE on Memory Accesses
----------------------
There are essentially two classes of TDX memory: private and shared.
Private memory receives full TDX protections. Its content is protected
against access from the hypervisor. Shared memory is expected to be
shared between guest and hypervisor and does not receive full TDX
protections.
A TD guest is in control of whether its memory accesses are treated as
private or shared. It selects the behavior with a bit in its page table
entries. This helps ensure that a guest does not place sensitive
information in shared memory, exposing it to the untrusted hypervisor.
#VE on Shared Memory
~~~~~~~~~~~~~~~~~~~~
Access to shared mappings can cause a #VE. The hypervisor ultimately
controls whether a shared memory access causes a #VE, so the guest must be
careful to only reference shared pages it can safely handle a #VE. For
instance, the guest should be careful not to access shared memory in the
#VE handler before it reads the #VE info structure (TDG.VP.VEINFO.GET).
Shared mapping content is entirely controlled by the hypervisor. The guest
should only use shared mappings for communicating with the hypervisor.
Shared mappings must never be used for sensitive memory content like kernel
stacks. A good rule of thumb is that hypervisor-shared memory should be
treated the same as memory mapped to userspace. Both the hypervisor and
userspace are completely untrusted.
MMIO for virtual devices is implemented as shared memory. The guest must
be careful not to access device MMIO regions unless it is also prepared to
handle a #VE.
#VE on Private Pages
~~~~~~~~~~~~~~~~~~~~
An access to private mappings can also cause a #VE. Since all kernel
memory is also private memory, the kernel might theoretically need to
handle a #VE on arbitrary kernel memory accesses. This is not feasible, so
TDX guests ensure that all guest memory has been "accepted" before memory
is used by the kernel.
A modest amount of memory (typically 512M) is pre-accepted by the firmware
before the kernel runs to ensure that the kernel can start up without
being subjected to a #VE.
The hypervisor is permitted to unilaterally move accepted pages to a
"blocked" state. However, if it does this, page access will not generate a
#VE. It will, instead, cause a "TD Exit" where the hypervisor is required
to handle the exception.
Linux #VE handler
-----------------
Just like page faults or #GP's, #VE exceptions can be either handled or be
fatal. Typically, an unhandled userspace #VE results in a SIGSEGV.
An unhandled kernel #VE results in an oops.
Handling nested exceptions on x86 is typically nasty business. A #VE
could be interrupted by an NMI which triggers another #VE and hilarity
ensues. The TDX #VE architecture anticipated this scenario and includes a
feature to make it slightly less nasty.
During #VE handling, the TDX module ensures that all interrupts (including
NMIs) are blocked. The block remains in place until the guest makes a
TDG.VP.VEINFO.GET TDCALL. This allows the guest to control when interrupts
or a new #VE can be delivered.
However, the guest kernel must still be careful to avoid potential
#VE-triggering actions (discussed above) while this block is in place.
While the block is in place, any #VE is elevated to a double fault (#DF)
which is not recoverable.
MMIO handling
-------------
In non-TDX VMs, MMIO is usually implemented by giving a guest access to a
mapping which will cause a VMEXIT on access, and then the hypervisor
emulates the access. That is not possible in TDX guests because VMEXIT
will expose the register state to the host. TDX guests don't trust the host
and can't have their state exposed to the host.
In TDX, MMIO regions typically trigger a #VE exception in the guest. The
guest #VE handler then emulates the MMIO instruction inside the guest and
converts it into a controlled TDCALL to the host, rather than exposing
guest state to the host.
MMIO addresses on x86 are just special physical addresses. They can
theoretically be accessed with any instruction that accesses memory.
However, the kernel instruction decoding method is limited. It is only
designed to decode instructions like those generated by io.h macros.
MMIO access via other means (like structure overlays) may result in an
oops.
Shared Memory Conversions
-------------------------
All TDX guest memory starts out as private at boot. This memory can not
be accessed by the hypervisor. However, some kernel users like device
drivers might have a need to share data with the hypervisor. To do this,
memory must be converted between shared and private. This can be
accomplished using some existing memory encryption helpers:
* set_memory_decrypted() converts a range of pages to shared.
* set_memory_encrypted() converts memory back to private.
Device drivers are the primary user of shared memory, but there's no need
to touch every driver. DMA buffers and ioremap() do the conversions
automatically.
TDX uses SWIOTLB for most DMA allocations. The SWIOTLB buffer is
converted to shared on boot.
For coherent DMA allocation, the DMA buffer gets converted on the
allocation. Check force_dma_unencrypted() for details.
Attestation
===========
Attestation is used to verify the TDX guest trustworthiness to other
entities before provisioning secrets to the guest. For example, a key
server may want to use attestation to verify that the guest is the
desired one before releasing the encryption keys to mount the encrypted
rootfs or a secondary drive.
The TDX module records the state of the TDX guest in various stages of
the guest boot process using the build time measurement register (MRTD)
and runtime measurement registers (RTMR). Measurements related to the
guest initial configuration and firmware image are recorded in the MRTD
register. Measurements related to initial state, kernel image, firmware
image, command line options, initrd, ACPI tables, etc are recorded in
RTMR registers. For more details, as an example, please refer to TDX
Virtual Firmware design specification, section titled "TD Measurement".
At TDX guest runtime, the attestation process is used to attest to these
measurements.
The attestation process consists of two steps: TDREPORT generation and
Quote generation.
TDX guest uses TDCALL[TDG.MR.REPORT] to get the TDREPORT (TDREPORT_STRUCT)
from the TDX module. TDREPORT is a fixed-size data structure generated by
the TDX module which contains guest-specific information (such as build
and boot measurements), platform security version, and the MAC to protect
the integrity of the TDREPORT. A user-provided 64-Byte REPORTDATA is used
as input and included in the TDREPORT. Typically it can be some nonce
provided by attestation service so the TDREPORT can be verified uniquely.
More details about the TDREPORT can be found in Intel TDX Module
specification, section titled "TDG.MR.REPORT Leaf".
After getting the TDREPORT, the second step of the attestation process
is to send it to the Quoting Enclave (QE) to generate the Quote. TDREPORT
by design can only be verified on the local platform as the MAC key is
bound to the platform. To support remote verification of the TDREPORT,
TDX leverages Intel SGX Quoting Enclave to verify the TDREPORT locally
and convert it to a remotely verifiable Quote. Method of sending TDREPORT
to QE is implementation specific. Attestation software can choose
whatever communication channel available (i.e. vsock or TCP/IP) to
send the TDREPORT to QE and receive the Quote.
References
==========
TDX reference material is collected here:
https://www.intel.com/content/www/us/en/developer/articles/technical/intel-trust-domain-extensions.html
3. 한국어 전문 번역
영어 원문의 문단 순서와 의미를 유지한 전체 번역입니다. 코드, 함수명, symbol과 URL은 원문 표기를 유지합니다.
TDX host 기반 구조
1-31이 문서는 `SPDX-License-Identifier: GPL-2.0`으로 배포됩니다. Intel Trust Domain Extensions(TDX)는 guest register state를 격리하고 guest memory를 암호화해 confidential guest VM을 host와 물리 공격으로부터 보호합니다. TDX에서는 특별한 mode로 실행되는 전용 module이 host와 guest 사이에 위치해 둘의 분리를 관리합니다.
TDX는 Secure Arbitration Mode(SEAM)라는 새 CPU mode와 SEAM Ranger Register(SEAMRR)가 가리키는 격리 범위를 도입합니다. CPU가 attestation한 software인 `TDX module`은 이 격리 범위 안에서 실행되며 protected VM을 관리하고 실행하는 기능을 제공합니다.
VM의 cryptographic protection에는 Intel Multi-Key Total Memory Encryption(MKTME)도 활용합니다. TDX는 MKTME KeyID 일부를 SEAM mode에서만 access할 수 있는 TDX private KeyID로 예약하며, legacy MKTME KeyID와 TDX KeyID의 partition은 BIOS가 담당합니다.
protected VM을 만들고 실행하기 전에 TDX module을 격리 범위에 load하고 올바르게 initialize해야 합니다. TDX architecture 자체는 BIOS가 module을 load하도록 요구하지 않지만 kernel은 BIOS가 이미 load했다고 가정합니다.
boot-time TDX 감지
32-39kernel은 boot 중 TDX private KeyID를 감지해 TDX를 판별합니다. BIOS에서 TDX를 활성화하면 dmesg에 private KeyID 범위가 다음과 같이 나타납니다.
[..] virt/tdx: BIOS enabled: private KeyID range: [16, 64)
TDX module과 CPU 초기화
40-101kernel은 새 `SEAMCALL` instruction으로 TDX module과 통신합니다. TDX module은 kernel이 module을 initialize할 수 있도록 `SEAMCALL` leaf function을 구현합니다.
TDX module이 load되지 않았다면 `SEAMCALL`이 특별한 error로 실패합니다. 이때 kernel은 module initialization을 실패 처리하고 module이 load되지 않았다고 보고합니다.
[..] virt/tdx: module not loaded
TDX module initialization은 TDX memory용 metadata로 system RAM의 약 1/256을 소비합니다. TDX module 자체와 이 metadata를 initialize하는 데 추가 CPU 시간도 필요하며 둘 다 무시할 수 없는 비용입니다. 따라서 kernel은 runtime에 demand가 생길 때 TDX module을 initialize합니다.
module initialization과 별도로, 어떤 CPU에서든 다른 `SEAMCALL`을 실행하기 전에 그 CPU에서 per-CPU initialization `SEAMCALL`을 한 번 수행해야 합니다.
kernel은 TDX 사용자가 TDX module 전체와 local CPU의 TDX를 각각 활성화할 수 있도록 `tdx_enable()`과 `tdx_cpu_enable()`을 제공합니다.
`SEAMCALL`을 실행하려면 해당 CPU에서 `VMXON`이 먼저 완료되어야 합니다. 현재 `VMXON`을 구현하는 것은 KVM뿐입니다. `VMXON`을 내부에서 처리하는 일이 간단하지 않으므로 현재 `tdx_enable()`과 `tdx_cpu_enable()`은 caller가 이를 보장한다고 가정합니다.
TDX를 활성화하는 caller는 1) CPU hotplug를 잠시 비활성화하고, 2) online CPU 모두에서 `VMXON`과 `tdx_enable_cpu()`를 수행한 뒤, 3) `tdx_enable()`을 호출해야 합니다. 예시는 다음과 같습니다.
cpus_read_lock();
on_each_cpu(vmxon_and_tdx_cpu_enable());
ret = tdx_enable();
cpus_read_unlock();
if (ret)
goto no_tdx;
// TDX is ready to use
TDX caller는 어느 CPU에서든 다른 `SEAMCALL`을 실행하기 전에 `tdx_cpu_enable()`이 성공했음을 반드시 보장해야 합니다. 일반적인 사용법은 CPU hotplug online callback에서 `VMXON`과 `tdx_cpu_enable()`을 모두 실행하고, `tdx_cpu_enable()`이 실패한 CPU의 online 전환을 거부하는 것입니다.
사용자는 dmesg에서 TDX module initialization 여부를 확인할 수 있습니다. 성공하면 PAMT에 allocate한 크기와 initialization 완료가 다음처럼 표시됩니다.
[..] virt/tdx: 262668 KBs allocated for PAMT
[..] virt/tdx: module initialized
TDX module initialization이 실패한 경우에도 dmesg가 실패를 알립니다.
[..] virt/tdx: module initialization failed ...
memory policy와 physical·logical hotplug
102-143TDX는 `Convertible Memory Region`(CMR) 목록으로 TDX와 호환되는 memory를 kernel에 알립니다. kernel은 CMR 안에서 `TDX-usable` memory region 목록을 구성해 TDX module에 전달해야 합니다. 한번 설정한 `TDX-usable` region은 module의 lifetime 동안 고정됩니다.
현재 kernel은 구현을 단순하게 유지하기 위해 page allocator의 모든 page가 TDX memory임을 보장합니다. 구체적으로 TDX module initialization 시점에 core-mm에 있는 system memory 전부를 TDX memory로 사용하며, memory hotplug에서 non-TDX memory를 online하는 것은 거부합니다.
TDX는 convertible memory가 machine runtime 내내 물리적으로 존재한다고 가정합니다. 정상적인 BIOS라면 어떤 convertible memory도 hot-remove하도록 지원해서는 안 됩니다. 현재 구현은 ACPI memory removal을 직접 처리하지 않고 BIOS가 올바르게 동작한다고 가정합니다.
TDX module은 각 CPU에서 다른 `SEAMCALL`을 실행하기 전에 per-CPU initialization `SEAMCALL`이 완료되어야 한다고 요구합니다. kernel은 TDX 사용자가 새 CPU를 TDX 작업에 쓰려 할 때 이를 수행할 수 있도록 `tdx_cpu_enable()`을 제공합니다.
TDX는 physical ACPI CPU hotplug를 지원하지 않습니다. machine boot 중 TDX를 활성화하기 전에 boot 시점에 존재하는 logical CPU가 모두 TDX-compatible인지 검증합니다. 정상적인 BIOS는 physical CPU hot-add 또는 hot-removal을 지원해서는 안 됩니다. 현재 kernel은 physical CPU hotplug를 처리하지 않고 BIOS의 올바른 동작에 의존합니다.
반면 TDX는 logical CPU의 online/offline과 함께 동작하므로 kernel은 logical CPU를 offline했다가 다시 online하는 것을 허용합니다.
erratum, kexec와 전원 상태
144-195초기 몇 세대 TDX hardware에는 erratum이 있습니다. TDX private memory cacheline에 partial write를 하면 해당 line이 조용히 `poison`되고, 이후 read가 poison을 소비하면서 machine check가 발생합니다.
partial write란 cacheline보다 작은 write transaction이 memory controller에 도달하는 memory write입니다. CPU는 `MOVNTI` 같은 non-temporal write instruction이나 UC/WC memory mapping으로 이를 만들 수 있으며, device도 DMA를 통해 partial write를 수행할 수 있습니다.
이론적으로 kernel bug가 TDX private memory에 partial write를 수행해 예상하지 못한 machine check를 일으킬 수 있습니다. 더구나 실제로는 software가 촉발한 문제인데도 machine-check code는 이를 `Hardware error`로 표시합니다. 다만 이 문제는 실제로 촉발하기 어렵습니다.
platform에 이 erratum이 있다면 kernel은 machine-check handler에 추가 message를 출력해, TDX private memory에 대한 kernel bug가 machine check의 원인일 수 있음을 사용자에게 알립니다.
앞서 설명한 erratum이 있는 TDX platform에서는 현재 `kexec`가 동작하지 않으며 kexec kernel image를 load하는 단계에서 실패합니다. 이 erratum이 없으면 정상적으로 동작합니다.
TDX는 S3 이하의 더 깊은 power state를 통과해 유지될 수 없습니다. platform이 S3 이하로 들어가면 hardware가 reset되고 TDX를 완전히 비활성화하며 TDX guest와 TDX module이 모두 영구적으로 파괴됩니다.
kernel은 suspend-to-RAM에 S3를, hibernation에 S4 이하의 state를 사용합니다. 현재 kernel은 구현을 단순화하기 위해 TDX가 S3 및 hibernation과 상호 배타적이 되도록 합니다.
hibernation support를 사용할 수 있으면 kernel은 early boot 중 TDX를 비활성화하고 다음 message를 출력합니다.
[..] virt/tdx: initialization failed: Hibernation support is enabled
TDX를 사용하려면 `nohibernate` kernel command-line option을 추가해 hibernation을 비활성화해야 합니다. TDX가 활성화된 경우 ACPI S3도 kernel early boot에서 비활성화됩니다. S3를 사용하려면 BIOS에서 TDX를 꺼야 합니다.
TDX guest 지원 모델
196-206host가 guest register나 memory에 직접 access할 수 없으므로 hypervisor의 일반 기능 상당 부분을 guest 안으로 옮겨야 합니다. guest kernel이 처리하는 Virtualization Exception(`#VE`)으로 이를 구현합니다. `#VE`는 guest kernel 안에서 전부 처리하지만 일부 경우에는 hypervisor와 상의해야 합니다.
TDX는 guest가 hypervisor 또는 TDX module과 통신할 수 있도록 hypercall과 비슷한 새 mechanism을 제공합니다.
instruction, MSR와 CPUID 예외
207-279TDX guest는 bare-metal이나 전통적인 VMX guest와 다르게 동작합니다. 일반적으로 정상인 instruction 또는 memory access도 TDX guest에서는 `#VE`나 `#GP` exception을 일으킬 수 있습니다. 별표가 붙은 instruction은 조건에 따라 exception을 일으키며 세부 동작은 아래에서 설명합니다.
다음 instruction은 instruction 기반 `#VE`를 일으킵니다.
- Port I/O: `INS`, `OUTS`, `IN`, `OUT`
- `HLT`
- `MONITOR`, `MWAIT`
- `WBINVD`, `INVD`
- `VMCALL`
- `RDMSR*`, `WRMSR*`
- `CPUID*`
다음 instruction은 instruction 기반 `#GP`를 일으킵니다.
- 모든 VMX instruction: `INVEPT`, `INVVPID`, `VMCLEAR`, `VMFUNC`, `VMLAUNCH`, `VMPTRLD`, `VMPTRST`, `VMREAD`, `VMRESUME`, `VMWRITE`, `VMXOFF`, `VMXON`
- `ENCLS`, `ENCLU`
- `GETSEC`
- `RSM`
- `ENQCMD`
- `RDMSR*`, `WRMSR*`
MSR access 동작은 다음 세 범주로 나뉩니다.
| 범주 | guest 동작 |
|---|---|
| `#GP` generated | guest에서 사용하지 않아야 합니다. 사용했다면 guest bug일 가능성이 크며 hypercall로 처리하려 해도 성공 가능성이 낮습니다. |
| `#VE` generated | 대체로 hypervisor가 처리할 수 있으므로 guest가 hypercall로 `#VE` 처리를 요청할 수 있습니다. |
| `Just works` | 특별한 guest 처리가 필요 없습니다. hardware에 MSR을 직접 pass-through하거나 TDX module이 trap해 처리할 수 있으며, 느릴 가능성을 제외하면 bare metal과 같은 방식으로 동작합니다. |
일부 CPUID leaf와 sub-leaf에서 guest `EAX/EBX/ECX/EDX`로 반환되는 virtualized bit field는 hypervisor가 구성할 수 있습니다. Intel TDX module architecture는 이런 경우 두 virtualization type을 정의합니다.
- guest TD가 보는 값을 hypervisor가 직접 제어하는 bit field.
- guest TD가 native value 또는 0만 보도록 hypervisor가 구성하는 bit field. hypervisor는 native value를 mask off할 수 있지만 값을 새로 turn on할 수는 없습니다.
TDX module이 처리 방법을 모르는 CPUID leaf와 sub-leaf에는 `#VE`가 발생합니다. guest kernel은 hypercall로 그 값을 hypervisor에 요청할 수 있습니다.
shared·private memory의 #VE
280-331TDX memory는 본질적으로 private과 shared 두 종류입니다. private memory는 TDX protection을 모두 적용받고 hypervisor의 access로부터 content를 보호합니다. shared memory는 guest와 hypervisor가 공유하기 위한 것이며 완전한 TDX protection을 받지 않습니다.
TD guest는 page-table entry의 bit 하나로 memory access를 private 또는 shared로 취급할지 제어합니다. 이 방식은 guest가 sensitive information을 shared memory에 두어 untrusted hypervisor에 노출하지 않게 돕습니다.
shared mapping에 access하면 `#VE`가 발생할 수 있습니다. shared-memory access가 `#VE`를 일으킬지는 최종적으로 hypervisor가 제어하므로 guest는 `#VE`를 안전하게 처리할 수 있는 shared page만 참조해야 합니다. 예를 들어 `#VE` handler가 `TDG.VP.VEINFO.GET`으로 `#VE` info structure를 읽기 전에는 shared memory에 access하지 않도록 주의해야 합니다.
shared mapping content는 hypervisor가 완전히 제어합니다. guest는 hypervisor와 통신할 때만 shared mapping을 사용해야 하며 kernel stack 같은 sensitive memory를 절대 두어서는 안 됩니다. hypervisor-shared memory는 userspace에 mapping한 memory와 똑같이 취급하는 것이 좋은 원칙입니다. hypervisor와 userspace 모두 완전히 신뢰할 수 없습니다.
virtual device의 MMIO는 shared memory로 구현합니다. guest는 `#VE`를 처리할 준비가 된 경우에만 device MMIO region에 access해야 합니다.
private mapping access도 `#VE`를 일으킬 수 있습니다. 모든 kernel memory가 private memory이므로 이론적으로 kernel은 임의의 kernel-memory access에서 `#VE`를 처리해야 할 수도 있습니다. 이는 현실적으로 불가능하므로 TDX guest는 kernel이 memory를 사용하기 전에 모든 guest memory가 `accepted` 상태임을 보장합니다.
kernel이 `#VE` 없이 boot를 시작할 수 있도록 firmware가 kernel 실행 전에 보통 512M 정도의 memory를 미리 accept합니다.
hypervisor는 accepted page를 일방적으로 `blocked` state로 옮길 수 있습니다. 다만 이렇게 하면 page access가 `#VE`를 일으키지 않고 `TD Exit`을 발생시키며, hypervisor가 그 exception을 처리해야 합니다.
Linux #VE handler와 MMIO
332-375page fault나 `#GP`와 마찬가지로 `#VE` exception은 처리할 수도 있고 fatal일 수도 있습니다. 일반적으로 처리하지 못한 userspace `#VE`는 `SIGSEGV`가 되고, 처리하지 못한 kernel `#VE`는 oops가 됩니다.
x86에서 nested exception 처리는 매우 까다롭습니다. `#VE` 처리 도중 NMI가 끼어들어 또 다른 `#VE`를 일으킬 수 있습니다. TDX `#VE` architecture는 이 상황을 조금 덜 위험하게 만드는 기능을 포함합니다.
`#VE`를 처리하는 동안 TDX module은 NMI를 포함한 모든 interrupt를 block합니다. 이 block은 guest가 `TDG.VP.VEINFO.GET` `TDCALL`을 실행할 때까지 유지되며, guest가 interrupt 또는 새 `#VE`를 언제 전달받을지 제어할 수 있게 합니다.
block이 유지되는 동안 guest kernel은 앞서 설명한 `#VE` 유발 가능 동작을 피해야 합니다. 이 상태에서 발생하는 모든 `#VE`는 복구할 수 없는 double fault(`#DF`)로 승격됩니다.
non-TDX VM의 MMIO는 보통 access 시 `VMEXIT`을 일으키는 mapping을 guest에 제공하고 hypervisor가 access를 emulate하는 방식으로 구현합니다. 그러나 `VMEXIT`은 register state를 host에 노출하므로 host를 신뢰하지 않는 TDX guest에서는 이 방식을 쓸 수 없습니다.
TDX의 MMIO region은 일반적으로 guest 안에서 `#VE` exception을 발생시킵니다. guest `#VE` handler가 guest 내부에서 MMIO instruction을 emulate한 뒤 guest state를 host에 노출하는 대신 통제된 `TDCALL`로 변환해 host에 보냅니다.
x86의 MMIO address는 특별한 physical address일 뿐이므로 이론적으로 memory에 access하는 어떤 instruction으로도 접근할 수 있습니다. 하지만 kernel instruction decoder는 제한적이며 `io.h` macro가 생성하는 형태의 instruction만 decode하도록 설계되었습니다.
structure overlay 같은 다른 방식으로 MMIO에 access하면 oops가 발생할 수 있습니다.
shared/private memory 변환
376-397TDX guest memory는 boot 시 모두 private 상태로 시작하므로 hypervisor가 access할 수 없습니다. 그러나 device driver 같은 일부 kernel user는 hypervisor와 data를 공유해야 하므로 memory를 shared와 private 사이에서 변환해야 합니다. 기존 memory-encryption helper로 이를 수행할 수 있습니다.
- `set_memory_decrypted()`는 page range를 shared로 변환합니다.
- `set_memory_encrypted()`는 memory를 다시 private으로 변환합니다.
shared memory의 주 사용자는 device driver이지만 모든 driver를 수정할 필요는 없습니다. DMA buffer와 `ioremap()`이 변환을 자동으로 수행합니다.
TDX는 대부분의 DMA allocation에 SWIOTLB를 사용하며 SWIOTLB buffer는 boot 때 shared로 변환됩니다.
coherent DMA allocation에서는 allocation 시 DMA buffer를 변환합니다. 자세한 내용은 `force_dma_unencrypted()`를 확인하십시오.
TDREPORT와 Quote attestation
398-440attestation은 guest에 secret을 provision하기 전에 다른 entity가 TDX guest의 trustworthiness를 검증하는 데 사용합니다. 예를 들어 key server는 encrypted rootfs나 secondary drive를 mount할 encryption key를 내주기 전에 attestation으로 원하는 guest가 맞는지 확인할 수 있습니다.
TDX module은 guest boot process의 여러 단계에서 build-time measurement register(MRTD)와 runtime measurement register(RTMR)를 사용해 TDX guest state를 기록합니다. guest initial configuration과 firmware image 관련 measurement는 MRTD에 기록합니다. initial state, kernel image, firmware image, command-line option, initrd, ACPI table 등의 measurement는 RTMR에 기록합니다.
자세한 예시는 TDX Virtual Firmware design specification의 `TD Measurement` section을 참고하십시오. TDX guest runtime에는 attestation process로 이러한 measurement를 attest합니다.
attestation process는 `TDREPORT` generation과 `Quote` generation의 두 단계로 구성됩니다.
| 단계 | 입력과 검증 범위 | 결과 |
|---|---|---|
| `TDCALL[TDG.MR.REPORT]` | 사용자가 제공한 64-byte `REPORTDATA`, guest build·boot measurement와 platform security version | platform-bound MAC으로 integrity를 보호하는 fixed-size `TDREPORT_STRUCT` |
| Quoting Enclave(QE) | local platform에서만 MAC을 검증할 수 있는 `TDREPORT` | Intel SGX QE가 local 검증 후 변환한 remotely verifiable `Quote` |
TDX guest는 `TDCALL[TDG.MR.REPORT]`로 TDX module에서 `TDREPORT`(`TDREPORT_STRUCT`)를 받습니다. `TDREPORT`는 TDX module이 만드는 fixed-size data structure이며 build와 boot measurement 같은 guest-specific information, platform security version, integrity 보호용 MAC을 포함합니다.
사용자가 입력한 64-byte `REPORTDATA`도 `TDREPORT`에 포함됩니다. 일반적으로 attestation service가 제공한 nonce를 넣어 `TDREPORT`를 고유하게 검증할 수 있게 합니다. 세부 내용은 Intel TDX Module specification의 `TDG.MR.REPORT Leaf` section에 있습니다.
`TDREPORT`를 받은 뒤 두 번째 단계는 Quoting Enclave(QE)에 보내 `Quote`를 생성하는 것입니다. MAC key가 platform에 bind되어 있어 `TDREPORT`는 설계상 local platform에서만 검증할 수 있습니다.
remote verification을 지원하기 위해 TDX는 Intel SGX Quoting Enclave로 `TDREPORT`를 local에서 검증하고 remotely verifiable `Quote`로 변환합니다. `TDREPORT`를 QE에 보내는 방법은 implementation-specific입니다. attestation software는 `vsock`이나 `TCP/IP` 등 사용할 수 있는 어떤 communication channel로도 QE에 `TDREPORT`를 보내고 `Quote`를 받을 수 있습니다.
참고 자료
441-446TDX reference material은 다음 Intel 페이지에 모여 있습니다.
https://www.intel.com/content/www/us/en/developer/articles/technical/intel-trust-domain-extensions.html
요약과 해설
tdx.rst:1-446TDX는 SEAM 안의 CPU-attested TDX module과 MKTME private KeyID로 guest register state와 memory를 host에서 격리합니다. host kernel은 VMXON, per-CPU `tdx_cpu_enable()`, module-wide `tdx_enable()` 순서를 지키고 TDX-usable memory, hotplug, erratum과 power-state 제약을 관리합니다.
guest에서는 host에 register state를 노출하지 않도록 `#VE` handler와 `TDCALL`이 CPUID, MSR, MMIO를 중재합니다. private/shared memory의 trust boundary와 accept 절차를 지켜야 하며, attestation은 MRTD·RTMR measurement를 담은 `TDREPORT`를 SGX Quoting Enclave가 원격 검증 가능한 `Quote`로 바꾸는 두 단계로 진행됩니다.