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

Re: [PATCH v3 07/18] x86/hvm: introduce per-vCPU L3 page-table



On Wed, Oct 07, 2026 at 11:40:40AM +0100, George Dunlap wrote:
> From: Roger Pau Monné <roger.pau@xxxxxxxxxx>
> 
> The per-domain area is currently a single domain-wide structure: one L3,

nit: s/structure/entry/ might be better?

> referenced from every root page-table associated with the domain, so
> every mapping in it is visible to every vCPU of the domain.  Its
> contents are already laid out in per-vCPU slices (each vCPU's
> COMPAT_ARG_XLAT pages, and for PV each vCPU's GDT/LDT window and
> mapcache entries), but the visibility is domain-wide.  Meanwhile, much
> of the per-vCPU state Xen maintains -- the VMCB, the VMX MSR load/save
> areas, FPU/XSAVE state -- lives in always-mapped memory.
> 
> Allow the "per-domain" area to be per-vCPU instead ("VCPU-PT") for HVM
> domains.  HVM monitor tables are already per-vCPU, so this needs nothing
> at context switch: every monitor table carries its vCPU's own L3 from
> creation, and the per-vCPU mappings are isolated from other running
> vCPUs at once.  We will later build on this, adding a per-vCPU mapcache
> and per-vCPU mapped areas, into which we can move the vCPU state
> mentioned above.
> 
> Add pervcpu_l3_pg to the arch_vcpu struct, to correspond to the
> perdomain_l3_pg in the domain struct.  (We retain both so that we can
> switch between per-domain and per-vCPU on a domain-by-domain basis; PV
> domains always use the domain-wide one.)
> 
> In {create,destroy}_perdomain_mapping(), if d->arch.vcpu_pt, use
> pervcpu_l3_pg as the per-domain L3 (allocating it for a vCPU if it's
> NULL, just as we allocate for a domain in !vcpu_pt mode); otherwise,
> use perdomain_l3_pg.  Introduce a helper, perdomain_l3(), to
> consistently choose the correct one.
> 
> Introduce free_pervcpu_mappings() to free this tree, called from
> arch_vcpu_destroy() on normal teardown.  Since the vcpu structure holds
> the only reference to pervcpu_l3_pg, arch_vcpu_create() must also call
> it on its error paths: nothing else records the allocation once the
> vcpu struct is torn down.  The domain-wide free_perdomain_mappings() is
> unchanged and keeps covering non-vCPU-PT domains.
> 
> Give init_xen_l4_slots() the vCPU a table is private to, and use

Can't you replace the domain parameter with a vcpu one, and obtain
domain as v->domain?

> perdomain_l3() to select the value to install in slot 260 when one is
> given.  The HVM monitor tables and the 32-bit PV Xen-owned L4s pass
> their vCPU; tables the domain's vCPUs share -- promoted PV guest L4s,
> PV shadow L4s, and the PV dom0 L4 under construction -- pass NULL and
> keep the domain-wide L3, which is all a PV domain has.
> 
> Signed-off-by: Roger Pau Monné <roger.pau@xxxxxxxxxx>
> Assisted-by: Claude Code:claude-fable-5, Claude Code:claude-opus-4-8, Claude 
> Code:claude-fable-5-1, Claude Code:claude-opus-5-5
> Signed-off-by: George Dunlap <gwd@xxxxxxxxxxxxxx>
> ---
> Changes in v3:
> - HVM only: PV domains keep the domain-wide area, so the PV parts go
>   (the single-vCPU limit, the shadow refusals, the XPTI slot install).
>   init_xen_l4_slots() takes the vCPU as an extra, optional argument
>   rather than in place of the domain, so PV guest L4s, which no vCPU
>   owns, are promoted exactly as before.

Hm, I see, so this is the reason. PV L4s are promoted ahead of being
loaded into a vCPU %cr3, and hence when init_xen_l4_slots() is called
the per-vCPU L3 to use is not clear.

> - Retitle (was: "x86/mm: introduce per-vCPU L3 page-table").
> - free_compat_arg_xlat()'s comment names free_pervcpu_mappings() as the
>   teardown for a vCPU-PT domain.
> 
> Changes in v2:
> - Added to the series
> 
> Changes since the previously posted version:
> - Expand the commit message with the motivation and the design
>   rationale.
> - Keep domain-wide freeing intact and introduce a vCPU-scoped
>   free_pervcpu_mappings() instead of re-scoping
>   free_perdomain_mappings(); fix the arch_vcpu_create() error-path
>   leaks of a partially built per-vCPU hierarchy.
> - Add a perdomain_l3() helper for the root selection, rather than
>   open-coding the vcpu_pt choice (and testing both root pointers) at
>   each site.
> ---
>  xen/arch/x86/domain.c             | 10 +++--
>  xen/arch/x86/include/asm/domain.h | 12 +++++
>  xen/arch/x86/include/asm/mm.h     |  5 ++-
>  xen/arch/x86/mm.c                 | 73 ++++++++++++++++++++++---------
>  xen/arch/x86/mm/hap/hap.c         |  2 +-
>  xen/arch/x86/mm/shadow/hvm.c      |  2 +-
>  xen/arch/x86/mm/shadow/multi.c    |  5 ++-
>  xen/arch/x86/pv/dom0_build.c      |  2 +-
>  xen/arch/x86/pv/domain.c          |  2 +-
>  xen/arch/x86/x86_64/mm.c          |  7 +--
>  10 files changed, 87 insertions(+), 33 deletions(-)
> 
> diff --git a/xen/arch/x86/domain.c b/xen/arch/x86/domain.c
> index b670cea52b..0aba2cca2e 100644
> --- a/xen/arch/x86/domain.c
> +++ b/xen/arch/x86/domain.c
> @@ -513,7 +513,7 @@ int arch_vcpu_create(struct vcpu *v)
>  
>      rc = mapcache_vcpu_init(v);
>      if ( rc )
> -        return rc;
> +        goto fail_early;
>  
>      if ( !is_idle_domain(d) )
>      {
> @@ -525,12 +525,12 @@ int arch_vcpu_create(struct vcpu *v)
>           */
>          rc = create_perdomain_mapping(v, PERDOMAIN_VIRT_START, 0, NULL, 
> NULL);
>          if ( rc )
> -            return rc;
> +            goto fail_early;
>  
>          paging_vcpu_init(v);
>  
>          if ( (rc = vcpu_init_fpu(v)) != 0 )
> -            return rc;
> +            goto fail_early;
>  
>          vmce_init_vcpu(v);
>  
> @@ -578,6 +578,8 @@ int arch_vcpu_create(struct vcpu *v)
>      vcpu_destroy_fpu(v);
>      xfree(v->arch.msrs);
>      v->arch.msrs = NULL;
> + fail_early:
> +    free_pervcpu_mappings(v);

Do we really need the extra label?  It would seem to me like the
existing cleanup functions are harmless to execute regardless of
whether the matching initialization routine has been called.

>  
>      return rc;
>  }
> @@ -598,6 +600,8 @@ void arch_vcpu_destroy(struct vcpu *v)
>          pv_vcpu_destroy(v);
>      else
>          ASSERT_UNREACHABLE();
> +
> +    free_pervcpu_mappings(v);
>  }
>  
>  int arch_sanitise_domain_config(struct xen_domctl_createdomain *config)
> diff --git a/xen/arch/x86/include/asm/domain.h 
> b/xen/arch/x86/include/asm/domain.h
> index b192c86bc1..a6c6eaf53a 100644
> --- a/xen/arch/x86/include/asm/domain.h
> +++ b/xen/arch/x86/include/asm/domain.h
> @@ -335,6 +335,11 @@ struct monitor_write_data {
>  
>  struct arch_domain
>  {
> +    /*
> +     * Domain-wide L3 page-table for the L4 per-domain slot, used when
> +     * the domain does not use per-vCPU page-tables (!d->arch.vcpu_pt).
> +     * NULL otherwise (see v->arch.pervcpu_l3_pg and perdomain_l3()).
> +     */
>      struct page_info *perdomain_l3_pg;
>  
>      /* I/O-port admin-specified access capabilities. */
> @@ -691,6 +696,13 @@ struct arch_vcpu
>  
>      struct vcpu_msrs *msrs;
>  
> +    /*
> +     * Per-vCPU L3 page-table for the L4 per-domain slot, used when the
> +     * domain uses per-vCPU page-tables (d->arch.vcpu_pt).  NULL
> +     * otherwise (see d->arch.perdomain_l3_pg and perdomain_l3()).
> +     */
> +    struct page_info *pervcpu_l3_pg;
> +
>      struct {
>          bool next_interrupt_enabled;
>      } monitor;
> diff --git a/xen/arch/x86/include/asm/mm.h b/xen/arch/x86/include/asm/mm.h
> index 87bbe1a6f5..87131dd4bf 100644
> --- a/xen/arch/x86/include/asm/mm.h
> +++ b/xen/arch/x86/include/asm/mm.h
> @@ -369,8 +369,10 @@ int devalidate_page(struct page_info *page, unsigned 
> long type,
>                           int preemptible);
>  
>  void init_xen_pae_l2_slots(l2_pgentry_t *l2t, const struct domain *d);
> +struct page_info *perdomain_l3(const struct vcpu *v);
>  void init_xen_l4_slots(l4_pgentry_t *l4t, mfn_t l4mfn,
> -                       const struct domain *d, mfn_t sl4mfn, bool ro_mpt);
> +                       const struct domain *d, const struct vcpu *v,
> +                       mfn_t sl4mfn, bool ro_mpt);
>  bool fill_ro_mpt(mfn_t mfn);
>  void zap_ro_mpt(mfn_t mfn);
>  
> @@ -609,6 +611,7 @@ int create_perdomain_mapping(struct vcpu *v, unsigned 
> long va,
>  void destroy_perdomain_mapping(const struct vcpu *v, unsigned long va,
>                                 unsigned int nr);
>  void free_perdomain_mappings(struct domain *d);
> +void free_pervcpu_mappings(struct vcpu *v);
>  
>  void __iomem *ioremap_wc(paddr_t pa, size_t len);
>  
> diff --git a/xen/arch/x86/mm.c b/xen/arch/x86/mm.c
> index ca22107cd3..7258935528 100644
> --- a/xen/arch/x86/mm.c
> +++ b/xen/arch/x86/mm.c
> @@ -1638,6 +1638,17 @@ static int promote_l3_table(struct page_info *page)
>  }
>  #endif /* CONFIG_PV */
>  
> +/*
> + * The root of the per-domain area in use by @v: the vCPU's own L3 for a
> + * vCPU-PT domain, the domain-wide one otherwise.
> + */
> +struct page_info *perdomain_l3(const struct vcpu *v)
> +{
> +    const struct domain *d = v->domain;
> +
> +    return d->arch.vcpu_pt ? v->arch.pervcpu_l3_pg : d->arch.perdomain_l3_pg;
> +}
> +
>  /*
>   * Fill an L4 with Xen entries.
>   *
> @@ -1645,12 +1656,16 @@ static int promote_l3_table(struct page_info *page)
>   * values a guest may have left there from promote_l4_table().
>   *
>   * l4t, l4mfn, and d are mandatory, but l4mfn doesn't need to be the mfn 
> under
> - * *l4t.  All other parameters are optional and will either fill or zero the
> - * appropriate slots.  Pagetables not shared with guests will gain the
> + * *l4t.  v names the vCPU the table is private to (an HVM monitor table, a
> + * 32-bit PV vCPU's Xen-owned L4), or is NULL for tables the domain's vCPUs
> + * share; it is mandatory for a vCPU-PT domain, whose per-domain area is
> + * per-vCPU.  All other parameters are optional and will either fill or zero
> + * the appropriate slots.  Pagetables not shared with guests will gain the
>   * extended directmap.
>   */
>  void init_xen_l4_slots(l4_pgentry_t *l4t, mfn_t l4mfn,
> -                       const struct domain *d, mfn_t sl4mfn, bool ro_mpt)
> +                       const struct domain *d, const struct vcpu *v,
> +                       mfn_t sl4mfn, bool ro_mpt)
>  {
>      /*
>       * PV vcpus need a shortened directmap.  HVM and Idle vcpus get the full
> @@ -1678,8 +1693,11 @@ void init_xen_l4_slots(l4_pgentry_t *l4t, mfn_t l4mfn,
>          l4e_from_mfn(sl4mfn, __PAGE_HYPERVISOR_RW);
>  
>      /* Slot 260: Per-domain mappings. */
> +    ASSERT(!v || v->domain == d);
> +    ASSERT(v || !d->arch.vcpu_pt);
>      l4t[l4_table_offset(PERDOMAIN_VIRT_START)] =
> -        l4e_from_page(d->arch.perdomain_l3_pg, __PAGE_HYPERVISOR_RW);
> +        l4e_from_page(v ? perdomain_l3(v) : d->arch.perdomain_l3_pg,
> +                      __PAGE_HYPERVISOR_RW);
>  
>      /* Slot 4: Per-domain mappings mirror. */
>      BUILD_BUG_ON(IS_ENABLED(CONFIG_PV32) &&
> @@ -1835,7 +1853,7 @@ static int promote_l4_table(struct page_info *page)
>      if ( !rc )
>      {
>          init_xen_l4_slots(pl4e, l4mfn,
> -                          d, INVALID_MFN, VM_ASSIST(d, m2p_strict));
> +                          d, NULL, INVALID_MFN, VM_ASSIST(d, m2p_strict));
>          atomic_inc(&d->arch.pv.nr_l4_pages);
>      }
>      unmap_domain_page(pl4e);
> @@ -6216,7 +6234,7 @@ int create_perdomain_mapping(struct vcpu *v, unsigned 
> long va,
>                               struct page_info **ppg)
>  {
>      struct domain *d = v->domain;
> -    struct page_info *pg;
> +    struct page_info *pg, *l3_pg = perdomain_l3(v);
>      l3_pgentry_t *l3tab;
>      l2_pgentry_t *l2tab;
>      l1_pgentry_t *l1tab;
> @@ -6225,14 +6243,17 @@ int create_perdomain_mapping(struct vcpu *v, unsigned 
> long va,
>      ASSERT(va >= PERDOMAIN_VIRT_START &&
>             va < PERDOMAIN_VIRT_SLOT(PERDOMAIN_SLOTS));
>  
> -    if ( !d->arch.perdomain_l3_pg )
> +    if ( !l3_pg )
>      {
>          pg = alloc_domheap_page(d, MEMF_no_owner);
>          if ( !pg )
>              return -ENOMEM;
>          l3tab = __map_domain_page(pg);
>          clear_page(l3tab);
> -        d->arch.perdomain_l3_pg = pg;
> +        if ( d->arch.vcpu_pt )
> +            v->arch.pervcpu_l3_pg = pg;
> +        else
> +            d->arch.perdomain_l3_pg = pg;
>          if ( !nr )
>          {
>              unmap_domain_page(l3tab);
> @@ -6242,7 +6263,7 @@ int create_perdomain_mapping(struct vcpu *v, unsigned 
> long va,
>      else if ( !nr )
>          return 0;
>      else
> -        l3tab = __map_domain_page(d->arch.perdomain_l3_pg);
> +        l3tab = __map_domain_page(l3_pg);
>  
>      ASSERT(!l3_table_offset(va ^ (va + nr * PAGE_SIZE - 1)));
>  
> @@ -6339,16 +6360,16 @@ void destroy_perdomain_mapping(const struct vcpu *v, 
> unsigned long va,
>                                 unsigned int nr)
>  {
>      const l3_pgentry_t *l3tab, *pl3e;
> -    const struct domain *d = v->domain;
> +    struct page_info *l3_pg = perdomain_l3(v);
>  
>      ASSERT(va >= PERDOMAIN_VIRT_START &&
>             va < PERDOMAIN_VIRT_SLOT(PERDOMAIN_SLOTS));
>      ASSERT(!nr || !l3_table_offset(va ^ (va + nr * PAGE_SIZE - 1)));
>  
> -    if ( !d->arch.perdomain_l3_pg )
> +    if ( !l3_pg )
>          return;
>  
> -    l3tab = __map_domain_page(d->arch.perdomain_l3_pg);
> +    l3tab = __map_domain_page(l3_pg);
>      pl3e = l3tab + l3_table_offset(va);
>  
>      if ( l3e_get_flags(*pl3e) & _PAGE_PRESENT )
> @@ -6387,16 +6408,11 @@ void destroy_perdomain_mapping(const struct vcpu *v, 
> unsigned long va,
>      unmap_domain_page(l3tab);
>  }
>  
> -void free_perdomain_mappings(struct domain *d)
> +static void free_perdomain_l3(struct page_info *l3pg)
>  {
> -    l3_pgentry_t *l3tab;
> +    l3_pgentry_t *l3tab = __map_domain_page(l3pg);
>      unsigned int i;
>  
> -    if ( !d->arch.perdomain_l3_pg )
> -        return;
> -
> -    l3tab = __map_domain_page(d->arch.perdomain_l3_pg);
> -
>      for ( i = 0; i < PERDOMAIN_SLOTS; ++i)
>          if ( l3e_get_flags(l3tab[i]) & _PAGE_PRESENT )
>          {
> @@ -6432,10 +6448,27 @@ void free_perdomain_mappings(struct domain *d)
>          }
>  
>      unmap_domain_page(l3tab);
> -    free_domheap_page(d->arch.perdomain_l3_pg);
> +    free_domheap_page(l3pg);
> +}
> +
> +void free_perdomain_mappings(struct domain *d)
> +{
> +    if ( !d->arch.perdomain_l3_pg )
> +        return;
> +
> +    free_perdomain_l3(d->arch.perdomain_l3_pg);
>      d->arch.perdomain_l3_pg = NULL;
>  }
>  
> +void free_pervcpu_mappings(struct vcpu *v)
> +{
> +    if ( !v->arch.pervcpu_l3_pg )
> +        return;
> +
> +    free_perdomain_l3(v->arch.pervcpu_l3_pg);
> +    v->arch.pervcpu_l3_pg = NULL;

We have been (recently) following the model of first setting the
pointer to NULL, and then freeing it, ie:

struct page_info *l3_pg = v->arch.pervcpu_l3_pg;

if ( !l3_pg )
    return;

v->arch.pervcpu_l3_pg = NULL;
free_perdomain_l3(l3_pg);

Like XFREE() and similar macros.  It's possibly worth using the same
pattern here.

Thanks, Roger.



 


Rackspace

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