[PATCH v14 4/5] x86/sev: Perform RMP optimizations asynchronously

From: Ashish Kalra

Date: Thu Sep 10 2026 - 18:02:03 EST


From: Ashish Kalra <ashish.kalra@xxxxxxx>

When SNP is enabled, all writes to memory are checked to ensure memory
integrity. This imposes performance overhead on the whole system.

RMPOPT is a new instruction that minimizes the performance overhead of
RMP checks on the hypervisor and on non-SNP guests by allowing RMP
checks to be skipped for 1GB regions of memory that are known not to
contain any SNP guest memory.

Add support for performing RMP optimizations asynchronously using a
dedicated workqueue.

At RMP initialization time, run an optimization pass over all physical
memory (up to 2TB of system RAM, starting from the lowest physical
memory address aligned down to a 1GB boundary), skipping RMP checks for
1GB regions that do not contain SNP guest memory (excluding preassigned
pages such as the RMP table and firmware pages).

As SNP guests are launched, RMPUPDATE assigns their private pages to
guest-owned state; when such a page falls within an optimized 1GB
region, the hardware clears that region's RMPOPT optimization and RMP
checks resume there to protect the guest memory.

Since launching SNP guests clears these optimizations, perform them
again asynchronously using the dedicated workqueue.

Suggested-by: Thomas Lendacky <thomas.lendacky@xxxxxxx>
Suggested-by: Dave Hansen <dave.hansen@xxxxxxxxxxxxxxx>
Signed-off-by: Ashish Kalra <ashish.kalra@xxxxxxx>
---
v14: Reworked the setup/teardown - initialize once and do not tear it down;
rmpopt_disable() now only cancels the pending pass. Use a dedicated
per-CPU workqueue (WQ_PERCPU) and drop the migrate_disable()/
migrate_enable() around the local scan. Added a binutils-version
comment above the RMPOPT .byte and trimmed redundant comments and the
2 TB pr_info(). Subject reworded from "Add support to perform RMP
optimizations asynchronously". Dropped Reviewed-by: Ackerley Tng and
Tom Lendacky due to the rework (kept Suggested-by).

arch/x86/virt/svm/sev.c | 92 ++++++++++++++++++++++++++++++++++++++++-
1 file changed, 91 insertions(+), 1 deletion(-)

diff --git a/arch/x86/virt/svm/sev.c b/arch/x86/virt/svm/sev.c
index a059327dc107..35678b1f535d 100644
--- a/arch/x86/virt/svm/sev.c
+++ b/arch/x86/virt/svm/sev.c
@@ -19,6 +19,7 @@
#include <linux/iommu.h>
#include <linux/amd-iommu.h>
#include <linux/nospec.h>
+#include <linux/workqueue.h>

#include <asm/sev.h>
#include <asm/processor.h>
@@ -124,7 +125,16 @@ static void *rmp_bookkeeping __ro_after_init;

static u64 probed_rmp_base, probed_rmp_size;

-static phys_addr_t rmpopt_pa_start;
+static u64 rmpopt_pa_start, rmpopt_pa_end;
+
+enum rmpopt_op_type {
+ RMPOPT_OP_VERIFY_AND_REPORT_STATUS,
+ RMPOPT_OP_REPORT_STATUS
+};
+
+static struct workqueue_struct *rmpopt_wq;
+static struct delayed_work rmpopt_delayed_work;
+static DEFINE_MUTEX(rmpopt_wq_mutex);

static LIST_HEAD(snp_leaked_pages_list);
static DEFINE_SPINLOCK(snp_leaked_pages_list_lock);
@@ -557,6 +567,14 @@ int snp_prepare(void)
}
EXPORT_SYMBOL_FOR_MODULES(snp_prepare, "ccp");

