* [PATCH v3 1/9] x86/virt/tdx: Add SEAMCALL wrappers for IOMMU setup/clear
2026-11-12 4:30 [PATCH v3 0/9] coco/tdx-host: Introduce trusted DMA initialization support Lu Baolu
@ 2026-11-12 4:30 ` Lu Baolu
2026-11-12 4:30 ` [PATCH v3 2/9] x86/virt/tdx: Read global metadata for trusted IOMMU Lu Baolu
` (7 subsequent siblings)
8 siblings, 0 replies; 10+ messages in thread
From: Lu Baolu @ 2026-11-12 4:30 UTC (permalink / raw)
To: iommu, x86, linux-coco, kvm
Cc: Jason Gunthorpe, Kevin Tian, Kiryl Shutsemau, Rick Edgecombe,
yilun.xu, xiaoyao.li, Chao Gao, Binbin Wu, linux-kernel,
Lu Baolu
Intel VT-d hardware gains a second set of DMA translation structures,
managed by the TDX module and used only for devices assigned to TDs.
Each IOMMU has a trusted root-table address register and a trusted
invalidation queue, both accessible only from SEAM. The existing
untrusted DMA translation path of the IOMMU stays under host control.
Before trusted DMA can be used through an IOMMU, the TDX module must
initialize these resources. TDH.IOMMU.SETUP does this: it puts the
IOMMU into Secure TDX Mode, programs the trusted root table and the
trusted invalidation queue, and protects the related I/O resources
from further untrusted host access. The host provides the memory for
this through a parameter page that lists the invalidation queue
buffers and the metadata pages the TDX module needs for the IOMMU.
TDH.IOMMU.CLEAR does the reverse. It tears down the Secure TDX Mode
state of the IOMMU, after which the host can reclaim the memory.
Add wrappers for both SEAMCALLs and export them to the tdx-host driver,
which allocates the memory and issues them.
TDH.IOMMU.SETUP and TDH.IOMMU.CLEAR are defined in the Intel TDX
Connect ABI Specification [1].
[1] https://cdrdv2.intel.com/v1/dl/getContent/858625
Signed-off-by: Lu Baolu <baolu.lu@linux.intel.com>
---
arch/x86/include/asm/tdx.h | 14 ++++++++++
arch/x86/virt/vmx/tdx/tdx.h | 2 ++
arch/x86/virt/vmx/tdx/tdx.c | 53 +++++++++++++++++++++++++++++++++++++
3 files changed, 69 insertions(+)
diff --git a/arch/x86/include/asm/tdx.h b/arch/x86/include/asm/tdx.h
index 89e7628796b0..d077990802b9 100644
--- a/arch/x86/include/asm/tdx.h
+++ b/arch/x86/include/asm/tdx.h
@@ -162,6 +162,17 @@ struct tdx_td {
int tdcx_nr_pages;
};
+#define TDX_IOMMU_MT_IQ_NR 2
+#define TDX_IOMMU_MT_MAX_PAGES (PAGE_SIZE / sizeof(u64) - TDX_IOMMU_MT_IQ_NR)
+
+/* IOMMU_MT parameter page for TDH.IOMMU.SETUP (ABI spec Table 3.35). */
+struct tdx_iommu_mt_param {
+ u64 iq[TDX_IOMMU_MT_IQ_NR]; /* HPA | number of 4K pages */
+ u64 mt[TDX_IOMMU_MT_MAX_PAGES]; /* HPA of each metadata page */
+};
+
+static_assert(sizeof(struct tdx_iommu_mt_param) == PAGE_SIZE);
+
struct tdx_vp {
/* TDVP root page */
struct page *tdvpr_page;
@@ -202,6 +213,9 @@ u64 tdh_mem_page_remove(struct tdx_td *td, u64 gpa, enum pg_level level, u64 *ex
u64 tdh_phymem_cache_wb(bool resume);
u64 tdh_phymem_page_wbinvd_tdr(struct tdx_td *td);
u64 tdh_phymem_page_wbinvd_hkid(u64 hkid, kvm_pfn_t pfn);
+u64 tdh_iommu_setup(u64 reg_base, struct tdx_iommu_mt_param *param,
+ u64 *tdx_iommu_id);
+u64 tdh_iommu_clear(u64 tdx_iommu_id);
#else
static inline void tdx_init(void) { }
static inline u32 tdx_get_nr_guest_keyids(void) { return 0; }
diff --git a/arch/x86/virt/vmx/tdx/tdx.h b/arch/x86/virt/vmx/tdx/tdx.h
index cac0a5abbff1..693adda89311 100644
--- a/arch/x86/virt/vmx/tdx/tdx.h
+++ b/arch/x86/virt/vmx/tdx/tdx.h
@@ -53,6 +53,8 @@
#define TDH_EXT_INIT 60
#define TDH_EXT_MEM_ADD 61
#define TDH_SYS_DISABLE 69
+#define TDH_IOMMU_SETUP 128
+#define TDH_IOMMU_CLEAR 129
/* TDX page types */
#define PT_NDA 0x0
diff --git a/arch/x86/virt/vmx/tdx/tdx.c b/arch/x86/virt/vmx/tdx/tdx.c
index 4cce77b6a31f..a1f35188a272 100644
--- a/arch/x86/virt/vmx/tdx/tdx.c
+++ b/arch/x86/virt/vmx/tdx/tdx.c
@@ -2567,3 +2567,56 @@ void tdx_sys_disable(void)
if (ret && (ret & TDX_SW_ERROR) != TDX_SW_ERROR)
pr_err("TDH.SYS.DISABLE failed: 0x%016llx\n", ret);
}
+
+/* No cache flush is needed; hardware keeps caches coherent across KeyIDs. */
+u64 tdh_iommu_setup(u64 reg_base, struct tdx_iommu_mt_param *param,
+ u64 *tdx_iommu_id)
+{
+ struct tdx_module_args args = {
+ .rcx = reg_base,
+ .rdx = __pa(param),
+ };
+ u64 ret;
+
+ /*
+ * Retry only on TDX_INTERRUPTED_RESUMABLE:
+ *
+ * - TDX_INTERRUPTED_RESUMABLE guarantees forward progress between
+ * calls.
+ * - On TDX_INTERRUPTED_RESUMABLE, all registers except RAX remain
+ * unchanged.
+ *
+ * The ABI spec also allows retrying on TDX_OPERAND_BUSY, but that
+ * status doesn't guarantee forward progress, so retrying could spin
+ * forever. The host serializes all TDH.IOMMU.SETUP and
+ * TDH.IOMMU.CLEAR calls for an IOMMU, so the TDX module should never
+ * report the operand as busy.
+ */
+ do {
+ ret = seamcall_ret(TDH_IOMMU_SETUP, &args);
+ } while (ret == TDX_INTERRUPTED_RESUMABLE);
+
+ *tdx_iommu_id = args.rcx;
+
+ return ret;
+}
+EXPORT_SYMBOL_FOR_MODULES(tdh_iommu_setup, "tdx-host");
+
+u64 tdh_iommu_clear(u64 tdx_iommu_id)
+{
+ struct tdx_module_args args = {
+ .rcx = tdx_iommu_id,
+ };
+ u64 ret;
+
+ /*
+ * Don't retry on TDX_OPERAND_BUSY. See the comment in
+ * tdh_iommu_setup().
+ */
+ do {
+ ret = seamcall(TDH_IOMMU_CLEAR, &args);
+ } while (ret == TDX_INTERRUPTED_RESUMABLE);
+
+ return ret;
+}
+EXPORT_SYMBOL_FOR_MODULES(tdh_iommu_clear, "tdx-host");
--
2.43.0
^ permalink raw reply [flat|nested] 10+ messages in thread* [PATCH v3 2/9] x86/virt/tdx: Read global metadata for trusted IOMMU
2026-11-12 4:30 [PATCH v3 0/9] coco/tdx-host: Introduce trusted DMA initialization support Lu Baolu
2026-11-12 4:30 ` [PATCH v3 1/9] x86/virt/tdx: Add SEAMCALL wrappers for IOMMU setup/clear Lu Baolu
@ 2026-11-12 4:30 ` Lu Baolu
2026-11-12 4:30 ` [PATCH v3 3/9] x86/virt/tdx: Export Dynamic PAMT helpers to tdx-host Lu Baolu
` (6 subsequent siblings)
8 siblings, 0 replies; 10+ messages in thread
From: Lu Baolu @ 2026-11-12 4:30 UTC (permalink / raw)
To: iommu, x86, linux-coco, kvm
Cc: Jason Gunthorpe, Kevin Tian, Kiryl Shutsemau, Rick Edgecombe,
yilun.xu, xiaoyao.li, Chao Gao, Binbin Wu, linux-kernel,
Lu Baolu
Add support for reading the global metadata of the trusted IOMMU.
The trusted IOMMU feature is optionally enumerated via TDX_FEATURES0.
Check this feature bit before reading the metadata to avoid causing
the entire TDX initialization to fail on platforms that do not support
it.
The read value represents the number of pages required for the IOMMU
metadata. The host will allocate this count of pages and provide them to
the TDX module when it configures the IOMMU to operate in Secure TDX mode
via the TDH.IOMMU.SETUP SEAMCALL.
Signed-off-by: Lu Baolu <baolu.lu@linux.intel.com>
---
arch/x86/include/asm/tdx.h | 1 +
arch/x86/include/asm/tdx_global_metadata.h | 5 +++++
arch/x86/virt/vmx/tdx/tdx_global_metadata.c | 20 ++++++++++++++++++++
3 files changed, 26 insertions(+)
diff --git a/arch/x86/include/asm/tdx.h b/arch/x86/include/asm/tdx.h
index d077990802b9..0ed0062c9b0a 100644
--- a/arch/x86/include/asm/tdx.h
+++ b/arch/x86/include/asm/tdx.h
@@ -35,6 +35,7 @@
/* Bit definitions of TDX_FEATURES0 metadata field */
#define TDX_FEATURES0_TD_PRESERVING BIT_ULL(1)
+#define TDX_FEATURES0_TDXCONNECT BIT_ULL(6)
#define TDX_FEATURES0_NO_RBP_MOD BIT_ULL(18)
#define TDX_FEATURES0_DYNAMIC_PAMT BIT_ULL(36)
#define TDX_FEATURES0_EXT BIT_ULL(39)
diff --git a/arch/x86/include/asm/tdx_global_metadata.h b/arch/x86/include/asm/tdx_global_metadata.h
index f18e63b671c2..e5e54159ad36 100644
--- a/arch/x86/include/asm/tdx_global_metadata.h
+++ b/arch/x86/include/asm/tdx_global_metadata.h
@@ -51,12 +51,17 @@ struct tdx_sys_info_ext {
u32 required_pages;
};
+struct tdx_sys_info_connect {
+ u16 iommu_mt_page_count;
+};
+
struct tdx_sys_info {
struct tdx_sys_info_version version;
struct tdx_sys_info_features features;
struct tdx_sys_info_tdmr tdmr;
struct tdx_sys_info_td_ctrl td_ctrl;
struct tdx_sys_info_td_conf td_conf;
+ struct tdx_sys_info_connect tdx_connect;
};
#endif
diff --git a/arch/x86/virt/vmx/tdx/tdx_global_metadata.c b/arch/x86/virt/vmx/tdx/tdx_global_metadata.c
index 9ab4d021017f..9b46f34cb469 100644
--- a/arch/x86/virt/vmx/tdx/tdx_global_metadata.c
+++ b/arch/x86/virt/vmx/tdx/tdx_global_metadata.c
@@ -139,6 +139,20 @@ static __init int get_tdx_sys_info_ext(struct tdx_sys_info_ext *sysinfo_ext)
return 0;
}
+static __init int get_tdx_sys_info_connect(struct tdx_sys_info_connect *sysinfo_connect)
+{
+ int ret;
+ u64 val;
+
+ ret = read_sys_metadata_field(0x3000000100000003, &val);
+ if (ret)
+ return ret;
+
+ sysinfo_connect->iommu_mt_page_count = val;
+
+ return 0;
+}
+
static __init int get_tdx_sys_info(struct tdx_sys_info *sysinfo)
{
int ret = 0;
@@ -164,5 +178,11 @@ static __init int get_tdx_sys_info(struct tdx_sys_info *sysinfo)
if (!ret && tdx_supports_dynamic_pamt(sysinfo))
ret = get_tdx_sys_info_tdmr_dpamt(&sysinfo->tdmr);
+ if (ret)
+ return ret;
+
+ if (sysinfo->features.tdx_features0 & TDX_FEATURES0_TDXCONNECT)
+ ret = get_tdx_sys_info_connect(&sysinfo->tdx_connect);
+
return ret;
}
--
2.43.0
^ permalink raw reply [flat|nested] 10+ messages in thread* [PATCH v3 3/9] x86/virt/tdx: Export Dynamic PAMT helpers to tdx-host
2026-11-12 4:30 [PATCH v3 0/9] coco/tdx-host: Introduce trusted DMA initialization support Lu Baolu
2026-11-12 4:30 ` [PATCH v3 1/9] x86/virt/tdx: Add SEAMCALL wrappers for IOMMU setup/clear Lu Baolu
2026-11-12 4:30 ` [PATCH v3 2/9] x86/virt/tdx: Read global metadata for trusted IOMMU Lu Baolu
@ 2026-11-12 4:30 ` Lu Baolu
2026-11-12 4:30 ` [PATCH v3 4/9] iommu/vt-d: Add interface for tdx-host when enabling trusted DMA Lu Baolu
` (5 subsequent siblings)
8 siblings, 0 replies; 10+ messages in thread
From: Lu Baolu @ 2026-11-12 4:30 UTC (permalink / raw)
To: iommu, x86, linux-coco, kvm
Cc: Jason Gunthorpe, Kevin Tian, Kiryl Shutsemau, Rick Edgecombe,
yilun.xu, xiaoyao.li, Chao Gao, Binbin Wu, linux-kernel,
Lu Baolu
With Dynamic PAMT, any page handed to the TDX module needs PAMT backing
first, via tdx_pamt_get(), and must drop it with tdx_pamt_put() once the
TDX module gives the page back. Besides KVM, the tdx-host driver now
hands pages to the TDX module: the IOMMU_MT pages for TDH.IOMMU.SETUP.
Export the two helpers to tdx-host as well. A symbol can only be
exported once, so replace EXPORT_SYMBOL_FOR_KVM() with an explicit list.
Only kvm-intel calls these helpers in KVM.
Signed-off-by: Lu Baolu <baolu.lu@linux.intel.com>
---
arch/x86/virt/vmx/tdx/tdx.c | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/arch/x86/virt/vmx/tdx/tdx.c b/arch/x86/virt/vmx/tdx/tdx.c
index a1f35188a272..3b9cfab6cf68 100644
--- a/arch/x86/virt/vmx/tdx/tdx.c
+++ b/arch/x86/virt/vmx/tdx/tdx.c
@@ -2426,7 +2426,7 @@ int tdx_pamt_get(kvm_pfn_t pfn, struct tdx_pamt_cache *cache)
return ret;
}
-EXPORT_SYMBOL_FOR_KVM(tdx_pamt_get);
+EXPORT_SYMBOL_FOR_MODULES(tdx_pamt_get, "kvm-intel,tdx-host");
/* Drop DPAMT refcount for the given pfn and free DPAMT backing if needed. */
void tdx_pamt_put(kvm_pfn_t pfn)
@@ -2470,7 +2470,7 @@ void tdx_pamt_put(kvm_pfn_t pfn)
out_unlock:
spin_unlock(&dpamt_lock);
}
-EXPORT_SYMBOL_FOR_KVM(tdx_pamt_put);
+EXPORT_SYMBOL_FOR_MODULES(tdx_pamt_put, "kvm-intel,tdx-host");
void tdx_free_pamt_cache(struct tdx_pamt_cache *cache)
{
--
2.43.0
^ permalink raw reply [flat|nested] 10+ messages in thread* [PATCH v3 4/9] iommu/vt-d: Add interface for tdx-host when enabling trusted DMA
2026-11-12 4:30 [PATCH v3 0/9] coco/tdx-host: Introduce trusted DMA initialization support Lu Baolu
` (2 preceding siblings ...)
2026-11-12 4:30 ` [PATCH v3 3/9] x86/virt/tdx: Export Dynamic PAMT helpers to tdx-host Lu Baolu
@ 2026-11-12 4:30 ` Lu Baolu
2026-11-12 4:30 ` [PATCH v3 5/9] iommu/vt-d: Block GCMD and ECMD while acquired by the TDX module Lu Baolu
` (4 subsequent siblings)
8 siblings, 0 replies; 10+ messages in thread
From: Lu Baolu @ 2026-11-12 4:30 UTC (permalink / raw)
To: iommu, x86, linux-coco, kvm
Cc: Jason Gunthorpe, Kevin Tian, Kiryl Shutsemau, Rick Edgecombe,
yilun.xu, xiaoyao.li, Chao Gao, Binbin Wu, linux-kernel,
Lu Baolu
An Intel IOMMU that supports trusted DMA (ECAP.TDXCS) provides a second
set of DMA translation resources, managed by the TDX module and used
only for devices assigned to TDs. Before they can be used, the tdx-host
driver issues TDH.IOMMU.SETUP to put the IOMMU into Secure TDX Mode, and
issues TDH.IOMMU.CLEAR to take it out again.
The VT-d driver owns the hardware state these SEAMCALLs depend on, so
it checks that state and keeps its own use of the IOMMU out of the TDX
module's way. This is cooperation, not enforcement: the TDX module
protects trusted DMA regardless, and a misbehaving host can only cause
failures.
Add a per-device interface so that an IOMMU is put into Secure TDX Mode
only when a device behind it needs trusted DMA. tdx-host tracks how many
devices use each IOMMU and calls into the VT-d driver only for the first
and the last of them:
- dmar_tdxc_get_params() looks up the DMAR unit translating the device
and returns its register base address, which is how the TDX module
identifies an IOMMU, and its NUMA node, so that tdx-host can allocate
the TDX module's metadata pages local to that IOMMU.
- dmar_tdxc_acquire() is called right before TDH.IOMMU.SETUP. It checks
the preconditions TDH.IOMMU.SETUP requires (DMA translation, queued
invalidation and scalable mode enabled) and confines the host to the
lower half of the domain ID space, since the TDX module owns the half
with the MSB set.
- dmar_tdxc_release() undoes acquire(). tdx-host calls it if
TDH.IOMMU.SETUP fails, or after TDH.IOMMU.CLEAR succeeds for the last
user. If TDH.IOMMU.CLEAR fails, tdx-host doesn't call it, so the upper
half of the domain ID space stays reserved for the TDX module.
DMAR units advertising ECAP.TDXCS are excluded from DMAR hotplug. No
platform currently supports both trusted DMA and DMAR hotplug, so that
flow can't be validated on hardware. Hot-removing such a unit is also
unsafe, because tdx-host may hold a reference to it and the TDX module
may own its trusted DMA resources. This also keeps the unit handle
returned by dmar_tdxc_get_params() valid for the lifetime of the system.
The symbols are exported only to the tdx-host module.
Signed-off-by: Lu Baolu <baolu.lu@linux.intel.com>
---
drivers/iommu/intel/iommu.h | 3 +
include/linux/dmar.h | 23 +++++
drivers/iommu/intel/dmar.c | 185 ++++++++++++++++++++++++++++++++++++
3 files changed, 211 insertions(+)
diff --git a/drivers/iommu/intel/iommu.h b/drivers/iommu/intel/iommu.h
index 5b3d234ae265..18fef91753a0 100644
--- a/drivers/iommu/intel/iommu.h
+++ b/drivers/iommu/intel/iommu.h
@@ -187,6 +187,7 @@
*/
#define ecap_pms(e) (((e) >> 51) & 0x1)
+#define ecap_tdxcs(e) (((e) >> 50) & 0x1)
#define ecap_rps(e) (((e) >> 49) & 0x1)
#define ecap_smpwc(e) (((e) >> 48) & 0x1)
#define ecap_flts(e) (((e) >> 47) & 0x1)
@@ -689,6 +690,8 @@ struct intel_iommu {
u64 ecmdcap[DMA_MAX_NUM_ECMDCAP];
u32 gcmd; /* Holds TE, EAFL. Don't need SRTP, SFL, WBF */
raw_spinlock_t register_lock; /* protect register handling */
+ /* Acquired for the TDX module. Protected by register_lock. */
+ bool tdxc_acquired;
int seq_id; /* sequence id of the iommu */
int agaw; /* agaw of this iommu */
int msagaw; /* max sagaw of this iommu */
diff --git a/include/linux/dmar.h b/include/linux/dmar.h
index 63e35df2cef4..a39ca13f13dc 100644
--- a/include/linux/dmar.h
+++ b/include/linux/dmar.h
@@ -47,9 +47,32 @@ struct dmar_drhd_unit {
u8 ignored:1; /* ignore drhd */
u8 include_all:1;
u8 gfx_dedicated:1; /* graphic dedicated */
+ struct mutex tdxc_lock; /* serializes acquire/release */
struct intel_iommu *iommu;
};
+/*
+ * struct dmar_tdxc_params - IOMMU information for driving TDH.IOMMU.*
+ * @reg_base_addr: Register base address of the IOMMU. The TDX module
+ * identifies an IOMMU by it.
+ * @node: NUMA node of the IOMMU, for allocating the TDX module's
+ * metadata pages local to it.
+ * @drhd: VT-d internal handle; the caller must not dereference it. It
+ * stays valid for the lifetime of the system because TDX
+ * Connect-capable units are never hot-removed.
+ */
+struct dmar_tdxc_params {
+ u64 reg_base_addr;
+ int node;
+ struct dmar_drhd_unit *drhd;
+};
+
+int dmar_tdxc_get_params(struct pci_dev *pdev, struct dmar_tdxc_params *params);
+int dmar_tdxc_acquire(struct pci_dev *pdev,
+ const struct dmar_tdxc_params *params);
+void dmar_tdxc_release(struct pci_dev *pdev,
+ const struct dmar_tdxc_params *params);
+
struct dmar_pci_path {
u8 bus;
u8 device;
diff --git a/drivers/iommu/intel/dmar.c b/drivers/iommu/intel/dmar.c
index 6377ba97a9ea..60d4c8ab1b4c 100644
--- a/drivers/iommu/intel/dmar.c
+++ b/drivers/iommu/intel/dmar.c
@@ -431,6 +431,7 @@ static int dmar_parse_one_drhd(struct acpi_dmar_header *header, void *arg)
/* The size of the register set is 2 ^ N 4 KB pages. */
dmaru->reg_size = 1UL << (drhd->size + 12);
dmaru->include_all = drhd->flags & 0x1; /* BIT0: INCLUDE_ALL */
+ mutex_init(&dmaru->tdxc_lock);
dmaru->devices = dmar_alloc_dev_scope((void *)(drhd + 1),
((void *)drhd) + drhd->header.length,
&dmaru->devices_cnt);
@@ -2367,6 +2368,25 @@ static int dmar_hp_release_drhd(struct acpi_dmar_header *header, void *arg)
return 0;
}
+static int dmar_hp_check_tdxc(struct acpi_dmar_header *header, void *arg)
+{
+ struct dmar_drhd_unit *dmaru;
+
+ dmaru = dmar_find_dmaru((struct acpi_dmar_hardware_unit *)header);
+ if (!dmaru || !dmaru->iommu || !ecap_tdxcs(dmaru->iommu->ecap))
+ return 0;
+
+ /*
+ * No platform currently supports both trusted DMA and DMAR hotplug,
+ * so it cannot be validated on real hardware. Removal is also unsafe:
+ * tdx-host may hold a pointer to this unit and the TDX module may own
+ * its trusted DMA resources.
+ */
+ pr_info("%s: hotplug of an IOMMU with trusted DMA support is not supported\n",
+ dmaru->iommu->name);
+ return -EOPNOTSUPP;
+}
+
static int dmar_hotplug_insert(acpi_handle handle)
{
int ret;
@@ -2396,6 +2416,11 @@ static int dmar_hotplug_insert(acpi_handle handle)
if (ret)
goto release_atsr;
+ ret = dmar_walk_dsm_resource(handle, DMAR_DSM_FUNC_DRHD,
+ &dmar_hp_check_tdxc, NULL);
+ if (ret)
+ goto release_atsr;
+
ret = dmar_walk_dsm_resource(handle, DMAR_DSM_FUNC_DRHD,
&dmar_hp_add_drhd, NULL);
if (!ret)
@@ -2422,6 +2447,11 @@ static int dmar_hotplug_remove(acpi_handle handle)
if (ret)
return ret;
+ ret = dmar_walk_dsm_resource(handle, DMAR_DSM_FUNC_DRHD,
+ &dmar_hp_check_tdxc, NULL);
+ if (ret)
+ return ret;
+
ret = dmar_walk_dsm_resource(handle, DMAR_DSM_FUNC_DRHD,
&dmar_hp_remove_drhd, NULL);
if (ret == 0) {
@@ -2518,3 +2548,158 @@ bool dmar_platform_optin(void)
return ret;
}
EXPORT_SYMBOL_GPL(dmar_platform_optin);
+
+static void dmar_tdxc_fill_params(struct dmar_drhd_unit *drhd,
+ struct dmar_tdxc_params *params)
+{
+ params->reg_base_addr = drhd->reg_base_addr;
+ params->node = drhd->iommu->node;
+ /*
+ * params->drhd stays valid because trusted DMA-capable units are
+ * never hot-removed.
+ */
+ params->drhd = drhd;
+}
+
+/*
+ * When the IOMMU runs in Secure TDX Mode, the TDX module owns the
+ * MSB-tagged half of the domain ID space. Confine the host to the
+ * lower half, failing if any upper-half DID is already in use.
+ */
+static int dmar_tdxc_reserve_did(struct intel_iommu *iommu)
+{
+ unsigned long ndoms = cap_ndoms(iommu->cap);
+
+ guard(mutex)(&iommu->did_lock);
+ if (ida_find_first_range(&iommu->domain_ida, ndoms >> 1, ndoms - 1) >= 0)
+ return -EBUSY;
+
+ iommu->max_domain_id = ndoms >> 1;
+ return 0;
+}
+
+static void dmar_tdxc_unreserve_did(struct intel_iommu *iommu)
+{
+ guard(mutex)(&iommu->did_lock);
+ iommu->max_domain_id = cap_ndoms(iommu->cap);
+}
+
+static void dmar_tdxc_set_acquired(struct intel_iommu *iommu, bool acquired)
+{
+ guard(raw_spinlock_irqsave)(&iommu->register_lock);
+ iommu->tdxc_acquired = acquired;
+}
+
+static struct dmar_drhd_unit *dmar_tdxc_find_drhd(struct pci_dev *pdev)
+{
+ struct dmar_drhd_unit *drhd;
+
+ if (!intel_iommu_enabled)
+ return ERR_PTR(-EOPNOTSUPP);
+
+ guard(rwsem_read)(&dmar_global_lock);
+
+ drhd = dmar_find_matched_drhd_unit(pdev);
+ if (!drhd || drhd->ignored || !drhd->iommu)
+ return ERR_PTR(-ENODEV);
+
+ if (!ecap_tdxcs(drhd->iommu->ecap))
+ return ERR_PTR(-EOPNOTSUPP);
+
+ return drhd;
+}
+
+/*
+ * dmar_tdxc_get_params() - Get the IOMMU information for a device
+ * @dev: PCI device that will use trusted DMA
+ * @params: Filled in on success
+ *
+ * Look up the IOMMU that translates @dev and return what tdx-host needs
+ * to issue TDH.IOMMU.SETUP and TDH.IOMMU.CLEAR on it. This only reads
+ * information and doesn't change any state. Devices behind the same
+ * IOMMU get the same @params->reg_base_addr.
+ *
+ * Return 0 on success and error code on failure.
+ */
+int dmar_tdxc_get_params(struct pci_dev *pdev, struct dmar_tdxc_params *params)
+{
+ struct dmar_drhd_unit *drhd = dmar_tdxc_find_drhd(pdev);
+
+ if (IS_ERR(drhd))
+ return PTR_ERR(drhd);
+
+ dmar_tdxc_fill_params(drhd, params);
+
+ return 0;
+}
+EXPORT_SYMBOL_FOR_MODULES(dmar_tdxc_get_params, "tdx-host");
+
+/*
+ * dmar_tdxc_acquire() - Acquire an IOMMU for TDH.IOMMU.SETUP
+ * @dev: PCI device that will use trusted DMA
+ * @params: Returned by dmar_tdxc_get_params() for @dev
+ *
+ * Call this right before issuing TDH.IOMMU.SETUP for the first user of
+ * the IOMMU. On success, the caller must issue TDH.IOMMU.SETUP and call
+ * dmar_tdxc_release() if it fails. The caller owns the per-IOMMU refcount
+ * and must serialize all calls for the same IOMMU.
+ *
+ * Return: 0 on success and error code on failure.
+ */
+int dmar_tdxc_acquire(struct pci_dev *pdev,
+ const struct dmar_tdxc_params *params)
+{
+ struct dmar_drhd_unit *drhd = params->drhd;
+ struct intel_iommu *iommu = drhd->iommu;
+ int ret;
+
+ if (WARN_ON(dmar_tdxc_find_drhd(pdev) != drhd))
+ return -EINVAL;
+
+ guard(mutex)(&drhd->tdxc_lock);
+ /* Still set if TDH.IOMMU.CLEAR failed and tdx-host was reloaded. */
+ if (iommu->tdxc_acquired)
+ return -EBUSY;
+
+ /*
+ * TDH.IOMMU.SETUP requires DMA translation, queued invalidation and
+ * scalable mode to be enabled. Catch these here so the error is
+ * -EINVAL rather than an opaque TDX_IOMMU_INVALID_STATE.
+ */
+ if (!(iommu->gcmd & DMA_GCMD_TE) || !iommu->qi || !sm_supported(iommu))
+ return -EINVAL;
+
+ ret = dmar_tdxc_reserve_did(iommu);
+ if (ret)
+ return ret;
+
+ dmar_tdxc_set_acquired(iommu, true);
+
+ return 0;
+}
+EXPORT_SYMBOL_FOR_MODULES(dmar_tdxc_acquire, "tdx-host");
+
+/*
+ * dmar_tdxc_release() - Take an IOMMU out of Secure TDX Mode
+ * @dev: PCI device whose IOMMU was acquired via dmar_tdxc_acquire()
+ * @params: Returned by dmar_tdxc_get_params() for @dev
+ *
+ * Call this if TDH.IOMMU.SETUP fails, or for the last user of the IOMMU
+ * after TDH.IOMMU.CLEAR has succeeded.
+ */
+void dmar_tdxc_release(struct pci_dev *pdev,
+ const struct dmar_tdxc_params *params)
+{
+ struct dmar_drhd_unit *drhd = params->drhd;
+
+ if (WARN_ON(dmar_tdxc_find_drhd(pdev) != drhd))
+ return;
+
+ guard(mutex)(&drhd->tdxc_lock);
+ if (WARN_ON(!drhd->iommu->tdxc_acquired))
+ return;
+
+ dmar_tdxc_set_acquired(drhd->iommu, false);
+ dmar_tdxc_unreserve_did(drhd->iommu);
+}
+EXPORT_SYMBOL_FOR_MODULES(dmar_tdxc_release, "tdx-host");
--
2.43.0
^ permalink raw reply [flat|nested] 10+ messages in thread* [PATCH v3 5/9] iommu/vt-d: Block GCMD and ECMD while acquired by the TDX module
2026-11-12 4:30 [PATCH v3 0/9] coco/tdx-host: Introduce trusted DMA initialization support Lu Baolu
` (3 preceding siblings ...)
2026-11-12 4:30 ` [PATCH v3 4/9] iommu/vt-d: Add interface for tdx-host when enabling trusted DMA Lu Baolu
@ 2026-11-12 4:30 ` Lu Baolu
2026-11-12 4:30 ` [PATCH v3 6/9] iommu/vt-d: Refuse system suspend " Lu Baolu
` (3 subsequent siblings)
8 siblings, 0 replies; 10+ messages in thread
From: Lu Baolu @ 2026-11-12 4:30 UTC (permalink / raw)
To: iommu, x86, linux-coco, kvm
Cc: Jason Gunthorpe, Kevin Tian, Kiryl Shutsemau, Rick Edgecombe,
yilun.xu, xiaoyao.li, Chao Gao, Binbin Wu, linux-kernel,
Lu Baolu
Once tdx-host acquires an IOMMU for TDH.IOMMU.SETUP, the TDX module
owns its Secure TDX Mode state. The VT-d driver must not change the
global state of the IOMMU behind the TDX module's back, nor issue
enhanced commands, until the IOMMU is released after TDH.IOMMU.CLEAR.
Use the tdxc_acquired flag of struct intel_iommu, which is protected
by register_lock, to block both. dmar_tdxc_acquire() now fails with
-EBUSY if an enhanced command is still in progress when it sets the
flag. All GCMD writers hold register_lock and wait for completion before
dropping it, so setting the flag under the same lock guarantees no GCMD
operation is in flight once acquire() returns. While the flag is set,
ecmd_submit_sync() fails with -EBUSY rather than waiting, as it may be
called from atomic context.
Write buffer flushing is the only GCMD write reachable on the runtime
map path. Rather than adding a check there, refuse to acquire IOMMUs
that require it (rwbf_quirk or CAP.RWBF). Move rwbf_quirk and
rwbf_required() to iommu.h so dmar.c can use them.
The remaining GCMD writers (root table, translation enable/disable,
queued invalidation and interrupt remapping) are only used at init,
hotplug and suspend/resume. Acquired units are never hot-removed,
hibernation is unavailable with TDX, and the following patch refuses
system suspend while an IOMMU is acquired. Guard them with
WARN_ON_ONCE() ahead of their companion register writes (RTADDR,
IQT/IQA, IRTA).
intel_iommu_shutdown() is the exception. It is only called from
native_machine_shutdown() via machine_shutdown(). On restart, halt,
power-off and kexec, syscore_shutdown() runs first and tdx_shutdown()
issues TDH.SYS.DISABLE, which takes every IOMMU out of Secure TDX Mode.
The crash kexec path doesn't call it. Clear tdxc_acquired there so
translation is still turned off.
Signed-off-by: Lu Baolu <baolu.lu@linux.intel.com>
---
drivers/iommu/intel/iommu.h | 8 ++++-
drivers/iommu/intel/dmar.c | 50 ++++++++++++++++++++++++-----
drivers/iommu/intel/iommu.c | 33 +++++++++++++++++--
drivers/iommu/intel/irq_remapping.c | 10 ++++++
4 files changed, 89 insertions(+), 12 deletions(-)
diff --git a/drivers/iommu/intel/iommu.h b/drivers/iommu/intel/iommu.h
index 18fef91753a0..26135248675d 100644
--- a/drivers/iommu/intel/iommu.h
+++ b/drivers/iommu/intel/iommu.h
@@ -690,7 +690,10 @@ struct intel_iommu {
u64 ecmdcap[DMA_MAX_NUM_ECMDCAP];
u32 gcmd; /* Holds TE, EAFL. Don't need SRTP, SFL, WBF */
raw_spinlock_t register_lock; /* protect register handling */
- /* Acquired for the TDX module. Protected by register_lock. */
+ /*
+ * GCMD and ECMD writes are rejected while the TDX module owns this
+ * IOMMU. Protected by register_lock.
+ */
bool tdxc_acquired;
int seq_id; /* sequence id of the iommu */
int agaw; /* agaw of this iommu */
@@ -1354,6 +1357,9 @@ static inline bool intel_domain_is_ss_paging(struct dmar_domain *domain)
}
extern int intel_iommu_sm;
+extern int rwbf_quirk;
+
+#define rwbf_required(iommu) (rwbf_quirk || cap_rwbf((iommu)->cap))
int iommu_calculate_agaw(struct intel_iommu *iommu);
int iommu_calculate_max_sagaw(struct intel_iommu *iommu);
int ecmd_submit_sync(struct intel_iommu *iommu, u8 ecmd, u64 oa, u64 ob);
diff --git a/drivers/iommu/intel/dmar.c b/drivers/iommu/intel/dmar.c
index 60d4c8ab1b4c..91826705a06b 100644
--- a/drivers/iommu/intel/dmar.c
+++ b/drivers/iommu/intel/dmar.c
@@ -1731,6 +1731,9 @@ void dmar_disable_qi(struct intel_iommu *iommu)
raw_spin_lock_irqsave(&iommu->register_lock, flags);
+ if (WARN_ON_ONCE(iommu->tdxc_acquired))
+ goto end;
+
sts = readl(iommu->reg + DMAR_GSTS_REG);
if (!(sts & DMA_GSTS_QIES))
goto end;
@@ -1762,9 +1765,6 @@ static void __dmar_enable_qi(struct intel_iommu *iommu)
struct q_inval *qi = iommu->qi;
u64 val = virt_to_phys(qi->desc);
- qi->free_head = qi->free_tail = 0;
- qi->free_cnt = QI_LENGTH;
-
/*
* Set DW=1 and QS=1 in IQA_REG when Scalable Mode capability
* is present.
@@ -1774,6 +1774,12 @@ static void __dmar_enable_qi(struct intel_iommu *iommu)
raw_spin_lock_irqsave(&iommu->register_lock, flags);
+ if (WARN_ON_ONCE(iommu->tdxc_acquired))
+ goto out;
+
+ qi->free_head = qi->free_tail = 0;
+ qi->free_cnt = QI_LENGTH;
+
/* write zero to the tail reg */
writel(0, iommu->reg + DMAR_IQT_REG);
@@ -1785,6 +1791,7 @@ static void __dmar_enable_qi(struct intel_iommu *iommu)
/* Make sure hardware complete it */
IOMMU_WAIT_OP(iommu, DMAR_GSTS_REG, readl, (sts & DMA_GSTS_QIES), sts);
+out:
raw_spin_unlock_irqrestore(&iommu->register_lock, flags);
}
@@ -2584,10 +2591,29 @@ static void dmar_tdxc_unreserve_did(struct intel_iommu *iommu)
iommu->max_domain_id = cap_ndoms(iommu->cap);
}
-static void dmar_tdxc_set_acquired(struct intel_iommu *iommu, bool acquired)
+static int dmar_tdxc_block_regs(struct intel_iommu *iommu)
{
- guard(raw_spinlock_irqsave)(&iommu->register_lock);
- iommu->tdxc_acquired = acquired;
+ unsigned long flags;
+ int ret = 0;
+
+ raw_spin_lock_irqsave(&iommu->register_lock, flags);
+ if (cap_ecmds(iommu->cap) &&
+ (readq(iommu->reg + DMAR_ECRSP_REG) & DMA_ECMD_ECRSP_IP))
+ ret = -EBUSY;
+ else
+ iommu->tdxc_acquired = true;
+ raw_spin_unlock_irqrestore(&iommu->register_lock, flags);
+
+ return ret;
+}
+
+static void dmar_tdxc_unblock_regs(struct intel_iommu *iommu)
+{
+ unsigned long flags;
+
+ raw_spin_lock_irqsave(&iommu->register_lock, flags);
+ iommu->tdxc_acquired = false;
+ raw_spin_unlock_irqrestore(&iommu->register_lock, flags);
}
static struct dmar_drhd_unit *dmar_tdxc_find_drhd(struct pci_dev *pdev)
@@ -2669,11 +2695,19 @@ int dmar_tdxc_acquire(struct pci_dev *pdev,
if (!(iommu->gcmd & DMA_GCMD_TE) || !iommu->qi || !sm_supported(iommu))
return -EINVAL;
+ /* Write buffer flushing needs GCMD, which is blocked once acquired. */
+ if (rwbf_required(iommu))
+ return -EOPNOTSUPP;
+
ret = dmar_tdxc_reserve_did(iommu);
if (ret)
return ret;
- dmar_tdxc_set_acquired(iommu, true);
+ ret = dmar_tdxc_block_regs(iommu);
+ if (ret) {
+ dmar_tdxc_unreserve_did(iommu);
+ return ret;
+ }
return 0;
}
@@ -2699,7 +2733,7 @@ void dmar_tdxc_release(struct pci_dev *pdev,
if (WARN_ON(!drhd->iommu->tdxc_acquired))
return;
- dmar_tdxc_set_acquired(drhd->iommu, false);
+ dmar_tdxc_unblock_regs(drhd->iommu);
dmar_tdxc_unreserve_did(drhd->iommu);
}
EXPORT_SYMBOL_FOR_MODULES(dmar_tdxc_release, "tdx-host");
diff --git a/drivers/iommu/intel/iommu.c b/drivers/iommu/intel/iommu.c
index 5a2e705a2f2e..69e8c2c8680a 100644
--- a/drivers/iommu/intel/iommu.c
+++ b/drivers/iommu/intel/iommu.c
@@ -46,9 +46,7 @@
static void __init check_tylersburg_isoch(void);
static int intel_iommu_set_dirty_tracking(struct iommu_domain *domain,
bool enable);
-static int rwbf_quirk;
-
-#define rwbf_required(iommu) (rwbf_quirk || cap_rwbf((iommu)->cap))
+int rwbf_quirk;
/*
* Skip forcing iommu on and avoid tboot-related kernel panics during
@@ -699,6 +697,10 @@ static void iommu_set_root_entry(struct intel_iommu *iommu)
addr |= DMA_RTADDR_SMT;
raw_spin_lock_irqsave(&iommu->register_lock, flag);
+ if (WARN_ON_ONCE(iommu->tdxc_acquired)) {
+ raw_spin_unlock_irqrestore(&iommu->register_lock, flag);
+ return;
+ }
writeq(addr, iommu->reg + DMAR_RTADDR_REG);
writel(iommu->gcmd | DMA_GCMD_SRTP, iommu->reg + DMAR_GCMD_REG);
@@ -731,12 +733,15 @@ void iommu_flush_write_buffer(struct intel_iommu *iommu)
return;
raw_spin_lock_irqsave(&iommu->register_lock, flag);
+ if (WARN_ON_ONCE(iommu->tdxc_acquired))
+ goto out;
writel(iommu->gcmd | DMA_GCMD_WBF, iommu->reg + DMAR_GCMD_REG);
/* Make sure hardware complete it */
IOMMU_WAIT_OP(iommu, DMAR_GSTS_REG,
readl, (!(val & DMA_GSTS_WBFS)), val);
+out:
raw_spin_unlock_irqrestore(&iommu->register_lock, flag);
}
@@ -956,6 +961,8 @@ static void iommu_enable_translation(struct intel_iommu *iommu)
unsigned long flags;
raw_spin_lock_irqsave(&iommu->register_lock, flags);
+ if (WARN_ON_ONCE(iommu->tdxc_acquired))
+ goto out;
iommu->gcmd |= DMA_GCMD_TE;
writel(iommu->gcmd, iommu->reg + DMAR_GCMD_REG);
@@ -963,6 +970,7 @@ static void iommu_enable_translation(struct intel_iommu *iommu)
IOMMU_WAIT_OP(iommu, DMAR_GSTS_REG,
readl, (sts & DMA_GSTS_TES), sts);
+out:
raw_spin_unlock_irqrestore(&iommu->register_lock, flags);
}
@@ -976,6 +984,8 @@ static void iommu_disable_translation(struct intel_iommu *iommu)
return;
raw_spin_lock_irqsave(&iommu->register_lock, flag);
+ if (WARN_ON_ONCE(iommu->tdxc_acquired))
+ goto out;
iommu->gcmd &= ~DMA_GCMD_TE;
writel(iommu->gcmd, iommu->reg + DMAR_GCMD_REG);
@@ -983,6 +993,7 @@ static void iommu_disable_translation(struct intel_iommu *iommu)
IOMMU_WAIT_OP(iommu, DMAR_GSTS_REG,
readl, (!(sts & DMA_GSTS_TES)), sts);
+out:
raw_spin_unlock_irqrestore(&iommu->register_lock, flag);
}
@@ -2513,6 +2524,12 @@ void intel_iommu_shutdown(void)
/* Disable PMRs explicitly here. */
iommu_disable_protect_mem_regions(iommu);
+ /*
+ * Only reached via machine_shutdown(), after syscore_shutdown()
+ * ran TDH.SYS.DISABLE and took the IOMMU out of Secure TDX Mode.
+ */
+ iommu->tdxc_acquired = false;
+
/* Make sure the IOMMUs are switched off */
iommu_disable_translation(iommu);
}
@@ -4336,6 +4353,16 @@ int ecmd_submit_sync(struct intel_iommu *iommu, u8 ecmd, u64 oa, u64 ob)
raw_spin_lock_irqsave(&iommu->register_lock, flags);
+ /*
+ * The TDX module owns this IOMMU. This may be called from atomic
+ * context (e.g. perf pmu::start/stop), so don't sleep; just reject
+ * the request.
+ */
+ if (iommu->tdxc_acquired) {
+ ret = -EBUSY;
+ goto err;
+ }
+
res = readq(iommu->reg + DMAR_ECRSP_REG);
if (res & DMA_ECMD_ECRSP_IP) {
ret = -EBUSY;
diff --git a/drivers/iommu/intel/irq_remapping.c b/drivers/iommu/intel/irq_remapping.c
index 25c26f706984..6e1fe0fcafd9 100644
--- a/drivers/iommu/intel/irq_remapping.c
+++ b/drivers/iommu/intel/irq_remapping.c
@@ -464,6 +464,10 @@ static void iommu_set_irq_remapping(struct intel_iommu *iommu, int mode)
addr = virt_to_phys((void *)iommu->ir_table->base);
raw_spin_lock_irqsave(&iommu->register_lock, flags);
+ if (WARN_ON_ONCE(iommu->tdxc_acquired)) {
+ raw_spin_unlock_irqrestore(&iommu->register_lock, flags);
+ return;
+ }
writeq((addr) | IR_X2APIC_MODE(mode) | INTR_REMAP_TABLE_REG_SIZE,
iommu->reg + DMAR_IRTA_REG);
@@ -489,6 +493,8 @@ static void iommu_enable_irq_remapping(struct intel_iommu *iommu)
u32 sts;
raw_spin_lock_irqsave(&iommu->register_lock, flags);
+ if (WARN_ON_ONCE(iommu->tdxc_acquired))
+ goto out;
/* Enable interrupt-remapping */
iommu->gcmd |= DMA_GCMD_IRE;
@@ -514,6 +520,7 @@ static void iommu_enable_irq_remapping(struct intel_iommu *iommu)
"Compatibility-format IRQs enabled despite intr remapping;\n"
"you are vulnerable to IRQ injection.\n");
+out:
raw_spin_unlock_irqrestore(&iommu->register_lock, flags);
}
@@ -658,6 +665,9 @@ static void iommu_disable_irq_remapping(struct intel_iommu *iommu)
raw_spin_lock_irqsave(&iommu->register_lock, flags);
+ if (WARN_ON_ONCE(iommu->tdxc_acquired))
+ goto end;
+
sts = readl(iommu->reg + DMAR_GSTS_REG);
if (!(sts & DMA_GSTS_IRES))
goto end;
--
2.43.0
^ permalink raw reply [flat|nested] 10+ messages in thread* [PATCH v3 6/9] iommu/vt-d: Refuse system suspend while acquired by the TDX module
2026-11-12 4:30 [PATCH v3 0/9] coco/tdx-host: Introduce trusted DMA initialization support Lu Baolu
` (4 preceding siblings ...)
2026-11-12 4:30 ` [PATCH v3 5/9] iommu/vt-d: Block GCMD and ECMD while acquired by the TDX module Lu Baolu
@ 2026-11-12 4:30 ` Lu Baolu
2026-11-12 4:30 ` [PATCH v3 7/9] coco/tdx-host: Add skeleton to support PCI link encryption Lu Baolu
` (2 subsequent siblings)
8 siblings, 0 replies; 10+ messages in thread
From: Lu Baolu @ 2026-11-12 4:30 UTC (permalink / raw)
To: iommu, x86, linux-coco, kvm
Cc: Jason Gunthorpe, Kevin Tian, Kiryl Shutsemau, Rick Edgecombe,
yilun.xu, xiaoyao.li, Chao Gao, Binbin Wu, linux-kernel,
Lu Baolu
While tdx-host has acquired an IOMMU, the VT-d driver blocks its GCMD
writes. TDX disables S3 by clearing acpi_suspend_lowlevel, but that is
only checked after the syscore suspend callbacks have run, so an S3
request, e.g. "echo mem > /sys/power/state", still runs iommu_suspend()
and then iommu_resume():
suspend_enter()
syscore_suspend()
iommu_suspend() <- runs on the acquired IOMMU
suspend_ops->enter() = acpi_suspend_enter()
if (!acpi_suspend_lowlevel) <- TDX's "S3 disabled" check
return -ENOSYS
syscore_resume()
iommu_resume() <- runs on the acquired IOMMU
Register a PM notifier for each DMAR unit that supports trusted DMA and
refuse PM_SUSPEND_PREPARE with -EBUSY while the unit is acquired. The
suspend then fails before tasks are frozen and before syscore callbacks
run.
A check at PM_SUSPEND_PREPARE alone leaves a window: user space is only
frozen after the notifiers run, so a device connect may still acquire
the unit in between. Close it with a per-unit tdxc_suspending flag, set
when the notifier lets the suspend proceed and cleared at
PM_POST_SUSPEND. dmar_tdxc_acquire() fails with -EBUSY while it is set.
Both the notifier and dmar_tdxc_acquire() hold the unit's tdxc_lock, so
the check and the acquisition can't interleave.
The notifiers are registered at init only for units advertising
ECAP.TDXCS. Such units are never hot-added or hot-removed, so the set
doesn't change afterwards and the notifiers are never unregistered.
Signed-off-by: Lu Baolu <baolu.lu@linux.intel.com>
---
drivers/iommu/intel/iommu.h | 1 +
include/linux/dmar.h | 4 ++-
drivers/iommu/intel/dmar.c | 53 +++++++++++++++++++++++++++++++++++++
drivers/iommu/intel/iommu.c | 1 +
4 files changed, 58 insertions(+), 1 deletion(-)
diff --git a/drivers/iommu/intel/iommu.h b/drivers/iommu/intel/iommu.h
index 26135248675d..f7554df8cd2d 100644
--- a/drivers/iommu/intel/iommu.h
+++ b/drivers/iommu/intel/iommu.h
@@ -1363,6 +1363,7 @@ extern int rwbf_quirk;
int iommu_calculate_agaw(struct intel_iommu *iommu);
int iommu_calculate_max_sagaw(struct intel_iommu *iommu);
int ecmd_submit_sync(struct intel_iommu *iommu, u8 ecmd, u64 oa, u64 ob);
+void dmar_tdxc_pm_init(void);
static inline bool ecmd_has_pmu_essential(struct intel_iommu *iommu)
{
diff --git a/include/linux/dmar.h b/include/linux/dmar.h
index a39ca13f13dc..8b407bc6d179 100644
--- a/include/linux/dmar.h
+++ b/include/linux/dmar.h
@@ -47,7 +47,9 @@ struct dmar_drhd_unit {
u8 ignored:1; /* ignore drhd */
u8 include_all:1;
u8 gfx_dedicated:1; /* graphic dedicated */
- struct mutex tdxc_lock; /* serializes acquire/release */
+ struct mutex tdxc_lock; /* serializes acquire/release/suspend */
+ struct notifier_block tdxc_pm_nb;
+ bool tdxc_suspending; /* protected by tdxc_lock */
struct intel_iommu *iommu;
};
diff --git a/drivers/iommu/intel/dmar.c b/drivers/iommu/intel/dmar.c
index 91826705a06b..ad6b22601446 100644
--- a/drivers/iommu/intel/dmar.c
+++ b/drivers/iommu/intel/dmar.c
@@ -28,6 +28,7 @@
#include <linux/iommu.h>
#include <linux/numa.h>
#include <linux/limits.h>
+#include <linux/suspend.h>
#include <asm/irq_remapping.h>
#include "iommu.h"
@@ -2683,6 +2684,9 @@ int dmar_tdxc_acquire(struct pci_dev *pdev,
return -EINVAL;
guard(mutex)(&drhd->tdxc_lock);
+ if (drhd->tdxc_suspending)
+ return -EBUSY;
+
/* Still set if TDH.IOMMU.CLEAR failed and tdx-host was reloaded. */
if (iommu->tdxc_acquired)
return -EBUSY;
@@ -2737,3 +2741,52 @@ void dmar_tdxc_release(struct pci_dev *pdev,
dmar_tdxc_unreserve_did(drhd->iommu);
}
EXPORT_SYMBOL_FOR_MODULES(dmar_tdxc_release, "tdx-host");
+
+/*
+ * TDX only fails S3 after syscore suspend/resume have run, and those
+ * would hit the blocked GCMD paths. Refuse the suspend up front instead.
+ */
+static int dmar_tdxc_pm_notify(struct notifier_block *nb,
+ unsigned long action, void *data)
+{
+ struct dmar_drhd_unit *drhd =
+ container_of(nb, struct dmar_drhd_unit, tdxc_pm_nb);
+
+ guard(mutex)(&drhd->tdxc_lock);
+
+ switch (action) {
+ case PM_SUSPEND_PREPARE:
+ if (drhd->iommu->tdxc_acquired)
+ return notifier_from_errno(-EBUSY);
+ drhd->tdxc_suspending = true;
+ break;
+ case PM_POST_SUSPEND:
+ drhd->tdxc_suspending = false;
+ break;
+ }
+
+ return NOTIFY_DONE;
+}
+
+void __init dmar_tdxc_pm_init(void)
+{
+ struct dmar_drhd_unit *drhd;
+ struct intel_iommu *iommu;
+
+ down_read(&dmar_global_lock);
+ for_each_active_iommu(iommu, drhd) {
+ if (!ecap_tdxcs(iommu->ecap))
+ continue;
+
+ drhd->tdxc_pm_nb.notifier_call = dmar_tdxc_pm_notify;
+ /*
+ * CPU hotplug takes dmar_global_lock under the PM notifier chain
+ * lock. Drop it to avoid a lock order splat; TDXCS units are never
+ * hot-removed, so @drhd stays valid.
+ */
+ up_read(&dmar_global_lock);
+ register_pm_notifier(&drhd->tdxc_pm_nb);
+ down_read(&dmar_global_lock);
+ }
+ up_read(&dmar_global_lock);
+}
diff --git a/drivers/iommu/intel/iommu.c b/drivers/iommu/intel/iommu.c
index 69e8c2c8680a..d8b9610ae791 100644
--- a/drivers/iommu/intel/iommu.c
+++ b/drivers/iommu/intel/iommu.c
@@ -2041,6 +2041,7 @@ static struct syscore iommu_syscore = {
static void __init init_iommu_pm_ops(void)
{
register_syscore(&iommu_syscore);
+ dmar_tdxc_pm_init();
}
#else
--
2.43.0
^ permalink raw reply [flat|nested] 10+ messages in thread* [PATCH v3 7/9] coco/tdx-host: Add skeleton to support PCI link encryption
2026-11-12 4:30 [PATCH v3 0/9] coco/tdx-host: Introduce trusted DMA initialization support Lu Baolu
` (5 preceding siblings ...)
2026-11-12 4:30 ` [PATCH v3 6/9] iommu/vt-d: Refuse system suspend " Lu Baolu
@ 2026-11-12 4:30 ` Lu Baolu
2026-11-12 4:30 ` [PATCH v3 8/9] coco/tdx-host: Rename tdx-host.c to main.c Lu Baolu
2026-11-12 4:30 ` [PATCH v3 9/9] coco/tdx-host: Enable trusted DMA on IOMMUs on device connect Lu Baolu
8 siblings, 0 replies; 10+ messages in thread
From: Lu Baolu @ 2026-11-12 4:30 UTC (permalink / raw)
To: iommu, x86, linux-coco, kvm
Cc: Jason Gunthorpe, Kevin Tian, Kiryl Shutsemau, Rick Edgecombe,
yilun.xu, xiaoyao.li, Chao Gao, Binbin Wu, linux-kernel,
Lu Baolu
Register a Link TSM instance to support host side TSM operations for
TDISP, when the TDX Connect support bit is set by TDX Module in
tdx_feature0.
This is the main purpose of an independent tdx-host module out of TDX
core. Recall that a TEE Security Manager (TSM) is a platform agent that
speaks the TEE Device Interface Security Protocol (TDISP) to PCIe
devices and manages private memory resources for the platform. An
independent tdx-host module allows for device-security enumeration and
initialization flows to be deferred from other TDX Module initialization
requirements. Crucially, when / if TDX Module init moves earlier in x86
initialization flow this driver is still guaranteed to run after IOMMU
and PCI init (i.e. subsys_initcall() vs device_initcall()).
The ability to unload the module, or unbind the driver is also useful
for debug and coarse grained transitioning between PCI TSM operation and
PCI CMA operation (native kernel PCI device authentication).
For now only check for TDX Connect support in the TDX Module and
register the Link TSM. The TSM support is basic boilerplate with
operation flows to be added later.
Based on work by Yilun and Dan.
Signed-off-by: Lu Baolu <baolu.lu@linux.intel.com>
---
drivers/virt/coco/tdx-host/Kconfig | 2 +
arch/x86/include/asm/tdx.h | 8 ++
drivers/virt/coco/tdx-host/tdx-host.c | 118 +++++++++++++++++++++++++-
3 files changed, 127 insertions(+), 1 deletion(-)
diff --git a/drivers/virt/coco/tdx-host/Kconfig b/drivers/virt/coco/tdx-host/Kconfig
index 57d0c01a4357..f8c8cc29d46c 100644
--- a/drivers/virt/coco/tdx-host/Kconfig
+++ b/drivers/virt/coco/tdx-host/Kconfig
@@ -1,6 +1,8 @@
config TDX_HOST_SERVICES
tristate
depends on INTEL_TDX_HOST
+ depends on PCI
select FW_LOADER
select FW_UPLOAD
+ select PCI_TSM
default m
diff --git a/arch/x86/include/asm/tdx.h b/arch/x86/include/asm/tdx.h
index 0ed0062c9b0a..fc284a555984 100644
--- a/arch/x86/include/asm/tdx.h
+++ b/arch/x86/include/asm/tdx.h
@@ -142,6 +142,14 @@ int tdx_topup_pamt_cache(struct tdx_pamt_cache *cache, unsigned long npages);
int tdx_pamt_get(kvm_pfn_t pfn, struct tdx_pamt_cache *cache);
void tdx_pamt_put(kvm_pfn_t pfn);
+static inline bool tdx_supports_tdisp(const struct tdx_sys_info *sysinfo)
+{
+ if (!(sysinfo->features.tdx_features0 & TDX_FEATURES0_TDXCONNECT))
+ return false;
+
+ return false; /* To be enabled when kernel is ready */
+}
+
int tdx_guest_keyid_alloc(void);
u32 tdx_get_nr_guest_keyids(void);
void tdx_guest_keyid_free(unsigned int keyid);
diff --git a/drivers/virt/coco/tdx-host/tdx-host.c b/drivers/virt/coco/tdx-host/tdx-host.c
index e8ed8dfa526e..0908aaaa28ac 100644
--- a/drivers/virt/coco/tdx-host/tdx-host.c
+++ b/drivers/virt/coco/tdx-host/tdx-host.c
@@ -9,10 +9,14 @@
#include <linux/firmware.h>
#include <linux/module.h>
#include <linux/sysfs.h>
+#include <linux/pci.h>
+#include <linux/pci-tsm.h>
+#include <linux/tsm.h>
#include <asm/cpu_device_id.h>
#include <asm/seamldr.h>
#include <asm/tdx.h>
+#include <asm/tdx_global_metadata.h>
static const struct x86_cpu_id tdx_host_ids[] = {
X86_MATCH_FEATURE(X86_FEATURE_TDX_HOST_PLATFORM, NULL),
@@ -20,6 +24,112 @@ static const struct x86_cpu_id tdx_host_ids[] = {
};
MODULE_DEVICE_TABLE(x86cpu, tdx_host_ids);
+struct tdx_tsm_link {
+ struct pci_tsm_pf0 pci;
+};
+
+static struct tdx_tsm_link *to_tdx_tsm_link(struct pci_tsm *tsm)
+{
+ return container_of(tsm, struct tdx_tsm_link, pci.base_tsm);
+}
+
+static int tdx_tsm_link_connect(struct pci_dev *pdev)
+{
+ return -ENXIO;
+}
+
+static void tdx_tsm_link_disconnect(struct pci_dev *pdev)
+{
+}
+
+static struct pci_tsm *tdx_tsm_link_pf0_probe(struct tsm_dev *tsm_dev,
+ struct pci_dev *pdev)
+{
+ int rc;
+
+ struct tdx_tsm_link *tlink __free(kfree) = kzalloc_obj(*tlink);
+ if (!tlink)
+ return NULL;
+
+ rc = pci_tsm_pf0_constructor(pdev, &tlink->pci, tsm_dev);
+ if (rc)
+ return NULL;
+
+ return &no_free_ptr(tlink)->pci.base_tsm;
+}
+
+static void tdx_tsm_link_pf0_remove(struct pci_tsm *tsm)
+{
+ struct tdx_tsm_link *tlink = to_tdx_tsm_link(tsm);
+
+ pci_tsm_pf0_destructor(&tlink->pci);
+ kfree(tlink);
+}
+
+static struct pci_tsm *tdx_tsm_link_fn_probe(struct tsm_dev *tsm_dev,
+ struct pci_dev *pdev)
+{
+ int rc;
+
+ struct pci_tsm *pci_tsm __free(kfree) = kzalloc_obj(*pci_tsm);
+ if (!pci_tsm)
+ return NULL;
+
+ rc = pci_tsm_link_constructor(pdev, pci_tsm, tsm_dev);
+ if (rc)
+ return NULL;
+
+ return no_free_ptr(pci_tsm);
+}
+
+static struct pci_tsm *tdx_tsm_link_probe(struct tsm_dev *tsm_dev,
+ struct pci_dev *pdev)
+{
+ if (is_pci_tsm_pf0(pdev))
+ return tdx_tsm_link_pf0_probe(tsm_dev, pdev);
+
+ return tdx_tsm_link_fn_probe(tsm_dev, pdev);
+}
+
+static void tdx_tsm_link_remove(struct pci_tsm *tsm)
+{
+ if (is_pci_tsm_pf0(tsm->pdev)) {
+ tdx_tsm_link_pf0_remove(tsm);
+ return;
+ }
+
+ /* for sub-functions */
+ kfree(tsm);
+}
+
+static struct pci_tsm_ops tdx_tsm_link_ops = {
+ .probe = tdx_tsm_link_probe,
+ .remove = tdx_tsm_link_remove,
+ .connect = tdx_tsm_link_connect,
+ .disconnect = tdx_tsm_link_disconnect,
+};
+
+static void unregister_link_tsm(void *link)
+{
+ tsm_unregister(link);
+}
+
+static int tdx_tdisp_init(struct device *dev)
+{
+ const struct tdx_sys_info *sysinfo = tdx_get_sysinfo();
+ struct tsm_dev *link;
+
+ if (!sysinfo || !tdx_supports_tdisp(sysinfo))
+ return 0;
+
+ link = tsm_register(dev, &tdx_tsm_link_ops);
+ if (IS_ERR(link))
+ return dev_err_probe(dev, PTR_ERR(link),
+ "failed to register TSM\n");
+
+ return devm_add_action_or_reset(dev, unregister_link_tsm, link);
+}
+
static ssize_t version_show(struct device *dev, struct device_attribute *attr,
char *buf)
{
@@ -204,7 +314,13 @@ static int seamldr_init(struct device *dev)
static int tdx_host_probe(struct faux_device *fdev)
{
- return seamldr_init(&fdev->dev);
+ int ret;
+
+ ret = seamldr_init(&fdev->dev);
+ if (ret)
+ return ret;
+
+ return tdx_tdisp_init(&fdev->dev);
}
static const struct faux_device_ops tdx_host_ops = {
--
2.43.0
^ permalink raw reply [flat|nested] 10+ messages in thread* [PATCH v3 8/9] coco/tdx-host: Rename tdx-host.c to main.c
2026-11-12 4:30 [PATCH v3 0/9] coco/tdx-host: Introduce trusted DMA initialization support Lu Baolu
` (6 preceding siblings ...)
2026-11-12 4:30 ` [PATCH v3 7/9] coco/tdx-host: Add skeleton to support PCI link encryption Lu Baolu
@ 2026-11-12 4:30 ` Lu Baolu
2026-11-12 4:30 ` [PATCH v3 9/9] coco/tdx-host: Enable trusted DMA on IOMMUs on device connect Lu Baolu
8 siblings, 0 replies; 10+ messages in thread
From: Lu Baolu @ 2026-11-12 4:30 UTC (permalink / raw)
To: iommu, x86, linux-coco, kvm
Cc: Jason Gunthorpe, Kevin Tian, Kiryl Shutsemau, Rick Edgecombe,
yilun.xu, xiaoyao.li, Chao Gao, Binbin Wu, linux-kernel,
Lu Baolu
The tdx-host driver is about to gain more source files. Rename
tdx-host.c to main.c and build the tdx-host module from it, so the
following changes can add objects to the module without renaming the
main file at the same time.
No functional change.
Signed-off-by: Lu Baolu <baolu.lu@linux.intel.com>
---
drivers/virt/coco/tdx-host/Makefile | 2 ++
drivers/virt/coco/tdx-host/{tdx-host.c => main.c} | 0
2 files changed, 2 insertions(+)
rename drivers/virt/coco/tdx-host/{tdx-host.c => main.c} (100%)
diff --git a/drivers/virt/coco/tdx-host/Makefile b/drivers/virt/coco/tdx-host/Makefile
index e61e749a8dff..a90ffba13a49 100644
--- a/drivers/virt/coco/tdx-host/Makefile
+++ b/drivers/virt/coco/tdx-host/Makefile
@@ -1 +1,3 @@
obj-$(CONFIG_TDX_HOST_SERVICES) += tdx-host.o
+
+tdx-host-y := main.o
diff --git a/drivers/virt/coco/tdx-host/tdx-host.c b/drivers/virt/coco/tdx-host/main.c
similarity index 100%
rename from drivers/virt/coco/tdx-host/tdx-host.c
rename to drivers/virt/coco/tdx-host/main.c
--
2.43.0
^ permalink raw reply [flat|nested] 10+ messages in thread* [PATCH v3 9/9] coco/tdx-host: Enable trusted DMA on IOMMUs on device connect
2026-11-12 4:30 [PATCH v3 0/9] coco/tdx-host: Introduce trusted DMA initialization support Lu Baolu
` (7 preceding siblings ...)
2026-11-12 4:30 ` [PATCH v3 8/9] coco/tdx-host: Rename tdx-host.c to main.c Lu Baolu
@ 2026-11-12 4:30 ` Lu Baolu
8 siblings, 0 replies; 10+ messages in thread
From: Lu Baolu @ 2026-11-12 4:30 UTC (permalink / raw)
To: iommu, x86, linux-coco, kvm
Cc: Jason Gunthorpe, Kevin Tian, Kiryl Shutsemau, Rick Edgecombe,
yilun.xu, xiaoyao.li, Chao Gao, Binbin Wu, linux-kernel,
Lu Baolu
Assigning a device to a TD for private memory access requires that DMA
from that device be translated by structures the TDX module controls.
An Intel IOMMU that implements the trusted DMA extension provides a
parallel set of translation and invalidation resources for this purpose.
They become usable once TDH.IOMMU.SETUP puts the IOMMU into Secure TDX
Mode and the TDX module initializes them. The host keeps the regular
translation path.
Issue TDH.IOMMU.SETUP on demand, when the first device behind an IOMMU
is connected through the TSM, rather than for every capable IOMMU at
probe time. Secure TDX Mode costs host resources: the TDX module's
metadata pages and half of the IOMMU's domain ID space. That cost should
only be paid for IOMMUs that actually serve such devices.
tdx_iommu_get() and tdx_iommu_put() keep a per-IOMMU refcount. The first
get() issues TDH.IOMMU.SETUP and the last put() issues TDH.IOMMU.CLEAR.
A single mutex covers both the refcount and the SEAMCALL that follows,
so a device cannot join an IOMMU that is being cleared or whose setup
has not finished. The refcount lives here rather than in the VT-d
driver because tdx-host already owns the per-IOMMU state that must
outlive TDH.IOMMU.SETUP: the IOMMU_MT pages and the TDX IOMMU ID.
The IOMMU_MT parameter page lists two contiguous buffers for the
trusted invalidation queue, followed by the metadata pages. The TDX
module reports how many metadata pages it needs in its global metadata.
With Dynamic PAMT, every 4K page given to the TDX module needs PAMT
backing first. Take it for each page of the invalidation queue buffers
and the metadata pages before TDH.IOMMU.SETUP, and drop it if setup
fails or once TDH.IOMMU.CLEAR succeeds. The parameter page itself only
carries the page list into the SEAMCALL and needs no backing.
If TDH.IOMMU.CLEAR fails, the TDX module may still reference the
IOMMU_MT pages, so they are leaked together with their PAMT backing,
and the IOMMU is marked unusable. Later get() calls for that IOMMU fail
with -EBUSY rather than reuse an IOMMU in an unknown state. tdx-host
pins itself while such an IOMMU exists, so unloading can't lose track
of it. If the failure happens during unload itself, only the tracking
entry is freed; the VT-d driver still has the IOMMU acquired and keeps
rejecting it.
Signed-off-by: Lu Baolu <baolu.lu@linux.intel.com>
---
drivers/virt/coco/tdx-host/Kconfig | 5 +
drivers/virt/coco/tdx-host/Makefile | 1 +
drivers/virt/coco/tdx-host/private.h | 21 ++
drivers/virt/coco/tdx-host/main.c | 11 +
drivers/virt/coco/tdx-host/viommu.c | 401 +++++++++++++++++++++++++++
5 files changed, 439 insertions(+)
create mode 100644 drivers/virt/coco/tdx-host/private.h
create mode 100644 drivers/virt/coco/tdx-host/viommu.c
diff --git a/drivers/virt/coco/tdx-host/Kconfig b/drivers/virt/coco/tdx-host/Kconfig
index f8c8cc29d46c..1395929f05bb 100644
--- a/drivers/virt/coco/tdx-host/Kconfig
+++ b/drivers/virt/coco/tdx-host/Kconfig
@@ -6,3 +6,8 @@ config TDX_HOST_SERVICES
select FW_UPLOAD
select PCI_TSM
default m
+
+config TDX_CONNECT
+ def_bool y
+ depends on TDX_HOST_SERVICES
+ depends on INTEL_IOMMU
diff --git a/drivers/virt/coco/tdx-host/Makefile b/drivers/virt/coco/tdx-host/Makefile
index a90ffba13a49..bfef8567f16a 100644
--- a/drivers/virt/coco/tdx-host/Makefile
+++ b/drivers/virt/coco/tdx-host/Makefile
@@ -1,3 +1,4 @@
obj-$(CONFIG_TDX_HOST_SERVICES) += tdx-host.o
tdx-host-y := main.o
+tdx-host-$(CONFIG_TDX_CONNECT) += viommu.o
diff --git a/drivers/virt/coco/tdx-host/private.h b/drivers/virt/coco/tdx-host/private.h
new file mode 100644
index 000000000000..53a250ddd695
--- /dev/null
+++ b/drivers/virt/coco/tdx-host/private.h
@@ -0,0 +1,21 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/* Copyright (C) 2026 Intel Corporation */
+#ifndef __TDX_HOST_PRIVATE_H
+#define __TDX_HOST_PRIVATE_H
+
+#include <linux/dmar.h>
+
+struct pci_dev;
+
+#ifdef CONFIG_TDX_CONNECT
+int tdx_iommu_get(struct pci_dev *pdev);
+void tdx_iommu_put(struct pci_dev *pdev);
+#else
+static inline int tdx_iommu_get(struct pci_dev *pdev)
+{
+ return -EOPNOTSUPP;
+}
+
+static inline void tdx_iommu_put(struct pci_dev *pdev) { }
+#endif /* CONFIG_TDX_CONNECT */
+#endif /* __TDX_HOST_PRIVATE_H */
diff --git a/drivers/virt/coco/tdx-host/main.c b/drivers/virt/coco/tdx-host/main.c
index 0908aaaa28ac..a815dbe5127a 100644
--- a/drivers/virt/coco/tdx-host/main.c
+++ b/drivers/virt/coco/tdx-host/main.c
@@ -18,6 +18,8 @@
#include <asm/tdx.h>
#include <asm/tdx_global_metadata.h>
+#include "private.h"
+
static const struct x86_cpu_id tdx_host_ids[] = {
X86_MATCH_FEATURE(X86_FEATURE_TDX_HOST_PLATFORM, NULL),
{}
@@ -35,11 +37,20 @@ static struct tdx_tsm_link *to_tdx_tsm_link(struct pci_tsm *tsm)
static int tdx_tsm_link_connect(struct pci_dev *pdev)
{
+ int ret;
+
+ ret = tdx_iommu_get(pdev);
+ if (ret)
+ return ret;
+
+ /* TDISP connect is not implemented yet. */
+ tdx_iommu_put(pdev);
return -ENXIO;
}
static void tdx_tsm_link_disconnect(struct pci_dev *pdev)
{
+ tdx_iommu_put(pdev);
}
static struct pci_tsm *tdx_tsm_link_pf0_probe(struct tsm_dev *tsm_dev,
diff --git a/drivers/virt/coco/tdx-host/viommu.c b/drivers/virt/coco/tdx-host/viommu.c
new file mode 100644
index 000000000000..c0e263926272
--- /dev/null
+++ b/drivers/virt/coco/tdx-host/viommu.c
@@ -0,0 +1,401 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * TDX IOMMU Extensions support
+ *
+ * Copyright (C) 2026 Intel Corporation
+ */
+#define pr_fmt(fmt) "tdx-host: " fmt
+
+#include <linux/dmar.h>
+#include <linux/list.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/pci.h>
+#include <asm/vmx.h>
+#include <asm/tdx.h>
+
+#include "private.h"
+
+/* These describe the trusted invalidation queue buffers. */
+#define IQ_BUFFER_PAGES 2
+#define IQ_BUFFER_SIZE SZ_8K
+
+/*
+ * Represents a list of pages for TDX module defined IOMMU_MT object.
+ * Typically it uses a "root page" as the medium to exchange a list of
+ * data pages between host and TDX module.
+ */
+struct tdx_iommu_pages {
+ struct tdx_iommu_mt_param *root;
+ void **pages;
+ unsigned int nr_entries;
+};
+
+/**
+ * struct tdx_iommu - Host state of an IOMMU in Secure TDX Mode
+ * @node: Link in tdx_iommu_list
+ * @params: IOMMU information from the VT-d driver
+ * @tdx_iommu_id: Identifier returned by TDH.IOMMU.SETUP
+ * @mt_pages: Pages handed to the TDX module by TDH.IOMMU.SETUP
+ * @users: Number of devices using trusted DMA through this IOMMU
+ * @stuck: TDH.IOMMU.CLEAR failed. The TDX module may still own
+ * @mt_pages, so they are leaked and the IOMMU is not reused.
+ */
+struct tdx_iommu {
+ struct list_head node;
+ struct dmar_tdxc_params params;
+ u64 tdx_iommu_id;
+ struct tdx_iommu_pages *mt_pages;
+ unsigned int users;
+ bool stuck;
+};
+
+/*
+ * Serializes the refcounts together with TDH.IOMMU.SETUP and
+ * TDH.IOMMU.CLEAR, so a device can't join an IOMMU that is being
+ * cleared, or one whose setup hasn't finished.
+ */
+static DEFINE_MUTEX(tdx_iommu_lock);
+static LIST_HEAD(tdx_iommu_list);
+
+static void *tdx_iommu_alloc_pages(int node, size_t size)
+{
+ struct folio *folio;
+
+ if (node == NUMA_NO_NODE)
+ node = numa_mem_id();
+
+ folio = __folio_alloc_node(GFP_KERNEL | __GFP_ZERO, get_order(size), node);
+ if (!folio)
+ return NULL;
+
+ return folio_address(folio);
+}
+
+static void tdx_iommu_free_pages(void *virt)
+{
+ if (!virt)
+ return;
+
+ folio_put(virt_to_folio(virt));
+}
+
+/* Number of 4K pages backing IOMMU_MT entry @i. */
+static unsigned int mt_entry_nr_pages(unsigned int i)
+{
+ return i < TDX_IOMMU_MT_IQ_NR ? IQ_BUFFER_SIZE >> PAGE_SHIFT : 1;
+}
+
+/*
+ * Drop the PAMT backing of the first @nr 4K pages handed to the TDX
+ * module, in the order mt_pamt_get() took it. Pass UINT_MAX for all.
+ */
+static void mt_pamt_put(struct tdx_iommu_pages *mt, unsigned int nr)
+{
+ unsigned int i, j;
+
+ for (i = 0; i < mt->nr_entries && nr; i++) {
+ unsigned long pfn = PHYS_PFN(__pa(mt->pages[i]));
+
+ for (j = 0; j < mt_entry_nr_pages(i) && nr; j++, nr--)
+ tdx_pamt_put(pfn + j);
+ }
+}
+
+/*
+ * With Dynamic PAMT, every 4K page handed to the TDX module needs PAMT
+ * backing first. This covers the IQ buffers and the metadata pages, not
+ * the root page, which only carries the list of pages into the
+ * SEAMCALL. tdx_pamt_get() is a no-op without Dynamic PAMT.
+ */
+static int mt_pamt_get(struct tdx_iommu_pages *mt)
+{
+ unsigned int i, j, nr = 0;
+ int ret;
+
+ for (i = 0; i < mt->nr_entries; i++) {
+ unsigned long pfn = PHYS_PFN(__pa(mt->pages[i]));
+
+ for (j = 0; j < mt_entry_nr_pages(i); j++) {
+ ret = tdx_pamt_get(pfn + j, NULL);
+ if (ret) {
+ mt_pamt_put(mt, nr);
+ return ret;
+ }
+ nr++;
+ }
+ }
+
+ return 0;
+}
+
+static void free_mt_pages(struct tdx_iommu_pages *array)
+{
+ unsigned int i;
+
+ if (IS_ERR_OR_NULL(array))
+ return;
+
+ mt_pamt_put(array, UINT_MAX);
+
+ for (i = 0; i < array->nr_entries; i++)
+ tdx_iommu_free_pages(array->pages[i]);
+
+ tdx_iommu_free_pages(array->root);
+ kfree(array->pages);
+ kfree(array);
+}
+
+DEFINE_FREE(free_mt_pages, struct tdx_iommu_pages *, free_mt_pages(_T))
+
+static void **alloc_mt_pages(unsigned int nr_entries, int node)
+{
+ unsigned int i;
+ void **pages;
+ void *vaddr;
+
+ pages = kzalloc_objs(*pages, nr_entries);
+ if (!pages)
+ return NULL;
+
+ /* Allocate two contiguous buffers for the invalidation queue. */
+ pages[0] = tdx_iommu_alloc_pages(node, IQ_BUFFER_SIZE);
+ if (!pages[0])
+ goto free_pages;
+
+ pages[1] = tdx_iommu_alloc_pages(node, IQ_BUFFER_SIZE);
+ if (!pages[1])
+ goto free_pages;
+
+ /* Allocate the required number of pages for the IOMMU metadata. */
+ for (i = TDX_IOMMU_MT_IQ_NR; i < nr_entries; i++) {
+ vaddr = tdx_iommu_alloc_pages(node, SZ_4K);
+ if (!vaddr)
+ goto free_pages;
+ pages[i] = vaddr;
+ }
+
+ return pages;
+
+free_pages:
+ for (i = 0; i < nr_entries; i++)
+ tdx_iommu_free_pages(pages[i]);
+ kfree(pages);
+
+ return NULL;
+}
+
+static void populate_mt_pages(struct tdx_iommu_pages *array)
+{
+ struct tdx_iommu_mt_param *param = array->root;
+ void **pages = array->pages;
+ unsigned int i;
+
+ for (i = 0; i < TDX_IOMMU_MT_IQ_NR; i++)
+ param->iq[i] = __pa(pages[i]) | IQ_BUFFER_PAGES;
+
+ for (; i < array->nr_entries; i++)
+ param->mt[i - TDX_IOMMU_MT_IQ_NR] = __pa(pages[i]);
+}
+
+/* Allocate the IOMMU_MT pages with PAMT backing. Free with free_mt_pages(). */
+static struct tdx_iommu_pages *alloc_iommu_mt(int node, unsigned int nr_mt_pages)
+{
+ unsigned int nr_entries = nr_mt_pages + TDX_IOMMU_MT_IQ_NR;
+ struct tdx_iommu_pages *array;
+ unsigned int i;
+ int ret = -ENOMEM;
+
+ if (!nr_mt_pages || nr_mt_pages > TDX_IOMMU_MT_MAX_PAGES)
+ return ERR_PTR(-EINVAL);
+
+ array = kzalloc_obj(*array);
+ if (!array)
+ return ERR_PTR(-ENOMEM);
+
+ array->root = tdx_iommu_alloc_pages(node, sizeof(*array->root));
+ if (!array->root)
+ goto free_array;
+
+ array->nr_entries = nr_entries;
+ array->pages = alloc_mt_pages(nr_entries, node);
+ if (!array->pages)
+ goto free_root;
+
+ populate_mt_pages(array);
+
+ ret = mt_pamt_get(array);
+ if (ret)
+ goto free_pages;
+
+ return array;
+
+free_pages:
+ for (i = 0; i < nr_entries; i++)
+ tdx_iommu_free_pages(array->pages[i]);
+ kfree(array->pages);
+free_root:
+ tdx_iommu_free_pages(array->root);
+free_array:
+ kfree(array);
+ return ERR_PTR(ret);
+}
+
+static struct tdx_iommu *tdx_iommu_find(u64 reg_base_addr)
+{
+ struct tdx_iommu *tiommu;
+
+ lockdep_assert_held(&tdx_iommu_lock);
+
+ list_for_each_entry(tiommu, &tdx_iommu_list, node)
+ if (tiommu->params.reg_base_addr == reg_base_addr)
+ return tiommu;
+
+ return NULL;
+}
+
+/* Allocate the IOMMU_MT pages and issue TDH.IOMMU.SETUP. */
+static int tdx_iommu_setup(struct pci_dev *pdev, struct tdx_iommu *tiommu)
+{
+ const struct tdx_sys_info *sysinfo = tdx_get_sysinfo();
+ struct dmar_tdxc_params *params = &tiommu->params;
+ u64 r, tdx_iommu_id;
+ int ret;
+
+ if (!sysinfo)
+ return -ENXIO;
+
+ struct tdx_iommu_pages *mt __free(free_mt_pages) =
+ alloc_iommu_mt(params->node,
+ sysinfo->tdx_connect.iommu_mt_page_count);
+ if (IS_ERR(mt))
+ return PTR_ERR(mt);
+
+ ret = dmar_tdxc_acquire(pdev, params);
+ if (ret)
+ return ret;
+
+ r = tdh_iommu_setup(params->reg_base_addr, mt->root, &tdx_iommu_id);
+ if (r) {
+ dmar_tdxc_release(pdev, params);
+ pr_err("TDH.IOMMU.SETUP failed, regbase 0x%llx status 0x%llx\n",
+ params->reg_base_addr, r);
+ return -EIO;
+ }
+
+ tiommu->tdx_iommu_id = tdx_iommu_id;
+ tiommu->mt_pages = no_free_ptr(mt);
+
+ pr_info("Trusted DMA initialized, regbase 0x%llx\n", params->reg_base_addr);
+
+ return 0;
+}
+
+/* Issue TDH.IOMMU.CLEAR and release what TDH.IOMMU.SETUP took. */
+static int tdx_iommu_clear(struct pci_dev *pdev, struct tdx_iommu *tiommu)
+{
+ u64 r;
+
+ r = tdh_iommu_clear(tiommu->tdx_iommu_id);
+ if (r) {
+ pr_err("TDH.IOMMU.CLEAR failed, regbase 0x%llx tdx_iommu_id 0x%llx status 0x%llx\n",
+ tiommu->params.reg_base_addr, tiommu->tdx_iommu_id, r);
+ /* The pages and their PAMT backing stay with the TDX module. */
+ return -EIO;
+ }
+
+ dmar_tdxc_release(pdev, &tiommu->params);
+ free_mt_pages(tiommu->mt_pages);
+ tiommu->mt_pages = NULL;
+
+ return 0;
+}
+
+/*
+ * tdx_iommu_get() - Enable trusted DMA on the IOMMU translating a device
+ * @pdev: PCI device that will use trusted DMA
+ *
+ * The first device behind an IOMMU transitions that IOMMU into Secure
+ * TDX Mode. Later devices only take a reference. Every successful call
+ * must be balanced by tdx_iommu_put().
+ *
+ * Return: 0 on success and error number on failure.
+ */
+int tdx_iommu_get(struct pci_dev *pdev)
+{
+ struct dmar_tdxc_params params;
+ struct tdx_iommu *tiommu;
+ int ret;
+
+ ret = dmar_tdxc_get_params(pdev, ¶ms);
+ if (ret)
+ return ret;
+
+ guard(mutex)(&tdx_iommu_lock);
+
+ tiommu = tdx_iommu_find(params.reg_base_addr);
+ if (tiommu) {
+ if (tiommu->stuck)
+ return -EBUSY;
+ if (WARN_ON(tiommu->users == UINT_MAX))
+ return -EOVERFLOW;
+ tiommu->users++;
+ return 0;
+ }
+
+ tiommu = kzalloc_obj(*tiommu);
+ if (!tiommu)
+ return -ENOMEM;
+
+ tiommu->params = params;
+ ret = tdx_iommu_setup(pdev, tiommu);
+ if (ret) {
+ kfree(tiommu);
+ return ret;
+ }
+
+ tiommu->users = 1;
+ list_add(&tiommu->node, &tdx_iommu_list);
+
+ return 0;
+}
+
+/*
+ * tdx_iommu_put() - Drop a reference taken by tdx_iommu_get()
+ * @pdev: Same device passed to tdx_iommu_get()
+ *
+ * The last device behind an IOMMU takes that IOMMU out of Secure TDX
+ * Mode. If TDH.IOMMU.CLEAR fails, the TDX module may still own the
+ * IOMMU_MT pages, so they are leaked. The IOMMU then stays in Secure TDX
+ * Mode, later tdx_iommu_get() calls for it fail, and the module can no
+ * longer be unloaded.
+ */
+void tdx_iommu_put(struct pci_dev *pdev)
+{
+ struct dmar_tdxc_params params;
+ struct tdx_iommu *tiommu;
+
+ if (WARN_ON(dmar_tdxc_get_params(pdev, ¶ms)))
+ return;
+
+ guard(mutex)(&tdx_iommu_lock);
+
+ tiommu = tdx_iommu_find(params.reg_base_addr);
+ if (WARN_ON(!tiommu || tiommu->stuck || !tiommu->users))
+ return;
+
+ if (--tiommu->users)
+ return;
+
+ if (tdx_iommu_clear(pdev, tiommu)) {
+ /* VT-d still sees it acquired; don't let unload forget it. */
+ if (try_module_get(THIS_MODULE)) {
+ tiommu->stuck = true;
+ return;
+ }
+ /* Unloading: free the entry; IOMMU_MT pages stay leaked. */
+ }
+
+ list_del(&tiommu->node);
+ kfree(tiommu);
+}
--
2.43.0
^ permalink raw reply [flat|nested] 10+ messages in thread