Linux Trace Kernel
 help / color / mirror / Atom feed
* [RFC PATCH v4 17/27] mm/oom: NP_OPS_OOM_ELIGIBLE - private node OOM participation
From: Gregory Price @ 2026-02-22  8:48 UTC (permalink / raw)
  To: lsf-pc
  Cc: linux-kernel, linux-cxl, cgroups, linux-mm, linux-trace-kernel,
	damon, kernel-team, gregkh, rafael, dakr, dave, jonathan.cameron,
	dave.jiang, alison.schofield, vishal.l.verma, ira.weiny,
	dan.j.williams, longman, akpm, david, lorenzo.stoakes,
	Liam.Howlett, vbabka, rppt, surenb, mhocko, osalvador, ziy,
	matthew.brost, joshua.hahnjy, rakie.kim, byungchul, gourry,
	ying.huang, apopple, axelrasmussen, yuanchu, weixugc, yury.norov,
	linux, mhiramat, mathieu.desnoyers, tj, hannes, mkoutny, jackmanb,
	sj, baolin.wang, npache, ryan.roberts, dev.jain, baohua,
	lance.yang, muchun.song, xu.xin16, chengming.zhou, jannh,
	linmiaohe, nao.horiguchi, pfalcato, rientjes, shakeel.butt, riel,
	harry.yoo, cl, roman.gushchin, chrisl, kasong, shikemeng, nphamcs,
	bhe, zhengqi.arch, terry.bowman
In-Reply-To: <20260222084842.1824063-1-gourry@gourry.net>

The OOM killer must know whether killing a task can actually free
memory such that pressure is reduced.

A private node only contributes to relieving pressure if it participates
in both reclaim and demotion. Without this check, the check, the OOM
killer may select an undeserving victim.

Introduce NP_OPS_OOM_ELIGIBLE and helpers node_oom_eligible() and
zone_oom_eligible().

Replace cpuset_mems_allowed_intersects() in oom_cpuset_eligible()
with oom_mems_intersect() that iterates N_MEMORY nodes and skips
ineligible private nodes.

Update constrained_alloc() to use zone_oom_eligible() for constraint
detection and node_oom_eligible() to exclude ineligible nodes from
totalpages accounting.

Remove cpuset_mems_allowed_intersects() as it has no remaining callers.

Signed-off-by: Gregory Price <gourry@gourry.net>
---
 include/linux/cpuset.h       |  9 -------
 include/linux/node_private.h |  3 +++
 kernel/cgroup/cpuset.c       | 17 ------------
 mm/oom_kill.c                | 52 ++++++++++++++++++++++++++++++++----
 4 files changed, 50 insertions(+), 31 deletions(-)

diff --git a/include/linux/cpuset.h b/include/linux/cpuset.h
index 7b2f3f6b68a9..53ccfb00b277 100644
--- a/include/linux/cpuset.h
+++ b/include/linux/cpuset.h
@@ -97,9 +97,6 @@ static inline bool cpuset_zone_allowed(struct zone *z, gfp_t gfp_mask)
 	return true;
 }
 
-extern int cpuset_mems_allowed_intersects(const struct task_struct *tsk1,
-					  const struct task_struct *tsk2);
-
 #ifdef CONFIG_CPUSETS_V1
 #define cpuset_memory_pressure_bump() 				\
 	do {							\
@@ -241,12 +238,6 @@ static inline bool cpuset_zone_allowed(struct zone *z, gfp_t gfp_mask)
 	return true;
 }
 
-static inline int cpuset_mems_allowed_intersects(const struct task_struct *tsk1,
-						 const struct task_struct *tsk2)
-{
-	return 1;
-}
-
 static inline void cpuset_memory_pressure_bump(void) {}
 
 static inline void cpuset_task_status_allowed(struct seq_file *m,
diff --git a/include/linux/node_private.h b/include/linux/node_private.h
index 34be52383255..34d862f09e24 100644
--- a/include/linux/node_private.h
+++ b/include/linux/node_private.h
@@ -141,6 +141,9 @@ struct node_private_ops {
 /* Kernel reclaim (kswapd, direct reclaim, OOM) operates on this node */
 #define NP_OPS_RECLAIM			BIT(4)
 
+/* Private node is OOM-eligible: reclaim can run and pages can be demoted here */
+#define NP_OPS_OOM_ELIGIBLE		(NP_OPS_RECLAIM | NP_OPS_DEMOTION)
+
 /**
  * struct node_private - Per-node container for N_MEMORY_PRIVATE nodes
  *
diff --git a/kernel/cgroup/cpuset.c b/kernel/cgroup/cpuset.c
index 1a597f0c7c6c..29789d544fd5 100644
--- a/kernel/cgroup/cpuset.c
+++ b/kernel/cgroup/cpuset.c
@@ -4530,23 +4530,6 @@ int cpuset_mem_spread_node(void)
 	return cpuset_spread_node(&current->cpuset_mem_spread_rotor);
 }
 
-/**
- * cpuset_mems_allowed_intersects - Does @tsk1's mems_allowed intersect @tsk2's?
- * @tsk1: pointer to task_struct of some task.
- * @tsk2: pointer to task_struct of some other task.
- *
- * Description: Return true if @tsk1's mems_allowed intersects the
- * mems_allowed of @tsk2.  Used by the OOM killer to determine if
- * one of the task's memory usage might impact the memory available
- * to the other.
- **/
-
-int cpuset_mems_allowed_intersects(const struct task_struct *tsk1,
-				   const struct task_struct *tsk2)
-{
-	return nodes_intersects(tsk1->mems_allowed, tsk2->mems_allowed);
-}
-
 /**
  * cpuset_print_current_mems_allowed - prints current's cpuset and mems_allowed
  *
diff --git a/mm/oom_kill.c b/mm/oom_kill.c
index 5eb11fbba704..cd0d65ccd1e8 100644
--- a/mm/oom_kill.c
+++ b/mm/oom_kill.c
@@ -74,7 +74,45 @@ static inline bool is_memcg_oom(struct oom_control *oc)
 	return oc->memcg != NULL;
 }
 
+/* Private nodes are only eligible if they support both reclaim and demotion */
+static inline bool node_oom_eligible(int nid)
+{
+	if (!node_state(nid, N_MEMORY_PRIVATE))
+		return true;
+	return (node_private_flags(nid) & NP_OPS_OOM_ELIGIBLE) ==
+		NP_OPS_OOM_ELIGIBLE;
+}
+
+static inline bool zone_oom_eligible(struct zone *zone, gfp_t gfp_mask)
+{
+	if (!node_oom_eligible(zone_to_nid(zone)))
+		return false;
+	return cpuset_zone_allowed(zone, gfp_mask);
+}
+
 #ifdef CONFIG_NUMA
+/*
+ * Killing a task can only relieve system pressure if freed memory can be
+ * demoted there and reclaim can operate on the node's pages, so we
+ * omit private nodes that aren't eligible.
+ */
+static bool oom_mems_intersect(const struct task_struct *tsk1,
+			       const struct task_struct *tsk2)
+{
+	int nid;
+
+	for_each_node_state(nid, N_MEMORY) {
+		if (!node_isset(nid, tsk1->mems_allowed))
+			continue;
+		if (!node_isset(nid, tsk2->mems_allowed))
+			continue;
+		if (!node_oom_eligible(nid))
+			continue;
+		return true;
+	}
+	return false;
+}
+
 /**
  * oom_cpuset_eligible() - check task eligibility for kill
  * @start: task struct of which task to consider
@@ -107,9 +145,10 @@ static bool oom_cpuset_eligible(struct task_struct *start,
 		} else {
 			/*
 			 * This is not a mempolicy constrained oom, so only
-			 * check the mems of tsk's cpuset.
+			 * check the mems of tsk's cpuset, excluding private
+			 * nodes that do not participate in kernel reclaim.
 			 */
-			ret = cpuset_mems_allowed_intersects(current, tsk);
+			ret = oom_mems_intersect(current, tsk);
 		}
 		if (ret)
 			break;
@@ -291,16 +330,19 @@ static enum oom_constraint constrained_alloc(struct oom_control *oc)
 		return CONSTRAINT_MEMORY_POLICY;
 	}
 
-	/* Check this allocation failure is caused by cpuset's wall function */
+	/* Check this allocation failure is caused by cpuset or private node constraints */
 	for_each_zone_zonelist_nodemask(zone, z, oc->zonelist,
 			highest_zoneidx, oc->nodemask)
-		if (!cpuset_zone_allowed(zone, oc->gfp_mask))
+		if (!zone_oom_eligible(zone, oc->gfp_mask))
 			cpuset_limited = true;
 
 	if (cpuset_limited) {
 		oc->totalpages = total_swap_pages;
-		for_each_node_mask(nid, cpuset_current_mems_allowed)
+		for_each_node_mask(nid, cpuset_current_mems_allowed) {
+			if (!node_oom_eligible(nid))
+				continue;
 			oc->totalpages += node_present_pages(nid);
+		}
 		return CONSTRAINT_CPUSET;
 	}
 	return CONSTRAINT_NONE;
-- 
2.53.0


^ permalink raw reply related

* [RFC PATCH v4 16/27] mm: NP_OPS_RECLAIM - private node reclaim participation
From: Gregory Price @ 2026-02-22  8:48 UTC (permalink / raw)
  To: lsf-pc
  Cc: linux-kernel, linux-cxl, cgroups, linux-mm, linux-trace-kernel,
	damon, kernel-team, gregkh, rafael, dakr, dave, jonathan.cameron,
	dave.jiang, alison.schofield, vishal.l.verma, ira.weiny,
	dan.j.williams, longman, akpm, david, lorenzo.stoakes,
	Liam.Howlett, vbabka, rppt, surenb, mhocko, osalvador, ziy,
	matthew.brost, joshua.hahnjy, rakie.kim, byungchul, gourry,
	ying.huang, apopple, axelrasmussen, yuanchu, weixugc, yury.norov,
	linux, mhiramat, mathieu.desnoyers, tj, hannes, mkoutny, jackmanb,
	sj, baolin.wang, npache, ryan.roberts, dev.jain, baohua,
	lance.yang, muchun.song, xu.xin16, chengming.zhou, jannh,
	linmiaohe, nao.horiguchi, pfalcato, rientjes, shakeel.butt, riel,
	harry.yoo, cl, roman.gushchin, chrisl, kasong, shikemeng, nphamcs,
	bhe, zhengqi.arch, terry.bowman
In-Reply-To: <20260222084842.1824063-1-gourry@gourry.net>

Private node services that drive kswapd via watermark_boost need
control over the reclaim policy.  There are three problems:

1) Boosted reclaim suppresses may_swap and may_writepage.  When
   demotion is not possible, swap is the only evict path, so kswapd
   cannot make progress and pages are stranded.

2) __setup_per_zone_wmarks() unconditionally zeros watermark_boost,
   killing the service's pressure signal.

3) Not all private nodes want reclaim to touch their pages.

Add a reclaim_policy callback to struct node_private_ops and a
struct node_reclaim_policy with:

  - active:             set by the helper when a callback was invoked
  - may_swap:           allow swap writeback during boosted reclaim
  - may_writepage:      allow writepage during boosted reclaim
  - managed_watermarks: service owns watermark_boost lifecycle

We do not allow disabling swap/writepage, as core MM may have
explicitly enabled them on a non-boosted pass.

We only allow enablign swap/writepage, so that the supression during
a boost can be overridden.  This allows a device to force evictions
even when the system otherwise would not percieve pressure.

This is important for a service like compressed RAM, as device capacity
may differ from reported capacity, and device may want to relieve real
pressure (poor compression ratio) as opposed to percieved pressure
(i.e. how many pages are in use).

Add zone_reclaim_allowed() to filter private nodes that have not
opted into reclaim.

Regular nodes fall through to cpuset_zone_allowed() unchanged.

Signed-off-by: Gregory Price <gourry@gourry.net>
---
 include/linux/node_private.h | 28 ++++++++++++++++++++++++++++
 mm/internal.h                | 36 ++++++++++++++++++++++++++++++++++++
 mm/page_alloc.c              | 11 ++++++++++-
 mm/vmscan.c                  | 25 +++++++++++++++++++++++--
 4 files changed, 97 insertions(+), 3 deletions(-)

diff --git a/include/linux/node_private.h b/include/linux/node_private.h
index 27d6e5d84e61..34be52383255 100644
--- a/include/linux/node_private.h
+++ b/include/linux/node_private.h
@@ -14,6 +14,24 @@ struct page;
 struct vm_area_struct;
 struct vm_fault;
 
+/**
+ * struct node_reclaim_policy - Reclaim policy overrides for private nodes
+ * @active: set by node_private_reclaim_policy() when a callback was invoked
+ * @may_swap: allow swap writeback during boosted reclaim
+ * @may_writepage: allow writepage during boosted reclaim
+ * @managed_watermarks: service owns watermark_boost lifecycle; kswapd must
+ *                      not clear it after boosted reclaim
+ *
+ * Passed to the reclaim_policy callback so each private node service can
+ * inject its own reclaim policy before kswapd runs boosted reclaim.
+ */
+struct node_reclaim_policy {
+	bool active;
+	bool may_swap;
+	bool may_writepage;
+	bool managed_watermarks;
+};
+
 /**
  * struct node_private_ops - Callbacks for private node services
  *
@@ -88,6 +106,13 @@ struct vm_fault;
  *
  *   Returns: vm_fault_t result (0, VM_FAULT_RETRY, etc.)
  *
+ * @reclaim_policy: Configure reclaim policy for boosted reclaim.
+ *   [called hodling rcu_read_lock, MUST NOT sleep]
+ *   Called by kswapd before boosted reclaim to let the service override
+ *   may_swap / may_writepage.  If provided, the service also owns the
+ *   watermark_boost lifecycle (kswapd will not clear it).
+ *   If NULL, normal boost policy applies.
+ *
  * @flags: Operation exclusion flags (NP_OPS_* constants).
  *
  */
@@ -101,6 +126,7 @@ struct node_private_ops {
 	void (*folio_migrate)(struct folio *src, struct folio *dst);
 	vm_fault_t (*handle_fault)(struct folio *folio, struct vm_fault *vmf,
 				   enum pgtable_level level);
+	void (*reclaim_policy)(int nid, struct node_reclaim_policy *policy);
 	unsigned long flags;
 };
 