+static void rmpopt_disable(void)
+{
+ guard(mutex)(&rmpopt_wq_mutex);
+
+ if (rmpopt_wq)
+ cancel_delayed_work_sync(&rmpopt_delayed_work);
+}
+
void snp_shutdown(void)
{
u64 syscfg;
@@ -565,6 +583,8 @@ void snp_shutdown(void)
if (syscfg & MSR_AMD64_SYSCFG_SNP_EN)
return;

+ rmpopt_disable();
+
clear_rmp();
on_each_cpu(mfd_reconfigure, NULL, 1);

@@ -583,6 +603,43 @@ static bool rmpopt_capable(void)
cc_platform_has(CC_ATTR_HOST_SEV_SNP);
}

+/*
+ * RMPOPT optimizations skip RMP checks at 1GB granularity if this range of
+ * memory does not contain any SNP guest memory.
+ *
+ * @pa is a system physical address; RMPOPT operates on the containing 1GB.
+ */
+static void rmpopt(u64 pa)
+{
+ enum rmpopt_op_type op = RMPOPT_OP_VERIFY_AND_REPORT_STATUS;
+ u64 pa_start = ALIGN_DOWN(pa, SZ_1G);
+
+ /* Supported by binutils 2.48+ */
+ asm volatile(".byte 0xf2, 0x0f, 0x01, 0xfc"
+ :: "a" (pa_start), "c" (op)
+ : "memory", "cc");
+}
+
+/* on_each_cpu() callback: optimize the whole RMPOPT range on this CPU. */
+static void rmpopt_scan_range(void *arg)
+{
+ u64 pa;
+
+ for (pa = rmpopt_pa_start; pa < rmpopt_pa_end; pa += SZ_1G)
+ rmpopt(pa);
+}
+
+static void do_rmpopt_work(struct work_struct *work)
+{
+ /*
+ * Warm up the RMPOPT cache on this pinned per-CPU worker with interrupts
+ * on, so the IRQ-disabled fan-out below only issues cache-hit RMPOPTs.
+ */
+ rmpopt_scan_range(NULL);
+
+ on_each_cpu_mask(cpu_primary_thread_mask, rmpopt_scan_range, NULL, true);
+}
+
void snp_setup_rmpopt(void)
{
u64 rmpopt_base;
@@ -591,6 +648,30 @@ void snp_setup_rmpopt(void)
if (!rmpopt_capable())
return;

+ guard(mutex)(&rmpopt_wq_mutex);
+
+ /*
+ * Set up once: the workqueue and RMPOPT_BASE MSRs are left in place on
+ * shutdown, so a later re-initialization just re-queues the optimization
+ * pass rather than redoing the setup.
+ */
+ if (rmpopt_wq) {
+ queue_delayed_work(rmpopt_wq, &rmpopt_delayed_work, 0);
+ return;
+ }
+
+ /*
+ * Use a dedicated per-CPU workqueue so the potentially lengthy warm-up
+ * scan does not tie up a shared workqueue worker.
+ */
+ rmpopt_wq = alloc_workqueue("rmpopt_wq", WQ_PERCPU, 1);
+ if (!rmpopt_wq) {
+ pr_err("Failed to allocate RMPOPT workqueue\n");
+ return;
+ }
+
+ INIT_DELAYED_WORK(&rmpopt_delayed_work, do_rmpopt_work);
+
rmpopt_pa_start = ALIGN_DOWN(PFN_PHYS(min_low_pfn), SZ_1G);
rmpopt_base = rmpopt_pa_start | MSR_AMD64_RMPOPT_ENABLE;

@@ -600,6 +681,15 @@ void snp_setup_rmpopt(void)
*/
for_each_cpu(cpu, cpu_primary_thread_mask)
wrmsrq_on_cpu(cpu, MSR_AMD64_RMPOPT_BASE, rmpopt_base);
+
+ rmpopt_pa_end = ALIGN(PFN_PHYS(max_pfn), SZ_1G);
+
+ if ((rmpopt_pa_end - rmpopt_pa_start) > SZ_2T)
+ rmpopt_pa_end = rmpopt_pa_start + SZ_2T;
+
+ queue_delayed_work(rmpopt_wq, &rmpopt_delayed_work, 0);
+
+ pr_info("RMPOPT optimizations enabled\n");
}
EXPORT_SYMBOL_FOR_MODULES(snp_setup_rmpopt, "ccp");

--
2.43.0