[RFC 1/6] KVM: arm64: Introduce TLBID virtualization infrastructure

Jinqian Yang yangjinqian1 at huawei.com
Thu Oct 29 23:56:12 PDT 2026


struct tlbidomain, embedded in kvm_arch, holds the per-VM vTLBID
state; kvm_arm_init_tlbidomain()/free_tlbid_vdomain() manage it over
the VM lifetime, and kvm_arm_tlbidomain_vcpu_init() defaults each
vCPU to vDomain0.

The UAPI adds three interfaces: KVM_CAP_ARM_TLBIDOMAIN lets userspace
probe the feature, KVM_ARM_GET_VDOMAIN_NUM returns the number of
vDomains available to the VM, and KVM_ARM_VCPU_SET_VDOMAIN sets each
vCPU's vDomain membership bitmap. All three are documented in
Documentation/virt/kvm/api.rst.

The feature is VHE-only: VTLBIDn_EL2 cannot be written from EL1.

Signed-off-by: Jinqian Yang <yangjinqian1 at huawei.com>
Signed-off-by: Zhou Wang <wangzhou1 at hisilicon.com>
---
 Documentation/virt/kvm/api.rst    |  59 ++++++++
 arch/arm64/include/asm/kvm_host.h |  55 +++++++
 arch/arm64/kvm/Kconfig            |  12 ++
 arch/arm64/kvm/arm.c              | 241 +++++++++++++++++++++++++++++-
 include/uapi/linux/kvm.h          |  14 ++
 5 files changed, 380 insertions(+), 1 deletion(-)

diff --git a/Documentation/virt/kvm/api.rst b/Documentation/virt/kvm/api.rst
index e0430cc750c9..d8a2854e32b1 100644
--- a/Documentation/virt/kvm/api.rst
+++ b/Documentation/virt/kvm/api.rst
@@ -6665,6 +6665,49 @@ significant bit):
 
 .. _kvm_run:
 
+4.146 KVM_ARM_GET_VDOMAIN_NUM
+------------------------------
+
+:Capability: KVM_CAP_ARM_TLBIDOMAIN
+:Architectures: arm64
+:Type: vm ioctl
+:Parameters: struct kvm_arm_get_vdomain (out)
+:Returns: 0 on success, < 0 on error
+
+Parameters are specified via the following structure::
+
+	struct kvm_arm_get_vdomain {
+		__u8 max_vdomains;
+	};
+
+The ``max_vdomains`` field is the maximum number of vDomains supported
+by the virtual TLBID implementation. This value is derived from
+TLBIDIDR_EL1.NVIS and is capped at 32.
+
+4.147 KVM_ARM_VCPU_SET_VDOMAIN
+------------------------------
+
+:Capability: KVM_CAP_ARM_TLBIDOMAIN
+:Architectures: arm64
+:Type: vcpu ioctl
+:Parameters: struct kvm_arm_set_vdomain (in)
+:Returns: 0 on success, < 0 on error
+
+This ioctl allows KVM to obtain TLBI Domain of Guest.
+
+Parameters are specified via the following structure::
+
+	struct kvm_arm_set_vdomain {
+		__u32 vdomain_bitmap;
+		__u8 num_vdomains;
+	};
+
+The ``vdomain_bitmap`` field is a bitmap, where '1' indicates that vDomain
+includes this vCPU. For example, ``vCPU0:  vdomain_bitmap=0b1011`` indicates
+that vCPU0 included in vDomain(0, 1, 3).
+
+The ``num_vdomains`` field is the number of vDomains.
+
 5. The kvm_run structure
 ========================
 
@@ -9056,6 +9099,22 @@ enabled, cmma can't be enabled anymore and pfmfi and the storage key
 interpretation are disabled. If cmma has already been enabled or the
 hpage_2g module parameter is not set to 1, -EINVAL is returned.
 
