AI Trajectory


Trajectory: (Expand/Collapse All)
Seq Timestamp Type Name Duration
0/0 2026/07/26 10:55 flow patch-triage
1m Args:
null

Results:
{
  "EnableConfigs": [
    "INTEL_TDX_HOST",
    "KVM_INTEL_TDX"
  ],
  "FocusSymbols": [
    "tdx_pamt_get",
    "tdx_pamt_put",
    "tdx_topup_pamt_cache",
    "tdx_free_pamt_cache",
    "tdx_alloc_control_page",
    "tdx_free_control_page"
  ],
  "Reasoning": "The patch introduces Dynamic Physical Address Metadata Table (PAMT) support for Intel TDX in KVM/x86. It adds functional core kernel logic to dynamically allocate and free PAMT memory pages for TDX guests, modifying KVM MMU and TDX initialization paths. This is reachable in virtualized environments running on TDX-capable hardware (e.g., GCP C3 instances) and should be fuzzed to ensure memory management correctness and prevent leaks or crashes in the TDX host support.",
  "WorthFuzzing": true
}

1/1 2026/07/26 10:55 action read-patch-diff
0m Args:
null

Results:
{
  "PatchDiff": "commit ced24102b124c89a56627e7c9f1a3ba8a7cff39a\nAuthor: syz-cluster \u003ctriage@syzkaller.com\u003e\nDate:   Sun Jul 26 10:55:20 2026 +0000\n\n    syz-cluster: applied patch under review\n\ndiff --git a/Documentation/admin-guide/kernel-parameters.txt b/Documentation/admin-guide/kernel-parameters.txt\nindex b5493a7f8f2281..a0a7670246bc58 100644\n--- a/Documentation/admin-guide/kernel-parameters.txt\n+++ b/Documentation/admin-guide/kernel-parameters.txt\n@@ -7512,6 +7512,16 @@ Kernel parameters\n \n \ttdfx=\t\t[HW,DRM]\n \n+\ttdx_dpamt=\n+\t\t\t[X86] Controls whether TDX will use Dynamic PAMT\n+\t\t\tto save memory, when supported.\n+\n+\t\t\tValid parameters: \"on\", \"off\"\n+\t\t\tDefault: \"off\"\n+\n+\t\t\tFor details see:\n+\t\t\tDocumentation/arch/x86/tdx.rst\n+\n \ttest_suspend=\t[SUSPEND]\n \t\t\tFormat: { \"mem\" | \"standby\" | \"freeze\" }[,N]\n \t\t\tSpecify \"mem\" (for Suspend-to-RAM) or \"standby\" (for\ndiff --git a/Documentation/arch/x86/tdx.rst b/Documentation/arch/x86/tdx.rst\nindex 3303499ad4c6f8..a1c23092305807 100644\n--- a/Documentation/arch/x86/tdx.rst\n+++ b/Documentation/arch/x86/tdx.rst\n@@ -200,6 +200,34 @@ reflects the TCB of the currently running TDX module and therefore\n changes after an update. By contrast, TEE_TCB_SVN reflects the TCB at TD\n launch time and is not affected.\n \n+Dynamic PAMT\n+------------\n+\n+Physical Address Metadata Table (PAMT) is memory that the TDX module needs\n+to keep data about each page (think like struct page). It needs to be handed\n+to the TDX module for its exclusive use. For normal PAMT, this is installed\n+when the TDX module is first loaded and comes to about 0.4% of system memory.\n+\n+Dynamic PAMT is a TDX module feature that allows VMM to allocate part of the\n+PAMT as needed (the parts for tracking 4KB size pages). The other page sizes\n+(1GB and 2MB) are still allocated statically at the time of TDX module\n+initialization. This reduces the amount of memory that TDX uses while TDs are\n+not in use.\n+\n+When Dynamic PAMT is in use, dmesg shows it like::\n+\n+  [..] virt/tdx: Enable Dynamic PAMT\n+  [..] virt/tdx: 10092 KB allocated for PAMT\n+  [..] virt/tdx: TDX-Module initialized\n+\n+Dynamic PAMT is only enabled when supported and the ``tdx_dpamt=`` kernel\n+parameter is set to \"on\". The feature is off by default because TDX module\n+internal details prevent Dynamic PAMT from working on all keyid partitioning\n+configurations. When the TDX module is fixed to include these constraints in\n+its enumeration of Dynamic PAMT support, kernel support can be changed to\n+default on. For more information, consult the Intel TDX documentation about\n+Dynamic PAMT.\n+\n TDX Interaction to Other Kernel Components\n ------------------------------------------\n \ndiff --git a/arch/x86/include/asm/kvm-x86-ops.h b/arch/x86/include/asm/kvm-x86-ops.h\nindex 83dc5086138b39..588563dfe88d57 100644\n--- a/arch/x86/include/asm/kvm-x86-ops.h\n+++ b/arch/x86/include/asm/kvm-x86-ops.h\n@@ -98,6 +98,7 @@ KVM_X86_OP_OPTIONAL_RET0(tdp_has_smep)\n KVM_X86_OP(load_mmu_pgd)\n KVM_X86_OP_OPTIONAL_RET0(set_external_spte)\n KVM_X86_OP_OPTIONAL(free_external_spt)\n+KVM_X86_OP_OPTIONAL_RET0(topup_external_cache)\n KVM_X86_OP(has_wbinvd_exit)\n KVM_X86_OP(get_l2_tsc_offset)\n KVM_X86_OP(get_l2_tsc_multiplier)\ndiff --git a/arch/x86/include/asm/kvm_host.h b/arch/x86/include/asm/kvm_host.h\nindex 5f6c1ce9673b7f..0dbf6564dd0c51 100644\n--- a/arch/x86/include/asm/kvm_host.h\n+++ b/arch/x86/include/asm/kvm_host.h\n@@ -1922,6 +1922,7 @@ struct kvm_x86_ops {\n \t/* Update external page tables for page table about to be freed. */\n \tvoid (*free_external_spt)(struct kvm *kvm, struct kvm_mmu_page *sp);\n \n+\tint (*topup_external_cache)(struct kvm_vcpu *vcpu, int min_nr_spts);\n \n \tbool (*has_wbinvd_exit)(void);\n \ndiff --git a/arch/x86/include/asm/tdx.h b/arch/x86/include/asm/tdx.h\nindex 89e97d5761d89e..7910901a7ba21b 100644\n--- a/arch/x86/include/asm/tdx.h\n+++ b/arch/x86/include/asm/tdx.h\n@@ -36,6 +36,7 @@\n /* Bit definitions of TDX_FEATURES0 metadata field */\n #define TDX_FEATURES0_TD_PRESERVING\tBIT_ULL(1)\n #define TDX_FEATURES0_NO_RBP_MOD\tBIT_ULL(18)\n+#define TDX_FEATURES0_DYNAMIC_PAMT\tBIT_ULL(36)\n \n #ifndef __ASSEMBLER__\n \n@@ -118,12 +119,37 @@ static inline bool tdx_supports_runtime_update(const struct tdx_sys_info *sysinf\n \treturn sysinfo-\u003efeatures.tdx_features0 \u0026 TDX_FEATURES0_TD_PRESERVING;\n }\n \n+bool tdx_supports_dynamic_pamt(const struct tdx_sys_info *sysinfo);\n+\n+/* Simple structure for pre-allocating Dynamic PAMT pages outside of spinlocks. */\n+struct tdx_pamt_cache {\n+\tstruct list_head page_list;\n+\tint cnt;\n+};\n+\n+static inline void tdx_init_pamt_cache(struct tdx_pamt_cache *cache)\n+{\n+\tINIT_LIST_HEAD(\u0026cache-\u003epage_list);\n+\tcache-\u003ecnt = 0;\n+}\n+\n+void tdx_free_pamt_cache(struct tdx_pamt_cache *cache);\n+int tdx_topup_pamt_cache(struct tdx_pamt_cache *cache, unsigned long npages);\n+int tdx_pamt_get(kvm_pfn_t pfn, struct tdx_pamt_cache *cache);\n+void tdx_pamt_put(kvm_pfn_t pfn);\n+\n int tdx_guest_keyid_alloc(void);\n u32 tdx_get_nr_guest_keyids(void);\n void tdx_guest_keyid_free(unsigned int keyid);\n \n void tdx_quirk_reset_paddr(unsigned long base, unsigned long size);\n \n+/* Number of PAMT pages to be provided to TDX module per 2MB region of PA */\n+#define TDX_DPAMT_ENTRY_PAGE_CNT 2\n+\n+struct page *tdx_alloc_control_page(void);\n+void tdx_free_control_page(struct page *page);\n+\n struct tdx_td {\n \t/* TD root structure: */\n \tstruct page *tdr_page;\ndiff --git a/arch/x86/include/asm/tdx_global_metadata.h b/arch/x86/include/asm/tdx_global_metadata.h\nindex 41150d546589c1..2a42551fc33cd6 100644\n--- a/arch/x86/include/asm/tdx_global_metadata.h\n+++ b/arch/x86/include/asm/tdx_global_metadata.h\n@@ -21,6 +21,9 @@ struct tdx_sys_info_tdmr {\n \tu16 pamt_4k_entry_size;\n \tu16 pamt_2m_entry_size;\n \tu16 pamt_1g_entry_size;\n+\n+\t/* Optional metadata, if Dynamic PAMT is supported */\n+\tu8  pamt_page_bitmap_entry_bits;\n };\n \n struct tdx_sys_info_td_ctrl {\ndiff --git a/arch/x86/kvm/mmu/mmu.c b/arch/x86/kvm/mmu/mmu.c\nindex 234d0a95abf534..6dab99654f1702 100644\n--- a/arch/x86/kvm/mmu/mmu.c\n+++ b/arch/x86/kvm/mmu/mmu.c\n@@ -614,6 +614,10 @@ static int mmu_topup_memory_caches(struct kvm_vcpu *vcpu, bool maybe_indirect)\n \t\t\t\t\t       PT64_ROOT_MAX_LEVEL);\n \t\tif (r)\n \t\t\treturn r;\n+\n+\t\tr = kvm_x86_call(topup_external_cache)(vcpu, PT64_ROOT_MAX_LEVEL);\n+\t\tif (r)\n+\t\t\treturn r;\n \t}\n \tr = kvm_mmu_topup_memory_cache(\u0026vcpu-\u003earch.mmu_shadow_page_cache,\n \t\t\t\t       PT64_ROOT_MAX_LEVEL);\ndiff --git a/arch/x86/kvm/vmx/tdx.c b/arch/x86/kvm/vmx/tdx.c\nindex 545b03d9d10b81..09573112540e98 100644\n--- a/arch/x86/kvm/vmx/tdx.c\n+++ b/arch/x86/kvm/vmx/tdx.c\n@@ -362,7 +362,7 @@ static void tdx_reclaim_control_page(struct page *ctrl_page)\n \tif (tdx_reclaim_page(ctrl_page))\n \t\treturn;\n \n-\t__free_page(ctrl_page);\n+\ttdx_free_control_page(ctrl_page);\n }\n \n struct tdx_flush_vp_arg {\n@@ -589,7 +589,7 @@ static void tdx_reclaim_td_control_pages(struct kvm *kvm)\n \n \ttdx_quirk_reset_paddr(page_to_phys(kvm_tdx-\u003etd.tdr_page), PAGE_SIZE);\n \n-\t__free_page(kvm_tdx-\u003etd.tdr_page);\n+\ttdx_free_control_page(kvm_tdx-\u003etd.tdr_page);\n \tkvm_tdx-\u003etd.tdr_page = NULL;\n }\n \n@@ -681,6 +681,8 @@ int tdx_vcpu_create(struct kvm_vcpu *vcpu)\n \tif (!irqchip_split(vcpu-\u003ekvm))\n \t\treturn -EINVAL;\n \n+\ttdx_init_pamt_cache(\u0026tdx-\u003epamt_cache);\n+\n \tfpstate_set_confidential(\u0026vcpu-\u003earch.guest_fpu);\n \tvcpu-\u003earch.apic-\u003eguest_apic_protected = true;\n \tINIT_LIST_HEAD(\u0026tdx-\u003evt.pi_wakeup_list);\n@@ -866,6 +868,8 @@ void tdx_vcpu_free(struct kvm_vcpu *vcpu)\n \tstruct vcpu_tdx *tdx = to_tdx(vcpu);\n \tint i;\n \n+\ttdx_free_pamt_cache(\u0026tdx-\u003epamt_cache);\n+\n \tif (vcpu-\u003ecpu != -1) {\n \t\tKVM_BUG_ON(tdx-\u003estate == VCPU_TD_STATE_INITIALIZED, vcpu-\u003ekvm);\n \t\ttdx_flush_vp_on_cpu(vcpu);\n@@ -1621,6 +1625,17 @@ void tdx_load_mmu_pgd(struct kvm_vcpu *vcpu, hpa_t root_hpa, int pgd_level)\n \ttd_vmcs_write64(to_tdx(vcpu), SHARED_EPT_POINTER, root_hpa);\n }\n \n+static int tdx_topup_external_pamt_cache(struct kvm_vcpu *vcpu, int min_nr_spts)\n+{\n+\t/*\n+\t * Minus one page to exclude the root SPT, but plus one page for a\n+\t * possible 4KB private mapping.\n+\t */\n+\tmin_nr_spts += -1 + 1;\n+\n+\treturn tdx_topup_pamt_cache(\u0026to_tdx(vcpu)-\u003epamt_cache, min_nr_spts);\n+}\n+\n static int tdx_mem_page_add(struct kvm *kvm, gfn_t gfn, enum pg_level level,\n \t\t\t    kvm_pfn_t pfn)\n {\n@@ -1679,16 +1694,28 @@ static struct page *tdx_spte_to_sept_pt(struct kvm *kvm, gfn_t gfn,\n static int tdx_sept_map_nonleaf_spte(struct kvm *kvm, gfn_t gfn,\n \t\t\t\t     enum pg_level level, u64 new_spte)\n {\n+\tstruct kvm_vcpu *vcpu = kvm_get_running_vcpu();\n \tgpa_t gpa = gfn_to_gpa(gfn);\n \tu64 err, entry, level_state;\n \tstruct page *sept_pt;\n+\tint ret;\n+\n+\tif (KVM_BUG_ON(!vcpu, kvm))\n+\t\treturn -EIO;\n \n \tsept_pt = tdx_spte_to_sept_pt(kvm, gfn, new_spte, level);\n \tif (!sept_pt)\n \t\treturn -EIO;\n \n+\tret = tdx_pamt_get(page_to_pfn(sept_pt), \u0026to_tdx(vcpu)-\u003epamt_cache);\n+\tif (KVM_BUG_ON(ret, kvm))\n+\t\treturn ret;\n+\n \terr = tdh_mem_sept_add(\u0026to_kvm_tdx(kvm)-\u003etd, gpa, level, sept_pt,\n \t\t\t       \u0026entry, \u0026level_state);\n+\tif (err)\n+\t\ttdx_pamt_put(page_to_pfn(sept_pt));\n+\n \tif (unlikely(tdx_operand_busy(err)))\n \t\treturn -EBUSY;\n \n@@ -1701,8 +1728,13 @@ static int tdx_sept_map_nonleaf_spte(struct kvm *kvm, gfn_t gfn,\n static int tdx_sept_map_leaf_spte(struct kvm *kvm, gfn_t gfn, enum pg_level level,\n \t\t\t\t  u64 new_spte)\n {\n+\tstruct kvm_vcpu *vcpu = kvm_get_running_vcpu();\n \tstruct kvm_tdx *kvm_tdx = to_kvm_tdx(kvm);\n \tkvm_pfn_t pfn = spte_to_pfn(new_spte);\n+\tint ret;\n+\n+\tif (KVM_BUG_ON(!vcpu, kvm))\n+\t\treturn -EIO;\n \n \t/* TODO: handle large pages. */\n \tif (KVM_BUG_ON(level != PG_LEVEL_4K, kvm))\n@@ -1710,6 +1742,10 @@ static int tdx_sept_map_leaf_spte(struct kvm *kvm, gfn_t gfn, enum pg_level leve\n \n \tWARN_ON_ONCE((new_spte \u0026 VMX_EPT_RWX_MASK) != VMX_EPT_RWX_MASK);\n \n+\tret = tdx_pamt_get(pfn, \u0026to_tdx(vcpu)-\u003epamt_cache);\n+\tif (KVM_BUG_ON(ret, kvm))\n+\t\treturn ret;\n+\n \t/*\n \t * Ensure pre_fault_allowed is read by kvm_arch_vcpu_pre_fault_memory()\n \t * before kvm_tdx-\u003estate.  Userspace must not be allowed to pre-fault\n@@ -1722,10 +1758,15 @@ static int tdx_sept_map_leaf_spte(struct kvm *kvm, gfn_t gfn, enum pg_level leve\n \t * If the TD isn't finalized/runnable, then userspace is initializing\n \t * the VM image via KVM_TDX_INIT_MEM_REGION; ADD the page to the TD.\n \t */\n-\tif (unlikely(kvm_tdx-\u003estate != TD_STATE_RUNNABLE))\n-\t\treturn tdx_mem_page_add(kvm, gfn, level, pfn);\n+\tif (likely(kvm_tdx-\u003estate == TD_STATE_RUNNABLE))\n+\t\tret = tdx_mem_page_aug(kvm, gfn, level, pfn);\n+\telse\n+\t\tret = tdx_mem_page_add(kvm, gfn, level, pfn);\n+\n+\tif (ret)\n+\t\ttdx_pamt_put(pfn);\n \n-\treturn tdx_mem_page_aug(kvm, gfn, level, pfn);\n+\treturn ret;\n }\n \n /*\n@@ -1822,6 +1863,7 @@ static int tdx_sept_remove_leaf_spte(struct kvm *kvm, gfn_t gfn,\n \t\treturn -EIO;\n \n \ttdx_quirk_reset_paddr(PFN_PHYS(pfn), PAGE_SIZE);\n+\ttdx_pamt_put(pfn);\n \treturn 0;\n }\n \n@@ -1865,6 +1907,8 @@ static int tdx_sept_set_private_spte(struct kvm *kvm, gfn_t gfn, u64 old_spte,\n  */\n static void tdx_sept_free_private_spt(struct kvm *kvm, struct kvm_mmu_page *sp)\n {\n+\tstruct page *sept_pt = virt_to_page(sp-\u003eexternal_spt);\n+\n \t/*\n \t * KVM doesn't (yet) zap page table pages in mirror page table while\n \t * TD is active, though guest pages mapped in mirror page table could be\n@@ -1878,15 +1922,15 @@ static void tdx_sept_free_private_spt(struct kvm *kvm, struct kvm_mmu_page *sp)\n \t * the page to prevent the kernel from accessing the encrypted page.\n \t */\n \tif (KVM_BUG_ON(is_hkid_assigned(to_kvm_tdx(kvm)), kvm) ||\n-\t    tdx_reclaim_page(virt_to_page(sp-\u003eexternal_spt)))\n+\t    tdx_reclaim_page(sept_pt))\n \t\tgoto out;\n \n \t/*\n-\t * Immediately free the S-EPT page because RCU-time free is unnecessary\n-\t * after TDH.PHYMEM.PAGE.RECLAIM ensures there are no outstanding\n-\t * readers.\n+\t * Immediately free the S-EPT page as the TDX subsystem doesn't support\n+\t * freeing pages from RCU callbacks, and more importantly because\n+\t * TDH.PHYMEM.PAGE.RECLAIM ensures there are no outstanding readers.\n \t */\n-\tfree_page((unsigned long)sp-\u003eexternal_spt);\n+\ttdx_free_control_page(sept_pt);\n out:\n \tsp-\u003eexternal_spt = NULL;\n }\n@@ -2459,7 +2503,7 @@ static int __tdx_td_init(struct kvm *kvm, struct td_params *td_params,\n \n \tret = -ENOMEM;\n \n-\ttdr_page = alloc_page(GFP_KERNEL_ACCOUNT);\n+\ttdr_page = tdx_alloc_control_page();\n \tif (!tdr_page)\n \t\tgoto free_hkid;\n \n@@ -2472,7 +2516,7 @@ static int __tdx_td_init(struct kvm *kvm, struct td_params *td_params,\n \t\tgoto free_tdr;\n \n \tfor (i = 0; i \u003c kvm_tdx-\u003etd.tdcs_nr_pages; i++) {\n-\t\ttdcs_pages[i] = alloc_page(GFP_KERNEL_ACCOUNT);\n+\t\ttdcs_pages[i] = tdx_alloc_control_page();\n \t\tif (!tdcs_pages[i])\n \t\t\tgoto free_tdcs;\n \t}\n@@ -2590,10 +2634,8 @@ static int __tdx_td_init(struct kvm *kvm, struct td_params *td_params,\n teardown:\n \t/* Only free pages not yet added, so start at 'i' */\n \tfor (; i \u003c kvm_tdx-\u003etd.tdcs_nr_pages; i++) {\n-\t\tif (tdcs_pages[i]) {\n-\t\t\t__free_page(tdcs_pages[i]);\n-\t\t\ttdcs_pages[i] = NULL;\n-\t\t}\n+\t\ttdx_free_control_page(tdcs_pages[i]);\n+\t\ttdcs_pages[i] = NULL;\n \t}\n \tif (!kvm_tdx-\u003etd.tdcs_pages)\n \t\tkfree(tdcs_pages);\n@@ -2608,16 +2650,13 @@ static int __tdx_td_init(struct kvm *kvm, struct td_params *td_params,\n \tfree_cpumask_var(packages);\n \n free_tdcs:\n-\tfor (i = 0; i \u003c kvm_tdx-\u003etd.tdcs_nr_pages; i++) {\n-\t\tif (tdcs_pages[i])\n-\t\t\t__free_page(tdcs_pages[i]);\n-\t}\n+\tfor (i = 0; i \u003c kvm_tdx-\u003etd.tdcs_nr_pages; i++)\n+\t\ttdx_free_control_page(tdcs_pages[i]);\n \tkfree(tdcs_pages);\n \tkvm_tdx-\u003etd.tdcs_pages = NULL;\n \n free_tdr:\n-\tif (tdr_page)\n-\t\t__free_page(tdr_page);\n+\ttdx_free_control_page(tdr_page);\n \tkvm_tdx-\u003etd.tdr_page = NULL;\n \n free_hkid:\n@@ -2951,7 +2990,7 @@ static int tdx_td_vcpu_init(struct kvm_vcpu *vcpu, u64 vcpu_rcx)\n \tint ret, i;\n \tu64 err;\n \n-\tpage = alloc_page(GFP_KERNEL_ACCOUNT);\n+\tpage = tdx_alloc_control_page();\n \tif (!page)\n \t\treturn -ENOMEM;\n \ttdx-\u003evp.tdvpr_page = page;\n@@ -2971,7 +3010,7 @@ static int tdx_td_vcpu_init(struct kvm_vcpu *vcpu, u64 vcpu_rcx)\n \t}\n \n \tfor (i = 0; i \u003c kvm_tdx-\u003etd.tdcx_nr_pages; i++) {\n-\t\tpage = alloc_page(GFP_KERNEL_ACCOUNT);\n+\t\tpage = tdx_alloc_control_page();\n \t\tif (!page) {\n \t\t\tret = -ENOMEM;\n \t\t\tgoto free_tdcx;\n@@ -2993,7 +3032,7 @@ static int tdx_td_vcpu_init(struct kvm_vcpu *vcpu, u64 vcpu_rcx)\n \t\t\t * method, but the rest are freed here.\n \t\t\t */\n \t\t\tfor (; i \u003c kvm_tdx-\u003etd.tdcx_nr_pages; i++) {\n-\t\t\t\t__free_page(tdx-\u003evp.tdcx_pages[i]);\n+\t\t\t\ttdx_free_control_page(tdx-\u003evp.tdcx_pages[i]);\n \t\t\t\ttdx-\u003evp.tdcx_pages[i] = NULL;\n \t\t\t}\n \t\t\treturn -EIO;\n@@ -3021,16 +3060,14 @@ static int tdx_td_vcpu_init(struct kvm_vcpu *vcpu, u64 vcpu_rcx)\n \n free_tdcx:\n \tfor (i = 0; i \u003c kvm_tdx-\u003etd.tdcx_nr_pages; i++) {\n-\t\tif (tdx-\u003evp.tdcx_pages[i])\n-\t\t\t__free_page(tdx-\u003evp.tdcx_pages[i]);\n+\t\ttdx_free_control_page(tdx-\u003evp.tdcx_pages[i]);\n \t\ttdx-\u003evp.tdcx_pages[i] = NULL;\n \t}\n \tkfree(tdx-\u003evp.tdcx_pages);\n \ttdx-\u003evp.tdcx_pages = NULL;\n \n free_tdvpr:\n-\tif (tdx-\u003evp.tdvpr_page)\n-\t\t__free_page(tdx-\u003evp.tdvpr_page);\n+\ttdx_free_control_page(tdx-\u003evp.tdvpr_page);\n \ttdx-\u003evp.tdvpr_page = NULL;\n \ttdx-\u003evp.tdvpr_pa = 0;\n \n@@ -3493,6 +3530,10 @@ int __init tdx_hardware_setup(void)\n \n \tvt_x86_ops.set_external_spte = tdx_sept_set_private_spte;\n \tvt_x86_ops.free_external_spt = tdx_sept_free_private_spt;\n+\n+\tif (tdx_supports_dynamic_pamt(tdx_sysinfo))\n+\t\tvt_x86_ops.topup_external_cache = tdx_topup_external_pamt_cache;\n+\n \tvt_x86_ops.protected_apic_has_interrupt = tdx_protected_apic_has_interrupt;\n \treturn 0;\n \ndiff --git a/arch/x86/kvm/vmx/tdx.h b/arch/x86/kvm/vmx/tdx.h\nindex ac8323a68b163a..fd368e3ee060b6 100644\n--- a/arch/x86/kvm/vmx/tdx.h\n+++ b/arch/x86/kvm/vmx/tdx.h\n@@ -72,6 +72,8 @@ struct vcpu_tdx {\n \n \tu64 map_gpa_next;\n \tu64 map_gpa_end;\n+\n+\tstruct tdx_pamt_cache pamt_cache;\n };\n \n void tdh_vp_rd_failed(struct vcpu_tdx *tdx, char *uclass, u32 field, u64 err);\ndiff --git a/arch/x86/virt/vmx/tdx/tdx.c b/arch/x86/virt/vmx/tdx/tdx.c\nindex 42df8ea464c472..dec45709e454a6 100644\n--- a/arch/x86/virt/vmx/tdx/tdx.c\n+++ b/arch/x86/virt/vmx/tdx/tdx.c\n@@ -30,6 +30,7 @@\n #include \u003clinux/suspend.h\u003e\n #include \u003clinux/syscore_ops.h\u003e\n #include \u003clinux/idr.h\u003e\n+#include \u003clinux/vmalloc.h\u003e\n #include \u003casm/page.h\u003e\n #include \u003casm/special_insns.h\u003e\n #include \u003casm/msr-index.h\u003e\n@@ -46,6 +47,8 @@\n #include \"seamcall_internal.h\"\n #include \"tdx.h\"\n \n+static bool tdx_enable_dpamt __ro_after_init;\n+\n struct tdx_module_state {\n \tbool initialized;\n \tbool sysinit_done;\n@@ -63,6 +66,14 @@ static DEFINE_PER_CPU(bool, tdx_lp_initialized);\n \n static struct tdmr_info_list tdx_tdmr_list;\n \n+/*\n+ * On a machine with Dynamic PAMT, the kernel maintains a reference counter\n+ * for every 2MB range. The counter indicates how many users there are for\n+ * the PAMT memory of the 2MB range. The kernel allocates PAMT refcounts at\n+ * initialization.\n+ */\n+static atomic_t *pamt_refcounts;\n+\n /* All TDX-usable memory regions.  Protected by mem_hotplug_lock. */\n static LIST_HEAD(tdx_memlist);\n \n@@ -252,6 +263,42 @@ static struct syscore tdx_syscore = {\n \t.ops = \u0026tdx_syscore_ops,\n };\n \n+/*\n+ * Allocate PAMT reference counters for all physical memory.\n+ *\n+ * It consumes 2MB for every 1TB of physical memory.\n+ */\n+static __init int init_pamt_refcounts(void)\n+{\n+\tsize_t size = DIV_ROUND_UP(max_pfn, PTRS_PER_PTE) * sizeof(*pamt_refcounts);\n+\n+\tif (!tdx_supports_dynamic_pamt(\u0026tdx_sysinfo))\n+\t\treturn 0;\n+\n+\tpamt_refcounts = vzalloc(size);\n+\tif (!pamt_refcounts)\n+\t\treturn -ENOMEM;\n+\n+\treturn 0;\n+}\n+\n+static __init void free_pamt_refcounts(void)\n+{\n+\tif (!tdx_supports_dynamic_pamt(\u0026tdx_sysinfo))\n+\t\treturn;\n+\n+\tvfree(pamt_refcounts);\n+\tpamt_refcounts = NULL;\n+}\n+\n+static atomic_t *tdx_find_pamt_refcount(unsigned long pfn)\n+{\n+\t/* Find which PMD a PFN is in. */\n+\tunsigned long index = pfn \u003e\u003e (PMD_SHIFT - PAGE_SHIFT);\n+\n+\treturn \u0026pamt_refcounts[index];\n+}\n+\n /*\n  * Add a memory region as a TDX memory block.  The caller must make sure\n  * all memory regions are added in address ascending order and don't\n@@ -510,35 +557,37 @@ static __init int fill_out_tdmrs(struct list_head *tmb_list,\n \treturn 0;\n }\n \n+static __init unsigned long tdmr_get_pamt_bitmap_sz(struct tdmr_info *tdmr)\n+{\n+\tunsigned long pamt_sz, nr_pamt_entries;\n+\tint bits_per_entry;\n+\n+\tbits_per_entry = tdx_sysinfo.tdmr.pamt_page_bitmap_entry_bits;\n+\tnr_pamt_entries = tdmr-\u003esize \u003e\u003e PAGE_SHIFT;\n+\tpamt_sz = DIV_ROUND_UP(nr_pamt_entries * bits_per_entry, BITS_PER_BYTE);\n+\n+\treturn PAGE_ALIGN(pamt_sz);\n+}\n+\n /*\n  * Calculate PAMT size given a TDMR and a page size.  The returned\n  * PAMT size is always aligned up to 4K page boundary.\n  */\n-static __init unsigned long tdmr_get_pamt_sz(struct tdmr_info *tdmr, int pgsz,\n-\t\t\t\t\t     u16 pamt_entry_size)\n+static __init unsigned long tdmr_get_pamt_sz(struct tdmr_info *tdmr, int pgsz)\n {\n \tunsigned long pamt_sz, nr_pamt_entries;\n+\tconst int tdx_pg_size_shift[TDX_PS_NR] = { PAGE_SHIFT, PMD_SHIFT, PUD_SHIFT };\n+\tconst u16 pamt_entry_size[TDX_PS_NR] = {\n+\t\ttdx_sysinfo.tdmr.pamt_4k_entry_size,\n+\t\ttdx_sysinfo.tdmr.pamt_2m_entry_size,\n+\t\ttdx_sysinfo.tdmr.pamt_1g_entry_size,\n+\t};\n \n-\tswitch (pgsz) {\n-\tcase TDX_PS_4K:\n-\t\tnr_pamt_entries = tdmr-\u003esize \u003e\u003e PAGE_SHIFT;\n-\t\tbreak;\n-\tcase TDX_PS_2M:\n-\t\tnr_pamt_entries = tdmr-\u003esize \u003e\u003e PMD_SHIFT;\n-\t\tbreak;\n-\tcase TDX_PS_1G:\n-\t\tnr_pamt_entries = tdmr-\u003esize \u003e\u003e PUD_SHIFT;\n-\t\tbreak;\n-\tdefault:\n-\t\tWARN_ON_ONCE(1);\n-\t\treturn 0;\n-\t}\n+\tnr_pamt_entries = tdmr-\u003esize \u003e\u003e tdx_pg_size_shift[pgsz];\n+\tpamt_sz = nr_pamt_entries * pamt_entry_size[pgsz];\n \n-\tpamt_sz = nr_pamt_entries * pamt_entry_size;\n \t/* TDX requires PAMT size must be 4K aligned */\n-\tpamt_sz = ALIGN(pamt_sz, PAGE_SIZE);\n-\n-\treturn pamt_sz;\n+\treturn PAGE_ALIGN(pamt_sz);\n }\n \n /*\n@@ -576,15 +625,11 @@ static __init int tdmr_get_nid(struct tdmr_info *tdmr, struct list_head *tmb_lis\n  * within @tdmr, and set up PAMTs for @tdmr.\n  */\n static __init int tdmr_set_up_pamt(struct tdmr_info *tdmr,\n-\t\t\t\t   struct list_head *tmb_list,\n-\t\t\t\t   u16 pamt_entry_size[])\n+\t\t\t\t   struct list_head *tmb_list)\n {\n-\tunsigned long pamt_base[TDX_PS_NR];\n-\tunsigned long pamt_size[TDX_PS_NR];\n-\tunsigned long tdmr_pamt_base;\n \tunsigned long tdmr_pamt_size;\n \tstruct page *pamt;\n-\tint pgsz, nid;\n+\tint nid;\n \n \tnid = tdmr_get_nid(tdmr, tmb_list);\n \n@@ -592,13 +637,18 @@ static __init int tdmr_set_up_pamt(struct tdmr_info *tdmr,\n \t * Calculate the PAMT size for each TDX supported page size\n \t * and the total PAMT size.\n \t */\n-\ttdmr_pamt_size = 0;\n-\tfor (pgsz = TDX_PS_4K; pgsz \u003c TDX_PS_NR; pgsz++) {\n-\t\tpamt_size[pgsz] = tdmr_get_pamt_sz(tdmr, pgsz,\n-\t\t\t\t\tpamt_entry_size[pgsz]);\n-\t\ttdmr_pamt_size += pamt_size[pgsz];\n+\ttdmr-\u003epamt_1g_size = tdmr_get_pamt_sz(tdmr, TDX_PS_1G);\n+\ttdmr-\u003epamt_2m_size = tdmr_get_pamt_sz(tdmr, TDX_PS_2M);\n+\n+\tif (tdx_supports_dynamic_pamt(\u0026tdx_sysinfo)) {\n+\t\t/* With Dynamic PAMT, PAMT_4K is replaced with a bitmap */\n+\t\ttdmr-\u003epamt_4k_size = tdmr_get_pamt_bitmap_sz(tdmr);\n+\t} else {\n+\t\ttdmr-\u003epamt_4k_size = tdmr_get_pamt_sz(tdmr, TDX_PS_4K);\n \t}\n \n+\ttdmr_pamt_size = tdmr-\u003epamt_4k_size + tdmr-\u003epamt_2m_size + tdmr-\u003epamt_1g_size;\n+\n \t/*\n \t * Allocate one chunk of physically contiguous memory for all\n \t * PAMTs.  This helps minimize the PAMT's use of reserved areas\n@@ -606,25 +656,17 @@ static __init int tdmr_set_up_pamt(struct tdmr_info *tdmr,\n \t */\n \tpamt = alloc_contig_pages(tdmr_pamt_size \u003e\u003e PAGE_SHIFT, GFP_KERNEL,\n \t\t\tnid, \u0026node_online_map);\n-\tif (!pamt)\n-\t\treturn -ENOMEM;\n \n \t/*\n-\t * Break the contiguous allocation back up into the\n-\t * individual PAMTs for each page size.\n+\t * tdmr-\u003epamt_4k_base is still zero so the error\n+\t * path of the caller will skip freeing the PAMT.\n \t */\n-\ttdmr_pamt_base = page_to_pfn(pamt) \u003c\u003c PAGE_SHIFT;\n-\tfor (pgsz = TDX_PS_4K; pgsz \u003c TDX_PS_NR; pgsz++) {\n-\t\tpamt_base[pgsz] = tdmr_pamt_base;\n-\t\ttdmr_pamt_base += pamt_size[pgsz];\n-\t}\n+\tif (!pamt)\n+\t\treturn -ENOMEM;\n \n-\ttdmr-\u003epamt_4k_base = pamt_base[TDX_PS_4K];\n-\ttdmr-\u003epamt_4k_size = pamt_size[TDX_PS_4K];\n-\ttdmr-\u003epamt_2m_base = pamt_base[TDX_PS_2M];\n-\ttdmr-\u003epamt_2m_size = pamt_size[TDX_PS_2M];\n-\ttdmr-\u003epamt_1g_base = pamt_base[TDX_PS_1G];\n-\ttdmr-\u003epamt_1g_size = pamt_size[TDX_PS_1G];\n+\ttdmr-\u003epamt_4k_base = page_to_phys(pamt);\n+\ttdmr-\u003epamt_2m_base = tdmr-\u003epamt_4k_base + tdmr-\u003epamt_4k_size;\n+\ttdmr-\u003epamt_1g_base = tdmr-\u003epamt_2m_base + tdmr-\u003epamt_2m_size;\n \n \treturn 0;\n }\n@@ -655,10 +697,7 @@ static __init void tdmr_do_pamt_func(struct tdmr_info *tdmr,\n \ttdmr_get_pamt(tdmr, \u0026pamt_base, \u0026pamt_size);\n \n \t/* Do nothing if PAMT hasn't been allocated for this TDMR */\n-\tif (!pamt_size)\n-\t\treturn;\n-\n-\tif (WARN_ON_ONCE(!pamt_base))\n+\tif (!pamt_base)\n \t\treturn;\n \n \tpamt_func(pamt_base, pamt_size);\n@@ -684,14 +723,12 @@ static __init void tdmrs_free_pamt_all(struct tdmr_info_list *tdmr_list)\n \n /* Allocate and set up PAMTs for all TDMRs */\n static __init int tdmrs_set_up_pamt_all(struct tdmr_info_list *tdmr_list,\n-\t\t\t\t\tstruct list_head *tmb_list,\n-\t\t\t\t\tu16 pamt_entry_size[])\n+\t\t\t\t struct list_head *tmb_list)\n {\n \tint i, ret = 0;\n \n \tfor (i = 0; i \u003c tdmr_list-\u003enr_consumed_tdmrs; i++) {\n-\t\tret = tdmr_set_up_pamt(tdmr_entry(tdmr_list, i), tmb_list,\n-\t\t\t\tpamt_entry_size);\n+\t\tret = tdmr_set_up_pamt(tdmr_entry(tdmr_list, i), tmb_list);\n \t\tif (ret)\n \t\t\tgoto err;\n \t}\n@@ -968,18 +1005,13 @@ static __init int construct_tdmrs(struct list_head *tmb_list,\n \t\t\t\t  struct tdmr_info_list *tdmr_list,\n \t\t\t\t  struct tdx_sys_info_tdmr *sysinfo_tdmr)\n {\n-\tu16 pamt_entry_size[TDX_PS_NR] = {\n-\t\tsysinfo_tdmr-\u003epamt_4k_entry_size,\n-\t\tsysinfo_tdmr-\u003epamt_2m_entry_size,\n-\t\tsysinfo_tdmr-\u003epamt_1g_entry_size,\n-\t};\n \tint ret;\n \n \tret = fill_out_tdmrs(tmb_list, tdmr_list);\n \tif (ret)\n \t\treturn ret;\n \n-\tret = tdmrs_set_up_pamt_all(tdmr_list, tmb_list, pamt_entry_size);\n+\tret = tdmrs_set_up_pamt_all(tdmr_list, tmb_list);\n \tif (ret)\n \t\treturn ret;\n \n@@ -998,6 +1030,8 @@ static __init int construct_tdmrs(struct list_head *tmb_list,\n \treturn ret;\n }\n \n+#define TDX_SYS_CONFIG_DYNAMIC_PAMT\tBIT(16)\n+\n static __init int config_tdx_module(struct tdmr_info_list *tdmr_list,\n \t\t\t\t    u64 global_keyid)\n {\n@@ -1026,6 +1060,12 @@ static __init int config_tdx_module(struct tdmr_info_list *tdmr_list,\n \targs.rcx = __pa(tdmr_pa_array);\n \targs.rdx = tdmr_list-\u003enr_consumed_tdmrs;\n \targs.r8 = global_keyid;\n+\n+\tif (tdx_supports_dynamic_pamt(\u0026tdx_sysinfo)) {\n+\t\tpr_info(\"Enable Dynamic PAMT\\n\");\n+\t\targs.r8 |= TDX_SYS_CONFIG_DYNAMIC_PAMT;\n+\t}\n+\n \tret = seamcall_prerr(TDH_SYS_CONFIG, \u0026args);\n \n \t/* Free the array as it is not required anymore. */\n@@ -1167,10 +1207,14 @@ static __init int init_tdx_module(void)\n \t */\n \tget_online_mems();\n \n-\tret = build_tdx_memlist(\u0026tdx_memlist);\n+\tret = init_pamt_refcounts();\n \tif (ret)\n \t\tgoto out_put_tdxmem;\n \n+\tret = build_tdx_memlist(\u0026tdx_memlist);\n+\tif (ret)\n+\t\tgoto err_free_pamt_refcounts;\n+\n \t/* Allocate enough space for constructing TDMRs */\n \tret = alloc_tdmr_list(\u0026tdx_tdmr_list, \u0026tdx_sysinfo.tdmr);\n \tif (ret)\n@@ -1220,6 +1264,8 @@ static __init int init_tdx_module(void)\n \tfree_tdmr_list(\u0026tdx_tdmr_list);\n err_free_tdxmem:\n \tfree_tdx_memlist(\u0026tdx_memlist);\n+err_free_pamt_refcounts:\n+\tfree_pamt_refcounts();\n \tgoto out_put_tdxmem;\n }\n \n@@ -2003,6 +2049,288 @@ u64 tdh_phymem_page_wbinvd_hkid(u64 hkid, kvm_pfn_t pfn)\n }\n EXPORT_SYMBOL_FOR_KVM(tdh_phymem_page_wbinvd_hkid);\n \n+bool tdx_supports_dynamic_pamt(const struct tdx_sys_info *sysinfo)\n+{\n+\treturn sysinfo-\u003efeatures.tdx_features0 \u0026 TDX_FEATURES0_DYNAMIC_PAMT \u0026\u0026\n+\t       tdx_enable_dpamt;\n+}\n+EXPORT_SYMBOL_FOR_KVM(tdx_supports_dynamic_pamt);\n+\n+static struct page *tdx_alloc_page_pamt_cache(struct tdx_pamt_cache *cache)\n+{\n+\tstruct page *page;\n+\n+\tpage = list_first_entry_or_null(\u0026cache-\u003epage_list, struct page, lru);\n+\tif (page) {\n+\t\tlist_del(\u0026page-\u003elru);\n+\t\tcache-\u003ecnt--;\n+\t}\n+\n+\treturn page;\n+}\n+\n+static struct page *alloc_dpamt_page(struct tdx_pamt_cache *cache)\n+{\n+\tif (cache)\n+\t\treturn tdx_alloc_page_pamt_cache(cache);\n+\n+\treturn alloc_page(GFP_KERNEL_ACCOUNT);\n+}\n+\n+static int alloc_pamt_array(struct page **pamt_pages, struct tdx_pamt_cache *cache)\n+{\n+\tint i, j;\n+\n+\tfor (i = 0; i \u003c TDX_DPAMT_ENTRY_PAGE_CNT; i++) {\n+\t\tpamt_pages[i] = alloc_dpamt_page(cache);\n+\t\tif (!pamt_pages[i])\n+\t\t\tgoto err;\n+\t}\n+\n+\treturn 0;\n+\n+err:\n+\tfor (j = 0; j \u003c i; j++)\n+\t\t__free_page(pamt_pages[j]);\n+\n+\treturn -ENOMEM;\n+}\n+\n+static void free_pamt_array(struct page **pamt_pages)\n+{\n+\tfor (int i = 0; i \u003c TDX_DPAMT_ENTRY_PAGE_CNT; i++) {\n+\t\t/*\n+\t\t * Reset pages unconditionally to cover cases\n+\t\t * where they were passed to the TDX module.\n+\t\t */\n+\t\ttdx_quirk_reset_paddr(page_to_phys(pamt_pages[i]), PAGE_SIZE);\n+\n+\t\t__free_page(pamt_pages[i]);\n+\t}\n+}\n+\n+/*\n+ * Calculate the arg needed for operating on the DPAMT backing for\n+ * a given 4KB page.\n+ */\n+static u64 pamt_2mb_arg(kvm_pfn_t pfn)\n+{\n+\t/* Arg value will specify a 2MB region of physical address space. */\n+\tunsigned long hpa_2mb = ALIGN_DOWN(pfn \u003c\u003c PAGE_SHIFT, PMD_SIZE);\n+\n+\treturn hpa_2mb | TDX_PS_2M;\n+}\n+\n+/* Add PAMT backing for the 2MB region surrounding the given pfn. */\n+static u64 tdh_phymem_pamt_add(kvm_pfn_t pfn, struct page **pamt_pages)\n+{\n+\tstruct tdx_module_args args = {\n+\t\t.rcx = pamt_2mb_arg(pfn),\n+\t\t.rdx = page_to_phys(pamt_pages[0]),\n+\t\t.r8 = page_to_phys(pamt_pages[1]),\n+\t};\n+\n+\treturn seamcall(TDH_PHYMEM_PAMT_ADD, \u0026args);\n+}\n+\n+/* Remove PAMT backing for the 2MB region surrounding the given pfn. */\n+static u64 tdh_phymem_pamt_remove(kvm_pfn_t pfn, struct page **pamt_pages)\n+{\n+\tstruct tdx_module_args args = {\n+\t\t.rcx = pamt_2mb_arg(pfn),\n+\t};\n+\tu64 ret;\n+\n+\tret = seamcall_ret(TDH_PHYMEM_PAMT_REMOVE, \u0026args);\n+\tif (ret)\n+\t\treturn ret;\n+\n+\t/* Copy PAMT pages out of the struct per the TDX ABI */\n+\tpamt_pages[0] = phys_to_page(args.rdx);\n+\tpamt_pages[1] = phys_to_page(args.r8);\n+\n+\treturn 0;\n+}\n+\n+/* Serializes adding/removing PAMT memory */\n+static DEFINE_SPINLOCK(pamt_lock);\n+\n+/* Bump PAMT refcount for the given pfn and allocate PAMT backing if needed. */\n+int tdx_pamt_get(kvm_pfn_t pfn, struct tdx_pamt_cache *cache)\n+{\n+\tstruct page *pamt_pages[TDX_DPAMT_ENTRY_PAGE_CNT];\n+\tatomic_t *pamt_refcount;\n+\tu64 tdx_status;\n+\tint ret;\n+\n+\tif (!tdx_supports_dynamic_pamt(\u0026tdx_sysinfo))\n+\t\treturn 0;\n+\n+\tpamt_refcount = tdx_find_pamt_refcount(pfn);\n+\n+\t/*\n+\t * If the pamt page is already added (i.e. refcount \u003e= 1),\n+\t * then just increment the refcount.\n+\t */\n+\tif (atomic_inc_not_zero(pamt_refcount))\n+\t\treturn 0;\n+\n+\tret = alloc_pamt_array(pamt_pages, cache);\n+\tif (ret)\n+\t\treturn ret;\n+\n+\tspin_lock(\u0026pamt_lock);\n+\n+\t/*\n+\t * Unlike tdx_pamt_put() which uses atomic_dec_and_lock() to\n+\t * atomically handle the 1-\u003e0 transition, the get side has no\n+\t * equivalent combined primitive for 0-\u003e1. Recheck under the\n+\t * lock since another get may have already done the 0-\u003e1\n+\t * transition after both saw atomic_inc_not_zero() fail.\n+\t */\n+\tif (atomic_inc_not_zero(pamt_refcount))\n+\t\tgoto out_free;\n+\n+\ttdx_status = tdh_phymem_pamt_add(pfn, pamt_pages);\n+\tif (WARN_ON_ONCE(tdx_status != TDX_SUCCESS)) {\n+\t\tret = -EIO;\n+\t\tgoto out_free;\n+\t}\n+\n+\t/*\n+\t * The refcount is zero, and this locked path is the\n+\t * only way to increase it from 0-\u003e1.\n+\t */\n+\tatomic_set(pamt_refcount, 1);\n+\tspin_unlock(\u0026pamt_lock);\n+\treturn 0;\n+\n+out_free:\n+\tspin_unlock(\u0026pamt_lock);\n+\tfree_pamt_array(pamt_pages);\n+\n+\treturn ret;\n+}\n+EXPORT_SYMBOL_FOR_KVM(tdx_pamt_get);\n+\n+/* Drop PAMT refcount for the given pfn and free PAMT backing if needed. */\n+void tdx_pamt_put(kvm_pfn_t pfn)\n+{\n+\tstruct page *pamt_pages[TDX_DPAMT_ENTRY_PAGE_CNT] = {};\n+\tatomic_t *pamt_refcount;\n+\tu64 tdx_status;\n+\n+\tif (!tdx_supports_dynamic_pamt(\u0026tdx_sysinfo))\n+\t\treturn;\n+\n+\tpamt_refcount = tdx_find_pamt_refcount(pfn);\n+\n+\t/*\n+\t * If there is more than 1 reference on the pamt page, don't\n+\t * remove it yet. Just decrement the refcount.\n+\t */\n+\tif (!atomic_dec_and_lock(pamt_refcount, \u0026pamt_lock))\n+\t\treturn;\n+\n+\ttdx_status = tdh_phymem_pamt_remove(pfn, pamt_pages);\n+\n+\t/*\n+\t * Don't free pamt_pages as it could hold garbage when\n+\t * tdh_phymem_pamt_remove() fails.  Don't panic/BUG_ON(), as\n+\t * there is no risk of data corruption, but do yell loudly as\n+\t * failure indicates a kernel bug, memory is being leaked, and\n+\t * the dangling PAMT entry may cause future operations to fail.\n+\t */\n+\tif (WARN_ON_ONCE(tdx_status != TDX_SUCCESS)) {\n+\t\t/*\n+\t\t * atomic_dec_and_lock() already decremented it to 0,\n+\t\t * but the PAMT entry still exists since REMOVE failed.\n+\t\t */\n+\t\tatomic_set(pamt_refcount, 1);\n+\t\tgoto out_unlock;\n+\t}\n+\n+\tspin_unlock(\u0026pamt_lock);\n+\tfree_pamt_array(pamt_pages);\n+\treturn;\n+out_unlock:\n+\tspin_unlock(\u0026pamt_lock);\n+}\n+EXPORT_SYMBOL_FOR_KVM(tdx_pamt_put);\n+\n+void tdx_free_pamt_cache(struct tdx_pamt_cache *cache)\n+{\n+\tstruct page *page;\n+\n+\twhile ((page = tdx_alloc_page_pamt_cache(cache)))\n+\t\t__free_page(page);\n+}\n+EXPORT_SYMBOL_FOR_KVM(tdx_free_pamt_cache);\n+\n+int tdx_topup_pamt_cache(struct tdx_pamt_cache *cache, unsigned long npages)\n+{\n+\tif (WARN_ON_ONCE(!tdx_supports_dynamic_pamt(\u0026tdx_sysinfo)))\n+\t\treturn 0;\n+\n+\tnpages *= TDX_DPAMT_ENTRY_PAGE_CNT;\n+\n+\twhile (cache-\u003ecnt \u003c npages) {\n+\t\tstruct page *page = alloc_page(GFP_KERNEL_ACCOUNT);\n+\n+\t\tif (!page)\n+\t\t\treturn -ENOMEM;\n+\n+\t\tlist_add(\u0026page-\u003elru, \u0026cache-\u003epage_list);\n+\t\tcache-\u003ecnt++;\n+\t}\n+\n+\treturn 0;\n+}\n+EXPORT_SYMBOL_FOR_KVM(tdx_topup_pamt_cache);\n+\n+/*\n+ * Return a page that can be gifted to the TDX-Module for use as a \"control\"\n+ * page, i.e. pages that are used for control structures for a given TDX\n+ * guest, and thus obtain TDX protections, including PAMT tracking.\n+ */\n+struct page *tdx_alloc_control_page(void)\n+{\n+\tstruct page *page;\n+\n+\tpage = alloc_page(GFP_KERNEL_ACCOUNT);\n+\tif (!page)\n+\t\treturn NULL;\n+\n+\tif (tdx_pamt_get(page_to_pfn(page), NULL)) {\n+\t\t__free_page(page);\n+\t\treturn NULL;\n+\t}\n+\n+\treturn page;\n+}\n+EXPORT_SYMBOL_FOR_KVM(tdx_alloc_control_page);\n+\n+/*\n+ * Free a page that was gifted to the TDX-Module for use as a control\n+ * page. After this, the page is no longer protected by TDX.\n+ */\n+void tdx_free_control_page(struct page *page)\n+{\n+\tif (!page)\n+\t\treturn;\n+\n+\ttdx_pamt_put(page_to_pfn(page));\n+\t__free_page(page);\n+}\n+EXPORT_SYMBOL_FOR_KVM(tdx_free_control_page);\n+\n+static int __init tdx_dpamt_setup(char *str)\n+{\n+\treturn kstrtobool(str, \u0026tdx_enable_dpamt) == 0;\n+}\n+\n+__setup(\"tdx_dpamt=\", tdx_dpamt_setup);\n+\n void tdx_sys_disable(void)\n {\n \tstruct tdx_module_args args = {};\ndiff --git a/arch/x86/virt/vmx/tdx/tdx.h b/arch/x86/virt/vmx/tdx/tdx.h\nindex bdfd0e1e337ac0..a886c54decaadc 100644\n--- a/arch/x86/virt/vmx/tdx/tdx.h\n+++ b/arch/x86/virt/vmx/tdx/tdx.h\n@@ -48,6 +48,8 @@\n #define TDH_SYS_CONFIG\t\t\t45\n #define TDH_SYS_SHUTDOWN\t\t52\n #define TDH_SYS_UPDATE\t\t\t53\n+#define TDH_PHYMEM_PAMT_ADD\t\t58\n+#define TDH_PHYMEM_PAMT_REMOVE\t\t59\n #define TDH_SYS_DISABLE\t\t\t69\n \n /*\ndiff --git a/arch/x86/virt/vmx/tdx/tdx_global_metadata.c b/arch/x86/virt/vmx/tdx/tdx_global_metadata.c\nindex e49c300f23d435..8393d2aa59dbe7 100644\n--- a/arch/x86/virt/vmx/tdx/tdx_global_metadata.c\n+++ b/arch/x86/virt/vmx/tdx/tdx_global_metadata.c\n@@ -1,6 +1,6 @@\n // SPDX-License-Identifier: GPL-2.0\n /*\n- * Automatically generated functions to read TDX global metadata.\n+ * Functions to read TDX global metadata.\n  *\n  * This file doesn't compile on its own as it lacks of inclusion\n  * of SEAMCALL wrapper primitive which reads global metadata.\n@@ -33,6 +33,18 @@ static __init int get_tdx_sys_info_features(struct tdx_sys_info_features *sysinf\n \treturn ret;\n }\n \n+static __init int get_tdx_sys_info_tdmr_dpamt(struct tdx_sys_info_tdmr *sysinfo_tdmr)\n+{\n+\tint ret;\n+\tu64 val;\n+\n+\tret = read_sys_metadata_field(0x9100000100000013, \u0026val);\n+\tif (!ret)\n+\t\tsysinfo_tdmr-\u003epamt_page_bitmap_entry_bits = val;\n+\n+\treturn ret;\n+}\n+\n static __init int get_tdx_sys_info_tdmr(struct tdx_sys_info_tdmr *sysinfo_tdmr)\n {\n \tint ret = 0;\n@@ -129,5 +141,14 @@ static __init int get_tdx_sys_info(struct tdx_sys_info *sysinfo)\n \tret = ret ?: get_tdx_sys_info_td_ctrl(\u0026sysinfo-\u003etd_ctrl);\n \tret = ret ?: get_tdx_sys_info_td_conf(\u0026sysinfo-\u003etd_conf);\n \n+\t/*\n+\t * The kernel supports using TDX without Dynamic PAMT, so\n+\t * avoid reporting failure if it's not supported. Don't try\n+\t * to support buggy TDX modules that advertise Dynamic PAMT\n+\t * but don't expose the metadata.\n+\t */\n+\tif (!ret \u0026\u0026 tdx_supports_dynamic_pamt(sysinfo))\n+\t\tret = get_tdx_sys_info_tdmr_dpamt(\u0026sysinfo-\u003etdmr);\n+\n \treturn ret;\n }\n"
}

