[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[PATCH v5 33/45] x86: move the saved register frame behind the vCPU state window



While a vCPU is not running, its register frame is saved in struct
vcpu: a xenheap page, mapped in every context, the sparse directmap view
included.  Under Address Space Isolation that leaves the general-purpose
registers of every descheduled vCPU reachable from other domains'
contexts, whether or not the vCPU's own domain is on the sparse view.

Keep the frame instead in a domain-heap page of the vCPU's own, mapped
for the life of the vCPU at a new slot (index 6) of its state window,
like the SVM VMCB and the VMX MSR list; indexes 3-5 are left for the
guest XSAVE area, which takes up to three pages.  Like the VMCB and MSR
list moves, this does not depend on asi=, and it applies to every guest
vCPU, PV and HVM: struct vcpu no longer holds a copy.  The page is
allocated in arch_vcpu_create() ahead of arch_vcpu_regs_init(), which
initialises the frame, and unmapped before it is freed, on the failure
path as at teardown.

An HVM vCPU using per-vCPU page tables has its window in its own page
tables.  For any other vCPU, PV ones included, the window is in the
per-domain area that all its domain's vCPUs share, which only that
domain's page tables map: a context on the sparse view reaches no other
domain's frames.  That includes a domain left off the sparse view, such
as dom0 under asi=1,dom0=0, whose vCPUs' frames are no longer mapped in
the contexts of the domUs on the sparse view.

__context_switch() saves the outgoing vCPU's frame while its page tables
are still loaded, and now restores the incoming vCPU's only after
write_ptbase(), once its window is mapped.  Nothing in between reads the
frame: set_xcr0(), the FPU restore, the ctxt_switch->to() hooks, and the
PSR and GDT updates.  Both copies go through the window address, with
interrupts off; a transient mapping would complete the lazy context
switch and re-enter __context_switch().

The save is skipped if the outgoing vCPU's page tables are no longer
loaded.  Only the shadow-mode paths which abandon a PV vCPU's page
tables early (update_cr3() failure in new_guest_cr3() and
_toggle_guest_pt()) leave them so, loading the idle page tables once
they have crashed the domain; anything else reaching that point is a
page-table tracking bug, fatal in every build.  After such a crash the
vCPU's saved frame is the one it was last switched in with, not its
registers at the crash, and that is what XEN_DOMCTL_getvcpucontext and
the debug keys report for it.

VCPU_MAP_REGS() returns the window address when the vCPU is running
here on its own page tables (v == current and this_cpu(pgtable_vcpu) ==
v), and otherwise a transient mapping of the page, which, for a single
page, cannot fail.  Its users all work on a vCPU other than the one
running here, normally paused or not yet run: vCPU creation, the
toolstack's vCPU context, the HVM CPU record, VCPUOP_initialise,
INIT/SIPI and S3 resets, a hypercall continued on another CPU, PV dom0
construction, and the debug-key dumps.  The vCPU's own page tables
being loaded is not enough: while a CPU holds a vCPU's state lazily
they are loaded there, but an IPI syncing that state for another CPU
can switch them away at any time.  VCPU_UNMAP_REGS() drops a transient
mapping with unmap_vcpu_state_page(), which also flushes this CPU's
translation.

The live frame on the pCPU's stack, which holds a running vCPU's
registers, is not affected.

Cost: a page per guest vCPU, and a transient mapping on each of the
cold paths above.  On every switch into a guest vCPU, the restore
reaches the frame through a 4KiB, non-global PTE in the per-domain
area, just after switch_cr3_cr4() has flushed the TLB, so it takes a
page walk, where struct vcpu was reached through a superpage of the
full directmap.  struct vcpu shrinks from 3072 to 2880 bytes (x86_64
defconfig).

Assisted-by: Claude Code:claude-opus-5-5
Signed-off-by: George Dunlap <gwd@xxxxxxxxxxxxxx>
---
Changes in v5:
- New in this version.
---
 xen/arch/x86/domain.c             | 98 ++++++++++++++++++++++++++++++-
 xen/arch/x86/include/asm/config.h |  1 +
 xen/arch/x86/include/asm/domain.h | 26 +++++---
 3 files changed, 116 insertions(+), 9 deletions(-)

diff --git a/xen/arch/x86/domain.c b/xen/arch/x86/domain.c
index 6911f8484c..2c27be4437 100644
--- a/xen/arch/x86/domain.c
+++ b/xen/arch/x86/domain.c
@@ -17,6 +17,7 @@
 #include <xen/cpu.h>
 #include <xen/delay.h>
 #include <xen/domain.h>
+#include <xen/domain_page.h>
 #include <xen/errno.h>
 #include <xen/event.h>
 #include <xen/grant_table.h>
@@ -493,6 +494,66 @@ unsigned int arch_domain_struct_memflags(void)
     return MEMF_bits(bits);
 }
 