+7.48 KVM_CAP_ARM_TLBIDOMAIN
+-------------------------------------
+
+:Architectures: arm64
+:Target: VM
+:Parameters: None
+:Returns: 0 on success, negative value on error
+
+This capability enables TLBID virtualization, which optimizes tlbi broadcast
+range to avoid unnecessary broadcasts.
+
+When this capability is enabled, KVM can obtain the vDomain bitmaps of VM. When
+a vCPU is loaded, KVM writes the mapping between the vDomain and the pDomain
+into the VTLBID(n). If guest enabled TLBID, hardware will convert vDomain id to
+pDomain id for TLBI broadcast.
+
 8. Other capabilities.
 ======================
 
diff --git a/arch/arm64/include/asm/kvm_host.h b/arch/arm64/include/asm/kvm_host.h
index 27fe0cd5b2d7..1e19e197d736 100644
--- a/arch/arm64/include/asm/kvm_host.h
+++ b/arch/arm64/include/asm/kvm_host.h
@@ -55,6 +55,7 @@
 #define KVM_REQ_GUEST_HYP_IRQ_PENDING	KVM_ARCH_REQ(9)
 #define KVM_REQ_MAP_L1_VNCR_EL2		KVM_ARCH_REQ(10)
 #define KVM_REQ_VGIC_PROCESS_UPDATE	KVM_ARCH_REQ(11)
+#define KVM_REQ_RELOAD_VTLBID	KVM_ARCH_REQ(12)
 
 #define KVM_DIRTY_LOG_MANUAL_CAPS   (KVM_DIRTY_LOG_MANUAL_PROTECT_ENABLE | \
 				     KVM_DIRTY_LOG_INITIALLY_SET)
@@ -306,6 +307,56 @@ enum fgt_group_id {
 	__NR_FGT_GROUP_IDS__
 };
 
