gnttab: cope with version changes racing other operations

Dropping and re-acquiring the grant table lock for a particular operation
requires special care, as in the meantime the grant table version can
change.

During a v2 -> v1 change, status frames going away means that pre-
calculated status pointers go stale, referencing freed (and possibly
already re-used) memory. Record in-flight v2 operations, permitting the
version change only when there are none of them. Recalculate "status" in
the one place (map_grant_ref()'s error path) where it could be stale, but
confine this to reserved entries.

This is CVE-2026-62436.

Reported-by: Mark Esler <mark@hexproof.dev>

During a v1 -> v2 change, the number of shared table entries reduces,
meaning that previously validated grant references may now be out of
bounds. Because of the checking of pin counts in gnttab_set_version()
(with the grant table lock held for writing), for now-out-of-bounds gref-s
neither active mappings can exist, nor can there be in-progress copy
operations. Nevertheless bounds checks are added there, just to be on the
safe side.

For gnttab_transfer(), to cover the gap between the lock being dropped by
gnttab_prepare_for_transfer() and it being re-acquired, have the helper
return the version it found, and fail the operation if the version turns
out to have changed after re-acquiring the lock.

Further avoid needless use of shared_entry_header(), as it involves
pointer arithmetic which, when using an out-of-bounds ref, is UB.

This is CVE-2026-62435.

Everything together is XSA-501.

Fixes: a98dc13703 ("Introduce a grant_entry_v2 structure")
Signed-off-by: Jan Beulich <jbeulich@suse.com>
Tested-by: Mark Esler <mark@hexproof.dev>
Reviewed-by: Andrew Cooper <andrew.cooper3@citrix.com>
This commit is contained in:
Jan Beulich authored and Andrew Cooper committed 2026-07-28 12:40:10 +01:00
1 parent 61b0a59ce2
commit 96dfeb4177
1 file changed
+73 -20
+73 -20
View File
@@ -71,6 +71,10 @@ struct grant_table {
unsigned int nr_grant_frames; unsigned int nr_grant_frames;
/* Number of grant status frames shared with guest (for version 2) */ /* Number of grant status frames shared with guest (for version 2) */
unsigned int nr_status_frames; unsigned int nr_status_frames;
/* Number of version 2 operations in progress. */
atomic_t nr_v2_ops;
/* /*
* Number of available maptrack entries. For cleanup purposes it is * Number of available maptrack entries. For cleanup purposes it is
* important to realize that this field and @maptrack further down will * important to realize that this field and @maptrack further down will
@@ -933,6 +937,9 @@ static void reduce_status_for_pin(struct domain *rd,
{ {
unsigned int clear_flags = act->pin ? 0 : GTF_reading; unsigned int clear_flags = act->pin ? 0 : GTF_reading;
if ( unlikely(!status) )
return;
if ( !readonly && !(act->pin & (GNTPIN_hstw_mask | GNTPIN_devw_mask)) ) if ( !readonly && !(act->pin & (GNTPIN_hstw_mask | GNTPIN_devw_mask)) )
clear_flags |= GTF_writing; clear_flags |= GTF_writing;
@@ -1343,6 +1350,22 @@ map_grant_ref(
grant_read_lock(rgt); grant_read_lock(rgt);
if ( unlikely(evaluate_nospec((rgt->gt_version == 1) !=
(status == &shah->flags))) )
{
/*
* After a v1 -> v2 change behind our backs "ref" may now be out of
* bounds. Recalculate it, but only for reserved entries. Others
* will have been cleared anyway by the version change.
*/
if ( ref < GNTTAB_NR_RESERVED_ENTRIES )
status = evaluate_nospec(rgt->gt_version == 1)
? &shah->flags
: &status_entry(rgt, ref);
else
status = NULL;
}
act = active_entry_acquire(rgt, op->ref); act = active_entry_acquire(rgt, op->ref);
act->pin -= pin_incr; act->pin -= pin_incr;
@@ -1586,9 +1609,8 @@ unmap_common_complete(struct gnttab_unmap_common *op)
struct domain *ld, *rd = op->rd; struct domain *ld, *rd = op->rd;
struct grant_table *rgt; struct grant_table *rgt;
struct active_grant_entry *act; struct active_grant_entry *act;
grant_entry_header_t *sha;
struct page_info *pg; struct page_info *pg;
uint16_t *status; uint16_t *status = NULL;
if ( evaluate_nospec(!op->done) ) if ( evaluate_nospec(!op->done) )
{ {
@@ -1604,11 +1626,10 @@ unmap_common_complete(struct gnttab_unmap_common *op)
grant_read_lock(rgt); grant_read_lock(rgt);
act = active_entry_acquire(rgt, op->ref); act = active_entry_acquire(rgt, op->ref);
sha = shared_entry_header(rgt, op->ref);
if ( evaluate_nospec(rgt->gt_version == 1) ) if ( evaluate_nospec(rgt->gt_version == 1) )
status = &sha->flags; status = &shared_entry_v1(rgt, op->ref).flags;
else else if ( evaluate_nospec(op->ref < nr_grant_entries(rgt)) )
status = &status_entry(rgt, op->ref); status = &status_entry(rgt, op->ref);
pg = !is_iomem_page(act->mfn) ? mfn_to_page(op->mfn) : NULL; pg = !is_iomem_page(act->mfn) ? mfn_to_page(op->mfn) : NULL;
@@ -2196,14 +2217,14 @@ gnttab_query_size(
* Check that the given grant reference (rd,ref) allows 'ld' to transfer * Check that the given grant reference (rd,ref) allows 'ld' to transfer
* ownership of a page frame. If so, lock down the grant entry. * ownership of a page frame. If so, lock down the grant entry.
*/ */
static int static unsigned int
gnttab_prepare_for_transfer( gnttab_prepare_for_transfer(
struct domain *rd, struct domain *ld, grant_ref_t ref) struct domain *rd, struct domain *ld, grant_ref_t ref)
{ {
struct grant_table *rgt = rd->grant_table; struct grant_table *rgt = rd->grant_table;
uint32_t *raw_shah; uint32_t *raw_shah;
union grant_combo scombo; union grant_combo scombo;
int retries = 0; unsigned int retries = 0, ver;
grant_read_lock(rgt); grant_read_lock(rgt);
@@ -2248,8 +2269,11 @@ gnttab_prepare_for_transfer(
scombo = prev; scombo = prev;
} }
ver = rgt->gt_version;
grant_read_unlock(rgt); grant_read_unlock(rgt);
return 1;
return ver;
fail: fail:
grant_read_unlock(rgt); grant_read_unlock(rgt);
@@ -2274,7 +2298,7 @@ gnttab_transfer(
for ( i = 0; i < count; i++ ) for ( i = 0; i < count; i++ )
{ {
bool okay; unsigned int ver;
int rc; int rc;
if ( i && hypercall_preempt_check() ) if ( i && hypercall_preempt_check() )
@@ -2414,14 +2438,14 @@ gnttab_transfer(
* pagelist. * pagelist.
*/ */
nrspin_unlock(&e->page_alloc_lock); nrspin_unlock(&e->page_alloc_lock);
okay = gnttab_prepare_for_transfer(e, d, gop.ref); ver = gnttab_prepare_for_transfer(e, d, gop.ref);
/* /*
* Make sure the reference bound check in gnttab_prepare_for_transfer * Make sure the reference bound check in gnttab_prepare_for_transfer
* is respected and speculative execution is blocked accordingly * is respected and speculative execution is blocked accordingly
*/ */
if ( unlikely(!evaluate_nospec(okay)) || if ( unlikely(!evaluate_nospec(ver)) ||
unlikely(assign_pages(page, 1, e, MEMF_no_refcount)) ) unlikely(assign_pages(page, 1, e, MEMF_no_refcount)) )
{ {
bool drop_dom_ref; bool drop_dom_ref;
@@ -2433,7 +2457,7 @@ gnttab_transfer(
drop_dom_ref = !domain_adjust_tot_pages(e, -1); drop_dom_ref = !domain_adjust_tot_pages(e, -1);
nrspin_unlock(&e->page_alloc_lock); nrspin_unlock(&e->page_alloc_lock);
if ( okay /* i.e. e->is_dying due to the surrounding if() */ ) if ( ver /* i.e. e->is_dying due to the surrounding if() */ )
gdprintk(XENLOG_INFO, "Transferee d%d is now dying\n", gdprintk(XENLOG_INFO, "Transferee d%d is now dying\n",
e->domain_id); e->domain_id);
@@ -2453,7 +2477,13 @@ gnttab_transfer(
grant_read_lock(e->grant_table); grant_read_lock(e->grant_table);
act = active_entry_acquire(e->grant_table, gop.ref); act = active_entry_acquire(e->grant_table, gop.ref);
if ( evaluate_nospec(e->grant_table->gt_version == 1) ) if ( unlikely(evaluate_nospec(e->grant_table->gt_version != ver)) )
{
rc = -EILSEQ;
goto release;
}
if ( evaluate_nospec(ver == 1) )
{ {
grant_entry_v1_t *sha = &shared_entry_v1(e->grant_table, gop.ref); grant_entry_v1_t *sha = &shared_entry_v1(e->grant_table, gop.ref);
@@ -2473,6 +2503,7 @@ gnttab_transfer(
shared_entry_header(e->grant_table, gop.ref)->flags |= shared_entry_header(e->grant_table, gop.ref)->flags |=
GTF_transfer_completed; GTF_transfer_completed;
release:
active_entry_release(act); active_entry_release(act);
grant_read_unlock(e->grant_table); grant_read_unlock(e->grant_table);
@@ -2501,28 +2532,27 @@ release_grant_for_copy(
struct domain *rd, grant_ref_t gref, bool readonly) struct domain *rd, grant_ref_t gref, bool readonly)
{ {
struct grant_table *rgt = rd->grant_table; struct grant_table *rgt = rd->grant_table;
grant_entry_header_t *sha;
struct active_grant_entry *act; struct active_grant_entry *act;
mfn_t mfn; mfn_t mfn;
uint16_t *status; uint16_t *status = NULL;
grant_ref_t trans_gref; grant_ref_t trans_gref;
struct domain *td; struct domain *td;
grant_read_lock(rgt); grant_read_lock(rgt);
act = active_entry_acquire(rgt, gref); act = active_entry_acquire(rgt, gref);
sha = shared_entry_header(rgt, gref);
mfn = act->mfn; mfn = act->mfn;
if ( evaluate_nospec(rgt->gt_version == 1) ) if ( evaluate_nospec(rgt->gt_version == 1) )
{ {
status = &sha->flags; status = &shared_entry_v1(rgt, gref).flags;
td = rd; td = rd;
trans_gref = gref; trans_gref = gref;
} }
else else
{ {
status = &status_entry(rgt, gref); if ( evaluate_nospec(gref < nr_grant_entries(rgt)) )
status = &status_entry(rgt, gref);
td = (act->src_domid == rd->domain_id) td = (act->src_domid == rd->domain_id)
? rd : knownalive_domain_from_domid(act->src_domid); ? rd : knownalive_domain_from_domid(act->src_domid);
trans_gref = act->trans_gref; trans_gref = act->trans_gref;
@@ -2541,6 +2571,9 @@ release_grant_for_copy(
reduce_status_for_pin(rd, act, status, readonly); reduce_status_for_pin(rd, act, status, readonly);
if ( !act->pin && act->is_sub_page )
atomic_dec(&rgt->nr_v2_ops);
active_entry_release(act); active_entry_release(act);
grant_read_unlock(rgt); grant_read_unlock(rgt);
@@ -2672,8 +2705,10 @@ acquire_grant_for_copy(
/* /*
* acquire_grant_for_copy() will take the lock on the remote table, * acquire_grant_for_copy() will take the lock on the remote table,
* so we have to drop the lock here and reacquire. * so we have to drop the lock here and reacquire. Before doing so,
* record that a v2 operation is in progress.
*/ */
atomic_inc(&rgt->nr_v2_ops);
active_entry_release(act); active_entry_release(act);
grant_read_unlock(rgt); grant_read_unlock(rgt);
@@ -2687,6 +2722,7 @@ acquire_grant_for_copy(
if ( rc != GNTST_okay ) if ( rc != GNTST_okay )
{ {
atomic_dec(&rgt->nr_v2_ops);
rcu_unlock_domain(td); rcu_unlock_domain(td);
reduce_status_for_pin(rd, act, status, readonly); reduce_status_for_pin(rd, act, status, readonly);
active_entry_release(act); active_entry_release(act);
@@ -2723,6 +2759,8 @@ acquire_grant_for_copy(
rcu_unlock_domain(td); rcu_unlock_domain(td);
grant_read_lock(rgt); grant_read_lock(rgt);
atomic_dec(&rgt->nr_v2_ops);
act = active_entry_acquire(rgt, gref); act = active_entry_acquire(rgt, gref);
reduce_status_for_pin(rd, act, status, readonly); reduce_status_for_pin(rd, act, status, readonly);
active_entry_release(act); active_entry_release(act);
@@ -2749,6 +2787,8 @@ acquire_grant_for_copy(
*/ */
act->is_sub_page = true; act->is_sub_page = true;
} }
else
atomic_dec(&rgt->nr_v2_ops);
} }
else if ( !old_pin || else if ( !old_pin ||
(!readonly && !(old_pin & (GNTPIN_devw_mask|GNTPIN_hstw_mask))) ) (!readonly && !(old_pin & (GNTPIN_devw_mask|GNTPIN_hstw_mask))) )
@@ -2803,6 +2843,9 @@ acquire_grant_for_copy(
act->src_domid = td->domain_id; act->src_domid = td->domain_id;
act->trans_gref = trans_gref; act->trans_gref = trans_gref;
act->mfn = grant_mfn; act->mfn = grant_mfn;
if ( is_sub_page )
atomic_inc(&rgt->nr_v2_ops);
} }
else if ( !mfn_eq(act->mfn, grant_mfn) || else if ( !mfn_eq(act->mfn, grant_mfn) ||
act->src_domid != td->domain_id || act->src_domid != td->domain_id ||
@@ -3232,7 +3275,17 @@ gnttab_set_version(XEN_GUEST_HANDLE_PARAM(gnttab_set_version_t) uop)
if ( res < 0) if ( res < 0)
goto out_unlock; goto out_unlock;
break; break;
case 2: case 2:
if ( atomic_read(&gt->nr_v2_ops) )
{
gdprintk(XENLOG_WARNING,
"tried to change to grant table v1, but %d v2 operations still in progress\n",
atomic_read(&gt->nr_v2_ops));
res = -EAGAIN;
goto out_unlock;
}
for ( i = 0; i < GNTTAB_NR_RESERVED_ENTRIES; i++ ) for ( i = 0; i < GNTTAB_NR_RESERVED_ENTRIES; i++ )
{ {
switch ( shared_entry_v2(gt, i).hdr.flags & GTF_type_mask ) switch ( shared_entry_v2(gt, i).hdr.flags & GTF_type_mask )
Reference in new issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.