2/1 2026/07/26 10:55 agent patch-evaluator
1m Model:
best-expensive

Args:
null

Results:
{
  "EnableConfigs": [
    "INTEL_TDX_HOST",
    "KVM_INTEL_TDX"
  ],
  "FocusSymbols": [
    "tdx_pamt_get",
    "tdx_pamt_put",
    "tdx_topup_pamt_cache",
    "tdx_free_pamt_cache",
    "tdx_alloc_control_page",
    "tdx_free_control_page"
  ],
  "Reasoning": "The patch introduces Dynamic Physical Address Metadata Table (PAMT) support for Intel TDX in KVM/x86. It adds functional core kernel logic to dynamically allocate and free PAMT memory pages for TDX guests, modifying KVM MMU and TDX initialization paths. This is reachable in virtualized environments running on TDX-capable hardware (e.g., GCP C3 instances) and should be fuzzed to ensure memory management correctness and prevent leaks or crashes in the TDX host support.",
  "WorthFuzzing": true
}

Instruction:
You are an expert Linux kernel maintainer.
Your job is to review a provided patch series and determine
if it makes functional changes to the kernel that should be fuzzed.

IMPORTANT: The changes have ALREADY been applied and committed as the HEAD commit in
your workspace. Do NOT rely on your internal knowledge of the kernel. You must actively
use your code access tools to examine the actual source code and confirm any assumptions.

Return WorthFuzzing=false if the patch only contains:
- Modifications to Documentation/, Kconfig files, or code comments.
- Purely decorative changes, such as logging (e.g., pr_err, printk) or tracepoints.
- Changes to numeric constants or macros that do not functionally alter execution flow.
- Code paths that are impossible to reach in virtualized environments like GCE or QEMU,
  even when utilizing software-emulated hardware (e.g., usb gadget, mac80211_hwsim).
- Code in vendor-specific PCIe switch, SmartNIC, or GPU drivers (e.g., mlxsw, pds_core, qed,
  ionic, amdgpu) that require physical PCIe hardware cards not emulated in standard QEMU.
- Driver .remove, .shutdown, or pci_unregister_driver teardown callbacks (e.g., igb_remove)
  that are executed only during PCI hot-unplug or sysfs driver unbind operations.

If it modifies reachable core kernel logic, drivers, or architectures, use your code search
tools to verify the code can be executed, then return WorthFuzzing=true.

When returning WorthFuzzing=true, you MUST ALSO:
1. Extract any specific kernel functions that should be heavily fuzzed into FocusSymbols.
   Avoid listing generic hot-path functions to prevent skewed test distributions.
   Prefer non-static, non-inlined API entrypoint functions over internal static helper functions
   (which are inlined by the compiler and do not have distinct symbol addresses).
2. Identify any specific CONFIG_ options required to properly test this new/modified feature.
   Go and look into the Kconfig files and check for ifdefs around the code, do not make assumptions.
   Also check "depends on" lines in Kconfig to include any non-standard parent subsystem configs
   needed for Kbuild to compile the code statically into vmlinux. List them in the EnableConfigs
   output array, and DO NOT add a 'CONFIG_' prefix (e.g., return "NET_IPV4" instead of "CONFIG_NET_IPV4").
Prefer calling several tools at the same time to save round-trips.


Use set-results tool to provide results of the analysis.
It must be called exactly once before the final reply.
Ignore results of this tool.

Prompt:
For your convenience, here is the diff of the changes:
commit ced24102b124c89a56627e7c9f1a3ba8a7cff39a
Author: syz-cluster <triage@syzkaller.com>
Date:   Sun Jul 26 10:55:20 2026 +0000

    syz-cluster: applied patch under review

diff --git a/Documentation/admin-guide/kernel-parameters.txt b/Documentation/admin-guide/kernel-parameters.txt
index b5493a7f8f2281..a0a7670246bc58 100644
--- a/Documentation/admin-guide/kernel-parameters.txt
+++ b/Documentation/admin-guide/kernel-parameters.txt
@@ -7512,6 +7512,16 @@ Kernel parameters
 
 	tdfx=		[HW,DRM]
 
+	tdx_dpamt=
+			[X86] Controls whether TDX will use Dynamic PAMT
+			to save memory, when supported.
+
+			Valid parameters: "on", "off"
+			Default: "off"
+
+			For details see:
+			Documentation/arch/x86/tdx.rst
+
 	test_suspend=	[SUSPEND]
 			Format: { "mem" | "standby" | "freeze" }[,N]
 			Specify "mem" (for Suspend-to-RAM) or "standby" (for
diff --git a/Documentation/arch/x86/tdx.rst b/Documentation/arch/x86/tdx.rst
index 3303499ad4c6f8..a1c23092305807 100644
--- a/Documentation/arch/x86/tdx.rst
+++ b/Documentation/arch/x86/tdx.rst
@@ -200,6 +200,34 @@ reflects the TCB of the currently running TDX module and therefore
 changes after an update. By contrast, TEE_TCB_SVN reflects the TCB at TD
 launch time and is not affected.
 
+Dynamic PAMT
+------------
+
+Physical Address Metadata Table (PAMT) is memory that the TDX module needs
+to keep data about each page (think like struct page). It needs to be handed
+to the TDX module for its exclusive use. For normal PAMT, this is installed
+when the TDX module is first loaded and comes to about 0.4% of system memory.
+
+Dynamic PAMT is a TDX module feature that allows VMM to allocate part of the
+PAMT as needed (the parts for tracking 4KB size pages). The other page sizes
+(1GB and 2MB) are still allocated statically at the time of TDX module
+initialization. This reduces the amount of memory that TDX uses while TDs are
+not in use.
+
+When Dynamic PAMT is in use, dmesg shows it like::
+
+  [..] virt/tdx: Enable Dynamic PAMT
+  [..] virt/tdx: 10092 KB allocated for PAMT
+  [..] virt/tdx: TDX-Module initialized
+
+Dynamic PAMT is only enabled when supported and the ``tdx_dpamt=`` kernel
+parameter is set to "on". The feature is off by default because TDX module
+internal details prevent Dynamic PAMT from working on all keyid partitioning
+configurations. When the TDX module is fixed to include these constraints in
+its enumeration of Dynamic PAMT support, kernel support can be changed to
+default on. For more information, consult the Intel TDX documentation about
+Dynamic PAMT.
+
 TDX Interaction to Other Kernel Components
 ------------------------------------------
 
diff --git a/arch/x86/include/asm/kvm-x86-ops.h b/arch/x86/include/asm/kvm-x86-ops.h
index 83dc5086138b39..588563dfe88d57 100644
--- a/arch/x86/include/asm/kvm-x86-ops.h
+++ b/arch/x86/include/asm/kvm-x86-ops.h
@@ -98,6 +98,7 @@ KVM_X86_OP_OPTIONAL_RET0(tdp_has_smep)
 KVM_X86_OP(load_mmu_pgd)
 KVM_X86_OP_OPTIONAL_RET0(set_external_spte)
 KVM_X86_OP_OPTIONAL(free_external_spt)
+KVM_X86_OP_OPTIONAL_RET0(topup_external_cache)
 KVM_X86_OP(has_wbinvd_exit)
 KVM_X86_OP(get_l2_tsc_offset)
 KVM_X86_OP(get_l2_tsc_multiplier)
diff --git a/arch/x86/include/asm/kvm_host.h b/arch/x86/include/asm/kvm_host.h
index 5f6c1ce9673b7f..0dbf6564dd0c51 100644
--- a/arch/x86/include/asm/kvm_host.h
+++ b/arch/x86/include/asm/kvm_host.h
@@ -1922,6 +1922,7 @@ struct kvm_x86_ops {
 	/* Update external page tables for page table about to be freed. */
 	void (*free_external_spt)(struct kvm *kvm, struct kvm_mmu_page *sp);
 
+	int (*topup_external_cache)(struct kvm_vcpu *vcpu, int min_nr_spts);
 
 	bool (*has_wbinvd_exit)(void);
 
diff --git a/arch/x86/include/asm/tdx.h b/arch/x86/include/asm/tdx.h
index 89e97d5761d89e..7910901a7ba21b 100644
--- a/arch/x86/include/asm/tdx.h
+++ b/arch/x86/include/asm/tdx.h
@@ -36,6 +36,7 @@
 /* Bit definitions of TDX_FEATURES0 metadata field */
 #define TDX_FEATURES0_TD_PRESERVING	BIT_ULL(1)
 #define TDX_FEATURES0_NO_RBP_MOD	BIT_ULL(18)
+#define TDX_FEATURES0_DYNAMIC_PAMT	BIT_ULL(36)
 
 #ifndef __ASSEMBLER__
 
@@ -118,12 +119,37 @@ static inline bool tdx_supports_runtime_update(const struct tdx_sys_info *sysinf
 	return sysinfo->features.tdx_features0 & TDX_FEATURES0_TD_PRESERVING;
 }
 
+bool tdx_supports_dynamic_pamt(const struct tdx_sys_info *sysinfo);
+
+/* Simple structure for pre-allocating Dynamic PAMT pages outside of spinlocks. */
+struct tdx_pamt_cache {
+	struct list_head page_list;
+	int cnt;
+};
+
+static inline void tdx_init_pamt_cache(struct tdx_pamt_cache *cache)
+{
+	INIT_LIST_HEAD(&cache->page_list);
+	cache->cnt = 0;
+}
+
+void tdx_free_pamt_cache(struct tdx_pamt_cache *cache);
+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);
+
 int tdx_guest_keyid_alloc(void);
 u32 tdx_get_nr_guest_keyids(void);
 void tdx_guest_keyid_free(unsigned int keyid);
 
 void tdx_quirk_reset_paddr(unsigned long base, unsigned long size);
 
+/* Number of PAMT pages to be provided to TDX module per 2MB region of PA */
+#define TDX_DPAMT_ENTRY_PAGE_CNT 2
+
+struct page *tdx_alloc_control_page(void);
+void tdx_free_control_page(struct page *page);
+
 struct tdx_td {
 	/* TD root structure: */
 	struct page *tdr_page;
diff --git a/arch/x86/include/asm/tdx_global_metadata.h b/arch/x86/include/asm/tdx_global_metadata.h
index 41150d546589c1..2a42551fc33cd6 100644
--- a/arch/x86/include/asm/tdx_global_metadata.h
+++ b/arch/x86/include/asm/tdx_global_metadata.h
@@ -21,6 +21,9 @@ struct tdx_sys_info_tdmr {
 	u16 pamt_4k_entry_size;
 	u16 pamt_2m_entry_size;
 	u16 pamt_1g_entry_size;
+
+	/* Optional metadata, if Dynamic PAMT is supported */
+	u8  pamt_page_bitmap_entry_bits;
 };
 
 struct tdx_sys_info_td_ctrl {
diff --git a/arch/x86/kvm/mmu/mmu.c b/arch/x86/kvm/mmu/mmu.c
index 234d0a95abf534..6dab99654f1702 100644
--- a/arch/x86/kvm/mmu/mmu.c
+++ b/arch/x86/kvm/mmu/mmu.c
@@ -614,6 +614,10 @@ static int mmu_topup_memory_caches(struct kvm_vcpu *vcpu, bool maybe_indirect)
 					       PT64_ROOT_MAX_LEVEL);
 		if (r)
 			return r;
+
+		r = kvm_x86_call(topup_external_cache)(vcpu, PT64_ROOT_MAX_LEVEL);
+		if (r)
+			return r;
 	}
 	r = kvm_mmu_topup_memory_cache(&vcpu->arch.mmu_shadow_page_cache,
 				       PT64_ROOT_MAX_LEVEL);
diff --git a/arch/x86/kvm/vmx/tdx.c b/arch/x86/kvm/vmx/tdx.c
index 545b03d9d10b81..09573112540e98 100644
--- a/arch/x86/kvm/vmx/tdx.c
+++ b/arch/x86/kvm/vmx/tdx.c
@@ -362,7 +362,7 @@ static void tdx_reclaim_control_page(struct page *ctrl_page)
 	if (tdx_reclaim_page(ctrl_page))
 		return;
 
-	__free_page(ctrl_page);
+	tdx_free_control_page(ctrl_page);
 }
 
 struct tdx_flush_vp_arg {
@@ -589,7 +589,7 @@ static void tdx_reclaim_td_control_pages(struct kvm *kvm)
 
 	tdx_quirk_reset_paddr(page_to_phys(kvm_tdx->td.tdr_page), PAGE_SIZE);
 
-	__free_page(kvm_tdx->td.tdr_page);
+	tdx_free_control_page(kvm_tdx->td.tdr_page);
 	kvm_tdx->td.tdr_page = NULL;
 }
 
@@ -681,6 +681,8 @@ int tdx_vcpu_create(struct kvm_vcpu *vcpu)
 	if (!irqchip_split(vcpu->kvm))
 		return -EINVAL;
 
+	tdx_init_pamt_cache(&tdx->pamt_cache);
+
 	fpstate_set_confidential(&vcpu->arch.guest_fpu);
 	vcpu->arch.apic->guest_apic_protected = true;
 	INIT_LIST_HEAD(&tdx->vt.pi_wakeup_list);
@@ -866,6 +868,8 @@ void tdx_vcpu_free(struct kvm_vcpu *vcpu)
 	struct vcpu_tdx *tdx = to_tdx(vcpu);
 	int i;
 
+	tdx_free_pamt_cache(&tdx->pamt_cache);
+
 	if (vcpu->cpu != -1) {
 		KVM_BUG_ON(tdx->state == VCPU_TD_STATE_INITIALIZED, vcpu->kvm);
 		tdx_flush_vp_on_cpu(vcpu);
@@ -1621,6 +1625,17 @@ void tdx_load_mmu_pgd(struct kvm_vcpu *vcpu, hpa_t root_hpa, int pgd_level)
 	td_vmcs_write64(to_tdx(vcpu), SHARED_EPT_POINTER, root_hpa);
 }
 
