Skip to contentSkip to conversation

x86/pv: free the shared_info page switch_compat() replaces

PATCH In review George Dunlap 2 participants 7 messages Nov 10

lore
Share & tools
lore
#0 lore raw

PATCH 1/1 x86/pv: free the shared_info page switch_compat() replaces

For a 32-bit PV guest, switch_compat() replaces a shared_info page above 4GiB with one below, and drops the old page with a bare put_page().

Unfortunately, this violates the expected xenheap-page-shared-with-guest freeing discipline. Xenheap pages don't start with a reference count, and aren't put back into the allocation pool after their reference count drops to zero; they're removed from the domain's xenheap list, but someone needs to explicitly call free_xenheap_page() on them for them to return to their pool.

d->shared_info here only has a non-zero reference count because it was shared with a guest; and the thing that holds that reference is the PGC_allocated bit. Dropping the reference without having been the one to clear the PGC_allocated bit puts the page in an invalid state.

If there are no outstanding references to d->shared_info when switch_compat() happens, the result for pages moved this way is merely a memory leak: the page is removed from the xenheap list, and nothing ever calls free_xenheap_page() on it. So whenever this path is triggered, a page is leaked.

Theoretically, if there *is* an outstanding reference to d->shared_info, *and* that reference is still held when the domain is destroyed, then the result is a reference count underflow: Because the reference count never drops to zero, the page stays on the xenheap list; then during domain destruction, relinquish_memory() walks the list, sees the PGC_allocated bit, and calls put_page() again -- potentially dropping the page count to zero. When the other reference holder drops their reference, the count underflows. (put_page() only ASSERT()s that the count is non-zero, so this wouldn't cause a crash in a release build; and the page is never freed, so other potential hazards of general ref count underflow are avoided.) Fortunately this is impossible with the in-tree toolstack, which only switches a domain while building or restoring it, and doesn't have a mapping of shared_info that it holds across that call.

Release the page the way domain destruction releases d->shared_info:
drop the allocation reference with put_page_alloc_ref(), as relinquish_memory() does, which clears PGC_allocated and takes the page off the xenheap list once nothing else refers to it, and free it with free_xenheap_page() once an RCU grace period has passed.

The free waits for the grace period because do_settime() writes every domain's wallclock holding only wc_lock and the domain list's RCU read lock, neither of which excludes switch_compat(), through a pointer it may have read before the switch. The RCU callback uses the domain's own RCU head, d->rcu, and holds a reference to the domain. That reference keeps the domain from being destroyed, and its struct freed, before the callback has run. It also keeps d->rcu's only other user, domain_destroy(), from queueing the head in the meantime; if the callback drops the last reference, domain_destroy() queues d->rcu again, which RCU has finished with by then. No other reference to the page is expected: with no memory, the domain has no page tables that could map it, and the in-tree toolstack holds no mapping of it across the switch.

Also allocate the new page into a local variable rather than straight into d->shared_info: if the allocation failed, a concurrent do_settime() could dereference the NULL pointer before the old value was restored.

Fixes 8a9255405ca6 ("x86/domain: adjust limitation on shared_info allocation below 4G")
Assisted-by Claude Code:claude-opus-5-5
---
 xen/arch/x86/include/asm/domain.h |  3 +++
 xen/arch/x86/pv/domain.c          | 33 +++++++++++++++++++++++++------
 2 files changed, 30 insertions(+), 6 deletions(-)
xen/arch/x86/include/asm/domain.h +3 -0
index 634d0710bf..906078641c 100644
@@ -302,6 +302,9 @@ struct pv_domain
 
     /* Maximum physical-address bitwidth supported by this guest. */
     unsigned int physaddr_bitsize;
+
+    /* shared_info page switch_compat() replaced, until it is freed. */
+    void *old_shared_info;
 #endif
 
     /* map_domain_page() mapping cache. */
xen/arch/x86/pv/domain.c +27 -6
index 0c42ae58aa..dc481bcc07 100644
@@ -231,6 +231,14 @@ unsigned long pv_make_cr4(const struct vcpu *v)
 }
 
 #ifdef CONFIG_PV32
