diff --git a/config.yaml b/config.yaml index fd0422a..3c4d52f 100644 --- a/config.yaml +++ b/config.yaml @@ -103,15 +103,18 @@ patches: - patch: 0003-x86-amd_node-fix-null-pointer-dereference-if-amd_smn.patch lower: '6.17' - patches: - - 0001-xen-events-add-_on_node-variants-of-the-lateeoi-bind.patch - - 0002-xen-xenbus-expose-host-NUMA-node-of-a-mapped-ring.patch - - 0003-xen-netback-place-per-queue-kthreads-and-IRQs-near-t.patch - - 0004-xen-blkback-place-per-ring-kthread-and-IRQ-near-the-.patch - - 0005-xen-make-xen_alloc_unpopulated_pages-NUMA-aware.patch - - 0006-xen-xenbus-collapse-xenbus_ring_host_node-to-a-page_.patch + - 0001-xen-evtchn-diagnose-ring_overflow-with-post-barrier-.patch + - 0002-xen-add-xen_mfn_to_node-to-resolve-a-foreign-frame-s.patch + - 0003-xen-make-xen_alloc_unpopulated_pages-NUMA-aware.patch + - 0004-xen-grant-table-add-gnttab_alloc_pages_node.patch + - 0005-xen-events-add-_on_node-variants-of-the-lateeoi-bind.patch + - 0006-xen-xenbus-home-ring-mappings-on-the-foreign-frame-s.patch - 0007-xen-xenbus-add-xenbus_setup_ring_node-for-per-node-r.patch - - 0008-xen-netfront-place-per-queue-rings-on-per-queue-node.patch - - 0009-xen-blkfront-place-per-ring-buffers-on-per-hctx-node.patch + - 0008-xen-netback-place-per-queue-kthreads-and-IRQs-near-t.patch + - 0009-xen-blkback-place-per-ring-kthread-and-IRQ-near-the-.patch + - 0010-xen-netfront-place-per-queue-rings-on-per-queue-node.patch + - 0011-xen-blkfront-place-per-ring-buffers-on-per-hctx-node.patch + - 0012-xen-gntdev-home-grant-map-placeholder-pages-on-the-f.patch lower: '6.18' - patches: - 0001-xen-xlate_mmu-relocate-remap_pfn-utils.patch diff --git a/patches/0001-xen-evtchn-diagnose-ring_overflow-with-post-barrier-.patch b/patches/0001-xen-evtchn-diagnose-ring_overflow-with-post-barrier-.patch new file mode 100644 index 0000000..813a715 --- /dev/null +++ b/patches/0001-xen-evtchn-diagnose-ring_overflow-with-post-barrier-.patch @@ -0,0 +1,100 @@ +From 35760e39333ee8b3786ca54eb8e23ad72358b035 Mon Sep 17 00:00:00 2001 +From: Steven Noonan +Date: Mon, 20 Jul 2026 13:49:47 -0700 +Subject: [PATCH 01/12] xen/evtchn: diagnose ring_overflow with post-barrier + cons re-read + +We've been seeing /dev/xen/evtchn fds enter the ring_overflow=1 +state under sustained event load (specifically: many ports bound +to a single fd, all firing concurrently during a daemon-side +metrics-collection storm). Once the flag latches, every read +returns -EFBIG and the fd is dead for event delivery until reset. + +The per-port masking invariant (each bound port can occupy at +most one ring slot, because lateeoi_ack_dynirq masks the channel +at Xen level before evtchn_interrupt runs and userspace can't +unmask until it reads + writes the port back) combined with the +ring being sized to nr_evtchns means overflow should not be +reachable. No "Interrupt for port N, but apparently not enabled" +WARNs precede the EFBIG state on hosts where this happens, so +the masking invariant is holding. + +One plausible candidate: the producer's READ_ONCE(ring_cons) in +evtchn_interrupt has no acquire pairing with the consumer's +WRITE_ONCE in evtchn_read -- they take ring_prod_lock and +ring_cons_mutex respectively, neither of which synchronizes +ring_cons between them. The smp_wmb/smp_rmb pair already +present covers ring entry visibility against ring_prod, not +ring_cons. A producer that fires right after the consumer +drains can observe a stale ring_cons and compute +(prod - stale_cons) >= ring_size when the ring actually has +room. On x86-TSO the staleness window is bounded to +store-buffer drain time but is non-zero; on weakly-ordered +hardware it's wider. + +Add an observe-only diagnostic at the overflow point: before +setting ring_overflow, issue an smp_mb() and re-read ring_cons. +Emit a rate-limited pr_warn with prod, both cons values, both +depths, ring_size, nr_evtchns, and the triggering port's +enabled/unbinding flags, tagging the log with " SPURIOUS" when +the post-barrier depth is below ring_size (i.e., the original +overflow detection was based on a stale view). + +This does not change runtime behavior other than emitting a log +line at the overflow boundary (which already required entering +a path that wedges the fd, so the printk cost is irrelevant). +If the diagnostic confirms the stale-cons hypothesis, the real +fix is to switch ring_cons access to smp_load_acquire / +smp_store_release pairing. + +Signed-off-by: Steven Noonan +--- + drivers/xen/evtchn.c | 31 ++++++++++++++++++++++++++++++- + 1 file changed, 30 insertions(+), 1 deletion(-) + +diff --git a/drivers/xen/evtchn.c b/drivers/xen/evtchn.c +index 7e4a13e632dc..85619b041ee8 100644 +--- a/drivers/xen/evtchn.c ++++ b/drivers/xen/evtchn.c +@@ -189,8 +189,37 @@ static irqreturn_t evtchn_interrupt(int irq, void *data) + kill_fasync(&u->evtchn_async_queue, + SIGIO, POLL_IN); + } +- } else ++ } else { ++ unsigned int cons_now; ++ ++ /* ++ * Diagnostic: the READ_ONCE of ring_cons above has no ++ * acquire pairing with the WRITE_ONCE in evtchn_read -- ++ * the producer holds ring_prod_lock and the consumer ++ * holds ring_cons_mutex, so they don't synchronize on ++ * ring_cons. On weakly-ordered hardware (and within ++ * the x86 store-buffer drain window) the producer can ++ * observe a stale ring_cons and conclude the ring is ++ * full when it actually has room. Re-read with a full ++ * barrier before declaring overflow and tag the log as ++ * SPURIOUS when the post-barrier read shows the ring ++ * had room -- that pins the cause on the cons ++ * visibility race rather than a real fill-to-capacity ++ * or a broken per-port masking invariant. ++ */ ++ smp_mb(); ++ cons_now = READ_ONCE(u->ring_cons); ++ ++ pr_warn_ratelimited( ++ "xen-evtchn overflow on %s: port=%u prod=%u cons=%u cons_after_mb=%u depth=%u depth_after_mb=%u ring_size=%u nr_evtchns=%u enabled=%d unbinding=%d%s\n", ++ u->name, evtchn->port, prod, cons, cons_now, ++ prod - cons, prod - cons_now, ++ u->ring_size, u->nr_evtchns, ++ (int)evtchn->enabled, (int)evtchn->unbinding, ++ (prod - cons_now) < u->ring_size ? " SPURIOUS" : ""); ++ + u->ring_overflow = 1; ++ } + + spin_unlock(&u->ring_prod_lock); + +-- +2.55.0 + diff --git a/patches/0002-xen-add-xen_mfn_to_node-to-resolve-a-foreign-frame-s.patch b/patches/0002-xen-add-xen_mfn_to_node-to-resolve-a-foreign-frame-s.patch new file mode 100644 index 0000000..870c21a --- /dev/null +++ b/patches/0002-xen-add-xen_mfn_to_node-to-resolve-a-foreign-frame-s.patch @@ -0,0 +1,268 @@ +From b675cdf0b8448a460ff207a708aa272532b8a9a3 Mon Sep 17 00:00:00 2001 +From: Steven Noonan +Date: Mon, 20 Jul 2026 13:51:31 -0700 +Subject: [PATCH 02/12] xen: add xen_mfn_to_node to resolve a foreign frame's + host NUMA node + +Dom0 code that grant-maps foreign pages -- xenbus ring mappings, +gntdev -- has no way to learn which host NUMA node backs a foreign +frame, so nothing downstream (kthread and IRQ placement, placeholder +page homing) can be made node-local to the guest being served. Every +backend kthread for every queue piles onto whatever node it happens +to land on, regardless of the guest's vNUMA layout. + +Add xen_mfn_to_node(), resolving one host MFN to a Linux node id via +the new XENMEM_get_mfn_pxms hypercall, along with the hypercall's +interface definition. It lives in grant-table.c because its callers +are the grant-mapping paths that need to home placeholder pages. + +Three NUMA identifier namespaces are involved. Xen returns host PXM +(firmware-supplied), matching what dom0's own SRAT already uses; +pxm_to_node() converts to a Linux dom0 node id, which is what callers +feed to Linux helpers like cpumask_of_node() and +kthread_create_on_node(). Keeping the Xen-side ABI in PXM-space lets +callers translate with one standard lookup instead of maintaining +their own Xen-nid -> Linux-node table. + +The query is meaningful only in the hardware domain: Xen restricts +XENMEM_get_mfn_pxms to it, and a guest has no view of host topology +to localize against in any case. xen_mfn_to_node returns +NUMA_NO_NODE immediately when !xen_initial_domain(), without issuing +the hypercall. + +Older or non-Edera hypervisors lack the hypercall. The first call +returns -ENOSYS, which latches a global "unsupported" flag; every +subsequent call returns NUMA_NO_NODE without entering the +hypervisor. -EPERM (XSM policy, a late hardware domain) latches the +same way, so no boot pays more than one refused attempt. Callers +seeing NUMA_NO_NODE fall back to NUMA-oblivious behaviour, making the +kernel-side change safe to ship without a lockstep hypervisor update. + +Gated by CONFIG_XEN_BACKEND_NUMA_AFFINITY, which depends on +XEN_BACKEND, NUMA, ACPI_NUMA, and XEN_UNPOPULATED_ALLOC and defaults +to y. XEN_UNPOPULATED_ALLOC is a hard dependency rather than a soft +one because the placement work the rest of this series performs is +only meaningful when grant-map placeholders come from a per-node +pool: with the generic balloon allocator, page_to_nid() reflects only +where dom0's free RAM happened to be, and per-ring NUMA placement +would commit to wrong nodes confidently rather than silently no-op. +Disabling the option compiles the query out; the header supplies a +NUMA_NO_NODE stub so callers need no ifdefs. + +Sampling policy is left to callers. The hypercall accepts a batch of +MFNs, though the consumers in this series query only a ring's first +frame. + +Signed-off-by: Steven Noonan +--- + drivers/xen/Kconfig | 25 ++++++++++ + drivers/xen/grant-table.c | 86 ++++++++++++++++++++++++++++++++++ + include/xen/interface/memory.h | 26 ++++++++++ + include/xen/xen.h | 16 +++++++ + 4 files changed, 153 insertions(+) + +diff --git a/drivers/xen/Kconfig b/drivers/xen/Kconfig +index f9a35ed266ec..147e6acb231b 100644 +--- a/drivers/xen/Kconfig ++++ b/drivers/xen/Kconfig +@@ -96,6 +96,31 @@ config XEN_BACKEND + Support for backend device drivers that provide I/O services + to other virtual machines. + ++config XEN_BACKEND_NUMA_AFFINITY ++ bool "NUMA affinity for Xen backend drivers" ++ depends on XEN_BACKEND && NUMA && ACPI_NUMA && XEN_UNPOPULATED_ALLOC ++ default y ++ help ++ Allow Xen backend drivers (netback, blkback, gntdev consumers) ++ to discover the host NUMA node that hosts a grant-mapped ring ++ page, and to place their service threads and IRQs on that node. ++ ++ XEN_UNPOPULATED_ALLOC provides the per-node placeholder-page pool ++ the relocation logic in xenbus_map_ring_valloc() draws from. ++ Without it, placeholders come from the generic balloon allocator, ++ whose page_to_nid() reflects only where dom0's free RAM happened ++ to be -- not the host node of the foreign frame the placeholder ++ will end up backing. In that configuration the per-ring ++ placement decisions would be confidently wrong rather than just ++ absent, so the Kconfig hard-depends on XEN_UNPOPULATED_ALLOC ++ rather than silently degrading. ++ ++ Requires hypervisor support for the XENMEM_get_mfn_pxms ++ hypercall. Without that support the feature is silently a ++ no-op, equivalent to NUMA-oblivious behaviour. ++ ++ If unsure, say Y. ++ + config XENFS + tristate "Xen filesystem" + select XEN_PRIVCMD +diff --git a/drivers/xen/grant-table.c b/drivers/xen/grant-table.c +index 478d2ad725ac..f8b86a8679de 100644 +--- a/drivers/xen/grant-table.c ++++ b/drivers/xen/grant-table.c +@@ -67,6 +67,10 @@ + + #include + ++#ifdef CONFIG_XEN_BACKEND_NUMA_AFFINITY ++#include ++#endif ++ + #define GNTTAB_LIST_END 0xffffffff + + static grant_ref_t **gnttab_list; +@@ -881,6 +885,88 @@ int gnttab_pages_set_private(int nr_pages, struct page **pages) + } + EXPORT_SYMBOL_GPL(gnttab_pages_set_private); + ++#ifdef CONFIG_XEN_BACKEND_NUMA_AFFINITY ++/* ++ * Tri-state cache for XENMEM_get_mfn_pxms availability. -ENOSYS ++ * (hypervisor without the hypercall) or -EPERM (Xen restricts the ++ * query to the hardware domain) from the first attempt latches ++ * "unsupported", short-circuiting future calls. Any positive ACK ++ * (including the legitimate XEN_INVALID_NUMA_ID answer for an MFN Xen ++ * does not know about) latches "supported". ++ * ++ * Lock-free: at most one transition each direction, and "unsupported" ++ * is a stable terminal state once entered. A racing reader might ++ * issue one redundant hypercall before observing the cached state, ++ * which is harmless. ++ */ ++#define XEN_MFN_PXM_UNKNOWN 0 ++#define XEN_MFN_PXM_SUPPORTED 1 ++#define XEN_MFN_PXM_UNSUPPORTED 2 ++ ++static int xen_mfn_pxm_state = XEN_MFN_PXM_UNKNOWN; ++ ++/* ++ * Resolve one foreign MFN to a Linux node id. Returns NUMA_NO_NODE ++ * for any failure mode: hypercall unsupported, MFN unknown to Xen, ++ * PXM not registered with the dom0 ACPI namespace. ++ * ++ * Three NUMA identifier namespaces are involved here. Xen returns ++ * host PXM (firmware-supplied). pxm_to_node() translates to a Linux ++ * dom0 node id. Callers then use the result against Linux helpers ++ * like cpumask_of_node() and kthread_create_on_node(). ++ */ ++int xen_mfn_to_node(unsigned long mfn) ++{ ++ struct xen_get_mfn_pxms req; ++ xen_pfn_t mfn_arg = mfn; ++ uint32_t pxm = XEN_INVALID_NUMA_ID; ++ int rc; ++ ++ /* ++ * Xen answers this query only for the hardware domain. A domU ++ * mapping a foreign ring (a driver domain, say) would just earn ++ * an -EPERM per connect; skip the doomed hypercall and stay ++ * node-oblivious, which is the only honest answer for a guest ++ * with no view of host topology anyway. ++ */ ++ if (!xen_initial_domain()) ++ return NUMA_NO_NODE; ++ ++ if (READ_ONCE(xen_mfn_pxm_state) == XEN_MFN_PXM_UNSUPPORTED) ++ return NUMA_NO_NODE; ++ ++ memset(&req, 0, sizeof(req)); ++ set_xen_guest_handle(req.mfns, &mfn_arg); ++ set_xen_guest_handle(req.pxms, &pxm); ++ req.nr_mfns = 1; ++ ++ rc = HYPERVISOR_memory_op(XENMEM_get_mfn_pxms, &req); ++ if (rc < 0) { ++ /* ++ * Both errnos are stable properties of this boot: -ENOSYS ++ * means the hypervisor lacks the hypercall (no CONFIG_NUMA ++ * or too old), -EPERM that it refuses us despite the ++ * initial-domain check above (XSM policy, or a late ++ * hardware domain). Latch either so we never retry. ++ */ ++ if (rc == -ENOSYS || rc == -EPERM) { ++ WRITE_ONCE(xen_mfn_pxm_state, XEN_MFN_PXM_UNSUPPORTED); ++ pr_info("XENMEM_get_mfn_pxms unavailable (%d), NUMA affinity for grant mappings disabled\n", ++ rc); ++ } ++ return NUMA_NO_NODE; ++ } ++ ++ WRITE_ONCE(xen_mfn_pxm_state, XEN_MFN_PXM_SUPPORTED); ++ ++ if (pxm == XEN_INVALID_NUMA_ID) ++ return NUMA_NO_NODE; ++ ++ return pxm_to_node(pxm); ++} ++EXPORT_SYMBOL_GPL(xen_mfn_to_node); ++#endif /* CONFIG_XEN_BACKEND_NUMA_AFFINITY */ ++ + /** + * gnttab_alloc_pages - alloc pages suitable for grant mapping into + * @nr_pages: number of pages to alloc +diff --git a/include/xen/interface/memory.h b/include/xen/interface/memory.h +index 1a371a825c55..1998a12a9465 100644 +--- a/include/xen/interface/memory.h ++++ b/include/xen/interface/memory.h +@@ -325,4 +325,30 @@ struct xen_mem_acquire_resource { + }; + DEFINE_GUEST_HANDLE_STRUCT(xen_mem_acquire_resource); + ++/* ++ * XENMEM_get_mfn_pxms: resolve a batch of host MFNs to their firmware ++ * proximity-domain identifiers (host PXM on x86 ACPI). ++ * ++ * Returned values are in the host PXM namespace (the same value space ++ * dom0's own SRAT uses), not Xen's internal node id. Callers convert ++ * to a Linux node id with pxm_to_node(). Slots Xen has no node info ++ * for receive XEN_INVALID_NUMA_ID rather than failing the whole batch. ++ * ++ * Restricted to the hardware domain. On hypervisors that do not ++ * provide this op (older or non-Edera Xen, or builds without ++ * CONFIG_NUMA), the hypercall returns -ENOSYS; callers treat that as ++ * "feature unavailable" and fall back to NUMA-oblivious behaviour. ++ */ ++#define XENMEM_get_mfn_pxms 40 ++ ++#define XEN_INVALID_NUMA_ID (~(uint32_t)0) ++ ++struct xen_get_mfn_pxms { ++ GUEST_HANDLE(xen_pfn_t) mfns; ++ GUEST_HANDLE(uint32_t) pxms; ++ uint32_t nr_mfns; ++ uint32_t flags; ++}; ++DEFINE_GUEST_HANDLE_STRUCT(xen_get_mfn_pxms); ++ + #endif /* __XEN_PUBLIC_MEMORY_H__ */ +diff --git a/include/xen/xen.h b/include/xen/xen.h +index f280c5dcf923..f7f3dd7a3abe 100644 +--- a/include/xen/xen.h ++++ b/include/xen/xen.h +@@ -89,6 +89,22 @@ static inline void xen_free_unpopulated_pages(unsigned int nr_pages, + } + #endif + ++/* ++ * Resolve a foreign frame's host MFN to the Linux node id of the memory ++ * backing it, via XENMEM_get_mfn_pxms. NUMA_NO_NODE for any failure ++ * mode (hypercall unsupported or refused, MFN unknown to Xen, PXM not ++ * in the ACPI namespace) -- callers degrade to node-oblivious behaviour. ++ */ ++#include ++#ifdef CONFIG_XEN_BACKEND_NUMA_AFFINITY ++int xen_mfn_to_node(unsigned long mfn); ++#else ++static inline int xen_mfn_to_node(unsigned long mfn) ++{ ++ return NUMA_NO_NODE; ++} ++#endif ++ + #if defined(CONFIG_XEN_DOM0) && defined(CONFIG_ACPI) && defined(CONFIG_X86) + bool __init xen_processor_present(uint32_t acpi_id); + #else +-- +2.55.0 + diff --git a/patches/0002-xen-xenbus-expose-host-NUMA-node-of-a-mapped-ring.patch b/patches/0002-xen-xenbus-expose-host-NUMA-node-of-a-mapped-ring.patch deleted file mode 100644 index 7482925..0000000 --- a/patches/0002-xen-xenbus-expose-host-NUMA-node-of-a-mapped-ring.patch +++ /dev/null @@ -1,348 +0,0 @@ -From 0cc05d6a3795ccb1f832afde9920c74c12328368 Mon Sep 17 00:00:00 2001 -From: Steven Noonan -Date: Tue, 19 May 2026 18:35:49 -0700 -Subject: [PATCH 2/9] xen/xenbus: expose host NUMA node of a mapped ring - -Xenbus backends in PVH dom0 today have no visibility into which host -NUMA node the foreign frame backing a grant-mapped ring lives on. -xen_alloc_unpopulated_pages() registers its pool with NUMA_NO_NODE, -so page_to_nid() of any grant-mapped page reports NUMA_NO_NODE and -backends have nothing to drive kthread or IRQ placement off of. The -result is that every backend kthread for every queue piles onto one -host node regardless of the guest's vNUMA layout. - -Add xenbus_ring_host_node(dev, vaddr) returning the Linux node id of -the host node hosting the first ring page, or NUMA_NO_NODE when the -information is unavailable. The value is resolved once at map time -using the new XENMEM_get_mfn_pxms hypercall and cached on the -xenbus_map_node, so the public helper is a cheap list lookup. - -Three NUMA identifier namespaces are involved in this code path: host -PXM (firmware), Xen-internal nid (assigned in SRAT scan order), and -dom0 Linux node id. The hypercall returns host PXM, matching what -dom0's own SRAT already uses; pxm_to_node() converts to a Linux node -id, which is what backends will feed to cpumask_of_node() and -kthread_create_on_node(). Keeping the Xen-side ABI in PXM-space lets -callers translate with one standard lookup instead of maintaining -their own Xen-nid -> Linux-node table. - -Older or non-Edera hypervisors lack the new hypercall. The first -call there returns -ENOSYS, which latches a global "unsupported" -flag; every subsequent xenbus_ring_host_node() returns NUMA_NO_NODE -without issuing further hypercalls. Backend patches that key off the -helper see NUMA_NO_NODE and silently fall back to today's -NUMA-oblivious behaviour. This makes the kernel-side change safe to -ship without a lockstep hypervisor update. - -Gated by CONFIG_XEN_BACKEND_NUMA_AFFINITY, which depends on -XEN_BACKEND, NUMA, ACPI_NUMA, and XEN_UNPOPULATED_ALLOC and defaults -to y. XEN_UNPOPULATED_ALLOC is a hard dependency rather than a soft -one because the per-ring placement work the rest of this series -performs is only meaningful when grant-map placeholders come from the -per-node pool registered via memremap_pages(pgmap, node). Without -XEN_UNPOPULATED_ALLOC, placeholders come from the generic balloon -allocator and page_to_nid() reflects only where dom0's free RAM -happened to be, not the host node of the foreign frame. Per-ring -NUMA placement under that configuration would commit to wrong nodes -confidently rather than silently no-op. Disabling the option -compiles the query out and the public helper becomes a stub returning -NUMA_NO_NODE. - -PV dom0 is not a NUMA-affinity target: the PV map path explicitly -sets host_node = NUMA_NO_NODE and the helper handles the -pv.area->addr lookup path symmetrically with hvm.addr, returning -NUMA_NO_NODE in both cases when no match is found. - -The foreign MFN is sourced from dev_bus_addr in the gnttab map -result. Xen sets that field unconditionally for host_map operations -(see grant_table.c in the hypervisor), so it is reliable in PVH dom0 -without IOMMU isolation. Sampling only the first ring page's MFN is -sufficient for the common case where a ring is contiguous on one -host node; multi-node huge-page grants are a future concern. - -Signed-off-by: Steven Noonan ---- - drivers/xen/Kconfig | 25 +++++++ - drivers/xen/xenbus/xenbus_client.c | 111 +++++++++++++++++++++++++++++ - include/xen/interface/memory.h | 26 +++++++ - include/xen/xenbus.h | 13 ++++ - 4 files changed, 175 insertions(+) - -diff --git a/drivers/xen/Kconfig b/drivers/xen/Kconfig -index f9a35ed266ec..147e6acb231b 100644 ---- a/drivers/xen/Kconfig -+++ b/drivers/xen/Kconfig -@@ -96,6 +96,31 @@ config XEN_BACKEND - Support for backend device drivers that provide I/O services - to other virtual machines. - -+config XEN_BACKEND_NUMA_AFFINITY -+ bool "NUMA affinity for Xen backend drivers" -+ depends on XEN_BACKEND && NUMA && ACPI_NUMA && XEN_UNPOPULATED_ALLOC -+ default y -+ help -+ Allow Xen backend drivers (netback, blkback, gntdev consumers) -+ to discover the host NUMA node that hosts a grant-mapped ring -+ page, and to place their service threads and IRQs on that node. -+ -+ XEN_UNPOPULATED_ALLOC provides the per-node placeholder-page pool -+ the relocation logic in xenbus_map_ring_valloc() draws from. -+ Without it, placeholders come from the generic balloon allocator, -+ whose page_to_nid() reflects only where dom0's free RAM happened -+ to be -- not the host node of the foreign frame the placeholder -+ will end up backing. In that configuration the per-ring -+ placement decisions would be confidently wrong rather than just -+ absent, so the Kconfig hard-depends on XEN_UNPOPULATED_ALLOC -+ rather than silently degrading. -+ -+ Requires hypervisor support for the XENMEM_get_mfn_pxms -+ hypercall. Without that support the feature is silently a -+ no-op, equivalent to NUMA-oblivious behaviour. -+ -+ If unsure, say Y. -+ - config XENFS - tristate "Xen filesystem" - select XEN_PRIVCMD -diff --git a/drivers/xen/xenbus/xenbus_client.c b/drivers/xen/xenbus/xenbus_client.c -index 2dc874fb5506..8e0695ba39a3 100644 ---- a/drivers/xen/xenbus/xenbus_client.c -+++ b/drivers/xen/xenbus/xenbus_client.c -@@ -31,15 +31,18 @@ - */ - - #include -+#include - #include - #include - #include - #include - #include -+#include - #include - #include - #include - #include -+#include - #include - #include - #include -@@ -47,6 +50,10 @@ - #include - #include - -+#ifdef CONFIG_XEN_BACKEND_NUMA_AFFINITY -+#include -+#endif -+ - #include "xenbus.h" - - #define XENBUS_PAGES(_grants) (DIV_ROUND_UP(_grants, XEN_PFN_PER_PAGE)) -@@ -67,6 +74,7 @@ struct xenbus_map_node { - }; - grant_handle_t handles[XENBUS_MAX_RING_GRANTS]; - unsigned int nr_handles; -+ int host_node; /* Linux node id of foreign frame, or NUMA_NO_NODE */ - }; - - struct map_ring_valloc { -@@ -85,6 +93,72 @@ struct map_ring_valloc { - static DEFINE_SPINLOCK(xenbus_valloc_lock); - static LIST_HEAD(xenbus_valloc_pages); - -+#ifdef CONFIG_XEN_BACKEND_NUMA_AFFINITY -+/* -+ * Tri-state cache for XENMEM_get_mfn_pxms availability. -ENOSYS from -+ * the first attempt latches "unsupported", short-circuiting future -+ * calls. Any positive ACK (including the legitimate XEN_INVALID_NUMA_ID -+ * answer for an MFN Xen does not know about) latches "supported". -+ * -+ * Lock-free: at most one transition each direction, and "unsupported" -+ * is a stable terminal state once entered. A racing reader might -+ * issue one redundant hypercall before observing the cached state, -+ * which is harmless. -+ */ -+#define XEN_MFN_PXM_UNKNOWN 0 -+#define XEN_MFN_PXM_SUPPORTED 1 -+#define XEN_MFN_PXM_UNSUPPORTED 2 -+ -+static int xen_mfn_pxm_state = XEN_MFN_PXM_UNKNOWN; -+ -+/* -+ * Resolve one foreign MFN to a Linux node id. Returns NUMA_NO_NODE -+ * for any failure mode: hypercall unsupported, MFN unknown to Xen, -+ * PXM not registered with the dom0 ACPI namespace. -+ * -+ * Three NUMA identifier namespaces are involved here. Xen returns -+ * host PXM (firmware-supplied). pxm_to_node() translates to a Linux -+ * dom0 node id. Callers then use the result against Linux helpers -+ * like cpumask_of_node() and kthread_create_on_node(). -+ */ -+static int xenbus_query_mfn_node(unsigned long mfn) -+{ -+ struct xen_get_mfn_pxms req; -+ xen_pfn_t mfn_arg = mfn; -+ uint32_t pxm = XEN_INVALID_NUMA_ID; -+ int rc; -+ -+ if (READ_ONCE(xen_mfn_pxm_state) == XEN_MFN_PXM_UNSUPPORTED) -+ return NUMA_NO_NODE; -+ -+ memset(&req, 0, sizeof(req)); -+ set_xen_guest_handle(req.mfns, &mfn_arg); -+ set_xen_guest_handle(req.pxms, &pxm); -+ req.nr_mfns = 1; -+ -+ rc = HYPERVISOR_memory_op(XENMEM_get_mfn_pxms, &req); -+ if (rc < 0) { -+ if (rc == -ENOSYS) { -+ WRITE_ONCE(xen_mfn_pxm_state, XEN_MFN_PXM_UNSUPPORTED); -+ pr_info("xenbus: hypervisor lacks XENMEM_get_mfn_pxms, backend NUMA affinity disabled\n"); -+ } -+ return NUMA_NO_NODE; -+ } -+ -+ WRITE_ONCE(xen_mfn_pxm_state, XEN_MFN_PXM_SUPPORTED); -+ -+ if (pxm == XEN_INVALID_NUMA_ID) -+ return NUMA_NO_NODE; -+ -+ return pxm_to_node(pxm); -+} -+#else -+static int xenbus_query_mfn_node(unsigned long mfn) -+{ -+ return NUMA_NO_NODE; -+} -+#endif /* CONFIG_XEN_BACKEND_NUMA_AFFINITY */ -+ - struct xenbus_ring_ops { - int (*map)(struct xenbus_device *dev, struct map_ring_valloc *info, - grant_ref_t *gnt_refs, unsigned int nr_grefs, -@@ -678,6 +752,8 @@ static int xenbus_map_ring_hvm(struct xenbus_device *dev, - bool leaked = false; - unsigned int nr_pages = XENBUS_PAGES(nr_grefs); - -+ node->host_node = NUMA_NO_NODE; -+ - err = xen_alloc_unpopulated_pages(nr_pages, node->hvm.pages); - if (err) - goto out_err; -@@ -693,6 +769,17 @@ static int xenbus_map_ring_hvm(struct xenbus_device *dev, - if (err) - goto out_free_ballooned_pages; - -+ /* -+ * Xen unconditionally fills dev_bus_addr with the foreign frame's -+ * machine address on a successful host_map (see grant_table.c in -+ * the hypervisor). Pick up the first ring page's MFN and resolve -+ * it now while we still have the map info; the result is cached on -+ * the xenbus_map_node so backends can look it up cheaply later. -+ */ -+ if (nr_grefs > 0) -+ node->host_node = xenbus_query_mfn_node( -+ PFN_DOWN(info->map[0].dev_bus_addr)); -+ - addr = vmap(node->hvm.pages, nr_pages, VM_MAP | VM_IOREMAP, - PAGE_KERNEL); - if (!addr) { -@@ -743,6 +830,27 @@ int xenbus_unmap_ring_vfree(struct xenbus_device *dev, void *vaddr) - } - EXPORT_SYMBOL_GPL(xenbus_unmap_ring_vfree); - -+int xenbus_ring_host_node(struct xenbus_device *dev, void *vaddr) -+{ -+ struct xenbus_map_node *node; -+ int node_id = NUMA_NO_NODE; -+ -+ spin_lock(&xenbus_valloc_lock); -+ list_for_each_entry(node, &xenbus_valloc_pages, next) { -+ void *addr = xen_pv_domain() ? node->pv.area->addr -+ : node->hvm.addr; -+ -+ if (addr == vaddr) { -+ node_id = node->host_node; -+ break; -+ } -+ } -+ spin_unlock(&xenbus_valloc_lock); -+ -+ return node_id; -+} -+EXPORT_SYMBOL_GPL(xenbus_ring_host_node); -+ - #ifdef CONFIG_XEN_PV - static int map_ring_apply(pte_t *pte, unsigned long addr, void *data) - { -@@ -763,6 +871,9 @@ static int xenbus_map_ring_pv(struct xenbus_device *dev, - bool leaked = false; - int err = -ENOMEM; - -+ /* PV dom0 is not a NUMA-affinity target; leave the value unset. */ -+ node->host_node = NUMA_NO_NODE; -+ - area = get_vm_area(XEN_PAGE_SIZE * nr_grefs, VM_IOREMAP); - if (!area) - return -ENOMEM; -diff --git a/include/xen/interface/memory.h b/include/xen/interface/memory.h -index 1a371a825c55..1998a12a9465 100644 ---- a/include/xen/interface/memory.h -+++ b/include/xen/interface/memory.h -@@ -325,4 +325,30 @@ struct xen_mem_acquire_resource { - }; - DEFINE_GUEST_HANDLE_STRUCT(xen_mem_acquire_resource); - -+/* -+ * XENMEM_get_mfn_pxms: resolve a batch of host MFNs to their firmware -+ * proximity-domain identifiers (host PXM on x86 ACPI). -+ * -+ * Returned values are in the host PXM namespace (the same value space -+ * dom0's own SRAT uses), not Xen's internal node id. Callers convert -+ * to a Linux node id with pxm_to_node(). Slots Xen has no node info -+ * for receive XEN_INVALID_NUMA_ID rather than failing the whole batch. -+ * -+ * Restricted to the hardware domain. On hypervisors that do not -+ * provide this op (older or non-Edera Xen, or builds without -+ * CONFIG_NUMA), the hypercall returns -ENOSYS; callers treat that as -+ * "feature unavailable" and fall back to NUMA-oblivious behaviour. -+ */ -+#define XENMEM_get_mfn_pxms 40 -+ -+#define XEN_INVALID_NUMA_ID (~(uint32_t)0) -+ -+struct xen_get_mfn_pxms { -+ GUEST_HANDLE(xen_pfn_t) mfns; -+ GUEST_HANDLE(uint32_t) pxms; -+ uint32_t nr_mfns; -+ uint32_t flags; -+}; -+DEFINE_GUEST_HANDLE_STRUCT(xen_get_mfn_pxms); -+ - #endif /* __XEN_PUBLIC_MEMORY_H__ */ -diff --git a/include/xen/xenbus.h b/include/xen/xenbus.h -index 7dab04cf4a36..18b902bf79ef 100644 ---- a/include/xen/xenbus.h -+++ b/include/xen/xenbus.h -@@ -225,6 +225,19 @@ int xenbus_map_ring_valloc(struct xenbus_device *dev, grant_ref_t *gnt_refs, - - int xenbus_unmap_ring_vfree(struct xenbus_device *dev, void *vaddr); - -+/* -+ * Return the host NUMA node (Linux node id) of the foreign frame -+ * backing the first page of a mapping previously established by -+ * xenbus_map_ring_valloc(). Returns NUMA_NO_NODE if the hypervisor -+ * cannot provide the information, the mapping is not found, or the -+ * kernel was built without CONFIG_XEN_BACKEND_NUMA_AFFINITY. -+ * -+ * Intended for backends placing their service threads and IRQs on -+ * the node hosting the ring. The value is resolved at map time and -+ * cached on the mapping, so this call is a cheap lookup. -+ */ -+int xenbus_ring_host_node(struct xenbus_device *dev, void *vaddr); -+ - int xenbus_alloc_evtchn(struct xenbus_device *dev, evtchn_port_t *port); - int xenbus_free_evtchn(struct xenbus_device *dev, evtchn_port_t port); - --- -2.54.0 - diff --git a/patches/0005-xen-make-xen_alloc_unpopulated_pages-NUMA-aware.patch b/patches/0003-xen-make-xen_alloc_unpopulated_pages-NUMA-aware.patch similarity index 72% rename from patches/0005-xen-make-xen_alloc_unpopulated_pages-NUMA-aware.patch rename to patches/0003-xen-make-xen_alloc_unpopulated_pages-NUMA-aware.patch index d179c44..aabd746 100644 --- a/patches/0005-xen-make-xen_alloc_unpopulated_pages-NUMA-aware.patch +++ b/patches/0003-xen-make-xen_alloc_unpopulated_pages-NUMA-aware.patch @@ -1,7 +1,7 @@ -From aa8e71b14c040931222a36998a789e4ce3484c70 Mon Sep 17 00:00:00 2001 +From 56ea4dae7db066a61b6c15a0afd3bdad55e38ad3 Mon Sep 17 00:00:00 2001 From: Steven Noonan -Date: Tue, 19 May 2026 18:56:34 -0700 -Subject: [PATCH 5/9] xen: make xen_alloc_unpopulated_pages NUMA-aware +Date: Mon, 20 Jul 2026 13:51:49 -0700 +Subject: [PATCH 03/12] xen: make xen_alloc_unpopulated_pages NUMA-aware xen_alloc_unpopulated_pages today hands out ZONE_DEVICE placeholder pages from a single global free list, with the underlying section @@ -28,9 +28,8 @@ Two exported entry points: xen_alloc_unpopulated_pages(nr, pages) -- existing API, now a wrapper that passes numa_node_id(). Strictly an improvement over - NUMA_NO_NODE for non-xenbus callers (gntdev, xlate_mmu, privcmd, - drm) that have no node hint of their own: the caller's local node - is at least as good a guess as none. + NUMA_NO_NODE for callers with no node hint of their own: the + caller's local node is at least as good a guess as none. xen_free_unpopulated_pages routes each page back to the pool of the node it came from via page_to_nid(). No new arguments; existing @@ -42,22 +41,15 @@ not contention-sensitive paths. MAX_NUMNODES sizes the two arrays; on CONFIG_NUMA=n that is 1 and the behaviour is byte-for-byte equivalent to the previous single-list -implementation. - -Wire xenbus_map_ring_hvm() through the new API. Placeholders are -first drawn from numa_node_id() because the foreign MFN is not known -before the grant_map. After grant_map succeeds and the first page's -host node is known via XENMEM_get_mfn_pxms, the placeholders are -relocated: unmap, free, re-allocate on the target node, re-map. When -the initial allocation already landed on the right node the relocate -is skipped. +implementation. The consumers that pair this allocator with +xen_mfn_to_node to home placeholders on the foreign frame's node come +in the patches that follow. Signed-off-by: Steven Noonan --- - drivers/xen/unpopulated-alloc.c | 103 +++++++++++++++++++++++------ - drivers/xen/xenbus/xenbus_client.c | 60 +++++++++++++++++ - include/xen/xen.h | 7 ++ - 3 files changed, 149 insertions(+), 21 deletions(-) + drivers/xen/unpopulated-alloc.c | 103 +++++++++++++++++++++++++------- + include/xen/xen.h | 7 +++ + 2 files changed, 89 insertions(+), 21 deletions(-) diff --git a/drivers/xen/unpopulated-alloc.c b/drivers/xen/unpopulated-alloc.c index 1dc0b495c8e5..ea9c76895459 100644 @@ -257,79 +249,8 @@ index 1dc0b495c8e5..ea9c76895459 100644 } mutex_unlock(&list_lock); } -diff --git a/drivers/xen/xenbus/xenbus_client.c b/drivers/xen/xenbus/xenbus_client.c -index 8e0695ba39a3..7f94b135727a 100644 ---- a/drivers/xen/xenbus/xenbus_client.c -+++ b/drivers/xen/xenbus/xenbus_client.c -@@ -780,6 +780,66 @@ static int xenbus_map_ring_hvm(struct xenbus_device *dev, - node->host_node = xenbus_query_mfn_node( - PFN_DOWN(info->map[0].dev_bus_addr)); - -+ /* -+ * Placeholder pages came from numa_node_id()'s pool, which only -+ * matches the foreign frame's node by coincidence. If they -+ * disagree, drop the mapping, return the placeholders, and redo -+ * the map with placeholders drawn from the correct pool. After -+ * this, page_to_nid() of every ring page equals the host node of -+ * its foreign MFN by construction, which keeps grant-mapped pages -+ * truthful to every NUMA-aware code path that consults page_to_nid. -+ * -+ * The cost is one extra grant unmap + map pair per backend -+ * connect (a rare event) and is paid only when the placeholder -+ * pool's node disagrees with the foreign frame. PV mappings and -+ * cases where Xen cannot supply node info skip the dance entirely -+ * (host_node stays NUMA_NO_NODE). -+ */ -+ if (node->host_node != NUMA_NO_NODE && -+ page_to_nid(node->hvm.pages[0]) != node->host_node) { -+ int relocate_err; -+ -+ relocate_err = xenbus_unmap_ring(dev, node->handles, nr_grefs, -+ info->addrs); -+ if (relocate_err != GNTST_okay) { -+ /* -+ * Partial unmap: at least one grant may still be -+ * live against a placeholder we can no longer -+ * reach safely. Mark the pages leaked and fail -+ * the whole map. -+ */ -+ leaked = true; -+ err = -EIO; -+ goto out_free_ballooned_pages; -+ } -+ -+ xen_free_unpopulated_pages(nr_pages, node->hvm.pages); -+ -+ err = xen_alloc_unpopulated_pages_node(nr_pages, -+ node->hvm.pages, -+ node->host_node); -+ if (err) { -+ /* -+ * Pages already gone; clear the array so the -+ * cleanup path does not try to free them again. -+ */ -+ memset(node->hvm.pages, 0, -+ nr_pages * sizeof(*node->hvm.pages)); -+ node->nr_handles = 0; -+ goto out_err; -+ } -+ -+ info->idx = 0; -+ gnttab_foreach_grant(node->hvm.pages, nr_grefs, -+ xenbus_map_ring_setup_grant_hvm, -+ info); -+ -+ err = __xenbus_map_ring(dev, gnt_ref, nr_grefs, node->handles, -+ info, GNTMAP_host_map, &leaked); -+ if (err) -+ goto out_free_ballooned_pages; -+ } -+ - addr = vmap(node->hvm.pages, nr_pages, VM_MAP | VM_IOREMAP, - PAGE_KERNEL); - if (!addr) { diff --git a/include/xen/xen.h b/include/xen/xen.h -index f280c5dcf923..f38cb138d837 100644 +index f7f3dd7a3abe..ccf9bfa33813 100644 --- a/include/xen/xen.h +++ b/include/xen/xen.h @@ -70,6 +70,8 @@ extern u64 xen_saved_max_mem_size; @@ -354,5 +275,5 @@ index f280c5dcf923..f38cb138d837 100644 struct page **pages) { -- -2.54.0 +2.55.0 diff --git a/patches/0004-xen-grant-table-add-gnttab_alloc_pages_node.patch b/patches/0004-xen-grant-table-add-gnttab_alloc_pages_node.patch new file mode 100644 index 0000000..c6ab059 --- /dev/null +++ b/patches/0004-xen-grant-table-add-gnttab_alloc_pages_node.patch @@ -0,0 +1,95 @@ +From 0051d0715d03b8515e26ab5c01144a7d61017216 Mon Sep 17 00:00:00 2001 +From: Steven Noonan +Date: Mon, 20 Jul 2026 13:51:51 -0700 +Subject: [PATCH 04/12] xen/grant-table: add gnttab_alloc_pages_node + +gnttab_alloc_pages draws grant-mapping placeholder pages via +xen_alloc_unpopulated_pages, which prefers the calling CPU's NUMA +node. That is the right default for a caller with no better +information, but a consumer that already knows the host node of the +foreign frames it is about to map -- because it queried the node via +xen_mfn_to_node after a first map -- has no way to ask for +placeholders from the matching pool. + +Add gnttab_alloc_pages_node, taking an explicit node preference and +passing it through to xen_alloc_unpopulated_pages_node, and reduce +gnttab_alloc_pages to a wrapper passing NUMA_NO_NODE (which the +unpopulated allocator clamps to the local node, preserving today's +behaviour exactly). Frees are unchanged: gnttab_free_pages already +routes each page back to its home pool via page_to_nid. + +No functional change for existing callers. The first user is gntdev, +which will relocate a fresh grant mapping's placeholders onto the +foreign frames' node the same way the xenbus ring path gains later in +this series. + +Signed-off-by: Steven Noonan +--- + drivers/xen/grant-table.c | 25 ++++++++++++++++++++++--- + include/xen/grant_table.h | 1 + + 2 files changed, 23 insertions(+), 3 deletions(-) + +diff --git a/drivers/xen/grant-table.c b/drivers/xen/grant-table.c +index f8b86a8679de..879e08104805 100644 +--- a/drivers/xen/grant-table.c ++++ b/drivers/xen/grant-table.c +@@ -968,15 +968,23 @@ EXPORT_SYMBOL_GPL(xen_mfn_to_node); + #endif /* CONFIG_XEN_BACKEND_NUMA_AFFINITY */ + + /** +- * gnttab_alloc_pages - alloc pages suitable for grant mapping into ++ * gnttab_alloc_pages_node - alloc pages suitable for grant mapping into, ++ * drawn from a specific NUMA node's placeholder pool + * @nr_pages: number of pages to alloc + * @pages: returns the pages ++ * @node: node to draw the pages from, or NUMA_NO_NODE for the caller's ++ * local node ++ * ++ * A caller that knows the host node of the foreign frames it is about ++ * to map can request placeholders whose page_to_nid() matches; the ++ * generic gnttab_alloc_pages() below has no such knowledge and settles ++ * for the local node. + */ +-int gnttab_alloc_pages(int nr_pages, struct page **pages) ++int gnttab_alloc_pages_node(int nr_pages, struct page **pages, int node) + { + int ret; + +- ret = xen_alloc_unpopulated_pages(nr_pages, pages); ++ ret = xen_alloc_unpopulated_pages_node(nr_pages, pages, node); + if (ret < 0) + return ret; + +@@ -986,6 +994,17 @@ int gnttab_alloc_pages(int nr_pages, struct page **pages) + + return ret; + } ++EXPORT_SYMBOL_GPL(gnttab_alloc_pages_node); ++ ++/** ++ * gnttab_alloc_pages - alloc pages suitable for grant mapping into ++ * @nr_pages: number of pages to alloc ++ * @pages: returns the pages ++ */ ++int gnttab_alloc_pages(int nr_pages, struct page **pages) ++{ ++ return gnttab_alloc_pages_node(nr_pages, pages, NUMA_NO_NODE); ++} + EXPORT_SYMBOL_GPL(gnttab_alloc_pages); + + #ifdef CONFIG_XEN_UNPOPULATED_ALLOC +diff --git a/include/xen/grant_table.h b/include/xen/grant_table.h +index 69ac6d80a006..d13566a00ce7 100644 +--- a/include/xen/grant_table.h ++++ b/include/xen/grant_table.h +@@ -211,6 +211,7 @@ void gnttab_free_auto_xlat_frames(void); + + #define gnttab_map_vaddr(map) ((void *)(map.host_virt_addr)) + ++int gnttab_alloc_pages_node(int nr_pages, struct page **pages, int node); + int gnttab_alloc_pages(int nr_pages, struct page **pages); + void gnttab_free_pages(int nr_pages, struct page **pages); + +-- +2.55.0 + diff --git a/patches/0001-xen-events-add-_on_node-variants-of-the-lateeoi-bind.patch b/patches/0005-xen-events-add-_on_node-variants-of-the-lateeoi-bind.patch similarity index 98% rename from patches/0001-xen-events-add-_on_node-variants-of-the-lateeoi-bind.patch rename to patches/0005-xen-events-add-_on_node-variants-of-the-lateeoi-bind.patch index e2b995e..c3bf869 100644 --- a/patches/0001-xen-events-add-_on_node-variants-of-the-lateeoi-bind.patch +++ b/patches/0005-xen-events-add-_on_node-variants-of-the-lateeoi-bind.patch @@ -1,7 +1,7 @@ -From bd2dbbb1f3a05a5d77d18548d41cdb25c0b81912 Mon Sep 17 00:00:00 2001 +From 9aaace179740c21966407a4e241d83c03214ea21 Mon Sep 17 00:00:00 2001 From: Steven Noonan -Date: Tue, 19 May 2026 23:00:20 -0700 -Subject: [PATCH 1/9] xen/events: add _on_node variants of the lateeoi bind +Date: Mon, 20 Jul 2026 13:51:53 -0700 +Subject: [PATCH 05/12] xen/events: add _on_node variants of the lateeoi bind helpers xen_allocate_irq_dynamic() unconditionally calls @@ -294,5 +294,5 @@ index de5da58a0205..1abc068557b4 100644 /* * Common unbind function for all event sources. Takes IRQ to unbind from. -- -2.54.0 +2.55.0 diff --git a/patches/0006-xen-xenbus-collapse-xenbus_ring_host_node-to-a-page_.patch b/patches/0006-xen-xenbus-collapse-xenbus_ring_host_node-to-a-page_.patch deleted file mode 100644 index d872882..0000000 --- a/patches/0006-xen-xenbus-collapse-xenbus_ring_host_node-to-a-page_.patch +++ /dev/null @@ -1,158 +0,0 @@ -From 14a4b2b5f12fa8359b84e48ec01586285d1f71e9 Mon Sep 17 00:00:00 2001 -From: Steven Noonan -Date: Tue, 19 May 2026 19:09:31 -0700 -Subject: [PATCH 6/9] xen/xenbus: collapse xenbus_ring_host_node to a - page_to_nid wrapper - -Now that xen_alloc_unpopulated_pages places ring placeholders on the -foreign frame's host node and xenbus_map_ring_hvm relocates the -placeholders post-map when they land in the wrong pool, page_to_nid -of any vmap'd ring page in PVH dom0 already reports the host node of -its foreign MFN. xenbus_ring_host_node can therefore drop the -list-walk + cached integer + hypercall plumbing and become a thin -wrapper around vmalloc_to_page() + page_to_nid(). - -The XENMEM_get_mfn_pxms hypercall is still issued at map time inside -xenbus_map_ring_hvm to learn the foreign frame's host node before -deciding whether to relocate. The decision now uses a local variable -rather than caching the value on struct xenbus_map_node, so the -host_node field is removed entirely. - -PV dom0 keeps the early NUMA_NO_NODE return: PV mappings install -foreign MFNs directly in the PTEs and have no struct page in dom0's -mem_map for the foreign frame, so vmalloc_to_page on a PV ring vaddr -returns the placeholder for the wrong frame (or NULL). PV dom0 is -not a NUMA-affinity target on Edera in any case. - -Backend call sites do not change: netback and blkback continue to -call xenbus_ring_host_node exactly as before. The work moves from -"walk a list keyed on vaddr, return a cached integer set at map time" -to "look up the struct page for vaddr, return its nid". Cheaper at -runtime; no longer needs xenbus_valloc_lock; same return value. - -Signed-off-by: Steven Noonan ---- - drivers/xen/xenbus/xenbus_client.c | 55 ++++++++++++++---------------- - 1 file changed, 25 insertions(+), 30 deletions(-) - -diff --git a/drivers/xen/xenbus/xenbus_client.c b/drivers/xen/xenbus/xenbus_client.c -index 7f94b135727a..f357c4a0372f 100644 ---- a/drivers/xen/xenbus/xenbus_client.c -+++ b/drivers/xen/xenbus/xenbus_client.c -@@ -74,7 +74,6 @@ struct xenbus_map_node { - }; - grant_handle_t handles[XENBUS_MAX_RING_GRANTS]; - unsigned int nr_handles; -- int host_node; /* Linux node id of foreign frame, or NUMA_NO_NODE */ - }; - - struct map_ring_valloc { -@@ -748,12 +747,11 @@ static int xenbus_map_ring_hvm(struct xenbus_device *dev, - { - struct xenbus_map_node *node = info->node; - int err; -+ int host_node = NUMA_NO_NODE; - void *addr; - bool leaked = false; - unsigned int nr_pages = XENBUS_PAGES(nr_grefs); - -- node->host_node = NUMA_NO_NODE; -- - err = xen_alloc_unpopulated_pages(nr_pages, node->hvm.pages); - if (err) - goto out_err; -@@ -770,14 +768,13 @@ static int xenbus_map_ring_hvm(struct xenbus_device *dev, - goto out_free_ballooned_pages; - - /* -- * Xen unconditionally fills dev_bus_addr with the foreign frame's -- * machine address on a successful host_map (see grant_table.c in -- * the hypervisor). Pick up the first ring page's MFN and resolve -- * it now while we still have the map info; the result is cached on -- * the xenbus_map_node so backends can look it up cheaply later. -+ * Xen fills dev_bus_addr with the foreign frame's machine -+ * address on a successful host_map (see grant_table.c in the -+ * hypervisor). Resolve the host node now so we know whether the -+ * placeholders need to be relocated below. - */ - if (nr_grefs > 0) -- node->host_node = xenbus_query_mfn_node( -+ host_node = xenbus_query_mfn_node( - PFN_DOWN(info->map[0].dev_bus_addr)); - - /* -@@ -792,11 +789,10 @@ static int xenbus_map_ring_hvm(struct xenbus_device *dev, - * The cost is one extra grant unmap + map pair per backend - * connect (a rare event) and is paid only when the placeholder - * pool's node disagrees with the foreign frame. PV mappings and -- * cases where Xen cannot supply node info skip the dance entirely -- * (host_node stays NUMA_NO_NODE). -+ * cases where Xen cannot supply node info skip the dance entirely. - */ -- if (node->host_node != NUMA_NO_NODE && -- page_to_nid(node->hvm.pages[0]) != node->host_node) { -+ if (host_node != NUMA_NO_NODE && -+ page_to_nid(node->hvm.pages[0]) != host_node) { - int relocate_err; - - relocate_err = xenbus_unmap_ring(dev, node->handles, nr_grefs, -@@ -817,7 +813,7 @@ static int xenbus_map_ring_hvm(struct xenbus_device *dev, - - err = xen_alloc_unpopulated_pages_node(nr_pages, - node->hvm.pages, -- node->host_node); -+ host_node); - if (err) { - /* - * Pages already gone; clear the array so the -@@ -892,22 +888,24 @@ EXPORT_SYMBOL_GPL(xenbus_unmap_ring_vfree); - - int xenbus_ring_host_node(struct xenbus_device *dev, void *vaddr) - { -- struct xenbus_map_node *node; -- int node_id = NUMA_NO_NODE; -+ struct page *page; - -- spin_lock(&xenbus_valloc_lock); -- list_for_each_entry(node, &xenbus_valloc_pages, next) { -- void *addr = xen_pv_domain() ? node->pv.area->addr -- : node->hvm.addr; -+ /* -+ * PV mappings install foreign MFNs directly in the PTEs and have -+ * no struct page in dom0's mem_map for the foreign frame. PVH -+ * dom0 keeps a placeholder struct page (allocated from the -+ * matching per-node pool of xen_alloc_unpopulated_pages_node) -+ * whose page_to_nid() reports the host node of the foreign frame -+ * by construction. -+ */ -+ if (xen_pv_domain()) -+ return NUMA_NO_NODE; - -- if (addr == vaddr) { -- node_id = node->host_node; -- break; -- } -- } -- spin_unlock(&xenbus_valloc_lock); -+ page = vmalloc_to_page(vaddr); -+ if (!page) -+ return NUMA_NO_NODE; - -- return node_id; -+ return page_to_nid(page); - } - EXPORT_SYMBOL_GPL(xenbus_ring_host_node); - -@@ -931,9 +929,6 @@ static int xenbus_map_ring_pv(struct xenbus_device *dev, - bool leaked = false; - int err = -ENOMEM; - -- /* PV dom0 is not a NUMA-affinity target; leave the value unset. */ -- node->host_node = NUMA_NO_NODE; -- - area = get_vm_area(XEN_PAGE_SIZE * nr_grefs, VM_IOREMAP); - if (!area) - return -ENOMEM; --- -2.54.0 - diff --git a/patches/0006-xen-xenbus-home-ring-mappings-on-the-foreign-frame-s.patch b/patches/0006-xen-xenbus-home-ring-mappings-on-the-foreign-frame-s.patch new file mode 100644 index 0000000..09489f6 --- /dev/null +++ b/patches/0006-xen-xenbus-home-ring-mappings-on-the-foreign-frame-s.patch @@ -0,0 +1,221 @@ +From 28bf8d0a16b76307151e0c35f5da1a20ff1ab9dd Mon Sep 17 00:00:00 2001 +From: Steven Noonan +Date: Mon, 20 Jul 2026 13:52:31 -0700 +Subject: [PATCH 06/12] xen/xenbus: home ring mappings on the foreign frame's + host node + +Xenbus backends in PVH dom0 today have no visibility into which host +NUMA node the foreign frame backing a grant-mapped ring lives on, so +nothing can drive kthread or IRQ placement off of it, and every +backend kthread for every queue piles onto one host node regardless +of the guest's vNUMA layout. + +Make xenbus_map_ring_hvm place its placeholder pages on the foreign +frame's node. Placeholders are first drawn from numa_node_id()'s +pool, because the foreign MFN is not known before the grant_map. +After grant_map succeeds, the first page's host node is resolved via +xen_mfn_to_node and the placeholders are relocated: unmap, free, +re-allocate on the target node, re-map. When the initial allocation +already landed on the right node, the relocate is skipped. After +this, page_to_nid() of every mapped ring page equals the host node of +its foreign MFN by construction. + +Expose the result as xenbus_ring_host_node(dev, vaddr), returning the +Linux node id of the node hosting a mapped ring, or NUMA_NO_NODE when +the information is unavailable. Thanks to the relocation, it is a +thin wrapper around vmalloc_to_page() + page_to_nid(): no hypercall, +no lock, no per-map caching. Backends feed the value to +cpumask_of_node() and kthread_create_on_node(). + +The foreign MFN is sourced from dev_bus_addr in the gnttab map +result. Xen sets that field unconditionally for host_map operations +(see grant_table.c in the hypervisor), so it is reliable in PVH dom0. +Sampling only the first ring page's MFN is sufficient for the common +case where a ring is contiguous on one host node; multi-node +huge-page grants are a future concern. + +The relocate costs one extra grant unmap + map pair per backend +connect (a rare event), paid only when the placeholder pool's node +disagrees with the foreign frame's. A failed unmap during the +relocate marks the pages leaked and fails the whole map, since at +least one grant may still be live against a placeholder that can no +longer be reached safely. + +PV dom0 is not a NUMA-affinity target: PV mappings install foreign +MFNs directly in the PTEs and have no struct page in dom0's mem_map +for the foreign frame, so xenbus_ring_host_node returns NUMA_NO_NODE +there. On hypervisors without XENMEM_get_mfn_pxms the node resolves +to NUMA_NO_NODE at map time and the relocate is skipped entirely. +Backends seeing NUMA_NO_NODE fall back to today's NUMA-oblivious +behaviour. + +Signed-off-by: Steven Noonan +--- + drivers/xen/xenbus/xenbus_client.c | 96 ++++++++++++++++++++++++++++++ + include/xen/xenbus.h | 13 ++++ + 2 files changed, 109 insertions(+) + +diff --git a/drivers/xen/xenbus/xenbus_client.c b/drivers/xen/xenbus/xenbus_client.c +index 2dc874fb5506..34ac3f197b92 100644 +--- a/drivers/xen/xenbus/xenbus_client.c ++++ b/drivers/xen/xenbus/xenbus_client.c +@@ -31,15 +31,18 @@ + */ + + #include ++#include + #include + #include + #include + #include + #include ++#include + #include + #include + #include + #include ++#include + #include + #include + #include +@@ -674,6 +677,7 @@ static int xenbus_map_ring_hvm(struct xenbus_device *dev, + { + struct xenbus_map_node *node = info->node; + int err; ++ int host_node = NUMA_NO_NODE; + void *addr; + bool leaked = false; + unsigned int nr_pages = XENBUS_PAGES(nr_grefs); +@@ -693,6 +697,75 @@ static int xenbus_map_ring_hvm(struct xenbus_device *dev, + if (err) + goto out_free_ballooned_pages; + ++ /* ++ * Xen fills dev_bus_addr with the foreign frame's machine ++ * address on a successful host_map (see grant_table.c in the ++ * hypervisor). Resolve the host node now so we know whether the ++ * placeholders need to be relocated below. ++ */ ++ if (nr_grefs > 0) ++ host_node = xen_mfn_to_node( ++ PFN_DOWN(info->map[0].dev_bus_addr)); ++ ++ /* ++ * Placeholder pages came from numa_node_id()'s pool, which only ++ * matches the foreign frame's node by coincidence. If they ++ * disagree, drop the mapping, return the placeholders, and redo ++ * the map with placeholders drawn from the correct pool. After ++ * this, page_to_nid() of every ring page equals the host node of ++ * its foreign MFN by construction, which keeps grant-mapped pages ++ * truthful to every NUMA-aware code path that consults page_to_nid. ++ * ++ * The cost is one extra grant unmap + map pair per backend ++ * connect (a rare event) and is paid only when the placeholder ++ * pool's node disagrees with the foreign frame. PV mappings and ++ * cases where Xen cannot supply node info skip the dance entirely. ++ */ ++ if (host_node != NUMA_NO_NODE && ++ page_to_nid(node->hvm.pages[0]) != host_node) { ++ int relocate_err; ++ ++ relocate_err = xenbus_unmap_ring(dev, node->handles, nr_grefs, ++ info->addrs); ++ if (relocate_err != GNTST_okay) { ++ /* ++ * Partial unmap: at least one grant may still be ++ * live against a placeholder we can no longer ++ * reach safely. Mark the pages leaked and fail ++ * the whole map. ++ */ ++ leaked = true; ++ err = -EIO; ++ goto out_free_ballooned_pages; ++ } ++ ++ xen_free_unpopulated_pages(nr_pages, node->hvm.pages); ++ ++ err = xen_alloc_unpopulated_pages_node(nr_pages, ++ node->hvm.pages, ++ host_node); ++ if (err) { ++ /* ++ * Pages already gone; clear the array so the ++ * cleanup path does not try to free them again. ++ */ ++ memset(node->hvm.pages, 0, ++ nr_pages * sizeof(*node->hvm.pages)); ++ node->nr_handles = 0; ++ goto out_err; ++ } ++ ++ info->idx = 0; ++ gnttab_foreach_grant(node->hvm.pages, nr_grefs, ++ xenbus_map_ring_setup_grant_hvm, ++ info); ++ ++ err = __xenbus_map_ring(dev, gnt_ref, nr_grefs, node->handles, ++ info, GNTMAP_host_map, &leaked); ++ if (err) ++ goto out_free_ballooned_pages; ++ } ++ + addr = vmap(node->hvm.pages, nr_pages, VM_MAP | VM_IOREMAP, + PAGE_KERNEL); + if (!addr) { +@@ -743,6 +816,29 @@ int xenbus_unmap_ring_vfree(struct xenbus_device *dev, void *vaddr) + } + EXPORT_SYMBOL_GPL(xenbus_unmap_ring_vfree); + ++int xenbus_ring_host_node(struct xenbus_device *dev, void *vaddr) ++{ ++ struct page *page; ++ ++ /* ++ * PV mappings install foreign MFNs directly in the PTEs and have ++ * no struct page in dom0's mem_map for the foreign frame. PVH ++ * dom0 keeps a placeholder struct page (allocated from the ++ * matching per-node pool of xen_alloc_unpopulated_pages_node) ++ * whose page_to_nid() reports the host node of the foreign frame ++ * by construction. ++ */ ++ if (xen_pv_domain()) ++ return NUMA_NO_NODE; ++ ++ page = vmalloc_to_page(vaddr); ++ if (!page) ++ return NUMA_NO_NODE; ++ ++ return page_to_nid(page); ++} ++EXPORT_SYMBOL_GPL(xenbus_ring_host_node); ++ + #ifdef CONFIG_XEN_PV + static int map_ring_apply(pte_t *pte, unsigned long addr, void *data) + { +diff --git a/include/xen/xenbus.h b/include/xen/xenbus.h +index 7dab04cf4a36..18b902bf79ef 100644 +--- a/include/xen/xenbus.h ++++ b/include/xen/xenbus.h +@@ -225,6 +225,19 @@ int xenbus_map_ring_valloc(struct xenbus_device *dev, grant_ref_t *gnt_refs, + + int xenbus_unmap_ring_vfree(struct xenbus_device *dev, void *vaddr); + ++/* ++ * Return the host NUMA node (Linux node id) of the foreign frame ++ * backing the first page of a mapping previously established by ++ * xenbus_map_ring_valloc(). Returns NUMA_NO_NODE if the hypervisor ++ * cannot provide the information, the mapping is not found, or the ++ * kernel was built without CONFIG_XEN_BACKEND_NUMA_AFFINITY. ++ * ++ * Intended for backends placing their service threads and IRQs on ++ * the node hosting the ring. The value is resolved at map time and ++ * cached on the mapping, so this call is a cheap lookup. ++ */ ++int xenbus_ring_host_node(struct xenbus_device *dev, void *vaddr); ++ + int xenbus_alloc_evtchn(struct xenbus_device *dev, evtchn_port_t *port); + int xenbus_free_evtchn(struct xenbus_device *dev, evtchn_port_t port); + +-- +2.55.0 + diff --git a/patches/0007-xen-xenbus-add-xenbus_setup_ring_node-for-per-node-r.patch b/patches/0007-xen-xenbus-add-xenbus_setup_ring_node-for-per-node-r.patch index 188df71..ea5628f 100644 --- a/patches/0007-xen-xenbus-add-xenbus_setup_ring_node-for-per-node-r.patch +++ b/patches/0007-xen-xenbus-add-xenbus_setup_ring_node-for-per-node-r.patch @@ -1,8 +1,8 @@ -From 150666b04dd7ebdef6641d9a09f0d90bfbf801aa Mon Sep 17 00:00:00 2001 +From f82bb54fab9f7662be84a7c2c6cff6916ca4e521 Mon Sep 17 00:00:00 2001 From: Steven Noonan -Date: Tue, 19 May 2026 19:17:39 -0700 -Subject: [PATCH 7/9] xen/xenbus: add xenbus_setup_ring_node for per-node ring - allocation +Date: Mon, 20 Jul 2026 13:52:33 -0700 +Subject: [PATCH 07/12] xen/xenbus: add xenbus_setup_ring_node for per-node + ring allocation Frontend drivers today route every ring allocation through xenbus_setup_ring, which calls alloc_pages_exact with no node @@ -51,7 +51,7 @@ Signed-off-by: Steven Noonan 2 files changed, 99 insertions(+), 10 deletions(-) diff --git a/drivers/xen/xenbus/xenbus_client.c b/drivers/xen/xenbus/xenbus_client.c -index f357c4a0372f..7419a8183903 100644 +index 34ac3f197b92..d3108f9fab32 100644 --- a/drivers/xen/xenbus/xenbus_client.c +++ b/drivers/xen/xenbus/xenbus_client.c @@ -31,6 +31,7 @@ @@ -62,7 +62,7 @@ index f357c4a0372f..7419a8183903 100644 #include #include #include -@@ -448,33 +449,103 @@ static void xenbus_switch_fatal(struct xenbus_device *dev, int depth, int err, +@@ -378,33 +379,103 @@ static void xenbus_switch_fatal(struct xenbus_device *dev, int depth, int err, } /* @@ -176,7 +176,7 @@ index f357c4a0372f..7419a8183903 100644 ret = gnttab_alloc_grant_references(nr_pages, &gref_head); if (ret) { xenbus_dev_fatal(dev, ret, "granting access to %u ring pages", -@@ -508,6 +579,20 @@ int xenbus_setup_ring(struct xenbus_device *dev, gfp_t gfp, void **vaddr, +@@ -438,6 +509,20 @@ int xenbus_setup_ring(struct xenbus_device *dev, gfp_t gfp, void **vaddr, return ret; } @@ -213,5 +213,5 @@ index 18b902bf79ef..8ce096797b86 100644 unsigned int nr_pages, grant_ref_t *grefs); void xenbus_teardown_ring(void **vaddr, unsigned int nr_pages, -- -2.54.0 +2.55.0 diff --git a/patches/0003-xen-netback-place-per-queue-kthreads-and-IRQs-near-t.patch b/patches/0008-xen-netback-place-per-queue-kthreads-and-IRQs-near-t.patch similarity index 98% rename from patches/0003-xen-netback-place-per-queue-kthreads-and-IRQs-near-t.patch rename to patches/0008-xen-netback-place-per-queue-kthreads-and-IRQs-near-t.patch index f5ceaeb..4fa651e 100644 --- a/patches/0003-xen-netback-place-per-queue-kthreads-and-IRQs-near-t.patch +++ b/patches/0008-xen-netback-place-per-queue-kthreads-and-IRQs-near-t.patch @@ -1,7 +1,7 @@ -From d9b521a2e9bba0940078bb2a8d8052ef6871e57f Mon Sep 17 00:00:00 2001 +From 7f3a09f4dac943f542dc8ebaee8c872b1cd33497 Mon Sep 17 00:00:00 2001 From: Steven Noonan -Date: Tue, 19 May 2026 18:40:35 -0700 -Subject: [PATCH 3/9] xen-netback: place per-queue kthreads and IRQs near the +Date: Mon, 20 Jul 2026 13:52:53 -0700 +Subject: [PATCH 08/12] xen-netback: place per-queue kthreads and IRQs near the ring Today both xenvif kthreads (guest-rx and dealloc) and the per-queue @@ -254,5 +254,5 @@ index a0a438881388..798f77595529 100644 vif->ctrl_irq = 0; } -- -2.54.0 +2.55.0 diff --git a/patches/0004-xen-blkback-place-per-ring-kthread-and-IRQ-near-the-.patch b/patches/0009-xen-blkback-place-per-ring-kthread-and-IRQ-near-the-.patch similarity index 97% rename from patches/0004-xen-blkback-place-per-ring-kthread-and-IRQ-near-the-.patch rename to patches/0009-xen-blkback-place-per-ring-kthread-and-IRQ-near-the-.patch index 38ddf46..2c8371c 100644 --- a/patches/0004-xen-blkback-place-per-ring-kthread-and-IRQ-near-the-.patch +++ b/patches/0009-xen-blkback-place-per-ring-kthread-and-IRQ-near-the-.patch @@ -1,7 +1,8 @@ -From dfc8f5b06ce2855798e26f1221a7c176b6ae9cd1 Mon Sep 17 00:00:00 2001 +From 99386006b8da0d96cbb90fc92f87f7f1893e10f9 Mon Sep 17 00:00:00 2001 From: Steven Noonan -Date: Tue, 19 May 2026 18:50:03 -0700 -Subject: [PATCH 4/9] xen-blkback: place per-ring kthread and IRQ near the ring +Date: Mon, 20 Jul 2026 13:52:54 -0700 +Subject: [PATCH 09/12] xen-blkback: place per-ring kthread and IRQ near the + ring The xenblkd kthread and per-ring event-channel IRQ today run wherever the scheduler happens to place them, which in PVH dom0 is typically @@ -178,5 +179,5 @@ index 0621878940ae..7ded88e97472 100644 ring->irq = 0; } -- -2.54.0 +2.55.0 diff --git a/patches/0008-xen-netfront-place-per-queue-rings-on-per-queue-node.patch b/patches/0010-xen-netfront-place-per-queue-rings-on-per-queue-node.patch similarity index 98% rename from patches/0008-xen-netfront-place-per-queue-rings-on-per-queue-node.patch rename to patches/0010-xen-netfront-place-per-queue-rings-on-per-queue-node.patch index 7d63b90..9719fc7 100644 --- a/patches/0008-xen-netfront-place-per-queue-rings-on-per-queue-node.patch +++ b/patches/0010-xen-netfront-place-per-queue-rings-on-per-queue-node.patch @@ -1,7 +1,7 @@ -From 2dd13fab395903f26e7f37eaeacec05c8fdd0233 Mon Sep 17 00:00:00 2001 +From 6f614fd873050c104e80392784fb0e3f154c6365 Mon Sep 17 00:00:00 2001 From: Steven Noonan -Date: Tue, 19 May 2026 19:21:52 -0700 -Subject: [PATCH 8/9] xen-netfront: place per-queue rings on per-queue nodes, +Date: Mon, 20 Jul 2026 13:52:57 -0700 +Subject: [PATCH 10/12] xen-netfront: place per-queue rings on per-queue nodes, with XPS Today every netfront queue allocates its tx and rx rings from @@ -266,5 +266,5 @@ index a11a0e949400..7c18db3de8d4 100644 fail: -- -2.54.0 +2.55.0 diff --git a/patches/0009-xen-blkfront-place-per-ring-buffers-on-per-hctx-node.patch b/patches/0011-xen-blkfront-place-per-ring-buffers-on-per-hctx-node.patch similarity index 96% rename from patches/0009-xen-blkfront-place-per-ring-buffers-on-per-hctx-node.patch rename to patches/0011-xen-blkfront-place-per-ring-buffers-on-per-hctx-node.patch index 59637c1..8fa9914 100644 --- a/patches/0009-xen-blkfront-place-per-ring-buffers-on-per-hctx-node.patch +++ b/patches/0011-xen-blkfront-place-per-ring-buffers-on-per-hctx-node.patch @@ -1,7 +1,7 @@ -From 9f2eced2a6cb89bf0f8fcfc4889389880b595e39 Mon Sep 17 00:00:00 2001 +From 3f4b79bd1c1e8fac01ae80121a2eec2b9b05d81d Mon Sep 17 00:00:00 2001 From: Steven Noonan -Date: Tue, 19 May 2026 19:25:21 -0700 -Subject: [PATCH 9/9] xen-blkfront: place per-ring buffers on per-hctx nodes +Date: Mon, 20 Jul 2026 13:52:59 -0700 +Subject: [PATCH 11/12] xen-blkfront: place per-ring buffers on per-hctx nodes Today every blkfront ring (one per hctx in multi-queue mode) is allocated from xenbus_setup_ring() with no node preference, so all @@ -154,5 +154,5 @@ index 04fc6b552c04..8414dc737157 100644 fail: blkif_free(info, 0); -- -2.54.0 +2.55.0 diff --git a/patches/0012-xen-gntdev-home-grant-map-placeholder-pages-on-the-f.patch b/patches/0012-xen-gntdev-home-grant-map-placeholder-pages-on-the-f.patch new file mode 100644 index 0000000..f68ddc3 --- /dev/null +++ b/patches/0012-xen-gntdev-home-grant-map-placeholder-pages-on-the-f.patch @@ -0,0 +1,198 @@ +From 1ba42d8e6f950e6946bee2e2f971c2ffdaa89317 Mon Sep 17 00:00:00 2001 +From: Steven Noonan +Date: Mon, 20 Jul 2026 13:53:00 -0700 +Subject: [PATCH 12/12] xen/gntdev: home grant-map placeholder pages on the + foreign frame's node + +On an auto-translated dom0, gntdev backs each userspace grant mapping +with placeholder pages drawn from the unpopulated-page pool of the +mapping CPU's local node. page_to_nid() of such a page therefore +reflects where the mapper happened to run, not where the foreign frame +actually lives, and userspace has no way at all to learn the frame's +home: gntdev never surfaces dev_bus_addr, and /proc/self/pagemap only +yields the placeholder's dom0 pseudo-physical frame. + +Do for gntdev what xenbus_map_ring_hvm already does for kernel ring +mappings. After the first successful gnttab_map_refs -- and before +the pages become visible outside the map: gntdev_mmap() only inserts +them into the VMA afterwards, and map->in_use limits a map to a single +mmap -- resolve the first frame's host node via xen_mfn_to_node. If +the placeholders live elsewhere, allocate replacements from the +matching pool, unmap, and remap onto them. The frames of one map +overwhelmingly share a node (they are rings or buffers the granting +guest allocated together), so following the first frame mirrors the +xenbus precedent. DMA-allocated backing (CONFIG_XEN_GRANT_DMA_ALLOC) +is skipped: its physical placement is part of its contract. + +The ordering makes failure handling one-directional. Replacement +pages are allocated before anything moves, so allocation failure -- +like an unresolvable node, a hint-free hypervisor, or a domU caller +(xen_mfn_to_node answers only in the hardware domain) -- leaves the +original mapping untouched. Once the old mapping has been torn down, +a failure poisons the map's statuses and handles so the caller's +accounting loop reports the map as wholly failed and teardown does not +unmap a second time; should the unmap itself fail, the old pages may +still carry live foreign mappings, so they are deliberately leaked +rather than returned to the allocator, exactly as the xenbus relocate +leaks placeholders it can no longer reach safely. + +The payoff is that the node information becomes visible to userspace +through an existing interface: get_mempolicy(MPOL_F_ADDR | MPOL_F_NODE) +on a gntdev-mapped address returns page_to_nid of the placeholder +(gntdev VMAs are VM_MIXEDMAP with normal PTEs, which the GUP slow path +resolves), which now equals the foreign frame's node. A userspace +paravirtual backend can map a guest's ring, ask the kernel which node +that ring lives on, and bind its memory and I/O threads to match -- +the userspace analogue of the netback/blkback per-queue placement. +move_pages(2) and /proc/self/numa_maps still ignore these mappings; +both explicitly skip ZONE_DEVICE pages. + +Signed-off-by: Steven Noonan +--- + drivers/xen/gntdev.c | 123 +++++++++++++++++++++++++++++++++++++++++++ + 1 file changed, 123 insertions(+) + +diff --git a/drivers/xen/gntdev.c b/drivers/xen/gntdev.c +index 2c960f187f7c..31da3c9641f1 100644 +--- a/drivers/xen/gntdev.c ++++ b/drivers/xen/gntdev.c +@@ -327,6 +327,119 @@ static int find_grant_ptes(pte_t *pte, unsigned long addr, void *data) + return 0; + } + ++/* ++ * Relocate a freshly mapped map's placeholder pages onto the NUMA node ++ * hosting the foreign frames, so that page_to_nid() of a grant-mapped ++ * page reports the frame's true home -- the same dance ++ * xenbus_map_ring_hvm() performs for kernel ring mappings. Userspace ++ * can then recover the node of a mapping it created with ++ * get_mempolicy(MPOL_F_ADDR | MPOL_F_NODE) and place its I/O threads ++ * and buffers accordingly. ++ * ++ * Runs after the first successful gnttab_map_refs() and before the ++ * pages are visible outside the map (gntdev_mmap() inserts them into ++ * the VMA only after gntdev_map_grant_pages() returns, and map->in_use ++ * limits a map to a single mmap), so the unmap/remap below cannot race ++ * a user access. ++ * ++ * Best-effort while nothing has moved: any failure to learn or improve ++ * the placement leaves the original mapping untouched and returns 0. A ++ * failure while moving the mapping poisons the map and returns an ++ * error, since a half-relocated map must not be exposed. ++ */ ++static int gntdev_relocate_map(struct gntdev_grant_map *map) ++{ ++ struct page **pages; ++ int node, i, err; ++ ++#ifdef CONFIG_XEN_GRANT_DMA_ALLOC ++ /* DMA-allocated backing must keep its physical placement. */ ++ if (map->dma_vaddr) ++ return 0; ++#endif ++ ++ /* Only relocate a fully successful map. */ ++ for (i = 0; i < map->count; i++) ++ if (map->map_ops[i].status != GNTST_okay) ++ return 0; ++ ++ /* ++ * Xen filled dev_bus_addr with the foreign frame's machine ++ * address on the successful host_map. The frames of one map ++ * overwhelmingly share a node (they are rings or buffers the ++ * granting guest allocated together), so follow the first frame, ++ * as the xenbus ring path does. ++ */ ++ node = xen_mfn_to_node(PFN_DOWN(map->map_ops[0].dev_bus_addr)); ++ if (node == NUMA_NO_NODE || node == page_to_nid(map->pages[0])) ++ return 0; ++ ++ pages = kvcalloc(map->count, sizeof(pages[0]), GFP_KERNEL); ++ if (!pages) ++ return 0; ++ if (gnttab_alloc_pages_node(map->count, pages, node)) { ++ kvfree(pages); ++ return 0; ++ } ++ ++ for (i = 0; i < map->count; i++) { ++ map->unmap_ops[i].handle = map->map_ops[i].handle; ++ if (map->flags & GNTMAP_device_map) ++ map->unmap_ops[i].dev_bus_addr = ++ map->map_ops[i].dev_bus_addr; ++ } ++ err = gnttab_unmap_refs(map->unmap_ops, map->kunmap_ops, map->pages, ++ map->count); ++ if (err) { ++ /* ++ * The old placeholders may still carry live foreign ++ * mappings; freeing them would hand foreign-mapped frames ++ * back to the allocator. Swap in the clean replacements ++ * so teardown frees those instead, deliberately leak the ++ * old pages, and fail the map. ++ */ ++ pr_err("relocate unmap failed (%d); leaking %d pages\n", ++ err, map->count); ++ memcpy(map->pages, pages, map->count * sizeof(pages[0])); ++ kvfree(pages); ++ goto poison; ++ } ++ ++ gnttab_free_pages(map->count, map->pages); ++ memcpy(map->pages, pages, map->count * sizeof(pages[0])); ++ kvfree(pages); ++ ++ for (i = 0; i < map->count; i++) { ++ unsigned long addr = (unsigned long) ++ pfn_to_kaddr(page_to_pfn(map->pages[i])); ++ gnttab_set_map_op(&map->map_ops[i], addr, map->flags, ++ map->grants[i].ref, map->grants[i].domid); ++ gnttab_set_unmap_op(&map->unmap_ops[i], addr, map->flags, ++ INVALID_GRANT_HANDLE); ++ } ++ err = gnttab_map_refs(map->map_ops, map->kmap_ops, map->pages, ++ map->count); ++ if (err) { ++ pr_err("relocate remap on node %d failed (%d)\n", node, err); ++ goto poison; ++ } ++ ++ return 0; ++ ++poison: ++ /* ++ * Present the map as wholly failed: no handle survives for the ++ * caller's status loop to account, or for teardown to unmap a ++ * second time. ++ */ ++ for (i = 0; i < map->count; i++) { ++ map->map_ops[i].status = GNTST_general_error; ++ map->map_ops[i].handle = INVALID_GRANT_HANDLE; ++ map->unmap_ops[i].handle = INVALID_GRANT_HANDLE; ++ } ++ return err; ++} ++ + int gntdev_map_grant_pages(struct gntdev_grant_map *map) + { + size_t alloced = 0; +@@ -377,6 +490,16 @@ int gntdev_map_grant_pages(struct gntdev_grant_map *map) + err = gnttab_map_refs(map->map_ops, map->kmap_ops, map->pages, + map->count); + ++ /* ++ * Auto-translated placeholders came from the local node's pool, ++ * which matches the foreign frames' node only by coincidence; ++ * move them to the frames' real home while nothing can observe ++ * the map. PV installs foreign MFNs directly in the PTEs and has ++ * no dom0 struct page to relocate. ++ */ ++ if (!err && !xen_pv_domain()) ++ err = gntdev_relocate_map(map); ++ + for (i = 0; i < map->count; i++) { + if (map->map_ops[i].status == GNTST_okay) { + map->unmap_ops[i].handle = map->map_ops[i].handle; +-- +2.55.0 +