+/*
+ * A vCPU keeps its register frame, while it is not running, in a domain-heap
+ * page of its own, mapped for the life of the vCPU at a fixed slot of its
+ * state window.
+ */
+static struct cpu_user_regs *regs_window(const struct vcpu *v)
+{
+    return (void *)VCPU_STATE_ADDR(v, VCPU_STATE_IDX_REGS);
+}
+
+static int vcpu_regs_alloc(struct vcpu *v)
+{
+    struct page_info *pg = alloc_domheap_page(v->domain, MEMF_no_owner);
+    mfn_t mfn;
+
+    if ( !pg )
+        return -ENOMEM;
+
+    mfn = page_to_mfn(pg);
+    clear_domain_page(mfn);
+    populate_perdomain_mapping(v, VCPU_STATE_ADDR(v, VCPU_STATE_IDX_REGS),
+                               &mfn, 1, __PAGE_HYPERVISOR_RW);
+    v->arch.regs_pg = pg;
+
+    return 0;
+}
+
+static void vcpu_regs_free(struct vcpu *v)
+{
+    struct page_info *pg = v->arch.regs_pg;
+
+    if ( !pg )
+        return;
+
+    unmap_vcpu_state(v, VCPU_STATE_IDX_REGS, 1);
+    v->arch.regs_pg = NULL;
+    free_domheap_page(pg);
+}
+
+struct cpu_user_regs *vcpu_map_regs(const struct vcpu *v)
+{
+    /*
+     * v's page tables being loaded is not enough: while this CPU holds v's
+     * state lazily, an IPI syncing that state for another CPU can switch
+     * them away at any time.
+     */
+    if ( v == current && this_cpu(pgtable_vcpu) == v )
+        return regs_window(v);
+
+    return __map_domain_page(v->arch.regs_pg);
+}
+
+void vcpu_unmap_regs(const struct vcpu *v, const struct cpu_user_regs *regs)
+{
+    if ( !regs || regs == regs_window(v) )
+        return;
+
+    unmap_vcpu_state_page(regs);
+}
+
 /* Initialise various registers to their architectural INIT/RESET state. */
 void arch_vcpu_regs_init(struct vcpu *v)
 {
@@ -534,6 +595,7 @@ int arch_vcpu_create(struct vcpu *v)
         BUILD_BUG_ON(VCPU_STATE_IDX_VMCB >= VCPU_STATE_NR_PAGES);
         BUILD_BUG_ON(VCPU_STATE_IDX_N2VMCB >= VCPU_STATE_NR_PAGES);
         BUILD_BUG_ON(VCPU_STATE_IDX_VMX_MSR >= VCPU_STATE_NR_PAGES);
+        BUILD_BUG_ON(VCPU_STATE_IDX_REGS >= VCPU_STATE_NR_PAGES);
         /* Page tables only; torn down with the rest of the per-domain area. */
         rc = create_perdomain_mapping(v, VCPU_STATE_START(v),
                                       VCPU_STATE_NR_PAGES,
@@ -548,6 +610,10 @@ int arch_vcpu_create(struct vcpu *v)
 
         vmce_init_vcpu(v);
 
+        /* The frame's storage, ahead of its initialisation. */
+        if ( (rc = vcpu_regs_alloc(v)) != 0 )
+            goto fail;
+
         arch_vcpu_regs_init(v);
 
         if ( (rc = init_vcpu_msr_policy(v)) )
@@ -590,6 +656,7 @@ int arch_vcpu_create(struct vcpu *v)
  fail:
     paging_vcpu_teardown(v);
     vcpu_destroy_fpu(v);
+    vcpu_regs_free(v);
     xfree(v->arch.msrs);
     v->arch.msrs = NULL;
     free_pervcpu_mappings(v);
@@ -614,6 +681,8 @@ void arch_vcpu_destroy(struct vcpu *v)
     else
         ASSERT_UNREACHABLE();
 
+    vcpu_regs_free(v);
+
     free_pervcpu_mappings(v);
 }
 
@@ -2129,6 +2198,19 @@ static void load_default_gdt(unsigned int cpu)
     per_cpu(full_gdt_loaded, cpu) = false;
 }
 
+/*
+ * The register frame of p or n, for __context_switch(), which runs with
+ * interrupts off and with the vCPU's own page tables loaded: the window
+ * address serves.  A transient mapping is not an option from here: it would
+ * complete the lazy context switch and re-enter __context_switch().
+ */
+static struct cpu_user_regs *vcpu_user_regs(const struct vcpu *v)
+{
+    BUG_ON(this_cpu(pgtable_vcpu) != v);
+
+    return regs_window(v);
+}
+
 static void __context_switch(void)
 {
     struct cpu_user_regs *stack_regs = guest_cpu_user_regs();
@@ -2143,7 +2225,17 @@ static void __context_switch(void)
     if ( !is_idle_domain(pd) )
     {
         ASSERT(read_atomic(&p->dirty_cpu) == cpu);
-        memcpy(&p->arch._user_regs, stack_regs, CTXT_SWITCH_STACK_BYTES);
+        /*
+         * p's frame is reached through its state window, mapped while p's
+         * own page tables are loaded, as they normally still are here.  Not
+         * so after the shadow-mode paths which abandon a vCPU's tables early
+         * (update_cr3() failure), which have crashed the domain: the frame
+         * is left unsaved.  A transient mapping would re-enter this function.
+         */
+        if ( likely(this_cpu(pgtable_vcpu) == p) )
+            memcpy(vcpu_user_regs(p), stack_regs, CTXT_SWITCH_STACK_BYTES);
+        else
+            BUG_ON(!domain_shutting_down(pd));
         vcpu_save_fpu(p);
         pd->arch.ctxt_switch->from(p);
     }
@@ -2159,7 +2251,6 @@ static void __context_switch(void)
 
     if ( !is_idle_domain(nd) )
     {
-        memcpy(stack_regs, &n->arch._user_regs, CTXT_SWITCH_STACK_BYTES);
         if ( cpu_has_xsave )
         {
             if ( !set_xcr0(n->arch.xcr0 ?: XSTATE_FP_SSE) )
@@ -2183,6 +2274,9 @@ static void __context_switch(void)
 
     write_ptbase(n);
 
+    if ( !is_idle_domain(nd) )
+        memcpy(stack_regs, vcpu_user_regs(n), CTXT_SWITCH_STACK_BYTES);
+
 #ifdef CONFIG_PV
     /* Prefetch the VMCB if we expect to use it later in the context switch */
     if ( using_svm() && is_pv_64bit_domain(nd) && !is_idle_domain(nd) )
diff --git a/xen/arch/x86/include/asm/config.h 
b/xen/arch/x86/include/asm/config.h
index 8a265e25c8..c8d4a8ddc7 100644
--- a/xen/arch/x86/include/asm/config.h
+++ b/xen/arch/x86/include/asm/config.h
@@ -311,6 +311,7 @@ extern unsigned long xen_phys_start;
 #define VCPU_STATE_IDX_VMCB      0
 #define VCPU_STATE_IDX_N2VMCB    1
 #define VCPU_STATE_IDX_VMX_MSR   2
+#define VCPU_STATE_IDX_REGS      6
 
 #define ELFSIZE 64
 
diff --git a/xen/arch/x86/include/asm/domain.h 
b/xen/arch/x86/include/asm/domain.h
index 457eb69ace..6b27af9eb5 100644
--- a/xen/arch/x86/include/asm/domain.h
+++ b/xen/arch/x86/include/asm/domain.h
@@ -633,11 +633,12 @@ struct pv_vcpu
 struct arch_vcpu
 {
     /*
-     * The vCPU's register frame while it is not running: __context_switch()
-     * copies it here from the stack when the vCPU is switched out, and back
-     * when it is switched in.  Everything else goes through VCPU_MAP_REGS().
+     * The page holding the vCPU's register frame while it is not running,
+     * mapped in its state window: __context_switch() copies the frame there
+     * from the stack when the vCPU is switched out, and back when it is
+     * switched in.  Everything else goes through VCPU_MAP_REGS().
      */
-    struct cpu_user_regs _user_regs;
+    struct page_info *regs_pg;
 
     /* Debug registers. */
     unsigned long dr[4];
@@ -752,12 +753,20 @@ static inline void free_vcpu_guest_context(struct 
vcpu_guest_context *vgc)
  * registers for code working on a vCPU other than the one running (whose
  * registers are on the stack, see guest_cpu_user_regs()).
  *
+ * The frame is in the vCPU's state window: the window address serves while
+ * the vCPU runs here on its own page tables, and the page is mapped
+ * transiently otherwise, which cannot fail.
+ *
  * The pointer is valid until VCPU_UNMAP_REGS(), which clears it.  A mapping
- * must not be held across a scheduling point.
+ * must not be held across a scheduling point: a transient one is only valid
+ * on the pCPU which made it.
  *
  * @param v Owner of the register frame
  */
-#define VCPU_MAP_REGS(v) (&(v)->arch._user_regs)
+struct cpu_user_regs *vcpu_map_regs(const struct vcpu *v);
+void vcpu_unmap_regs(const struct vcpu *v, const struct cpu_user_regs *regs);
+
+#define VCPU_MAP_REGS(v) vcpu_map_regs(v)
 
 /*
  * Drops the mapping of a vCPU's register frame and nullifies its pointer.
@@ -765,7 +774,10 @@ static inline void free_vcpu_guest_context(struct 
vcpu_guest_context *vgc)
  * @param v Owner of the register frame
  * @param r Register frame of v, from VCPU_MAP_REGS()
  */
-#define VCPU_UNMAP_REGS(v, r) do { (void)(v); (r) = NULL; } while ( false )
+#define VCPU_UNMAP_REGS(v, r) do {  \
+    vcpu_unmap_regs(v, r);          \
+    (r) = NULL;                     \
+} while ( false )
 
 void arch_vcpu_regs_init(struct vcpu *v);
 
-- 
2.56.0




 


Rackspace

Lists.xenproject.org is hosted with RackSpace, monitoring our
servers 24x7x365 and backed by RackSpace's Fanatical Support®.