[PATCH 2/7] x86/mm: introduce populate_perdomain_mapping()
George Dunlap <[email protected]>
| Newsgroups | org.xenproject.lists.xen-devel |
|---|---|
| Message-ID | <[email protected]> |
From: Roger Pau Monné <[email protected]> The per-domain area already has central machinery for building its page-tables and for managing the backing pages it owns itself: create_perdomain_mapping() / destroy_perdomain_mapping(), used by the mapcache bitmaps, the compat argument-translation area, and the GDT/LDT slots alike. What the interface lacks is a way to install a caller's own pages at a chosen address. The PV GDT and LDT code open-codes its own modifications to the per-domain area, capturing aliases of its L1 tables at creation time (create_perdomain_mapping()'s pl1tab argument) and stashing them in d->arch.pv.gdt_ldt_l1tab. Introduce populate_perdomain_mapping(v, va, mfn, nr, flags) to close this gap, giving the perdomain area's rules a single place to live. populate_perdomain_mapping writes the given MFNs, with the given page-table flags, into v's view of the per-domain area by walking its per-domain page-tables. Those are xenheap pages, reached through their always-mapped alias, so the walk involves no mapping and is usable from any context -- including the context switch, before the incoming vcpu's page-tables are loaded. Callers don't need to know where the page-tables live, how the area is structured, or whether it is per-domain or per-vcpu. We require the range to already have been populated down to the L1 tables by create_perdomain_mapping(). TLB flushing is left to the caller. A present entry not owned by the area (!_PAGE_AVAIL0) is replaced. A present entry owned by the area (_PAGE_AVAIL0, installed by create_perdomain_mapping() itself) is freed and replaced: such a page is referenced only by the mapping, so displacing it without freeing it would leak it. Nothing in this series replaces area-owned backing, so the free is marked ASSERT_UNREACHABLE(); note that freeing requires a context where the allocator may be entered -- IRQs enabled, not in interrupt context (see ASSERT_ALLOC_CONTEXT()) -- so any future caller replacing area-owned backing must not do so from the context switch path. Missing page-table structure is a hypervisor bug and BUG_ON(): there is no safe continuation, least of all from the context switch, where the next descriptor fetch through an unmapped GDT slot would be fatal. Subsequent patches convert the users of the stashed L1 tables to this interface, starting with the Xen slots of the full GDT; the stash -- which could in any case not represent per-vcpu mappings without being replicated for every vcpu and slot -- is then removed, leaving create_perdomain_mapping() to manage only the page-table structure and the pages the area owns itself. Later parts of the series use the new interface for their own mappings rather than adding further mechanisms. Signed-off-by: Roger Pau Monné <[email protected]> Assisted-by: Claude Code:claude-fable-5, Claude Code:claude-opus-4-8 Signed-off-by: George Dunlap <[email protected]> --- Changes since the previously posted version: - Split the introduction of populate_perdomain_mapping() from its first user (previously one patch: "x86/pv: introduce function to populate perdomain area and use it to map Xen GDT"). - Drop the linear-map fast path and the sync_local_execstate() call: with the per-domain page-tables in the xenheap (previous patch) the walk needs no mapping, so a single path serves all callers and contexts. - Keep the ASSERT_UNREACHABLE() + free_domheap_page() handling of a replaced area-owned entry, and document the allocation-context requirement it places on callers replacing such entries. BUG_ON() missing page-table structure, instead of domain_crash(). - Take the page-table flags as a parameter (the Xen GDT and guest GDT slots want RW mappings; the zero page backing torn-down GDT slots is mapped read-only, as today). - Document the contract in a header comment. - Make the mfn parameter const and nr unsigned int, matching {create,destroy}_perdomain_mapping(). - Drop the unused cr3_mfn() helper. Considered, but not done to limit churn against the previously posted version: splitting the interface into a "populate" variant (any present entry is a bug) and an "update" variant (replacement expected), so that call sites declare their intent and unexpected collisions become detectable. Of the eventual call sites in the wider series, roughly half are of each kind. Could be done as a follow-up if there is interest. --- xen/arch/x86/include/asm/mm.h | 3 ++ xen/arch/x86/mm.c | 68 +++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) diff --git a/xen/arch/x86/include/asm/mm.h b/xen/arch/x86/include/asm/mm.h index 2254a7e3fe..1888807394 100644 --- a/xen/arch/x86/include/asm/mm.h +++ b/xen/arch/x86/include/asm/mm.h @@ -606,6 +606,9 @@ int compat_arch_memory_op(unsigned long cmd, XEN_GUEST_HANDLE_PARAM(void) arg); int create_perdomain_mapping(struct domain *d, unsigned long va, unsigned int nr, l1_pgentry_t **pl1tab, struct page_info **ppg); +void populate_perdomain_mapping(const struct vcpu *v, unsigned long va, + const mfn_t *mfn, unsigned int nr, + unsigned int flags); void destroy_perdomain_mapping(struct domain *d, unsigned long va, unsigned int nr); void free_perdomain_mappings(struct domain *d); diff --git a/xen/arch/x86/mm.c b/xen/arch/x86/mm.c index 38a0f984fc..1810971677 100644 --- a/xen/arch/x86/mm.c +++ b/xen/arch/x86/mm.c @@ -6311,6 +6311,74 @@ int create_perdomain_mapping(struct domain *d, unsigned long va, return rc; } +/* + * Map @nr pages, @mfn[0..nr-1], at consecutive pages from @va in v's view of + * the per-domain area, with page-table @flags. The range must lie within a + * single per-domain slot, and must already have been plumbed down to the L1 + * tables by create_perdomain_mapping(): missing structure is a bug. A + * present entry not owned by the area (no _PAGE_AVAIL0) is silently + * replaced, as that is how callers update their mappings; a present + * area-owned entry is freed and replaced, which constrains the calling + * context (see the comment in the body). No TLB flushing is done: the + * caller decides whether the old translations can still be cached + * anywhere. + * + * The walk goes through the always-mapped xenheap alias of the per-domain + * page-tables, so it needs nothing from the current address space and is + * usable from any context -- including the context switch, before the + * incoming vcpu's page-tables are loaded. + */ +void populate_perdomain_mapping(const struct vcpu *v, unsigned long va, + const mfn_t *mfn, unsigned int nr, + unsigned int flags) +{ + l1_pgentry_t *l1tab = NULL, *pl1e; + const l3_pgentry_t *l3tab; + const l2_pgentry_t *l2tab; + + ASSERT(va >= PERDOMAIN_VIRT_START && + va < PERDOMAIN_VIRT_SLOT(PERDOMAIN_SLOTS)); + ASSERT(!nr || !l3_table_offset(va ^ (va + nr * PAGE_SIZE - 1))); + /* Area-owned pages are installed by create_perdomain_mapping() only. */ + ASSERT(!(flags & _PAGE_AVAIL0)); + + l3tab = v->domain->arch.perdomain_l3; + BUG_ON(!l3tab); + BUG_ON(!(l3e_get_flags(l3tab[l3_table_offset(va)]) & _PAGE_PRESENT)); + + l2tab = maddr_to_virt(l3e_get_paddr(l3tab[l3_table_offset(va)])); + + for ( ; nr--; va += PAGE_SIZE, mfn++ ) + { + if ( !l1tab || !l1_table_offset(va) ) + { + const l2_pgentry_t *pl2e = l2tab + l2_table_offset(va); + + BUG_ON(!(l2e_get_flags(*pl2e) & _PAGE_PRESENT)); + l1tab = maddr_to_virt(l2e_get_paddr(*pl2e)); + } + + pl1e = &l1tab[l1_table_offset(va)]; + + /* + * An area-owned entry (installed by create_perdomain_mapping(), + * marked _PAGE_AVAIL0) holds the only reference to its page, so + * displacing it means freeing it. Nothing in this series replaces + * area-owned backing, hence the ASSERT_UNREACHABLE(); any future + * caller doing so must run where freeing is permitted -- IRQs + * enabled, not in interrupt context (see ASSERT_ALLOC_CONTEXT()) + * -- which the context switch path is not. + */ + if ( unlikely(perdomain_l1e_needs_freeing(*pl1e)) ) + { + ASSERT_UNREACHABLE(); + free_domheap_page(l1e_get_page(*pl1e)); + } + + l1e_write(pl1e, l1e_from_mfn(*mfn, flags)); + } +} + void destroy_perdomain_mapping(struct domain *d, unsigned long va, unsigned int nr) { -- 2.55.0