[PATCH v5 28/36] mm: add NODE_PRIVATE_CAP_HOTUNPLUG for opted-in private nodes

From: Gregory Price

Date: Mon Jul 20 2026 - 15:44:05 EST


Add CAP_HOTUNPLUG, which allows memory_hotplug to migrate a private
node's folios for the purpose of hotunplugging memory.

Without this, hotunplug fails if any folio on the node is allocated.

Add node_allows_hotunplug() - which returns true for any normal node
and private nodes with CAP_HOTUNPLUG.

Signed-off-by: Gregory Price <gourry@xxxxxxxxxx>
---
include/linux/node_private.h | 26 ++++++++++++++++++++++++++
mm/memory_hotplug.c | 4 ++--
2 files changed, 28 insertions(+), 2 deletions(-)

diff --git a/include/linux/node_private.h b/include/linux/node_private.h
index 655fe9ec5cb61..7b617b1fa9c28 100644
--- a/include/linux/node_private.h
+++ b/include/linux/node_private.h
@@ -14,6 +14,7 @@ struct page;
*/
#define NODE_PRIVATE_CAP_RECLAIM (1UL << 0) /* allow mm reclaim */
#define NODE_PRIVATE_CAP_USER_NUMA (1UL << 1) /* allow mempolicy */
+#define NODE_PRIVATE_CAP_HOTUNPLUG (1UL << 2) /* allow hot-unplug */

/**
* struct node_private - Per-node container for N_MEMORY_PRIVATE nodes
@@ -97,6 +98,26 @@ static inline bool node_allows_user_numa(int nid)
return ret;
}

+/**
+ * node_allows_hotunplug - may hot-unplug migrate this node's folios?
+ * @nid: the node to test
+ *
+ * True for normal nodes and private nodes opted into CAP_HOTUNPLUG.
+ */
+static inline bool node_allows_hotunplug(int nid)
+{
+ struct node_private *np;
+ bool ret;
+
+ if (!node_state(nid, N_MEMORY_PRIVATE))
+ return true;
+ rcu_read_lock();
+ np = rcu_dereference(NODE_DATA(nid)->node_private);
+ ret = np && (np->caps & NODE_PRIVATE_CAP_HOTUNPLUG);
+ rcu_read_unlock();
+ return ret;
+}
+
#else /* !CONFIG_NUMA */

static inline bool folio_is_private_node(struct folio *folio)
@@ -124,6 +145,11 @@ static inline bool node_allows_user_numa(int nid)
return true;
}

+static inline bool node_allows_hotunplug(int nid)
+{
+ return true;
+}
+
#endif /* CONFIG_NUMA */

#if defined(CONFIG_NUMA) && defined(CONFIG_MEMORY_HOTPLUG)
diff --git a/mm/memory_hotplug.c b/mm/memory_hotplug.c
index 1f42ed303366c..e9573f90e0dcd 100644
--- a/mm/memory_hotplug.c
+++ b/mm/memory_hotplug.c
@@ -1943,8 +1943,8 @@ static int do_migrate_range(unsigned long start_pfn, unsigned long end_pfn)
goto put_folio;
}

- /* Private node folios cannot migrate, fail outright */
- if (folio_is_private_node(folio)) {
+ /* Fail outright on private nodes w/o hotunplug support */
+ if (!node_allows_hotunplug(folio_nid(folio))) {
pr_info_ratelimited("memory offline refused: node %d pfn %lx\n",
folio_nid(folio), pfn);
folio_put(folio);
--
2.53.0-Meta