+static void cf_check _free_old_shared_info(struct rcu_head *head)
+{
+    struct domain *d = container_of(head, struct domain, rcu);
+
+    FREE_XENHEAP_PAGE(d->arch.pv.old_shared_info);
+    put_domain(d);
+}
+
 int switch_compat(struct domain *d)
 {
     struct vcpu *v;
@@ -256,16 +264,16 @@ int switch_compat(struct domain *d)
     if ( virt_to_maddr(d->shared_info) >> 32 )
     {
         shared_info_t *prev = d->shared_info;
+        shared_info_t *next = alloc_xenheap_pages(0, MEMF_bits(32));
 
-        d->shared_info = alloc_xenheap_pages(0, MEMF_bits(32));
-        if ( !d->shared_info )
+        if ( !next )
         {
-            d->shared_info = prev;
             rc = -ENOMEM;
             goto undo_and_fail;
         }
-        clear_page(d->shared_info);
-        share_xen_page_with_guest(virt_to_page(d->shared_info), d, SHARE_rw);
+        clear_page(next);
+        share_xen_page_with_guest(virt_to_page(next), d, SHARE_rw);
+        d->shared_info = next;
         /*
          * Ensure all pointers to the old shared_info page are replaced.  vCPUs
          * below XEN_LEGACY_MAX_VCPUS may have stashed a pointer to
@@ -273,7 +281,20 @@ int switch_compat(struct domain *d)
          */
         for_each_vcpu ( d, v )
             vcpu_info_reset(v);
-        put_page(virt_to_page(prev));
+
+        /*
+         * Drop the allocation reference, which takes the page off the
+         * domain's xenheap list, and free the page once an RCU grace period
+         * has passed: do_settime() may still be writing through a pointer to
+         * it.  The reference taken on the domain keeps it alive until then,
+         * and keeps domain_destroy(), the only other user of d->rcu, from
+         * queueing it while the callback is pending.
+         */
+        put_page_alloc_ref(virt_to_page(prev));
+        ASSERT(!d->arch.pv.old_shared_info);
+        d->arch.pv.old_shared_info = prev;
+        get_knownalive_domain(d);
+        call_rcu(&d->rcu, _free_old_shared_info);
     }
 
     for_each_vcpu( d, v )
Signature · 1 line
2.56.0
#1 lore raw
Quoted 44 lines · George Dunlap

For a 32-bit PV guest, switch_compat() replaces a shared_info page above 4GiB with one below, and drops the old page with a bare put_page().

Unfortunately, this violates the expected xenheap-page-shared-with-guest freeing discipline. Xenheap pages don't start with a reference count, and aren't put back into the allocation pool after their reference count drops to zero; they're removed from the domain's xenheap list, but someone needs to explicitly call free_xenheap_page() on them for them to return to their pool.

d->shared_info here only has a non-zero reference count because it was shared with a guest; and the thing that holds that reference is the PGC_allocated bit. Dropping the reference without having been the one to clear the PGC_allocated bit puts the page in an invalid state.

If there are no outstanding references to d->shared_info when switch_compat() happens, the result for pages moved this way is merely a memory leak: the page is removed from the xenheap list, and nothing ever calls free_xenheap_page() on it. So whenever this path is triggered, a page is leaked.

Theoretically, if there *is* an outstanding reference to d->shared_info, *and* that reference is still held when the domain is destroyed, then the result is a reference count underflow: Because the reference count never drops to zero, the page stays on the xenheap list; then during domain destruction, relinquish_memory() walks the list, sees the PGC_allocated bit, and calls put_page() again -- potentially dropping the page count to zero. When the other reference holder drops their reference, the count underflows. (put_page() only ASSERT()s that the count is non-zero, so this wouldn't cause a crash in a release build; and the page is never freed, so other potential hazards of general ref count underflow are avoided.) Fortunately this is impossible with the in-tree toolstack, which only switches a domain while building or restoring it, and doesn't have a mapping of shared_info that it holds across that call.

Release the page the way domain destruction releases d->shared_info:
drop the allocation reference with put_page_alloc_ref(), as relinquish_memory() does, which clears PGC_allocated and takes the page off the xenheap list once nothing else refers to it, and free it with free_xenheap_page() once an RCU grace period has passed.

Quoted 13 lines

The free waits for the grace period because do_settime() writes every domain's wallclock holding only wc_lock and the domain list's RCU read lock, neither of which excludes switch_compat(), through a pointer it may have read before the switch. The RCU callback uses the domain's own RCU head, d->rcu, and holds a reference to the domain. That reference keeps the domain from being destroyed, and its struct freed, before the callback has run. It also keeps d->rcu's only other user, domain_destroy(), from queueing the head in the meantime; if the callback drops the last reference, domain_destroy() queues d->rcu again, which RCU has finished with by then. No other reference to the page is expected: with no memory, the domain has no page tables that could map it, and the in-tree toolstack holds no mapping of it across the switch.

Yet there's still the theoretical possibility of there being some other mapping. In such a case imo we'd better avoid the freeing, accepting the leak (with a log message perhaps).

Quoted 7 lines

Also allocate the new page into a local variable rather than straight into d->shared_info: if the allocation failed, a concurrent do_settime() could dereference the NULL pointer before the old value was restored.

Fixes 8a9255405ca6 ("x86/domain: adjust limitation on shared_info allocation below 4G")

Did I? (I don't seem to recall.)

Jan

#2 lore raw
Quoted 63 lines · Jan Beulich
Quoted 44 lines · George Dunlap

For a 32-bit PV guest, switch_compat() replaces a shared_info page above 4GiB with one below, and drops the old page with a bare put_page().

Unfortunately, this violates the expected xenheap-page-shared-with-guest freeing discipline. Xenheap pages don't start with a reference count, and aren't put back into the allocation pool after their reference count drops to zero; they're removed from the domain's xenheap list, but someone needs to explicitly call free_xenheap_page() on them for them to return to their pool.

d->shared_info here only has a non-zero reference count because it was shared with a guest; and the thing that holds that reference is the PGC_allocated bit. Dropping the reference without having been the one to clear the PGC_allocated bit puts the page in an invalid state.

If there are no outstanding references to d->shared_info when switch_compat() happens, the result for pages moved this way is merely a memory leak: the page is removed from the xenheap list, and nothing ever calls free_xenheap_page() on it. So whenever this path is triggered, a page is leaked.

Theoretically, if there *is* an outstanding reference to d->shared_info, *and* that reference is still held when the domain is destroyed, then the result is a reference count underflow: Because the reference count never drops to zero, the page stays on the xenheap list; then during domain destruction, relinquish_memory() walks the list, sees the PGC_allocated bit, and calls put_page() again -- potentially dropping the page count to zero. When the other reference holder drops their reference, the count underflows. (put_page() only ASSERT()s that the count is non-zero, so this wouldn't cause a crash in a release build; and the page is never freed, so other potential hazards of general ref count underflow are avoided.) Fortunately this is impossible with the in-tree toolstack, which only switches a domain while building or restoring it, and doesn't have a mapping of shared_info that it holds across that call.

Release the page the way domain destruction releases d->shared_info:
drop the allocation reference with put_page_alloc_ref(), as relinquish_memory() does, which clears PGC_allocated and takes the page off the xenheap list once nothing else refers to it, and free it with free_xenheap_page() once an RCU grace period has passed.

Quoted 13 lines

The free waits for the grace period because do_settime() writes every domain's wallclock holding only wc_lock and the domain list's RCU read lock, neither of which excludes switch_compat(), through a pointer it may have read before the switch. The RCU callback uses the domain's own RCU head, d->rcu, and holds a reference to the domain. That reference keeps the domain from being destroyed, and its struct freed, before the callback has run. It also keeps d->rcu's only other user, domain_destroy(), from queueing the head in the meantime; if the callback drops the last reference, domain_destroy() queues d->rcu again, which RCU has finished with by then. No other reference to the page is expected: with no memory, the domain has no page tables that could map it, and the in-tree toolstack holds no mapping of it across the switch.

Yet there's still the theoretical possibility of there being some other mapping. In such a case imo we'd better avoid the freeing, accepting the leak (with a log message perhaps).

[snip]

Quoted 3 lines
Quoted 1 line

Did I? (I don't seem to recall.)

I sent an earlier version of the patch to the security team due to the theoretical underflow (which was in the end deemed not a security issue); that version kept the page and freed it when the domain was destroyed. In response, you wrote:

8<---

As to the patch: I understand the need for deferred freeing of the page, but does it really need deferring as much? Wouldn't RCU-based freeing suffice (there can't be any other references to this page without the domain having been launched, or without the domain having at least some memory assigned to it, so it could have page tables referencing the page)?

--->8

Which, naturally, required me to become familiar with how Xen's RCU system worked enough to get it implemented. (Claude did the writing, but I need to understand things well enough to direct it and review the results.) I don't think it's unreasonable for me to be annoyed that you're now saying you want it back the way I wrote it originally.

Now that we have the RCU version implemented and tested, I think we should keep it. It's safe now, and we can keep it safe in the future. If you want we could add a comment to the field warning that no references should be taken to it because it may change; but honestly that seems like overkill to me.

-George

#3 lore raw
Quoted 78 lines · George Dunlap
Quoted 63 lines · Jan Beulich
Quoted 44 lines · George Dunlap

For a 32-bit PV guest, switch_compat() replaces a shared_info page above 4GiB with one below, and drops the old page with a bare put_page().

Unfortunately, this violates the expected xenheap-page-shared-with-guest freeing discipline. Xenheap pages don't start with a reference count, and aren't put back into the allocation pool after their reference count drops to zero; they're removed from the domain's xenheap list, but someone needs to explicitly call free_xenheap_page() on them for them to return to their pool.

d->shared_info here only has a non-zero reference count because it was shared with a guest; and the thing that holds that reference is the PGC_allocated bit. Dropping the reference without having been the one to clear the PGC_allocated bit puts the page in an invalid state.

If there are no outstanding references to d->shared_info when switch_compat() happens, the result for pages moved this way is merely a memory leak: the page is removed from the xenheap list, and nothing ever calls free_xenheap_page() on it. So whenever this path is triggered, a page is leaked.

Theoretically, if there *is* an outstanding reference to d->shared_info, *and* that reference is still held when the domain is destroyed, then the result is a reference count underflow: Because the reference count never drops to zero, the page stays on the xenheap list; then during domain destruction, relinquish_memory() walks the list, sees the PGC_allocated bit, and calls put_page() again -- potentially dropping the page count to zero. When the other reference holder drops their reference, the count underflows. (put_page() only ASSERT()s that the count is non-zero, so this wouldn't cause a crash in a release build; and the page is never freed, so other potential hazards of general ref count underflow are avoided.) Fortunately this is impossible with the in-tree toolstack, which only switches a domain while building or restoring it, and doesn't have a mapping of shared_info that it holds across that call.

Release the page the way domain destruction releases d->shared_info:
drop the allocation reference with put_page_alloc_ref(), as relinquish_memory() does, which clears PGC_allocated and takes the page off the xenheap list once nothing else refers to it, and free it with free_xenheap_page() once an RCU grace period has passed.

Quoted 13 lines

The free waits for the grace period because do_settime() writes every domain's wallclock holding only wc_lock and the domain list's RCU read lock, neither of which excludes switch_compat(), through a pointer it may have read before the switch. The RCU callback uses the domain's own RCU head, d->rcu, and holds a reference to the domain. That reference keeps the domain from being destroyed, and its struct freed, before the callback has run. It also keeps d->rcu's only other user, domain_destroy(), from queueing the head in the meantime; if the callback drops the last reference, domain_destroy() queues d->rcu again, which RCU has finished with by then. No other reference to the page is expected: with no memory, the domain has no page tables that could map it, and the in-tree toolstack holds no mapping of it across the switch.

Yet there's still the theoretical possibility of there being some other mapping. In such a case imo we'd better avoid the freeing, accepting the leak (with a log message perhaps).

[snip]

Quoted 3 lines
Quoted 1 line

Did I? (I don't seem to recall.)

I sent an earlier version of the patch to the security team due to the theoretical underflow (which was in the end deemed not a security issue); that version kept the page and freed it when the domain was destroyed. In response, you wrote:

8<---

Quoted 8 lines

As to the patch: I understand the need for deferred freeing of the page, but does it really need deferring as much? Wouldn't RCU-based freeing suffice (there can't be any other references to this page without the domain having been launched, or without the domain having at least some memory assigned to it, so it could have page tables referencing the page)?

--->8

Oh, I see.

Quoted 5 lines

Which, naturally, required me to become familiar with how Xen's RCU system worked enough to get it implemented. (Claude did the writing, but I need to understand things well enough to direct it and review the results.) I don't think it's unreasonable for me to be annoyed that you're now saying you want it back the way I wrote it originally.

But I didn't ask for that. Now that I read your reply, I think I see how you may have extended my reply to arrive there, though.

What I asked for here is that we free the page if its refcount is zero (the common case), exactly as you do (via RCU). Yet we would skip the freeing (and leak the page) in the unexpected case of the refcount not being zero after the put_page_alloc_ref().

Quoted 5 lines

Now that we have the RCU version implemented and tested, I think we should keep it. It's safe now, and we can keep it safe in the future. If you want we could add a comment to the field warning that no references should be taken to it because it may change; but honestly that seems like overkill to me.

For the field I'm not concerned - it's purpose is clear, and anyone trying to add obtaining of a ref using it as input will hopefully be caught the latest during review. It's a ref existing ahead of the put_page_alloc_ref() which worries me (some).

Jan

#4 lore raw
Quoted 4 lines · Jan Beulich

What I asked for here is that we free the page if its refcount is zero (the common case), exactly as you do (via RCU). Yet we would skip the freeing (and leak the page) in the unexpected case of the refcount not being zero after the put_page_alloc_ref().

OK, I think I see. Yes, I'd considered that. I mean, to be honest, the whole xenheap allocation thing is janky; somehow we've taken a useful, well-understood primitive that should work simply and consistently (dropping the last ref causes a free) and added extra bits so that it's fragile once again. (If shared_info were a domheap page, do_settime() would just grab a ref, map & access the page, then drop the ref, knowing the page wouldn't be freed while it held the ref, and *would* once it was dropped.)

Given that dropping a ref doesn't automatically / atomically free the page, it seems like the rule should be that you *never* grab a ref to a xenheap page, unless you've gone through the rest of the code and verified that the freeing path works correctly.

And given *that*, since nobody else is currently grabbing the ref, not checking the refcount should be safe.

I suppose you'll say, sure, but this is a non-hot path, and if we ever *do* free with an outstanding refcount it's a security issue, so why not be safe? You have to draw the line on internal consistency checks *somewhere*; I'd leave the check off, but I can add it if you really want.

Quoted 10 lines
Quoted 5 lines

Now that we have the RCU version implemented and tested, I think we should keep it. It's safe now, and we can keep it safe in the future. If you want we could add a comment to the field warning that no references should be taken to it because it may change; but honestly that seems like overkill to me.

For the field I'm not concerned - it's purpose is clear, and anyone trying to add obtaining of a ref using it as input will hopefully be caught the latest during review. It's a ref existing ahead of the put_page_alloc_ref() which worries me (some).

For clarity, I meant a comment on d->shared_info, saying not to retain pointers into it outside of a domain rcu_lock; and I guess now also adding not to grab an additional reference to it without coordinating free-on-last-reference properly. But as I said, both should be a general rule, not something we need to add to each data structure affected.

-George

#5 lore raw
Quoted 20 lines · George Dunlap
Quoted 4 lines · Jan Beulich

What I asked for here is that we free the page if its refcount is zero (the common case), exactly as you do (via RCU). Yet we would skip the freeing (and leak the page) in the unexpected case of the refcount not being zero after the put_page_alloc_ref().

OK, I think I see. Yes, I'd considered that. I mean, to be honest, the whole xenheap allocation thing is janky; somehow we've taken a useful, well-understood primitive that should work simply and consistently (dropping the last ref causes a free) and added extra bits so that it's fragile once again. (If shared_info were a domheap page, do_settime() would just grab a ref, map & access the page, then drop the ref, knowing the page wouldn't be freed while it held the ref, and *would* once it was dropped.)

Given that dropping a ref doesn't automatically / atomically free the page, it seems like the rule should be that you *never* grab a ref to a xenheap page, unless you've gone through the rest of the code and verified that the freeing path works correctly.

Quoted 8 lines

And given *that*, since nobody else is currently grabbing the ref, not checking the refcount should be safe.

I suppose you'll say, sure, but this is a non-hot path, and if we ever *do* free with an outstanding refcount it's a security issue, so why not be safe? You have to draw the line on internal consistency checks *somewhere*; I'd leave the check off, but I can add it if you really want.

But is this only an _internal_ consistency check? Can't a toolstack or DM map a domain's shared-info page (as soon as it exists)? IOW I didn't think of the check as a consistency one at all, but rather as a guard towards e.g. toolstack behavior changing.

Quoted 11 lines
Quoted 10 lines
Quoted 5 lines

Now that we have the RCU version implemented and tested, I think we should keep it. It's safe now, and we can keep it safe in the future. If you want we could add a comment to the field warning that no references should be taken to it because it may change; but honestly that seems like overkill to me.

For the field I'm not concerned - it's purpose is clear, and anyone trying to add obtaining of a ref using it as input will hopefully be caught the latest during review. It's a ref existing ahead of the put_page_alloc_ref() which worries me (some).

Quoted 6 lines

For clarity, I meant a comment on d->shared_info, saying not to retain pointers into it outside of a domain rcu_lock; and I guess now also adding not to grab an additional reference to it without coordinating free-on-last-reference properly. But as I said, both should be a general rule, not something we need to add to each data structure affected.

Okay, looks like we agree here then that no new commentary needs adding.

Jan

#6 lore raw
Quoted 10 lines · Jan Beulich
Quoted 5 lines

I suppose you'll say, sure, but this is a non-hot path, and if we ever *do* free with an outstanding refcount it's a security issue, so why not be safe? You have to draw the line on internal consistency checks *somewhere*; I'd leave the check off, but I can add it if you really want.

But is this only an _internal_ consistency check? Can't a toolstack or DM map a domain's shared-info page (as soon as it exists)? IOW I didn't think of the check as a consistency one at all, but rather as a guard towards e.g. toolstack behavior changing.

Oh, I see. Actually, Claude thinks that ATM libxc has a bug in this respect on the restore path: It reads and stores the frame number for the shared info page *before* the switch; then maps the stale frame number when it fills in the restored data. If the page is moved, then on staging the refcount will be zero, get_page() will return an error and the restore operation should fail.

So I'll add a leak-if-nonzero-refcount check to this patch, and add a second patch making sure we don't retain a shared_info frame number across a switch.

-George