+static int tdx_topup_external_pamt_cache(struct kvm_vcpu *vcpu, int min_nr_spts)
+{
+	/*
+	 * Minus one page to exclude the root SPT, but plus one page for a
+	 * possible 4KB private mapping.
+	 */
+	min_nr_spts += -1 + 1;
+
+	return tdx_topup_pamt_cache(&to_tdx(vcpu)->pamt_cache, min_nr_spts);
+}
+
 static int tdx_mem_page_add(struct kvm *kvm, gfn_t gfn, enum pg_level level,
 			    kvm_pfn_t pfn)
 {
@@ -1679,16 +1694,28 @@ static struct page *tdx_spte_to_sept_pt(struct kvm *kvm, gfn_t gfn,
 static int tdx_sept_map_nonleaf_spte(struct kvm *kvm, gfn_t gfn,
 				     enum pg_level level, u64 new_spte)
 {
+	struct kvm_vcpu *vcpu = kvm_get_running_vcpu();
 	gpa_t gpa = gfn_to_gpa(gfn);
 	u64 err, entry, level_state;
 	struct page *sept_pt;
+	int ret;
+
+	if (KVM_BUG_ON(!vcpu, kvm))
+		return -EIO;
 
 	sept_pt = tdx_spte_to_sept_pt(kvm, gfn, new_spte, level);
 	if (!sept_pt)
 		return -EIO;
 
+	ret = tdx_pamt_get(page_to_pfn(sept_pt), &to_tdx(vcpu)->pamt_cache);
+	if (KVM_BUG_ON(ret, kvm))
+		return ret;
+
 	err = tdh_mem_sept_add(&to_kvm_tdx(kvm)->td, gpa, level, sept_pt,
 			       &entry, &level_state);
+	if (err)
+		tdx_pamt_put(page_to_pfn(sept_pt));
+
 	if (unlikely(tdx_operand_busy(err)))
 		return -EBUSY;
 
@@ -1701,8 +1728,13 @@ static int tdx_sept_map_nonleaf_spte(struct kvm *kvm, gfn_t gfn,
 static int tdx_sept_map_leaf_spte(struct kvm *kvm, gfn_t gfn, enum pg_level level,
 				  u64 new_spte)
 {
+	struct kvm_vcpu *vcpu = kvm_get_running_vcpu();
 	struct kvm_tdx *kvm_tdx = to_kvm_tdx(kvm);
 	kvm_pfn_t pfn = spte_to_pfn(new_spte);
+	int ret;
+
+	if (KVM_BUG_ON(!vcpu, kvm))
+		return -EIO;
 
 	/* TODO: handle large pages. */
 	if (KVM_BUG_ON(level != PG_LEVEL_4K, kvm))
@@ -1710,6 +1742,10 @@ static int tdx_sept_map_leaf_spte(struct kvm *kvm, gfn_t gfn, enum pg_level leve
 
 	WARN_ON_ONCE((new_spte & VMX_EPT_RWX_MASK) != VMX_EPT_RWX_MASK);
 
+	ret = tdx_pamt_get(pfn, &to_tdx(vcpu)->pamt_cache);
+	if (KVM_BUG_ON(ret, kvm))
+		return ret;
+
 	/*
 	 * Ensure pre_fault_allowed is read by kvm_arch_vcpu_pre_fault_memory()
 	 * before kvm_tdx->state.  Userspace must not be allowed to pre-fault
@@ -1722,10 +1758,15 @@ static int tdx_sept_map_leaf_spte(struct kvm *kvm, gfn_t gfn, enum pg_level leve
 	 * If the TD isn't finalized/runnable, then userspace is initializing
 	 * the VM image via KVM_TDX_INIT_MEM_REGION; ADD the page to the TD.
 	 */
-	if (unlikely(kvm_tdx->state != TD_STATE_RUNNABLE))
-		return tdx_mem_page_add(kvm, gfn, level, pfn);
+	if (likely(kvm_tdx->state == TD_STATE_RUNNABLE))
+		ret = tdx_mem_page_aug(kvm, gfn, level, pfn);
+	else
+		ret = tdx_mem_page_add(kvm, gfn, level, pfn);
+
+	if (ret)
+		tdx_pamt_put(pfn);
 
-	return tdx_mem_page_aug(kvm, gfn, level, pfn);
+	return ret;
 }
 
 /*
@@ -1822,6 +1863,7 @@ static int tdx_sept_remove_leaf_spte(struct kvm *kvm, gfn_t gfn,
 		return -EIO;
 
 	tdx_quirk_reset_paddr(PFN_PHYS(pfn), PAGE_SIZE);
+	tdx_pamt_put(pfn);
 	return 0;
 }
 
@@ -1865,6 +1907,8 @@ static int tdx_sept_set_private_spte(struct kvm *kvm, gfn_t gfn, u64 old_spte,
  */
 static void tdx_sept_free_private_spt(struct kvm *kvm, struct kvm_mmu_page *sp)
 {
+	struct page *sept_pt = virt_to_page(sp->external_spt);
+
 	/*
 	 * KVM doesn't (yet) zap page table pages in mirror page table while
 	 * TD is active, though guest pages mapped in mirror page table could be
@@ -1878,15 +1922,15 @@ static void tdx_sept_free_private_spt(struct kvm *kvm, struct kvm_mmu_page *sp)
 	 * the page to prevent the kernel from accessing the encrypted page.
 	 */
 	if (KVM_BUG_ON(is_hkid_assigned(to_kvm_tdx(kvm)), kvm) ||
-	    tdx_reclaim_page(virt_to_page(sp->external_spt)))
+	    tdx_reclaim_page(sept_pt))
 		goto out;
 
 	/*
-	 * Immediately free the S-EPT page because RCU-time free is unnecessary
-	 * after TDH.PHYMEM.PAGE.RECLAIM ensures there are no outstanding
-	 * readers.
+	 * Immediately free the S-EPT page as the TDX subsystem doesn't support
+	 * freeing pages from RCU callbacks, and more importantly because
+	 * TDH.PHYMEM.PAGE.RECLAIM ensures there are no outstanding readers.
 	 */
-	free_page((unsigned long)sp->external_spt);
+	tdx_free_control_page(sept_pt);
 out:
 	sp->external_spt = NULL;
 }
@@ -2459,7 +2503,7 @@ static int __tdx_td_init(struct kvm *kvm, struct td_params *td_params,
 
 	ret = -ENOMEM;
 
-	tdr_page = alloc_page(GFP_KERNEL_ACCOUNT);
+	tdr_page = tdx_alloc_control_page();
 	if (!tdr_page)
 		goto free_hkid;
 
@@ -2472,7 +2516,7 @@ static int __tdx_td_init(struct kvm *kvm, struct td_params *td_params,
 		goto free_tdr;
 
 	for (i = 0; i < kvm_tdx->td.tdcs_nr_pages; i++) {
-		tdcs_pages[i] = alloc_page(GFP_KERNEL_ACCOUNT);
+		tdcs_pages[i] = tdx_alloc_control_page();
 		if (!tdcs_pages[i])
 			goto free_tdcs;
 	}
@@ -2590,10 +2634,8 @@ static int __tdx_td_init(struct kvm *kvm, struct td_params *td_params,
 teardown:
 	/* Only free pages not yet added, so start at 'i' */
 	for (; i < kvm_tdx->td.tdcs_nr_pages; i++) {
-		if (tdcs_pages[i]) {
-			__free_page(tdcs_pages[i]);
-			tdcs_pages[i] = NULL;
-		}
+		tdx_free_control_page(tdcs_pages[i]);
+		tdcs_pages[i] = NULL;
 	}
 	if (!kvm_tdx->td.tdcs_pages)
 		kfree(tdcs_pages);
@@ -2608,16 +2650,13 @@ static int __tdx_td_init(struct kvm *kvm, struct td_params *td_params,
 	free_cpumask_var(packages);
 
 free_tdcs:
-	for (i = 0; i < kvm_tdx->td.tdcs_nr_pages; i++) {
-		if (tdcs_pages[i])
-			__free_page(tdcs_pages[i]);
-	}
+	for (i = 0; i < kvm_tdx->td.tdcs_nr_pages; i++)
+		tdx_free_control_page(tdcs_pages[i]);
 	kfree(tdcs_pages);
 	kvm_tdx->td.tdcs_pages = NULL;
 
 free_tdr:
-	if (tdr_page)
-		__free_page(tdr_page);
+	tdx_free_control_page(tdr_page);
 	kvm_tdx->td.tdr_page = NULL;
 
 free_hkid:
@@ -2951,7 +2990,7 @@ static int tdx_td_vcpu_init(struct kvm_vcpu *vcpu, u64 vcpu_rcx)
 	int ret, i;
 	u64 err;
 
-	page = alloc_page(GFP_KERNEL_ACCOUNT);
+	page = tdx_alloc_control_page();
 	if (!page)
 		return -ENOMEM;
 	tdx->vp.tdvpr_page = page;
@@ -2971,7 +3010,7 @@ static int tdx_td_vcpu_init(struct kvm_vcpu *vcpu, u64 vcpu_rcx)
 	}
 
 	for (i = 0; i < kvm_tdx->td.tdcx_nr_pages; i++) {
-		page = alloc_page(GFP_KERNEL_ACCOUNT);
+		page = tdx_alloc_control_page();
 		if (!page) {
 			ret = -ENOMEM;
 			goto free_tdcx;
@@ -2993,7 +3032,7 @@ static int tdx_td_vcpu_init(struct kvm_vcpu *vcpu, u64 vcpu_rcx)
 			 * method, but the rest are freed here.
 			 */
 			for (; i < kvm_tdx->td.tdcx_nr_pages; i++) {
-				__free_page(tdx->vp.tdcx_pages[i]);
+				tdx_free_control_page(tdx->vp.tdcx_pages[i]);
 				tdx->vp.tdcx_pages[i] = NULL;
 			}
 			return -EIO;
@@ -3021,16 +3060,14 @@ static int tdx_td_vcpu_init(struct kvm_vcpu *vcpu, u64 vcpu_rcx)
 
 free_tdcx:
 	for (i = 0; i < kvm_tdx->td.tdcx_nr_pages; i++) {
-		if (tdx->vp.tdcx_pages[i])
-			__free_page(tdx->vp.tdcx_pages[i]);
+		tdx_free_control_page(tdx->vp.tdcx_pages[i]);
 		tdx->vp.tdcx_pages[i] = NULL;
 	}
 	kfree(tdx->vp.tdcx_pages);
 	tdx->vp.tdcx_pages = NULL;
 
 free_tdvpr:
-	if (tdx->vp.tdvpr_page)
-		__free_page(tdx->vp.tdvpr_page);
+	tdx_free_control_page(tdx->vp.tdvpr_page);
 	tdx->vp.tdvpr_page = NULL;
 	tdx->vp.tdvpr_pa = 0;
 
@@ -3493,6 +3530,10 @@ int __init tdx_hardware_setup(void)
 
 	vt_x86_ops.set_external_spte = tdx_sept_set_private_spte;
 	vt_x86_ops.free_external_spt = tdx_sept_free_private_spt;
+
+	if (tdx_supports_dynamic_pamt(tdx_sysinfo))
+		vt_x86_ops.topup_external_cache = tdx_topup_external_pamt_cache;
+
 	vt_x86_ops.protected_apic_has_interrupt = tdx_protected_apic_has_interrupt;
 	return 0;
 
diff --git a/arch/x86/kvm/vmx/tdx.h b/arch/x86/kvm/vmx/tdx.h
index ac8323a68b163a..fd368e3ee060b6 100644
--- a/arch/x86/kvm/vmx/tdx.h
+++ b/arch/x86/kvm/vmx/tdx.h
@@ -72,6 +72,8 @@ struct vcpu_tdx {
 
 	u64 map_gpa_next;
 	u64 map_gpa_end;
+
+	struct tdx_pamt_cache pamt_cache;
 };
 
 void tdh_vp_rd_failed(struct vcpu_tdx *tdx, char *uclass, u32 field, u64 err);
diff --git a/arch/x86/virt/vmx/tdx/tdx.c b/arch/x86/virt/vmx/tdx/tdx.c
index 42df8ea464c472..dec45709e454a6 100644
--- a/arch/x86/virt/vmx/tdx/tdx.c
+++ b/arch/x86/virt/vmx/tdx/tdx.c
@@ -30,6 +30,7 @@
 #include <linux/suspend.h>
 #include <linux/syscore_ops.h>
 #include <linux/idr.h>
+#include <linux/vmalloc.h>
 #include <asm/page.h>
 #include <asm/special_insns.h>
 #include <asm/msr-index.h>
@@ -46,6 +47,8 @@
 #include "seamcall_internal.h"
 #include "tdx.h"
 
+static bool tdx_enable_dpamt __ro_after_init;
+
 struct tdx_module_state {
 	bool initialized;
 	bool sysinit_done;
@@ -63,6 +66,14 @@ static DEFINE_PER_CPU(bool, tdx_lp_initialized);
 
 static struct tdmr_info_list tdx_tdmr_list;
 
+/*
+ * On a machine with Dynamic PAMT, the kernel maintains a reference counter
+ * for every 2MB range. The counter indicates how many users there are for
+ * the PAMT memory of the 2MB range. The kernel allocates PAMT refcounts at
+ * initialization.
+ */
+static atomic_t *pamt_refcounts;
+
 /* All TDX-usable memory regions.  Protected by mem_hotplug_lock. */
 static LIST_HEAD(tdx_memlist);
 
@@ -252,6 +263,42 @@ static struct syscore tdx_syscore = {
 	.ops = &tdx_syscore_ops,
 };
 
+/*
+ * Allocate PAMT reference counters for all physical memory.
+ *
+ * It consumes 2MB for every 1TB of physical memory.
+ */
+static __init int init_pamt_refcounts(void)
+{
+	size_t size = DIV_ROUND_UP(max_pfn, PTRS_PER_PTE) * sizeof(*pamt_refcounts);
+
+	if (!tdx_supports_dynamic_pamt(&tdx_sysinfo))
+		return 0;
+
+	pamt_refcounts = vzalloc(size);
+	if (!pamt_refcounts)
+		return -ENOMEM;
+
+	return 0;
+}
+
+static __init void free_pamt_refcounts(void)
+{
+	if (!tdx_supports_dynamic_pamt(&tdx_sysinfo))
+		return;
+
+	vfree(pamt_refcounts);
+	pamt_refcounts = NULL;
+}
+
+static atomic_t *tdx_find_pamt_refcount(unsigned long pfn)
+{
+	/* Find which PMD a PFN is in. */
+	unsigned long index = pfn >> (PMD_SHIFT - PAGE_SHIFT);
+
+	return &pamt_refcounts[index];
+}
+
 /*
  * Add a memory region as a TDX memory block.  The caller must make sure
  * all memory regions are added in address ascending order and don't
@@ -510,35 +557,37 @@ static __init int fill_out_tdmrs(struct list_head *tmb_list,
 	return 0;
 }
 
+static __init unsigned long tdmr_get_pamt_bitmap_sz(struct tdmr_info *tdmr)
+{
+	unsigned long pamt_sz, nr_pamt_entries;
+	int bits_per_entry;
+
+	bits_per_entry = tdx_sysinfo.tdmr.pamt_page_bitmap_entry_bits;
+	nr_pamt_entries = tdmr->size >> PAGE_SHIFT;
+	pamt_sz = DIV_ROUND_UP(nr_pamt_entries * bits_per_entry, BITS_PER_BYTE);
+
+	return PAGE_ALIGN(pamt_sz);
+}
+
 /*
  * Calculate PAMT size given a TDMR and a page size.  The returned
  * PAMT size is always aligned up to 4K page boundary.
  */
-static __init unsigned long tdmr_get_pamt_sz(struct tdmr_info *tdmr, int pgsz,
-					     u16 pamt_entry_size)
+static __init unsigned long tdmr_get_pamt_sz(struct tdmr_info *tdmr, int pgsz)
 {
 	unsigned long pamt_sz, nr_pamt_entries;
+	const int tdx_pg_size_shift[TDX_PS_NR] = { PAGE_SHIFT, PMD_SHIFT, PUD_SHIFT };
+	const u16 pamt_entry_size[TDX_PS_NR] = {
+		tdx_sysinfo.tdmr.pamt_4k_entry_size,
+		tdx_sysinfo.tdmr.pamt_2m_entry_size,
+		tdx_sysinfo.tdmr.pamt_1g_entry_size,
+	};
 
-	switch (pgsz) {
-	case TDX_PS_4K:
-		nr_pamt_entries = tdmr->size >> PAGE_SHIFT;
-		break;
-	case TDX_PS_2M:
-		nr_pamt_entries = tdmr->size >> PMD_SHIFT;
-		break;
-	case TDX_PS_1G:
-		nr_pamt_entries = tdmr->size >> PUD_SHIFT;
-		break;
-	default:
-		WARN_ON_ONCE(1);
-		return 0;
-	}
+	nr_pamt_entries = tdmr->size >> tdx_pg_size_shift[pgsz];
+	pamt_sz = nr_pamt_entries * pamt_entry_size[pgsz];
 
-	pamt_sz = nr_pamt_entries * pamt_entry_size;
 	/* TDX requires PAMT size must be 4K aligned */
-	pamt_sz = ALIGN(pamt_sz, PAGE_SIZE);
-
-	return pamt_sz;
+	return PAGE_ALIGN(pamt_sz);
 }
 
 /*
@@ -576,15 +625,11 @@ static __init int tdmr_get_nid(struct tdmr_info *tdmr, struct list_head *tmb_lis
  * within @tdmr, and set up PAMTs for @tdmr.
  */
 static __init int tdmr_set_up_pamt(struct tdmr_info *tdmr,
-				   struct list_head *tmb_list,
-				   u16 pamt_entry_size[])
+				   struct list_head *tmb_list)
 {
-	unsigned long pamt_base[TDX_PS_NR];
-	unsigned long pamt_size[TDX_PS_NR];
-	unsigned long tdmr_pamt_base;
 	unsigned long tdmr_pamt_size;
 	struct page *pamt;
-	int pgsz, nid;
+	int nid;
 
 	nid = tdmr_get_nid(tdmr, tmb_list);
 
@@ -592,13 +637,18 @@ static __init int tdmr_set_up_pamt(struct tdmr_info *tdmr,
 	 * Calculate the PAMT size for each TDX supported page size
 	 * and the total PAMT size.
 	 */
-	tdmr_pamt_size = 0;
-	for (pgsz = TDX_PS_4K; pgsz < TDX_PS_NR; pgsz++) {
-		pamt_size[pgsz] = tdmr_get_pamt_sz(tdmr, pgsz,
-					pamt_entry_size[pgsz]);
-		tdmr_pamt_size += pamt_size[pgsz];
+	tdmr->pamt_1g_size = tdmr_get_pamt_sz(tdmr, TDX_PS_1G);
+	tdmr->pamt_2m_size = tdmr_get_pamt_sz(tdmr, TDX_PS_2M);
+
+	if (tdx_supports_dynamic_pamt(&tdx_sysinfo)) {
+		/* With Dynamic PAMT, PAMT_4K is replaced with a bitmap */
+		tdmr->pamt_4k_size = tdmr_get_pamt_bitmap_sz(tdmr);
+	} else {
+		tdmr->pamt_4k_size = tdmr_get_pamt_sz(tdmr, TDX_PS_4K);
 	}
 
+	tdmr_pamt_size = tdmr->pamt_4k_size + tdmr->pamt_2m_size + tdmr->pamt_1g_size;
+
 	/*
 	 * Allocate one chunk of physically contiguous memory for all
 	 * PAMTs.  This helps minimize the PAMT's use of reserved areas
@@ -606,25 +656,17 @@ static __init int tdmr_set_up_pamt(struct tdmr_info *tdmr,
 	 */
 	pamt = alloc_contig_pages(tdmr_pamt_size >> PAGE_SHIFT, GFP_KERNEL,
 			nid, &node_online_map);
-	if (!pamt)
-		return -ENOMEM;
 
 	/*
-	 * Break the contiguous allocation back up into the
-	 * individual PAMTs for each page size.
+	 * tdmr->pamt_4k_base is still zero so the error
+	 * path of the caller will skip freeing the PAMT.
 	 */
-	tdmr_pamt_base = page_to_pfn(pamt) << PAGE_SHIFT;
-	for (pgsz = TDX_PS_4K; pgsz < TDX_PS_NR; pgsz++) {
-		pamt_base[pgsz] = tdmr_pamt_base;
-		tdmr_pamt_base += pamt_size[pgsz];
-	}
+	if (!pamt)
+		return -ENOMEM;
 
-	tdmr->pamt_4k_base = pamt_base[TDX_PS_4K];
-	tdmr->pamt_4k_size = pamt_size[TDX_PS_4K];
-	tdmr->pamt_2m_base = pamt_base[TDX_PS_2M];
-	tdmr->pamt_2m_size = pamt_size[TDX_PS_2M];
-	tdmr->pamt_1g_base = pamt_base[TDX_PS_1G];
-	tdmr->pamt_1g_size = pamt_size[TDX_PS_1G];
+	tdmr->pamt_4k_base = page_to_phys(pamt);
+	tdmr->pamt_2m_base = tdmr->pamt_4k_base + tdmr->pamt_4k_size;
+	tdmr->pamt_1g_base = tdmr->pamt_2m_base + tdmr->pamt_2m_size;
 
 	return 0;
 }
@@ -655,10 +697,7 @@ static __init void tdmr_do_pamt_func(struct tdmr_info *tdmr,
 	tdmr_get_pamt(tdmr, &pamt_base, &pamt_size);
 
 	/* Do nothing if PAMT hasn't been allocated for this TDMR */
-	if (!pamt_size)
-		return;
-
-	if (WARN_ON_ONCE(!pamt_base))
+	if (!pamt_base)
 		return;
 
 	pamt_func(pamt_base, pamt_size);
@@ -684,14 +723,12 @@ static __init void tdmrs_free_pamt_all(struct tdmr_info_list *tdmr_list)
 
 /* Allocate and set up PAMTs for all TDMRs */
 static __init int tdmrs_set_up_pamt_all(struct tdmr_info_list *tdmr_list,
-					struct list_head *tmb_list,
-					u16 pamt_entry_size[])
+				 struct list_head *tmb_list)
 {
 	int i, ret = 0;
 
 	for (i = 0; i < tdmr_list->nr_consumed_tdmrs; i++) {
-		ret = tdmr_set_up_pamt(tdmr_entry(tdmr_list, i), tmb_list,
-				pamt_entry_size);
+		ret = tdmr_set_up_pamt(tdmr_entry(tdmr_list, i), tmb_list);
 		if (ret)
 			goto err;
 	}
@@ -968,18 +1005,13 @@ static __init int construct_tdmrs(struct list_head *tmb_list,
 				  struct tdmr_info_list *tdmr_list,
 				  struct tdx_sys_info_tdmr *sysinfo_tdmr)
 {
-	u16 pamt_entry_size[TDX_PS_NR] = {
-		sysinfo_tdmr->pamt_4k_entry_size,
-		sysinfo_tdmr->pamt_2m_entry_size,
-		sysinfo_tdmr->pamt_1g_entry_size,
-	};
 	int ret;
 
 	ret = fill_out_tdmrs(tmb_list, tdmr_list);
 	if (ret)
 		return ret;
 
-	ret = tdmrs_set_up_pamt_all(tdmr_list, tmb_list, pamt_entry_size);
+	ret = tdmrs_set_up_pamt_all(tdmr_list, tmb_list);
 	if (ret)
 		return ret;
 
@@ -998,6 +1030,8 @@ static __init int construct_tdmrs(struct list_head *tmb_list,
 	return ret;
 }
 
+#define TDX_SYS_CONFIG_DYNAMIC_PAMT	BIT(16)
+
 static __init int config_tdx_module(struct tdmr_info_list *tdmr_list,
 				    u64 global_keyid)
 {
@@ -1026,6 +1060,12 @@ static __init int config_tdx_module(struct tdmr_info_list *tdmr_list,
 	args.rcx = __pa(tdmr_pa_array);
 	args.rdx = tdmr_list->nr_consumed_tdmrs;
 	args.r8 = global_keyid;
+
+	if (tdx_supports_dynamic_pamt(&tdx_sysinfo)) {
+		pr_info("Enable Dynamic PAMT\n");
+		args.r8 |= TDX_SYS_CONFIG_DYNAMIC_PAMT;
+	}
+
 	ret = seamcall_prerr(TDH_SYS_CONFIG, &args);
 
 	/* Free the array as it is not required anymore. */
@@ -1167,10 +1207,14 @@ static __init int init_tdx_module(void)
 	 */
 	get_online_mems();
 
-	ret = build_tdx_memlist(&tdx_memlist);
+	ret = init_pamt_refcounts();
 	if (ret)
 		goto out_put_tdxmem;
 
+	ret = build_tdx_memlist(&tdx_memlist);
+	if (ret)
+		goto err_free_pamt_refcounts;
+
 	/* Allocate enough space for constructing TDMRs */
 	ret = alloc_tdmr_list(&tdx_tdmr_list, &tdx_sysinfo.tdmr);
 	if (ret)
@@ -1220,6 +1264,8 @@ static __init int init_tdx_module(void)
 	free_tdmr_list(&tdx_tdmr_list);
 err_free_tdxmem:
 	free_tdx_memlist(&tdx_memlist);
+err_free_pamt_refcounts:
+	free_pamt_refcounts();
 	goto out_put_tdxmem;
 }
 
@@ -2003,6 +2049,288 @@ u64 tdh_phymem_page_wbinvd_hkid(u64 hkid, kvm_pfn_t pfn)
 }
 EXPORT_SYMBOL_FOR_KVM(tdh_phymem_page_wbinvd_hkid);
 
+bool tdx_supports_dynamic_pamt(const struct tdx_sys_info *sysinfo)
+{
+	return sysinfo->features.tdx_features0 & TDX_FEATURES0_DYNAMIC_PAMT &&
+	       tdx_enable_dpamt;
+}
+EXPORT_SYMBOL_FOR_KVM(tdx_supports_dynamic_pamt);
+
+static struct page *tdx_alloc_page_pamt_cache(struct tdx_pamt_cache *cache)
+{
+	struct page *page;
+
+	page = list_first_entry_or_null(&cache->page_list, struct page, lru);
+	if (page) {
+		list_del(&page->lru);
+		cache->cnt--;
+	}
+
+	return page;
+}
+
+static struct page *alloc_dpamt_page(struct tdx_pamt_cache *cache)
+{
+	if (cache)
+		return tdx_alloc_page_pamt_cache(cache);
+
+	return alloc_page(GFP_KERNEL_ACCOUNT);
+}
+
+static int alloc_pamt_array(struct page **pamt_pages, struct tdx_pamt_cache *cache)
+{
+	int i, j;
+
+	for (i = 0; i < TDX_DPAMT_ENTRY_PAGE_CNT; i++) {
+		pamt_pages[i] = alloc_dpamt_page(cache);
+		if (!pamt_pages[i])
+			goto err;
+	}
+
+	return 0;
+
+err:
+	for (j = 0; j < i; j++)
+		__free_page(pamt_pages[j]);
+
+	return -ENOMEM;
+}
+
+static void free_pamt_array(struct page **pamt_pages)
+{
+	for (int i = 0; i < TDX_DPAMT_ENTRY_PAGE_CNT; i++) {
+		/*
+		 * Reset pages unconditionally to cover cases
+		 * where they were passed to the TDX module.
+		 */
+		tdx_quirk_reset_paddr(page_to_phys(pamt_pages[i]), PAGE_SIZE);
+
+		__free_page(pamt_pages[i]);
+	}
+}
+
+/*
+ * Calculate the arg needed for operating on the DPAMT backing for
+ * a given 4KB page.
+ */
+static u64 pamt_2mb_arg(kvm_pfn_t pfn)
+{
+	/* Arg value will specify a 2MB region of physical address space. */
+	unsigned long hpa_2mb = ALIGN_DOWN(pfn << PAGE_SHIFT, PMD_SIZE);
+
+	return hpa_2mb | TDX_PS_2M;
+}
+
+/* Add PAMT backing for the 2MB region surrounding the given pfn. */
+static u64 tdh_phymem_pamt_add(kvm_pfn_t pfn, struct page **pamt_pages)
+{
+	struct tdx_module_args args = {
+		.rcx = pamt_2mb_arg(pfn),
+		.rdx = page_to_phys(pamt_pages[0]),
+		.r8 = page_to_phys(pamt_pages[1]),
+	};
+
+	return seamcall(TDH_PHYMEM_PAMT_ADD, &args);
+}
+
+/* Remove PAMT backing for the 2MB region surrounding the given pfn. */
+static u64 tdh_phymem_pamt_remove(kvm_pfn_t pfn, struct page **pamt_pages)
+{
+	struct tdx_module_args args = {
+		.rcx = pamt_2mb_arg(pfn),
+	};
+	u64 ret;
+
+	ret = seamcall_ret(TDH_PHYMEM_PAMT_REMOVE, &args);
+	if (ret)
+		return ret;
+
+	/* Copy PAMT pages out of the struct per the TDX ABI */
+	pamt_pages[0] = phys_to_page(args.rdx);
+	pamt_pages[1] = phys_to_page(args.r8);
+
+	return 0;
+}
+
+/* Serializes adding/removing PAMT memory */
+static DEFINE_SPINLOCK(pamt_lock);
+
+/* Bump PAMT refcount for the given pfn and allocate PAMT backing if needed. */
+int tdx_pamt_get(kvm_pfn_t pfn, struct tdx_pamt_cache *cache)
+{
+	struct page *pamt_pages[TDX_DPAMT_ENTRY_PAGE_CNT];
+	atomic_t *pamt_refcount;
+	u64 tdx_status;
+	int ret;
+
+	if (!tdx_supports_dynamic_pamt(&tdx_sysinfo))
+		return 0;
+
+	pamt_refcount = tdx_find_pamt_refcount(pfn);
+
+	/*
+	 * If the pamt page is already added (i.e. refcount >= 1),
+	 * then just increment the refcount.
+	 */
+	if (atomic_inc_not_zero(pamt_refcount))
+		return 0;
+
+	ret = alloc_pamt_array(pamt_pages, cache);
+	if (ret)
+		return ret;
+
+	spin_lock(&pamt_lock);
+
+	/*
+	 * Unlike tdx_pamt_put() which uses atomic_dec_and_lock() to
+	 * atomically handle the 1->0 transition, the get side has no
+	 * equivalent combined primitive for 0->1. Recheck under the
+	 * lock since another get may have already done the 0->1
+	 * transition after both saw atomic_inc_not_zero() fail.
+	 */
+	if (atomic_inc_not_zero(pamt_refcount))
+		goto out_free;
+
+	tdx_status = tdh_phymem_pamt_add(pfn, pamt_pages);
+	if (WARN_ON_ONCE(tdx_status != TDX_SUCCESS)) {
+		ret = -EIO;
+		goto out_free;
+	}
+
+	/*
+	 * The refcount is zero, and this locked path is the
+	 * only way to increase it from 0->1.
+	 */
+	atomic_set(pamt_refcount, 1);
+	spin_unlock(&pamt_lock);
+	return 0;
+
+out_free:
+	spin_unlock(&pamt_lock);
+	free_pamt_array(pamt_pages);
+
+	return ret;
+}
+EXPORT_SYMBOL_FOR_KVM(tdx_pamt_get);
+
+/* Drop PAMT refcount for the given pfn and free PAMT backing if needed. */
+void tdx_pamt_put(kvm_pfn_t pfn)
+{
+	struct page *pamt_pages[TDX_DPAMT_ENTRY_PAGE_CNT] = {};
+	atomic_t *pamt_refcount;
+	u64 tdx_status;
+
+	if (!tdx_supports_dynamic_pamt(&tdx_sysinfo))
+		return;
+
+	pamt_refcount = tdx_find_pamt_refcount(pfn);
+
+	/*
+	 * If there is more than 1 reference on the pamt page, don't
+	 * remove it yet. Just decrement the refcount.
+	 */
+	if (!atomic_dec_and_lock(pamt_refcount, &pamt_lock))
+		return;
+
+	tdx_status = tdh_phymem_pamt_remove(pfn, pamt_pages);
+
+	/*
+	 * Don't free pamt_pages as it could hold garbage when
+	 * tdh_phymem_pamt_remove() fails.  Don't panic/BUG_ON(), as
+	 * there is no risk of data corruption, but do yell loudly as
+	 * failure indicates a kernel bug, memory is being leaked, and
+	 * the dangling PAMT entry may cause future operations to fail.
+	 */
+	if (WARN_ON_ONCE(tdx_status != TDX_SUCCESS)) {
+		/*
+		 * atomic_dec_and_lock() already decremented it to 0,
+		 * but the PAMT entry still exists since REMOVE failed.
+		 */
+		atomic_set(pamt_refcount, 1);
+		goto out_unlock;
+	}
+
+	spin_unlock(&pamt_lock);
+	free_pamt_array(pamt_pages);
+	return;
+out_unlock:
+	spin_unlock(&pamt_lock);
+}
+EXPORT_SYMBOL_FOR_KVM(tdx_pamt_put);
+
+void tdx_free_pamt_cache(struct tdx_pamt_cache *cache)
+{
+	struct page *page;
+
+	while ((page = tdx_alloc_page_pamt_cache(cache)))
+		__free_page(page);
+}
+EXPORT_SYMBOL_FOR_KVM(tdx_free_pamt_cache);
+
+int tdx_topup_pamt_cache(struct tdx_pamt_cache *cache, unsigned long npages)
+{
+	if (WARN_ON_ONCE(!tdx_supports_dynamic_pamt(&tdx_sysinfo)))
+		return 0;
+
+	npages *= TDX_DPAMT_ENTRY_PAGE_CNT;
+
+	while (cache->cnt < npages) {
+		struct page *page = alloc_page(GFP_KERNEL_ACCOUNT);
+
+		if (!page)
+			return -ENOMEM;
+
+		list_add(&page->lru, &cache->page_list);
+		cache->cnt++;
+	}
+
+	return 0;
+}
+EXPORT_SYMBOL_FOR_KVM(tdx_topup_pamt_cache);
+
+/*
+ * Return a page that can be gifted to the TDX-Module for use as a "control"
+ * page, i.e. pages that are used for control structures for a given TDX
+ * guest, and thus obtain TDX protections, including PAMT tracking.
+ */
+struct page *tdx_alloc_control_page(void)
+{
+	struct page *page;
+
+	page = alloc_page(GFP_KERNEL_ACCOUNT);
+	if (!page)
+		return NULL;
+
+	if (tdx_pamt_get(page_to_pfn(page), NULL)) {
+		__free_page(page);
+		return NULL;
+	}
+
+	return page;
+}
+EXPORT_SYMBOL_FOR_KVM(tdx_alloc_control_page);
+
+/*
+ * Free a page that was gifted to the TDX-Module for use as a control
+ * page. After this, the page is no longer protected by TDX.
+ */
+void tdx_free_control_page(struct page *page)
+{
+	if (!page)
+		return;
+
+	tdx_pamt_put(page_to_pfn(page));
+	__free_page(page);
+}
+EXPORT_SYMBOL_FOR_KVM(tdx_free_control_page);
+
+static int __init tdx_dpamt_setup(char *str)
+{
+	return kstrtobool(str, &tdx_enable_dpamt) == 0;
+}
+
+__setup("tdx_dpamt=", tdx_dpamt_setup);
+
 void tdx_sys_disable(void)
 {
 	struct tdx_module_args args = {};
diff --git a/arch/x86/virt/vmx/tdx/tdx.h b/arch/x86/virt/vmx/tdx/tdx.h
index bdfd0e1e337ac0..a886c54decaadc 100644
--- a/arch/x86/virt/vmx/tdx/tdx.h
+++ b/arch/x86/virt/vmx/tdx/tdx.h
@@ -48,6 +48,8 @@
 #define TDH_SYS_CONFIG			45
 #define TDH_SYS_SHUTDOWN		52
 #define TDH_SYS_UPDATE			53
+#define TDH_PHYMEM_PAMT_ADD		58
+#define TDH_PHYMEM_PAMT_REMOVE		59
 #define TDH_SYS_DISABLE			69
 
 /*
diff --git a/arch/x86/virt/vmx/tdx/tdx_global_metadata.c b/arch/x86/virt/vmx/tdx/tdx_global_metadata.c
index e49c300f23d435..8393d2aa59dbe7 100644
--- a/arch/x86/virt/vmx/tdx/tdx_global_metadata.c
+++ b/arch/x86/virt/vmx/tdx/tdx_global_metadata.c
@@ -1,6 +1,6 @@
 // SPDX-License-Identifier: GPL-2.0
 /*
- * Automatically generated functions to read TDX global metadata.
+ * Functions to read TDX global metadata.
  *
  * This file doesn't compile on its own as it lacks of inclusion
  * of SEAMCALL wrapper primitive which reads global metadata.
@@ -33,6 +33,18 @@ static __init int get_tdx_sys_info_features(struct tdx_sys_info_features *sysinf
 	return ret;
 }
 
+static __init int get_tdx_sys_info_tdmr_dpamt(struct tdx_sys_info_tdmr *sysinfo_tdmr)
+{
+	int ret;
+	u64 val;
+
+	ret = read_sys_metadata_field(0x9100000100000013, &val);
+	if (!ret)
+		sysinfo_tdmr->pamt_page_bitmap_entry_bits = val;
+
+	return ret;
+}
+
 static __init int get_tdx_sys_info_tdmr(struct tdx_sys_info_tdmr *sysinfo_tdmr)
 {
 	int ret = 0;
@@ -129,5 +141,14 @@ static __init int get_tdx_sys_info(struct tdx_sys_info *sysinfo)
 	ret = ret ?: get_tdx_sys_info_td_ctrl(&sysinfo->td_ctrl);
 	ret = ret ?: get_tdx_sys_info_td_conf(&sysinfo->td_conf);
 
+	/*
+	 * The kernel supports using TDX without Dynamic PAMT, so
+	 * avoid reporting failure if it's not supported. Don't try
+	 * to support buggy TDX modules that advertise Dynamic PAMT
+	 * but don't expose the metadata.
+	 */
+	if (!ret && tdx_supports_dynamic_pamt(sysinfo))
+		ret = get_tdx_sys_info_tdmr_dpamt(&sysinfo->tdmr);
+
 	return ret;
 }

3/2 2026/07/26 10:55 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 17235
							output: 20
							thoughts: 933
						

Thoughts:
**Dynamic PAMT Support for TDX in KVM - Reachability Analysis**

Okay, so I've got this patch introducing "Dynamic PAMT" for Intel Trust Domain Extensions (TDX) in KVM/x86. It's a significant change, touching a lot of core files: documentation, headers, and several KVM modules (mmu, vmx, TDX specific code, etc.). It looks like it adds a new kernel parameter, `tdx_dpamt`, and a suite of new functions related to PAMT cache management (`tdx_pamt_get`, `tdx_pamt_put`, `tdx_topup_pamt_cache`, etc.). The MMU is also being updated to call `tdx_topup_external_pamt_cache`. This is all pretty substantial.

The immediate question is: Is this code path even *reachable* in a standard environment, especially when considering the context provided? TDX, as I understand it, relies on specific Intel hardware. It's a virtualization feature, so I need to think about how this interacts with QEMU and other environments. Standard QEMU doesn't emulate TDX hardware natively without KVM TDX support, and even then, it depends on actual TDX hardware to run TDX guests.

But, and this is important, Syzkaller heavily fuzzez KVM. And KVM TDX support *is* being upstreamed. So, the next thought is: Is Syzkaller fuzzing TDX? I need to check. Even if Syzkaller is fuzzing KVM, it will probably depend on TDX hardware setup to actually enable TDX. This would likely cause `tdx_hardware_setup` to fail or just not enable TDX.

Then, there's the GCE (Google Compute Engine) factor. Syzkaller runs on GCE, and GCE *does* have TDX instances (specifically, C3 instances). But does Syzkaller actually *use* those TDX instances? I need to verify that. I need to check if this code path is actually reachable within Syzkaller's setup.

If TDX is not supported by the CPU, `tdx_hardware_setup` will not enable it, so if my target hardware doesn't support TDX, I can disregard this. This may be the key.

Now, the prompt specifically mentions code paths that are impossible to reach in virtualized environments like GCE or QEMU if the hardware is not available. Then it offers examples, like vendor-specific PCIe switch, SmartNIC, or GPU drivers. I need to figure out if TDX falls into this category.

TDX is a *CPU* feature, not a PCIe card, so that example doesn't quite apply. However, it still depends on the CPU having TDX capability. So, this comes back to my previous points. Let's check if this specific implementation (using the `tdx_hardware_setup` path) is considered reachable.

I'll search for `tdx_hardware_setup` or `tdx_init_module`. That should tell me if we should consider it reachable.



4/2 2026/07/26 10:55 tool grepper
0m Args:
{
  "Expression": "tdx_hardware_setup"
}

Results:
{
  "Output": "arch/x86/kvm/vmx/main.c=24=static __init int vt_hardware_setup(void)\n--\narch/x86/kvm/vmx/main.c-31-\narch/x86/kvm/vmx/main.c:32:\treturn enable_tdx ? tdx_hardware_setup() : 0;\narch/x86/kvm/vmx/main.c-33-}\n--\narch/x86/kvm/vmx/tdx.c=3394=void tdx_hardware_unsetup(void)\n--\narch/x86/kvm/vmx/tdx.c-3398-\narch/x86/kvm/vmx/tdx.c:3399:static int __init __tdx_hardware_setup(void)\narch/x86/kvm/vmx/tdx.c-3400-{\n--\narch/x86/kvm/vmx/tdx.c-3472-\narch/x86/kvm/vmx/tdx.c:3473:int __init tdx_hardware_setup(void)\narch/x86/kvm/vmx/tdx.c-3474-{\n--\narch/x86/kvm/vmx/tdx.c-3508-\narch/x86/kvm/vmx/tdx.c:3509:\tr = __tdx_hardware_setup();\narch/x86/kvm/vmx/tdx.c-3510-\tif (r) {\n--\narch/x86/kvm/vmx/tdx.h-9-\narch/x86/kvm/vmx/tdx.h:10:int tdx_hardware_setup(void);\narch/x86/kvm/vmx/tdx.h-11-void tdx_hardware_unsetup(void);\n"
}

5/2 2026/07/26 10:55 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 19362
							output: 18
							thoughts: 38
						

Thoughts:
**Analyzing the TDX Hardware Setup**

Okay, let's trace this through. I need to understand the execution flow, especially around the TDX hardware configuration. The crucial function seems to be `tdx_hardware_setup`. I see it's definitely being called, good. `vt_hardware_setup` calls it, which gives me a starting point. Now, a critical question is the initial state: is `enable_tdx` set to true by default? That's what I really need to know to understand the system's baseline behavior. I need to confirm its default value. Knowing the default value of `enable_tdx` will significantly illuminate the usual hardware configuration profile.



6/2 2026/07/26 10:55 tool grepper
0m Args:
{
  "Expression": "enable_tdx"
}

Results:
{
  "Output": "arch/x86/kvm/vmx/main.c=16=static void vt_disable_virtualization_cpu(void)\n--\narch/x86/kvm/vmx/main.c-18-\t/* Note, TDX *and* VMX need to be disabled if TDX is enabled. */\narch/x86/kvm/vmx/main.c:19:\tif (enable_tdx)\narch/x86/kvm/vmx/main.c-20-\t\ttdx_disable_virtualization_cpu();\n--\narch/x86/kvm/vmx/main.c=24=static __init int vt_hardware_setup(void)\n--\narch/x86/kvm/vmx/main.c-31-\narch/x86/kvm/vmx/main.c:32:\treturn enable_tdx ? tdx_hardware_setup() : 0;\narch/x86/kvm/vmx/main.c-33-}\n--\narch/x86/kvm/vmx/main.c=35=static void vt_hardware_unsetup(void)\narch/x86/kvm/vmx/main.c-36-{\narch/x86/kvm/vmx/main.c:37:\tif (enable_tdx)\narch/x86/kvm/vmx/main.c-38-\t\ttdx_hardware_unsetup();\n--\narch/x86/kvm/vmx/main.c=1050=static int __init vt_init(void)\n--\narch/x86/kvm/vmx/main.c-1065-\tvcpu_align = __alignof__(struct vcpu_vmx);\narch/x86/kvm/vmx/main.c:1066:\tif (enable_tdx) {\narch/x86/kvm/vmx/main.c-1067-\t\tvcpu_size = max_t(unsigned, vcpu_size,\n--\narch/x86/kvm/vmx/tdx.c-55-\narch/x86/kvm/vmx/tdx.c:56:bool enable_tdx __ro_after_init;\narch/x86/kvm/vmx/tdx.c:57:module_param_named(tdx, enable_tdx, bool, 0444);\narch/x86/kvm/vmx/tdx.c-58-\n--\narch/x86/kvm/vmx/tdx.c=3473=int __init tdx_hardware_setup(void)\n--\narch/x86/kvm/vmx/tdx.c-3480-\narch/x86/kvm/vmx/tdx.c:3481:\tif (!enable_tdx)\narch/x86/kvm/vmx/tdx.c-3482-\t\treturn 0;\n--\narch/x86/kvm/vmx/tdx.c-3540-success_disable_tdx:\narch/x86/kvm/vmx/tdx.c:3541:\tenable_tdx = 0;\narch/x86/kvm/vmx/tdx.c-3542-\treturn 0;\n--\narch/x86/kvm/vmx/tdx.h=11=void tdx_hardware_unsetup(void);\narch/x86/kvm/vmx/tdx.h-12-\narch/x86/kvm/vmx/tdx.h:13:extern bool enable_tdx;\narch/x86/kvm/vmx/tdx.h-14-\n--\narch/x86/kvm/vmx/tdx.h=187=TDX_BUILD_TDVPS_ACCESSORS(64, STATE_NON_ARCH, state_non_arch);\n--\narch/x86/kvm/vmx/tdx.h-189-#else\narch/x86/kvm/vmx/tdx.h:190:#define enable_tdx\t0\narch/x86/kvm/vmx/tdx.h-191-\n"
}

7/2 2026/07/26 10:55 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 20644
							output: 22
							thoughts: 356
						

Thoughts:
**TDX Fuzzing Feasibility - A Quick Audit**

Okay, so `enable_tdx` maps to `kvm_intel.tdx`, that's straightforward. The question is: can we *actually* fuzz TDX? I know from experience that TDX requires specific hardware support. Standard Syzkaller GCE instances aren't likely to have that unless someone specifically set them up. And, since QEMU doesn't emulate TDX natively, that's another hurdle.

But hang on… if Syzkaller *was* running on TDX hardware (highly unlikely, but possible), then maybe? Ah, right, the prompt says to ignore things that are unreachable in virtualization, which would include emulated hardware. So, is TDX one of those “impossible” cases in GCE or QEMU?

Hold on... I'm getting ahead of myself, GCE *does* offer TDX instances (C3 series!), and QEMU/KVM *does* support TDX. So, my initial assumption was wrong! Okay, the plot thickens. Now, does *this specific patch* actually introduce any functional changes, anything worth fuzzing? Yes, it's about Dynamic PAMT support. So is it reachable?

If it's reachable, it's worth fuzzing. To figure out reachability, I need to check `tdx_supports_dynamic_pamt`, which depends on `tdx_enable_dpamt`. Ah, and *that* parameter is controlled by `tdx_dpamt=on` kernel parameter. But the prompt is asking for `EnableConfigs` not kernel parameters. Wait, that's Kconfig! I need to check the Kconfig for TDX related options now.



8/2 2026/07/26 10:55 tool grepper
0m Args:
{
  "Expression": "config INTEL_TDX_HOST"
}

Results:
{
  "Output": "arch/x86/Kconfig=1844=config X86_USER_SHADOW_STACK\n--\narch/x86/Kconfig-1863-\narch/x86/Kconfig:1864:config INTEL_TDX_HOST\narch/x86/Kconfig-1865-\tbool \"Intel Trust Domain Extensions (TDX) host support\"\n"
}

9/2 2026/07/26 10:55 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 21500
							output: 36
							thoughts: 15
						

Thoughts:
Here's my take on those thoughts, tailored for an expert audience:

**Assessing INTEL_TDX_HOST Dependencies**

Okay, time to dive in. My immediate thought is, "Let's make sure the `INTEL_TDX_HOST` dependencies are all squared away." I'm mentally running through the probable list: the hypervisor, the firmware, the specific CPU microcode revision, likely certain kernel versions... I'm looking for any obvious red flags, known incompatibilities, or overlooked prerequisites that could trip us up later. I need to be methodical here; missing even a seemingly minor dependency can lead to obscure and frustrating debugging down the line, particularly with something as complex as TDX. Best to get this foundational stuff validated early. Let's see what the documentation suggests and cross-reference that with the current platform configuration.



10/2 2026/07/26 10:55 tool grepper
0m Args:
{
  "Expression": "config INTEL_TDX_HOST",
  "PathPrefix": "arch/x86/Kconfig"
}

Results:
{
  "Output": "arch/x86/Kconfig=1844=config X86_USER_SHADOW_STACK\n--\narch/x86/Kconfig-1863-\narch/x86/Kconfig:1864:config INTEL_TDX_HOST\narch/x86/Kconfig-1865-\tbool \"Intel Trust Domain Extensions (TDX) host support\"\n"
}

11/2 2026/07/26 10:55 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 21836
							output: 37
							thoughts: 17
						

Thoughts:
Okay, let's dive into this. Here's my take on those thoughts, as if they're my own:

**Analyzing the Kconfig for INTEL_TDX_HOST**

Alright, time to get into the details. My initial thought is to pull up the Kconfig file for `INTEL_TDX_HOST`. I need a clear understanding of what’s going on with this configuration option. Knowing this is the foundation for everything else, so let's get down to brass tacks. I want to see the dependencies, the defaults, the help text – the whole shebang. This is where the core logic of the feature lives and this config is key for anything related to Intel TDX host functionality, I need to know where it fits into the broader picture. Let’s get to reading.



12/2 2026/07/26 10:55 tool read-file
0m Args:
{
  "File": "arch/x86/Kconfig",
  "FirstLine": 1860,
  "LineCount": 20
}

Results:
{
  "Contents": "1860:\t\t  See Documentation/arch/x86/shstk.rst for more information.\n1861:\t\n1862:\t\t  If unsure, say N.\n1863:\t\n1864:\tconfig INTEL_TDX_HOST\n1865:\t\tbool \"Intel Trust Domain Extensions (TDX) host support\"\n1866:\t\tdepends on CPU_SUP_INTEL\n1867:\t\tdepends on X86_64\n1868:\t\tdepends on KVM_INTEL\n1869:\t\tdepends on X86_X2APIC\n1870:\t\tselect ARCH_KEEP_MEMBLOCK\n1871:\t\tdepends on CONTIG_ALLOC\n1872:\t\tdepends on X86_MCE\n1873:\t\thelp\n1874:\t\t  Intel Trust Domain Extensions (TDX) protects guest VMs from malicious\n1875:\t\t  host and certain physical attacks.  This option enables necessary TDX\n1876:\t\t  support in the host kernel to run confidential VMs.\n1877:\t\n1878:\t\t  If unsure, say N.\n1879:\t\n"
}

13/2 2026/07/26 10:55 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 22363
							output: 23
							thoughts: 27
						

Thoughts:
Okay, here's my summary, framed as my internal thought process:

**TDX Configuration Inquiry**

Right, so I'm looking at this and it seems `INTEL_TDX_HOST` is the key configuration setting here. But, hang on a second... is there even a KVM-specific configuration for TDX? I need to clarify if this is just a general setting or if it needs to be integrated into the KVM environment. I should probably double-check the KVM documentation to see if there's a dedicated setting for TDX under the hood, or if this general setting is sufficient for enabling the feature in that context. This is something I need to fully understand before I proceed.



14/2 2026/07/26 10:56 tool grepper
0m Args:
{
  "Expression": "config KVM_INTEL_TDX"
}

Results:
{
  "Output": "arch/x86/kvm/Kconfig=122=config X86_SGX_KVM\n--\narch/x86/kvm/Kconfig-133-\narch/x86/kvm/Kconfig:134:config KVM_INTEL_TDX\narch/x86/kvm/Kconfig-135-\tbool \"Intel Trust Domain Extensions (TDX) support\"\n"
}

15/2 2026/07/26 10:56 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 22676
							output: 39
							thoughts: 23
						

Thoughts:
Okay, let's dive into this. Here's my thought process on the matter, summarized as requested:

**KVM Configuration Deep Dive**

Alright, time to get my hands dirty. I need to understand this KVM configuration and what's going on at line 134 in `arch/x86/kvm/Kconfig`. Let me pull up that file... right, the Kconfig file for the x86 KVM architecture. I’m guessing it's some sort of feature configuration option or a dependency check. I need to be sure I understand its implications. I'll need to remember to cross-reference this with relevant code in the `kvm` directory and understand how the particular config interacts with other parts of the hypervisor. This is how I build a comprehensive picture. Time to get started!



16/2 2026/07/26 10:56 tool read-file
0m Args:
{
  "File": "arch/x86/kvm/Kconfig",
  "FirstLine": 130,
  "LineCount": 20
}

Results:
{
  "Contents": " 130:\t\t  guests via a device node, e.g. /dev/sgx_vepc.\n 131:\t\n 132:\t\t  If unsure, say N.\n 133:\t\n 134:\tconfig KVM_INTEL_TDX\n 135:\t\tbool \"Intel Trust Domain Extensions (TDX) support\"\n 136:\t\tdefault y\n 137:\t\tdepends on INTEL_TDX_HOST\n 138:\t\tselect KVM_GENERIC_MEMORY_ATTRIBUTES\n 139:\t\tselect HAVE_KVM_ARCH_GMEM_POPULATE\n 140:\t\thelp\n 141:\t\t  Provides support for launching Intel Trust Domain Extensions (TDX)\n 142:\t\t  confidential VMs on Intel processors.\n 143:\t\n 144:\t\t  If unsure, say N.\n 145:\t\n 146:\tconfig KVM_AMD\n 147:\t\ttristate \"KVM for AMD processors support\"\n 148:\t\tdepends on KVM \u0026\u0026 (CPU_SUP_AMD || CPU_SUP_HYGON)\n 149:\t\thelp\n"
}

17/2 2026/07/26 10:56 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 23216
							output: 31
							thoughts: 788
						

Thoughts:
**Worth Fuzzing Decision: TDX Patch**

Okay, so I'm looking at this patch and trying to decide if it's worth fuzzing. The key here seems to be `KVM_INTEL_TDX` and `INTEL_TDX_HOST`, the relevant configs. The prompt is asking if this code can be reached, specifically, if it's "impossible to reach in virtualized environments like GCE or QEMU...". TDX is a *hardware* feature, so that's the sticking point.

Can standard QEMU hit this? Nope, QEMU doesn't emulate TDX. It *can* run as a TDX host, but only if the underlying hardware has the support. Syzkaller runs on GCE, and GCE *does* have TDX instances, so that’s a possibility. I was initially leaning toward "impossible to reach" but then realized that Syzkaller will test TDX if it can. It heavily tests KVM, and if TDX is enabled, it should be tested.

But then I saw the kernel parameter, `tdx_dpamt=on`. That’s a potential roadblock. If it’s not set, `tdx_enable_dpamt` is false, and the code path is unreachable. Syzkaller doesn't automatically pass new kernel parameters unless it's configured to do so. However, the prompt is asking about functional changes, so the parameter aspect is a secondary consideration at this stage.

Then I had to remind myself of the prompt's wording: it specifically mentions "Code paths that are impossible to reach in virtualized environments like GCE or QEMU, even when utilizing software-emulated hardware". Now, QEMU, in a *standard* configuration (without KVM and TDX hardware) can’t emulate TDX, so the feature would be inaccessible. QEMU does *not* emulate it! TDX requires physical CPU support and a TDX module loaded by the BIOS/firmware. So, in standard QEMU or even KVM without the TDX module, it *is* unreachable.

I checked if Syzkaller has any specific `qemu_tdx` config, but no. It runs KVM, and some GCE instances might have TDX, but most default ones probably don't. However, the prompt also says to consider if code requires vendor-specific PCIe switch, SmartNIC, or GPU drivers, that require physical hardware not emulated in standard QEMU. TDX is a CPU feature, so it's not the same.

And then I had a breakthrough! KVM features like SEV and TDX are *often* tested by syzkaller if they run on capable hardware. Syzkaller has pools for AMD SEV and Intel TDX, specifically for this purpose. These are core KVM/x86 code, not a vendor-specific PCIe card, so that's the difference. The functions I need to focus on are `tdx_pamt_get`, `tdx_pamt_put`, `tdx_topup_pamt_cache`, `tdx_alloc_control_page`, and `tdx_free_control_page`. These are called from KVM MMU and TDX vcpu/vm init code. So, I have to consider this in that context, and given that these are core functions of the TDX implementation, the answer is WorthFuzzing=true.



18/2 2026/07/26 10:56 tool grepper
0m Args:
{
  "Expression": "EXPORT_SYMBOL_FOR_KVM\\(tdx_pamt_get\\)"
}

Results:
{
  "Output": "arch/x86/virt/vmx/tdx/tdx.c=2159=int tdx_pamt_get(kvm_pfn_t pfn, struct tdx_pamt_cache *cache)\n--\narch/x86/virt/vmx/tdx/tdx.c-2213-}\narch/x86/virt/vmx/tdx/tdx.c:2214:EXPORT_SYMBOL_FOR_KVM(tdx_pamt_get);\narch/x86/virt/vmx/tdx/tdx.c-2215-\n"
}

19/2 2026/07/26 10:56 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 24936
							output: 26
							thoughts: 668
						

Thoughts:
**TDX Support Analysis for Fuzzing**

Okay, so I'm looking at this code and confirming that the TDX functions are, in fact, exported for KVM. The main ones I'm seeing are `tdx_pamt_get`, `tdx_pamt_put`, `tdx_topup_pamt_cache`, `tdx_alloc_control_page`, and `tdx_free_control_page`. I quickly see that `tdx_pamt_get` is called within both `tdx_sept_map_nonleaf_spte` and `tdx_sept_map_leaf_spte`, and `tdx_alloc_control_page` gets called from `__tdx_td_init` and `tdx_td_vcpu_init`. This confirms they are definitely reachable if TDX is enabled.

Now, let's look at the `EnableConfigs`. I see `INTEL_TDX_HOST`, `KVM_INTEL_TDX`, and `KVM_INTEL`. I'll verify the dependencies for `KVM_INTEL_TDX`. It depends on `INTEL_TDX_HOST`, which in turn depends on `CPU_SUP_INTEL`, `X86_64`, `KVM_INTEL`, `X86_X2APIC`, `CONTIG_ALLOC`, and `X86_MCE`. This means the `EnableConfigs` need to include both `INTEL_TDX_HOST` and `KVM_INTEL_TDX` to function.

The question then is, is there any reason to return `WorthFuzzing=false`? The concern is "Code paths that are impossible to reach in virtualized environments like GCE or QEMU." Can TDX be reached in QEMU? Well, if QEMU is run with KVM on a TDX-capable host, then yes. If it's running with TCG, then no. Syzkaller runs QEMU with KVM, so if the host has TDX, it's reachable. The crucial point is that TDX, like SEV or SGX, is generally considered reachable because Syzkaller can be deployed on hardware that supports these CPU features. Vendor-specific PCIe cards are explicitly considered unreachable, but TDX is a CPU feature that is available in standard cloud VMs (like GCP C3 instances), so this is a crucial distinction. Therefore, TDX is absolutely reachable.

Let's double-check `FocusSymbols`. I see the same functions as before: `tdx_pamt_get`, `tdx_pamt_put`, `tdx_topup_pamt_cache`, `tdx_alloc_control_page`, `tdx_free_control_page`, plus `tdx_supports_dynamic_pamt` and `tdx_topup_external_pamt_cache` (which is static in `arch/x86/kvm/vmx/tdx.c`). Need to review `tdx_topup_external_pamt_cache`.



20/2 2026/07/26 10:56 tool grepper
0m Args:
{
  "Expression": "tdx_topup_external_pamt_cache"
}

Results:
{
  "Output": "arch/x86/kvm/vmx/tdx.c=1617=void tdx_load_mmu_pgd(struct kvm_vcpu *vcpu, hpa_t root_hpa, int pgd_level)\n--\narch/x86/kvm/vmx/tdx.c-1627-\narch/x86/kvm/vmx/tdx.c:1628:static int tdx_topup_external_pamt_cache(struct kvm_vcpu *vcpu, int min_nr_spts)\narch/x86/kvm/vmx/tdx.c-1629-{\n--\narch/x86/kvm/vmx/tdx.c=3473=int __init tdx_hardware_setup(void)\n--\narch/x86/kvm/vmx/tdx.c-3534-\tif (tdx_supports_dynamic_pamt(tdx_sysinfo))\narch/x86/kvm/vmx/tdx.c:3535:\t\tvt_x86_ops.topup_external_cache = tdx_topup_external_pamt_cache;\narch/x86/kvm/vmx/tdx.c-3536-\n"
}

21/2 2026/07/26 10:56 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 26638
							output: 33
							thoughts: 115
						

Thoughts:
**Analyzing the Exposed Functions**

Okay, so I'm looking at this code and, right off the bat, I see something static. Given my usual preferences for clarity and testability, I immediately think that we should exclude it from the `FocusSymbols` list. Non-static is usually better.

Now, let's break down these exported functions. We have `tdx_pamt_get`, `tdx_pamt_put`, `tdx_topup_pamt_cache`, `tdx_alloc_control_page`, and `tdx_free_control_page`. That's a solid set, a good start. I'm noting the clear get/put pattern here, and the control page allocation/deallocation pair. The cache top-up function is interesting; it suggests a performance-focused design.

But hold on, are those *all* the exported functions? I need to be absolutely sure I haven't missed anything crucial. Ah, yes, there are two more: `tdx_supports_dynamic_pamt` and `tdx_free_pamt_cache`. *Dynamic pamt support* is an interesting addition, and a cache deallocation function means a complete memory lifecycle is accounted for. Okay, with all of this information, I can move on to the next step.



22/2 2026/07/26 10:56 tool grepper
0m Args:
{
  "Expression": "EXPORT_SYMBOL_FOR_KVM\\(tdx_free_pamt_cache\\)"
}

Results:
{
  "Output": "arch/x86/virt/vmx/tdx/tdx.c=2261=void tdx_free_pamt_cache(struct tdx_pamt_cache *cache)\n--\narch/x86/virt/vmx/tdx/tdx.c-2267-}\narch/x86/virt/vmx/tdx/tdx.c:2268:EXPORT_SYMBOL_FOR_KVM(tdx_free_pamt_cache);\narch/x86/virt/vmx/tdx/tdx.c-2269-\n"
}

23/2 2026/07/26 10:56 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 27222
							output: 40
							thoughts: 377
						

Thoughts:
**My Analysis of the TDX Dynamic PAMT Code**

Okay, so I'm looking at `tdx_free_pamt_cache`, specifically. I've got a list of focus symbols in mind: `tdx_pamt_get`, `tdx_pamt_put`, `tdx_topup_pamt_cache`, `tdx_free_pamt_cache`, `tdx_alloc_control_page`, and `tdx_free_control_page`. My initial question is whether setting `tdx_dpamt=on` is a prerequisite for this code path.

Right, `tdx_supports_dynamic_pamt` checks `tdx_enable_dpamt`, which *is* set by the `tdx_dpamt=on` kernel parameter. If the parameter is absent, `tdx_supports_dynamic_pamt` returns false, and a large portion of this dynamic PAMT logic is bypassed. But wait, `tdx_alloc_control_page` *calls* `tdx_pamt_get`. Now I'm understanding; even if `tdx_supports_dynamic_pamt` is false, `tdx_pamt_get` will still be called, but it will immediately return 0. So, this code *is* reachable, even without `tdx_dpamt=on`, but the dynamic PAMT features will only be active if the parameter is actually set.

Now, the important question is: is this a potential issue for fuzzing? Thinking it through, Syzkaller *can* be configured to pass kernel parameters, so it's perfectly capable of setting `tdx_dpamt=on`. Given that, and the fact that this is core kernel functionality, I'm confident we should mark this as `WorthFuzzing=true`.

Just to cover all the bases, let's quickly review the EnableConfigs that come into play here. I'm seeing `INTEL_TDX_HOST` and `KVM_INTEL_TDX`. Wait... I need to double-check: Does `KVM_INTEL_TDX` have dependencies I need to take into consideration? Right, back to `arch/x86/kvm/Kconfig` to refresh my memory...



24/2 2026/07/26 10:56 tool grepper
0m Args:
{
  "Expression": "config KVM_INTEL_TDX",
  "PathPrefix": "arch/x86/kvm/Kconfig"
}

Results:
{
  "Output": "arch/x86/kvm/Kconfig=122=config X86_SGX_KVM\n--\narch/x86/kvm/Kconfig-133-\narch/x86/kvm/Kconfig:134:config KVM_INTEL_TDX\narch/x86/kvm/Kconfig-135-\tbool \"Intel Trust Domain Extensions (TDX) support\"\n"
}

25/2 2026/07/26 10:56 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 28245
							output: 220
							thoughts: 753
						

Thoughts:
**My Analysis of the TDX KVM Patch**

Okay, so this patch introduces support for the Dynamic Physical Address Metadata Table (PAMT) for Intel TDX within the KVM/x86 environment.  I need to confirm my understanding of the relevant configurations: it's clear that `KVM_INTEL_TDX` depends on `INTEL_TDX_HOST`, so those are the key configurations I'll need to enable for testing.

Now, let's nail down the functions that are modified or added by this patch, so I can focus my fuzzing efforts.  I've identified these as the most likely areas of concern, based on the text: `tdx_pamt_get`, `tdx_pamt_put`, `tdx_topup_pamt_cache`, `tdx_free_pamt_cache`, `tdx_alloc_control_page`, and `tdx_free_control_page`. They are exported for KVM.

I've also scanned `arch/x86/virt/vmx/tdx/tdx.c` for any other functions that were modified or introduced, but they appear to be static helpers. These are the ones I found: `init_pamt_refcounts`, `free_pamt_refcounts`, `tdx_find_pamt_refcount`, `tdmr_get_pamt_bitmap_sz`, `tdmr_get_pamt_sz`, `tdmr_set_up_pamt`, `tdmrs_set_up_pamt_all`, `config_tdx_module`, `init_tdx_module`, `tdx_alloc_page_pamt_cache`, `alloc_dpamt_page`, `alloc_pamt_array`, `free_pamt_array`, `pamt_2mb_arg`, `tdh_phymem_pamt_add`, `tdh_phymem_pamt_remove`.

My main concern with the exported functions is that they are likely to be used by KVM code, and are thus more likely to be subject to a range of inputs. Since this patch introduces dynamic allocation and freeing of PAMT memory, I have to ensure that memory management is correct in this TDX implementation and that memory leaks and crashes are prevented in TDX host support.

The patch author explicitly noted to exclude code paths unreachable in virtualized environments.  However, this TDX feature, unlike vendor-specific hardware drivers, is definitely reachable in virtualized environments like Google Cloud Engine (GCE) if the hardware supports TDX. Therefore, it's absolutely worth fuzzing. I can reproduce a TDX-capable instance in a virtualized environment.

Here’s my reasoning succinctly: This patch brings Dynamic PAMT support to TDX in KVM/x86, enabling dynamic allocation and freeing of PAMT memory pages for guests, touching KVM MMU and TDX initialization. It *is* reachable in virtualized environments with TDX hardware, specifically in TDX-capable instances, necessitating fuzzing to guarantee correct memory management and avoid leaks or crashes in TDX host support.

So, here's my plan:

*   Enable Configurations: `INTEL_TDX_HOST`, `KVM_INTEL_TDX`
*   Focus Symbols for fuzzing: `tdx_pamt_get`, `tdx_pamt_put`, `tdx_topup_pamt_cache`, `tdx_free_pamt_cache`, `tdx_alloc_control_page`, `tdx_free_control_page`

I'll skip `tdx_supports_dynamic_pamt` as it looks like a simple getter.

Now, I'm ready to `set-results`.



26/2 2026/07/26 10:56 tool set-results
0m Args:
{
  "EnableConfigs": [
    "INTEL_TDX_HOST",
    "KVM_INTEL_TDX"
  ],
  "FocusSymbols": [
    "tdx_pamt_get",
    "tdx_pamt_put",
    "tdx_topup_pamt_cache",
    "tdx_free_pamt_cache",
    "tdx_alloc_control_page",
    "tdx_free_control_page"
  ],
  "Reasoning": "The patch introduces Dynamic Physical Address Metadata Table (PAMT) support for Intel TDX in KVM/x86. It adds functional core kernel logic to dynamically allocate and free PAMT memory pages for TDX guests, modifying KVM MMU and TDX initialization paths. This is reachable in virtualized environments running on TDX-capable hardware (e.g., GCP C3 instances) and should be fuzzed to ensure memory management correctness and prevent leaks or crashes in the TDX host support.",
  "WorthFuzzing": true
}

Results:
{
  "EnableConfigs": [
    "INTEL_TDX_HOST",
    "KVM_INTEL_TDX"
  ],
  "FocusSymbols": [
    "tdx_pamt_get",
    "tdx_pamt_put",
    "tdx_topup_pamt_cache",
    "tdx_free_pamt_cache",
    "tdx_alloc_control_page",
    "tdx_free_control_page"
  ],
  "Reasoning": "The patch introduces Dynamic Physical Address Metadata Table (PAMT) support for Intel TDX in KVM/x86. It adds functional core kernel logic to dynamically allocate and free PAMT memory pages for TDX guests, modifying KVM MMU and TDX initialization paths. This is reachable in virtualized environments running on TDX-capable hardware (e.g., GCP C3 instances) and should be fuzzed to ensure memory management correctness and prevent leaks or crashes in the TDX host support.",
  "WorthFuzzing": true
}

LLM Calls Summary:
Total Calls Total Tokens Avg Tokens Total Duration (Seconds) Avg Duration (Seconds)
Tool Calls Summary:
Total Calls Total Duration (Seconds) Avg Duration (Seconds)