@@ -112,6 +138,8 @@ struct node_private_ops {
 #define NP_OPS_DEMOTION			BIT(2)
 /* Prevent mprotect/NUMA from upgrading PTEs to writable on this node */
 #define NP_OPS_PROTECT_WRITE		BIT(3)
+/* Kernel reclaim (kswapd, direct reclaim, OOM) operates on this node */
+#define NP_OPS_RECLAIM			BIT(4)
 
 /**
  * struct node_private - Per-node container for N_MEMORY_PRIVATE nodes
diff --git a/mm/internal.h b/mm/internal.h
index ae4ff86e8dc6..db32cb2d7a29 100644
--- a/mm/internal.h
+++ b/mm/internal.h
@@ -1572,6 +1572,42 @@ static inline void folio_managed_migrate_notify(struct folio *src,
 		ops->folio_migrate(src, dst);
 }
 
+/**
+ * node_private_reclaim_policy - invoke the service's reclaim policy callback
+ * @nid: NUMA node id
+ * @policy: reclaim policy struct to fill in
+ *
+ * Called by kswapd before boosted reclaim.  Zeroes @policy, then if the
+ * private node service provides a reclaim_policy callback, invokes it
+ * and sets policy->active to true.
+ */
+#ifdef CONFIG_NUMA
+static inline void node_private_reclaim_policy(int nid,
+					       struct node_reclaim_policy *policy)
+{
+	struct node_private *np;
+
+	memset(policy, 0, sizeof(*policy));
+
+	if (!node_state(nid, N_MEMORY_PRIVATE))
+		return;
+
+	rcu_read_lock();
+	np = rcu_dereference(NODE_DATA(nid)->node_private);
+	if (np && np->ops && np->ops->reclaim_policy) {
+		np->ops->reclaim_policy(nid, policy);
+		policy->active = true;
+	}
+	rcu_read_unlock();
+}
+#else
+static inline void node_private_reclaim_policy(int nid,
+					       struct node_reclaim_policy *policy)
+{
+	memset(policy, 0, sizeof(*policy));
+}
+#endif
+
 struct vm_struct *__get_vm_area_node(unsigned long size,
 				     unsigned long align, unsigned long shift,
 				     unsigned long vm_flags, unsigned long start,
diff --git a/mm/page_alloc.c b/mm/page_alloc.c
index e272dfdc6b00..9692048ab5fb 100644
--- a/mm/page_alloc.c
+++ b/mm/page_alloc.c
@@ -55,6 +55,7 @@
 #include <linux/delayacct.h>
 #include <linux/cacheinfo.h>
 #include <linux/pgalloc_tag.h>
+#include <linux/node_private.h>
 #include <asm/div64.h>
 #include "internal.h"
 #include "shuffle.h"
@@ -6437,6 +6438,8 @@ static void __setup_per_zone_wmarks(void)
 	unsigned long lowmem_pages = 0;
 	struct zone *zone;
 	unsigned long flags;
+	struct node_reclaim_policy rp;
+	int prev_nid = NUMA_NO_NODE;
 
 	/* Calculate total number of !ZONE_HIGHMEM and !ZONE_MOVABLE pages */
 	for_each_zone(zone) {
@@ -6446,6 +6449,7 @@ static void __setup_per_zone_wmarks(void)
 
 	for_each_zone(zone) {
 		u64 tmp;
+		int nid = zone_to_nid(zone);
 
 		spin_lock_irqsave(&zone->lock, flags);
 		tmp = (u64)pages_min * zone_managed_pages(zone);
@@ -6482,7 +6486,12 @@ static void __setup_per_zone_wmarks(void)
 			    mult_frac(zone_managed_pages(zone),
 				      watermark_scale_factor, 10000));
 
-		zone->watermark_boost = 0;
+		if (nid != prev_nid) {
+			node_private_reclaim_policy(nid, &rp);
+			prev_nid = nid;
+		}
+		if (!rp.managed_watermarks)
+			zone->watermark_boost = 0;
 		zone->_watermark[WMARK_LOW]  = min_wmark_pages(zone) + tmp;
 		zone->_watermark[WMARK_HIGH] = low_wmark_pages(zone) + tmp;
 		zone->_watermark[WMARK_PROMO] = high_wmark_pages(zone) + tmp;
diff --git a/mm/vmscan.c b/mm/vmscan.c
index 0f534428ea88..07de666c1276 100644
--- a/mm/vmscan.c
+++ b/mm/vmscan.c
@@ -73,6 +73,13 @@
 #define CREATE_TRACE_POINTS
 #include <trace/events/vmscan.h>
 
+static inline bool zone_reclaim_allowed(struct zone *zone, gfp_t gfp_mask)
+{
+	if (node_state(zone_to_nid(zone), N_MEMORY_PRIVATE))
+		return zone_private_flags(zone, NP_OPS_RECLAIM);
+	return cpuset_zone_allowed(zone, gfp_mask);
+}
+
 struct scan_control {
 	/* How many pages shrink_list() should reclaim */
 	unsigned long nr_to_reclaim;
@@ -6274,7 +6281,7 @@ static void shrink_zones(struct zonelist *zonelist, struct scan_control *sc)
 		 * to global LRU.
 		 */
 		if (!cgroup_reclaim(sc)) {
-			if (!cpuset_zone_allowed(zone,
+			if (!zone_reclaim_allowed(zone,
 						 GFP_KERNEL | __GFP_HARDWALL))
 				continue;
 
@@ -6992,6 +6999,7 @@ static int balance_pgdat(pg_data_t *pgdat, int order, int highest_zoneidx)
 	unsigned long zone_boosts[MAX_NR_ZONES] = { 0, };
 	bool boosted;
 	struct zone *zone;
+	struct node_reclaim_policy policy;
 	struct scan_control sc = {
 		.gfp_mask = GFP_KERNEL,
 		.order = order,
@@ -7016,6 +7024,9 @@ static int balance_pgdat(pg_data_t *pgdat, int order, int highest_zoneidx)
 	}
 	boosted = nr_boost_reclaim;
 
+	/* Query/cache private node reclaim policy once per balance() */
+	node_private_reclaim_policy(pgdat->node_id, &policy);
+
 restart:
 	set_reclaim_active(pgdat, highest_zoneidx);
 	sc.priority = DEF_PRIORITY;
@@ -7083,6 +7094,12 @@ static int balance_pgdat(pg_data_t *pgdat, int order, int highest_zoneidx)
 		sc.may_writepage = !laptop_mode && !nr_boost_reclaim;
 		sc.may_swap = !nr_boost_reclaim;
 
+		/* Private nodes may enable swap/writepage when using boost */
+		if (policy.active) {
+			sc.may_swap |= policy.may_swap;
+			sc.may_writepage |= policy.may_writepage;
+		}
+
 		/*
 		 * Do some background aging, to give pages a chance to be
 		 * referenced before reclaiming. All pages are rotated
@@ -7176,6 +7193,10 @@ static int balance_pgdat(pg_data_t *pgdat, int order, int highest_zoneidx)
 			if (!zone_boosts[i])
 				continue;
 
+			/* Some private nodes may own the\ boost lifecycle */
+			if (policy.managed_watermarks)
+				continue;
+
 			/* Increments are under the zone lock */
 			zone = pgdat->node_zones + i;
 			spin_lock_irqsave(&zone->lock, flags);
@@ -7406,7 +7427,7 @@ void wakeup_kswapd(struct zone *zone, gfp_t gfp_flags, int order,
 	if (!managed_zone(zone))
 		return;
 
-	if (!cpuset_zone_allowed(zone, gfp_flags))
+	if (!zone_reclaim_allowed(zone, gfp_flags))
 		return;
 
 	pgdat = zone->zone_pgdat;
-- 
2.53.0


^ permalink raw reply related

* [RFC PATCH v4 15/27] mm/mprotect: NP_OPS_PROTECT_WRITE - gate PTE/PMD write-upgrades
From: Gregory Price @ 2026-02-22  8:48 UTC (permalink / raw)
  To: lsf-pc
  Cc: linux-kernel, linux-cxl, cgroups, linux-mm, linux-trace-kernel,
	damon, kernel-team, gregkh, rafael, dakr, dave, jonathan.cameron,
	dave.jiang, alison.schofield, vishal.l.verma, ira.weiny,
	dan.j.williams, longman, akpm, david, lorenzo.stoakes,
	Liam.Howlett, vbabka, rppt, surenb, mhocko, osalvador, ziy,
	matthew.brost, joshua.hahnjy, rakie.kim, byungchul, gourry,
	ying.huang, apopple, axelrasmussen, yuanchu, weixugc, yury.norov,
	linux, mhiramat, mathieu.desnoyers, tj, hannes, mkoutny, jackmanb,
	sj, baolin.wang, npache, ryan.roberts, dev.jain, baohua,
	lance.yang, muchun.song, xu.xin16, chengming.zhou, jannh,
	linmiaohe, nao.horiguchi, pfalcato, rientjes, shakeel.butt, riel,
	harry.yoo, cl, roman.gushchin, chrisl, kasong, shikemeng, nphamcs,
	bhe, zhengqi.arch, terry.bowman
In-Reply-To: <20260222084842.1824063-1-gourry@gourry.net>

Services that intercept write faults (e.g., for promotion tracking)
need PTEs to stay read-only. This requires preventing mprotect
from silently upgrade the PTE, bypassing the service's handle_fault
callback.

Add NP_OPS_PROTECT_WRITE and folio_managed_wrprotect().

In change_pte_range() and change_huge_pmd(), suppress PTE write-upgrade
when MM_CP_TRY_CHANGE_WRITABLE is sees the folio is write-protected.

In handle_pte_fault() and do_huge_pmd_wp_page(), dispatch to the node's
ops->handle_fault callback when set, allowing the service to handle write
faults with promotion or other custom logic.

NP_OPS_MEMPOLICY is incompatible with NP_OPS_PROTECT_WRITE to avoid the
footgun of binding a writable VMA to a write-protected node.

Signed-off-by: Gregory Price <gourry@gourry.net>
---
 drivers/base/node.c          |  4 ++
 include/linux/node_private.h | 22 ++++++++
 mm/huge_memory.c             | 17 ++++++-
 mm/internal.h                | 99 ++++++++++++++++++++++++++++++++++++
 mm/memory.c                  | 15 ++++++
 mm/migrate.c                 | 14 +----
 mm/mprotect.c                |  4 +-
 7 files changed, 159 insertions(+), 16 deletions(-)

diff --git a/drivers/base/node.c b/drivers/base/node.c
index c08b5a948779..a4955b9b5b93 100644
--- a/drivers/base/node.c
+++ b/drivers/base/node.c
@@ -957,6 +957,10 @@ int node_private_set_ops(int nid, const struct node_private_ops *ops)
 	    !(ops->flags & NP_OPS_MIGRATION))
 		return -EINVAL;
 
+	if ((ops->flags & NP_OPS_MEMPOLICY) &&
+	    (ops->flags & NP_OPS_PROTECT_WRITE))
+		return -EINVAL;
+
 	mutex_lock(&node_private_lock);
 	np = rcu_dereference_protected(NODE_DATA(nid)->node_private,
 				       lockdep_is_held(&node_private_lock));
diff --git a/include/linux/node_private.h b/include/linux/node_private.h
index e254e36056cd..27d6e5d84e61 100644
--- a/include/linux/node_private.h
+++ b/include/linux/node_private.h
@@ -70,6 +70,24 @@ struct vm_fault;
  *     PFN-based metadata (compression tables, device page tables, DMA
  *     mappings, etc.) before any access through the page tables.
  *
+ * @handle_fault: Handle fault on folio on this private node.
+ *   [folio-referenced callback, PTL held on entry]
+ *
+ *   Called from handle_pte_fault() (PTE level) or do_huge_pmd_wp_page()
+ *   (PMD level) after lock acquisition and entry verification.
+ *   @folio is the faulting folio, @level indicates the page table level.
+ *
+ *   For PGTABLE_LEVEL_PTE: vmf->pte is mapped and vmf->ptl is the
+ *   PTE lock.  Release via pte_unmap_unlock(vmf->pte, vmf->ptl).
+ *
+ *   For PGTABLE_LEVEL_PMD: vmf->pte is NULL and vmf->ptl is the
+ *   PMD lock.  Release via spin_unlock(vmf->ptl).
+ *
+ *   The callback MUST release PTL on ALL paths.
+ *   The caller will NOT touch the page table entry after this returns.
+ *
+ *   Returns: vm_fault_t result (0, VM_FAULT_RETRY, etc.)
+ *
  * @flags: Operation exclusion flags (NP_OPS_* constants).
  *
  */
@@ -81,6 +99,8 @@ struct node_private_ops {
 				  enum migrate_reason reason,
 				  unsigned int *nr_succeeded);
 	void (*folio_migrate)(struct folio *src, struct folio *dst);
+	vm_fault_t (*handle_fault)(struct folio *folio, struct vm_fault *vmf,
+				   enum pgtable_level level);
 	unsigned long flags;
 };
 
@@ -90,6 +110,8 @@ struct node_private_ops {
 #define NP_OPS_MEMPOLICY		BIT(1)
 /* Node participates as a demotion target in memory-tiers */
 #define NP_OPS_DEMOTION			BIT(2)
+/* Prevent mprotect/NUMA from upgrading PTEs to writable on this node */
+#define NP_OPS_PROTECT_WRITE		BIT(3)
 
 /**
  * struct node_private - Per-node container for N_MEMORY_PRIVATE nodes
diff --git a/mm/huge_memory.c b/mm/huge_memory.c
index 2ecae494291a..d9ba6593244d 100644
--- a/mm/huge_memory.c
+++ b/mm/huge_memory.c
@@ -2063,12 +2063,14 @@ vm_fault_t do_huge_pmd_wp_page(struct vm_fault *vmf)
 	struct page *page;
 	unsigned long haddr = vmf->address & HPAGE_PMD_MASK;
 	pmd_t orig_pmd = vmf->orig_pmd;
+	vm_fault_t ret;
+
 
 	vmf->ptl = pmd_lockptr(vma->vm_mm, vmf->pmd);
 	VM_BUG_ON_VMA(!vma->anon_vma, vma);
 
 	if (is_huge_zero_pmd(orig_pmd)) {
-		vm_fault_t ret = do_huge_zero_wp_pmd(vmf);
+		ret = do_huge_zero_wp_pmd(vmf);
 
 		if (!(ret & VM_FAULT_FALLBACK))
 			return ret;
@@ -2088,6 +2090,13 @@ vm_fault_t do_huge_pmd_wp_page(struct vm_fault *vmf)
 	folio = page_folio(page);
 	VM_BUG_ON_PAGE(!PageHead(page), page);
 
+	/* Private-managed write-protect: let the service handle the fault */
+	if (unlikely(folio_is_private_managed(folio))) {
+		if (folio_managed_handle_fault(folio, vmf,
+					      PGTABLE_LEVEL_PMD, &ret))
+			return ret;
+	}
+
 	/* Early check when only holding the PT lock. */
 	if (PageAnonExclusive(page))
 		goto reuse;
@@ -2633,7 +2642,8 @@ int change_huge_pmd(struct mmu_gather *tlb, struct vm_area_struct *vma,
 
 	/* See change_pte_range(). */
 	if ((cp_flags & MM_CP_TRY_CHANGE_WRITABLE) && !pmd_write(entry) &&
-	    can_change_pmd_writable(vma, addr, entry))
+	    can_change_pmd_writable(vma, addr, entry) &&
+	    !folio_managed_wrprotect(pmd_folio(entry)))
 		entry = pmd_mkwrite(entry, vma);
 
 	ret = HPAGE_PMD_NR;
@@ -4943,6 +4953,9 @@ void remove_migration_pmd(struct page_vma_mapped_walk *pvmw, struct page *new)
 	if (folio_test_dirty(folio) && softleaf_is_migration_dirty(entry))
 		pmde = pmd_mkdirty(pmde);
 
+	if (folio_managed_wrprotect(folio))
+		pmde = pmd_wrprotect(pmde);
+
 	if (folio_is_device_private(folio)) {
 		swp_entry_t entry;
 
diff --git a/mm/internal.h b/mm/internal.h
index 5950e20d4023..ae4ff86e8dc6 100644
--- a/mm/internal.h
+++ b/mm/internal.h
@@ -11,6 +11,7 @@
 #include <linux/khugepaged.h>
 #include <linux/mm.h>
 #include <linux/mm_inline.h>
+#include <linux/node_private.h>
 #include <linux/pagemap.h>
 #include <linux/pagewalk.h>
 #include <linux/rmap.h>
@@ -18,6 +19,7 @@
 #include <linux/leafops.h>
 #include <linux/swap_cgroup.h>
 #include <linux/tracepoint-defs.h>
+#include <linux/node_private.h>
 
 /* Internal core VMA manipulation functions. */
 #include "vma.h"
@@ -1449,6 +1451,103 @@ static inline bool folio_managed_on_free(struct folio *folio)
 	return false;
 }
 
+/*
+ * folio_managed_handle_fault - Dispatch fault on managed-memory folio
+ * @folio: the faulting folio (must not be NULL)
+ * @vmf: the vm_fault descriptor (PTL held: vmf->ptl locked)
+ * @level: page table level (PGTABLE_LEVEL_PTE or PGTABLE_LEVEL_PMD)
+ * @ret: output fault result if handled
+ *
+ * Called with PTL held.  If a handle_fault callback exists, it is invoked
+ * with PTL still held.  The callback is responsible for releasing PTL on
+ * all paths.
+ *
+ * Returns true if the service handled the fault (PTL released by callback,
+ * caller returns *ret).  Returns false if no handler exists (PTL still held,
+ * caller continues with normal fault handling).
+ */
+static inline bool folio_managed_handle_fault(struct folio *folio,
+					      struct vm_fault *vmf,
+					      enum pgtable_level level,
+					      vm_fault_t *ret)
+{
+	/* Zone device pages use swap entries; handled in do_swap_page */
+	if (folio_is_zone_device(folio))
+		return false;
+
+	if (folio_is_private_node(folio)) {
+		const struct node_private_ops *ops =
+			folio_node_private_ops(folio);
+
+		if (ops && ops->handle_fault) {
+			*ret = ops->handle_fault(folio, vmf, level);
+			return true;
+		}
+	}
+	return false;
+}
+
+/**
+ * folio_managed_wrprotect - Should this folio's mappings stay write-protected?
+ * @folio: the folio to check
+ *
+ * Returns true if the folio is on a private node with NP_OPS_PROTECT_WRITE,
+ * meaning page table entries (PTE or PMD) should not be made writable.
+ * Write faults are intercepted by the service's handle_fault callback
+ * to promote the folio to DRAM.
+ *
+ * Used by:
+ *   - change_pte_range() / change_huge_pmd(): prevent mprotect write-upgrade
+ *   - remove_migration_pte() / remove_migration_pmd(): strip write after migration
+ *   - do_huge_pmd_wp_page(): dispatch to fault handler instead of reuse
+ */
+static inline bool folio_managed_wrprotect(struct folio *folio)
+{
+	return unlikely(folio_is_private_node(folio) &&
+			folio_private_flags(folio, NP_OPS_PROTECT_WRITE));
+}
+
+/**
+ * folio_managed_fixup_migration_pte - Fixup PTE after migration for
+ *                                     managed memory pages.
+ * @new: the destination page
+ * @pte: the PTE being installed (normal PTE built by caller)
+ * @old_pte: the original PTE (before migration, for swap entry flags)
+ * @vma: the VMA
+ *
+ * For MEMORY_DEVICE_PRIVATE pages: replaces the PTE with a device-private
+ * swap entry, preserving soft_dirty and uffd_wp from old_pte.
+ *
+ * For N_MEMORY_PRIVATE pages with NP_OPS_PROTECT_WRITE: strips the write
+ * bit so the next write triggers the fault handler for promotion.
+ *
+ * For normal pages: returns pte unmodified.
+ */
+static inline pte_t folio_managed_fixup_migration_pte(struct page *new,
+						      pte_t pte,
+						      pte_t old_pte,
+						      struct vm_area_struct *vma)
+{
+	if (unlikely(is_device_private_page(new))) {
+		softleaf_t entry;
+
+		if (pte_write(pte))
+			entry = make_writable_device_private_entry(
+						page_to_pfn(new));
+		else
+			entry = make_readable_device_private_entry(
+						page_to_pfn(new));
+		pte = softleaf_to_pte(entry);
+		if (pte_swp_soft_dirty(old_pte))
+			pte = pte_swp_mksoft_dirty(pte);
+		if (pte_swp_uffd_wp(old_pte))
+			pte = pte_swp_mkuffd_wp(pte);
+	} else if (folio_managed_wrprotect(page_folio(new))) {
+		pte = pte_wrprotect(pte);
+	}
+	return pte;
+}
+
 /**
  * folio_managed_migrate_notify - Notify service that a folio changed location
  * @src: the old folio (about to be freed)
diff --git a/mm/memory.c b/mm/memory.c
index 2a55edc48a65..0f78988befef 100644
--- a/mm/memory.c
+++ b/mm/memory.c
@@ -6079,6 +6079,10 @@ static vm_fault_t do_numa_page(struct vm_fault *vmf)
 	 * Make it present again, depending on how arch implements
 	 * non-accessible ptes, some can allow access by kernel mode.
 	 */
+	if (unlikely(folio && folio_managed_wrprotect(folio))) {
+		writable = false;
+		ignore_writable = true;
+	}
 	if (folio && folio_test_large(folio))
 		numa_rebuild_large_mapping(vmf, vma, folio, pte, ignore_writable,
 					   pte_write_upgrade);
@@ -6228,6 +6232,7 @@ static void fix_spurious_fault(struct vm_fault *vmf,
  */
 static vm_fault_t handle_pte_fault(struct vm_fault *vmf)
 {
+	struct folio *folio;
 	pte_t entry;
 
 	if (unlikely(pmd_none(*vmf->pmd))) {
@@ -6284,6 +6289,16 @@ static vm_fault_t handle_pte_fault(struct vm_fault *vmf)
 		update_mmu_tlb(vmf->vma, vmf->address, vmf->pte);
 		goto unlock;
 	}
+
+	folio = vm_normal_folio(vmf->vma, vmf->address, entry);
+	if (unlikely(folio && folio_is_private_managed(folio))) {
+		vm_fault_t fault_ret;
+
+		if (folio_managed_handle_fault(folio, vmf, PGTABLE_LEVEL_PTE,
+					       &fault_ret))
+			return fault_ret;
+	}
+
 	if (vmf->flags & (FAULT_FLAG_WRITE|FAULT_FLAG_UNSHARE)) {
 		if (!pte_write(entry))
 			return do_wp_page(vmf);
diff --git a/mm/migrate.c b/mm/migrate.c
index a54d4af04df3..f632e8b03504 100644
--- a/mm/migrate.c
+++ b/mm/migrate.c
@@ -398,19 +398,7 @@ static bool remove_migration_pte(struct folio *folio,
 		if (folio_test_anon(folio) && !softleaf_is_migration_read(entry))
 			rmap_flags |= RMAP_EXCLUSIVE;
 
-		if (unlikely(is_device_private_page(new))) {
-			if (pte_write(pte))
-				entry = make_writable_device_private_entry(
-							page_to_pfn(new));
-			else
-				entry = make_readable_device_private_entry(
-							page_to_pfn(new));
-			pte = softleaf_to_pte(entry);
-			if (pte_swp_soft_dirty(old_pte))
-				pte = pte_swp_mksoft_dirty(pte);
-			if (pte_swp_uffd_wp(old_pte))
-				pte = pte_swp_mkuffd_wp(pte);
-		}
+		pte = folio_managed_fixup_migration_pte(new, pte, old_pte, vma);
 
 #ifdef CONFIG_HUGETLB_PAGE
 		if (folio_test_hugetlb(folio)) {
diff --git a/mm/mprotect.c b/mm/mprotect.c
index 283889e4f1ce..830be609bc24 100644
--- a/mm/mprotect.c
+++ b/mm/mprotect.c
@@ -30,6 +30,7 @@
 #include <linux/mm_inline.h>
 #include <linux/pgtable.h>
 #include <linux/userfaultfd_k.h>
+#include <linux/node_private.h>
 #include <uapi/linux/mman.h>
 #include <asm/cacheflush.h>
 #include <asm/mmu_context.h>
@@ -290,7 +291,8 @@ static long change_pte_range(struct mmu_gather *tlb,
 			 * COW or special handling is required.
 			 */
 			if ((cp_flags & MM_CP_TRY_CHANGE_WRITABLE) &&
-			     !pte_write(ptent))
+			     !pte_write(ptent) &&
+			     !(folio && folio_managed_wrprotect(folio)))
 				set_write_prot_commit_flush_ptes(vma, folio, page,
 				addr, pte, oldpte, ptent, nr_ptes, tlb);
 			else
-- 
2.53.0


^ permalink raw reply related

* [RFC PATCH v4 14/27] mm/memory-tiers: NP_OPS_DEMOTION - support private node demotion
From: Gregory Price @ 2026-02-22  8:48 UTC (permalink / raw)
  To: lsf-pc
  Cc: linux-kernel, linux-cxl, cgroups, linux-mm, linux-trace-kernel,
	damon, kernel-team, gregkh, rafael, dakr, dave, jonathan.cameron,
	dave.jiang, alison.schofield, vishal.l.verma, ira.weiny,
	dan.j.williams, longman, akpm, david, lorenzo.stoakes,
	Liam.Howlett, vbabka, rppt, surenb, mhocko, osalvador, ziy,
	matthew.brost, joshua.hahnjy, rakie.kim, byungchul, gourry,
	ying.huang, apopple, axelrasmussen, yuanchu, weixugc, yury.norov,
	linux, mhiramat, mathieu.desnoyers, tj, hannes, mkoutny, jackmanb,
	sj, baolin.wang, npache, ryan.roberts, dev.jain, baohua,
	lance.yang, muchun.song, xu.xin16, chengming.zhou, jannh,
	linmiaohe, nao.horiguchi, pfalcato, rientjes, shakeel.butt, riel,
	harry.yoo, cl, roman.gushchin, chrisl, kasong, shikemeng, nphamcs,
	bhe, zhengqi.arch, terry.bowman
In-Reply-To: <20260222084842.1824063-1-gourry@gourry.net>

The memory-tier subsystem needs to know which private nodes should
appear as demotion targets.

Add NP_OPS_DEMOTION (BIT(2)):
   Node can be added as a demotion target by memory-tiers.

Add demotion backpressure support so private nodes can reject
new demotions cleanly, allowing vmscan to fall back to swap.

In the demotion path, try demotion to private nodes invididually,
then clear private nodes from the demotion target mask until a
non-private node is found, then fall back to the remaining mask.
This prevents LRU inversion while still allowing forward progress.

This is the closest match to the current behavior without making
private nodes inaccessible or preventing forward progress. We
should probably completely re-do the demotion logic to allow less
fallback and kick kswapd instead - right now we induce LRU
inversions by simply falling back to any node in the demotion list.

Add memory_tier_refresh_demotion() export for services to trigger
re-evaluation of demotion targets after changing their flags.

Signed-off-by: Gregory Price <gourry@gourry.net>
---
 include/linux/memory-tiers.h |  9 +++++++
 include/linux/node_private.h | 22 +++++++++++++++++
 mm/internal.h                |  7 ++++++
 mm/memory-tiers.c            | 46 ++++++++++++++++++++++++++++++++----
 mm/page_alloc.c              | 12 +++++++---
 mm/vmscan.c                  | 30 ++++++++++++++++++++++-
 6 files changed, 117 insertions(+), 9 deletions(-)

diff --git a/include/linux/memory-tiers.h b/include/linux/memory-tiers.h
index 3e1159f6762c..e1476432e359 100644
--- a/include/linux/memory-tiers.h
+++ b/include/linux/memory-tiers.h
@@ -58,6 +58,7 @@ struct memory_dev_type *mt_get_memory_type(int adist);
 int next_demotion_node(int node);
 void node_get_allowed_targets(pg_data_t *pgdat, nodemask_t *targets);
 bool node_is_toptier(int node);
+void memory_tier_refresh_demotion(void);
 #else
 static inline int next_demotion_node(int node)
 {
@@ -73,6 +74,10 @@ static inline bool node_is_toptier(int node)
 {
 	return true;
 }
+
+static inline void memory_tier_refresh_demotion(void)
+{
+}
 #endif
 
 #else
@@ -106,6 +111,10 @@ static inline bool node_is_toptier(int node)
 	return true;
 }
 
+static inline void memory_tier_refresh_demotion(void)
+{
+}
+
 static inline int register_mt_adistance_algorithm(struct notifier_block *nb)
 {
 	return 0;
diff --git a/include/linux/node_private.h b/include/linux/node_private.h
index e9b58afa366b..e254e36056cd 100644
--- a/include/linux/node_private.h
+++ b/include/linux/node_private.h
@@ -88,6 +88,8 @@ struct node_private_ops {
 #define NP_OPS_MIGRATION		BIT(0)
 /* Allow mempolicy-directed allocation and mbind migration to this node */
 #define NP_OPS_MEMPOLICY		BIT(1)
+/* Node participates as a demotion target in memory-tiers */
+#define NP_OPS_DEMOTION			BIT(2)
 
 /**
  * struct node_private - Per-node container for N_MEMORY_PRIVATE nodes
@@ -101,12 +103,14 @@ struct node_private_ops {
  *		callbacks that may sleep; 0 = fully released)
  * @released: Signaled when refcount drops to 0; unregister waits on this
  * @ops: Service callbacks and exclusion flags (NULL until service registers)
+ * @migration_blocked: Service signals migrations should pause
  */
 struct node_private {
 	void *owner;
 	refcount_t refcount;
 	struct completion released;
 	const struct node_private_ops *ops;
+	bool migration_blocked;
 };
 
 #ifdef CONFIG_NUMA
@@ -306,6 +310,19 @@ static inline bool nodes_private_mpol_allowed(const nodemask_t *nodes)
 	}
 	return eligible;
 }
+
+static inline bool node_private_migration_blocked(int nid)
+{
+	struct node_private *np;
+	bool blocked;
+
+	rcu_read_lock();
+	np = rcu_dereference(NODE_DATA(nid)->node_private);
+	blocked = np && READ_ONCE(np->migration_blocked);
+	rcu_read_unlock();
+
+	return blocked;
+}
 #endif /* CONFIG_MEMORY_HOTPLUG */
 
 #else /* !CONFIG_NUMA */
@@ -404,6 +421,11 @@ static inline bool nodes_private_mpol_allowed(const nodemask_t *nodes)
 	return false;
 }
 
+static inline bool node_private_migration_blocked(int nid)
+{
+	return false;
+}
+
 static inline int node_private_register(int nid, struct node_private *np)
 {
 	return -ENODEV;
diff --git a/mm/internal.h b/mm/internal.h
index 6ab4679fe943..5950e20d4023 100644
--- a/mm/internal.h
+++ b/mm/internal.h
@@ -1206,6 +1206,8 @@ extern int node_reclaim_mode;
 
 extern int node_reclaim(struct pglist_data *, gfp_t, unsigned int);
 extern int find_next_best_node(int node, nodemask_t *used_node_mask);
+extern int find_next_best_node_in(int node, nodemask_t *used_node_mask,
+				  const nodemask_t *candidates);
 extern bool numa_zone_alloc_allowed(int alloc_flags, struct zone *zone,
 			      gfp_t gfp_mask);
 #else
@@ -1220,6 +1222,11 @@ static inline int find_next_best_node(int node, nodemask_t *used_node_mask)
 {
 	return NUMA_NO_NODE;
 }
+static inline int find_next_best_node_in(int node, nodemask_t *used_node_mask,
+					 const nodemask_t *candidates)
+{
+	return NUMA_NO_NODE;
+}
 static inline bool numa_zone_alloc_allowed(int alloc_flags, struct zone *zone,
 				     gfp_t gfp_mask)
 {
diff --git a/mm/memory-tiers.c b/mm/memory-tiers.c
index 9c742e18e48f..434190fdc078 100644
--- a/mm/memory-tiers.c
+++ b/mm/memory-tiers.c
@@ -3,6 +3,7 @@
 #include <linux/lockdep.h>
 #include <linux/sysfs.h>
 #include <linux/kobject.h>
+#include <linux/node_private.h>
 #include <linux/memory.h>
 #include <linux/memory-tiers.h>
 #include <linux/notifier.h>
@@ -380,6 +381,8 @@ static void disable_all_demotion_targets(void)
 		if (memtier)
 			memtier->lower_tier_mask = NODE_MASK_NONE;
 	}
+	for_each_node_state(node, N_MEMORY_PRIVATE)
+		node_demotion[node].preferred = NODE_MASK_NONE;
 	/*
 	 * Ensure that the "disable" is visible across the system.
 	 * Readers will see either a combination of before+disable
@@ -421,6 +424,7 @@ static void establish_demotion_targets(void)
 	int target = NUMA_NO_NODE, node;
 	int distance, best_distance;
 	nodemask_t tier_nodes, lower_tier;
+	nodemask_t all_memory;
 
 	lockdep_assert_held_once(&memory_tier_lock);
 
@@ -429,6 +433,13 @@ static void establish_demotion_targets(void)
 
 	disable_all_demotion_targets();
 
+	/* Include private nodes that have opted in to demotion. */
+	all_memory = node_states[N_MEMORY];
+	for_each_node_state(node, N_MEMORY_PRIVATE) {
+		if (node_private_has_flag(node, NP_OPS_DEMOTION))
+			node_set(node, all_memory);
+	}
+
 	for_each_node_state(node, N_MEMORY) {
 		best_distance = -1;
 		nd = &node_demotion[node];
@@ -442,12 +453,12 @@ static void establish_demotion_targets(void)
 		memtier = list_next_entry(memtier, list);
 		tier_nodes = get_memtier_nodemask(memtier);
 		/*
-		 * find_next_best_node, use 'used' nodemask as a skip list.
+		 * find_next_best_node_in, use 'used' nodemask as a skip list.
 		 * Add all memory nodes except the selected memory tier
 		 * nodelist to skip list so that we find the best node from the
 		 * memtier nodelist.
 		 */
-		nodes_andnot(tier_nodes, node_states[N_MEMORY], tier_nodes);
+		nodes_andnot(tier_nodes, all_memory, tier_nodes);
 
 		/*
 		 * Find all the nodes in the memory tier node list of same best distance.
@@ -455,7 +466,8 @@ static void establish_demotion_targets(void)
 		 * in the preferred mask when allocating pages during demotion.
 		 */
 		do {
-			target = find_next_best_node(node, &tier_nodes);
+			target = find_next_best_node_in(node, &tier_nodes,
+							&all_memory);
 			if (target == NUMA_NO_NODE)
 				break;
 
@@ -495,7 +507,7 @@ static void establish_demotion_targets(void)
 	 * allocation to a set of nodes that is closer the above selected
 	 * preferred node.
 	 */
-	lower_tier = node_states[N_MEMORY];
+	lower_tier = all_memory;
 	list_for_each_entry(memtier, &memory_tiers, list) {
 		/*
 		 * Keep removing current tier from lower_tier nodes,
@@ -542,7 +554,7 @@ static struct memory_tier *set_node_memory_tier(int node)
 
 	lockdep_assert_held_once(&memory_tier_lock);
 
-	if (!node_state(node, N_MEMORY))
+	if (!node_state(node, N_MEMORY) && !node_state(node, N_MEMORY_PRIVATE))
 		return ERR_PTR(-EINVAL);
 
 	mt_calc_adistance(node, &adist);
@@ -865,6 +877,30 @@ int mt_calc_adistance(int node, int *adist)
 }
 EXPORT_SYMBOL_GPL(mt_calc_adistance);
 
+/**
+ * memory_tier_refresh_demotion() - Re-establish demotion targets
+ *
+ * Called by services after registering or unregistering ops->migrate_to on
+ * a private node, so that establish_demotion_targets() picks up the change.
+ */
+void memory_tier_refresh_demotion(void)
+{
+	int nid;
+
+	mutex_lock(&memory_tier_lock);
+	/*
+	 * Ensure private nodes are registered with a tier, otherwise
+	 * they won't show up in any node's demotion targets nodemask.
+	 */
+	for_each_node_state(nid, N_MEMORY_PRIVATE) {
+		if (!__node_get_memory_tier(nid))
+			set_node_memory_tier(nid);
+	}
+	establish_demotion_targets();
+	mutex_unlock(&memory_tier_lock);
+}
+EXPORT_SYMBOL_GPL(memory_tier_refresh_demotion);
+
 static int __meminit memtier_hotplug_callback(struct notifier_block *self,
 					      unsigned long action, void *_arg)
 {
diff --git a/mm/page_alloc.c b/mm/page_alloc.c
index ec6c1f8e85d8..e272dfdc6b00 100644
--- a/mm/page_alloc.c
+++ b/mm/page_alloc.c
@@ -5589,7 +5589,8 @@ static int node_load[MAX_NUMNODES];
  *
  * Return: node id of the found node or %NUMA_NO_NODE if no node is found.
  */
-int find_next_best_node(int node, nodemask_t *used_node_mask)
+int find_next_best_node_in(int node, nodemask_t *used_node_mask,
+			   const nodemask_t *candidates)
 {
 	int n, val;
 	int min_val = INT_MAX;
@@ -5599,12 +5600,12 @@ int find_next_best_node(int node, nodemask_t *used_node_mask)
 	 * Use the local node if we haven't already, but for memoryless local
 	 * node, we should skip it and fall back to other nodes.
 	 */
-	if (!node_isset(node, *used_node_mask) && node_state(node, N_MEMORY)) {
+	if (!node_isset(node, *used_node_mask) && node_isset(node, *candidates)) {
 		node_set(node, *used_node_mask);
 		return node;
 	}
 
-	for_each_node_state(n, N_MEMORY) {
+	for_each_node_mask(n, *candidates) {
 
 		/* Don't want a node to appear more than once */
 		if (node_isset(n, *used_node_mask))
@@ -5636,6 +5637,11 @@ int find_next_best_node(int node, nodemask_t *used_node_mask)
 	return best_node;
 }
 
+int find_next_best_node(int node, nodemask_t *used_node_mask)
+{
+	return find_next_best_node_in(node, used_node_mask,
+				      &node_states[N_MEMORY]);
+}
 
 /*
  * Build zonelists ordered by node and zones within node.
diff --git a/mm/vmscan.c b/mm/vmscan.c
index 6113be4d3519..0f534428ea88 100644
--- a/mm/vmscan.c
+++ b/mm/vmscan.c
@@ -58,6 +58,7 @@
 #include <linux/random.h>
 #include <linux/mmu_notifier.h>
 #include <linux/parser.h>
+#include <linux/node_private.h>
 
 #include <asm/tlbflush.h>
 #include <asm/div64.h>
@@ -355,6 +356,10 @@ static bool can_demote(int nid, struct scan_control *sc,
 	if (demotion_nid == NUMA_NO_NODE)
 		return false;
 
+	/* Don't demote when the target's service signals backpressure */
+	if (node_private_migration_blocked(demotion_nid))
+		return false;
+
 	/* If demotion node isn't in the cgroup's mems_allowed, fall back */
 	return mem_cgroup_node_allowed(memcg, demotion_nid);
 }
@@ -1022,8 +1027,10 @@ static unsigned int demote_folio_list(struct list_head *demote_folios,
 				     struct pglist_data *pgdat)
 {
 	int target_nid = next_demotion_node(pgdat->node_id);
-	unsigned int nr_succeeded;
+	int first_nid = target_nid;
+	unsigned int nr_succeeded = 0;
 	nodemask_t allowed_mask;
+	int ret;
 
 	struct migration_target_control mtc = {
 		/*
@@ -1046,6 +1053,27 @@ static unsigned int demote_folio_list(struct list_head *demote_folios,
 
 	node_get_allowed_targets(pgdat, &allowed_mask);
 
+	/* Try private node targets until we find non-private node */
+	while (node_state(target_nid, N_MEMORY_PRIVATE)) {
+		unsigned int nr = 0;
+
+		ret = node_private_migrate_to(demote_folios, target_nid,
+					      MIGRATE_ASYNC, MR_DEMOTION,
+					      &nr);
+		nr_succeeded += nr;
+		if (ret == 0 || list_empty(demote_folios))
+			return nr_succeeded;
+
+		target_nid = next_node_in(target_nid, allowed_mask);
+		if (target_nid == first_nid)
+			return nr_succeeded;
+		if (!node_state(target_nid, N_MEMORY_PRIVATE))
+			break;
+	}
+
+	/* target_nid is a non-private node; use standard migration */
+	mtc.nid = target_nid;
+
 	/* Demotion ignores all cpuset and mempolicy settings */
 	migrate_pages(demote_folios, alloc_demote_folio, NULL,
 		      (unsigned long)&mtc, MIGRATE_ASYNC, MR_DEMOTION,
-- 
2.53.0


^ permalink raw reply related

* [RFC PATCH v4 13/27] mm/mempolicy: NP_OPS_MEMPOLICY - support private node mempolicy
From: Gregory Price @ 2026-02-22  8:48 UTC (permalink / raw)
  To: lsf-pc
  Cc: linux-kernel, linux-cxl, cgroups, linux-mm, linux-trace-kernel,
	damon, kernel-team, gregkh, rafael, dakr, dave, jonathan.cameron,
	dave.jiang, alison.schofield, vishal.l.verma, ira.weiny,
	dan.j.williams, longman, akpm, david, lorenzo.stoakes,
	Liam.Howlett, vbabka, rppt, surenb, mhocko, osalvador, ziy,
	matthew.brost, joshua.hahnjy, rakie.kim, byungchul, gourry,
	ying.huang, apopple, axelrasmussen, yuanchu, weixugc, yury.norov,
	linux, mhiramat, mathieu.desnoyers, tj, hannes, mkoutny, jackmanb,
	sj, baolin.wang, npache, ryan.roberts, dev.jain, baohua,
	lance.yang, muchun.song, xu.xin16, chengming.zhou, jannh,
	linmiaohe, nao.horiguchi, pfalcato, rientjes, shakeel.butt, riel,
	harry.yoo, cl, roman.gushchin, chrisl, kasong, shikemeng, nphamcs,
	bhe, zhengqi.arch, terry.bowman
In-Reply-To: <20260222084842.1824063-1-gourry@gourry.net>

Some private nodes want userland to directly allocate from the node
via set_mempolicy() and mbind() - but don't want that node as normal
allocable system memory in the fallback lists.

Add NP_OPS_MEMPOLICY flag requiring NP_OPS_MIGRATION (since mbind can
drive migrations).  Only allow private nodes in policy nodemasks if
all private nodes in the mask support NP_OPS_MEMPOLICY. This prevents
__GFP_PRIVATE from unlocking nodes without NP_OPS_MEMPOLICY support.

Add __GFP_PRIVATE to mempolicy migration sites so moves to opted-in
private nodes succeed.

Update the sysfs "has_memory" attribute to include N_MEMORY_PRIVATE
nodes with NP_OPS_MEMPOLICY set, allowing existing numactl userland
tools to work without modification.

Signed-off-by: Gregory Price <gourry@gourry.net>
---
 drivers/base/node.c            | 22 +++++++++++++-
 include/linux/node_private.h   | 40 +++++++++++++++++++++++++
 include/uapi/linux/mempolicy.h |  1 +
 mm/mempolicy.c                 | 54 ++++++++++++++++++++++++++++++----
 mm/page_alloc.c                |  5 ++++
 5 files changed, 116 insertions(+), 6 deletions(-)

diff --git a/drivers/base/node.c b/drivers/base/node.c
index e587f5781135..c08b5a948779 100644
--- a/drivers/base/node.c
+++ b/drivers/base/node.c
@@ -953,6 +953,10 @@ int node_private_set_ops(int nid, const struct node_private_ops *ops)
 	    (!ops->migrate_to || !ops->folio_migrate))
 		return -EINVAL;
 
+	if ((ops->flags & NP_OPS_MEMPOLICY) &&
+	    !(ops->flags & NP_OPS_MIGRATION))
+		return -EINVAL;
+
 	mutex_lock(&node_private_lock);
 	np = rcu_dereference_protected(NODE_DATA(nid)->node_private,
 				       lockdep_is_held(&node_private_lock));
@@ -1145,6 +1149,21 @@ static ssize_t show_node_state(struct device *dev,
 			  nodemask_pr_args(&node_states[na->state]));
 }
 
+/* has_memory includes N_MEMORY + N_MEMORY_PRIVATE that support mempolicy. */
+static ssize_t show_has_memory(struct device *dev,
+			       struct device_attribute *attr, char *buf)
+{
+	nodemask_t mask = node_states[N_MEMORY];
+	int nid;
+
+	for_each_node_state(nid, N_MEMORY_PRIVATE) {
+		if (node_private_has_flag(nid, NP_OPS_MEMPOLICY))
+			node_set(nid, mask);
+	}
+
+	return sysfs_emit(buf, "%*pbl\n", nodemask_pr_args(&mask));
+}
+
 #define _NODE_ATTR(name, state) \
 	{ __ATTR(name, 0444, show_node_state, NULL), state }
 
@@ -1155,7 +1174,8 @@ static struct node_attr node_state_attr[] = {
 #ifdef CONFIG_HIGHMEM
 	[N_HIGH_MEMORY] = _NODE_ATTR(has_high_memory, N_HIGH_MEMORY),
 #endif
-	[N_MEMORY] = _NODE_ATTR(has_memory, N_MEMORY),
+	[N_MEMORY] = { __ATTR(has_memory, 0444, show_has_memory, NULL),
+		       N_MEMORY },
 	[N_MEMORY_PRIVATE] = _NODE_ATTR(has_private_memory, N_MEMORY_PRIVATE),
 	[N_CPU] = _NODE_ATTR(has_cpu, N_CPU),
 	[N_GENERIC_INITIATOR] = _NODE_ATTR(has_generic_initiator,
diff --git a/include/linux/node_private.h b/include/linux/node_private.h
index 0c5be1ee6e60..e9b58afa366b 100644
--- a/include/linux/node_private.h
+++ b/include/linux/node_private.h
@@ -86,6 +86,8 @@ struct node_private_ops {
 
 /* Allow user/kernel migration; requires migrate_to and folio_migrate */
 #define NP_OPS_MIGRATION		BIT(0)
+/* Allow mempolicy-directed allocation and mbind migration to this node */
+#define NP_OPS_MEMPOLICY		BIT(1)
 
 /**
  * struct node_private - Per-node container for N_MEMORY_PRIVATE nodes
@@ -276,6 +278,34 @@ static inline int node_private_migrate_to(struct list_head *folios, int nid,
 
 	return ret;
 }
+
+static inline bool node_mpol_eligible(int nid)
+{
+	bool ret;
+
+	if (!node_state(nid, N_MEMORY_PRIVATE))
+		return node_state(nid, N_MEMORY);
+
+	rcu_read_lock();
+	ret = node_private_has_flag(nid, NP_OPS_MEMPOLICY);
+	rcu_read_unlock();
+	return ret;
+}
+
+static inline bool nodes_private_mpol_allowed(const nodemask_t *nodes)
+{
+	int nid;
+	bool eligible = false;
+
+	for_each_node_mask(nid, *nodes) {
+		if (!node_state(nid, N_MEMORY_PRIVATE))
+			continue;
+		if (!node_mpol_eligible(nid))
+			return false;
+		eligible = true;
+	}
+	return eligible;
+}
 #endif /* CONFIG_MEMORY_HOTPLUG */
 
 #else /* !CONFIG_NUMA */
@@ -364,6 +394,16 @@ static inline int node_private_migrate_to(struct list_head *folios, int nid,
 	return -ENODEV;
 }
 
+static inline bool node_mpol_eligible(int nid)
+{
+	return false;
+}
+
+static inline bool nodes_private_mpol_allowed(const nodemask_t *nodes)
+{
+	return false;
+}
+
 static inline int node_private_register(int nid, struct node_private *np)
 {
 	return -ENODEV;
diff --git a/include/uapi/linux/mempolicy.h b/include/uapi/linux/mempolicy.h
index 8fbbe613611a..b606eae983c8 100644
--- a/include/uapi/linux/mempolicy.h
+++ b/include/uapi/linux/mempolicy.h
@@ -64,6 +64,7 @@ enum {
 #define MPOL_F_SHARED  (1 << 0)	/* identify shared policies */
 #define MPOL_F_MOF	(1 << 3) /* this policy wants migrate on fault */
 #define MPOL_F_MORON	(1 << 4) /* Migrate On protnone Reference On Node */
+#define MPOL_F_PRIVATE	(1 << 5) /* policy targets private node; use __GFP_PRIVATE */
 
 /*
  * Enabling zone reclaim means the page allocator will attempt to fulfill
diff --git a/mm/mempolicy.c b/mm/mempolicy.c
index 2b0f9762d171..8ac014950e88 100644
--- a/mm/mempolicy.c
+++ b/mm/mempolicy.c
@@ -406,8 +406,6 @@ static int mpol_new_preferred(struct mempolicy *pol, const nodemask_t *nodes)
 static int mpol_set_nodemask(struct mempolicy *pol,
 		     const nodemask_t *nodes, struct nodemask_scratch *nsc)
 {
-	int ret;
-
 	/*
 	 * Default (pol==NULL) resp. local memory policies are not a
 	 * subject of any remapping. They also do not need any special
@@ -416,9 +414,12 @@ static int mpol_set_nodemask(struct mempolicy *pol,
 	if (!pol || pol->mode == MPOL_LOCAL)
 		return 0;
 
-	/* Check N_MEMORY */
+	/* Check N_MEMORY and N_MEMORY_PRIVATE*/
 	nodes_and(nsc->mask1,
 		  cpuset_current_mems_allowed, node_states[N_MEMORY]);
+	nodes_and(nsc->mask2, cpuset_current_mems_allowed,
+		  node_states[N_MEMORY_PRIVATE]);
+	nodes_or(nsc->mask1, nsc->mask1, nsc->mask2);
 
 	VM_BUG_ON(!nodes);
 
@@ -432,8 +433,13 @@ static int mpol_set_nodemask(struct mempolicy *pol,
 	else
 		pol->w.cpuset_mems_allowed = cpuset_current_mems_allowed;
 
-	ret = mpol_ops[pol->mode].create(pol, &nsc->mask2);
-	return ret;
+	/* All private nodes in the mask must have NP_OPS_MEMPOLICY. */
+	if (nodes_private_mpol_allowed(&nsc->mask2))
+		pol->flags |= MPOL_F_PRIVATE;
+	else if (nodes_intersects(nsc->mask2, node_states[N_MEMORY_PRIVATE]))
+		return -EINVAL;
+
+	return mpol_ops[pol->mode].create(pol, &nsc->mask2);
 }
 
 /*
@@ -500,6 +506,7 @@ static void mpol_rebind_default(struct mempolicy *pol, const nodemask_t *nodes)
 static void mpol_rebind_nodemask(struct mempolicy *pol, const nodemask_t *nodes)
 {
 	nodemask_t tmp;
+	int nid;
 
 	if (pol->flags & MPOL_F_STATIC_NODES)
 		nodes_and(tmp, pol->w.user_nodemask, *nodes);
@@ -514,6 +521,21 @@ static void mpol_rebind_nodemask(struct mempolicy *pol, const nodemask_t *nodes)
 	if (nodes_empty(tmp))
 		tmp = *nodes;
 
+	/*
+	 * Drop private nodes that don't have mempolicy support.
+	 * cpusets guarantees at least one N_MEMORY node in effective_mems
+	 * and mems_allowed, so dropping private nodes here is safe.
+	 */
+	for_each_node_mask(nid, tmp) {
+		if (node_state(nid, N_MEMORY_PRIVATE) &&
+		    !node_private_has_flag(nid, NP_OPS_MEMPOLICY))
+			node_clear(nid, tmp);
+	}
+	if (nodes_intersects(tmp, node_states[N_MEMORY_PRIVATE]))
+		pol->flags |= MPOL_F_PRIVATE;
+	else
+		pol->flags &= ~MPOL_F_PRIVATE;
+
 	pol->nodes = tmp;
 }
 
@@ -661,6 +683,9 @@ static void queue_folios_pmd(pmd_t *pmd, struct mm_walk *walk)
 	}
 	if (!queue_folio_required(folio, qp))
 		return;
+	if (folio_is_private_node(folio) &&
+	    !folio_private_flags(folio, NP_OPS_MIGRATION))
+		return;
 	if (!(qp->flags & (MPOL_MF_MOVE | MPOL_MF_MOVE_ALL)) ||
 	    !vma_migratable(walk->vma) ||
 	    !migrate_folio_add(folio, qp->pagelist, qp->flags))
@@ -717,6 +742,9 @@ static int queue_folios_pte_range(pmd_t *pmd, unsigned long addr,
 		folio = vm_normal_folio(vma, addr, ptent);
 		if (!folio || folio_is_zone_device(folio))
 			continue;
+		if (folio_is_private_node(folio) &&
+		    !folio_private_flags(folio, NP_OPS_MIGRATION))
+			continue;
 		if (folio_test_large(folio) && max_nr != 1)
 			nr = folio_pte_batch(folio, pte, ptent, max_nr);
 		/*
@@ -1451,6 +1479,9 @@ static struct folio *alloc_migration_target_by_mpol(struct folio *src,
 	else
 		gfp = GFP_HIGHUSER_MOVABLE | __GFP_RETRY_MAYFAIL | __GFP_COMP;
 
+	if (pol->flags & MPOL_F_PRIVATE)
+		gfp |= __GFP_PRIVATE;
+
 	return folio_alloc_mpol(gfp, order, pol, ilx, nid);
 }
 #else
@@ -2280,6 +2311,15 @@ static nodemask_t *policy_nodemask(gfp_t gfp, struct mempolicy *pol,
 			nodemask = &pol->nodes;
 		if (pol->home_node != NUMA_NO_NODE)
 			*nid = pol->home_node;
+		else if ((pol->flags & MPOL_F_PRIVATE) &&
+			 !node_isset(*nid, pol->nodes)) {
+			/*
+			 * Private nodes are not in N_MEMORY nodes' zonelists.
+			 * When the preferred nid (usually numa_node_id()) can't
+			 * reach the policy nodes, start from a policy node.
+			 */
+			*nid = first_node(pol->nodes);
+		}
 		/*
 		 * __GFP_THISNODE shouldn't even be used with the bind policy
 		 * because we might easily break the expectation to stay on the
@@ -2533,6 +2573,10 @@ struct folio *vma_alloc_folio_noprof(gfp_t gfp, int order, struct vm_area_struct
 		gfp |= __GFP_NOWARN;
 
 	pol = get_vma_policy(vma, addr, order, &ilx);
+
+	if (pol->flags & MPOL_F_PRIVATE)
+		gfp |= __GFP_PRIVATE;
+
 	folio = folio_alloc_mpol_noprof(gfp, order, pol, ilx, numa_node_id());
 	mpol_cond_put(pol);
 	return folio;
diff --git a/mm/page_alloc.c b/mm/page_alloc.c
index 5a1b35421d78..ec6c1f8e85d8 100644
--- a/mm/page_alloc.c
+++ b/mm/page_alloc.c
@@ -3849,8 +3849,13 @@ get_page_from_freelist(gfp_t gfp_mask, unsigned int order, int alloc_flags,
 		 * if another process has NUMA bindings and is causing
 		 * kswapd wakeups on only some nodes. Avoid accidental
 		 * "node_reclaim_mode"-like behavior in this case.
+		 *
+		 * Nodes without kswapd (some private nodes) are never
+		 * skipped - this causes some mempolicies to silently
+		 * fall back to DRAM even if the node is eligible.
 		 */
 		if (skip_kswapd_nodes &&
+		    zone->zone_pgdat->kswapd &&
 		    !waitqueue_active(&zone->zone_pgdat->kswapd_wait)) {
 			skipped_kswapd_nodes = true;
 			continue;
-- 
2.53.0


^ permalink raw reply related

* [RFC PATCH v4 12/27] mm/migrate: NP_OPS_MIGRATION - support private node user migration
From: Gregory Price @ 2026-02-22  8:48 UTC (permalink / raw)
  To: lsf-pc
  Cc: linux-kernel, linux-cxl, cgroups, linux-mm, linux-trace-kernel,
	damon, kernel-team, gregkh, rafael, dakr, dave, jonathan.cameron,
	dave.jiang, alison.schofield, vishal.l.verma, ira.weiny,
	dan.j.williams, longman, akpm, david, lorenzo.stoakes,
	Liam.Howlett, vbabka, rppt, surenb, mhocko, osalvador, ziy,
	matthew.brost, joshua.hahnjy, rakie.kim, byungchul, gourry,
	ying.huang, apopple, axelrasmussen, yuanchu, weixugc, yury.norov,
	linux, mhiramat, mathieu.desnoyers, tj, hannes, mkoutny, jackmanb,
	sj, baolin.wang, npache, ryan.roberts, dev.jain, baohua,
	lance.yang, muchun.song, xu.xin16, chengming.zhou, jannh,
	linmiaohe, nao.horiguchi, pfalcato, rientjes, shakeel.butt, riel,
	harry.yoo, cl, roman.gushchin, chrisl, kasong, shikemeng, nphamcs,
	bhe, zhengqi.arch, terry.bowman
In-Reply-To: <20260222084842.1824063-1-gourry@gourry.net>

Private node services may want to support user-driven migration
(migrate_pages syscall, mbind) to allow data movement between regular
and private nodes.

ZONE_DEVICE always rejects user migration, but private nodes should
be able to opt in.

Add NP_OPS_MIGRATION flag and folio_managed_user_migrate() wrapper that
dispatches migration requests.  Private nodes can either set the flag
and provide a custom migrate_to callback for driver-managed migration.

In migrate_to_node(), allows GFP_PRIVATE when the destination node
supports NP_OPS_MIGRATION, enabling migrate_pages syscall to target
private nodes.

Signed-off-by: Gregory Price <gourry@gourry.net>
---
 drivers/base/node.c          |   4 ++
 include/linux/migrate.h      |  10 +++
 include/linux/node_private.h | 122 +++++++++++++++++++++++++++++++++++
 mm/damon/paddr.c             |   3 +
 mm/internal.h                |  24 +++++++
 mm/mempolicy.c               |  10 +--
 mm/migrate.c                 |  49 ++++++++++----
 mm/rmap.c                    |   4 +-
 8 files changed, 206 insertions(+), 20 deletions(-)

diff --git a/drivers/base/node.c b/drivers/base/node.c
index 646dc48a23b5..e587f5781135 100644
--- a/drivers/base/node.c
+++ b/drivers/base/node.c
@@ -949,6 +949,10 @@ int node_private_set_ops(int nid, const struct node_private_ops *ops)
 	if (!node_possible(nid))
 		return -EINVAL;
 
+	if ((ops->flags & NP_OPS_MIGRATION) &&
+	    (!ops->migrate_to || !ops->folio_migrate))
+		return -EINVAL;
+
 	mutex_lock(&node_private_lock);
 	np = rcu_dereference_protected(NODE_DATA(nid)->node_private,
 				       lockdep_is_held(&node_private_lock));
diff --git a/include/linux/migrate.h b/include/linux/migrate.h
index 26ca00c325d9..7b2da3875ff2 100644
--- a/include/linux/migrate.h
+++ b/include/linux/migrate.h
@@ -71,6 +71,9 @@ void folio_migrate_flags(struct folio *newfolio, struct folio *folio);
 int folio_migrate_mapping(struct address_space *mapping,
 		struct folio *newfolio, struct folio *folio, int extra_count);
 int set_movable_ops(const struct movable_operations *ops, enum pagetype type);
+int migrate_folios_to_node(struct list_head *folios, int nid,
+				    enum migrate_mode mode,
+				    enum migrate_reason reason);
 
 #else
 
@@ -96,6 +99,13 @@ static inline int set_movable_ops(const struct movable_operations *ops, enum pag
 {
 	return -ENOSYS;
 }
+static inline int migrate_folios_to_node(struct list_head *folios,
+						  int nid,
+						  enum migrate_mode mode,
+						  enum migrate_reason reason)
+{
+	return -ENOSYS;
+}
 
 #endif /* CONFIG_MIGRATION */
 
diff --git a/include/linux/node_private.h b/include/linux/node_private.h
index f9dd2d25c8a5..0c5be1ee6e60 100644
--- a/include/linux/node_private.h
+++ b/include/linux/node_private.h
@@ -4,6 +4,7 @@
 
 #include <linux/completion.h>
 #include <linux/memremap.h>
+#include <linux/migrate_mode.h>
 #include <linux/mm.h>
 #include <linux/nodemask.h>
 #include <linux/rcupdate.h>
@@ -52,15 +53,40 @@ struct vm_fault;
  *     or NULL when called for the final (original) folio after all sub-folios
  *     have been split off.
  *
+ * @migrate_to: Migrate folios TO this node.
+ *	[refcounted callback]
+ *	Returns: 0 on full success, >0 = number of folios that failed to
+ *		 migrate, <0 = error.  Matches migrate_pages() semantics.
+ *		 @nr_succeeded is set to the number of successfully migrated
+ *		 folios (may be NULL if caller doesn't need it).
+ *
+ * @folio_migrate: Post-migration notification that a folio on this private node
+ *    changed physical location (on the same node or a different node).
+ *    [folio-referenced callback]
+ *     Called from migrate_folio_move() after data has been copied but before
+ *     migration entries are replaced with real PTEs.  Both @src and @dst are
+ *     locked.  Faults block in migration_entry_wait() until
+ *     remove_migration_ptes() runs, so the service can safely update
+ *     PFN-based metadata (compression tables, device page tables, DMA
+ *     mappings, etc.) before any access through the page tables.
+ *
  * @flags: Operation exclusion flags (NP_OPS_* constants).
  *
  */
 struct node_private_ops {
 	bool (*free_folio)(struct folio *folio);
 	void (*folio_split)(struct folio *folio, struct folio *new_folio);
+	int (*migrate_to)(struct list_head *folios, int nid,
+				  enum migrate_mode mode,
+				  enum migrate_reason reason,
+				  unsigned int *nr_succeeded);
+	void (*folio_migrate)(struct folio *src, struct folio *dst);
 	unsigned long flags;
 };
 
+/* Allow user/kernel migration; requires migrate_to and folio_migrate */
+#define NP_OPS_MIGRATION		BIT(0)
+
 /**
  * struct node_private - Per-node container for N_MEMORY_PRIVATE nodes
  *
@@ -177,6 +203,81 @@ static inline void folio_managed_split_cb(struct folio *original_folio,
 		node_private_split_cb(original_folio, new_folio);
 }
 
+#ifdef CONFIG_MEMORY_HOTPLUG
+static inline int folio_managed_allows_user_migrate(struct folio *folio)
+{
+	if (folio_is_zone_device(folio))
+		return -ENOENT;
+	return node_private_has_flag(folio_nid(folio), NP_OPS_MIGRATION) ?
+	       folio_nid(folio) : -ENOENT;
+}
+
+/**
+ * folio_managed_allows_migrate - Check if a managed folio supports migration
+ * @folio: The folio to check
+ *
+ * Returns true if the folio can be migrated.  For zone_device folios, only
+ * device_private and device_coherent support migration.  For private node
+ * folios, migration requires NP_OPS_MIGRATION.  Normal folios always
+ * return true.
+ */
+static inline bool folio_managed_allows_migrate(struct folio *folio)
+{
+	if (folio_is_zone_device(folio))
+		return folio_is_device_private(folio) ||
+		       folio_is_device_coherent(folio);
+	if (folio_is_private_node(folio))
+		return folio_private_flags(folio, NP_OPS_MIGRATION);
+	return true;
+}
+
+/**
+ * node_private_migrate_to - Attempt service-specific migration to a private node
+ * @folios: list of folios to migrate (may sleep)
+ * @nid: target node
+ * @mode: migration mode (MIGRATE_ASYNC, MIGRATE_SYNC, etc.)
+ * @reason: migration reason (MR_DEMOTION, MR_SYSCALL, etc.)
+ * @nr_succeeded: optional output for number of successfully migrated folios
+ *
+ * If @nid is an N_MEMORY_PRIVATE node with a migrate_to callback,
+ * invokes the callback and returns the result with migrate_pages()
+ * semantics (0 = full success, >0 = failure count, <0 = error).
+ * Returns -ENODEV if the node is not private or the service is being
+ * torn down.
+ *
+ * The source folios are on other nodes, so they do not pin the target
+ * node's node_private.  A temporary refcount is taken under rcu_read_lock
+ * to keep node_private (and the service module) alive across the callback.
+ */
+static inline int node_private_migrate_to(struct list_head *folios, int nid,
+					  enum migrate_mode mode,
+					  enum migrate_reason reason,
+					  unsigned int *nr_succeeded)
+{
+	int (*fn)(struct list_head *, int, enum migrate_mode,
+		  enum migrate_reason, unsigned int *);
+	struct node_private *np;
+	int ret;
+
+	rcu_read_lock();
+	np = rcu_dereference(NODE_DATA(nid)->node_private);
+	if (!np || !np->ops || !np->ops->migrate_to ||
+	    !refcount_inc_not_zero(&np->refcount)) {
+		rcu_read_unlock();
+		return -ENODEV;
+	}
+	fn = np->ops->migrate_to;
+	rcu_read_unlock();
+
+	ret = fn(folios, nid, mode, reason, nr_succeeded);
+
+	if (refcount_dec_and_test(&np->refcount))
+		complete(&np->released);
+
+	return ret;
+}
+#endif /* CONFIG_MEMORY_HOTPLUG */
+
 #else /* !CONFIG_NUMA */
 
 static inline bool folio_is_private_node(struct folio *folio)
@@ -242,6 +343,27 @@ int node_private_clear_ops(int nid, const struct node_private_ops *ops);
 
 #else /* !CONFIG_NUMA || !CONFIG_MEMORY_HOTPLUG */
 
+static inline int folio_managed_allows_user_migrate(struct folio *folio)
+{
+	return -ENOENT;
+}
+
+static inline bool folio_managed_allows_migrate(struct folio *folio)
+{
+	if (folio_is_zone_device(folio))
+		return folio_is_device_private(folio) ||
+		       folio_is_device_coherent(folio);
+	return true;
+}
+
+static inline int node_private_migrate_to(struct list_head *folios, int nid,
+					  enum migrate_mode mode,
+					  enum migrate_reason reason,
+					  unsigned int *nr_succeeded)
+{
+	return -ENODEV;
+}
+
 static inline int node_private_register(int nid, struct node_private *np)
 {
 	return -ENODEV;
diff --git a/mm/damon/paddr.c b/mm/damon/paddr.c
index 07a8aead439e..532b8e2c62b0 100644
--- a/mm/damon/paddr.c
+++ b/mm/damon/paddr.c
@@ -277,6 +277,9 @@ static unsigned long damon_pa_migrate(struct damon_region *r,
 		else
 			*sz_filter_passed += folio_size(folio) / addr_unit;
 
+		if (!folio_managed_allows_migrate(folio))
+			goto put_folio;
+
 		if (!folio_isolate_lru(folio))
 			goto put_folio;
 		list_add(&folio->lru, &folio_list);
diff --git a/mm/internal.h b/mm/internal.h
index 658da41cdb8e..6ab4679fe943 100644
--- a/mm/internal.h
+++ b/mm/internal.h
@@ -1442,6 +1442,30 @@ static inline bool folio_managed_on_free(struct folio *folio)
 	return false;
 }
 
+/**
+ * folio_managed_migrate_notify - Notify service that a folio changed location
+ * @src: the old folio (about to be freed)
+ * @dst: the new folio (data already copied, migration entries still in place)
+ *
+ * Called from migrate_folio_move() after data has been copied but before
+ * remove_migration_ptes() installs real PTEs pointing to @dst.  While
+ * migration entries are in place, faults block in migration_entry_wait(),
+ * so the service can safely update PFN-based metadata before any access
+ * through the page tables.  Both @src and @dst are locked.
+ */
+static inline void folio_managed_migrate_notify(struct folio *src,
+						struct folio *dst)
+{
+	const struct node_private_ops *ops;
+
+	if (!folio_is_private_node(src))
+		return;
+
+	ops = folio_node_private_ops(src);
+	if (ops && ops->folio_migrate)
+		ops->folio_migrate(src, dst);
+}
+
 struct vm_struct *__get_vm_area_node(unsigned long size,
 				     unsigned long align, unsigned long shift,
 				     unsigned long vm_flags, unsigned long start,
diff --git a/mm/mempolicy.c b/mm/mempolicy.c
index 68a98ba57882..2b0f9762d171 100644
--- a/mm/mempolicy.c
+++ b/mm/mempolicy.c
@@ -111,6 +111,7 @@
 #include <linux/mmu_notifier.h>
 #include <linux/printk.h>
 #include <linux/leafops.h>
+#include <linux/node_private.h>
 #include <linux/gcd.h>
 
 #include <asm/tlbflush.h>
@@ -1282,11 +1283,6 @@ static long migrate_to_node(struct mm_struct *mm, int source, int dest,
 	LIST_HEAD(pagelist);
 	long nr_failed;
 	long err = 0;
-	struct migration_target_control mtc = {
-		.nid = dest,
-		.gfp_mask = GFP_HIGHUSER_MOVABLE | __GFP_THISNODE,
-		.reason = MR_SYSCALL,
-	};
 
 	nodes_clear(nmask);
 	node_set(source, nmask);
@@ -1311,8 +1307,8 @@ static long migrate_to_node(struct mm_struct *mm, int source, int dest,
 	mmap_read_unlock(mm);
 
 	if (!list_empty(&pagelist)) {
-		err = migrate_pages(&pagelist, alloc_migration_target, NULL,
-			(unsigned long)&mtc, MIGRATE_SYNC, MR_SYSCALL, NULL);
+		err = migrate_folios_to_node(&pagelist, dest, MIGRATE_SYNC,
+					     MR_SYSCALL);
 		if (err)
 			putback_movable_pages(&pagelist);
 	}
diff --git a/mm/migrate.c b/mm/migrate.c
index 5169f9717f60..a54d4af04df3 100644
--- a/mm/migrate.c
+++ b/mm/migrate.c
@@ -43,6 +43,7 @@
 #include <linux/sched/sysctl.h>
 #include <linux/memory-tiers.h>
 #include <linux/pagewalk.h>
+#include <linux/node_private.h>
 
 #include <asm/tlbflush.h>
 
@@ -1387,6 +1388,8 @@ static int migrate_folio_move(free_folio_t put_new_folio, unsigned long private,
 	if (old_page_state & PAGE_WAS_MLOCKED)
 		lru_add_drain();
 
+	folio_managed_migrate_notify(src, dst);
+
 	if (old_page_state & PAGE_WAS_MAPPED)
 		remove_migration_ptes(src, dst, 0);
 
@@ -2165,6 +2168,7 @@ int migrate_pages(struct list_head *from, new_folio_t get_new_folio,
 
 	return rc_gather;
 }
+EXPORT_SYMBOL_GPL(migrate_pages);
 
 struct folio *alloc_migration_target(struct folio *src, unsigned long private)
 {
@@ -2204,6 +2208,31 @@ struct folio *alloc_migration_target(struct folio *src, unsigned long private)
 
 	return __folio_alloc(gfp_mask, order, nid, mtc->nmask);
 }
+EXPORT_SYMBOL_GPL(alloc_migration_target);
+
+static int __migrate_folios_to_node(struct list_head *folios, int nid,
+				    enum migrate_mode mode,
+				    enum migrate_reason reason)
+{
+	struct migration_target_control mtc = {
+		.nid = nid,
+		.gfp_mask = GFP_HIGHUSER_MOVABLE | __GFP_THISNODE,
+		.reason = reason,
+	};
+
+	return migrate_pages(folios, alloc_migration_target, NULL,
+			     (unsigned long)&mtc, mode, reason, NULL);
+}
+
+int migrate_folios_to_node(struct list_head *folios, int nid,
+			   enum migrate_mode mode,
+			   enum migrate_reason reason)
+{
+	if (node_state(nid, N_MEMORY_PRIVATE))
+		return node_private_migrate_to(folios, nid, mode,
+					       reason, NULL);
+	return __migrate_folios_to_node(folios, nid, mode, reason);
+}
 
 #ifdef CONFIG_NUMA
 
@@ -2221,14 +2250,8 @@ static int store_status(int __user *status, int start, int value, int nr)
 static int do_move_pages_to_node(struct list_head *pagelist, int node)
 {
 	int err;
-	struct migration_target_control mtc = {
-		.nid = node,
-		.gfp_mask = GFP_HIGHUSER_MOVABLE | __GFP_THISNODE,
-		.reason = MR_SYSCALL,
-	};
 
-	err = migrate_pages(pagelist, alloc_migration_target, NULL,
-		(unsigned long)&mtc, MIGRATE_SYNC, MR_SYSCALL, NULL);
+	err = migrate_folios_to_node(pagelist, node, MIGRATE_SYNC, MR_SYSCALL);
 	if (err)
 		putback_movable_pages(pagelist);
 	return err;
@@ -2240,7 +2263,7 @@ static int __add_folio_for_migration(struct folio *folio, int node,
 	if (is_zero_folio(folio) || is_huge_zero_folio(folio))
 		return -EFAULT;
 
-	if (folio_is_zone_device(folio))
+	if (!folio_managed_allows_migrate(folio))
 		return -ENOENT;
 
 	if (folio_nid(folio) == node)
@@ -2364,7 +2387,8 @@ static int do_pages_move(struct mm_struct *mm, nodemask_t task_nodes,
 		err = -ENODEV;
 		if (node < 0 || node >= MAX_NUMNODES)
 			goto out_flush;
-		if (!node_state(node, N_MEMORY))
+		if (!node_state(node, N_MEMORY) &&
+		    !node_state(node, N_MEMORY_PRIVATE))
 			goto out_flush;
 
 		err = -EACCES;
@@ -2449,8 +2473,8 @@ static void do_pages_stat_array(struct mm_struct *mm, unsigned long nr_pages,
 		if (folio) {
 			if (is_zero_folio(folio) || is_huge_zero_folio(folio))
 				err = -EFAULT;
-			else if (folio_is_zone_device(folio))
-				err = -ENOENT;
+			else if (unlikely(folio_is_private_managed(folio)))
+				err = folio_managed_allows_user_migrate(folio);
 			else
 				err = folio_nid(folio);
 			folio_walk_end(&fw, vma);
@@ -2660,6 +2684,9 @@ int migrate_misplaced_folio_prepare(struct folio *folio,
 	int nr_pages = folio_nr_pages(folio);
 	pg_data_t *pgdat = NODE_DATA(node);
 
+	if (!folio_managed_allows_migrate(folio))
+		return -ENOENT;
+
 	if (folio_is_file_lru(folio)) {
 		/*
 		 * Do not migrate file folios that are mapped in multiple
diff --git a/mm/rmap.c b/mm/rmap.c
index f955f02d570e..805f9ceb82f3 100644
--- a/mm/rmap.c
+++ b/mm/rmap.c
@@ -72,6 +72,7 @@
 #include <linux/backing-dev.h>
 #include <linux/page_idle.h>
 #include <linux/memremap.h>
+#include <linux/node_private.h>
 #include <linux/userfaultfd_k.h>
 #include <linux/mm_inline.h>
 #include <linux/oom.h>
@@ -2616,8 +2617,7 @@ void try_to_migrate(struct folio *folio, enum ttu_flags flags)
 					TTU_SYNC | TTU_BATCH_FLUSH)))
 		return;
 
-	if (folio_is_zone_device(folio) &&
-	    (!folio_is_device_private(folio) && !folio_is_device_coherent(folio)))
+	if (!folio_managed_allows_migrate(folio))
 		return;
 
 	/*
-- 
2.53.0


^ permalink raw reply related

* [RFC PATCH v4 11/27] mm/huge_memory.c: add private node folio split notification callback
From: Gregory Price @ 2026-02-22  8:48 UTC (permalink / raw)
  To: lsf-pc
  Cc: linux-kernel, linux-cxl, cgroups, linux-mm, linux-trace-kernel,
	damon, kernel-team, gregkh, rafael, dakr, dave, jonathan.cameron,
	dave.jiang, alison.schofield, vishal.l.verma, ira.weiny,
	dan.j.williams, longman, akpm, david, lorenzo.stoakes,
	Liam.Howlett, vbabka, rppt, surenb, mhocko, osalvador, ziy,
	matthew.brost, joshua.hahnjy, rakie.kim, byungchul, gourry,
	ying.huang, apopple, axelrasmussen, yuanchu, weixugc, yury.norov,
	linux, mhiramat, mathieu.desnoyers, tj, hannes, mkoutny, jackmanb,
	sj, baolin.wang, npache, ryan.roberts, dev.jain, baohua,
	lance.yang, muchun.song, xu.xin16, chengming.zhou, jannh,
	linmiaohe, nao.horiguchi, pfalcato, rientjes, shakeel.butt, riel,
	harry.yoo, cl, roman.gushchin, chrisl, kasong, shikemeng, nphamcs,
	bhe, zhengqi.arch, terry.bowman
In-Reply-To: <20260222084842.1824063-1-gourry@gourry.net>

Some private node services may need to update internal metadata when
a THP folio is split.  ZONE_DEVICE already has a split callback via
pgmap->ops; private nodes can provide the same capability.

Just like zone_device, some private node services may want to know
about a folio being split.  Add this optional callback to the ops
struct and add a wrapper for zone_device and private node callback
dispatch to be consolidated.

Wire this into __folio_split() where the zone_device check was made.

Signed-off-by: Gregory Price <gourry@gourry.net>
---
 include/linux/node_private.h | 33 +++++++++++++++++++++++++++++++++
 mm/huge_memory.c             |  6 ++++--
 2 files changed, 37 insertions(+), 2 deletions(-)

diff --git a/include/linux/node_private.h b/include/linux/node_private.h
index 09ea7c4cb13c..f9dd2d25c8a5 100644
--- a/include/linux/node_private.h
+++ b/include/linux/node_private.h
@@ -3,6 +3,7 @@
 #define _LINUX_NODE_PRIVATE_H
 
 #include <linux/completion.h>
+#include <linux/memremap.h>
 #include <linux/mm.h>
 #include <linux/nodemask.h>
 #include <linux/rcupdate.h>
@@ -44,11 +45,19 @@ struct vm_fault;
  *   Returns: true if handled (skip return to buddy)
  *            false if no op (return to buddy)
  *
+ * @folio_split: Notification that a folio on this private node is being split.
+ *    [folio-referenced callback]
+ *     Called from the folio split path via folio_managed_split_cb().
+ *     @folio is the original folio; @new_folio is the newly created folio,
+ *     or NULL when called for the final (original) folio after all sub-folios
+ *     have been split off.
+ *
  * @flags: Operation exclusion flags (NP_OPS_* constants).
  *
  */
 struct node_private_ops {
 	bool (*free_folio)(struct folio *folio);
+	void (*folio_split)(struct folio *folio, struct folio *new_folio);
 	unsigned long flags;
 };
 
@@ -150,6 +159,24 @@ static inline bool zone_private_flags(struct zone *z, unsigned long flag)
 	return node_private_flags(zone_to_nid(z)) & flag;
 }
 
+static inline void node_private_split_cb(struct folio *folio,
+					 struct folio *new_folio)
+{
+	const struct node_private_ops *ops = folio_node_private_ops(folio);
+
+	if (ops && ops->folio_split)
+		ops->folio_split(folio, new_folio);
+}
+
+static inline void folio_managed_split_cb(struct folio *original_folio,
+					  struct folio *new_folio)
+{
+	if (folio_is_zone_device(original_folio))
+		zone_device_private_split_cb(original_folio, new_folio);
+	else if (folio_is_private_node(original_folio))
+		node_private_split_cb(original_folio, new_folio);
+}
+
 #else /* !CONFIG_NUMA */
 
 static inline bool folio_is_private_node(struct folio *folio)
@@ -198,6 +225,12 @@ static inline bool zone_private_flags(struct zone *z, unsigned long flag)
 	return false;
 }
 
+static inline void folio_managed_split_cb(struct folio *original_folio,
+					  struct folio *new_folio)
+{
+	if (folio_is_zone_device(original_folio))
+		zone_device_private_split_cb(original_folio, new_folio);
+}
 #endif /* CONFIG_NUMA */
 
 #if defined(CONFIG_NUMA) && defined(CONFIG_MEMORY_HOTPLUG)
diff --git a/mm/huge_memory.c b/mm/huge_memory.c
index 40cf59301c21..2ecae494291a 100644
--- a/mm/huge_memory.c
+++ b/mm/huge_memory.c
@@ -24,6 +24,7 @@
 #include <linux/freezer.h>
 #include <linux/mman.h>
 #include <linux/memremap.h>
+#include <linux/node_private.h>
 #include <linux/pagemap.h>
 #include <linux/debugfs.h>
 #include <linux/migrate.h>
@@ -3850,7 +3851,7 @@ static int __folio_freeze_and_split_unmapped(struct folio *folio, unsigned int n
 
 			next = folio_next(new_folio);
 
-			zone_device_private_split_cb(folio, new_folio);
+			folio_managed_split_cb(folio, new_folio);
 
 			folio_ref_unfreeze(new_folio,
 					   folio_cache_ref_count(new_folio) + 1);
@@ -3889,7 +3890,8 @@ static int __folio_freeze_and_split_unmapped(struct folio *folio, unsigned int n
 			folio_put_refs(new_folio, nr_pages);
 		}
 
-		zone_device_private_split_cb(folio, NULL);
+		folio_managed_split_cb(folio, NULL);
+
 		/*
 		 * Unfreeze @folio only after all page cache entries, which
 		 * used to point to it, have been updated with new folios.
-- 
2.53.0


^ permalink raw reply related

* [RFC PATCH v4 10/27] mm/swap: add free_folio callback for folio release cleanup
From: Gregory Price @ 2026-02-22  8:48 UTC (permalink / raw)
  To: lsf-pc
  Cc: linux-kernel, linux-cxl, cgroups, linux-mm, linux-trace-kernel,
	damon, kernel-team, gregkh, rafael, dakr, dave, jonathan.cameron,
	dave.jiang, alison.schofield, vishal.l.verma, ira.weiny,
	dan.j.williams, longman, akpm, david, lorenzo.stoakes,
	Liam.Howlett, vbabka, rppt, surenb, mhocko, osalvador, ziy,
	matthew.brost, joshua.hahnjy, rakie.kim, byungchul, gourry,
	ying.huang, apopple, axelrasmussen, yuanchu, weixugc, yury.norov,
	linux, mhiramat, mathieu.desnoyers, tj, hannes, mkoutny, jackmanb,
	sj, baolin.wang, npache, ryan.roberts, dev.jain, baohua,
	lance.yang, muchun.song, xu.xin16, chengming.zhou, jannh,
	linmiaohe, nao.horiguchi, pfalcato, rientjes, shakeel.butt, riel,
	harry.yoo, cl, roman.gushchin, chrisl, kasong, shikemeng, nphamcs,
	bhe, zhengqi.arch, terry.bowman
In-Reply-To: <20260222084842.1824063-1-gourry@gourry.net>

When a folio's refcount drops to zero, the service may need to perform
cleanup before the page returns to the buddy allocator (e.g. zeroing
pages to scrub stale compressed data / release compression ratio).

Add folio_managed_on_free() to wrap both zone_device and private node
semantics for this operation since they are the same.

One difference between zone_device and private node folios:
  - private nodes may choose to either take a reference and return true
    ("handled"), or return false to return it back to the buddy.

  - zone_device returns the page to the buddy (always returns true)

Signed-off-by: Gregory Price <gourry@gourry.net>
---
 include/linux/node_private.h |  6 ++++++
 mm/internal.h                | 30 ++++++++++++++++++++++++++++++
 mm/swap.c                    | 21 ++++++++++-----------
 3 files changed, 46 insertions(+), 11 deletions(-)

diff --git a/include/linux/node_private.h b/include/linux/node_private.h
index 7687a4cf990c..09ea7c4cb13c 100644
--- a/include/linux/node_private.h
+++ b/include/linux/node_private.h
@@ -39,10 +39,16 @@ struct vm_fault;
  *   callback to prevent node_private from being freed.
  *   These callbacks MUST NOT sleep.
  *
+ * @free_folio: Called when a folio refcount drops to 0
+ *   [folio-referenced callback]
+ *   Returns: true if handled (skip return to buddy)
+ *            false if no op (return to buddy)
+ *
  * @flags: Operation exclusion flags (NP_OPS_* constants).
  *
  */
 struct node_private_ops {
+	bool (*free_folio)(struct folio *folio);
 	unsigned long flags;
 };
 
diff --git a/mm/internal.h b/mm/internal.h
index 97023748e6a9..658da41cdb8e 100644
--- a/mm/internal.h
+++ b/mm/internal.h
@@ -1412,6 +1412,36 @@ int numa_migrate_check(struct folio *folio, struct vm_fault *vmf,
 void free_zone_device_folio(struct folio *folio);
 int migrate_device_coherent_folio(struct folio *folio);
 
+/**
+ * folio_managed_on_free - Notify managed-memory service that folio
+ *                         refcount reached zero.
+ * @folio: the folio being freed
+ *
+ * Returns true if the folio is fully handled (zone_device -- caller
+ * must return immediately).  Returns false if the callback ran but
+ * the folio should continue through the normal free path
+ * (private_node -- pages go back to buddy).
+ *
+ * Returns false for normal folios (no-op).
+ */
+static inline bool folio_managed_on_free(struct folio *folio)
+{
+	if (folio_is_zone_device(folio)) {
+		free_zone_device_folio(folio);
+		return true;
+	}
+	if (folio_is_private_node(folio)) {
+		const struct node_private_ops *ops =
+			folio_node_private_ops(folio);
+
+		if (ops && ops->free_folio) {
+			if (ops->free_folio(folio))
+				return true;
+		}
+	}
+	return false;
+}
+
 struct vm_struct *__get_vm_area_node(unsigned long size,
 				     unsigned long align, unsigned long shift,
 				     unsigned long vm_flags, unsigned long start,
diff --git a/mm/swap.c b/mm/swap.c
index 2260dcd2775e..dca306e1ae6d 100644
--- a/mm/swap.c
+++ b/mm/swap.c
@@ -37,6 +37,7 @@
 #include <linux/page_idle.h>
 #include <linux/local_lock.h>
 #include <linux/buffer_head.h>
+#include <linux/node_private.h>
 
 #include "internal.h"
 
@@ -96,10 +97,9 @@ static void page_cache_release(struct folio *folio)
 
 void __folio_put(struct folio *folio)
 {
-	if (unlikely(folio_is_zone_device(folio))) {
-		free_zone_device_folio(folio);
-		return;
-	}
+	if (unlikely(folio_is_private_managed(folio)))
+		if (folio_managed_on_free(folio))
+			return;
 
 	if (folio_test_hugetlb(folio)) {
 		free_huge_folio(folio);
@@ -961,19 +961,18 @@ void folios_put_refs(struct folio_batch *folios, unsigned int *refs)
 		if (is_huge_zero_folio(folio))
 			continue;
 
-		if (folio_is_zone_device(folio)) {
+		if (!folio_ref_sub_and_test(folio, nr_refs))
+			continue;
+
+		if (unlikely(folio_is_private_managed(folio))) {
 			if (lruvec) {
 				unlock_page_lruvec_irqrestore(lruvec, flags);
 				lruvec = NULL;
 			}
-			if (folio_ref_sub_and_test(folio, nr_refs))
-				free_zone_device_folio(folio);
-			continue;
+			if (folio_managed_on_free(folio))
+				continue;
 		}
 
-		if (!folio_ref_sub_and_test(folio, nr_refs))
-			continue;
-
 		/* hugetlb has its own memcg */
 		if (folio_test_hugetlb(folio)) {
 			if (lruvec) {
-- 
2.53.0


^ permalink raw reply related

* [RFC PATCH v4 09/27] mm/khugepaged: skip private node folios when trying to collapse.
From: Gregory Price @ 2026-02-22  8:48 UTC (permalink / raw)
  To: lsf-pc
  Cc: linux-kernel, linux-cxl, cgroups, linux-mm, linux-trace-kernel,
	damon, kernel-team, gregkh, rafael, dakr, dave, jonathan.cameron,
	dave.jiang, alison.schofield, vishal.l.verma, ira.weiny,
	dan.j.williams, longman, akpm, david, lorenzo.stoakes,
	Liam.Howlett, vbabka, rppt, surenb, mhocko, osalvador, ziy,
	matthew.brost, joshua.hahnjy, rakie.kim, byungchul, gourry,
	ying.huang, apopple, axelrasmussen, yuanchu, weixugc, yury.norov,
	linux, mhiramat, mathieu.desnoyers, tj, hannes, mkoutny, jackmanb,
	sj, baolin.wang, npache, ryan.roberts, dev.jain, baohua,
	lance.yang, muchun.song, xu.xin16, chengming.zhou, jannh,
	linmiaohe, nao.horiguchi, pfalcato, rientjes, shakeel.butt, riel,
	harry.yoo, cl, roman.gushchin, chrisl, kasong, shikemeng, nphamcs,
	bhe, zhengqi.arch, terry.bowman
In-Reply-To: <20260222084842.1824063-1-gourry@gourry.net>

A collapse operation allocates a new large folio and migrates the
smaller folios into it.  This is an issue for private nodes:

  1. The private node service may not support migration
  2. Collapse may promotes pages from the private node to a local node,
     which may result in an LRU inversion that defeats memory tiering.

Handle this just like zone_device for now.

It may be possible to support this later for some private node services
that report explicit support for collapse (and migration).

Signed-off-by: Gregory Price <gourry@gourry.net>
---
 mm/khugepaged.c | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

diff --git a/mm/khugepaged.c b/mm/khugepaged.c
index 97d1b2824386..36f6bc5da53c 100644
--- a/mm/khugepaged.c
+++ b/mm/khugepaged.c
@@ -21,6 +21,7 @@
 #include <linux/shmem_fs.h>
 #include <linux/dax.h>
 #include <linux/ksm.h>
+#include <linux/node_private.h>
 #include <linux/pgalloc.h>
 
 #include <asm/tlb.h>
@@ -571,7 +572,7 @@ static int __collapse_huge_page_isolate(struct vm_area_struct *vma,
 			goto out;
 		}
 		page = vm_normal_page(vma, addr, pteval);
-		if (unlikely(!page) || unlikely(is_zone_device_page(page))) {
+		if (unlikely(!page) || unlikely(page_is_private_managed(page))) {
 			result = SCAN_PAGE_NULL;
 			goto out;
 		}
@@ -1323,7 +1324,7 @@ static int hpage_collapse_scan_pmd(struct mm_struct *mm,
 		}
 
 		page = vm_normal_page(vma, addr, pteval);
-		if (unlikely(!page) || unlikely(is_zone_device_page(page))) {
+		if (unlikely(!page) || unlikely(page_is_private_managed(page))) {
 			result = SCAN_PAGE_NULL;
 			goto out_unmap;
 		}
@@ -1575,7 +1576,7 @@ int collapse_pte_mapped_thp(struct mm_struct *mm, unsigned long addr,
 		}
 
 		page = vm_normal_page(vma, addr, ptent);
-		if (WARN_ON_ONCE(page && is_zone_device_page(page)))
+		if (WARN_ON_ONCE(page && page_is_private_managed(page)))
 			page = NULL;
 		/*
 		 * Note that uprobe, debugger, or MAP_PRIVATE may change the
-- 
2.53.0


^ permalink raw reply related

* [RFC PATCH v4 08/27] mm/ksm: skip KSM for managed-memory folios
From: Gregory Price @ 2026-02-22  8:48 UTC (permalink / raw)
  To: lsf-pc
  Cc: linux-kernel, linux-cxl, cgroups, linux-mm, linux-trace-kernel,
	damon, kernel-team, gregkh, rafael, dakr, dave, jonathan.cameron,
	dave.jiang, alison.schofield, vishal.l.verma, ira.weiny,
	dan.j.williams, longman, akpm, david, lorenzo.stoakes,
	Liam.Howlett, vbabka, rppt, surenb, mhocko, osalvador, ziy,
	matthew.brost, joshua.hahnjy, rakie.kim, byungchul, gourry,
	ying.huang, apopple, axelrasmussen, yuanchu, weixugc, yury.norov,
	linux, mhiramat, mathieu.desnoyers, tj, hannes, mkoutny, jackmanb,
	sj, baolin.wang, npache, ryan.roberts, dev.jain, baohua,
	lance.yang, muchun.song, xu.xin16, chengming.zhou, jannh,
	linmiaohe, nao.horiguchi, pfalcato, rientjes, shakeel.butt, riel,
	harry.yoo, cl, roman.gushchin, chrisl, kasong, shikemeng, nphamcs,
	bhe, zhengqi.arch, terry.bowman
In-Reply-To: <20260222084842.1824063-1-gourry@gourry.net>

Private node folios should not participate in KSM merging by default.
The driver manages the memory lifecycle and KSM's page sharing can
interfere with driver operations.

Extend the existing zone_device checks in get_mergeable_page and
ksm_next_page_pmd_entry to cover private node folios as well.

Signed-off-by: Gregory Price <gourry@gourry.net>
---
 mm/ksm.c | 9 ++++++---
 1 file changed, 6 insertions(+), 3 deletions(-)

diff --git a/mm/ksm.c b/mm/ksm.c
index 2d89a7c8b4eb..c48e95a6fff9 100644
--- a/mm/ksm.c
+++ b/mm/ksm.c
@@ -40,6 +40,7 @@
 #include <linux/oom.h>
 #include <linux/numa.h>
 #include <linux/pagewalk.h>
+#include <linux/node_private.h>
 
 #include <asm/tlbflush.h>
 #include "internal.h"
@@ -808,7 +809,7 @@ static struct page *get_mergeable_page(struct ksm_rmap_item *rmap_item)
 
 	folio = folio_walk_start(&fw, vma, addr, 0);
 	if (folio) {
-		if (!folio_is_zone_device(folio) &&
+		if (!folio_is_private_managed(folio) &&
 		    folio_test_anon(folio)) {
 			folio_get(folio);
 			page = fw.page;
@@ -2521,7 +2522,8 @@ static int ksm_next_page_pmd_entry(pmd_t *pmdp, unsigned long addr, unsigned lon
 				goto not_found_unlock;
 			folio = page_folio(page);
 
-			if (folio_is_zone_device(folio) || !folio_test_anon(folio))
+			if (unlikely(folio_is_private_managed(folio)) ||
+			    !folio_test_anon(folio))
 				goto not_found_unlock;
 
 			page += ((addr & (PMD_SIZE - 1)) >> PAGE_SHIFT);
@@ -2545,7 +2547,8 @@ static int ksm_next_page_pmd_entry(pmd_t *pmdp, unsigned long addr, unsigned lon
 			continue;
 		folio = page_folio(page);
 
-		if (folio_is_zone_device(folio) || !folio_test_anon(folio))
+		if (unlikely(folio_is_private_managed(folio)) ||
+		    !folio_test_anon(folio))
 			continue;
 		goto found_unlock;
 	}
-- 
2.53.0


^ permalink raw reply related

* [RFC PATCH v4 07/27] mm/madvise: skip madvise for managed-memory folios
From: Gregory Price @ 2026-02-22  8:48 UTC (permalink / raw)
  To: lsf-pc
  Cc: linux-kernel, linux-cxl, cgroups, linux-mm, linux-trace-kernel,
	damon, kernel-team, gregkh, rafael, dakr, dave, jonathan.cameron,
	dave.jiang, alison.schofield, vishal.l.verma, ira.weiny,
	dan.j.williams, longman, akpm, david, lorenzo.stoakes,
	Liam.Howlett, vbabka, rppt, surenb, mhocko, osalvador, ziy,
	matthew.brost, joshua.hahnjy, rakie.kim, byungchul, gourry,
	ying.huang, apopple, axelrasmussen, yuanchu, weixugc, yury.norov,
	linux, mhiramat, mathieu.desnoyers, tj, hannes, mkoutny, jackmanb,
	sj, baolin.wang, npache, ryan.roberts, dev.jain, baohua,
	lance.yang, muchun.song, xu.xin16, chengming.zhou, jannh,
	linmiaohe, nao.horiguchi, pfalcato, rientjes, shakeel.butt, riel,
	harry.yoo, cl, roman.gushchin, chrisl, kasong, shikemeng, nphamcs,
	bhe, zhengqi.arch, terry.bowman
In-Reply-To: <20260222084842.1824063-1-gourry@gourry.net>

Private node folios are managed by device drivers and should not be
subjectto madvise cold/pageout/free operations that would interfere
with the driver's memory management.

Extend the existing zone_device check to cover private nodes.

Signed-off-by: Gregory Price <gourry@gourry.net>
---
 mm/madvise.c | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/mm/madvise.c b/mm/madvise.c
index b617b1be0f53..3aac105e840b 100644
--- a/mm/madvise.c
+++ b/mm/madvise.c
@@ -32,6 +32,7 @@
 #include <linux/leafops.h>
 #include <linux/shmem_fs.h>
 #include <linux/mmu_notifier.h>
+#include <linux/node_private.h>
 
 #include <asm/tlb.h>
 
@@ -475,7 +476,7 @@ static int madvise_cold_or_pageout_pte_range(pmd_t *pmd,
 			continue;
 
 		folio = vm_normal_folio(vma, addr, ptent);
-		if (!folio || folio_is_zone_device(folio))
+		if (!folio || unlikely(folio_is_private_managed(folio)))
 			continue;
 
 		/*
@@ -704,7 +705,7 @@ static int madvise_free_pte_range(pmd_t *pmd, unsigned long addr,
 		}
 
 		folio = vm_normal_folio(vma, addr, ptent);
-		if (!folio || folio_is_zone_device(folio))
+		if (!folio || unlikely(folio_is_private_managed(folio)))
 			continue;
 
 		/*
-- 
2.53.0


^ permalink raw reply related

* [RFC PATCH v4 06/27] mm/mlock: skip mlock for managed-memory folios
From: Gregory Price @ 2026-02-22  8:48 UTC (permalink / raw)
  To: lsf-pc
  Cc: linux-kernel, linux-cxl, cgroups, linux-mm, linux-trace-kernel,
	damon, kernel-team, gregkh, rafael, dakr, dave, jonathan.cameron,
	dave.jiang, alison.schofield, vishal.l.verma, ira.weiny,
	dan.j.williams, longman, akpm, david, lorenzo.stoakes,
	Liam.Howlett, vbabka, rppt, surenb, mhocko, osalvador, ziy,
	matthew.brost, joshua.hahnjy, rakie.kim, byungchul, gourry,
	ying.huang, apopple, axelrasmussen, yuanchu, weixugc, yury.norov,
	linux, mhiramat, mathieu.desnoyers, tj, hannes, mkoutny, jackmanb,
	sj, baolin.wang, npache, ryan.roberts, dev.jain, baohua,
	lance.yang, muchun.song, xu.xin16, chengming.zhou, jannh,
	linmiaohe, nao.horiguchi, pfalcato, rientjes, shakeel.butt, riel,
	harry.yoo, cl, roman.gushchin, chrisl, kasong, shikemeng, nphamcs,
	bhe, zhengqi.arch, terry.bowman
In-Reply-To: <20260222084842.1824063-1-gourry@gourry.net>

Private node folios are managed by device drivers and should not be
mlocked.  The existing folio_is_zone_device check is already correctly
placed to handle this - simply extend it for private nodes.

Signed-off-by: Gregory Price <gourry@gourry.net>
---
 mm/mlock.c | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/mm/mlock.c b/mm/mlock.c
index 2f699c3497a5..c56159253e45 100644
--- a/mm/mlock.c
+++ b/mm/mlock.c
@@ -25,6 +25,7 @@
 #include <linux/memcontrol.h>
 #include <linux/mm_inline.h>
 #include <linux/secretmem.h>
+#include <linux/node_private.h>
 
 #include "internal.h"
 
@@ -366,7 +367,7 @@ static int mlock_pte_range(pmd_t *pmd, unsigned long addr,
 		if (is_huge_zero_pmd(*pmd))
 			goto out;
 		folio = pmd_folio(*pmd);
-		if (folio_is_zone_device(folio))
+		if (unlikely(folio_is_private_managed(folio)))
 			goto out;
 		if (vma->vm_flags & VM_LOCKED)
 			mlock_folio(folio);
@@ -386,7 +387,7 @@ static int mlock_pte_range(pmd_t *pmd, unsigned long addr,
 		if (!pte_present(ptent))
 			continue;
 		folio = vm_normal_folio(vma, addr, ptent);
-		if (!folio || folio_is_zone_device(folio))
+		if (!folio || unlikely(folio_is_private_managed(folio)))
 			continue;
 
 		step = folio_mlock_step(folio, pte, addr, end);
-- 
2.53.0


^ permalink raw reply related

* [RFC PATCH v4 05/27] mm: introduce folio_is_private_managed() unified predicate
From: Gregory Price @ 2026-02-22  8:48 UTC (permalink / raw)
  To: lsf-pc
  Cc: linux-kernel, linux-cxl, cgroups, linux-mm, linux-trace-kernel,
	damon, kernel-team, gregkh, rafael, dakr, dave, jonathan.cameron,
	dave.jiang, alison.schofield, vishal.l.verma, ira.weiny,
	dan.j.williams, longman, akpm, david, lorenzo.stoakes,
	Liam.Howlett, vbabka, rppt, surenb, mhocko, osalvador, ziy,
	matthew.brost, joshua.hahnjy, rakie.kim, byungchul, gourry,
	ying.huang, apopple, axelrasmussen, yuanchu, weixugc, yury.norov,
	linux, mhiramat, mathieu.desnoyers, tj, hannes, mkoutny, jackmanb,
	sj, baolin.wang, npache, ryan.roberts, dev.jain, baohua,
	lance.yang, muchun.song, xu.xin16, chengming.zhou, jannh,
	linmiaohe, nao.horiguchi, pfalcato, rientjes, shakeel.butt, riel,
	harry.yoo, cl, roman.gushchin, chrisl, kasong, shikemeng, nphamcs,
	bhe, zhengqi.arch, terry.bowman
In-Reply-To: <20260222084842.1824063-1-gourry@gourry.net>

Multiple mm/ subsystems already skip operations for ZONE_DEVICE folios,
and N_MEMORY_PRIVATE folios share the checkpoints for ZONE_DEVICE pages.

Add folio_is_private_managed() as a unified predicate that returns true
for folios on N_MEMORY_PRIVATE nodes or in ZONE_DEVICE.

This predicate replaces folio_is_zone_device at skip sites where both
folio types should be excluded from an MM operation.

At some locations, explicit zone_device vs private_node checks are more
appropriate when the operations between the two fundamentally differ.

The !CONFIG_NUMA stubs fall through to folio_is_zone_device() only,
preserving existing behavior when NUMA is disabled.

Signed-off-by: Gregory Price <gourry@gourry.net>
---
 include/linux/node_private.h | 20 ++++++++++++++++++++
 1 file changed, 20 insertions(+)

diff --git a/include/linux/node_private.h b/include/linux/node_private.h
index 6a70ec39d569..7687a4cf990c 100644
--- a/include/linux/node_private.h
+++ b/include/linux/node_private.h
@@ -92,6 +92,16 @@ static inline bool page_is_private_node(struct page *page)
 	return node_state(page_to_nid(page), N_MEMORY_PRIVATE);
 }
 
+static inline bool folio_is_private_managed(struct folio *folio)
+{
+	return folio_is_zone_device(folio) || folio_is_private_node(folio);
+}
+
+static inline bool page_is_private_managed(struct page *page)
+{
+	return folio_is_private_managed(page_folio(page));
+}
+
 static inline const struct node_private_ops *
 folio_node_private_ops(struct folio *folio)
 {
@@ -146,6 +156,16 @@ static inline bool page_is_private_node(struct page *page)
 	return false;
 }
 
+static inline bool folio_is_private_managed(struct folio *folio)
+{
+	return folio_is_zone_device(folio);
+}
+
+static inline bool page_is_private_managed(struct page *page)
+{
+	return folio_is_private_managed(page_folio(page));
+}
+
 static inline const struct node_private_ops *
 folio_node_private_ops(struct folio *folio)
 {
-- 
2.53.0


^ permalink raw reply related

* [RFC PATCH v4 04/27] mm/page_alloc: Add private node handling to build_zonelists
From: Gregory Price @ 2026-02-22  8:48 UTC (permalink / raw)
  To: lsf-pc
  Cc: linux-kernel, linux-cxl, cgroups, linux-mm, linux-trace-kernel,
	damon, kernel-team, gregkh, rafael, dakr, dave, jonathan.cameron,
	dave.jiang, alison.schofield, vishal.l.verma, ira.weiny,
	dan.j.williams, longman, akpm, david, lorenzo.stoakes,
	Liam.Howlett, vbabka, rppt, surenb, mhocko, osalvador, ziy,
	matthew.brost, joshua.hahnjy, rakie.kim, byungchul, gourry,
	ying.huang, apopple, axelrasmussen, yuanchu, weixugc, yury.norov,
	linux, mhiramat, mathieu.desnoyers, tj, hannes, mkoutny, jackmanb,
	sj, baolin.wang, npache, ryan.roberts, dev.jain, baohua,
	lance.yang, muchun.song, xu.xin16, chengming.zhou, jannh,
	linmiaohe, nao.horiguchi, pfalcato, rientjes, shakeel.butt, riel,
	harry.yoo, cl, roman.gushchin, chrisl, kasong, shikemeng, nphamcs,
	bhe, zhengqi.arch, terry.bowman
In-Reply-To: <20260222084842.1824063-1-gourry@gourry.net>

N_MEMORY fallback lists should not include N_MEMORY_PRIVATE nodes, at
worst this would allow allocation from them in some scenarios, and at
best it causes iterations over nodes that aren't eligible.

Private node primary fallback lists do include N_MEMORY nodes so
kernel/slab allocations made on behalf of the private node can
fall back to DRAM when __GFP_PRIVATE is not set.

The nofallback list contains only the node's own zones, restricting
__GFP_THISNODE allocations to the private node.

Signed-off-by: Gregory Price <gourry@gourry.net>
---
 mm/page_alloc.c | 20 ++++++++++++++++++++
 1 file changed, 20 insertions(+)

diff --git a/mm/page_alloc.c b/mm/page_alloc.c
index 47f2619d3840..5a1b35421d78 100644
--- a/mm/page_alloc.c
+++ b/mm/page_alloc.c
@@ -5683,6 +5683,26 @@ static void build_zonelists(pg_data_t *pgdat)
 	local_node = pgdat->node_id;
 	prev_node = local_node;
 
+	/*
+	 * Private nodes need N_MEMORY nodes as fallback for kernel allocations
+	 * (e.g., slab objects allocated on behalf of this node).
+	 */
+	if (node_state(local_node, N_MEMORY_PRIVATE)) {
+		node_order[nr_nodes++] = local_node;
+		node_set(local_node, used_mask);
+
+		while ((node = find_next_best_node(local_node, &used_mask)) >= 0)
+			node_order[nr_nodes++] = node;
+
+		build_zonelists_in_node_order(pgdat, node_order, nr_nodes);
+		build_thisnode_zonelists(pgdat);
+		pr_info("Fallback order for Node %d (private):", local_node);
+		for (node = 0; node < nr_nodes; node++)
+			pr_cont(" %d", node_order[node]);
+		pr_cont("\n");
+		return;
+	}
+
 	memset(node_order, 0, sizeof(node_order));
 	while ((node = find_next_best_node(local_node, &used_mask)) >= 0) {
 		/*
-- 
2.53.0


^ permalink raw reply related

* [RFC PATCH v4 03/27] mm/page_alloc: add numa_zone_allowed() and wire it up
From: Gregory Price @ 2026-02-22  8:48 UTC (permalink / raw)
  To: lsf-pc
  Cc: linux-kernel, linux-cxl, cgroups, linux-mm, linux-trace-kernel,
	damon, kernel-team, gregkh, rafael, dakr, dave, jonathan.cameron,
	dave.jiang, alison.schofield, vishal.l.verma, ira.weiny,
	dan.j.williams, longman, akpm, david, lorenzo.stoakes,
	Liam.Howlett, vbabka, rppt, surenb, mhocko, osalvador, ziy,
	matthew.brost, joshua.hahnjy, rakie.kim, byungchul, gourry,
	ying.huang, apopple, axelrasmussen, yuanchu, weixugc, yury.norov,
	linux, mhiramat, mathieu.desnoyers, tj, hannes, mkoutny, jackmanb,
	sj, baolin.wang, npache, ryan.roberts, dev.jain, baohua,
	lance.yang, muchun.song, xu.xin16, chengming.zhou, jannh,
	linmiaohe, nao.horiguchi, pfalcato, rientjes, shakeel.butt, riel,
	harry.yoo, cl, roman.gushchin, chrisl, kasong, shikemeng, nphamcs,
	bhe, zhengqi.arch, terry.bowman
In-Reply-To: <20260222084842.1824063-1-gourry@gourry.net>

Various locations in mm/ open-code cpuset filtering with:

  cpusets_enabled() && ALLOC_CPUSET && !__cpuset_zone_allowed()

This pattern does not account for N_MEMORY_PRIVATE nodes on systems
without cpusets, so private-node zones can leak into allocation
paths that should only see general-purpose memory.

Add numa_zone_allowed() which consolidates zone filtering. It checks
cpuset membership when cpusets are enabled, and otherwise gates
N_MEMORY_PRIVATE zones behind __GFP_PRIVATE globally.

Replace the open-coded patterns in mm/ with the new helper.

Signed-off-by: Gregory Price <gourry@gourry.net>
---
 mm/compaction.c |  6 ++----
 mm/hugetlb.c    |  2 +-
 mm/internal.h   |  7 +++++++
 mm/page_alloc.c | 31 ++++++++++++++++++++-----------
 mm/slub.c       |  3 ++-
 5 files changed, 32 insertions(+), 17 deletions(-)

diff --git a/mm/compaction.c b/mm/compaction.c
index 1e8f8eca318c..6a65145b03d8 100644
--- a/mm/compaction.c
+++ b/mm/compaction.c
@@ -2829,10 +2829,8 @@ enum compact_result try_to_compact_pages(gfp_t gfp_mask, unsigned int order,
 					ac->highest_zoneidx, ac->nodemask) {
 		enum compact_result status;
 
-		if (cpusets_enabled() &&
-			(alloc_flags & ALLOC_CPUSET) &&
-			!__cpuset_zone_allowed(zone, gfp_mask))
-				continue;
+		if (!numa_zone_alloc_allowed(alloc_flags, zone, gfp_mask))
+			continue;
 
 		if (prio > MIN_COMPACT_PRIORITY
 					&& compaction_deferred(zone, order)) {
diff --git a/mm/hugetlb.c b/mm/hugetlb.c
index 51273baec9e5..f2b914ab5910 100644
--- a/mm/hugetlb.c
+++ b/mm/hugetlb.c
@@ -1353,7 +1353,7 @@ static struct folio *dequeue_hugetlb_folio_nodemask(struct hstate *h, gfp_t gfp_
 	for_each_zone_zonelist_nodemask(zone, z, zonelist, gfp_zone(gfp_mask), nmask) {
 		struct folio *folio;
 
-		if (!cpuset_zone_allowed(zone, gfp_mask))
+		if (!numa_zone_alloc_allowed(ALLOC_CPUSET, zone, gfp_mask))
 			continue;
 		/*
 		 * no need to ask again on the same node. Pool is node rather than
diff --git a/mm/internal.h b/mm/internal.h
index 23ee14790227..97023748e6a9 100644
--- a/mm/internal.h
+++ b/mm/internal.h
@@ -1206,6 +1206,8 @@ extern int node_reclaim_mode;
 
 extern int node_reclaim(struct pglist_data *, gfp_t, unsigned int);
 extern int find_next_best_node(int node, nodemask_t *used_node_mask);
+extern bool numa_zone_alloc_allowed(int alloc_flags, struct zone *zone,
+			      gfp_t gfp_mask);
 #else
 #define node_reclaim_mode 0
 
@@ -1218,6 +1220,11 @@ static inline int find_next_best_node(int node, nodemask_t *used_node_mask)
 {
 	return NUMA_NO_NODE;
 }
+static inline bool numa_zone_alloc_allowed(int alloc_flags, struct zone *zone,
+				     gfp_t gfp_mask)
+{
+	return true;
+}
 #endif
 
 static inline bool node_reclaim_enabled(void)
diff --git a/mm/page_alloc.c b/mm/page_alloc.c
index 2facee0805da..47f2619d3840 100644
--- a/mm/page_alloc.c
+++ b/mm/page_alloc.c
@@ -3690,6 +3690,21 @@ static bool zone_allows_reclaim(struct zone *local_zone, struct zone *zone)
 	return node_distance(zone_to_nid(local_zone), zone_to_nid(zone)) <=
 				node_reclaim_distance;
 }
+
+/* Returns true if allocation from this zone is permitted */
+bool numa_zone_alloc_allowed(int alloc_flags, struct zone *zone, gfp_t gfp_mask)
+{
+	/* Gate N_MEMORY_PRIVATE zones behind __GFP_PRIVATE */
+	if (!(gfp_mask & __GFP_PRIVATE) &&
+	    node_state(zone_to_nid(zone), N_MEMORY_PRIVATE))
+		return false;
+
+	/* If cpusets is being used, check mems_allowed */
+	if (cpusets_enabled() && (alloc_flags & ALLOC_CPUSET))
+		return cpuset_zone_allowed(zone, gfp_mask);
+
+	return true;
+}
 #else	/* CONFIG_NUMA */
 static bool zone_allows_reclaim(struct zone *local_zone, struct zone *zone)
 {
@@ -3781,10 +3796,8 @@ get_page_from_freelist(gfp_t gfp_mask, unsigned int order, int alloc_flags,
 		struct page *page;
 		unsigned long mark;
 
-		if (cpusets_enabled() &&
-			(alloc_flags & ALLOC_CPUSET) &&
-			!__cpuset_zone_allowed(zone, gfp_mask))
-				continue;
+		if (!numa_zone_alloc_allowed(alloc_flags, zone, gfp_mask))
+			continue;
 		/*
 		 * When allocating a page cache page for writing, we
 		 * want to get it from a node that is within its dirty
@@ -4585,10 +4598,8 @@ should_reclaim_retry(gfp_t gfp_mask, unsigned order,
 		unsigned long min_wmark = min_wmark_pages(zone);
 		bool wmark;
 
-		if (cpusets_enabled() &&
-			(alloc_flags & ALLOC_CPUSET) &&
-			!__cpuset_zone_allowed(zone, gfp_mask))
-				continue;
+		if (!numa_zone_alloc_allowed(alloc_flags, zone, gfp_mask))
+			continue;
 
 		available = reclaimable = zone_reclaimable_pages(zone);
 		available += zone_page_state_snapshot(zone, NR_FREE_PAGES);
@@ -5084,10 +5095,8 @@ unsigned long alloc_pages_bulk_noprof(gfp_t gfp, int preferred_nid,
 	for_next_zone_zonelist_nodemask(zone, z, ac.highest_zoneidx, ac.nodemask) {
 		unsigned long mark;
 
-		if (cpusets_enabled() && (alloc_flags & ALLOC_CPUSET) &&
-		    !__cpuset_zone_allowed(zone, gfp)) {
+		if (!numa_zone_alloc_allowed(alloc_flags, zone, gfp))
 			continue;
-		}
 
 		if (nr_online_nodes > 1 && zone != zonelist_zone(ac.preferred_zoneref) &&
 		    zone_to_nid(zone) != zonelist_node_idx(ac.preferred_zoneref)) {
diff --git a/mm/slub.c b/mm/slub.c
index 861592ac5425..e4bd6ede81d1 100644
--- a/mm/slub.c
+++ b/mm/slub.c
@@ -3595,7 +3595,8 @@ static struct slab *get_any_partial(struct kmem_cache *s,
 
 			n = get_node(s, zone_to_nid(zone));
 
-			if (n && cpuset_zone_allowed(zone, pc->flags) &&
+			if (n && numa_zone_alloc_allowed(ALLOC_CPUSET, zone,
+						   pc->flags) &&
 					n->nr_partial > s->min_partial) {
 				slab = get_partial_node(s, n, pc);
 				if (slab) {
-- 
2.53.0


^ permalink raw reply related

* [RFC PATCH v4 02/27] mm,cpuset: gate allocations from N_MEMORY_PRIVATE behind __GFP_PRIVATE
From: Gregory Price @ 2026-02-22  8:48 UTC (permalink / raw)
  To: lsf-pc
  Cc: linux-kernel, linux-cxl, cgroups, linux-mm, linux-trace-kernel,
	damon, kernel-team, gregkh, rafael, dakr, dave, jonathan.cameron,
	dave.jiang, alison.schofield, vishal.l.verma, ira.weiny,
	dan.j.williams, longman, akpm, david, lorenzo.stoakes,
	Liam.Howlett, vbabka, rppt, surenb, mhocko, osalvador, ziy,
	matthew.brost, joshua.hahnjy, rakie.kim, byungchul, gourry,
	ying.huang, apopple, axelrasmussen, yuanchu, weixugc, yury.norov,
	linux, mhiramat, mathieu.desnoyers, tj, hannes, mkoutny, jackmanb,
	sj, baolin.wang, npache, ryan.roberts, dev.jain, baohua,
	lance.yang, muchun.song, xu.xin16, chengming.zhou, jannh,
	linmiaohe, nao.horiguchi, pfalcato, rientjes, shakeel.butt, riel,
	harry.yoo, cl, roman.gushchin, chrisl, kasong, shikemeng, nphamcs,
	bhe, zhengqi.arch, terry.bowman
In-Reply-To: <20260222084842.1824063-1-gourry@gourry.net>

N_MEMORY_PRIVATE nodes hold device-managed memory that should not be
used for general allocations. Without a gating mechanism, any allocation
could land on a private node if it appears in the task's mems_allowed.

Introduce __GFP_PRIVATE that explicitly opts in to allocation from
N_MEMORY_PRIVATE nodes.

Add the GFP_PRIVATE compound mask (__GFP_PRIVATE | __GFP_THISNODE)
for callers that explicitly target private nodes to help prevent
fallback allocations from DRAM.

Update cpuset_current_node_allowed() to filter out N_MEMORY_PRIVATE
nodes unless __GFP_PRIVATE is set.

In interrupt context, only N_MEMORY nodes are valid.

Update cpuset_handle_hotplug() to include N_MEMORY_PRIVATE nodes in
the effective mems set, allowing cgroup-level control over private
node access.

Signed-off-by: Gregory Price <gourry@gourry.net>
---
 include/linux/gfp_types.h      | 15 +++++++++++++--
 include/trace/events/mmflags.h |  4 ++--
 kernel/cgroup/cpuset.c         | 32 ++++++++++++++++++++++++++++----
 3 files changed, 43 insertions(+), 8 deletions(-)

diff --git a/include/linux/gfp_types.h b/include/linux/gfp_types.h
index 3de43b12209e..ac375f9a0fc2 100644
--- a/include/linux/gfp_types.h
+++ b/include/linux/gfp_types.h
@@ -33,7 +33,7 @@ enum {
 	___GFP_IO_BIT,
 	___GFP_FS_BIT,
 	___GFP_ZERO_BIT,
-	___GFP_UNUSED_BIT,	/* 0x200u unused */
+	___GFP_PRIVATE_BIT,
 	___GFP_DIRECT_RECLAIM_BIT,
 	___GFP_KSWAPD_RECLAIM_BIT,
 	___GFP_WRITE_BIT,
@@ -69,7 +69,7 @@ enum {
 #define ___GFP_IO		BIT(___GFP_IO_BIT)
 #define ___GFP_FS		BIT(___GFP_FS_BIT)
 #define ___GFP_ZERO		BIT(___GFP_ZERO_BIT)
-/* 0x200u unused */
+#define ___GFP_PRIVATE		BIT(___GFP_PRIVATE_BIT)
 #define ___GFP_DIRECT_RECLAIM	BIT(___GFP_DIRECT_RECLAIM_BIT)
 #define ___GFP_KSWAPD_RECLAIM	BIT(___GFP_KSWAPD_RECLAIM_BIT)
 #define ___GFP_WRITE		BIT(___GFP_WRITE_BIT)
@@ -139,6 +139,11 @@ enum {
  * %__GFP_ACCOUNT causes the allocation to be accounted to kmemcg.
  *
  * %__GFP_NO_OBJ_EXT causes slab allocation to have no object extension.
+ *
+ * %__GFP_PRIVATE allows allocation from N_MEMORY_PRIVATE nodes (e.g., compressed
+ * memory, accelerator memory). Without this flag, allocations are restricted
+ * to N_MEMORY nodes only. Used by migration/demotion paths when explicitly
+ * targeting private nodes.
  */
 #define __GFP_RECLAIMABLE ((__force gfp_t)___GFP_RECLAIMABLE)
 #define __GFP_WRITE	((__force gfp_t)___GFP_WRITE)
@@ -146,6 +151,7 @@ enum {
 #define __GFP_THISNODE	((__force gfp_t)___GFP_THISNODE)
 #define __GFP_ACCOUNT	((__force gfp_t)___GFP_ACCOUNT)
 #define __GFP_NO_OBJ_EXT   ((__force gfp_t)___GFP_NO_OBJ_EXT)
+#define __GFP_PRIVATE	((__force gfp_t)___GFP_PRIVATE)
 
 /**
  * DOC: Watermark modifiers
@@ -367,6 +373,10 @@ enum {
  * available and will not wake kswapd/kcompactd on failure. The _LIGHT
  * version does not attempt reclaim/compaction at all and is by default used
  * in page fault path, while the non-light is used by khugepaged.
+ *
+ * %GFP_PRIVATE adds %__GFP_THISNODE by default to prevent any fallback
+ * allocations to other nodes, given that the caller was already attempting
+ * to access driver-managed memory explicitly.
  */
 #define GFP_ATOMIC	(__GFP_HIGH|__GFP_KSWAPD_RECLAIM)
 #define GFP_KERNEL	(__GFP_RECLAIM | __GFP_IO | __GFP_FS)
@@ -382,5 +392,6 @@ enum {
 #define GFP_TRANSHUGE_LIGHT	((GFP_HIGHUSER_MOVABLE | __GFP_COMP | \
 			 __GFP_NOMEMALLOC | __GFP_NOWARN) & ~__GFP_RECLAIM)
 #define GFP_TRANSHUGE	(GFP_TRANSHUGE_LIGHT | __GFP_DIRECT_RECLAIM)
+#define GFP_PRIVATE	(__GFP_PRIVATE | __GFP_THISNODE)
 
 #endif /* __LINUX_GFP_TYPES_H */
diff --git a/include/trace/events/mmflags.h b/include/trace/events/mmflags.h
index a6e5a44c9b42..f042cd848451 100644
--- a/include/trace/events/mmflags.h
+++ b/include/trace/events/mmflags.h
@@ -37,7 +37,8 @@
 	TRACE_GFP_EM(HARDWALL)			\
 	TRACE_GFP_EM(THISNODE)			\
 	TRACE_GFP_EM(ACCOUNT)			\
-	TRACE_GFP_EM(ZEROTAGS)
+	TRACE_GFP_EM(ZEROTAGS)			\
+	TRACE_GFP_EM(PRIVATE)
 
 #ifdef CONFIG_KASAN_HW_TAGS
 # define TRACE_GFP_FLAGS_KASAN			\
@@ -73,7 +74,6 @@
 TRACE_GFP_FLAGS
 
 /* Just in case these are ever used */
-TRACE_DEFINE_ENUM(___GFP_UNUSED_BIT);
 TRACE_DEFINE_ENUM(___GFP_LAST_BIT);
 
 #define gfpflag_string(flag) {(__force unsigned long)flag, #flag}
diff --git a/kernel/cgroup/cpuset.c b/kernel/cgroup/cpuset.c
index 473aa9261e16..1a597f0c7c6c 100644
--- a/kernel/cgroup/cpuset.c
+++ b/kernel/cgroup/cpuset.c
@@ -444,21 +444,32 @@ static void guarantee_active_cpus(struct task_struct *tsk,
 }
 
 /*
- * Return in *pmask the portion of a cpusets's mems_allowed that
+ * Return in *pmask the portion of a cpuset's mems_allowed that
  * are online, with memory.  If none are online with memory, walk
  * up the cpuset hierarchy until we find one that does have some
  * online mems.  The top cpuset always has some mems online.
  *
  * One way or another, we guarantee to return some non-empty subset
- * of node_states[N_MEMORY].
+ * of node_states[N_MEMORY].  N_MEMORY_PRIVATE nodes from the
+ * original cpuset are preserved, but only N_MEMORY nodes are
+ * pulled from ancestors.
  *
  * Call with callback_lock or cpuset_mutex held.
  */
 static void guarantee_online_mems(struct cpuset *cs, nodemask_t *pmask)
 {
+	struct cpuset *orig_cs = cs;
+	int nid;
+
 	while (!nodes_intersects(cs->effective_mems, node_states[N_MEMORY]))
 		cs = parent_cs(cs);
+
 	nodes_and(*pmask, cs->effective_mems, node_states[N_MEMORY]);
+
+	for_each_node_state(nid, N_MEMORY_PRIVATE) {
+		if (node_isset(nid, orig_cs->effective_mems))
+			node_set(nid, *pmask);
+	}
 }
 
 /**
@@ -4075,7 +4086,9 @@ static void cpuset_handle_hotplug(void)
 
 	/* fetch the available cpus/mems and find out which changed how */
 	cpumask_copy(&new_cpus, cpu_active_mask);
-	new_mems = node_states[N_MEMORY];
+
+	/* Include N_MEMORY_PRIVATE so cpuset controls access the same way */
+	nodes_or(new_mems, node_states[N_MEMORY], node_states[N_MEMORY_PRIVATE]);
 
 	/*
 	 * If subpartitions_cpus is populated, it is likely that the check
@@ -4488,10 +4501,21 @@ bool cpuset_node_allowed(struct cgroup *cgroup, int nid)
  * __alloc_pages() will include all nodes.  If the slab allocator
  * is passed an offline node, it will fall back to the local node.
  * See kmem_cache_alloc_node().
+ *
+ *
+ * Private nodes aren't eligible for these allocations, so skip them.
+ * guarantee_online_mems guaranttes at least one N_MEMORY node is set.
  */
 static int cpuset_spread_node(int *rotor)
 {
-	return *rotor = next_node_in(*rotor, current->mems_allowed);
+	int node;
+
+	do {
+		node = next_node_in(*rotor, current->mems_allowed);
+		*rotor = node;
+	} while (node_state(node, N_MEMORY_PRIVATE));
+
+	return node;
 }
 
 /**
-- 
2.53.0


^ permalink raw reply related

* [RFC PATCH v4 01/27] numa: introduce N_MEMORY_PRIVATE node state
From: Gregory Price @ 2026-02-22  8:48 UTC (permalink / raw)
  To: lsf-pc
  Cc: linux-kernel, linux-cxl, cgroups, linux-mm, linux-trace-kernel,
	damon, kernel-team, gregkh, rafael, dakr, dave, jonathan.cameron,
	dave.jiang, alison.schofield, vishal.l.verma, ira.weiny,
	dan.j.williams, longman, akpm, david, lorenzo.stoakes,
	Liam.Howlett, vbabka, rppt, surenb, mhocko, osalvador, ziy,
	matthew.brost, joshua.hahnjy, rakie.kim, byungchul, gourry,
	ying.huang, apopple, axelrasmussen, yuanchu, weixugc, yury.norov,
	linux, mhiramat, mathieu.desnoyers, tj, hannes, mkoutny, jackmanb,
	sj, baolin.wang, npache, ryan.roberts, dev.jain, baohua,
	lance.yang, muchun.song, xu.xin16, chengming.zhou, jannh,
	linmiaohe, nao.horiguchi, pfalcato, rientjes, shakeel.butt, riel,
	harry.yoo, cl, roman.gushchin, chrisl, kasong, shikemeng, nphamcs,
	bhe, zhengqi.arch, terry.bowman
In-Reply-To: <20260222084842.1824063-1-gourry@gourry.net>

N_MEMORY nodes are intended to contain general System RAM. Today, some
device drivers hotplug their memory (marked Specific Purpose or Reserved)
to get access to mm/ services, but don't intend it for general consumption.

Create N_MEMORY_PRIVATE for memory nodes whose memory is not intended for
general consumption. This state is mutually exclusive with N_MEMORY.

Add the node_private infrastructure for N_MEMORY_PRIVATE nodes:

  - struct node_private: Per-node container stored in NODE_DATA(nid),
    holding driver callbacks (ops), owner, and refcount.

  - struct node_private_ops: Initial structure with void *reserved
    placeholder and flags field.  Callbacks will be added by subsequent
    commits as each consumer is wired up.

  - folio_is_private_node() / page_is_private_node(): check if a
    folio/page resides on a private node.

  - folio_node_private_ops() / node_private_flags(): retrieve the ops
    vtable or flags for a folio's node.

  - Registration API: node_private_register()/unregister() for drivers
    to register callbacks for private nodes. Only one driver callback
    can be registered per node - attempting to register different ops
    returns -EBUSY.

  - sysfs attribute exposing N_MEMORY_PRIVATE node state.

Zonelist construction changes for private nodes are deferred to a
subsequent commit.

Signed-off-by: Gregory Price <gourry@gourry.net>
---
 drivers/base/node.c          | 197 ++++++++++++++++++++++++++++++++
 include/linux/mmzone.h       |   4 +
 include/linux/node_private.h | 210 +++++++++++++++++++++++++++++++++++
 include/linux/nodemask.h     |   1 +
 4 files changed, 412 insertions(+)
 create mode 100644 include/linux/node_private.h

diff --git a/drivers/base/node.c b/drivers/base/node.c
index 00cf4532f121..646dc48a23b5 100644
--- a/drivers/base/node.c
+++ b/drivers/base/node.c
@@ -22,6 +22,7 @@
 #include <linux/swap.h>
 #include <linux/slab.h>
 #include <linux/memblock.h>
+#include <linux/node_private.h>
 
 static const struct bus_type node_subsys = {
 	.name = "node",
@@ -861,6 +862,198 @@ void register_memory_blocks_under_node_hotplug(int nid, unsigned long start_pfn,
 			   (void *)&nid, register_mem_block_under_node_hotplug);
 	return;
 }
+
+static DEFINE_MUTEX(node_private_lock);
+static bool node_private_initialized;
+
+/**
+ * node_private_register - Register a private node
+ * @nid: Node identifier
+ * @np: The node_private structure (driver-allocated, driver-owned)
+ *
+ * Register a driver for a private node. Only one driver can register
+ * per node. If another driver has already registered (with different np),
+ * -EBUSY is returned. Re-registration with the same np is allowed.
+ *
+ * The driver owns the node_private memory and must ensure it remains valid
+ * until refcount reaches 0 after node_private_unregister().
+ *
+ * Returns 0 on success, negative errno on failure.
+ */
+int node_private_register(int nid, struct node_private *np)
+{
+	struct node_private *existing;
+	pg_data_t *pgdat;
+	int ret = 0;
+
+	if (!np || !node_possible(nid))
+		return -EINVAL;
+
+	if (!node_private_initialized)
+		return -ENODEV;
+
+	mutex_lock(&node_private_lock);
+	mem_hotplug_begin();
+
+	/* N_MEMORY_PRIVATE and N_MEMORY are mutually exclusive */
+	if (node_state(nid, N_MEMORY)) {
+		ret = -EBUSY;
+		goto out;
+	}
+
+	pgdat = NODE_DATA(nid);
+	existing = rcu_dereference_protected(pgdat->node_private,
+					     lockdep_is_held(&node_private_lock));
+
+	/* Only one source my register this node */
+	if (existing) {
+		if (existing != np) {
+			ret = -EBUSY;
+			goto out;
+		}
+		goto out;
+	}
+
+	refcount_set(&np->refcount, 1);
+	init_completion(&np->released);
+
+	rcu_assign_pointer(pgdat->node_private, np);
+	pgdat->private = true;
+
+out:
+	mem_hotplug_done();
+	mutex_unlock(&node_private_lock);
+	return ret;
+}
+EXPORT_SYMBOL_GPL(node_private_register);
+
+/**
+ * node_private_set_ops - Set service callbacks on a registered private node
+ * @nid: Node identifier
+ * @ops: Service callbacks and flags (driver-owned, must outlive registration)
+ *
+ * Validates flag dependencies and sets the ops on the node's node_private.
+ * The node must already be registered via node_private_register().
+ *
+ * Returns 0 on success, -EINVAL for invalid flag combinations,
+ * -ENODEV if no node_private is registered on @nid.
+ */
+int node_private_set_ops(int nid, const struct node_private_ops *ops)
+{
+	struct node_private *np;
+	int ret = 0;
+
+	if (!ops)
+		return -EINVAL;
+
+	if (!node_possible(nid))
+		return -EINVAL;
+
+	mutex_lock(&node_private_lock);
+	np = rcu_dereference_protected(NODE_DATA(nid)->node_private,
+				       lockdep_is_held(&node_private_lock));
+	if (!np)
+		ret = -ENODEV;
+	else
+		np->ops = ops;
+	mutex_unlock(&node_private_lock);
+	return ret;
+}
+EXPORT_SYMBOL_GPL(node_private_set_ops);
+
+/**
+ * node_private_clear_ops - Clear service callbacks from a private node
+ * @nid: Node identifier
+ * @ops: Expected ops pointer (must match current ops)
+ *
+ * Clears the ops only if @ops matches the currently registered ops,
+ * preventing one service from accidentally clearing another's callbacks.
+ *
+ * Returns 0 on success, -ENODEV if no node_private is registered,
+ * -EINVAL if @ops does not match.
+ */
+int node_private_clear_ops(int nid, const struct node_private_ops *ops)
+{
+	struct node_private *np;
+	int ret = 0;
+
+	if (!node_possible(nid))
+		return -EINVAL;
+
+	mutex_lock(&node_private_lock);
+	np = rcu_dereference_protected(NODE_DATA(nid)->node_private,
+				       lockdep_is_held(&node_private_lock));
+	if (!np)
+		ret = -ENODEV;
+	else if (np->ops != ops)
+		ret = -EINVAL;
+	else
+		np->ops = NULL;
+	mutex_unlock(&node_private_lock);
+	return ret;
+}
+EXPORT_SYMBOL_GPL(node_private_clear_ops);
+
+/**
+ * node_private_unregister - Unregister a private node
+ * @nid: Node identifier
+ *
+ * Unregister the driver from a private node. Only succeeds if all memory
+ * has been offlined and the node is no longer N_MEMORY_PRIVATE.
+ * When successful, drops the refcount to 0 indicating the driver can
+ * free its context.
+ *
+ * N_MEMORY_PRIVATE state is cleared by offline_pages() when the last
+ * memory is offlined, not by this function.
+ *
+ * Return: 0 if unregistered, -EBUSY if N_MEMORY_PRIVATE is still set
+ * (other memory blocks remain on this node).
+ */
+int node_private_unregister(int nid)
+{
+	struct node_private *np;
+	pg_data_t *pgdat;
+
+	if (!node_possible(nid))
+		return 0;
+
+	mutex_lock(&node_private_lock);
+	mem_hotplug_begin();
+
+	pgdat = NODE_DATA(nid);
+	np = rcu_dereference_protected(pgdat->node_private,
+				       lockdep_is_held(&node_private_lock));
+	if (!np) {
+		mem_hotplug_done();
+		mutex_unlock(&node_private_lock);
+		return 0;
+	}
+
+	/*
+	 * Only unregister if all memory is offline and N_MEMORY_PRIVATE is
+	 * cleared. N_MEMORY_PRIVATE is cleared by offline_pages() when the
+	 * last memory block is offlined.
+	 */
+	if (node_state(nid, N_MEMORY_PRIVATE)) {
+		mem_hotplug_done();
+		mutex_unlock(&node_private_lock);
+		return -EBUSY;
+	}
+
+	rcu_assign_pointer(pgdat->node_private, NULL);
+	pgdat->private = false;
+
+	mem_hotplug_done();
+	mutex_unlock(&node_private_lock);
+
+	synchronize_rcu();
+
+	if (!refcount_dec_and_test(&np->refcount))
+		wait_for_completion(&np->released);
+	return 0;
+}
+EXPORT_SYMBOL_GPL(node_private_unregister);
+
 #endif /* CONFIG_MEMORY_HOTPLUG */
 
 /**
@@ -959,6 +1152,7 @@ static struct node_attr node_state_attr[] = {
 	[N_HIGH_MEMORY] = _NODE_ATTR(has_high_memory, N_HIGH_MEMORY),
 #endif
 	[N_MEMORY] = _NODE_ATTR(has_memory, N_MEMORY),
+	[N_MEMORY_PRIVATE] = _NODE_ATTR(has_private_memory, N_MEMORY_PRIVATE),
 	[N_CPU] = _NODE_ATTR(has_cpu, N_CPU),
 	[N_GENERIC_INITIATOR] = _NODE_ATTR(has_generic_initiator,
 					   N_GENERIC_INITIATOR),
@@ -972,6 +1166,7 @@ static struct attribute *node_state_attrs[] = {
 	&node_state_attr[N_HIGH_MEMORY].attr.attr,
 #endif
 	&node_state_attr[N_MEMORY].attr.attr,
+	&node_state_attr[N_MEMORY_PRIVATE].attr.attr,
 	&node_state_attr[N_CPU].attr.attr,
 	&node_state_attr[N_GENERIC_INITIATOR].attr.attr,
 	NULL
@@ -1007,5 +1202,7 @@ void __init node_dev_init(void)
 			panic("%s() failed to add node: %d\n", __func__, ret);
 	}
 
+	node_private_initialized = true;
+
 	register_memory_blocks_under_nodes();
 }
diff --git a/include/linux/mmzone.h b/include/linux/mmzone.h
index b01cb1e49896..992eb1c5a2c6 100644
--- a/include/linux/mmzone.h
+++ b/include/linux/mmzone.h
@@ -25,6 +25,8 @@
 #include <linux/zswap.h>
 #include <asm/page.h>
 
+struct node_private;
+
 /* Free memory management - zoned buddy allocator.  */
 #ifndef CONFIG_ARCH_FORCE_MAX_ORDER
 #define MAX_PAGE_ORDER 10
@@ -1514,6 +1516,8 @@ typedef struct pglist_data {
 	atomic_long_t		vm_stat[NR_VM_NODE_STAT_ITEMS];
 #ifdef CONFIG_NUMA
 	struct memory_tier __rcu *memtier;
+	struct node_private __rcu *node_private;
+	bool private;
 #endif
 #ifdef CONFIG_MEMORY_FAILURE
 	struct memory_failure_stats mf_stats;
diff --git a/include/linux/node_private.h b/include/linux/node_private.h
new file mode 100644
index 000000000000..6a70ec39d569
--- /dev/null
+++ b/include/linux/node_private.h
@@ -0,0 +1,210 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+#ifndef _LINUX_NODE_PRIVATE_H
+#define _LINUX_NODE_PRIVATE_H
+
+#include <linux/completion.h>
+#include <linux/mm.h>
+#include <linux/nodemask.h>
+#include <linux/rcupdate.h>
+#include <linux/refcount.h>
+
+struct page;
+struct vm_area_struct;
+struct vm_fault;
+
+/**
+ * struct node_private_ops - Callbacks for private node services
+ *
+ * Services register these callbacks to intercept MM operations that affect
+ * their private nodes.
+ *
+ * Flag bits control which MM subsystems may operate on folios on this node.
+ *
+ * The pgdat->node_private pointer is RCU-protected.  Callbacks fall into
+ * three categories based on their calling context:
+ *
+ * Folio-referenced callbacks (RCU released before callback):
+ *   The caller holds a reference to a folio on the private node, which
+ *   pins the node's memory online and prevents node_private teardown.
+ *
+ * Refcounted callbacks (RCU released before callback):
+ *   The caller has no folio on the private node (e.g., folios are on a
+ *   source node being migrated TO this node).  A temporary refcount is
+ *   taken on node_private under rcu_read_lock to keep the structure (and
+ *   the service module) alive across the callback.  node_private_unregister
+ *   waits for all temporary references to drain before returning.
+ *
+ * Non-folio callbacks (rcu_read_lock held during callback):
+ *   No folio reference exists, so rcu_read_lock is held across the
+ *   callback to prevent node_private from being freed.
+ *   These callbacks MUST NOT sleep.
+ *
+ * @flags: Operation exclusion flags (NP_OPS_* constants).
+ *
+ */
+struct node_private_ops {
+	unsigned long flags;
+};
+
+/**
+ * struct node_private - Per-node container for N_MEMORY_PRIVATE nodes
+ *
+ * This structure is allocated by the driver and passed to node_private_register().
+ * The driver owns the memory and must ensure it remains valid until after
+ * node_private_unregister() returns with the reference count dropped to 0.
+ *
+ * @owner: Opaque driver identifier
+ * @refcount: Reference count (1 = registered; temporary refs for non-folio
+ *		callbacks that may sleep; 0 = fully released)
+ * @released: Signaled when refcount drops to 0; unregister waits on this
+ * @ops: Service callbacks and exclusion flags (NULL until service registers)
+ */
+struct node_private {
+	void *owner;
+	refcount_t refcount;
+	struct completion released;
+	const struct node_private_ops *ops;
+};
+
+#ifdef CONFIG_NUMA
+
+#include <linux/mmzone.h>
+
+/**
+ * folio_is_private_node - Check if folio is on an N_MEMORY_PRIVATE node
+ * @folio: The folio to check
+ *
+ * Returns true if the folio resides on a private node.
+ */
+static inline bool folio_is_private_node(struct folio *folio)
+{
+	return node_state(folio_nid(folio), N_MEMORY_PRIVATE);
+}
+
+/**
+ * page_is_private_node - Check if page is on an N_MEMORY_PRIVATE node
+ * @page: The page to check
+ *
+ * Returns true if the page resides on a private node.
+ */
+static inline bool page_is_private_node(struct page *page)
+{
+	return node_state(page_to_nid(page), N_MEMORY_PRIVATE);
+}
+
+static inline const struct node_private_ops *
+folio_node_private_ops(struct folio *folio)
+{
+	const struct node_private_ops *ops;
+	struct node_private *np;
+
+	rcu_read_lock();
+	np = rcu_dereference(NODE_DATA(folio_nid(folio))->node_private);
+	ops = np ? np->ops : NULL;
+	rcu_read_unlock();
+
+	return ops;
+}
+
+static inline unsigned long node_private_flags(int nid)
+{
+	struct node_private *np;
+	unsigned long flags;
+
+	rcu_read_lock();
+	np = rcu_dereference(NODE_DATA(nid)->node_private);
+	flags = (np && np->ops) ? np->ops->flags : 0;
+	rcu_read_unlock();
+
+	return flags;
+}
+
+static inline bool folio_private_flags(struct folio *f, unsigned long flag)
+{
+	return node_private_flags(folio_nid(f)) & flag;
+}
+
+static inline bool node_private_has_flag(int nid, unsigned long flag)
+{
+	return node_private_flags(nid) & flag;
+}
+
+static inline bool zone_private_flags(struct zone *z, unsigned long flag)
+{
+	return node_private_flags(zone_to_nid(z)) & flag;
+}
+
+#else /* !CONFIG_NUMA */
+
+static inline bool folio_is_private_node(struct folio *folio)
+{
+	return false;
+}
+
+static inline bool page_is_private_node(struct page *page)
+{
+	return false;
+}
+
+static inline const struct node_private_ops *
+folio_node_private_ops(struct folio *folio)
+{
+	return NULL;
+}
+
+static inline unsigned long node_private_flags(int nid)
+{
+	return 0;
+}
+
+static inline bool folio_private_flags(struct folio *f, unsigned long flag)
+{
+	return false;
+}
+
+static inline bool node_private_has_flag(int nid, unsigned long flag)
+{
+	return false;
+}
+
+static inline bool zone_private_flags(struct zone *z, unsigned long flag)
+{
+	return false;
+}
+
+#endif /* CONFIG_NUMA */
+
+#if defined(CONFIG_NUMA) && defined(CONFIG_MEMORY_HOTPLUG)
+
+int node_private_register(int nid, struct node_private *np);
+int node_private_unregister(int nid);
+int node_private_set_ops(int nid, const struct node_private_ops *ops);
+int node_private_clear_ops(int nid, const struct node_private_ops *ops);
+
+#else /* !CONFIG_NUMA || !CONFIG_MEMORY_HOTPLUG */
+
+static inline int node_private_register(int nid, struct node_private *np)
+{
+	return -ENODEV;
+}
+
+static inline int node_private_unregister(int nid)
+{
+	return 0;
+}
+
+static inline int node_private_set_ops(int nid,
+				       const struct node_private_ops *ops)
+{
+	return -ENODEV;
+}
+
+static inline int node_private_clear_ops(int nid,
+					 const struct node_private_ops *ops)
+{
+	return -ENODEV;
+}
+
+#endif /* CONFIG_NUMA && CONFIG_MEMORY_HOTPLUG */
+
+#endif /* _LINUX_NODE_PRIVATE_H */
diff --git a/include/linux/nodemask.h b/include/linux/nodemask.h
index bd38648c998d..c9bcfd5a9a06 100644
--- a/include/linux/nodemask.h
+++ b/include/linux/nodemask.h
@@ -391,6 +391,7 @@ enum node_states {
 	N_HIGH_MEMORY = N_NORMAL_MEMORY,
 #endif
 	N_MEMORY,		/* The node has memory(regular, high, movable) */
+	N_MEMORY_PRIVATE,	/* The node's memory is private */
 	N_CPU,		/* The node has one or more cpus */
 	N_GENERIC_INITIATOR,	/* The node has one or more Generic Initiators */
 	NR_NODE_STATES
-- 
2.53.0


^ permalink raw reply related

* [LSF/MM/BPF TOPIC][RFC PATCH v4 00/27] Private Memory Nodes (w/ Compressed RAM)
From: Gregory Price @ 2026-02-22  8:48 UTC (permalink / raw)
  To: lsf-pc
  Cc: linux-kernel, linux-cxl, cgroups, linux-mm, linux-trace-kernel,
	damon, kernel-team, gregkh, rafael, dakr, dave, jonathan.cameron,
	dave.jiang, alison.schofield, vishal.l.verma, ira.weiny,
	dan.j.williams, longman, akpm, david, lorenzo.stoakes,
	Liam.Howlett, vbabka, rppt, surenb, mhocko, osalvador, ziy,
	matthew.brost, joshua.hahnjy, rakie.kim, byungchul, gourry,
	ying.huang, apopple, axelrasmussen, yuanchu, weixugc, yury.norov,
	linux, mhiramat, mathieu.desnoyers, tj, hannes, mkoutny, jackmanb,
	sj, baolin.wang, npache, ryan.roberts, dev.jain, baohua,
	lance.yang, muchun.song, xu.xin16, chengming.zhou, jannh,
	linmiaohe, nao.horiguchi, pfalcato, rientjes, shakeel.butt, riel,
	harry.yoo, cl, roman.gushchin, chrisl, kasong, shikemeng, nphamcs,
	bhe, zhengqi.arch, terry.bowman

Topic type: MM

Presenter: Gregory Price <gourry@gourry.net>

This series introduces N_MEMORY_PRIVATE, a NUMA node state for memory
managed by the buddy allocator but excluded from normal allocations.

I present it with an end-to-end Compressed RAM service (mm/cram.c)
that would otherwise not be possible (or would be considerably more
difficult, be device-specific, and add to the ZONE_DEVICE boondoggle).


TL;DR
===

N_MEMORY_PRIVATE is all about isolating NUMA nodes and then punching
explicit holes in that isolation to do useful things we couldn't do
before without re-implementing entire portions of mm/ in a driver.


/* This is my memory. There are many like it, but this one is mine. */
rc = add_private_memory_driver_managed(nid, start, size, name, flags,
                                       online_type, private_context);

page = alloc_pages_node(nid, __GFP_PRIVATE, 0);

/* Ok but I want to do something useful with it */
static const struct node_private_ops ops = {
        .migrate_to     = my_migrate_to,
        .folio_migrate  = my_folio_migrate,
        .flags = NP_OPS_MIGRATION | NP_OPS_MEMPOLICY,
};
node_private_set_ops(nid, &ops);

/* And now I can use mempolicy with my memory */
buf = mmap(...);
mbind(buf, len, mode, private_node, ...);
buf[0] = 0xdeadbeef;  /* Faults onto private node */

/* And to be clear, no one else gets my memory */
buf2 = malloc(4096);  /* Standard allocation */
buf2[0] = 0xdeadbeef; /* Can never land on private node */

/* But i can choose to migrate it to the private node */
move_pages(0, 1, &buf, &private_node, NULL, ...);

/* And more fun things like this */


Patchwork
===
A fully working branch based on cxl/next can be found here:
https://github.com/gourryinverse/linux/tree/private_compression

A QEMU device which can inject high/low interrupts can be found here:
https://github.com/gourryinverse/qemu/tree/compressed_cxl_clean

The additional patches on these branches are CXL and DAX driver
housecleaning only tangentially relevant to this RFC, so i've
omitted them for the sake of trying to keep it somewhat clean
here.  Those patches should (hopefully) be going upstream anyway.

Patches 1-22: Core Private Node Infrastructure

  Patch  1:      Introduce N_MEMORY_PRIVATE scaffolding
  Patch  2:      Introduce __GFP_PRIVATE
  Patch  3:      Apply allocation isolation mechanisms
  Patch  4:      Add N_MEMORY nodes to private fallback lists
  Patches 5-9:   Filter operations not yet supported
  Patch 10:      free_folio callback
  Patch 11:      split_folio callback
  Patches 12-20: mm/ service opt-ins:
                   Migration, Mempolicy, Demotion, Write Protect,
                   Reclaim, OOM, NUMA Balancing, Compaction,
                   LongTerm Pinning
  Patch 21:      memory_failure callback
  Patch 22:      Memory hotplug plumbing for private nodes

Patch 23: mm/cram -- Compressed RAM Management

Patches 24-27: CXL Driver examples
  Sysram Regions with Private node support
  Basic Driver Example: (MIGRATION | MEMPOLICY)
  Compression Driver Example (Generic)


Background
===

Today, drivers that want mm-like services on non-general-purpose
memory either use ZONE_DEVICE (self-managed memory) or hotplug into
N_MEMORY and accept the risk of uncontrolled allocation.

Neither option provides what we really want - the ability to:
	1) selectively participate in mm/ subsystems, while
	2) isolating that memory from general purpose use.

Some device-attached memory cannot be managed as fully general-purpose
system RAM.  CXL devices with inline compression, for example, may
corrupt data or crash the machine if the compression ratio drops
below a threshold -- we simply run out of physical memory.

This is a hard problem to solve: how does an operating system deal
with a device that basically lies about how much capacity it has?

(We'll discuss that in the CRAM section)


Core Proposal: N_MEMORY_PRIVATE
===

Introduce N_MEMORY_PRIVATE, a NUMA node state for memory managed by
the buddy allocator, but excluded from normal allocation paths.

Private nodes:

  - Are filtered from zonelist fallback: all existing callers to
    get_page_from_freelist cannot reach these nodes through any
    normal fallback mechanism.

  - Filter allocation requests on __GFP_PRIVATE
    	numa_zone_allowed() excludes them otherwise. 

    Applies to systems with and without cpusets.

    GFP_PRIVATE is (__GFP_PRIVATE | __GFP_THISNODE).

    Services use it when they need to allocate specifically from
    a private node (e.g., CRAM allocating a destination folio).

    No existing allocator path sets __GFP_PRIVATE, so private nodes
    are unreachable by default.

  - Use standard struct page / folio.  No ZONE_DEVICE, no pgmap,
    no struct page metadata limitations.

  - Use a node-scoped metadata structure to accomplish filtering
    and callback support.

  - May participate in the buddy allocator, reclaim, compaction,
    and LRU like normal memory, gated by an opt-in set of flags.

The key abstraction is node_private_ops: a per-node callback table
registered by a driver or service.  

Each callback is individually gated by an NP_OPS_* capability flag.

A driver opts in only to the mm/ operations it needs.

It is similar to ZONE_DEVICE's pgmap at a node granularity.

In fact...


Re-use of ZONE_DEVICE Hooks
===

The callback insertion points deliberately mirror existing ZONE_DEVICE
hooks to minimize the surface area of the mechanism.

I believe this could subsume most DEVICE_COHERENT users, and greatly
simplify the device-managed memory development process (no more
per-driver allocator and migration code).

(Also it's just "So Fresh, So Clean").

The base set of callbacks introduced include:

  free_folio           - mirrors ZONE_DEVICE's
                         free_zone_device_page() hook in
                         __folio_put() / folios_put_refs()

  folio_split          - mirrors ZONE_DEVICE's
  			 called when a huge page is split up

  migrate_to           - demote_folio_list() custom demotion (same
                         site as ZONE_DEVICE demotion rejection)

  folio_migrate        - called when private node folio is moved to
                         another location (e.g. compaction)

  handle_fault         - mirrors the ZONE_DEVICE fault dispatch in
                         handle_pte_fault() (do_wp_page path)

  reclaim_policy       - called by reclaim to let a driver own the
                         boost lifecycle (driver can driver node reclaim)

  memory_failure       - parallels memory_failure_dev_pagemap(),
                         but for online pages that enter the normal
                         hwpoison path

At skip sites (mlock, madvise, KSM, user migration), a unified
folio_is_private_managed() predicate covers both ZONE_DEVICE and
N_MEMORY_PRIVATE folios, consolidating existing zone_device checks
with private node checks rather than adding new ones.

  static inline bool folio_is_private_managed(struct folio *folio)
  {
          return folio_is_zone_device(folio) ||
                 folio_is_private_node(folio);
  }

Most integration points become a one-line swap:

  -     if (folio_is_zone_device(folio))
  +     if (unlikely(folio_is_private_managed(folio)))


Where a one-line integration is insufficient, the integration is
kept as clean as possible with zone_device, rather than simply
adding more call-sites on top of it:

static inline bool folio_managed_handle_fault(struct folio *folio,
  struct vm_fault *vmf, vm_fault_t *ret)
{
  /* Zone device pages use swap entries; handled in do_swap_page */
  if (folio_is_zone_device(folio))
    return false;

  if (folio_is_private_node(folio)) {
    const struct node_private_ops *ops = folio_node_private_ops(folio);

    if (ops && ops->handle_fault) {
      *ret = ops->handle_fault(vmf);
      return true;
    }
  }
  return false;
}



Flag-gated behavior (NP_OPS_*) controls:
===

We use OPS flags to denote what mm/ services we want to allow on our
private node.   I've plumbed these through so far:

  NP_OPS_MIGRATION       - Node supports migration
  NP_OPS_MEMPOLICY       - Node supports mempolicy actions
  NP_OPS_DEMOTION        - Node appears in demotion target lists
  NP_OPS_PROTECT_WRITE   - Node memory is read-only (wrprotect)
  NP_OPS_RECLAIM         - Node supports reclaim
  NP_OPS_NUMA_BALANCING  - Node supports numa balancing
  NP_OPS_COMPACTION      - Node supports compaction
  NP_OPS_LONGTERM_PIN    - Node supports longterm pinning
  NP_OPS_OOM_ELIGIBLE	 - (MIGRATION | DEMOTION), node is reachable
                           as normal system ram storage, so it should
			   be considered in OOM pressure calculations.

I wasn't quite sure how to classify ksm, khugepaged, madvise, and
mlock - so i have omitted those for now.

Most hooks are straightforward.

Including a node as a demotion-eligible target was as simple as:

static void establish_demotion_targets(void)
{
  ..... snip .....
  /*
   * Include private nodes that have opted in to demotion
   * via NP_OPS_DEMOTION.  A node might have custom migrate
   */
  all_memory = node_states[N_MEMORY];
  for_each_node_state(node, N_MEMORY_PRIVATE) {
      if (node_private_has_flag(node, NP_OPS_DEMOTION))
      node_set(node, all_memory);
  }
  ..... snip .....
}

The Migration and Mempolicy support are the two most complex pieces,
and most useful things are built on top of Migration (meaning the
remaining implementations are usually simple).


Private Node Hotplug Lifecycle
===

Registration follows a strict order enforced by
add_private_memory_driver_managed():

  1. Driver calls add_private_memory_driver_managed(nid, start,
     size, resource_name, mhp_flags, online_type, &np).

  2. node_private_register(nid, &np) stores the driver's
     node_private in pgdat and sets pgdat->private.  N_MEMORY and
     N_MEMORY_PRIVATE are mutually exclusive -- registration fails
     with -EBUSY if the node already has N_MEMORY set.

     Only one driver may register per private node.

  3. Memory is hotplugged via __add_memory_driver_managed().

     When online_pages() runs, it checks pgdat->private and sets
     N_MEMORY_PRIVATE instead of N_MEMORY.  

     Zonelist construction gives private nodes a self-only NOFALLBACK
     list and an N_MEMORY fallback list (so kernel/slab allocations on
     behalf of private node work can fall back to DRAM).

  4. kswapd and kcompactd are NOT started for private nodes.  The
     owning service is responsible for driving reclaim if needed
     (e.g., CRAM uses watermark_boost to wake kswapd on demand).

Teardown is the reverse:

  1. Driver calls offline_and_remove_private_memory(nid, start,
     size).

  2. offline_pages() offlines the memory.  When the last block is
     offlined, N_MEMORY_PRIVATE is cleared automatically.

  3. node_private_unregister() clears pgdat->node_private and
     drops the refcount.  It refuses to unregister (-EBUSY) if
     N_MEMORY_PRIVATE is still set (other memory ranges remain).

The driver is responsible for ensuring memory is hot-unpluggable
before teardown.  The service must ensure all memory is cleaned
up before hot-unplug - or the service must support migration (so
memory_hotplug.c can evacuate the memory itself).

In the CRAM example, the service supports migration, so memory
hot-unplug can remove memory without any special infrastructure.


Application: Compressed RAM (mm/cram)
===

Compressed RAM has a serious design issue:  Its capacity a lie.

A compression device reports more capacity than it physically has.
If workloads write faster than the OS can reclaim from the device,
we run out of real backing store and corrupt data or crash.

I call this problem: "Trying to Out Run A Bear"

I.e. This is only stable as long as we stay ahead of the pressure.

We don't want to design a system where stability depends on outrunning
a bear - I am slow and do not know where to acquire bear spray.

  Fun fact:   Grizzly bears have a top-speed of 56-64 km/h.
  Unfun Fact: Humans typically top out at ~24 km/h.

This MVP takes a conservative position:

   all compressed memory is mapped read-only.

  - Folios reach the private node only via reclaim (demotion)
  - migrate_to implements custom demotion with backpressure.
  - fixup_migration_pte write-protects PTEs on arrival.
  - wrprotect hooks prevent silent upgrades
  - handle_fault promotes folios back to DRAM on write.
  - free_folio scrubs stale data before buddy free.

Because pages are read-only, writes can never cause runaway
compression ratio loss behind the allocator's back.  Every write
goes through handle_fault, which promotes the folio to DRAM first.

The device only ever sees net compression (demotion in) and explicit
decompression (promotion out via fault or reclaim), and has a much
wider timeframe to respond to poor compression scenarios.

That means there's no bear to out run. The bears are safely asleep in
their bear den, and even if they show up we have a bear-proof cage.

The backpressure system is our bear-proof cage: the driver reports real
device utilization (generalized via watermark_boost on the private
node's zone), and CRAM throttles demotion when capacity is tight.

If compression ratios are bad, we stop demoting pages and start
evicting pages aggressively.

The service as designed is ~350 functional lines of code because it
re-uses mm/ services:

  - Existing reclaim/vmscan code handles demotion.
  - Existing migration code handles migration to/from.
  - Existing page fault handling dispatches faults.

The driver contains all the CXL nastiness core developers don't want
anything to do with - No vendor logic touches mm/ internals.



Future CRAM : Loosening the read-only constraint
===

The read-only model is safe but conservative.  For workloads where
compressed pages are occasionally written, the promotion fault adds
latency.  A future optimization could allow a tunable fraction of
compressed pages to be mapped writable, accepting some risk of
write-driven decompression in exchange for lower overhead.

The private node ops make this straightforward:

  - Adjust fixup_migration_pte to selectively skip
    write-protection.
  - Use the backpressure system to either revoke writable mappings,
    deny additional demotions, or evict when device pressure rises.

This comes at a mild memory overhead: 32MB of DRAM per 1TB of CRAM.
(1 bit per 4KB page).

This is not proposed here, but it should be somewhat trivial.


Discussion Topics
===
0. Obviously I've included the set as an RFC, please rip it apart.

1. Is N_MEMORY_PRIVATE the right isolation abstraction, or should
   this extend ZONE_DEVICE?  Prior feedback pushed away from new
   ZONE logic, but this will likely be debated further.

   My comments on this:

   ZONE_DEVICE requires re-implementing every service you want to
   provide to your device memory, including basic allocation.

   Private nodes use real struct pages with no metadata
   limitations, participate in the buddy allocator, and get NUMA
   topology for free.

2. Can this subsume ZONE_DEVICE COHERENT users?  The architecture
   was designed with this in mind, but it is only a thought experiment.

3. Is a dedicated mm/ service (cram) the right place for compressed
   memory management, or should this be purely driver-side until
   more devices exist?

   I wrote it this way because I forsee more "innovation" in the
   compressed RAM space given current... uh... "Market Conditions".

   I don't see CRAM being CXL-specific, though the only solutions I've
   seen have been CXL.  Nothing is stopping someone from soldering such
   memory directly to a PCB.

5. Where is your hardware-backed data that shows this works?

   I should have some by conference time.

Thanks for reading
Gregory (Gourry)


Gregory Price (27):
  numa: introduce N_MEMORY_PRIVATE node state
  mm,cpuset: gate allocations from N_MEMORY_PRIVATE behind __GFP_PRIVATE
  mm/page_alloc: add numa_zone_allowed() and wire it up
  mm/page_alloc: Add private node handling to build_zonelists
  mm: introduce folio_is_private_managed() unified predicate
  mm/mlock: skip mlock for managed-memory folios
  mm/madvise: skip madvise for managed-memory folios
  mm/ksm: skip KSM for managed-memory folios
  mm/khugepaged: skip private node folios when trying to collapse.
  mm/swap: add free_folio callback for folio release cleanup
  mm/huge_memory.c: add private node folio split notification callback
  mm/migrate: NP_OPS_MIGRATION - support private node user migration
  mm/mempolicy: NP_OPS_MEMPOLICY - support private node mempolicy
  mm/memory-tiers: NP_OPS_DEMOTION - support private node demotion
  mm/mprotect: NP_OPS_PROTECT_WRITE - gate PTE/PMD write-upgrades
  mm: NP_OPS_RECLAIM - private node reclaim participation
  mm/oom: NP_OPS_OOM_ELIGIBLE - private node OOM participation
  mm/memory: NP_OPS_NUMA_BALANCING - private node NUMA balancing
  mm/compaction: NP_OPS_COMPACTION - private node compaction support
  mm/gup: NP_OPS_LONGTERM_PIN - private node longterm pin support
  mm/memory-failure: add memory_failure callback to node_private_ops
  mm/memory_hotplug: add add_private_memory_driver_managed()
  mm/cram: add compressed ram memory management subsystem
  cxl/core: Add cxl_sysram region type
  cxl/core: Add private node support to cxl_sysram
  cxl: add cxl_mempolicy sample PCI driver
  cxl: add cxl_compression PCI driver

 drivers/base/node.c                           |  250 +++-
 drivers/cxl/Kconfig                           |    2 +
 drivers/cxl/Makefile                          |    2 +
 drivers/cxl/core/Makefile                     |    1 +
 drivers/cxl/core/core.h                       |    4 +
 drivers/cxl/core/port.c                       |    2 +
 drivers/cxl/core/region_sysram.c              |  381 ++++++
 drivers/cxl/cxl.h                             |   53 +
 drivers/cxl/type3_drivers/Kconfig             |    3 +
 drivers/cxl/type3_drivers/Makefile            |    3 +
 .../cxl/type3_drivers/cxl_compression/Kconfig |   20 +
 .../type3_drivers/cxl_compression/Makefile    |    4 +
 .../cxl_compression/compression.c             | 1025 +++++++++++++++++
 .../cxl/type3_drivers/cxl_mempolicy/Kconfig   |   16 +
 .../cxl/type3_drivers/cxl_mempolicy/Makefile  |    4 +
 .../type3_drivers/cxl_mempolicy/mempolicy.c   |  297 +++++
 include/linux/cpuset.h                        |    9 -
 include/linux/cram.h                          |   66 ++
 include/linux/gfp_types.h                     |   15 +-
 include/linux/memory-tiers.h                  |    9 +
 include/linux/memory_hotplug.h                |   11 +
 include/linux/migrate.h                       |   17 +-
 include/linux/mm.h                            |   22 +
 include/linux/mmzone.h                        |   16 +
 include/linux/node_private.h                  |  532 +++++++++
 include/linux/nodemask.h                      |    1 +
 include/trace/events/mmflags.h                |    4 +-
 include/uapi/linux/mempolicy.h                |    1 +
 kernel/cgroup/cpuset.c                        |   49 +-
 mm/Kconfig                                    |   10 +
 mm/Makefile                                   |    1 +
 mm/compaction.c                               |   32 +-
 mm/cram.c                                     |  508 ++++++++
 mm/damon/paddr.c                              |    3 +
 mm/huge_memory.c                              |   23 +-
 mm/hugetlb.c                                  |    2 +-
 mm/internal.h                                 |  226 +++-
 mm/khugepaged.c                               |    7 +-
 mm/ksm.c                                      |    9 +-
 mm/madvise.c                                  |    5 +-
 mm/memory-failure.c                           |   15 +
 mm/memory-tiers.c                             |   46 +-
 mm/memory.c                                   |   26 +
 mm/memory_hotplug.c                           |  122 +-
 mm/mempolicy.c                                |   69 +-
 mm/migrate.c                                  |   63 +-
 mm/mlock.c                                    |    5 +-
 mm/mprotect.c                                 |    4 +-
 mm/oom_kill.c                                 |   52 +-
 mm/page_alloc.c                               |   79 +-
 mm/rmap.c                                     |    4 +-
 mm/slub.c                                     |    3 +-
 mm/swap.c                                     |   21 +-
 mm/vmscan.c                                   |   55 +-
 54 files changed, 4057 insertions(+), 152 deletions(-)
 create mode 100644 drivers/cxl/core/region_sysram.c
 create mode 100644 drivers/cxl/type3_drivers/Kconfig
 create mode 100644 drivers/cxl/type3_drivers/Makefile
 create mode 100644 drivers/cxl/type3_drivers/cxl_compression/Kconfig
 create mode 100644 drivers/cxl/type3_drivers/cxl_compression/Makefile
 create mode 100644 drivers/cxl/type3_drivers/cxl_compression/compression.c
 create mode 100644 drivers/cxl/type3_drivers/cxl_mempolicy/Kconfig
 create mode 100644 drivers/cxl/type3_drivers/cxl_mempolicy/Makefile
 create mode 100644 drivers/cxl/type3_drivers/cxl_mempolicy/mempolicy.c
 create mode 100644 include/linux/cram.h
 create mode 100644 include/linux/node_private.h
 create mode 100644 mm/cram.c

-- 
2.53.0


^ permalink raw reply

* [syzbot] [net?] [trace?] WARNING: refcount bug in call_timer_fn (4)
From: syzbot @ 2026-02-21 22:24 UTC (permalink / raw)
  To: linux-kernel, linux-trace-kernel, netdev, peterz, syzkaller-bugs,
	tglx

Hello,

syzbot found the following issue on:

HEAD commit:    8b690556d8fe Merge tag 'for-linus' of git://git.kernel.org..
git tree:       upstream
console output: https://syzkaller.appspot.com/x/log.txt?x=11715658580000
kernel config:  https://syzkaller.appspot.com/x/.config?x=38a0c4cddc846161
dashboard link: https://syzkaller.appspot.com/bug?extid=07dcf509f4c013e25dc5
compiler:       Debian clang version 20.1.8 (++20250708063551+0c9f909b7976-1~exp1~20250708183702.136), Debian LLD 20.1.8

Unfortunately, I don't have any reproducer for this issue yet.

Downloadable assets:
disk image: https://storage.googleapis.com/syzbot-assets/3ca2379f6d75/disk-8b690556.raw.xz
vmlinux: https://storage.googleapis.com/syzbot-assets/ce7e9a97f8b4/vmlinux-8b690556.xz
kernel image: https://storage.googleapis.com/syzbot-assets/0936d2c0b069/bzImage-8b690556.xz

IMPORTANT: if you fix the issue, please add the following tag to the commit:
Reported-by: syzbot+07dcf509f4c013e25dc5@syzkaller.appspotmail.com

------------[ cut here ]------------
refcount_t: underflow; use-after-free.
WARNING: CPU: 1 PID: 29 at lib/refcount.c:28 refcount_warn_saturate+0x11a/0x1d0 lib/refcount.c:28
Modules linked in:
CPU: 1 UID: 0 PID: 29 Comm: ktimers/1 Not tainted syzkaller #0 PREEMPT_{RT,(full)} 
Hardware name: Google Google Compute Engine/Google Compute Engine, BIOS Google 10/25/2025
RIP: 0010:refcount_warn_saturate+0x11a/0x1d0 lib/refcount.c:28
Code: c0 30 3d 8b e8 c7 91 09 fd 90 0f 0b 90 90 eb d7 e8 ab 4a 45 fd c6 05 10 1a 47 0a 01 90 48 c7 c7 20 31 3d 8b e8 a7 91 09 fd 90 <0f> 0b 90 90 eb b7 e8 8b 4a 45 fd c6 05 ed 19 47 0a 01 90 48 c7 c7
RSP: 0018:ffffc90000a3f888 EFLAGS: 00010246
RAX: 74da3aaac3757600 RBX: 0000000000000003 RCX: ffff88801be99e00
RDX: 0000000000000100 RSI: 0000000000000000 RDI: 0000000000000100
RBP: ffffc90000a3f990 R08: 0000000000000000 R09: 0000000000000100
R10: dffffc0000000000 R11: ffffed101712487b R12: 1ffff92000147f18
R13: ffff88805d5311b8 R14: ffff88805d531020 R15: 0000000000000001
FS:  0000000000000000(0000) GS:ffff888126ef7000(0000) knlGS:0000000000000000
CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
CR2: 0000200000404030 CR3: 000000000d3a6000 CR4: 00000000003526f0
Call Trace:
 <TASK>
 call_timer_fn+0x17e/0x5f0 kernel/time/timer.c:1747
 expire_timers kernel/time/timer.c:1798 [inline]
 __run_timers kernel/time/timer.c:2372 [inline]
 __run_timer_base+0x648/0x970 kernel/time/timer.c:2384
 run_timer_base kernel/time/timer.c:2393 [inline]
 run_timer_softirq+0xb7/0x180 kernel/time/timer.c:2403
 handle_softirqs+0x22f/0x710 kernel/softirq.c:622
 __do_softirq kernel/softirq.c:656 [inline]
 run_ktimerd+0xcf/0x190 kernel/softirq.c:1138
 smpboot_thread_fn+0x542/0xa60 kernel/smpboot.c:160
 kthread+0x711/0x8a0 kernel/kthread.c:463
 ret_from_fork+0x4bc/0x870 arch/x86/kernel/process.c:158
 ret_from_fork_asm+0x1a/0x30 arch/x86/entry/entry_64.S:245
 </TASK>


---
This report is generated by a bot. It may contain errors.
See https://goo.gl/tpsmEJ for more information about syzbot.
syzbot engineers can be reached at syzkaller@googlegroups.com.

syzbot will keep track of this issue. See:
https://goo.gl/tpsmEJ#status for how to communicate with syzbot.

If the report is already addressed, let syzbot know by replying with:
#syz fix: exact-commit-title

If you want to overwrite report's subsystems, reply with:
#syz set subsystems: new-subsystem
(See the list of subsystem names on the web dashboard)

If the report is a duplicate of another one, reply with:
#syz dup: exact-subject-of-another-report

If you want to undo deduplication, reply with:
#syz undup

^ permalink raw reply

* Re: [PATCH v4 10/12] riscv: Implement HAVE_DYNAMIC_FTRACE_WITH_CALL_OPS
From: Conor Dooley @ 2026-02-21 12:15 UTC (permalink / raw)
  To: Andy Chiu
  Cc: linux-riscv, alexghiti, palmer, Puranjay Mohan,
	Björn Töpel, linux-kernel, linux-trace-kernel,
	Alexandre Ghiti, Mark Rutland, paul.walmsley, greentime.hu,
	nick.hu, nylon.chen, eric.lin, vicent.chen, zong.li,
	yongxuan.wang, samuel.holland, olivia.chu, c2232430, arnd,
	Sami Tolvanen, Kees Cook, Nathan Chancellor, llvm, pjw
In-Reply-To: <20250407180838.42877-10-andybnac@gmail.com>

[-- Attachment #1: Type: text/plain, Size: 7719 bytes --]

Hey,

On Tue, Apr 08, 2025 at 02:08:34AM +0800, Andy Chiu wrote:
> From: Puranjay Mohan <puranjay12@gmail.com>
> 
> This patch enables support for DYNAMIC_FTRACE_WITH_CALL_OPS on RISC-V.
> This allows each ftrace callsite to provide an ftrace_ops to the common
> ftrace trampoline, allowing each callsite to invoke distinct tracer
> functions without the need to fall back to list processing or to
> allocate custom trampolines for each callsite. This significantly speeds
> up cases where multiple distinct trace functions are used and callsites
> are mostly traced by a single tracer.
> 
> The idea and most of the implementation is taken from the ARM64's
> implementation of the same feature. The idea is to place a pointer to
> the ftrace_ops as a literal at a fixed offset from the function entry
> point, which can be recovered by the common ftrace trampoline.
> 
> We use -fpatchable-function-entry to reserve 8 bytes above the function
> entry by emitting 2 4 byte or 4 2 byte  nops depending on the presence of
> CONFIG_RISCV_ISA_C. These 8 bytes are patched at runtime with a pointer
> to the associated ftrace_ops for that callsite. Functions are aligned to
> 8 bytes to make sure that the accesses to this literal are atomic.
> 
> This approach allows for directly invoking ftrace_ops::func even for
> ftrace_ops which are dynamically-allocated (or part of a module),
> without going via ftrace_ops_list_func.
> 
> We've benchamrked this with the ftrace_ops sample module on Spacemit K1
> Jupiter:
> 
> Without this patch:
> 
> baseline (Linux rivos 6.14.0-09584-g7d06015d936c #3 SMP Sat Mar 29
> +-----------------------+-----------------+----------------------------+
> |  Number of tracers    | Total time (ns) | Per-call average time      |
> |-----------------------+-----------------+----------------------------|
> | Relevant | Irrelevant |    100000 calls | Total (ns) | Overhead (ns) |
> |----------+------------+-----------------+------------+---------------|
> |        0 |          0 |        1357958 |          13 |             - |
> |        0 |          1 |        1302375 |          13 |             - |
> |        0 |          2 |        1302375 |          13 |             - |
> |        0 |         10 |        1379084 |          13 |             - |
> |        0 |        100 |        1302458 |          13 |             - |
> |        0 |        200 |        1302333 |          13 |             - |
> |----------+------------+-----------------+------------+---------------|
> |        1 |          0 |       13677833 |         136 |           123 |
> |        1 |          1 |       18500916 |         185 |           172 |
> |        1 |          2 |       22856459 |         228 |           215 |
> |        1 |         10 |       58824709 |         588 |           575 |
> |        1 |        100 |      505141584 |        5051 |          5038 |
> |        1 |        200 |     1580473126 |       15804 |         15791 |
> |----------+------------+-----------------+------------+---------------|
> |        1 |          0 |       13561000 |         135 |           122 |
> |        2 |          0 |       19707292 |         197 |           184 |
> |       10 |          0 |       67774750 |         677 |           664 |
> |      100 |          0 |      714123125 |        7141 |          7128 |
> |      200 |          0 |     1918065668 |       19180 |         19167 |
> +----------+------------+-----------------+------------+---------------+
> 
> Note: per-call overhead is estimated relative to the baseline case with
> 0 relevant tracers and 0 irrelevant tracers.
> 
> With this patch:
> 
> v4-rc4 (Linux rivos 6.14.0-09598-gd75747611c93 #4 SMP Sat Mar 29
> +-----------------------+-----------------+----------------------------+
> |  Number of tracers    | Total time (ns) | Per-call average time      |
> |-----------------------+-----------------+----------------------------|
> | Relevant | Irrelevant |    100000 calls | Total (ns) | Overhead (ns) |
> |----------+------------+-----------------+------------+---------------|
> |        0 |          0 |         1459917 |         14 |             - |
> |        0 |          1 |         1408000 |         14 |             - |
> |        0 |          2 |         1383792 |         13 |             - |
> |        0 |         10 |         1430709 |         14 |             - |
> |        0 |        100 |         1383791 |         13 |             - |
> |        0 |        200 |         1383750 |         13 |             - |
> |----------+------------+-----------------+------------+---------------|
> |        1 |          0 |         5238041 |         52 |            38 |
> |        1 |          1 |         5228542 |         52 |            38 |
> |        1 |          2 |         5325917 |         53 |            40 |
> |        1 |         10 |         5299667 |         52 |            38 |
> |        1 |        100 |         5245250 |         52 |            39 |
> |        1 |        200 |         5238459 |         52 |            39 |
> |----------+------------+-----------------+------------+---------------|
> |        1 |          0 |         5239083 |         52 |            38 |
> |        2 |          0 |        19449417 |        194 |           181 |
> |       10 |          0 |        67718584 |        677 |           663 |
> |      100 |          0 |       709840708 |       7098 |          7085 |
> |      200 |          0 |      2203580626 |      22035 |         22022 |
> +----------+------------+-----------------+------------+---------------+
> 
> Note: per-call overhead is estimated relative to the baseline case with
> 0 relevant tracers and 0 irrelevant tracers.
> 
> As can be seen from the above:
> 
>  a) Whenever there is a single relevant tracer function associated with a
>     tracee, the overhead of invoking the tracer is constant, and does not
>     scale with the number of tracers which are *not* associated with that
>     tracee.
> 
>  b) The overhead for a single relevant tracer has dropped to ~1/3 of the
>     overhead prior to this series (from 122ns to 38ns). This is largely
>     due to permitting calls to dynamically-allocated ftrace_ops without
>     going through ftrace_ops_list_func.
> 
> Signed-off-by: Puranjay Mohan <puranjay12@gmail.com>
> 
> [update kconfig, asm, refactor]
> 
> Signed-off-by: Andy Chiu <andybnac@gmail.com>
> Tested-by: Björn Töpel <bjorn@rivosinc.com>

I bisected a boot failure to this commit [c217157bcd1df ("riscv:
Implement HAVE_DYNAMIC_FTRACE_WITH_CALL_OPS")] yesterday, that appears
to be affecting all LLVM versions that I currently have installed. From
some initial testing of Kconfig options, it looks like the issue is
CFI_CLANG related because when I disable CFI_CLANG things work once
more. Since this option depends on !CFI_CLANG, but is def_bool y, I
modified Kconfig to force disable it at all times and tested
!DYNAMIC_FTRACE_WITH_CALL_OPS && !CFG_CLANG, which did boot.

I dunno anything about what's going on in this patch, but so little in
it relates to having DYNAMIC_FTRACE_WITH_CALL_OPS, that I was able to
figure out that the problem is -fpatchable-function-entry=8,4

FWIW, if anyone checks out this commit directly, you'll need to
cherry-pick commit e9d86b8e17e72 ("scripts: Do not strip .rela.dyn
section"), as the base of the branch that c217157bcd1df is on is
v6.15-rc3, which is in itself broken in turn by the issue fixed by
e9d86b8e17e72. Probably not someone anyone will do, but made for an
awful time trying to figure out what commit was at fault!

Cheers,
Conor.

[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 228 bytes --]

^ permalink raw reply

* [syzbot] [trace?] [bpf?] KASAN: slab-use-after-free Read in bpf_trace_run2 (3)
From: syzbot @ 2026-02-21  4:46 UTC (permalink / raw)
  To: andrii, ast, bpf, daniel, eddyz87, haoluo, john.fastabend, jolsa,
	kpsingh, linux-kernel, linux-trace-kernel, martin.lau,
	mathieu.desnoyers, mattbobrowski, mhiramat, rostedt, sdf, song,
	syzkaller-bugs, yonghong.song

Hello,

syzbot found the following issue on:

HEAD commit:    8bf22c33e7a1 Merge tag 'net-7.0-rc1' of git://git.kernel.o..
git tree:       upstream
console output: https://syzkaller.appspot.com/x/log.txt?x=17c8c73a580000
kernel config:  https://syzkaller.appspot.com/x/.config?x=65722f41f7edc17e
dashboard link: https://syzkaller.appspot.com/bug?extid=59701a78e84b0bccfe1b
compiler:       Debian clang version 21.1.8 (++20251221033036+2078da43e25a-1~exp1~20251221153213.50), Debian LLD 21.1.8
syz repro:      https://syzkaller.appspot.com/x/repro.syz?x=141f7ffa580000

Downloadable assets:
disk image (non-bootable): https://storage.googleapis.com/syzbot-assets/d900f083ada3/non_bootable_disk-8bf22c33.raw.xz
vmlinux: https://storage.googleapis.com/syzbot-assets/ee3f5bf71c12/vmlinux-8bf22c33.xz
kernel image: https://storage.googleapis.com/syzbot-assets/ef5b82c2d846/bzImage-8bf22c33.xz

IMPORTANT: if you fix the issue, please add the following tag to the commit:
Reported-by: syzbot+59701a78e84b0bccfe1b@syzkaller.appspotmail.com

==================================================================
BUG: KASAN: slab-use-after-free in __bpf_trace_run kernel/trace/bpf_trace.c:2075 [inline]
BUG: KASAN: slab-use-after-free in bpf_trace_run2+0xb1/0x840 kernel/trace/bpf_trace.c:2129
Read of size 8 at addr ffff888053465b18 by task syz-executor/5307

CPU: 0 UID: 0 PID: 5307 Comm: syz-executor Not tainted syzkaller #0 PREEMPT(full) 
Hardware name: QEMU Standard PC (Q35 + ICH9, 2009), BIOS 1.16.3-debian-1.16.3-2 04/01/2014
Call Trace:
 <TASK>
 dump_stack_lvl+0xe8/0x150 lib/dump_stack.c:120
 print_address_description mm/kasan/report.c:378 [inline]
 print_report+0xba/0x230 mm/kasan/report.c:482
 kasan_report+0x117/0x150 mm/kasan/report.c:595
 __bpf_trace_run kernel/trace/bpf_trace.c:2075 [inline]
 bpf_trace_run2+0xb1/0x840 kernel/trace/bpf_trace.c:2129
 __traceiter_kfree+0x2e/0x50 include/trace/events/kmem.h:97
 __do_trace_kfree include/trace/events/kmem.h:97 [inline]
 trace_kfree include/trace/events/kmem.h:97 [inline]
 kfree+0x5b2/0x630 mm/slub.c:6428
 futex_hash_free+0x65/0xb0 kernel/futex/core.c:1736
 __mmput+0x38d/0x430 kernel/fork.c:1185
 exit_mm+0x168/0x220 kernel/exit.c:581
 do_exit+0x62e/0x2320 kernel/exit.c:959
 do_group_exit+0x21b/0x2d0 kernel/exit.c:1112
 get_signal+0x1284/0x1330 kernel/signal.c:3034
 arch_do_signal_or_restart+0xbc/0x830 arch/x86/kernel/signal.c:337
 __exit_to_user_mode_loop kernel/entry/common.c:64 [inline]
 exit_to_user_mode_loop+0x86/0x480 kernel/entry/common.c:98
 __exit_to_user_mode_prepare include/linux/irq-entry-common.h:226 [inline]
 syscall_exit_to_user_mode_prepare include/linux/irq-entry-common.h:256 [inline]
 syscall_exit_to_user_mode include/linux/entry-common.h:325 [inline]
 do_syscall_64+0x32d/0xf80 arch/x86/entry/syscall_64.c:100
 entry_SYSCALL_64_after_hwframe+0x77/0x7f
RIP: 0033:0x7fc4bbd57817
Code: Unable to access opcode bytes at 0x7fc4bbd577ed.
RSP: 002b:00007ffe1f8e8350 EFLAGS: 00000202 ORIG_RAX: 0000000000000001
RAX: 00000000000000b0 RBX: 0000555576290500 RCX: 00007fc4bbd57817
RDX: 00000000000000b0 RSI: 00007fc4b863ff50 RDI: 0000000000000003
RBP: 0000000000000000 R08: 0000000000000000 R09: 0000000000000000
R10: 0000000000000000 R11: 0000000000000202 R12: 00000000000000b0
R13: 00005555762a51c0 R14: 00007ffe1f8e88f0 R15: 00007fc4b863ff50
 </TASK>

Allocated by task 6102:
 kasan_save_stack mm/kasan/common.c:57 [inline]
 kasan_save_track+0x3e/0x80 mm/kasan/common.c:78
 poison_kmalloc_redzone mm/kasan/common.c:398 [inline]
 __kasan_kmalloc+0x93/0xb0 mm/kasan/common.c:415
 kasan_kmalloc include/linux/kasan.h:263 [inline]
 __kmalloc_cache_noprof+0x31c/0x660 mm/slub.c:5339
 kmalloc_noprof include/linux/slab.h:962 [inline]
 kzalloc_noprof include/linux/slab.h:1204 [inline]
 bpf_raw_tp_link_attach+0x278/0x700 kernel/bpf/syscall.c:4264
 bpf_raw_tracepoint_open+0x1b2/0x220 kernel/bpf/syscall.c:4312
 __sys_bpf+0x846/0x950 kernel/bpf/syscall.c:6271
 __do_sys_bpf kernel/bpf/syscall.c:6342 [inline]
 __se_sys_bpf kernel/bpf/syscall.c:6340 [inline]
 __x64_sys_bpf+0x7c/0x90 kernel/bpf/syscall.c:6340
 do_syscall_x64 arch/x86/entry/syscall_64.c:63 [inline]
 do_syscall_64+0x14d/0xf80 arch/x86/entry/syscall_64.c:94
 entry_SYSCALL_64_after_hwframe+0x77/0x7f

Freed by task 1100:
 kasan_save_stack mm/kasan/common.c:57 [inline]
 kasan_save_track+0x3e/0x80 mm/kasan/common.c:78
 kasan_save_free_info+0x46/0x50 mm/kasan/generic.c:584
 poison_slab_object mm/kasan/common.c:253 [inline]
 __kasan_slab_free+0x5c/0x80 mm/kasan/common.c:285
 kasan_slab_free include/linux/kasan.h:235 [inline]
 slab_free_hook mm/slub.c:2687 [inline]
 slab_free mm/slub.c:6124 [inline]
 kfree+0x1c1/0x630 mm/slub.c:6442
 rcu_do_batch kernel/rcu/tree.c:2617 [inline]
 rcu_core+0x7cd/0x1070 kernel/rcu/tree.c:2869
 handle_softirqs+0x22a/0x870 kernel/softirq.c:622
 do_softirq+0x76/0xd0 kernel/softirq.c:523
 __local_bh_enable_ip+0xf8/0x130 kernel/softirq.c:450
 spin_unlock_bh include/linux/spinlock.h:395 [inline]
 batadv_iv_ogm_queue_add+0x73e/0xd30 net/batman-adv/bat_iv_ogm.c:668
 batadv_iv_ogm_schedule_buff net/batman-adv/bat_iv_ogm.c:841 [inline]
 batadv_iv_ogm_schedule+0x874/0xf50 net/batman-adv/bat_iv_ogm.c:873
 batadv_iv_send_outstanding_bat_ogm_packet+0x6c8/0x7e0 net/batman-adv/bat_iv_ogm.c:1709
 process_one_work kernel/workqueue.c:3275 [inline]
 process_scheduled_works+0xb02/0x1830 kernel/workqueue.c:3358
 worker_thread+0xa50/0xfc0 kernel/workqueue.c:3439
 kthread+0x388/0x470 kernel/kthread.c:467
 ret_from_fork+0x51e/0xb90 arch/x86/kernel/process.c:158
 ret_from_fork_asm+0x1a/0x30 arch/x86/entry/entry_64.S:245

Last potentially related work creation:
 kasan_save_stack+0x3e/0x60 mm/kasan/common.c:57
 kasan_record_aux_stack+0xbd/0xd0 mm/kasan/generic.c:556
 __call_rcu_common kernel/rcu/tree.c:3131 [inline]
 call_rcu+0xee/0x890 kernel/rcu/tree.c:3251
 bpf_link_put_direct kernel/bpf/syscall.c:3323 [inline]
 bpf_link_release+0x6b/0x80 kernel/bpf/syscall.c:3330
 __fput+0x44f/0xa70 fs/file_table.c:469
 task_work_run+0x1d9/0x270 kernel/task_work.c:233
 resume_user_mode_work include/linux/resume_user_mode.h:50 [inline]
 __exit_to_user_mode_loop kernel/entry/common.c:67 [inline]
 exit_to_user_mode_loop+0xed/0x480 kernel/entry/common.c:98
 __exit_to_user_mode_prepare include/linux/irq-entry-common.h:226 [inline]
 syscall_exit_to_user_mode_prepare include/linux/irq-entry-common.h:256 [inline]
 syscall_exit_to_user_mode include/linux/entry-common.h:325 [inline]
 do_syscall_64+0x32d/0xf80 arch/x86/entry/syscall_64.c:100
 entry_SYSCALL_64_after_hwframe+0x77/0x7f

The buggy address belongs to the object at ffff888053465b00
 which belongs to the cache kmalloc-192 of size 192
The buggy address is located 24 bytes inside of
 freed 192-byte region [ffff888053465b00, ffff888053465bc0)

The buggy address belongs to the physical page:
page: refcount:0 mapcount:0 mapping:0000000000000000 index:0x0 pfn:0x53465
flags: 0x4fff00000000000(node=1|zone=1|lastcpupid=0x7ff)
page_type: f5(slab)
raw: 04fff00000000000 ffff88801a8413c0 dead000000000100 dead000000000122
raw: 0000000000000000 0000000000100010 00000000f5000000 0000000000000000
page dumped because: kasan: bad access detected
page_owner tracks the page as allocated
page last allocated via order 0, migratetype Unmovable, gfp_mask 0xd2cc0(GFP_KERNEL|__GFP_NOWARN|__GFP_NORETRY|__GFP_COMP|__GFP_NOMEMALLOC), pid 5436, tgid 5436 (syz-executor), ts 104947827691, free_ts 103695571452
 set_page_owner include/linux/page_owner.h:32 [inline]
 post_alloc_hook+0x231/0x280 mm/page_alloc.c:1889
 prep_new_page mm/page_alloc.c:1897 [inline]
 get_page_from_freelist+0x24dc/0x2580 mm/page_alloc.c:3962
 __alloc_frozen_pages_noprof+0x18d/0x380 mm/page_alloc.c:5250
 alloc_slab_page mm/slub.c:3255 [inline]
 allocate_slab+0x77/0x660 mm/slub.c:3444
 new_slab mm/slub.c:3502 [inline]
 refill_objects+0x331/0x3c0 mm/slub.c:7134
 refill_sheaf mm/slub.c:2804 [inline]
 __pcs_replace_empty_main+0x2b9/0x620 mm/slub.c:4578
 alloc_from_pcs mm/slub.c:4681 [inline]
 slab_alloc_node mm/slub.c:4815 [inline]
 __do_kmalloc_node mm/slub.c:5218 [inline]
 __kmalloc_node_track_caller_noprof+0x572/0x7b0 mm/slub.c:5327
 kmemdup_noprof+0x2b/0x70 mm/util.c:138
 kmemdup_noprof include/linux/fortify-string.h:763 [inline]
 neigh_parms_alloc+0x87/0x510 net/core/neighbour.c:1772
 ipv6_add_dev+0x40d/0x13a0 net/ipv6/addrconf.c:403
 addrconf_notify+0x771/0x1050 net/ipv6/addrconf.c:3655
 notifier_call_chain+0x1be/0x400 kernel/notifier.c:85
 call_netdevice_notifiers_extack net/core/dev.c:2287 [inline]
 call_netdevice_notifiers net/core/dev.c:2301 [inline]
 register_netdevice+0x173a/0x1cf0 net/core/dev.c:11451
 ip6gre_newlink_common+0x403/0x5b0 net/ipv6/ip6_gre.c:1965
 ip6gre_newlink+0x228/0x350 net/ipv6/ip6_gre.c:1998
 rtnl_newlink_create+0x329/0xb70 net/core/rtnetlink.c:3840
page last free pid 5378 tgid 5378 stack trace:
 reset_page_owner include/linux/page_owner.h:25 [inline]
 __free_pages_prepare mm/page_alloc.c:1433 [inline]
 __free_frozen_pages+0xc2b/0xdb0 mm/page_alloc.c:2978
 vfree+0x25a/0x400 mm/vmalloc.c:3479
 kcov_put kernel/kcov.c:442 [inline]
 kcov_close+0x28/0x50 kernel/kcov.c:543
 __fput+0x44f/0xa70 fs/file_table.c:469
 task_work_run+0x1d9/0x270 kernel/task_work.c:233
 exit_task_work include/linux/task_work.h:40 [inline]
 do_exit+0x69b/0x2320 kernel/exit.c:971
 do_group_exit+0x21b/0x2d0 kernel/exit.c:1112
 get_signal+0x1284/0x1330 kernel/signal.c:3034
 arch_do_signal_or_restart+0xbc/0x830 arch/x86/kernel/signal.c:337
 __exit_to_user_mode_loop kernel/entry/common.c:64 [inline]
 exit_to_user_mode_loop+0x86/0x480 kernel/entry/common.c:98
 __exit_to_user_mode_prepare include/linux/irq-entry-common.h:226 [inline]
 syscall_exit_to_user_mode_prepare include/linux/irq-entry-common.h:256 [inline]
 syscall_exit_to_user_mode include/linux/entry-common.h:325 [inline]
 do_syscall_64+0x32d/0xf80 arch/x86/entry/syscall_64.c:100
 entry_SYSCALL_64_after_hwframe+0x77/0x7f

Memory state around the buggy address:
 ffff888053465a00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
 ffff888053465a80: 00 00 00 fc fc fc fc fc fc fc fc fc fc fc fc fc
>ffff888053465b00: fa fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb
                            ^
 ffff888053465b80: fb fb fb fb fb fb fb fb fc fc fc fc fc fc fc fc
 ffff888053465c00: fa fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb
==================================================================


---
This report is generated by a bot. It may contain errors.
See https://goo.gl/tpsmEJ for more information about syzbot.
syzbot engineers can be reached at syzkaller@googlegroups.com.

syzbot will keep track of this issue. See:
https://goo.gl/tpsmEJ#status for how to communicate with syzbot.

If the report is already addressed, let syzbot know by replying with:
#syz fix: exact-commit-title

If you want syzbot to run the reproducer, reply with:
#syz test: git://repo/address.git branch-or-commit-hash
If you attach or paste a git patch, syzbot will apply it before testing.

If you want to overwrite report's subsystems, reply with:
#syz set subsystems: new-subsystem
(See the list of subsystem names on the web dashboard)

If the report is a duplicate of another one, reply with:
#syz dup: exact-subject-of-another-report

If you want to undo deduplication, reply with:
#syz undup

^ permalink raw reply

* [PATCH 2/2] fgraph: fix thresh_return nosleeptime double-adjust
From: hu.shengming @ 2026-02-21  3:33 UTC (permalink / raw)
  To: hu.shengming
  Cc: rostedt, mhiramat, mathieu.desnoyers, linux-kernel,
	linux-trace-kernel, zhang.run, yang.tao172, yang.yang29
In-Reply-To: <202602211116123227p4Je6QELxr2byqvy9kTE@zte.com.cn>

From: Shengming Hu <hu.shengming@zte.com.cn>

trace_graph_thresh_return() called handle_nosleeptime() and then
delegated to trace_graph_return(), which calls
handle_nosleeptime() again. When sleep-time accounting is
disabled this double-adjusts calltime and can produce bogus
durations (including underflow).

Fix this by computing rettime once, applying
handle_nosleeptime() only once, using the adjusted calltime
for threshold comparison, and writing the return event
directly via __trace_graph_return() when the threshold is met.

Fixes: 3c9880f3ab52b ("ftrace: Use a running sleeptime instead of
saving on shadow stack")
Signed-off-by: Shengming Hu <hu.shengming@zte.com.cn>
---
 kernel/trace/trace_functions_graph.c | 14 ++++++++++----
 1 file changed, 10 insertions(+), 4 deletions(-)

diff --git a/kernel/trace/trace_functions_graph.c b/kernel/trace/trace_functions_graph.c
index cbe43680c..b9c81fbd9 100644
--- a/kernel/trace/trace_functions_graph.c
+++ b/kernel/trace/trace_functions_graph.c
@@ -403,8 +403,12 @@ static void trace_graph_thresh_return(struct ftrace_graph_ret *trace,
 	unsigned long *task_var = fgraph_get_task_var(gops);
 	struct fgraph_times *ftimes;
 	struct trace_array *tr;
+	unsigned int trace_ctx;
+	u64 calltime, rettime;
 	int size;

+	rettime = trace_clock_local();
+
 	ftrace_graph_addr_finish(gops, trace);

 	if (*task_var & TRACE_GRAPH_NOTRACE) {
@@ -419,11 +423,13 @@ static void trace_graph_thresh_return(struct ftrace_graph_ret *trace,
 	tr = gops->private;
 	handle_nosleeptime(tr, trace, ftimes, size);

-	if (tracing_thresh &&
-	    (trace_clock_local() - ftimes->calltime < tracing_thresh))
+	calltime = ftimes->calltime;
+
+	if (tracing_thresh && (rettime - calltime < tracing_thresh))
 		return;
-	else
-		trace_graph_return(trace, gops, fregs);
+
+	trace_ctx = tracing_gen_ctx();
+	__trace_graph_return(tr, trace, trace_ctx, calltime, rettime);
 }

 static struct fgraph_ops funcgraph_ops = {
-- 
2.25.1

^ permalink raw reply related

* [PATCH 1/2] fgraph: fix thresh_return clear per-task notrace
From: hu.shengming @ 2026-02-21  3:30 UTC (permalink / raw)
  To: hu.shengming
  Cc: rostedt, mhiramat, mathieu.desnoyers, linux-kernel,
	linux-trace-kernel, zhang.run, yang.tao172, yang.yang29
In-Reply-To: <202602211116123227p4Je6QELxr2byqvy9kTE@zte.com.cn>

From: Shengming Hu <hu.shengming@zte.com.cn>

When tracing_thresh is enabled, function graph tracing uses
trace_graph_thresh_return() as the return handler. Unlike
trace_graph_return(), it did not clear the per-task
TRACE_GRAPH_NOTRACE flag set by the entry handler for
set_graph_notrace addresses. This could leave the task
permanently in "notrace" state and effectively disable
function graph tracing for that task.

Mirror trace_graph_return()'s per-task notrace handling by
clearing TRACE_GRAPH_NOTRACE and returning early when set.

Fixes: b84214890a9bc ("function_graph: Move graph notrace bit to
shadow stack global var")
Signed-off-by: Shengming Hu <hu.shengming@zte.com.cn>
---
 kernel/trace/trace_functions_graph.c | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/kernel/trace/trace_functions_graph.c b/kernel/trace/trace_functions_graph.c
index 1de6f1573..cbe43680c 100644
--- a/kernel/trace/trace_functions_graph.c
+++ b/kernel/trace/trace_functions_graph.c
@@ -400,14 +400,15 @@ static void trace_graph_thresh_return(struct ftrace_graph_ret *trace,
 				      struct fgraph_ops *gops,
 				      struct ftrace_regs *fregs)
 {
+	unsigned long *task_var = fgraph_get_task_var(gops);
 	struct fgraph_times *ftimes;
 	struct trace_array *tr;
 	int size;

 	ftrace_graph_addr_finish(gops, trace);

-	if (trace_recursion_test(TRACE_GRAPH_NOTRACE_BIT)) {
-		trace_recursion_clear(TRACE_GRAPH_NOTRACE_BIT);
+	if (*task_var & TRACE_GRAPH_NOTRACE) {
+		*task_var &= ~TRACE_GRAPH_NOTRACE;
 		return;
 	}

-- 
2.25.1

^ permalink raw reply related

* [PATCH 0/2] fgraph: fixes for thresh_return return handler
From: hu.shengming @ 2026-02-21  3:16 UTC (permalink / raw)
  To: rostedt, mhiramat
  Cc: mathieu.desnoyers, linux-kernel, linux-trace-kernel, zhang.run,
	yang.tao172, yang.yang29, hu.shengming


[-- Attachment #1.1.1: Type: text/plain, Size: 729 bytes --]

From: Shengming Hu <hu.shengming@zte.com.cn>

Hi,

This series fixes two issues in trace_graph_thresh_return(), the function
graph return handler used when tracing_thresh is enabled:

PATCH1: Clear the per-task TRACE_GRAPH_NOTRACE state like
trace_graph_return() does.
PATCH2: Avoid double no-sleep-time adjustment by emitting
the return event directly when the threshold is met.

Patch details are in the individual commit messages.

Shengming Hu (2):
  [PATCH 1/2] fgraph: fix thresh_return clear per-task notrace
  [PATCH 2/2] fgraph: fix thresh_return nosleeptime double-adjust

 kernel/trace/trace_functions_graph.c | 19 +++++++++++++------
 1 file changed, 13 insertions(+), 6 deletions(-)

-- 
2.25.1

[-- Attachment #1.1.2: Type: text/html , Size: 1619 bytes --]

^ permalink raw reply

* Re: [PATCH bpf-next 04/17] bpf: Add struct bpf_tramp_node object
From: kernel test robot @ 2026-02-21  3:00 UTC (permalink / raw)
  To: Jiri Olsa, Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko
  Cc: oe-kbuild-all, bpf, linux-trace-kernel, Martin KaFai Lau,
	Eduard Zingerman, Song Liu, Yonghong Song, Menglong Dong,
	Steven Rostedt
In-Reply-To: <20260220100649.628307-5-jolsa@kernel.org>

Hi Jiri,

kernel test robot noticed the following build errors:

[auto build test ERROR on bpf-next/master]

url:    https://github.com/intel-lab-lkp/linux/commits/Jiri-Olsa/ftrace-Add-ftrace_hash_count-function/20260220-181324
base:   https://git.kernel.org/pub/scm/linux/kernel/git/bpf/bpf-next.git master
patch link:    https://lore.kernel.org/r/20260220100649.628307-5-jolsa%40kernel.org
patch subject: [PATCH bpf-next 04/17] bpf: Add struct bpf_tramp_node object
config: riscv-randconfig-001-20260221 (https://download.01.org/0day-ci/archive/20260221/202602211023.EiuS4wkF-lkp@intel.com/config)
compiler: riscv64-linux-gcc (GCC) 8.5.0
reproduce (this is a W=1 build): (https://download.01.org/0day-ci/archive/20260221/202602211023.EiuS4wkF-lkp@intel.com/reproduce)

If you fix the issue in a separate patch/commit (i.e. not just a new version of
the same patch/commit), kindly add following tags
| Reported-by: kernel test robot <lkp@intel.com>
| Closes: https://lore.kernel.org/oe-kbuild-all/202602211023.EiuS4wkF-lkp@intel.com/

All errors (new ones prefixed by >>):

   arch/riscv/net/bpf_jit_comp64.c: In function 'invoke_bpf_prog':
>> arch/riscv/net/bpf_jit_comp64.c:944:7: error: 'struct bpf_tramp_link' has no member named 'cookie'
     if (l->cookie)
          ^~
   arch/riscv/net/bpf_jit_comp64.c:945:65: error: 'struct bpf_tramp_link' has no member named 'cookie'
      emit_store_stack_imm64(RV_REG_T1, -run_ctx_off + cookie_off, l->cookie, ctx);
                                                                    ^~
   arch/riscv/net/bpf_jit_comp64.c: At top level:
   arch/riscv/net/bpf_jit_comp64.c:999:30: warning: 'struct bpf_tramp_links' declared inside parameter list will not be visible outside of this definition or declaration
    static int invoke_bpf(struct bpf_tramp_links *tl, int args_off, int retval_off,
                                 ^~~~~~~~~~~~~~~
   arch/riscv/net/bpf_jit_comp64.c: In function 'invoke_bpf':
>> arch/riscv/net/bpf_jit_comp64.c:1005:20: error: dereferencing pointer to incomplete type 'struct bpf_tramp_links'
     for (i = 0; i < tl->nr_links; i++) {
                       ^~
   arch/riscv/net/bpf_jit_comp64.c: At top level:
   arch/riscv/net/bpf_jit_comp64.c:1024:14: warning: 'struct bpf_tramp_links' declared inside parameter list will not be visible outside of this definition or declaration
          struct bpf_tramp_links *tlinks,
                 ^~~~~~~~~~~~~~~
   arch/riscv/net/bpf_jit_comp64.c: In function '__arch_prepare_bpf_trampoline':
>> arch/riscv/net/bpf_jit_comp64.c:1033:42: error: invalid use of undefined type 'struct bpf_tramp_links'
     struct bpf_tramp_links *fentry = &tlinks[BPF_TRAMP_FENTRY];
                                             ^
   arch/riscv/net/bpf_jit_comp64.c:1033:42: error: dereferencing pointer to incomplete type 'struct bpf_tramp_links'
   arch/riscv/net/bpf_jit_comp64.c:1034:41: error: invalid use of undefined type 'struct bpf_tramp_links'
     struct bpf_tramp_links *fexit = &tlinks[BPF_TRAMP_FEXIT];
                                            ^
   arch/riscv/net/bpf_jit_comp64.c:1035:44: error: invalid use of undefined type 'struct bpf_tramp_links'
     struct bpf_tramp_links *fmod_ret = &tlinks[BPF_TRAMP_MODIFY_RETURN];
                                               ^
   arch/riscv/net/bpf_jit_comp64.c:1118:39: error: passing argument 1 of 'bpf_fsession_cookie_cnt' from incompatible pointer type [-Werror=incompatible-pointer-types]
     cookie_cnt = bpf_fsession_cookie_cnt(tlinks);
                                          ^~~~~~
   In file included from arch/riscv/net/bpf_jit_comp64.c:9:
   include/linux/bpf.h:2207:67: note: expected 'struct bpf_tramp_nodes *' but argument is of type 'struct bpf_tramp_links *'
    static inline int bpf_fsession_cookie_cnt(struct bpf_tramp_nodes *nodes)
                                              ~~~~~~~~~~~~~~~~~~~~~~~~^~~~~
   arch/riscv/net/bpf_jit_comp64.c:1175:23: error: passing argument 1 of 'bpf_fsession_cnt' from incompatible pointer type [-Werror=incompatible-pointer-types]
     if (bpf_fsession_cnt(tlinks)) {
                          ^~~~~~
   In file included from arch/riscv/net/bpf_jit_comp64.c:9:
   include/linux/bpf.h:2189:60: note: expected 'struct bpf_tramp_nodes *' but argument is of type 'struct bpf_tramp_links *'
    static inline int bpf_fsession_cnt(struct bpf_tramp_nodes *nodes)
                                       ~~~~~~~~~~~~~~~~~~~~~~~~^~~~~
   arch/riscv/net/bpf_jit_comp64.c:1191:20: error: passing argument 1 of 'invoke_bpf' from incompatible pointer type [-Werror=incompatible-pointer-types]
      ret = invoke_bpf(fentry, args_off, retval_off, run_ctx_off, func_meta_off,
                       ^~~~~~
   arch/riscv/net/bpf_jit_comp64.c:999:47: note: expected 'struct bpf_tramp_links *' but argument is of type 'struct bpf_tramp_links *'
    static int invoke_bpf(struct bpf_tramp_links *tl, int args_off, int retval_off,
                          ~~~~~~~~~~~~~~~~~~~~~~~~^~
   arch/riscv/net/bpf_jit_comp64.c:1198:16: warning: assignment to 'int *' from 'int' makes pointer from integer without a cast [-Wint-conversion]
      branches_off = kcalloc(fmod_ret->nr_links, sizeof(int), GFP_KERNEL);
                   ^
   arch/riscv/net/bpf_jit_comp64.c:1241:23: error: passing argument 1 of 'bpf_fsession_cnt' from incompatible pointer type [-Werror=incompatible-pointer-types]
     if (bpf_fsession_cnt(tlinks))
                          ^~~~~~
   In file included from arch/riscv/net/bpf_jit_comp64.c:9:
   include/linux/bpf.h:2189:60: note: expected 'struct bpf_tramp_nodes *' but argument is of type 'struct bpf_tramp_links *'
    static inline int bpf_fsession_cnt(struct bpf_tramp_nodes *nodes)
                                       ~~~~~~~~~~~~~~~~~~~~~~~~^~~~~
   arch/riscv/net/bpf_jit_comp64.c:1245:20: error: passing argument 1 of 'invoke_bpf' from incompatible pointer type [-Werror=incompatible-pointer-types]
      ret = invoke_bpf(fexit, args_off, retval_off, run_ctx_off, func_meta_off,
                       ^~~~~
   arch/riscv/net/bpf_jit_comp64.c:999:47: note: expected 'struct bpf_tramp_links *' but argument is of type 'struct bpf_tramp_links *'
    static int invoke_bpf(struct bpf_tramp_links *tl, int args_off, int retval_off,
                          ~~~~~~~~~~~~~~~~~~~~~~~~^~
   arch/riscv/net/bpf_jit_comp64.c: At top level:
   arch/riscv/net/bpf_jit_comp64.c:1308:16: warning: 'struct bpf_tramp_links' declared inside parameter list will not be visible outside of this definition or declaration
            struct bpf_tramp_links *tlinks, void *func_addr)
                   ^~~~~~~~~~~~~~~
   arch/riscv/net/bpf_jit_comp64.c:1307:5: error: conflicting types for 'arch_bpf_trampoline_size'
    int arch_bpf_trampoline_size(const struct btf_func_model *m, u32 flags,
        ^~~~~~~~~~~~~~~~~~~~~~~~
   In file included from arch/riscv/net/bpf_jit_comp64.c:9:
   include/linux/bpf.h:1271:5: note: previous declaration of 'arch_bpf_trampoline_size' was here
    int arch_bpf_trampoline_size(const struct btf_func_model *m, u32 flags,
        ^~~~~~~~~~~~~~~~~~~~~~~~
   arch/riscv/net/bpf_jit_comp64.c: In function 'arch_bpf_trampoline_size':
   arch/riscv/net/bpf_jit_comp64.c:1317:46: error: passing argument 3 of '__arch_prepare_bpf_trampoline' from incompatible pointer type [-Werror=incompatible-pointer-types]
     ret = __arch_prepare_bpf_trampoline(&im, m, tlinks, func_addr, flags, &ctx);
                                                 ^~~~~~
   arch/riscv/net/bpf_jit_comp64.c:1024:31: note: expected 'struct bpf_tramp_links *' but argument is of type 'struct bpf_tramp_links *'
          struct bpf_tramp_links *tlinks,
          ~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~
   arch/riscv/net/bpf_jit_comp64.c: At top level:
   arch/riscv/net/bpf_jit_comp64.c:1334:23: warning: 'struct bpf_tramp_links' declared inside parameter list will not be visible outside of this definition or declaration
        u32 flags, struct bpf_tramp_links *tlinks,
                          ^~~~~~~~~~~~~~~
   arch/riscv/net/bpf_jit_comp64.c:1332:5: error: conflicting types for 'arch_prepare_bpf_trampoline'
    int arch_prepare_bpf_trampoline(struct bpf_tramp_image *im, void *ro_image,
        ^~~~~~~~~~~~~~~~~~~~~~~~~~~
   In file included from arch/riscv/net/bpf_jit_comp64.c:9:
   include/linux/bpf.h:1264:5: note: previous declaration of 'arch_prepare_bpf_trampoline' was here
    int arch_prepare_bpf_trampoline(struct bpf_tramp_image *im, void *image, void *image_end,
        ^~~~~~~~~~~~~~~~~~~~~~~~~~~
   arch/riscv/net/bpf_jit_comp64.c: In function 'arch_prepare_bpf_trampoline':
   arch/riscv/net/bpf_jit_comp64.c:1349:45: error: passing argument 3 of '__arch_prepare_bpf_trampoline' from incompatible pointer type [-Werror=incompatible-pointer-types]
     ret = __arch_prepare_bpf_trampoline(im, m, tlinks, func_addr, flags, &ctx);
                                                ^~~~~~
   arch/riscv/net/bpf_jit_comp64.c:1024:31: note: expected 'struct bpf_tramp_links *' but argument is of type 'struct bpf_tramp_links *'
          struct bpf_tramp_links *tlinks,
          ~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~
   cc1: some warnings being treated as errors


vim +944 arch/riscv/net/bpf_jit_comp64.c

93fd420d71beed5 Menglong Dong  2026-02-08   936  
49b5e77ae3e214a Pu Lehui       2023-02-15   937  static int invoke_bpf_prog(struct bpf_tramp_link *l, int args_off, int retval_off,
49b5e77ae3e214a Pu Lehui       2023-02-15   938  			   int run_ctx_off, bool save_ret, struct rv_jit_context *ctx)
49b5e77ae3e214a Pu Lehui       2023-02-15   939  {
49b5e77ae3e214a Pu Lehui       2023-02-15   940  	int ret, branch_off;
49b5e77ae3e214a Pu Lehui       2023-02-15   941  	struct bpf_prog *p = l->link.prog;
49b5e77ae3e214a Pu Lehui       2023-02-15   942  	int cookie_off = offsetof(struct bpf_tramp_run_ctx, bpf_cookie);
49b5e77ae3e214a Pu Lehui       2023-02-15   943  
93fd420d71beed5 Menglong Dong  2026-02-08  @944  	if (l->cookie)
93fd420d71beed5 Menglong Dong  2026-02-08   945  		emit_store_stack_imm64(RV_REG_T1, -run_ctx_off + cookie_off, l->cookie, ctx);
93fd420d71beed5 Menglong Dong  2026-02-08   946  	else
49b5e77ae3e214a Pu Lehui       2023-02-15   947  		emit_sd(RV_REG_FP, -run_ctx_off + cookie_off, RV_REG_ZERO, ctx);
49b5e77ae3e214a Pu Lehui       2023-02-15   948  
49b5e77ae3e214a Pu Lehui       2023-02-15   949  	/* arg1: prog */
49b5e77ae3e214a Pu Lehui       2023-02-15   950  	emit_imm(RV_REG_A0, (const s64)p, ctx);
49b5e77ae3e214a Pu Lehui       2023-02-15   951  	/* arg2: &run_ctx */
49b5e77ae3e214a Pu Lehui       2023-02-15   952  	emit_addi(RV_REG_A1, RV_REG_FP, -run_ctx_off, ctx);
49b5e77ae3e214a Pu Lehui       2023-02-15   953  	ret = emit_call((const u64)bpf_trampoline_enter(p), true, ctx);
49b5e77ae3e214a Pu Lehui       2023-02-15   954  	if (ret)
49b5e77ae3e214a Pu Lehui       2023-02-15   955  		return ret;
49b5e77ae3e214a Pu Lehui       2023-02-15   956  
10541b374aa05c8 Xu Kuohai      2024-04-16   957  	/* store prog start time */
10541b374aa05c8 Xu Kuohai      2024-04-16   958  	emit_mv(RV_REG_S1, RV_REG_A0, ctx);
10541b374aa05c8 Xu Kuohai      2024-04-16   959  
49b5e77ae3e214a Pu Lehui       2023-02-15   960  	/* if (__bpf_prog_enter(prog) == 0)
49b5e77ae3e214a Pu Lehui       2023-02-15   961  	 *	goto skip_exec_of_prog;
49b5e77ae3e214a Pu Lehui       2023-02-15   962  	 */
49b5e77ae3e214a Pu Lehui       2023-02-15   963  	branch_off = ctx->ninsns;
49b5e77ae3e214a Pu Lehui       2023-02-15   964  	/* nop reserved for conditional jump */
49b5e77ae3e214a Pu Lehui       2023-02-15   965  	emit(rv_nop(), ctx);
49b5e77ae3e214a Pu Lehui       2023-02-15   966  
49b5e77ae3e214a Pu Lehui       2023-02-15   967  	/* arg1: &args_off */
49b5e77ae3e214a Pu Lehui       2023-02-15   968  	emit_addi(RV_REG_A0, RV_REG_FP, -args_off, ctx);
49b5e77ae3e214a Pu Lehui       2023-02-15   969  	if (!p->jited)
49b5e77ae3e214a Pu Lehui       2023-02-15   970  		/* arg2: progs[i]->insnsi for interpreter */
49b5e77ae3e214a Pu Lehui       2023-02-15   971  		emit_imm(RV_REG_A1, (const s64)p->insnsi, ctx);
49b5e77ae3e214a Pu Lehui       2023-02-15   972  	ret = emit_call((const u64)p->bpf_func, true, ctx);
49b5e77ae3e214a Pu Lehui       2023-02-15   973  	if (ret)
49b5e77ae3e214a Pu Lehui       2023-02-15   974  		return ret;
49b5e77ae3e214a Pu Lehui       2023-02-15   975  
7112cd26e606c7b Björn Töpel    2023-10-04   976  	if (save_ret) {
7112cd26e606c7b Björn Töpel    2023-10-04   977  		emit_sd(RV_REG_FP, -retval_off, RV_REG_A0, ctx);
7112cd26e606c7b Björn Töpel    2023-10-04   978  		emit_sd(RV_REG_FP, -(retval_off - 8), regmap[BPF_REG_0], ctx);
7112cd26e606c7b Björn Töpel    2023-10-04   979  	}
49b5e77ae3e214a Pu Lehui       2023-02-15   980  
49b5e77ae3e214a Pu Lehui       2023-02-15   981  	/* update branch with beqz */
49b5e77ae3e214a Pu Lehui       2023-02-15   982  	if (ctx->insns) {
49b5e77ae3e214a Pu Lehui       2023-02-15   983  		int offset = ninsns_rvoff(ctx->ninsns - branch_off);
49b5e77ae3e214a Pu Lehui       2023-02-15   984  		u32 insn = rv_beq(RV_REG_A0, RV_REG_ZERO, offset >> 1);
49b5e77ae3e214a Pu Lehui       2023-02-15   985  		*(u32 *)(ctx->insns + branch_off) = insn;
49b5e77ae3e214a Pu Lehui       2023-02-15   986  	}
49b5e77ae3e214a Pu Lehui       2023-02-15   987  
49b5e77ae3e214a Pu Lehui       2023-02-15   988  	/* arg1: prog */
49b5e77ae3e214a Pu Lehui       2023-02-15   989  	emit_imm(RV_REG_A0, (const s64)p, ctx);
49b5e77ae3e214a Pu Lehui       2023-02-15   990  	/* arg2: prog start time */
49b5e77ae3e214a Pu Lehui       2023-02-15   991  	emit_mv(RV_REG_A1, RV_REG_S1, ctx);
49b5e77ae3e214a Pu Lehui       2023-02-15   992  	/* arg3: &run_ctx */
49b5e77ae3e214a Pu Lehui       2023-02-15   993  	emit_addi(RV_REG_A2, RV_REG_FP, -run_ctx_off, ctx);
49b5e77ae3e214a Pu Lehui       2023-02-15   994  	ret = emit_call((const u64)bpf_trampoline_exit(p), true, ctx);
49b5e77ae3e214a Pu Lehui       2023-02-15   995  
49b5e77ae3e214a Pu Lehui       2023-02-15   996  	return ret;
49b5e77ae3e214a Pu Lehui       2023-02-15   997  }
49b5e77ae3e214a Pu Lehui       2023-02-15   998  
35b3515be0ecb9d Menglong Dong  2026-02-08   999  static int invoke_bpf(struct bpf_tramp_links *tl, int args_off, int retval_off,
35b3515be0ecb9d Menglong Dong  2026-02-08  1000  		      int run_ctx_off, int func_meta_off, bool save_ret, u64 func_meta,
35b3515be0ecb9d Menglong Dong  2026-02-08  1001  		      int cookie_off, struct rv_jit_context *ctx)
35b3515be0ecb9d Menglong Dong  2026-02-08  1002  {
35b3515be0ecb9d Menglong Dong  2026-02-08  1003  	int i, cur_cookie = (cookie_off - args_off) / 8;
35b3515be0ecb9d Menglong Dong  2026-02-08  1004  
35b3515be0ecb9d Menglong Dong  2026-02-08 @1005  	for (i = 0; i < tl->nr_links; i++) {
35b3515be0ecb9d Menglong Dong  2026-02-08  1006  		int err;
35b3515be0ecb9d Menglong Dong  2026-02-08  1007  
35b3515be0ecb9d Menglong Dong  2026-02-08  1008  		if (bpf_prog_calls_session_cookie(tl->links[i])) {
35b3515be0ecb9d Menglong Dong  2026-02-08  1009  			u64 meta = func_meta | ((u64)cur_cookie << BPF_TRAMP_COOKIE_INDEX_SHIFT);
35b3515be0ecb9d Menglong Dong  2026-02-08  1010  
35b3515be0ecb9d Menglong Dong  2026-02-08  1011  			emit_store_stack_imm64(RV_REG_T1, -func_meta_off, meta, ctx);
35b3515be0ecb9d Menglong Dong  2026-02-08  1012  			cur_cookie--;
35b3515be0ecb9d Menglong Dong  2026-02-08  1013  		}
35b3515be0ecb9d Menglong Dong  2026-02-08  1014  		err = invoke_bpf_prog(tl->links[i], args_off, retval_off, run_ctx_off,
35b3515be0ecb9d Menglong Dong  2026-02-08  1015  				      save_ret, ctx);
35b3515be0ecb9d Menglong Dong  2026-02-08  1016  		if (err)
35b3515be0ecb9d Menglong Dong  2026-02-08  1017  			return err;
35b3515be0ecb9d Menglong Dong  2026-02-08  1018  	}
35b3515be0ecb9d Menglong Dong  2026-02-08  1019  	return 0;
35b3515be0ecb9d Menglong Dong  2026-02-08  1020  }
35b3515be0ecb9d Menglong Dong  2026-02-08  1021  
49b5e77ae3e214a Pu Lehui       2023-02-15  1022  static int __arch_prepare_bpf_trampoline(struct bpf_tramp_image *im,
49b5e77ae3e214a Pu Lehui       2023-02-15  1023  					 const struct btf_func_model *m,
49b5e77ae3e214a Pu Lehui       2023-02-15  1024  					 struct bpf_tramp_links *tlinks,
49b5e77ae3e214a Pu Lehui       2023-02-15  1025  					 void *func_addr, u32 flags,
49b5e77ae3e214a Pu Lehui       2023-02-15  1026  					 struct rv_jit_context *ctx)
49b5e77ae3e214a Pu Lehui       2023-02-15  1027  {
49b5e77ae3e214a Pu Lehui       2023-02-15  1028  	int i, ret, offset;
49b5e77ae3e214a Pu Lehui       2023-02-15  1029  	int *branches_off = NULL;
6801b0aef79db47 Pu Lehui       2024-07-02  1030  	int stack_size = 0, nr_arg_slots = 0;
35b3515be0ecb9d Menglong Dong  2026-02-08  1031  	int retval_off, args_off, func_meta_off, ip_off, run_ctx_off, sreg_off, stk_arg_off;
35b3515be0ecb9d Menglong Dong  2026-02-08  1032  	int cookie_off, cookie_cnt;
49b5e77ae3e214a Pu Lehui       2023-02-15 @1033  	struct bpf_tramp_links *fentry = &tlinks[BPF_TRAMP_FENTRY];
49b5e77ae3e214a Pu Lehui       2023-02-15  1034  	struct bpf_tramp_links *fexit = &tlinks[BPF_TRAMP_FEXIT];
49b5e77ae3e214a Pu Lehui       2023-02-15  1035  	struct bpf_tramp_links *fmod_ret = &tlinks[BPF_TRAMP_MODIFY_RETURN];
1732ebc4a26181c Pu Lehui       2024-01-23  1036  	bool is_struct_ops = flags & BPF_TRAMP_F_INDIRECT;
49b5e77ae3e214a Pu Lehui       2023-02-15  1037  	void *orig_call = func_addr;
49b5e77ae3e214a Pu Lehui       2023-02-15  1038  	bool save_ret;
35b3515be0ecb9d Menglong Dong  2026-02-08  1039  	u64 func_meta;
49b5e77ae3e214a Pu Lehui       2023-02-15  1040  	u32 insn;
49b5e77ae3e214a Pu Lehui       2023-02-15  1041  
25ad10658dc1068 Pu Lehui       2023-07-21  1042  	/* Two types of generated trampoline stack layout:
25ad10658dc1068 Pu Lehui       2023-07-21  1043  	 *
25ad10658dc1068 Pu Lehui       2023-07-21  1044  	 * 1. trampoline called from function entry
25ad10658dc1068 Pu Lehui       2023-07-21  1045  	 * --------------------------------------
25ad10658dc1068 Pu Lehui       2023-07-21  1046  	 * FP + 8	    [ RA to parent func	] return address to parent
25ad10658dc1068 Pu Lehui       2023-07-21  1047  	 *					  function
25ad10658dc1068 Pu Lehui       2023-07-21  1048  	 * FP + 0	    [ FP of parent func ] frame pointer of parent
25ad10658dc1068 Pu Lehui       2023-07-21  1049  	 *					  function
25ad10658dc1068 Pu Lehui       2023-07-21  1050  	 * FP - 8           [ T0 to traced func ] return address of traced
25ad10658dc1068 Pu Lehui       2023-07-21  1051  	 *					  function
25ad10658dc1068 Pu Lehui       2023-07-21  1052  	 * FP - 16	    [ FP of traced func ] frame pointer of traced
25ad10658dc1068 Pu Lehui       2023-07-21  1053  	 *					  function
25ad10658dc1068 Pu Lehui       2023-07-21  1054  	 * --------------------------------------
49b5e77ae3e214a Pu Lehui       2023-02-15  1055  	 *
25ad10658dc1068 Pu Lehui       2023-07-21  1056  	 * 2. trampoline called directly
25ad10658dc1068 Pu Lehui       2023-07-21  1057  	 * --------------------------------------
25ad10658dc1068 Pu Lehui       2023-07-21  1058  	 * FP - 8	    [ RA to caller func ] return address to caller
49b5e77ae3e214a Pu Lehui       2023-02-15  1059  	 *					  function
25ad10658dc1068 Pu Lehui       2023-07-21  1060  	 * FP - 16	    [ FP of caller func	] frame pointer of caller
49b5e77ae3e214a Pu Lehui       2023-02-15  1061  	 *					  function
25ad10658dc1068 Pu Lehui       2023-07-21  1062  	 * --------------------------------------
49b5e77ae3e214a Pu Lehui       2023-02-15  1063  	 *
49b5e77ae3e214a Pu Lehui       2023-02-15  1064  	 * FP - retval_off  [ return value      ] BPF_TRAMP_F_CALL_ORIG or
49b5e77ae3e214a Pu Lehui       2023-02-15  1065  	 *					  BPF_TRAMP_F_RET_FENTRY_RET
49b5e77ae3e214a Pu Lehui       2023-02-15  1066  	 *                  [ argN              ]
49b5e77ae3e214a Pu Lehui       2023-02-15  1067  	 *                  [ ...               ]
49b5e77ae3e214a Pu Lehui       2023-02-15  1068  	 * FP - args_off    [ arg1              ]
49b5e77ae3e214a Pu Lehui       2023-02-15  1069  	 *
35b3515be0ecb9d Menglong Dong  2026-02-08  1070  	 * FP - func_meta_off [ regs count, etc ]
49b5e77ae3e214a Pu Lehui       2023-02-15  1071  	 *
49b5e77ae3e214a Pu Lehui       2023-02-15  1072  	 * FP - ip_off      [ traced func	] BPF_TRAMP_F_IP_ARG
49b5e77ae3e214a Pu Lehui       2023-02-15  1073  	 *
35b3515be0ecb9d Menglong Dong  2026-02-08  1074  	 *                  [ stack cookie N    ]
35b3515be0ecb9d Menglong Dong  2026-02-08  1075  	 *                  [ ...               ]
35b3515be0ecb9d Menglong Dong  2026-02-08  1076  	 * FP - cookie_off  [ stack cookie 1    ]
35b3515be0ecb9d Menglong Dong  2026-02-08  1077  	 *
49b5e77ae3e214a Pu Lehui       2023-02-15  1078  	 * FP - run_ctx_off [ bpf_tramp_run_ctx ]
49b5e77ae3e214a Pu Lehui       2023-02-15  1079  	 *
49b5e77ae3e214a Pu Lehui       2023-02-15  1080  	 * FP - sreg_off    [ callee saved reg	]
49b5e77ae3e214a Pu Lehui       2023-02-15  1081  	 *
49b5e77ae3e214a Pu Lehui       2023-02-15  1082  	 *		    [ pads              ] pads for 16 bytes alignment
6801b0aef79db47 Pu Lehui       2024-07-02  1083  	 *
6801b0aef79db47 Pu Lehui       2024-07-02  1084  	 *		    [ stack_argN        ]
6801b0aef79db47 Pu Lehui       2024-07-02  1085  	 *		    [ ...               ]
6801b0aef79db47 Pu Lehui       2024-07-02  1086  	 * FP - stk_arg_off [ stack_arg1        ] BPF_TRAMP_F_CALL_ORIG
49b5e77ae3e214a Pu Lehui       2023-02-15  1087  	 */
49b5e77ae3e214a Pu Lehui       2023-02-15  1088  
49b5e77ae3e214a Pu Lehui       2023-02-15  1089  	if (flags & (BPF_TRAMP_F_ORIG_STACK | BPF_TRAMP_F_SHARE_IPMODIFY))
49b5e77ae3e214a Pu Lehui       2023-02-15  1090  		return -ENOTSUPP;
49b5e77ae3e214a Pu Lehui       2023-02-15  1091  
6801b0aef79db47 Pu Lehui       2024-07-02  1092  	if (m->nr_args > MAX_BPF_FUNC_ARGS)
49b5e77ae3e214a Pu Lehui       2023-02-15  1093  		return -ENOTSUPP;
49b5e77ae3e214a Pu Lehui       2023-02-15  1094  
6801b0aef79db47 Pu Lehui       2024-07-02  1095  	for (i = 0; i < m->nr_args; i++)
6801b0aef79db47 Pu Lehui       2024-07-02  1096  		nr_arg_slots += round_up(m->arg_size[i], 8) / 8;
6801b0aef79db47 Pu Lehui       2024-07-02  1097  
25ad10658dc1068 Pu Lehui       2023-07-21  1098  	/* room of trampoline frame to store return address and frame pointer */
25ad10658dc1068 Pu Lehui       2023-07-21  1099  	stack_size += 16;
49b5e77ae3e214a Pu Lehui       2023-02-15  1100  
49b5e77ae3e214a Pu Lehui       2023-02-15  1101  	save_ret = flags & (BPF_TRAMP_F_CALL_ORIG | BPF_TRAMP_F_RET_FENTRY_RET);
d0bf7cd5df18466 Chenghao Duan  2025-09-22  1102  	if (save_ret)
7112cd26e606c7b Björn Töpel    2023-10-04  1103  		stack_size += 16; /* Save both A5 (BPF R0) and A0 */
49b5e77ae3e214a Pu Lehui       2023-02-15  1104  	retval_off = stack_size;
49b5e77ae3e214a Pu Lehui       2023-02-15  1105  
6801b0aef79db47 Pu Lehui       2024-07-02  1106  	stack_size += nr_arg_slots * 8;
49b5e77ae3e214a Pu Lehui       2023-02-15  1107  	args_off = stack_size;
49b5e77ae3e214a Pu Lehui       2023-02-15  1108  
35b3515be0ecb9d Menglong Dong  2026-02-08  1109  	/* function metadata, such as regs count */
49b5e77ae3e214a Pu Lehui       2023-02-15  1110  	stack_size += 8;
35b3515be0ecb9d Menglong Dong  2026-02-08  1111  	func_meta_off = stack_size;
49b5e77ae3e214a Pu Lehui       2023-02-15  1112  
49b5e77ae3e214a Pu Lehui       2023-02-15  1113  	if (flags & BPF_TRAMP_F_IP_ARG) {
49b5e77ae3e214a Pu Lehui       2023-02-15  1114  		stack_size += 8;
49b5e77ae3e214a Pu Lehui       2023-02-15  1115  		ip_off = stack_size;
49b5e77ae3e214a Pu Lehui       2023-02-15  1116  	}
49b5e77ae3e214a Pu Lehui       2023-02-15  1117  
35b3515be0ecb9d Menglong Dong  2026-02-08  1118  	cookie_cnt = bpf_fsession_cookie_cnt(tlinks);
35b3515be0ecb9d Menglong Dong  2026-02-08  1119  	/* room for session cookies */
35b3515be0ecb9d Menglong Dong  2026-02-08  1120  	stack_size += cookie_cnt * 8;
35b3515be0ecb9d Menglong Dong  2026-02-08  1121  	cookie_off = stack_size;
35b3515be0ecb9d Menglong Dong  2026-02-08  1122  
49b5e77ae3e214a Pu Lehui       2023-02-15  1123  	stack_size += round_up(sizeof(struct bpf_tramp_run_ctx), 8);
49b5e77ae3e214a Pu Lehui       2023-02-15  1124  	run_ctx_off = stack_size;
49b5e77ae3e214a Pu Lehui       2023-02-15  1125  
49b5e77ae3e214a Pu Lehui       2023-02-15  1126  	stack_size += 8;
49b5e77ae3e214a Pu Lehui       2023-02-15  1127  	sreg_off = stack_size;
49b5e77ae3e214a Pu Lehui       2023-02-15  1128  
a5912c37faf723c Puranjay Mohan 2024-07-08  1129  	if ((flags & BPF_TRAMP_F_CALL_ORIG) && (nr_arg_slots - RV_MAX_REG_ARGS > 0))
6801b0aef79db47 Pu Lehui       2024-07-02  1130  		stack_size += (nr_arg_slots - RV_MAX_REG_ARGS) * 8;
6801b0aef79db47 Pu Lehui       2024-07-02  1131  
e944fc8152744a4 Xiao Wang      2024-05-23  1132  	stack_size = round_up(stack_size, STACK_ALIGN);
49b5e77ae3e214a Pu Lehui       2023-02-15  1133  
6801b0aef79db47 Pu Lehui       2024-07-02  1134  	/* room for args on stack must be at the top of stack */
6801b0aef79db47 Pu Lehui       2024-07-02  1135  	stk_arg_off = stack_size;
6801b0aef79db47 Pu Lehui       2024-07-02  1136  
1732ebc4a26181c Pu Lehui       2024-01-23  1137  	if (!is_struct_ops) {
25ad10658dc1068 Pu Lehui       2023-07-21  1138  		/* For the trampoline called from function entry,
25ad10658dc1068 Pu Lehui       2023-07-21  1139  		 * the frame of traced function and the frame of
25ad10658dc1068 Pu Lehui       2023-07-21  1140  		 * trampoline need to be considered.
25ad10658dc1068 Pu Lehui       2023-07-21  1141  		 */
25ad10658dc1068 Pu Lehui       2023-07-21  1142  		emit_addi(RV_REG_SP, RV_REG_SP, -16, ctx);
25ad10658dc1068 Pu Lehui       2023-07-21  1143  		emit_sd(RV_REG_SP, 8, RV_REG_RA, ctx);
25ad10658dc1068 Pu Lehui       2023-07-21  1144  		emit_sd(RV_REG_SP, 0, RV_REG_FP, ctx);
25ad10658dc1068 Pu Lehui       2023-07-21  1145  		emit_addi(RV_REG_FP, RV_REG_SP, 16, ctx);
49b5e77ae3e214a Pu Lehui       2023-02-15  1146  
25ad10658dc1068 Pu Lehui       2023-07-21  1147  		emit_addi(RV_REG_SP, RV_REG_SP, -stack_size, ctx);
25ad10658dc1068 Pu Lehui       2023-07-21  1148  		emit_sd(RV_REG_SP, stack_size - 8, RV_REG_T0, ctx);
25ad10658dc1068 Pu Lehui       2023-07-21  1149  		emit_sd(RV_REG_SP, stack_size - 16, RV_REG_FP, ctx);
25ad10658dc1068 Pu Lehui       2023-07-21  1150  		emit_addi(RV_REG_FP, RV_REG_SP, stack_size, ctx);
25ad10658dc1068 Pu Lehui       2023-07-21  1151  	} else {
e63985ecd22681c Puranjay Mohan 2024-03-03  1152  		/* emit kcfi hash */
e63985ecd22681c Puranjay Mohan 2024-03-03  1153  		emit_kcfi(cfi_get_func_hash(func_addr), ctx);
25ad10658dc1068 Pu Lehui       2023-07-21  1154  		/* For the trampoline called directly, just handle
25ad10658dc1068 Pu Lehui       2023-07-21  1155  		 * the frame of trampoline.
25ad10658dc1068 Pu Lehui       2023-07-21  1156  		 */
25ad10658dc1068 Pu Lehui       2023-07-21  1157  		emit_addi(RV_REG_SP, RV_REG_SP, -stack_size, ctx);
25ad10658dc1068 Pu Lehui       2023-07-21  1158  		emit_sd(RV_REG_SP, stack_size - 8, RV_REG_RA, ctx);
25ad10658dc1068 Pu Lehui       2023-07-21  1159  		emit_sd(RV_REG_SP, stack_size - 16, RV_REG_FP, ctx);
49b5e77ae3e214a Pu Lehui       2023-02-15  1160  		emit_addi(RV_REG_FP, RV_REG_SP, stack_size, ctx);
25ad10658dc1068 Pu Lehui       2023-07-21  1161  	}
49b5e77ae3e214a Pu Lehui       2023-02-15  1162  
49b5e77ae3e214a Pu Lehui       2023-02-15  1163  	/* callee saved register S1 to pass start time */
49b5e77ae3e214a Pu Lehui       2023-02-15  1164  	emit_sd(RV_REG_FP, -sreg_off, RV_REG_S1, ctx);
49b5e77ae3e214a Pu Lehui       2023-02-15  1165  
49b5e77ae3e214a Pu Lehui       2023-02-15  1166  	/* store ip address of the traced function */
93fd420d71beed5 Menglong Dong  2026-02-08  1167  	if (flags & BPF_TRAMP_F_IP_ARG)
93fd420d71beed5 Menglong Dong  2026-02-08  1168  		emit_store_stack_imm64(RV_REG_T1, -ip_off, (u64)func_addr, ctx);
49b5e77ae3e214a Pu Lehui       2023-02-15  1169  
35b3515be0ecb9d Menglong Dong  2026-02-08  1170  	func_meta = nr_arg_slots;
35b3515be0ecb9d Menglong Dong  2026-02-08  1171  	emit_store_stack_imm64(RV_REG_T1, -func_meta_off, func_meta, ctx);
49b5e77ae3e214a Pu Lehui       2023-02-15  1172  
6801b0aef79db47 Pu Lehui       2024-07-02  1173  	store_args(nr_arg_slots, args_off, ctx);
49b5e77ae3e214a Pu Lehui       2023-02-15  1174  
35b3515be0ecb9d Menglong Dong  2026-02-08  1175  	if (bpf_fsession_cnt(tlinks)) {
35b3515be0ecb9d Menglong Dong  2026-02-08  1176  		/* clear all session cookies' value */
35b3515be0ecb9d Menglong Dong  2026-02-08  1177  		for (i = 0; i < cookie_cnt; i++)
35b3515be0ecb9d Menglong Dong  2026-02-08  1178  			emit_sd(RV_REG_FP, -cookie_off + 8 * i, RV_REG_ZERO, ctx);
35b3515be0ecb9d Menglong Dong  2026-02-08  1179  		/* clear return value to make sure fentry always get 0 */
35b3515be0ecb9d Menglong Dong  2026-02-08  1180  		emit_sd(RV_REG_FP, -retval_off, RV_REG_ZERO, ctx);
35b3515be0ecb9d Menglong Dong  2026-02-08  1181  	}
35b3515be0ecb9d Menglong Dong  2026-02-08  1182  
49b5e77ae3e214a Pu Lehui       2023-02-15  1183  	if (flags & BPF_TRAMP_F_CALL_ORIG) {
9f1e16fb1fc9826 Pu Lehui       2024-06-22  1184  		emit_imm(RV_REG_A0, ctx->insns ? (const s64)im : RV_MAX_COUNT_IMM, ctx);
49b5e77ae3e214a Pu Lehui       2023-02-15  1185  		ret = emit_call((const u64)__bpf_tramp_enter, true, ctx);
49b5e77ae3e214a Pu Lehui       2023-02-15  1186  		if (ret)
49b5e77ae3e214a Pu Lehui       2023-02-15  1187  			return ret;
49b5e77ae3e214a Pu Lehui       2023-02-15  1188  	}
49b5e77ae3e214a Pu Lehui       2023-02-15  1189  
35b3515be0ecb9d Menglong Dong  2026-02-08  1190  	if (fentry->nr_links) {
35b3515be0ecb9d Menglong Dong  2026-02-08  1191  		ret = invoke_bpf(fentry, args_off, retval_off, run_ctx_off, func_meta_off,
35b3515be0ecb9d Menglong Dong  2026-02-08  1192  				 flags & BPF_TRAMP_F_RET_FENTRY_RET, func_meta, cookie_off, ctx);
49b5e77ae3e214a Pu Lehui       2023-02-15  1193  		if (ret)
49b5e77ae3e214a Pu Lehui       2023-02-15  1194  			return ret;
49b5e77ae3e214a Pu Lehui       2023-02-15  1195  	}
49b5e77ae3e214a Pu Lehui       2023-02-15  1196  
49b5e77ae3e214a Pu Lehui       2023-02-15  1197  	if (fmod_ret->nr_links) {
49b5e77ae3e214a Pu Lehui       2023-02-15  1198  		branches_off = kcalloc(fmod_ret->nr_links, sizeof(int), GFP_KERNEL);
49b5e77ae3e214a Pu Lehui       2023-02-15  1199  		if (!branches_off)
49b5e77ae3e214a Pu Lehui       2023-02-15  1200  			return -ENOMEM;
49b5e77ae3e214a Pu Lehui       2023-02-15  1201  
49b5e77ae3e214a Pu Lehui       2023-02-15  1202  		/* cleanup to avoid garbage return value confusion */
49b5e77ae3e214a Pu Lehui       2023-02-15  1203  		emit_sd(RV_REG_FP, -retval_off, RV_REG_ZERO, ctx);
49b5e77ae3e214a Pu Lehui       2023-02-15  1204  		for (i = 0; i < fmod_ret->nr_links; i++) {
49b5e77ae3e214a Pu Lehui       2023-02-15  1205  			ret = invoke_bpf_prog(fmod_ret->links[i], args_off, retval_off,
49b5e77ae3e214a Pu Lehui       2023-02-15  1206  					      run_ctx_off, true, ctx);
49b5e77ae3e214a Pu Lehui       2023-02-15  1207  			if (ret)
49b5e77ae3e214a Pu Lehui       2023-02-15  1208  				goto out;
49b5e77ae3e214a Pu Lehui       2023-02-15  1209  			emit_ld(RV_REG_T1, -retval_off, RV_REG_FP, ctx);
49b5e77ae3e214a Pu Lehui       2023-02-15  1210  			branches_off[i] = ctx->ninsns;
49b5e77ae3e214a Pu Lehui       2023-02-15  1211  			/* nop reserved for conditional jump */
49b5e77ae3e214a Pu Lehui       2023-02-15  1212  			emit(rv_nop(), ctx);
49b5e77ae3e214a Pu Lehui       2023-02-15  1213  		}
49b5e77ae3e214a Pu Lehui       2023-02-15  1214  	}
49b5e77ae3e214a Pu Lehui       2023-02-15  1215  
49b5e77ae3e214a Pu Lehui       2023-02-15  1216  	if (flags & BPF_TRAMP_F_CALL_ORIG) {
8f3e00af8e52c0d Menglong Dong  2025-12-19  1217  		/* skip to actual body of traced function */
8f3e00af8e52c0d Menglong Dong  2025-12-19  1218  		orig_call += RV_FENTRY_NINSNS * 4;
6801b0aef79db47 Pu Lehui       2024-07-02  1219  		restore_args(min_t(int, nr_arg_slots, RV_MAX_REG_ARGS), args_off, ctx);
6801b0aef79db47 Pu Lehui       2024-07-02  1220  		restore_stack_args(nr_arg_slots - RV_MAX_REG_ARGS, args_off, stk_arg_off, ctx);
49b5e77ae3e214a Pu Lehui       2023-02-15  1221  		ret = emit_call((const u64)orig_call, true, ctx);
49b5e77ae3e214a Pu Lehui       2023-02-15  1222  		if (ret)
49b5e77ae3e214a Pu Lehui       2023-02-15  1223  			goto out;
49b5e77ae3e214a Pu Lehui       2023-02-15  1224  		emit_sd(RV_REG_FP, -retval_off, RV_REG_A0, ctx);
7112cd26e606c7b Björn Töpel    2023-10-04  1225  		emit_sd(RV_REG_FP, -(retval_off - 8), regmap[BPF_REG_0], ctx);
2382a405c581ae8 Pu Lehui       2024-06-22  1226  		im->ip_after_call = ctx->ro_insns + ctx->ninsns;
49b5e77ae3e214a Pu Lehui       2023-02-15  1227  		/* 2 nops reserved for auipc+jalr pair */
49b5e77ae3e214a Pu Lehui       2023-02-15  1228  		emit(rv_nop(), ctx);
49b5e77ae3e214a Pu Lehui       2023-02-15  1229  		emit(rv_nop(), ctx);
49b5e77ae3e214a Pu Lehui       2023-02-15  1230  	}
49b5e77ae3e214a Pu Lehui       2023-02-15  1231  
49b5e77ae3e214a Pu Lehui       2023-02-15  1232  	/* update branches saved in invoke_bpf_mod_ret with bnez */
49b5e77ae3e214a Pu Lehui       2023-02-15  1233  	for (i = 0; ctx->insns && i < fmod_ret->nr_links; i++) {
49b5e77ae3e214a Pu Lehui       2023-02-15  1234  		offset = ninsns_rvoff(ctx->ninsns - branches_off[i]);
49b5e77ae3e214a Pu Lehui       2023-02-15  1235  		insn = rv_bne(RV_REG_T1, RV_REG_ZERO, offset >> 1);
49b5e77ae3e214a Pu Lehui       2023-02-15  1236  		*(u32 *)(ctx->insns + branches_off[i]) = insn;
49b5e77ae3e214a Pu Lehui       2023-02-15  1237  	}
49b5e77ae3e214a Pu Lehui       2023-02-15  1238  
35b3515be0ecb9d Menglong Dong  2026-02-08  1239  	/* set "is_return" flag for fsession */
35b3515be0ecb9d Menglong Dong  2026-02-08  1240  	func_meta |= (1ULL << BPF_TRAMP_IS_RETURN_SHIFT);
35b3515be0ecb9d Menglong Dong  2026-02-08  1241  	if (bpf_fsession_cnt(tlinks))
35b3515be0ecb9d Menglong Dong  2026-02-08  1242  		emit_store_stack_imm64(RV_REG_T1, -func_meta_off, func_meta, ctx);
35b3515be0ecb9d Menglong Dong  2026-02-08  1243  
35b3515be0ecb9d Menglong Dong  2026-02-08  1244  	if (fexit->nr_links) {
35b3515be0ecb9d Menglong Dong  2026-02-08  1245  		ret = invoke_bpf(fexit, args_off, retval_off, run_ctx_off, func_meta_off,
35b3515be0ecb9d Menglong Dong  2026-02-08  1246  				 false, func_meta, cookie_off, ctx);
49b5e77ae3e214a Pu Lehui       2023-02-15  1247  		if (ret)
49b5e77ae3e214a Pu Lehui       2023-02-15  1248  			goto out;
49b5e77ae3e214a Pu Lehui       2023-02-15  1249  	}
49b5e77ae3e214a Pu Lehui       2023-02-15  1250  
49b5e77ae3e214a Pu Lehui       2023-02-15  1251  	if (flags & BPF_TRAMP_F_CALL_ORIG) {
2382a405c581ae8 Pu Lehui       2024-06-22  1252  		im->ip_epilogue = ctx->ro_insns + ctx->ninsns;
9f1e16fb1fc9826 Pu Lehui       2024-06-22  1253  		emit_imm(RV_REG_A0, ctx->insns ? (const s64)im : RV_MAX_COUNT_IMM, ctx);
49b5e77ae3e214a Pu Lehui       2023-02-15  1254  		ret = emit_call((const u64)__bpf_tramp_exit, true, ctx);
49b5e77ae3e214a Pu Lehui       2023-02-15  1255  		if (ret)
49b5e77ae3e214a Pu Lehui       2023-02-15  1256  			goto out;
49b5e77ae3e214a Pu Lehui       2023-02-15  1257  	}
49b5e77ae3e214a Pu Lehui       2023-02-15  1258  
49b5e77ae3e214a Pu Lehui       2023-02-15  1259  	if (flags & BPF_TRAMP_F_RESTORE_REGS)
6801b0aef79db47 Pu Lehui       2024-07-02  1260  		restore_args(min_t(int, nr_arg_slots, RV_MAX_REG_ARGS), args_off, ctx);
49b5e77ae3e214a Pu Lehui       2023-02-15  1261  
7112cd26e606c7b Björn Töpel    2023-10-04  1262  	if (save_ret) {
7112cd26e606c7b Björn Töpel    2023-10-04  1263  		emit_ld(regmap[BPF_REG_0], -(retval_off - 8), RV_REG_FP, ctx);
fd2e08128944a76 Hengqi Chen    2025-09-08  1264  		if (is_struct_ops) {
fd2e08128944a76 Hengqi Chen    2025-09-08  1265  			ret = sign_extend(RV_REG_A0, regmap[BPF_REG_0], m->ret_size,
fd2e08128944a76 Hengqi Chen    2025-09-08  1266  					  m->ret_flags & BTF_FMODEL_SIGNED_ARG, ctx);
fd2e08128944a76 Hengqi Chen    2025-09-08  1267  			if (ret)
fd2e08128944a76 Hengqi Chen    2025-09-08  1268  				goto out;
fd2e08128944a76 Hengqi Chen    2025-09-08  1269  		} else {
fd2e08128944a76 Hengqi Chen    2025-09-08  1270  			emit_ld(RV_REG_A0, -retval_off, RV_REG_FP, ctx);
fd2e08128944a76 Hengqi Chen    2025-09-08  1271  		}
7112cd26e606c7b Björn Töpel    2023-10-04  1272  	}
49b5e77ae3e214a Pu Lehui       2023-02-15  1273  
49b5e77ae3e214a Pu Lehui       2023-02-15  1274  	emit_ld(RV_REG_S1, -sreg_off, RV_REG_FP, ctx);
49b5e77ae3e214a Pu Lehui       2023-02-15  1275  
1732ebc4a26181c Pu Lehui       2024-01-23  1276  	if (!is_struct_ops) {
25ad10658dc1068 Pu Lehui       2023-07-21  1277  		/* trampoline called from function entry */
25ad10658dc1068 Pu Lehui       2023-07-21  1278  		emit_ld(RV_REG_T0, stack_size - 8, RV_REG_SP, ctx);
25ad10658dc1068 Pu Lehui       2023-07-21  1279  		emit_ld(RV_REG_FP, stack_size - 16, RV_REG_SP, ctx);
25ad10658dc1068 Pu Lehui       2023-07-21  1280  		emit_addi(RV_REG_SP, RV_REG_SP, stack_size, ctx);
25ad10658dc1068 Pu Lehui       2023-07-21  1281  
25ad10658dc1068 Pu Lehui       2023-07-21  1282  		emit_ld(RV_REG_RA, 8, RV_REG_SP, ctx);
25ad10658dc1068 Pu Lehui       2023-07-21  1283  		emit_ld(RV_REG_FP, 0, RV_REG_SP, ctx);
25ad10658dc1068 Pu Lehui       2023-07-21  1284  		emit_addi(RV_REG_SP, RV_REG_SP, 16, ctx);
25ad10658dc1068 Pu Lehui       2023-07-21  1285  
49b5e77ae3e214a Pu Lehui       2023-02-15  1286  		if (flags & BPF_TRAMP_F_SKIP_FRAME)
25ad10658dc1068 Pu Lehui       2023-07-21  1287  			/* return to parent function */
25ad10658dc1068 Pu Lehui       2023-07-21  1288  			emit_jalr(RV_REG_ZERO, RV_REG_RA, 0, ctx);
49b5e77ae3e214a Pu Lehui       2023-02-15  1289  		else
25ad10658dc1068 Pu Lehui       2023-07-21  1290  			/* return to traced function */
25ad10658dc1068 Pu Lehui       2023-07-21  1291  			emit_jalr(RV_REG_ZERO, RV_REG_T0, 0, ctx);
25ad10658dc1068 Pu Lehui       2023-07-21  1292  	} else {
25ad10658dc1068 Pu Lehui       2023-07-21  1293  		/* trampoline called directly */
25ad10658dc1068 Pu Lehui       2023-07-21  1294  		emit_ld(RV_REG_RA, stack_size - 8, RV_REG_SP, ctx);
25ad10658dc1068 Pu Lehui       2023-07-21  1295  		emit_ld(RV_REG_FP, stack_size - 16, RV_REG_SP, ctx);
49b5e77ae3e214a Pu Lehui       2023-02-15  1296  		emit_addi(RV_REG_SP, RV_REG_SP, stack_size, ctx);
49b5e77ae3e214a Pu Lehui       2023-02-15  1297  
49b5e77ae3e214a Pu Lehui       2023-02-15  1298  		emit_jalr(RV_REG_ZERO, RV_REG_RA, 0, ctx);
25ad10658dc1068 Pu Lehui       2023-07-21  1299  	}
49b5e77ae3e214a Pu Lehui       2023-02-15  1300  
49b5e77ae3e214a Pu Lehui       2023-02-15  1301  	ret = ctx->ninsns;
49b5e77ae3e214a Pu Lehui       2023-02-15  1302  out:
49b5e77ae3e214a Pu Lehui       2023-02-15  1303  	kfree(branches_off);
49b5e77ae3e214a Pu Lehui       2023-02-15  1304  	return ret;
49b5e77ae3e214a Pu Lehui       2023-02-15  1305  }
49b5e77ae3e214a Pu Lehui       2023-02-15  1306  

-- 
0-DAY CI Kernel Test Service
https://github.com/intel/lkp-tests/wiki

^ permalink raw reply


This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox