[PATCH v15 10/10] x86/shadow: limit the number of pages which may be in use as shadows

Jan Beulich <[email protected]> Tue, 28 Jul 2026 16:24:47 +0200
Newsgroups gmane.comp.emulators.xen.devel
Message-ID <[email protected]>
In order to bound the amount of work a single invocation of
shadow_unhook_mappings() may be doing all in one go, constrain the
number of pages which may be in use as shadows. To achieve that, simply
adjust the "success" exit condition of _shadow_prealloc(), thus forcing
removal of shadows not only when we're short of memory.

Note that, depending on workload, this may have a severe effect on
performance, due to the potentially much larger rate of thrashed
shadows.

In the context of "x86/shadow: account for log-dirty mode when pre-
allocating" it is relevant to note that we will be too strict in
sh_prealloc_okay() when log-dirty mode is enabled: Only part of the
pages considered are actually to become shadows. But I think accepting
this is better than further complicating the logic.

Requested-by: Roger Pau Monné <[email protected]>
Signed-off-by: Jan Beulich <[email protected]>
Acked-by: Tim Deegan <[email protected]>
---
What exactly we want the upper bound to be is up for discussion. This
may need to go together with an upper limit on the number of vCPU-s we
deem supportable in a (shadow) guest.

Really 32-bit HVM guests have 3 monitor tables. But I think that not
accounting for that in sh_prealloc_okay() is acceptable. How to
correctly do such accounting there would be unclear anyway, as we mean
to only take domain properties into account, whereas mode dependent
properties are per-vCPU.

Backporting note: The placement of the setting of the new per-domain
                  field relies on d->max_vcpus being set right at domain
                  creation. Hence this will need to move elsewhere for
                  4.11 and older (perhaps into shadow_set_allocation()'s
                  "if ( pages > 0 )" block, conditional upon the value
                  still being zero and max_vcpus already set).

Backporting note: 1d3668664df7 ("x86/shadow: restrict OOS allocation to
                  when it's really needed") is a necessary prereq for
                  the respective part of sh_prealloc_okay().
---
v14: Re-base over XSA-427 and new earlier patches. Restrict allowance
     for monitor tables to HVM. Restrict allowance for OOS to when
     that's actually in use.
v13: Prevent underflow in sh_prealloc_okay(). Re-base.
v12: Re-base past the XSA-410 series.
v11: Account for monitor tables and OOS snapshots in sh_prealloc_okay().
     Calculate the (default) maximum value once during domain
     initialization, into a new per-domain field.
v10: Extend commit message.
v9: New.

--- a/xen/arch/x86/include/asm/domain.h
+++ b/xen/arch/x86/include/asm/domain.h
@@ -108,6 +108,9 @@ void init_hypercall_page(struct domain *
 struct shadow_domain {
 #ifdef CONFIG_SHADOW_PAGING
     unsigned int      opt_flags;    /* runtime tunable optimizations on/off */
+
+    unsigned int      max_pages;    /* limit on the number of shadows in use */
+
     struct page_list_head pinned_shadows;
 
     /* 1-to-1 map for use when HVM vcpus have paging disabled */
--- a/xen/arch/x86/mm/shadow/common.c
+++ b/xen/arch/x86/mm/shadow/common.c
@@ -96,6 +96,32 @@ int shadow_domain_init(struct domain *d)
     d->arch.paging.flush_tlb = shadow_flush_tlb;
 #endif
 
+    /*
+     * Figure out the default for the highest acceptable quantity of shadow
+     * memory. This is because we need to bound the amount of work potentially
+     * in need of doing by a single shadow_unhook_mappings() invocation.
+     */
+    d->arch.paging.shadow.max_pages = 2048;
+    if ( d->max_vcpus > 8 )
+    {
+        /*
+         * This is
+         *
+         *             128 * (max_vcpus + 8)
+         * max_vcpus * ---------------------
+         *                   max_vcpus
+         *
+         * suitably resolved, with the right side of the multiplication
+         * (when expressed as f(x)) satisfying
+         *   f(8) = 256
+         *   lim f(x) = 128
+         *   x->∞
+         * i.e. continuous with the simpler case above and converging to
+         * shadow_min_acceptable_pages() for large values.
+         */
+        d->arch.paging.shadow.max_pages = 128 * (d->max_vcpus + 8);
+    }
+
     return 0;
 }
 
@@ -372,7 +398,7 @@ static inline void trace_shadow_prealloc
 }
 
 static bool sh_blow_tables(struct domain *d, unsigned int goal,
-                           bool *preempted);
+                           unsigned int type, bool *preempted);
 
 /* Make sure there are at least count pages of the order according to
  * type available in the shadow page pool.
@@ -396,7 +422,7 @@ bool shadow_prealloc(struct domain *d, u
          ((SHF_L1_ANY | SHF_FL1_ANY) & (1u << type)) )
         count += paging_logdirty_levels();
 
-    ret = sh_blow_tables(d, count, NULL);
+    ret = sh_blow_tables(d, count, type, NULL);
     if ( !ret && (!d->is_shutting_down || d->shutdown_code != SHUTDOWN_crash) )
         /*
          * Failing to allocate memory required for shadow usage can only result in
@@ -408,6 +434,31 @@ bool shadow_prealloc(struct domain *d, u
 }
 
 /*
+ * Check that
+ * - there are enough free pages,
+ * - there aren't too many pages in use as shadows already when about to make
+ *   a (set of) new shadow page(s).
+ */
+static bool sh_prealloc_okay(const struct domain *d, unsigned int goal,
+                             unsigned int type)
+{
+    if ( d->arch.paging.free_pages < goal )
+        return false;
+
+    if ( type < SH_type_min_shadow || type > SH_type_max_shadow )
+        return true;
+
+    return d->arch.paging.total_pages -
+           (d->arch.paging.free_pages - goal) <=
+           d->arch.paging.shadow.max_pages +
+#if (SHADOW_OPTIMIZATIONS & SHOPT_OUT_OF_SYNC)
+           (d->options & XEN_DOMCTL_CDF_oos_off ? 0 : SHADOW_OOS_PAGES) +
+#endif
+           /* Allow for one monitor table per HVM vCPU. */
+           paging_mode_external(d) * d->max_vcpus;
+}
+
+/*
  * When @goal is zero: Deliberately free all the memory we can: This will
  * tear down all of this domain's shadows.
  *
@@ -415,7 +466,7 @@ bool shadow_prealloc(struct domain *d, u
  * available in the shadow page pool.
  */
 static bool sh_blow_tables(struct domain *d, unsigned int goal,
-                           bool *preempted)
+                           unsigned int type, bool *preempted)
 {
     struct page_info *sp, *t;
     struct vcpu *v;
@@ -423,7 +474,7 @@ static bool sh_blow_tables(struct domain
     int i;
     unsigned int done = 0;
 
-    if ( goal && d->arch.paging.free_pages >= goal )
+    if ( goal && sh_prealloc_okay(d, goal, type) )
         return true;
 
     /*
@@ -465,7 +516,7 @@ static bool sh_blow_tables(struct domain
             while ( hash_foreach(d, masks[i], callbacks, _mfn(0)) )
             {
                 /* See if that freed up enough space */
-                if ( goal && d->arch.paging.free_pages >= goal )
+                if ( goal && sh_prealloc_okay(d, goal, type) )
                     return true;
 
                 if ( general_preempt_check() )
@@ -491,7 +542,7 @@ static bool sh_blow_tables(struct domain
         sh_unpin(d, smfn);
 
         /* See if that freed up enough space */
-        if ( goal && d->arch.paging.free_pages >= goal )
+        if ( goal && sh_prealloc_okay(d, goal, type) )
             return true;
 
         if ( preempted && !(++done & 0xff) && general_preempt_check() )
@@ -519,7 +570,7 @@ static bool sh_blow_tables(struct domain
                     0);
 
                 /* See if that freed up enough space */
-                if ( goal && d->arch.paging.free_pages >= goal )
+                if ( goal && sh_prealloc_okay(d, goal, type) )
                 {
                     guest_flush_tlb_mask(d, d->dirty_cpumask);
                     return true;
@@ -569,7 +620,7 @@ static bool sh_blow_tables(struct domain
  * this domain's shadows */
 void shadow_blow_tables(struct domain *d, bool *preempted)
 {
-    sh_blow_tables(d, 0, preempted);
+    sh_blow_tables(d, 0, SH_type_none, preempted);
 }
 
 void shadow_blow_tables_per_domain(struct domain *d)
@@ -904,7 +955,7 @@ int shadow_set_allocation(struct domain
         else if ( d->arch.paging.total_pages > pages )
         {
             /* Need to return memory to domheap */
-            if ( !sh_blow_tables(d, 1, preempted) )
+            if ( !sh_blow_tables(d, 1, SH_type_none, preempted) )
                 return preempted && *preempted ? 0 : -ENOMEM;
 
             sp = page_list_remove_head(&d->arch.paging.freelist);