+#define NIS_RANGE_MIN_1_8 1
+#define NIS_RANGE_MAX_1_8 8
+#define NIS_RANGE_MIN_9_16 9
+#define NIS_RANGE_MAX_9_16 16
+#define VTLBID_TD_WIDTH_NIS_1_8 8
+#define VTLBID_TD_WIDTH_NIS_9_16 16
+#define VTLBID_EL2_WIDTH 64
+/*
+ * Cap the supported NVIS so per-vDomain bitmaps have a compile-time
+ * upper bound; the hardware field itself is 5 bits wide.
+ */
+#define KVM_VTLBID_MAX_NVIS		5
+#define KVM_VTLBID_MAX_VDOMAINS		BIT(KVM_VTLBID_MAX_NVIS)
+
+struct vcpu_tlbid_data {
+	/* Node in per-pCPU vCPU list of tlbidomain */
+	struct list_head pcpu_node;
+	/* Store vDomain bitmap, indexed by vCPU ID */
+	u32 vdomain_bitmap;
+	/* Record the last pCPU where vCPU was running, indexed by vCPU ID */
+	int last_pcpu;
+};
+
+struct tlbidomain {
+	/* Using xarray to dynamically allocate vCPU-related TLBID info */
+	struct xarray vcpu_data_array;
+	/* The number of vDomains */
+	int num_domains;
+	/* Number of vDomains representable in the VTLBIDn registers */
+	int max_vdomains;
+	/* Mapping of pDomain to vDomain, indexed by vDomain ID */
+	int *domain_map;
+	/* Record the bitmap of pCPUs mapped to vDomain, indexed by vDomain ID */
+	cpumask_var_t *vdomain_cpumasks;
+	/* Whether virt-TLBID is enabled by KVM */
+	bool kvm_tlbid_enabled;
+	/* Whether virt-TLBID is enabled by Guest */
+	bool guest_tlbid_enabled;
+	/* TLBIDIDR.NVIS */
+	u8 nvis;
+	/* TLBIDIDR.NIS */
+	u8 nis;
+	/* Guest-visible TLBIDIDR value (NIS/NOS carry virtual domain counts) */
+	u64 tlbididr_val;
+	/* Used to prevent concurrent modifications to the domain mapping. */
+	spinlock_t tlbid_lock;
+	/* Per-pCPU list of vCPUs whose last_pcpu equals this pCPU */
+	struct list_head *pcpu_vcpu_list;
+};
+
 struct kvm_arch {
 	struct kvm_s2_mmu mmu;
 
@@ -427,6 +478,10 @@ struct kvm_arch {
 	/* Nested virtualization info */
 	struct dentry *debugfs_nv_dentry;
 #endif
+
+#ifdef CONFIG_KVM_ARM_VTLBID
+	struct tlbidomain tlbidomain;
+#endif
 };
 
 struct kvm_vcpu_fault_info {
diff --git a/arch/arm64/kvm/Kconfig b/arch/arm64/kvm/Kconfig
index 449154f9a485..c54ce82d31fc 100644
--- a/arch/arm64/kvm/Kconfig
+++ b/arch/arm64/kvm/Kconfig
@@ -104,4 +104,16 @@ config PKVM_STACKTRACE
 
 endif # NVHE_EL2_DEBUG
 endif # KVM
+config KVM_ARM_VTLBID
+	bool "Virtual TLBID support for KVM guests"
+	depends on KVM && ARM64_TLBID
+	default n
+	help
+	  Enable virtualization of FEAT_TLBID so that each KVM guest can
+	  use TLBI Domains independently of the host. The host TLBID
+	  hardware is partitioned per-VM through VTLBID_EL2 registers
+	  managed on the vCPU load/put path.
+
+	  If unsure, say N.
+
 endif # VIRTUALIZATION
diff --git a/arch/arm64/kvm/arm.c b/arch/arm64/kvm/arm.c
index 8b080804bc90..7292e3e0ec1c 100644
--- a/arch/arm64/kvm/arm.c
+++ b/arch/arm64/kvm/arm.c
@@ -214,6 +214,119 @@ static int kvm_arm_default_max_vcpus(void)
 	return vgic_present ? kvm_vgic_get_max_vcpus() : KVM_MAX_VCPUS;
 }
 
+#ifdef CONFIG_KVM_ARM_VTLBID
+static int kvm_arm_init_tlbidomain(struct kvm *kvm)
+{
+	struct tlbidomain *vdomain = &kvm->arch.tlbidomain;
+	int i, max_vdomains, pdomain_bits;
+	u64 tlbididr;
+
+	if (!system_supports_tlbid() || !has_vhe())
+		return 0;
+
+	tlbididr = read_sanitised_ftr_reg(SYS_TLBIDIDR_EL1);
+
+	vdomain->nis = FIELD_GET(TLBIDIDR_EL1_NIS_MASK, tlbididr);
+	vdomain->nvis = min_t(u8, FIELD_GET(TLBIDIDR_EL1_NVIS_MASK, tlbididr),
+			      KVM_VTLBID_MAX_NVIS);
+	vdomain->num_domains = -1;
+	/*
+	 * The four VTLBIDn registers hold at most 4 * 64 / pdomain_bits
+	 * vDomain fields. Disable vTLBID altogether when NIS is outside
+	 * the supported ranges (no useful ISH domains).
+	 */
+	if (vdomain->nis >= NIS_RANGE_MIN_1_8 && vdomain->nis <= NIS_RANGE_MAX_1_8)
+		pdomain_bits = VTLBID_TD_WIDTH_NIS_1_8;
+	else if (vdomain->nis >= NIS_RANGE_MIN_9_16 && vdomain->nis <= NIS_RANGE_MAX_9_16)
+		pdomain_bits = VTLBID_TD_WIDTH_NIS_9_16;
+	else
+		return 0;
+	max_vdomains = min_t(int, 1 << vdomain->nvis,
+			     4 * VTLBID_EL2_WIDTH / pdomain_bits);
+	vdomain->max_vdomains = max_vdomains;
+
+	kvm_info("TLBIDIDR: 0x%llx, NIS: %d, NVIS: %d\n", tlbididr, vdomain->nis, vdomain->nvis);
+
+	xa_init(&vdomain->vcpu_data_array);
+
+	vdomain->domain_map = kcalloc(max_vdomains, sizeof(*vdomain->domain_map),
+				      GFP_KERNEL);
+	if (!vdomain->domain_map)
+		goto destroy_vcpu_data_array;
+
+	vdomain->vdomain_cpumasks = kcalloc(max_vdomains,
+					    sizeof(cpumask_var_t),
+					    GFP_KERNEL);
+	if (!vdomain->vdomain_cpumasks)
+		goto free_domain_map;
+
+	for (i = 0; i < max_vdomains; i++) {
+		if (!alloc_cpumask_var(&vdomain->vdomain_cpumasks[i], GFP_KERNEL))
+			goto free_vdomain_cpumasks;
+		cpumask_clear(vdomain->vdomain_cpumasks[i]);
+	}
+
+	for (i = 0; i < max_vdomains; i++)
+		vdomain->domain_map[i] = -1;
+
+	spin_lock_init(&vdomain->tlbid_lock);
+
+	vdomain->pcpu_vcpu_list = kcalloc(nr_cpu_ids,
+					  sizeof(*vdomain->pcpu_vcpu_list),
+					  GFP_KERNEL);
+	if (!vdomain->pcpu_vcpu_list)
+		goto free_vdomain_cpumasks;
+
+	for (i = 0; i < nr_cpu_ids; i++)
+		INIT_LIST_HEAD(&vdomain->pcpu_vcpu_list[i]);
+
+	/* Enable vTLBID only after all resources are allocated. */
+	vdomain->kvm_tlbid_enabled = true;
+
+	return 0;
+
+free_vdomain_cpumasks:
+	while (--i >= 0)
+		free_cpumask_var(vdomain->vdomain_cpumasks[i]);
+	kfree(vdomain->vdomain_cpumasks);
+free_domain_map:
+	kfree(vdomain->domain_map);
+destroy_vcpu_data_array:
+	xa_destroy(&vdomain->vcpu_data_array);
+
+	kvm_err("vTLBID init failed with -ENOMEM, feature disabled\n");
+	return -ENOMEM;
+}
+
+static void free_tlbid_vdomain(struct kvm *kvm)
+{
+	struct tlbidomain *vdomain = &kvm->arch.tlbidomain;
+	struct vcpu_tlbid_data *data;
+	int i, max_vdomains;
+	unsigned long index;
+
+	if (!vdomain->kvm_tlbid_enabled)
+		return;
+
+	max_vdomains = vdomain->max_vdomains;
+
+	kfree(vdomain->domain_map);
+
+	for (i = 0; i < max_vdomains; i++)
+		free_cpumask_var(vdomain->vdomain_cpumasks[i]);
+	kfree(vdomain->vdomain_cpumasks);
+
+	kfree(vdomain->pcpu_vcpu_list);
+
+	xa_for_each(&vdomain->vcpu_data_array, index, data)
+		kfree(data);
+	xa_destroy(&vdomain->vcpu_data_array);
+}
+#else
+static inline int kvm_arm_init_tlbidomain(struct kvm *kvm) { return 0; }
+static inline void free_tlbid_vdomain(struct kvm *kvm) { }
+#endif
+
 /**
  * kvm_arch_init_vm - initializes a VM data structure
  * @kvm:	pointer to the KVM struct
@@ -276,6 +389,8 @@ int kvm_arch_init_vm(struct kvm *kvm, unsigned long type)
 
 	bitmap_zero(kvm->arch.vcpu_features, KVM_VCPU_MAX_FEATURES);
 
+	kvm_arm_init_tlbidomain(kvm);
+
 	return 0;
 
 err_uninit_mmu:
@@ -332,6 +447,8 @@ void kvm_arch_destroy_vm(struct kvm *kvm)
 	kvm_uninit_stage2_mmu(kvm);
 	kvm_destroy_mpidr_data(kvm);
 
+	free_tlbid_vdomain(kvm);
+
 	kfree(kvm->arch.sysreg_masks);
 	kvm_destroy_vcpus(kvm);
 
@@ -1731,6 +1848,52 @@ static int kvm_vcpu_set_target(struct kvm_vcpu *vcpu,
 	return 0;
 }
 
+#ifdef CONFIG_KVM_ARM_VTLBID
+static int kvm_arm_tlbidomain_vcpu_init(struct kvm_vcpu *vcpu)
+{
+	struct tlbidomain *vdomain;
+	struct vcpu_tlbid_data *data;
+	int ret;
+
+	if (!system_supports_tlbid())
+		return 0;
+
+	data = kzalloc_obj(*data, GFP_KERNEL);
+	if (!data)
+		return -ENOMEM;
+
+	/*
+	 * When the host has enabled TLBID but the guest has not,
+	 * KVM can map vDomain0 to pDomainN to optimize the TLBI
+	 * instructions for the guest. Therefore, the value is set
+	 * to 0x1, indicating that the vCPU is in vDomain0. If QEMU
+	 * calls KVM_ARM_VCPU_SET_VDOMAIN to configure vdomains, this
+	 * value will be overwritten to support the guest enabling
+	 * TLBID.
+	 */
+	data->vdomain_bitmap = 0x1;
+	data->last_pcpu = -1;
+	INIT_LIST_HEAD(&data->pcpu_node);
+
+	vdomain = &vcpu->kvm->arch.tlbidomain;
+	if (!vdomain->kvm_tlbid_enabled) {
+		kfree(data);
+		return 0;
+	}
+
+	ret = xa_insert(&vdomain->vcpu_data_array,
+			vcpu->vcpu_idx, data, GFP_KERNEL);
+	if (ret) {
+		kfree(data);
+		return ret == -EBUSY ? 0 : ret;
+	}
+
+	return 0;
+}
+#else
+static inline int kvm_arm_tlbidomain_vcpu_init(struct kvm_vcpu *vcpu) { return 0; }
+#endif
+
 static int kvm_arch_vcpu_ioctl_vcpu_init(struct kvm_vcpu *vcpu,
 					 struct kvm_vcpu_init *init)
 {
@@ -1781,7 +1944,9 @@ static int kvm_arch_vcpu_ioctl_vcpu_init(struct kvm_vcpu *vcpu,
 
 	spin_unlock(&vcpu->arch.mp_state_lock);
 
-	return 0;
+	ret = kvm_arm_tlbidomain_vcpu_init(vcpu);
+
+	return ret;
 }
 
 static int kvm_arm_vcpu_set_attr(struct kvm_vcpu *vcpu,
@@ -1852,6 +2017,59 @@ static int kvm_arm_vcpu_set_events(struct kvm_vcpu *vcpu,
 	return __kvm_arm_vcpu_set_events(vcpu, events);
 }
 
+#ifdef CONFIG_KVM_ARM_VTLBID
+static int kvm_arm_vcpu_set_vdomain(struct kvm_vcpu *vcpu,
+				    struct kvm_arm_set_vdomain *vdomain)
+{
+	struct tlbidomain *td = &vcpu->kvm->arch.tlbidomain;
+	u32 vdomain_bitmap = vdomain->vdomain_bitmap;
+	u8 num_vdomains = vdomain->num_vdomains;
+	struct vcpu_tlbid_data *data;
+
+	if (!td->kvm_tlbid_enabled)
+		return -EINVAL;
+
+	/*
+	 * The guest-visible ID registers and the FGU masks are frozen on
+	 * the first vCPU run; configuring vDomains afterwards would be
+	 * silently ineffective.
+	 */
+	if (kvm_vm_has_ran_once(vcpu->kvm))
+		return -EBUSY;
+
+	if (num_vdomains > td->max_vdomains)
+		return -EINVAL;
+
+	if (td->num_domains != -1 && td->num_domains != num_vdomains)
+		return -EINVAL;
+
+	if (num_vdomains < 32 && (vdomain_bitmap >> num_vdomains))
+		return -EINVAL;
+
+	data = xa_load(&td->vcpu_data_array, vcpu->vcpu_idx);
+	if (!data) {
+		kvm_err("vCPU%d: Failed to load vcpu_tlbid_data\n", vcpu->vcpu_idx);
+		return -EINVAL;
+	}
+
+	spin_lock(&td->tlbid_lock);
+	data->vdomain_bitmap = vdomain_bitmap;
+	td->num_domains = num_vdomains;
+	td->guest_tlbid_enabled = true;
+	spin_unlock(&td->tlbid_lock);
+
+	kvm_info("vCPU%d: vDomain bitmap: 0x%x\n", vcpu->vcpu_idx, vdomain_bitmap);
+
+	return 0;
+}
+#else
+static inline int kvm_arm_vcpu_set_vdomain(struct kvm_vcpu *vcpu,
+					   struct kvm_arm_set_vdomain *vdomain)
+{
+	return -EINVAL;
+}
+#endif
+
 long kvm_arch_vcpu_ioctl(struct file *filp,
 			 unsigned int ioctl, unsigned long arg)
 {
@@ -2031,6 +2249,27 @@ static int kvm_vm_set_attr(struct kvm *kvm, struct kvm_device_attr *attr)
 	}
 }
 
+#ifdef CONFIG_KVM_ARM_VTLBID
+static int kvm_arm_get_vdomain_num(struct kvm *kvm,
+				   struct kvm_arm_get_vdomain *vdomain)
+{
+	struct tlbidomain *td = &kvm->arch.tlbidomain;
+
+	if (!td->kvm_tlbid_enabled)
+		return -EINVAL;
+
+	vdomain->max_vdomains = td->max_vdomains;
+
+	return 0;
+}
+#else
+static inline int kvm_arm_get_vdomain_num(struct kvm *kvm,
+					  struct kvm_arm_get_vdomain *vdomain)
+{
+	return -EINVAL;
+}
+#endif
+
 int kvm_arch_vm_ioctl(struct file *filp, unsigned int ioctl, unsigned long arg)
 {
 	struct kvm *kvm = filp->private_data;
diff --git a/include/uapi/linux/kvm.h b/include/uapi/linux/kvm.h
index ac2d77d14963..030c76f1689e 100644
--- a/include/uapi/linux/kvm.h
+++ b/include/uapi/linux/kvm.h
@@ -999,6 +999,7 @@ struct kvm_enable_cap {
 #define KVM_CAP_S390_HPAGE_2G 249
 #define KVM_CAP_PPC_COMPAT_CAPS 250
 #define KVM_CAP_ARM_PMU_V3_STRICT 251
+#define KVM_CAP_ARM_TLBIDOMAIN 252
 
 struct kvm_irq_routing_irqchip {
 	__u32 irqchip;
@@ -1674,4 +1675,17 @@ struct kvm_pre_fault_memory {
 	__u64 padding[5];
 };
 
+struct kvm_arm_get_vdomain {
+	__u8 max_vdomains;
+};
+
+#define KVM_ARM_GET_VDOMAIN_NUM	_IOR(KVMIO, 0xd7, struct kvm_arm_get_vdomain)
+
+struct kvm_arm_set_vdomain {
+	__u32 vdomain_bitmap;
+	__u8 num_vdomains;
+};
+
+#define KVM_ARM_VCPU_SET_VDOMAIN	_IOW(KVMIO, 0xd8, struct kvm_arm_set_vdomain)
+
 #endif /* __LINUX_KVM_H */
-- 
2.33.0




More information about the linux-arm-kernel mailing list