BPF List
 help / color / mirror / Atom feed
* [PATCH v2 0/6] mm/page_owner: add bpf_iter and kfuncs
@ 2026-10-13 12:21 Zhen Ni
  2026-10-13 12:21 ` [PATCH v2 1/6] mm/page_owner: extract page_owner_next_eligible() from read loop Zhen Ni
                   ` (6 more replies)
  0 siblings, 7 replies; 14+ messages in thread
From: Zhen Ni @ 2026-10-13 12:21 UTC (permalink / raw)
  To: ast, daniel, andrii, eddyz87, memxor, martin.lau, song,
	yonghong.song, jolsa, emil, ihor.solodrai, akpm, vbabka, surenb,
	mhocko, brendan.jackman, hannes, ziy, shuah
  Cc: bpf, linux-mm, linux-kselftest, linux-kernel, Zhen Ni

Hi all,

Introduction
============

page_owner records who allocated every page and is a core memory-debug
facility, but it currently reaches userspace as the debugfs flat dump:
slicing allocations by pid, comm, NUMA node, cgroup or stack, etc.,
means streaming and parsing the entire file in userspace, which is
expensive on large hosts and hard to query live.

Exposing the same walk to eBPF lets a program filter and aggregate in
the kernel and surface only the records it cares about. This series
adds that access as a seq_file bpf_iter target and an open-coded
iterator.

Implementation
==============

The page walk is factored into page_owner_next_eligible(), shared by
the read path and the iterators so eligibility stays identical. Two
kfuncs expose information that a BPF program cannot decode from the
snapshot alone: per-page memcg info including the full cgroup path,
and the stack trace behind a stack_depot handle. Each record is
snapshotted under the page_ext RCU lock, so consumers read a
consistent copy.

Testing
=======

The selftest exercises the seq_file and open-coded iterators, and
validates the stack_snprint and get_memcg_info kfuncs.

Changes in v2
=============
- Route the zero-handle case through ext_put_continue.
- Drop the scan->pfn++.
- Fill page/po/pfn only when v is non-NULL.
- Implement page_owner_seq_stop().
- bpf_page_owner_stack_snprint() now takes the scan cursor.
- Buffer args renamed to buf__uninit/buf__sz.
- Removes the v1 get_nid() kfunc. The node is now stored in scan->nid.
- bpf_page_owner_get_memcg_info() now resolves the page with
  pfn_to_online_page() for an offline or removed pfn.
- Buffer/info args renamed to info__uninit/buf__uninit/buf__sz.
- Drop the get_nid kfunc coverage.
- Enable CONFIG_PAGE_OWNER in the bpf selftests config.

v1: https://lore.kernel.org/linux-mm/20261009112308.240769-1-zhen.ni@easystack.cn/

Zhen Ni (6):
  mm/page_owner: extract page_owner_next_eligible() from read loop
  mm/page_owner: add bpf_iter target "page_owner"
  mm/page_owner: add open-coded page_owner iterator
  mm/page_owner: record the NUMA node in the scan cursor
  mm/page_owner: add bpf_page_owner_get_memcg_info() kfunc
  selftests/bpf: add tests for the page_owner bpf_iter target

 kernel/bpf/helpers.c                          |   7 +
 mm/Makefile                                   |   3 +
 mm/page_owner.c                               | 219 +++++++++++-------
 mm/page_owner.h                               |  58 +++++
 mm/page_owner_iter.c                          | 197 ++++++++++++++++
 tools/testing/selftests/bpf/config            |   2 +
 .../bpf/prog_tests/page_owner_iter.c          | 193 +++++++++++++++
 .../selftests/bpf/progs/bpf_iter_page_owner.c | 159 +++++++++++++
 8 files changed, 758 insertions(+), 80 deletions(-)
 create mode 100644 mm/page_owner.h
 create mode 100644 mm/page_owner_iter.c
 create mode 100644 tools/testing/selftests/bpf/prog_tests/page_owner_iter.c
 create mode 100644 tools/testing/selftests/bpf/progs/bpf_iter_page_owner.c

-- 
2.20.1


^ permalink raw reply	[flat|nested] 14+ messages in thread

* [PATCH v2 1/6] mm/page_owner: extract page_owner_next_eligible() from read loop
  2026-10-13 12:21 [PATCH v2 0/6] mm/page_owner: add bpf_iter and kfuncs Zhen Ni
@ 2026-10-13 12:21 ` Zhen Ni
  2026-10-13 12:21 ` [PATCH v2 2/6] mm/page_owner: add bpf_iter target "page_owner" Zhen Ni
                   ` (5 subsequent siblings)
  6 siblings, 0 replies; 14+ messages in thread
From: Zhen Ni @ 2026-10-13 12:21 UTC (permalink / raw)
  To: ast, daniel, andrii, eddyz87, memxor, martin.lau, song,
	yonghong.song, jolsa, emil, ihor.solodrai, akpm, vbabka, surenb,
	mhocko, brendan.jackman, hannes, ziy, shuah
  Cc: bpf, linux-mm, linux-kselftest, linux-kernel, Zhen Ni

Move the page scanning logic out of read_page_owner() into a dedicated
helper, in preparation for using it in the page_owner bpf_iter target.

The helper advances a struct page_owner_scan cursor to the next page
eligible for page_owner output and returns true on a hit. On a hit it
takes a snapshot of the page_owner record inside the page_ext RCU
window.

No change to the output is intended. handle is read from the snapshot,
taken before the non-zero check, so the validated and reported handle
are the same value.

Signed-off-by: Zhen Ni <zhen.ni@easystack.cn>
---
Changes in v2:
- Route the zero-handle case through ext_put_continue.

v1: https://lore.kernel.org/linux-mm/20261009112308.240769-2-zhen.ni@easystack.cn/
---
 mm/page_owner.c | 103 ++++++++++++++++++++++++++++++------------------
 1 file changed, 65 insertions(+), 38 deletions(-)

diff --git a/mm/page_owner.c b/mm/page_owner.c
index fbbda7ba914b..56c75a669a6b 100644
--- a/mm/page_owner.c
+++ b/mm/page_owner.c
@@ -36,6 +36,16 @@ struct page_owner {
 	pid_t free_tgid;
 };
 
+/*
+ * Cursor and per-page result of page_owner_next_eligible().
+ */
+struct page_owner_scan {
+	/* resume point */
+	unsigned long pfn;
+	struct page *page;
+	struct page_owner po_snap;
+};
+
 struct stack {
 	struct stack_record *stack_record;
 	struct stack *next;
@@ -725,37 +735,24 @@ void __dump_page_owner(const struct page *page)
 	page_ext_put(page_ext);
 }
 
-static ssize_t
-read_page_owner(struct file *file, char __user *buf, size_t count, loff_t *ppos)
+/*
+ * Advance @scan to the next page eligible for page_owner output.
+ * On a hit, fill scan->pfn/page/po_snap and return true; the caller
+ * must advance scan->pfn past the hit before calling again to resume
+ * the scan.
+ */
+static bool page_owner_next_eligible(struct page_owner_scan *scan)
 {
-	unsigned long pfn;
-	struct page *page;
-	struct page_ext *page_ext;
-	struct page_owner *page_owner;
-	depot_stack_handle_t handle;
-	struct page_owner_filter_state *state = file->private_data;
+	unsigned long pfn = scan->pfn;
 
-	if (!static_branch_unlikely(&page_owner_inited))
-		return -EINVAL;
-
-	page = NULL;
-	if (*ppos == 0)
-		pfn = min_low_pfn;
-	else
-		pfn = *ppos;
 	/* Find a valid PFN or the start of a MAX_ORDER_NR_PAGES area */
 	while (!pfn_valid(pfn) && (pfn & (MAX_ORDER_NR_PAGES - 1)) != 0)
 		pfn++;
 
-	/* Find an allocated page */
 	for (; pfn < max_pfn; pfn++) {
-		/*
-		 * This temporary page_owner is required so
-		 * that we can avoid the context switches while holding
-		 * the rcu lock and copying the page owner information to
-		 * user through copy_to_user() or GFP_KERNEL allocations.
-		 */
-		struct page_owner page_owner_tmp;
+		struct page_owner *page_owner;
+		struct page_ext *page_ext;
+		struct page *page;
 
 		/*
 		 * If the new page is in a new MAX_ORDER_NR_PAGES area,
@@ -798,37 +795,67 @@ read_page_owner(struct file *file, char __user *buf, size_t count, loff_t *ppos)
 			goto ext_put_continue;
 
 		/*
-		 * Access to page_ext->handle isn't synchronous so we should
-		 * be careful to access it.
+		 * Take a snapshot of the page_owner record inside the RCU
+		 * window, then validate it, so that print_page_owner()
+		 * reads the checked handle from the same snapshot.
 		 */
-		handle = READ_ONCE(page_owner->handle);
-		if (!handle)
+		scan->pfn = pfn;
+		scan->page = page;
+		scan->po_snap = *page_owner;
+
+		if (!scan->po_snap.handle)
 			goto ext_put_continue;
 
+		page_ext_put(page_ext);
+		return true;
+
+ext_put_continue:
+		page_ext_put(page_ext);
+		cond_resched();
+	}
+
+	return false;
+}
+
+static ssize_t
+read_page_owner(struct file *file, char __user *buf, size_t count, loff_t *ppos)
+{
+	struct page_owner_scan scan;
+	struct page_owner_filter_state *state = file->private_data;
+
+	if (!static_branch_unlikely(&page_owner_inited))
+		return -EINVAL;
+
+	if (*ppos == 0)
+		scan.pfn = min_low_pfn;
+	else
+		scan.pfn = *ppos;
+
+	/* Find an allocated page */
+	while (page_owner_next_eligible(&scan)) {
 		if (state->nid_filter_enabled) {
 			int nid;
-			memdesc_flags_t page_flags = READ_ONCE(page->flags);
+			memdesc_flags_t page_flags =
+				READ_ONCE(scan.page->flags);
 
 			/*
 			 * Bypass PF_POISONED_CHECK() in page_to_nid() to avoid
 			 * VM_BUG_ON when accessing poisoned pages.
 			 */
 			if (page_flags.f == PAGE_POISON_PATTERN)
-				goto ext_put_continue;
+				goto skip_continue;
 			nid = memdesc_nid(&page_flags);
 			if (!node_isset(nid, state->nid_filter))
-				goto ext_put_continue;
+				goto skip_continue;
 		}
 
 		/* Record the next PFN to read in the file offset */
-		*ppos = pfn + 1;
+		*ppos = scan.pfn + 1;
 
-		page_owner_tmp = *page_owner;
-		page_ext_put(page_ext);
-		return print_page_owner(buf, count, pfn, page,
-				&page_owner_tmp, handle, state);
-ext_put_continue:
-		page_ext_put(page_ext);
+		return print_page_owner(buf, count, scan.pfn, scan.page,
+				&scan.po_snap, scan.po_snap.handle, state);
+skip_continue:
+		scan.pfn++;
 		cond_resched();
 	}
 
-- 
2.20.1


^ permalink raw reply related	[flat|nested] 14+ messages in thread

* [PATCH v2 2/6] mm/page_owner: add bpf_iter target "page_owner"
  2026-10-13 12:21 [PATCH v2 0/6] mm/page_owner: add bpf_iter and kfuncs Zhen Ni
  2026-10-13 12:21 ` [PATCH v2 1/6] mm/page_owner: extract page_owner_next_eligible() from read loop Zhen Ni
@ 2026-10-13 12:21 ` Zhen Ni
  2026-10-13 13:51   ` bot+bpf-ci
  2026-10-13 12:21 ` [PATCH v2 3/6] mm/page_owner: add open-coded page_owner iterator Zhen Ni
                   ` (4 subsequent siblings)
  6 siblings, 1 reply; 14+ messages in thread
From: Zhen Ni @ 2026-10-13 12:21 UTC (permalink / raw)
  To: ast, daniel, andrii, eddyz87, memxor, martin.lau, song,
	yonghong.song, jolsa, emil, ihor.solodrai, akpm, vbabka, surenb,
	mhocko, brendan.jackman, hannes, ziy, shuah
  Cc: bpf, linux-mm, linux-kselftest, linux-kernel, Zhen Ni

Register a bpf_iter target "page_owner" that walks the pages eligible
for page_owner output, with the same eligibility rules as the
/sys/kernel/debug/page_owner read path.

For each eligible page the attached BPF program receives the pfn, the
page and the page_owner record, and can freely filter and format output.

The po_snap record in the scan cursor is taken inside the page_ext
RCU window and handed to the BPF program as a copy, so no RCU window
is needed at consumption time. Torn reads remain possible if the
record is concurrently updated while it is copied, but this is the
same best-effort behavior as the read path has always had.

The target is registered from pageowner_init() when both
CONFIG_PAGE_OWNER and CONFIG_BPF_SYSCALL are enabled; a registration
failure only disables the bpf_iter target.

Userspace tools such as bpftrace can attach to this target like any
other bpf_iter. For example, to count pages owned by init (pid 1):

    bpftrace -e 'iter:page_owner / ctx->po->pid == 1 / { @ = count(); }'

Signed-off-by: Zhen Ni <zhen.ni@easystack.cn>
---
Changes in v2:
- Drop the scan->pfn++.
- Fill page/po/pfn only when v is non-NULL.
- Implement page_owner_seq_stop().

v1: https://lore.kernel.org/linux-mm/20261009112308.240769-3-zhen.ni@easystack.cn/
---
 mm/Makefile          |   3 ++
 mm/page_owner.c      |  33 +++---------
 mm/page_owner.h      |  48 +++++++++++++++++
 mm/page_owner_iter.c | 124 +++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 182 insertions(+), 26 deletions(-)
 create mode 100644 mm/page_owner.h
 create mode 100644 mm/page_owner_iter.c

diff --git a/mm/Makefile b/mm/Makefile
index e7245cb88c66..d7307b5a964c 100644
--- a/mm/Makefile
+++ b/mm/Makefile
@@ -115,6 +115,9 @@ obj-$(CONFIG_DEBUG_KMEMLEAK) += kmemleak.o
 obj-$(CONFIG_DEBUG_RODATA_TEST) += rodata_test.o
 obj-$(CONFIG_DEBUG_VM_PGTABLE) += debug_vm_pgtable.o
 obj-$(CONFIG_PAGE_OWNER) += page_owner.o
+ifdef CONFIG_BPF_SYSCALL
+obj-$(CONFIG_PAGE_OWNER) += page_owner_iter.o
+endif
 obj-$(CONFIG_MEMORY_ISOLATION) += page_isolation.o
 obj-$(CONFIG_ZSMALLOC)	+= zsmalloc.o
 obj-$(CONFIG_GENERIC_EARLY_IOREMAP) += early_ioremap.o
diff --git a/mm/page_owner.c b/mm/page_owner.c
index 56c75a669a6b..617346a4ab22 100644
--- a/mm/page_owner.c
+++ b/mm/page_owner.c
@@ -14,6 +14,7 @@
 #include <linux/sched/clock.h>
 
 #include "page_alloc.h"
+#include "page_owner.h"
 
 /*
  * TODO: teach PAGE_OWNER_STACK_DEPTH (__dump_page_owner and save_stack)
@@ -21,31 +22,6 @@
  */
 #define PAGE_OWNER_STACK_DEPTH (16)
 
-struct page_owner {
-	unsigned short order;
-	short last_migrate_reason;
-	gfp_t gfp_mask;
-	depot_stack_handle_t handle;
-	depot_stack_handle_t free_handle;
-	u64 ts_nsec;
-	u64 free_ts_nsec;
-	char comm[TASK_COMM_LEN];
-	pid_t pid;
-	pid_t tgid;
-	pid_t free_pid;
-	pid_t free_tgid;
-};
-
-/*
- * Cursor and per-page result of page_owner_next_eligible().
- */
-struct page_owner_scan {
-	/* resume point */
-	unsigned long pfn;
-	struct page *page;
-	struct page_owner po_snap;
-};
-
 struct stack {
 	struct stack_record *stack_record;
 	struct stack *next;
@@ -741,7 +717,7 @@ void __dump_page_owner(const struct page *page)
  * must advance scan->pfn past the hit before calling again to resume
  * the scan.
  */
-static bool page_owner_next_eligible(struct page_owner_scan *scan)
+bool page_owner_next_eligible(struct page_owner_scan *scan)
 {
 	unsigned long pfn = scan->pfn;
 
@@ -1196,6 +1172,11 @@ static int __init pageowner_init(void)
 			    &stack_fops);
 	debugfs_create_file("count_threshold", 0600, dir, NULL,
 			    &threshold_fops);
+
+#ifdef CONFIG_BPF_SYSCALL
+	if (page_owner_iter_register())
+		pr_warn("page_owner: failed to register bpf_iter target\n");
+#endif
 	return 0;
 }
 late_initcall(pageowner_init)
diff --git a/mm/page_owner.h b/mm/page_owner.h
new file mode 100644
index 000000000000..fda83be69275
--- /dev/null
+++ b/mm/page_owner.h
@@ -0,0 +1,48 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * mm/ internal page_owner and page_owner_iter declarations
+ */
+
+#ifndef __MM_PAGE_OWNER_H
+#define __MM_PAGE_OWNER_H
+
+#include <linux/page_ext.h>
+#include <linux/sched.h>
+#include <linux/stackdepot.h>
+
+/*
+ * mm/page_owner.c
+ */
+struct page_owner {
+	unsigned short order;
+	short last_migrate_reason;
+	gfp_t gfp_mask;
+	depot_stack_handle_t handle;
+	depot_stack_handle_t free_handle;
+	u64 ts_nsec;
+	u64 free_ts_nsec;
+	char comm[TASK_COMM_LEN];
+	pid_t pid;
+	pid_t tgid;
+	pid_t free_pid;
+	pid_t free_tgid;
+};
+
+/*
+ * Cursor and per-page result of page_owner_next_eligible().
+ */
+struct page_owner_scan {
+	/* resume point */
+	unsigned long pfn;
+	struct page *page;
+	struct page_owner po_snap;
+};
+
+bool page_owner_next_eligible(struct page_owner_scan *scan);
+
+/*
+ * mm/page_owner_iter.c
+ */
+int page_owner_iter_register(void);
+
+#endif /* __MM_PAGE_OWNER_H */
diff --git a/mm/page_owner_iter.c b/mm/page_owner_iter.c
new file mode 100644
index 000000000000..938cb42ab7d7
--- /dev/null
+++ b/mm/page_owner_iter.c
@@ -0,0 +1,124 @@
+// SPDX-License-Identifier: GPL-2.0
+#include <linux/bpf.h>
+#include <linux/btf_ids.h>
+#include <linux/memblock.h>
+#include <linux/mm.h>
+#include <linux/seq_file.h>
+
+#include "page_owner.h"
+
+struct bpf_iter__page_owner {
+	__bpf_md_ptr(struct bpf_iter_meta *, meta);
+	__bpf_md_ptr(struct page *, page);
+	__bpf_md_ptr(struct page_owner *, po);
+	u64 pfn;
+};
+
+DEFINE_BPF_ITER_FUNC(page_owner, struct bpf_iter_meta *meta,
+		     struct page *page, struct page_owner *po, u64 pfn)
+
+static void *page_owner_seq_start(struct seq_file *seq, loff_t *posp)
+{
+	struct page_owner_scan *scan = seq->private;
+
+	if (*posp == 0)
+		scan->pfn = min_low_pfn;
+
+	if (!page_owner_next_eligible(scan))
+		return NULL;
+	return scan->page;
+}
+
+static void *page_owner_seq_next(struct seq_file *seq, void *v, loff_t *posp)
+{
+	struct page_owner_scan *scan = seq->private;
+
+	++*posp;
+	scan->pfn++;
+	if (!page_owner_next_eligible(scan))
+		return NULL;
+
+	return scan->page;
+}
+
+static int page_owner_prog_seq_show(struct bpf_prog *prog,
+				    struct bpf_iter_meta *meta, void *v)
+{
+	struct page_owner_scan *scan = meta->seq->private;
+	struct bpf_iter__page_owner ctx = {};
+
+	ctx.meta = meta;
+	if (v) {
+		ctx.page = (struct page *)v;
+		/* @po: hit-time snapshot, no RCU window needed. */
+		ctx.po = &scan->po_snap;
+		ctx.pfn = scan->pfn;
+	}
+
+	return bpf_iter_run_prog(prog, &ctx);
+}
+
+static int page_owner_seq_show(struct seq_file *seq, void *v)
+{
+	struct bpf_iter_meta meta;
+	struct bpf_prog *prog;
+
+	meta.seq = seq;
+	prog = bpf_iter_get_info(&meta, false);
+	if (!prog)
+		return 0;
+
+	return page_owner_prog_seq_show(prog, &meta, v);
+}
+
+static void page_owner_seq_stop(struct seq_file *seq, void *v)
+{
+	if (!v) {
+		struct bpf_iter_meta meta = { .seq = seq };
+		struct bpf_prog *prog;
+
+		prog = bpf_iter_get_info(&meta, true);
+		if (prog)
+			page_owner_prog_seq_show(prog, &meta, NULL);
+	}
+}
+
+static const struct seq_operations page_owner_iter_seq_ops = {
+	.start	= page_owner_seq_start,
+	.next	= page_owner_seq_next,
+	.stop	= page_owner_seq_stop,
+	.show	= page_owner_seq_show,
+};
+
+static const struct bpf_iter_seq_info page_owner_iter_seq_info = {
+	.seq_ops		= &page_owner_iter_seq_ops,
+	.init_seq_private	= NULL,
+	.fini_seq_private	= NULL,
+	.seq_priv_size		= sizeof(struct page_owner_scan),
+};
+
+static struct bpf_iter_reg page_owner_iter_reg_info = {
+	.target			= "page_owner",
+	.ctx_arg_info_size	= 2,
+	.ctx_arg_info		= {
+		{ offsetof(struct bpf_iter__page_owner, page),
+		  PTR_TO_BTF_ID_OR_NULL },
+		{ offsetof(struct bpf_iter__page_owner, po),
+		  PTR_TO_BTF_ID_OR_NULL },
+	},
+	.seq_info		= &page_owner_iter_seq_info,
+};
+
+BTF_ID_LIST(page_owner_btf_ids)
+BTF_ID(struct, page)
+BTF_ID(struct, page_owner)
+
+int page_owner_iter_register(void)
+{
+	page_owner_iter_reg_info.ctx_arg_info[0].btf_id =
+		page_owner_btf_ids[0];
+	page_owner_iter_reg_info.ctx_arg_info[1].btf_id =
+		page_owner_btf_ids[1];
+
+	return bpf_iter_reg_target(&page_owner_iter_reg_info);
+}
-- 
2.20.1


^ permalink raw reply related	[flat|nested] 14+ messages in thread

* [PATCH v2 3/6] mm/page_owner: add open-coded page_owner iterator
  2026-10-13 12:21 [PATCH v2 0/6] mm/page_owner: add bpf_iter and kfuncs Zhen Ni
  2026-10-13 12:21 ` [PATCH v2 1/6] mm/page_owner: extract page_owner_next_eligible() from read loop Zhen Ni
  2026-10-13 12:21 ` [PATCH v2 2/6] mm/page_owner: add bpf_iter target "page_owner" Zhen Ni
@ 2026-10-13 12:21 ` Zhen Ni
  2026-10-13 12:49   ` sashiko-bot
  2026-10-13 13:51   ` bot+bpf-ci
  2026-10-13 12:21 ` [PATCH v2 4/6] mm/page_owner: record the NUMA node in the scan cursor Zhen Ni
                   ` (3 subsequent siblings)
  6 siblings, 2 replies; 14+ messages in thread
From: Zhen Ni @ 2026-10-13 12:21 UTC (permalink / raw)
  To: ast, daniel, andrii, eddyz87, memxor, martin.lau, song,
	yonghong.song, jolsa, emil, ihor.solodrai, akpm, vbabka, surenb,
	mhocko, brendan.jackman, hannes, ziy, shuah
  Cc: bpf, linux-mm, linux-kselftest, linux-kernel, Zhen Ni

Add an open-coded bpf_iter over the pages eligible for page_owner
output, reusing page_owner_next_eligible().

A sleepable BPF program drives it with
bpf_iter_page_owner_new()/next()/destroy(); each next() returns the
page_owner_scan. po_snap is copied inside the page_ext RCU window, so
the program needs no RCU section when it reads it.

Also add bpf_page_owner_stack_snprint(), which renders the stack trace
of the current page into a caller-provided buffer.

Signed-off-by: Zhen Ni <zhen.ni@easystack.cn>
---
Changes in v2:
- bpf_page_owner_stack_snprint() now takes the scan cursor.
- Buffer args renamed to buf__uninit/buf__sz.

v1: https://lore.kernel.org/linux-mm/20261009112308.240769-4-zhen.ni@easystack.cn/
---
 kernel/bpf/helpers.c |  6 +++++
 mm/page_owner_iter.c | 53 ++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 59 insertions(+)

diff --git a/kernel/bpf/helpers.c b/kernel/bpf/helpers.c
index 712dca5a2c5b..23a2d2d71f4d 100644
--- a/kernel/bpf/helpers.c
+++ b/kernel/bpf/helpers.c
@@ -4946,6 +4946,12 @@ BTF_ID_FLAGS(func, bpf_iter_dmabuf_new, KF_ITER_NEW | KF_SLEEPABLE)
 BTF_ID_FLAGS(func, bpf_iter_dmabuf_next, KF_ITER_NEXT | KF_RET_NULL | KF_SLEEPABLE)
 BTF_ID_FLAGS(func, bpf_iter_dmabuf_destroy, KF_ITER_DESTROY | KF_SLEEPABLE)
 #endif
+#ifdef CONFIG_PAGE_OWNER
+BTF_ID_FLAGS(func, bpf_iter_page_owner_new, KF_ITER_NEW | KF_SLEEPABLE)
+BTF_ID_FLAGS(func, bpf_iter_page_owner_next, KF_ITER_NEXT | KF_RET_NULL | KF_SLEEPABLE)
+BTF_ID_FLAGS(func, bpf_iter_page_owner_destroy, KF_ITER_DESTROY | KF_SLEEPABLE)
+BTF_ID_FLAGS(func, bpf_page_owner_stack_snprint, KF_SLEEPABLE)
+#endif
 BTF_ID_FLAGS(func, __bpf_trap)
 BTF_ID_FLAGS(func, bpf_strcmp, KF_PERFMON);
 BTF_ID_FLAGS(func, bpf_strcasecmp, KF_PERFMON);
diff --git a/mm/page_owner_iter.c b/mm/page_owner_iter.c
index 938cb42ab7d7..ac7e69ba0cef 100644
--- a/mm/page_owner_iter.c
+++ b/mm/page_owner_iter.c
@@ -4,6 +4,7 @@
 #include <linux/memblock.h>
 #include <linux/mm.h>
 #include <linux/seq_file.h>
+#include <linux/stackdepot.h>
 
 #include "page_owner.h"
 
@@ -122,3 +123,55 @@ int page_owner_iter_register(void)
 
 	return bpf_iter_reg_target(&page_owner_iter_reg_info);
 }
+
+/* opaque to BPF programs */
+struct bpf_iter_page_owner {
+	__u64 __opaque[16];
+} __attribute__((aligned(8)));
+
+struct bpf_iter_page_owner_kern {
+	struct page_owner_scan scan;
+} __attribute__((aligned(8)));
+
+__bpf_kfunc_start_defs();
+
+__bpf_kfunc int bpf_iter_page_owner_new(struct bpf_iter_page_owner *it)
+{
+	struct bpf_iter_page_owner_kern *kit = (void *)it;
+
+	BUILD_BUG_ON(sizeof(*kit) > sizeof(*it));
+	BUILD_BUG_ON(__alignof__(*kit) != __alignof__(*it));
+
+	kit->scan.pfn = min_low_pfn;
+	kit->scan.page = NULL;
+	return 0;
+}
+
+__bpf_kfunc struct page_owner_scan *
+bpf_iter_page_owner_next(struct bpf_iter_page_owner *it)
+{
+	struct bpf_iter_page_owner_kern *kit = (void *)it;
+
+	cond_resched();
+
+	if (kit->scan.page)
+		kit->scan.pfn++;
+
+	if (!page_owner_next_eligible(&kit->scan))
+		return NULL;
+
+	return &kit->scan;
+}
+
+__bpf_kfunc void bpf_iter_page_owner_destroy(struct bpf_iter_page_owner *it)
+{
+}
+
+__bpf_kfunc int bpf_page_owner_stack_snprint(struct page_owner_scan *scan,
+					     char *buf__uninit, u32 buf__sz)
+{
+	return stack_depot_snprint(scan->po_snap.handle, buf__uninit, buf__sz,
+				   0);
+}
+
+__bpf_kfunc_end_defs();
-- 
2.20.1


^ permalink raw reply related	[flat|nested] 14+ messages in thread

* [PATCH v2 4/6] mm/page_owner: record the NUMA node in the scan cursor
  2026-10-13 12:21 [PATCH v2 0/6] mm/page_owner: add bpf_iter and kfuncs Zhen Ni
                   ` (2 preceding siblings ...)
  2026-10-13 12:21 ` [PATCH v2 3/6] mm/page_owner: add open-coded page_owner iterator Zhen Ni
@ 2026-10-13 12:21 ` Zhen Ni
  2026-10-13 12:36   ` sashiko-bot
  2026-10-13 12:21 ` [PATCH v2 5/6] mm/page_owner: add bpf_page_owner_get_memcg_info() kfunc Zhen Ni
                   ` (2 subsequent siblings)
  6 siblings, 1 reply; 14+ messages in thread
From: Zhen Ni @ 2026-10-13 12:21 UTC (permalink / raw)
  To: ast, daniel, andrii, eddyz87, memxor, martin.lau, song,
	yonghong.song, jolsa, emil, ihor.solodrai, akpm, vbabka, surenb,
	mhocko, brendan.jackman, hannes, ziy, shuah
  Cc: bpf, linux-mm, linux-kselftest, linux-kernel, Zhen Ni

Compute the page's NUMA node in page_owner_next_eligible() while the
page is still valid, and store it in scan->nid.

The debugfs nid filter reads scan->nid instead of recomputing it, and a
BPF program iterating the cursor reads the node straight from scan->nid.

Resolve nid at scan time to avoid dereferencing a possibly-unmapped page.
Keep it in the transient scan cursor rather than struct page_owner, so
nid does not add per-page memory overhead.

Signed-off-by: Zhen Ni <zhen.ni@easystack.cn>
---
Changes in v2:
- Removes the v1 get_nid() kfunc. The node is now stored in scan->nid.

v1: https://lore.kernel.org/linux-mm/20261009112308.240769-5-zhen.ni@easystack.cn/
---
 mm/page_owner.c      | 28 ++++++++++++++++------------
 mm/page_owner.h      |  1 +
 mm/page_owner_iter.c |  5 ++++-
 3 files changed, 21 insertions(+), 13 deletions(-)

diff --git a/mm/page_owner.c b/mm/page_owner.c
index 617346a4ab22..113e4e7e0b3a 100644
--- a/mm/page_owner.c
+++ b/mm/page_owner.c
@@ -711,6 +711,19 @@ void __dump_page_owner(const struct page *page)
 	page_ext_put(page_ext);
 }
 
+/*
+ * NUMA node of @page, bypassing PF_POISONED_CHECK() in page_to_nid() to
+ * avoid VM_BUG_ON on poisoned pages; -1 for poisoned pages.
+ */
+static int page_owner_get_nid(struct page *page)
+{
+	memdesc_flags_t page_flags = READ_ONCE(page->flags);
+
+	if (page_flags.f == PAGE_POISON_PATTERN)
+		return -1;
+	return memdesc_nid(&page_flags);
+}
+
 /*
  * Advance @scan to the next page eligible for page_owner output.
  * On a hit, fill scan->pfn/page/po_snap and return true; the caller
@@ -782,6 +795,7 @@ bool page_owner_next_eligible(struct page_owner_scan *scan)
 		if (!scan->po_snap.handle)
 			goto ext_put_continue;
 
+		scan->nid = page_owner_get_nid(page);
 		page_ext_put(page_ext);
 		return true;
 
@@ -810,18 +824,8 @@ read_page_owner(struct file *file, char __user *buf, size_t count, loff_t *ppos)
 	/* Find an allocated page */
 	while (page_owner_next_eligible(&scan)) {
 		if (state->nid_filter_enabled) {
-			int nid;
-			memdesc_flags_t page_flags =
-				READ_ONCE(scan.page->flags);
-
-			/*
-			 * Bypass PF_POISONED_CHECK() in page_to_nid() to avoid
-			 * VM_BUG_ON when accessing poisoned pages.
-			 */
-			if (page_flags.f == PAGE_POISON_PATTERN)
-				goto skip_continue;
-			nid = memdesc_nid(&page_flags);
-			if (!node_isset(nid, state->nid_filter))
+			if (scan.nid < 0 ||
+			    !node_isset(scan.nid, state->nid_filter))
 				goto skip_continue;
 		}
 
diff --git a/mm/page_owner.h b/mm/page_owner.h
index fda83be69275..e8e52d22abed 100644
--- a/mm/page_owner.h
+++ b/mm/page_owner.h
@@ -36,6 +36,7 @@ struct page_owner_scan {
 	unsigned long pfn;
 	struct page *page;
 	struct page_owner po_snap;
+	int nid;
 };
 
 bool page_owner_next_eligible(struct page_owner_scan *scan);
diff --git a/mm/page_owner_iter.c b/mm/page_owner_iter.c
index ac7e69ba0cef..a2b90c874852 100644
--- a/mm/page_owner_iter.c
+++ b/mm/page_owner_iter.c
@@ -13,10 +13,12 @@ struct bpf_iter__page_owner {
 	__bpf_md_ptr(struct page *, page);
 	__bpf_md_ptr(struct page_owner *, po);
 	u64 pfn;
+	int nid;
 };
 
 DEFINE_BPF_ITER_FUNC(page_owner, struct bpf_iter_meta *meta,
-		     struct page *page, struct page_owner *po, u64 pfn)
+		     struct page *page, struct page_owner *po, u64 pfn,
+		     int nid)
 
 static void *page_owner_seq_start(struct seq_file *seq, loff_t *posp)
 {
@@ -54,6 +56,7 @@ static int page_owner_prog_seq_show(struct bpf_prog *prog,
 		/* @po: hit-time snapshot, no RCU window needed. */
 		ctx.po = &scan->po_snap;
 		ctx.pfn = scan->pfn;
+		ctx.nid = scan->nid;
 	}
 
 	return bpf_iter_run_prog(prog, &ctx);
-- 
2.20.1


^ permalink raw reply related	[flat|nested] 14+ messages in thread

* [PATCH v2 5/6] mm/page_owner: add bpf_page_owner_get_memcg_info() kfunc
  2026-10-13 12:21 [PATCH v2 0/6] mm/page_owner: add bpf_iter and kfuncs Zhen Ni
                   ` (3 preceding siblings ...)
  2026-10-13 12:21 ` [PATCH v2 4/6] mm/page_owner: record the NUMA node in the scan cursor Zhen Ni
@ 2026-10-13 12:21 ` Zhen Ni
  2026-10-13 12:49   ` sashiko-bot
  2026-10-13 12:21 ` [PATCH v2 6/6] selftests/bpf: add tests for the page_owner bpf_iter target Zhen Ni
  2026-10-13 17:17 ` [PATCH v2 0/6] mm/page_owner: add bpf_iter and kfuncs Zi Yan
  6 siblings, 1 reply; 14+ messages in thread
From: Zhen Ni @ 2026-10-13 12:21 UTC (permalink / raw)
  To: ast, daniel, andrii, eddyz87, memxor, martin.lau, song,
	yonghong.song, jolsa, emil, ihor.solodrai, akpm, vbabka, surenb,
	mhocko, brendan.jackman, hannes, ziy, shuah
  Cc: bpf, linux-mm, linux-kselftest, linux-kernel, Zhen Ni

Expose per-page memcg information to BPF programs, so that a bpf_iter
over page_owner can aggregate and filter allocations by memcg.
Attributing an allocation to a specific cgroup needs its full cgroup
path, not just the memcg name.

Split memcg info collection out of print_page_owner_memcg() into
get_page_memcg_info(), which fills struct memcg_info and, when given
a caller-provided buffer, writes the full cgroup path via
cgroup_path(). print_page_owner_memcg() now consumes the struct;
this introduces no functional change to the debugfs output.

The helper takes a single READ_ONCE snapshot within one RCU read-side
critical section, so the flags and cgroup path reflect a consistent
view of the page. cgroup_path() returns the path relative to the
cgroup hierarchy root, so memcg attribution works on both cgroup v1
and v2.

The kfunc gets the page via pfn_to_online_page(), so an offline or
removed pfn yields -ENODATA instead of a stale deref.

Signed-off-by: Zhen Ni <zhen.ni@easystack.cn>
---
Changes in v2:
- bpf_page_owner_get_memcg_info() now resolves the page with
  pfn_to_online_page() for an offline or removed pfn.
- Buffer/info args renamed to info__uninit/buf__uninit/buf__sz.

v1: https://lore.kernel.org/linux-mm/20261009112308.240769-6-zhen.ni@easystack.cn/
---
 kernel/bpf/helpers.c |  1 +
 mm/page_owner.c      | 85 ++++++++++++++++++++++++++++++++++----------
 mm/page_owner.h      |  9 +++++
 mm/page_owner_iter.c | 17 +++++++++
 4 files changed, 93 insertions(+), 19 deletions(-)

diff --git a/kernel/bpf/helpers.c b/kernel/bpf/helpers.c
index 23a2d2d71f4d..b46822eac894 100644
--- a/kernel/bpf/helpers.c
+++ b/kernel/bpf/helpers.c
@@ -4951,6 +4951,7 @@ BTF_ID_FLAGS(func, bpf_iter_page_owner_new, KF_ITER_NEW | KF_SLEEPABLE)
 BTF_ID_FLAGS(func, bpf_iter_page_owner_next, KF_ITER_NEXT | KF_RET_NULL | KF_SLEEPABLE)
 BTF_ID_FLAGS(func, bpf_iter_page_owner_destroy, KF_ITER_DESTROY | KF_SLEEPABLE)
 BTF_ID_FLAGS(func, bpf_page_owner_stack_snprint, KF_SLEEPABLE)
+BTF_ID_FLAGS(func, bpf_page_owner_get_memcg_info, KF_SLEEPABLE)
 #endif
 BTF_ID_FLAGS(func, __bpf_trap)
 BTF_ID_FLAGS(func, bpf_strcmp, KF_PERFMON);
diff --git a/mm/page_owner.c b/mm/page_owner.c
index 113e4e7e0b3a..664121be6686 100644
--- a/mm/page_owner.c
+++ b/mm/page_owner.c
@@ -538,16 +538,22 @@ void pagetypeinfo_showmixedcount_print(struct seq_file *m,
 
 #ifdef CONFIG_MEMCG
 /*
- * Looking for memcg information and print it out
+ * Collect memcg information of @page into @info; when @buf and a
+ * nonzero @size are given, also copy the full cgroup path of the
+ * charged memcg into @buf.
+ *
+ * Return: 0 if @page is charged to an attributable memcg; -ENODATA
+ * otherwise.
  */
-static inline int print_page_owner_memcg(char *kbuf, size_t count, int ret,
-					 struct page *page)
+int get_page_memcg_info(struct page *page, struct memcg_info *info,
+			char *buf, int size)
 {
 	unsigned long memcg_data;
 	struct obj_cgroup *objcg;
 	struct mem_cgroup *memcg;
-	bool online;
-	char name[80];
+	int ret = -ENODATA;
+
+	*info = (struct memcg_info){};
 
 	rcu_read_lock();
 	memcg_data = READ_ONCE(page->memcg_data);
@@ -555,31 +561,68 @@ static inline int print_page_owner_memcg(char *kbuf, size_t count, int ret,
 		goto out_unlock;
 
 	if (memcg_data & MEMCG_DATA_OBJEXTS) {
-		ret += scnprintf(kbuf + ret, count - ret,
-				"Slab cache page\n");
+		info->is_slab = true;
 		goto out_unlock;
 	}
 
 	objcg = (void *)(memcg_data & ~OBJEXTS_FLAGS_MASK);
-	memcg = objcg ? obj_cgroup_memcg(objcg) : NULL;
+	if (!objcg)
+		goto out_unlock;
+
+	memcg = obj_cgroup_memcg(objcg);
 	if (!memcg)
 		goto out_unlock;
 
-	online = css_is_online(&memcg->css);
-	cgroup_name(memcg->css.cgroup, name, sizeof(name));
-	ret += scnprintf(kbuf + ret, count - ret,
-			"Charged %sto %smemcg %s\n",
-			(memcg_data & MEMCG_DATA_KMEM) ? "(via objcg) " : "",
-			online ? "" : "offline ",
-			name);
+	info->is_kmem = (memcg_data & MEMCG_DATA_KMEM) != 0;
+	info->is_online = css_is_online(&memcg->css);
+	cgroup_name(memcg->css.cgroup, info->name, sizeof(info->name));
+
+	if (buf && size)
+		cgroup_path(memcg->css.cgroup, buf, size);
+	ret = 0;
 out_unlock:
 	rcu_read_unlock();
 
+	if (ret < 0 && buf && size)
+		buf[0] = '\0';
+
+	return ret;
+}
+
+/*
+ * Print memcg information from memcg_info
+ */
+static inline int print_page_owner_memcg(char *kbuf, size_t count, int ret,
+					 const struct memcg_info *info)
+{
+	if (!info)
+		return ret;
+
+	if (info->is_slab)
+		ret += scnprintf(kbuf + ret, count - ret,
+				"Slab cache page\n");
+
+	if (info->name[0])
+		ret += scnprintf(kbuf + ret, count - ret,
+				"Charged %sto %smemcg %s\n",
+				info->is_kmem ? "(via objcg) " : "",
+				info->is_online ? "" : "offline ",
+				info->name);
+
 	return ret;
 }
 #else
+int get_page_memcg_info(struct page *page, struct memcg_info *info,
+			char *buf, int size)
+{
+	*info = (struct memcg_info){};
+	if (buf && size)
+		buf[0] = '\0';
+	return -EOPNOTSUPP;
+}
+
 static inline int print_page_owner_memcg(char *kbuf, size_t count, int ret,
-					 struct page *page)
+					 const struct memcg_info *info)
 {
 	return ret;
 }
@@ -589,7 +632,8 @@ static ssize_t
 print_page_owner(char __user *buf, size_t count, unsigned long pfn,
 		struct page *page, struct page_owner *page_owner,
 		depot_stack_handle_t handle,
-		struct page_owner_filter_state *state)
+		struct page_owner_filter_state *state,
+		const struct memcg_info *memcg_info)
 {
 	int ret, pageblock_mt, page_mt;
 	char *kbuf;
@@ -639,7 +683,7 @@ print_page_owner(char __user *buf, size_t count, unsigned long pfn,
 			migrate_reason_names[page_owner->last_migrate_reason]);
 	}
 
-	ret = print_page_owner_memcg(kbuf, count, ret, page);
+	ret = print_page_owner_memcg(kbuf, count, ret, memcg_info);
 
 	ret += snprintf(kbuf + ret, count - ret, "\n");
 	if (ret >= count)
@@ -811,6 +855,7 @@ static ssize_t
 read_page_owner(struct file *file, char __user *buf, size_t count, loff_t *ppos)
 {
 	struct page_owner_scan scan;
+	struct memcg_info memcg_info;
 	struct page_owner_filter_state *state = file->private_data;
 
 	if (!static_branch_unlikely(&page_owner_inited))
@@ -832,8 +877,10 @@ read_page_owner(struct file *file, char __user *buf, size_t count, loff_t *ppos)
 		/* Record the next PFN to read in the file offset */
 		*ppos = scan.pfn + 1;
 
+		get_page_memcg_info(scan.page, &memcg_info, NULL, 0);
 		return print_page_owner(buf, count, scan.pfn, scan.page,
-				&scan.po_snap, scan.po_snap.handle, state);
+				&scan.po_snap, scan.po_snap.handle, state,
+				&memcg_info);
 skip_continue:
 		scan.pfn++;
 		cond_resched();
diff --git a/mm/page_owner.h b/mm/page_owner.h
index e8e52d22abed..febf1c2807de 100644
--- a/mm/page_owner.h
+++ b/mm/page_owner.h
@@ -39,7 +39,16 @@ struct page_owner_scan {
 	int nid;
 };
 
+struct memcg_info {
+	char name[80];
+	bool is_slab;
+	bool is_kmem;
+	bool is_online;
+};
+
 bool page_owner_next_eligible(struct page_owner_scan *scan);
+int get_page_memcg_info(struct page *page, struct memcg_info *info,
+			char *buf, int size);
 
 /*
  * mm/page_owner_iter.c
diff --git a/mm/page_owner_iter.c b/mm/page_owner_iter.c
index a2b90c874852..86e0b4b52bfc 100644
--- a/mm/page_owner_iter.c
+++ b/mm/page_owner_iter.c
@@ -177,4 +177,21 @@ __bpf_kfunc int bpf_page_owner_stack_snprint(struct page_owner_scan *scan,
 				   0);
 }
 
+__bpf_kfunc int bpf_page_owner_get_memcg_info(struct page_owner_scan *scan,
+					      struct memcg_info *info__uninit,
+					      char *buf__uninit, u32 buf__sz)
+{
+	/* Guard against the section being offlined after scan. */
+	struct page *page = pfn_to_online_page(scan->pfn);
+
+	if (!page) {
+		*info__uninit = (struct memcg_info){};
+		if (buf__uninit && buf__sz)
+			buf__uninit[0] = '\0';
+		return -ENODATA;
+	}
+
+	return get_page_memcg_info(page, info__uninit, buf__uninit, buf__sz);
+}
+
 __bpf_kfunc_end_defs();
-- 
2.20.1


^ permalink raw reply related	[flat|nested] 14+ messages in thread

* [PATCH v2 6/6] selftests/bpf: add tests for the page_owner bpf_iter target
  2026-10-13 12:21 [PATCH v2 0/6] mm/page_owner: add bpf_iter and kfuncs Zhen Ni
                   ` (4 preceding siblings ...)
  2026-10-13 12:21 ` [PATCH v2 5/6] mm/page_owner: add bpf_page_owner_get_memcg_info() kfunc Zhen Ni
@ 2026-10-13 12:21 ` Zhen Ni
  2026-10-13 12:38   ` sashiko-bot
  2026-10-13 17:17 ` [PATCH v2 0/6] mm/page_owner: add bpf_iter and kfuncs Zi Yan
  6 siblings, 1 reply; 14+ messages in thread
From: Zhen Ni @ 2026-10-13 12:21 UTC (permalink / raw)
  To: ast, daniel, andrii, eddyz87, memxor, martin.lau, song,
	yonghong.song, jolsa, emil, ihor.solodrai, akpm, vbabka, surenb,
	mhocko, brendan.jackman, hannes, ziy, shuah
  Cc: bpf, linux-mm, linux-kselftest, linux-kernel, Zhen Ni

Add a sleepable BPF program and a userspace runner covering the seq_file
and open-coded page_owner iterators. The test checks:

  - seq_file walk, unfiltered and pid-filtered: records read back from
    the iterator fd match the lines the program printed.
  - open-coded iterator parity with the seq path.
  - the kfuncs' contracts: a non-empty stack trace, and memcg
    classification.

CONFIG_PAGE_OWNER is enabled in the bpf selftests config so the
program's page_owner BTF types are available. Skipped when debugfs
page_owner is unavailable.

Signed-off-by: Zhen Ni <zhen.ni@easystack.cn>
---
Changes in v2:
- Drop the get_nid kfunc coverage.
- Enable CONFIG_PAGE_OWNER in the bpf selftests config.

v1: https://lore.kernel.org/linux-mm/20261009112308.240769-7-zhen.ni@easystack.cn/
---
 tools/testing/selftests/bpf/config            |   2 +
 .../bpf/prog_tests/page_owner_iter.c          | 193 ++++++++++++++++++
 .../selftests/bpf/progs/bpf_iter_page_owner.c | 159 +++++++++++++++
 3 files changed, 354 insertions(+)
 create mode 100644 tools/testing/selftests/bpf/prog_tests/page_owner_iter.c
 create mode 100644 tools/testing/selftests/bpf/progs/bpf_iter_page_owner.c

diff --git a/tools/testing/selftests/bpf/config b/tools/testing/selftests/bpf/config
index ea7044f30adc..731bc3d79881 100644
--- a/tools/testing/selftests/bpf/config
+++ b/tools/testing/selftests/bpf/config
@@ -136,3 +136,5 @@ CONFIG_SMC_HS_CTRL_BPF=y
 CONFIG_DIBS=y
 CONFIG_DIBS_LO=y
 CONFIG_PM_WAKELOCKS=y
+CONFIG_DEBUG_KERNEL=y
+CONFIG_PAGE_OWNER=y
diff --git a/tools/testing/selftests/bpf/prog_tests/page_owner_iter.c b/tools/testing/selftests/bpf/prog_tests/page_owner_iter.c
new file mode 100644
index 000000000000..3684ca308935
--- /dev/null
+++ b/tools/testing/selftests/bpf/prog_tests/page_owner_iter.c
@@ -0,0 +1,193 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (c) 2026 */
+
+#include <test_progs.h>
+#include "bpf_iter_page_owner.skel.h"
+
+#define DEBUGFS_PAGE_OWNER	"/sys/kernel/debug/page_owner"
+
+static bool page_owner_available(void)
+{
+	return access(DEBUGFS_PAGE_OWNER, R_OK) == 0;
+}
+
+/* Count link_fd output lines; -1 on error. */
+static long iter_record_count(int link_fd)
+{
+	char *line = NULL;
+	size_t len = 0;
+	long count = 0;
+	int iter_fd;
+	FILE *f;
+
+	iter_fd = bpf_iter_create(link_fd);
+	if (!ASSERT_GE(iter_fd, 0, "bpf_iter_create"))
+		return -1;
+
+	f = fdopen(iter_fd, "r");
+	if (!ASSERT_OK_PTR(f, "fdopen")) {
+		close(iter_fd);
+		return -1;
+	}
+
+	for (;;) {
+		if (getline(&line, &len, f) > 0) {
+			count++;
+			continue;
+		}
+		if (!ferror(f))
+			break;		/* clean EOF */
+		/*
+		 * bpf_seq_read() returns -EAGAIN when a chunk scanned.
+		 * Not an error, just retry the read.
+		 */
+		if (errno == EAGAIN) {
+			clearerr(f);
+			continue;
+		}
+		ASSERT_OK(errno, "iter_read");
+		count = -1;
+		break;
+	}
+
+	free(line);
+	fclose(f);
+	return count;
+}
+
+/* Validate bpf_page_owner_get_memcg_info() results from the open-coded run. */
+static void check_memcg_skel(struct bpf_iter_page_owner *skel)
+{
+	fprintf(stderr,
+		"memcg: seen=%u charged=%u online=%u nodata=%u notsup=%u bad=%u first='%s' name='%s' flags=%u\n",
+		skel->bss->memcg_seen, skel->bss->memcg_charged,
+		skel->bss->memcg_online, skel->bss->memcg_nodata,
+		skel->bss->memcg_notsup, skel->bss->memcg_bad,
+		skel->bss->memcg_first_path, skel->bss->memcg_first_name,
+		skel->bss->memcg_first_flags);
+
+	if (!ASSERT_GT(skel->bss->memcg_seen, 0, "memcg_seen_gt_0"))
+		return;
+
+	/* all -EOPNOTSUPP: CONFIG_MEMCG is off */
+	if (skel->bss->memcg_notsup == skel->bss->memcg_seen) {
+		ASSERT_EQ(skel->bss->memcg_bad, 0, "memcg_stub_invariants");
+		return;
+	}
+
+	if (!ASSERT_EQ(skel->bss->memcg_bad, 0, "memcg_kfunc_invariants"))
+		return;
+
+	/* Every call is either charged or nodata */
+	if (!ASSERT_EQ(skel->bss->memcg_charged + skel->bss->memcg_nodata,
+		       skel->bss->memcg_seen, "memcg_ret_classified"))
+		return;
+
+	if (!skel->bss->memcg_first_saved)
+		return;
+
+	if (!ASSERT_EQ(skel->bss->memcg_first_path[0], '/',
+		       "memcg_first_path_abs"))
+		return;
+
+	if (skel->bss->memcg_first_name[0])
+		ASSERT_OK_PTR(strstr(skel->bss->memcg_first_path,
+				    skel->bss->memcg_first_name),
+			      "memcg_name_in_path");
+}
+
+void serial_test_page_owner_iter(void)
+{
+	struct bpf_iter_page_owner *skel = NULL;
+	LIBBPF_OPTS(bpf_test_run_opts, opts);
+	long iter_count, unfiltered_count, oc_diff, pid;
+	unsigned int printed_before;
+	int link_fd, prog_fd;
+
+	if (!page_owner_available()) {
+		printf("%s:SKIP:page_owner debugfs unavailable "
+		       "(CONFIG_PAGE_OWNER + page_owner=on)\n", __func__);
+		test__skip();
+		return;
+	}
+
+	skel = bpf_iter_page_owner__open_and_load();
+	if (!ASSERT_OK_PTR(skel, "bpf_iter_page_owner__open_and_load"))
+		return;
+
+	/* --- seq file iterator: unfiltered run --- */
+	skel->bss->filter_en = 0;
+
+	if (!ASSERT_OK(bpf_iter_page_owner__attach(skel), "skel_attach"))
+		goto destroy;
+
+	link_fd = bpf_link__fd(skel->links.dump_page_owner);
+	iter_count = iter_record_count(link_fd);
+	if (iter_count < 0)
+		goto destroy;
+	unfiltered_count = iter_count;
+
+	fprintf(stderr, "iter_count=%ld printed_lines=%u total_seen=%u\n",
+		iter_count, skel->bss->printed_lines, skel->bss->total_seen);
+
+	ASSERT_GT(iter_count, 0, "unfiltered_lines_gt_0");
+
+	ASSERT_EQ((long)skel->bss->printed_lines, iter_count,
+		  "lines_match_printed");
+
+	/* unfiltered: every visited page is printed, so the two must match */
+	ASSERT_EQ(skel->bss->total_seen, skel->bss->printed_lines,
+		  "unfiltered_all_printed");
+
+	/* --- seq file iterator: pid-filtered run --- */
+	pid = getpid();
+	skel->bss->filter_en = 1;
+	skel->bss->filter_pid = (pid_t)pid;
+
+	/* printed_lines is cumulative across runs: compare the delta */
+	printed_before = skel->bss->printed_lines;
+	iter_count = iter_record_count(link_fd);
+	if (iter_count < 0)
+		goto destroy;
+
+	fprintf(stderr,
+		"pid=%ld iter_count=%ld printed_delta=%u\n",
+		pid, iter_count, skel->bss->printed_lines - printed_before);
+
+	ASSERT_GT(iter_count, 0, "filtered_lines_gt_0");
+	ASSERT_EQ(skel->bss->printed_lines - printed_before, iter_count,
+		  "filtered_printed_match");
+
+	/* --- open-coded iterator parity --- */
+	prog_fd = bpf_program__fd(skel->progs.scan_open_coded);
+
+	ASSERT_OK(bpf_prog_test_run_opts(prog_fd, &opts), "prog_test_run");
+	fprintf(stderr,
+		"open_coded_seen=%u nid_seen=%u first_nid=%d "
+		"stack_bytes=%u stack=[\n%s]\n",
+		skel->bss->open_coded_seen, skel->bss->nid_seen,
+		skel->data->first_nid, skel->bss->stack_bytes,
+		skel->bss->stack_text);
+
+	/* same eligibility as the seq path: counts must be close */
+	ASSERT_GT(skel->bss->open_coded_seen, 0, "oc_seen_gt_0");
+	oc_diff = (long)skel->bss->open_coded_seen - unfiltered_count;
+	if (oc_diff < 0)
+		oc_diff = -oc_diff;
+	ASSERT_LE(oc_diff, unfiltered_count / 100 + 64,
+		  "oc_seen_close_to_iter");
+
+	/* the nid kfunc returned a valid node for every page */
+	ASSERT_EQ(skel->bss->nid_seen, skel->bss->open_coded_seen,
+		  "nid_valid_all");
+	ASSERT_GE(skel->data->first_nid, 0, "first_nid_valid");
+
+	ASSERT_GT(skel->bss->stack_bytes, 0, "stack_snprint_ok");
+	ASSERT_OK_PTR(strstr(skel->bss->stack_text, "+0x"), "stack_frames");
+	ASSERT_OK_PTR(strchr(skel->bss->stack_text, '\n'), "stack_lines");
+
+	check_memcg_skel(skel);
+
+destroy:
+	bpf_iter_page_owner__destroy(skel);
+}
diff --git a/tools/testing/selftests/bpf/progs/bpf_iter_page_owner.c b/tools/testing/selftests/bpf/progs/bpf_iter_page_owner.c
new file mode 100644
index 000000000000..be2261b713ea
--- /dev/null
+++ b/tools/testing/selftests/bpf/progs/bpf_iter_page_owner.c
@@ -0,0 +1,159 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (c) 2026 */
+
+#include <vmlinux.h>
+#include <bpf/bpf_helpers.h>
+
+char _license[] SEC("license") = "GPL";
+
+__u32 filter_en;
+__u32 filter_pid;
+__u32 total_seen;
+__u32 printed_lines;
+
+__u32 open_coded_seen;
+__u32 nid_seen;
+__s32 first_nid = -1;
+char stack_text[2048];
+__u32 stack_bytes;
+
+__u32 memcg_seen;
+__u32 memcg_charged;
+__u32 memcg_online;
+__u32 memcg_nodata;
+__u32 memcg_notsup;
+__u32 memcg_bad;
+__u32 memcg_first_saved;
+__u32 memcg_first_flags;	/* bit0 is_online, bit1 is_kmem */
+__u32 memcg_first_path_len;
+char memcg_pathbuf[4096];
+char memcg_first_path[4096];
+char memcg_first_name[80];
+
+#define MEMCG_ENODATA 61
+#define MEMCG_EOPNOTSUPP 95
+
+#define MEMCG_F_ONLINE	(1 << 0)
+#define MEMCG_F_OBJCG	(1 << 1)
+
+extern int bpf_iter_page_owner_new(struct bpf_iter_page_owner *it) __ksym;
+extern struct page_owner_scan *
+bpf_iter_page_owner_next(struct bpf_iter_page_owner *it) __ksym;
+extern void bpf_iter_page_owner_destroy(struct bpf_iter_page_owner *it) __ksym;
+extern int bpf_page_owner_stack_snprint(struct page_owner_scan *scan,
+					char *buf__uninit, u32 buf__sz) __ksym;
+extern int bpf_page_owner_get_memcg_info(struct page_owner_scan *scan,
+					 struct memcg_info *info__uninit,
+					 char *buf__uninit, u32 buf__sz) __ksym;
+
+static __u32 copy_str(char *dst, const char *src, const __u32 max)
+{
+	__u32 i = 0;
+
+	while (i + 1 < max && src[i]) {
+		dst[i] = src[i];
+		i++;
+	}
+	dst[i] = '\0';
+	return i;
+}
+
+/*
+ * Exercise bpf_page_owner_get_memcg_info() until 32 pages charged to an
+ * ONLINE memcg have been seen, checking its per-return contract:
+ *  - 0:          buf holds an absolute cgroup path;
+ *  - -ENODATA:   buf was NUL-terminated;
+ *  - -EOPNOTSUPP: counted (CONFIG_MEMCG is off);
+ *
+ * The first online charged sample is saved for userspace to print and
+ * cross-check (name vs basename of path).
+ */
+static void check_memcg(struct page_owner_scan *scan)
+{
+	static struct memcg_info info;
+	int err;
+
+	err = bpf_page_owner_get_memcg_info(scan, &info, memcg_pathbuf,
+					    sizeof(memcg_pathbuf));
+	memcg_seen++;
+
+	if (err == 0) {
+		memcg_charged++;
+
+		if (memcg_pathbuf[0] != '/') {
+			memcg_bad++;
+			return;
+		}
+
+		if (!info.is_online)
+			return;
+		memcg_online++;
+
+		/* keep the first online sample for userspace */
+		if (!memcg_first_saved) {
+			memcg_first_path_len = copy_str(memcg_first_path,
+							memcg_pathbuf,
+							sizeof(memcg_first_path));
+			copy_str(memcg_first_name, info.name,
+				 sizeof(memcg_first_name));
+			memcg_first_flags = MEMCG_F_ONLINE |
+					    (info.is_kmem ? MEMCG_F_OBJCG : 0);
+			memcg_first_saved = 1;
+		}
+	} else if (err == -MEMCG_ENODATA) {
+		memcg_nodata++;
+		if (memcg_pathbuf[0] != '\0')
+			memcg_bad++;
+	} else if (err == -MEMCG_EOPNOTSUPP) {
+		memcg_notsup++;
+		if (memcg_pathbuf[0] != '\0')
+			memcg_bad++;
+	} else {
+		memcg_bad++;
+	}
+}
+
+SEC("syscall")
+int scan_open_coded(const void *ctx)
+{
+	struct bpf_iter_page_owner it;
+	struct page_owner_scan *scan;
+
+	bpf_iter_page_owner_new(&it);
+
+	while ((scan = bpf_iter_page_owner_next(&it))) {
+		open_coded_seen++;
+		if (scan->nid >= 0)
+			nid_seen++;
+		if (memcg_online < 32)
+			check_memcg(scan);
+		/* grab the stack of the 100th page for userspace to check */
+		if (open_coded_seen == 100) {
+			first_nid = scan->nid;
+			if (scan->po_snap.handle)
+				stack_bytes = bpf_page_owner_stack_snprint(
+					scan, stack_text, sizeof(stack_text));
+		}
+	}
+	bpf_iter_page_owner_destroy(&it);
+	return 0;
+}
+
+SEC("iter/page_owner")
+int dump_page_owner(struct bpf_iter__page_owner *ctx)
+{
+	struct page_owner *po = ctx->po;
+
+	if (!po)
+		return 0;
+
+	total_seen++;
+
+	if (filter_en && po->pid != (pid_t)filter_pid)
+		return 0;
+
+	BPF_SEQ_PRINTF(ctx->meta->seq, "pfn %llu pid %d comm %s\n",
+		       ctx->pfn, po->pid, po->comm);
+	printed_lines++;
+	return 0;
+}
-- 
2.20.1


^ permalink raw reply related	[flat|nested] 14+ messages in thread

* Re: [PATCH v2 4/6] mm/page_owner: record the NUMA node in the scan cursor
  2026-10-13 12:21 ` [PATCH v2 4/6] mm/page_owner: record the NUMA node in the scan cursor Zhen Ni
@ 2026-10-13 12:36   ` sashiko-bot
  0 siblings, 0 replies; 14+ messages in thread
From: sashiko-bot @ 2026-10-13 12:36 UTC (permalink / raw)
  To: Zhen Ni; +Cc: bpf

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] mm: incomplete fix leaves page_owner vulnerable to unmapped page dereference
--

commit 7b174b26adeea5712aaa10bd465514b5ce7dbe36
Author: Zhen Ni <zhen.ni@easystack.cn>
Subject: mm/page_owner: record the NUMA node in the scan cursor

This commit computes the page's NUMA node in page_owner_next_eligible()
and stores it in the scan cursor to avoid dereferencing a potentially
unmapped page later. It updates the debugfs nid filter and BPF program
to use this stored node value.

> diff --git a/mm/page_owner.c b/mm/page_owner.c
> index 617346a4ab22a..113e4e7e0b3a1 100644
> --- a/mm/page_owner.c
> +++ b/mm/page_owner.c
[ ... ]
> @@ -810,18 +824,8 @@ read_page_owner(struct file *file, char __user *buf, size_t count, loff_t *ppos)
>  	/* Find an allocated page */
>  	while (page_owner_next_eligible(&scan)) {
>  		if (state->nid_filter_enabled) {
> -			int nid;
> -			memdesc_flags_t page_flags =
> -				READ_ONCE(scan.page->flags);
> -
> -			/*
> -			 * Bypass PF_POISONED_CHECK() in page_to_nid() to avoid
> -			 * VM_BUG_ON when accessing poisoned pages.
> -			 */
> -			if (page_flags.f == PAGE_POISON_PATTERN)
> -				goto skip_continue;
> -			nid = memdesc_nid(&page_flags);
> -			if (!node_isset(nid, state->nid_filter))
> +			if (scan.nid < 0 ||
> +			    !node_isset(scan.nid, state->nid_filter))
>  				goto skip_continue;
>  		}
>  

[Severity: High]
Does this fix leave scan.page vulnerable to an unmapped page dereference
shortly after this block?

Although this patch successfully avoids dereferencing scan.page when checking
the node filter, page_owner_next_eligible() drops the RCU lock via
page_ext_put() before returning.

Just below the quoted block in read_page_owner(), scan.page is still
passed directly to print_page_owner():

read_page_owner() {
    ...
    while (page_owner_next_eligible(&scan)) {
        ...
        return print_page_owner(buf, count, scan.pfn, scan.page,
                                &scan.po_snap, scan.po_snap.handle, state);
    ...
}

Inside print_page_owner(), the pointer is dereferenced by scnprintf using
the %pGp formatter on &page->flags.f.

If a concurrent memory hot-remove operation frees the vmemmap backing this
struct page after the RCU lock is dropped, will this %pGp format access
trigger a kernel panic?

Additionally, a follow-up patch in this series (Patch 5) appears to introduce
get_page_memcg_info() which also unconditionally dereferences scan.page
outside the RCU lock from read_page_owner(). Does that face the same
use-after-free risk?

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261013122125.843451-1-zhen.ni@easystack.cn?part=4

^ permalink raw reply	[flat|nested] 14+ messages in thread

* Re: [PATCH v2 6/6] selftests/bpf: add tests for the page_owner bpf_iter target
  2026-10-13 12:21 ` [PATCH v2 6/6] selftests/bpf: add tests for the page_owner bpf_iter target Zhen Ni
@ 2026-10-13 12:38   ` sashiko-bot
  0 siblings, 0 replies; 14+ messages in thread
From: sashiko-bot @ 2026-10-13 12:38 UTC (permalink / raw)
  To: Zhen Ni; +Cc: bpf

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] bpf/selftests: hardcoded errno values break architecture portability
--

commit d63d4254b64f243f696e8d9b4ca8b07f371d8139
Author: Zhen Ni <zhen.ni@easystack.cn>

selftests/bpf: add tests for the page_owner bpf_iter target

This commit adds a sleepable BPF program and a userspace runner to test
the seq_file and open-coded page_owner iterators. It verifies iterator
parity, output matching, and kfunc contracts including memcg classification.

> diff --git a/tools/testing/selftests/bpf/progs/bpf_iter_page_owner.c b/tools/testing/selftests/bpf/progs/bpf_iter_page_owner.c
> new file mode 100644
> --- /dev/null
> +++ b/tools/testing/selftests/bpf/progs/bpf_iter_page_owner.c
[ ... ]
> +char memcg_pathbuf[4096];
> +char memcg_first_path[4096];
> +char memcg_first_name[80];
> +
> +#define MEMCG_ENODATA 61
> +#define MEMCG_EOPNOTSUPP 95
> +
> +#define MEMCG_F_ONLINE	(1 << 0)
> +#define MEMCG_F_OBJCG	(1 << 1)

[Severity: Medium]
Will these hardcoded errno values cause a regression in the selftests on
certain architectures?

The bpf_page_owner_get_memcg_info() kfunc returns standard kernel error
codes like -ENODATA and -EOPNOTSUPP. However, these error codes vary by
architecture. For example, ENODATA is 111 on SPARC and 86 on Alpha, while
EOPNOTSUPP is 122 on MIPS.

When the test runs on these architectures, the kfunc will return the
architecture-specific error code. This will fail the hardcoded comparison
in check_memcg(), incorrectly incrementing memcg_bad and causing the
selftest to fail.

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261013122125.843451-1-zhen.ni@easystack.cn?part=6

^ permalink raw reply	[flat|nested] 14+ messages in thread

* Re: [PATCH v2 5/6] mm/page_owner: add bpf_page_owner_get_memcg_info() kfunc
  2026-10-13 12:21 ` [PATCH v2 5/6] mm/page_owner: add bpf_page_owner_get_memcg_info() kfunc Zhen Ni
@ 2026-10-13 12:49   ` sashiko-bot
  0 siblings, 0 replies; 14+ messages in thread
From: sashiko-bot @ 2026-10-13 12:49 UTC (permalink / raw)
  To: Zhen Ni; +Cc: bpf

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [High] bpf: TOCTOU race in bpf_page_owner_get_memcg_info() leads to page fault
- [High] mm: wild pointer dereference in get_page_memcg_info() for tail pages
--

commit ab0ccc33a4ab8ccdaacb5f6b2dca8722bf6d6123
Author: Zhen Ni <zhen.ni@easystack.cn>

mm/page_owner: add bpf_page_owner_get_memcg_info() kfunc

This commit exposes per-page memcg information to BPF programs.
It splits memcg info collection out of print_page_owner_memcg() into
a new get_page_memcg_info() function and adds the
bpf_page_owner_get_memcg_info() kfunc.

Link: https://lore.kernel.org/linux-mm/20261009112308.240769-6-zhen.ni@easystack.cn/

> diff --git a/mm/page_owner.c b/mm/page_owner.c
> index 113e4e7e0b3a1..664121be66863 100644
> --- a/mm/page_owner.c
> +++ b/mm/page_owner.c
> @@ -538,16 +538,22 @@ void pagetypeinfo_showmixedcount_print(struct seq_file *m,

[ ... ]

> +int get_page_memcg_info(struct page *page, struct memcg_info *info,
> +			char *buf, int size)
>  {
>  	unsigned long memcg_data;
>  	struct obj_cgroup *objcg;
>  	struct mem_cgroup *memcg;
> +	int ret = -ENODATA;
> +
> +	*info = (struct memcg_info){};
>  
>  	rcu_read_lock();
>  	memcg_data = READ_ONCE(page->memcg_data);
> +	if (!memcg_data)
> +		goto out_unlock;

[Severity: High]
Does removing the PageTail() guard here expose a wild pointer dereference?

In struct folio, the first tail page's _nr_pages field aliases exactly with
the memcg_data offset when CONFIG_MEMCG is enabled. Due to a lack of memory
barriers in __update_page_owner_handle(), page_owner_next_eligible() can
observe PAGE_EXT_OWNER_ALLOCATED before page_owner->order is updated,
yielding a tail page.

If a tail page is passed here, the non-zero _nr_pages value (e.g., 512 for a
large folio) will bypass the !memcg_data check and be dereferenced as an
obj_cgroup pointer later in the function, leading to a kernel panic.

[ ... ]

> diff --git a/mm/page_owner_iter.c b/mm/page_owner_iter.c
> index a2b90c8748520..86e0b4b52bfc7 100644
> --- a/mm/page_owner_iter.c
> +++ b/mm/page_owner_iter.c
> @@ -177,4 +177,21 @@ __bpf_kfunc int bpf_page_owner_stack_snprint(struct page_owner_scan *scan,
>  				   0);
>  }
>  
> +__bpf_kfunc int bpf_page_owner_get_memcg_info(struct page_owner_scan *scan,
> +					      struct memcg_info *info__uninit,
> +					      char *buf__uninit, u32 buf__sz)
> +{
> +	/* Guard against the section being offlined after scan. */
> +	struct page *page = pfn_to_online_page(scan->pfn);
> +
> +	if (!page) {
> +		*info__uninit = (struct memcg_info){};
> +		if (buf__uninit && buf__sz)
> +			buf__uninit[0] = '\0';
> +		return -ENODATA;
> +	}
> +
> +	return get_page_memcg_info(page, info__uninit, buf__uninit, buf__sz);
> +}

[Severity: High]
Is this vulnerable to a time-of-check to time-of-use race with memory
hot-remove?

The pfn_to_online_page() check and subsequent get_page_memcg_info()
dereference occur without holding get_online_mems(). Because this is a
sleepable BPF kfunc (KF_SLEEPABLE), the thread can be preempted between the
check and the dereference.

If memory hot-remove offlines and removes the memory section concurrently, it
tears down the vmemmap backing the struct page. When the thread resumes and
get_page_memcg_info() attempts to read page->memcg_data, it will fault on the
unmapped address.

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261013122125.843451-1-zhen.ni@easystack.cn?part=5

^ permalink raw reply	[flat|nested] 14+ messages in thread

* Re: [PATCH v2 3/6] mm/page_owner: add open-coded page_owner iterator
  2026-10-13 12:21 ` [PATCH v2 3/6] mm/page_owner: add open-coded page_owner iterator Zhen Ni
@ 2026-10-13 12:49   ` sashiko-bot
  2026-10-13 13:51   ` bot+bpf-ci
  1 sibling, 0 replies; 14+ messages in thread
From: sashiko-bot @ 2026-10-13 12:49 UTC (permalink / raw)
  To: Zhen Ni; +Cc: bpf

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] mm: soft lockup in page_owner_next_eligible via BPF iter
--

commit b7f01516d35223b7a6800ce695c29a9203809699
Author: Zhen Ni <zhen.ni@easystack.cn>

mm/page_owner: add open-coded page_owner iterator

This patch adds an open-coded bpf_iter over the pages eligible for
page_owner output, reusing page_owner_next_eligible(). It also provides a
sleepable BPF program to drive the iterator and retrieve stack traces.

> diff --git a/mm/page_owner_iter.c b/mm/page_owner_iter.c
> index 938cb42ab7d79..ac7e69ba0cef3 100644
> --- a/mm/page_owner_iter.c
> +++ b/mm/page_owner_iter.c
[ ... ]
> @@ -122,3 +123,55 @@ int page_owner_iter_register(void)
>  
>  	return bpf_iter_reg_target(&page_owner_iter_reg_info);
>  }
> +
> +/* opaque to BPF programs */
> +struct bpf_iter_page_owner {
> +	__u64 __opaque[16];
> +} __attribute__((aligned(8)));
> +
> +struct bpf_iter_page_owner_kern {
> +	struct page_owner_scan scan;
> +} __attribute__((aligned(8)));
> +
> +__bpf_kfunc_start_defs();
[ ... ]
> +__bpf_kfunc struct page_owner_scan *
> +bpf_iter_page_owner_next(struct bpf_iter_page_owner *it)
> +{
> +	struct bpf_iter_page_owner_kern *kit = (void *)it;
> +
> +	cond_resched();
> +
> +	if (kit->scan.page)
> +		kit->scan.pfn++;
> +
> +	if (!page_owner_next_eligible(&kit->scan))
> +		return NULL;

[Severity: High]
Can this iterator cause a soft lockup or RCU stall?

If page_owner is disabled, page_ext_get() inside page_owner_next_eligible()
constantly returns NULL, triggering a continue on every single page up to
max_pfn:

mm/page_owner.c:page_owner_next_eligible() {
	...
	for (; pfn < max_pfn; pfn++) {
		...
		page_ext = page_ext_get(page);
		if (unlikely(!page_ext))
			continue;
		...
ext_put_continue:
		page_ext_put(page_ext);
		cond_resched();
	}
	...
}

Because the continue statement skips the ext_put_continue block, the loop
never calls cond_resched(). When called from this new sleepable BPF
iterator, won't it spin over the entire physical memory space in a tight
loop without yielding?

> +
> +	return &kit->scan;
> +}

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261013122125.843451-1-zhen.ni@easystack.cn?part=3

^ permalink raw reply	[flat|nested] 14+ messages in thread

* Re: [PATCH v2 2/6] mm/page_owner: add bpf_iter target "page_owner"
  2026-10-13 12:21 ` [PATCH v2 2/6] mm/page_owner: add bpf_iter target "page_owner" Zhen Ni
@ 2026-10-13 13:51   ` bot+bpf-ci
  0 siblings, 0 replies; 14+ messages in thread
From: bot+bpf-ci @ 2026-10-13 13:51 UTC (permalink / raw)
  To: zhen.ni, ast, daniel, andrii, eddyz87, memxor, martin.lau, song,
	yonghong.song, jolsa, emil, ihor.solodrai, akpm, vbabka, surenb,
	mhocko, brendan.jackman, hannes, ziy, shuah
  Cc: bpf, linux-mm, linux-kselftest, linux-kernel, zhen.ni, ast,
	andrii, daniel, martin.lau, eddyz87, yonghong.song, mason,
	ihor.solodrai

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

> mm/page_owner: add bpf_iter target "page_owner"

TL;DR:
- Missing BPF_ITER_RESCHED may let one read() loop up to 1M objects without
  yielding, risking RCU stalls on !PREEMPT
- Commit message bpftrace example counts allocations, not pages

>     mm/page_owner: add bpf_iter target "page_owner"
>
>     Register a bpf_iter target "page_owner" that walks the pages eligible
>     for page_owner output, with the same eligibility rules as the
>     /sys/kernel/debug/page_owner read path.
>
>     For each eligible page the attached BPF program receives the pfn, the
>     page and the page_owner record, and can freely filter and format output.
>
>     The po_snap record in the scan cursor is taken inside the page_ext
>     RCU window and handed to the BPF program as a copy, so no RCU window
>     is needed at consumption time. Torn reads remain possible if the
>     record is concurrently updated while it is copied, but this is the
>     same best-effort behavior as the read path has always had.
>
>     The target is registered from pageowner_init() when both
>     CONFIG_PAGE_OWNER and CONFIG_BPF_SYSCALL are enabled; a registration
>     failure only disables the bpf_iter target.
>
>     Userspace tools such as bpftrace can attach to this target like any
>     other bpf_iter. For example, to count pages owned by init (pid 1):
>
>         bpftrace -e 'iter:page_owner / ctx->po->pid == 1 / { @ = count(); }'
>
>     Signed-off-by: Zhen Ni <zhen.ni@easystack.cn>

The example in the commit message says it counts pages owned by init:

    For example, to count pages owned by init (pid 1):
        bpftrace -e 'iter:page_owner / ctx->po->pid == 1 / { @ = count(); }'

Is that accurate?  The iterator produces one record per allocation, not one
per page, because page_owner_next_eligible() skips the tail pages of
high-order allocations:

    /*
     * Don't print "tail" pages of high-order allocations as that
     * would inflate the stats.
     */
    if (!IS_ALIGNED(pfn, 1 << page_owner->order))
        goto ext_put_continue;

So '@ = count()' counts allocations made by pid 1.  An order-9 THP
allocation is counted as 1 instead of 512 pages.

Could the example or its description be corrected so that it reports what
it claims, for instance with '@ = sum(1 << ctx->po->order)' to count pages?

> diff --git a/mm/page_owner_iter.c b/mm/page_owner_iter.c
> new file mode 100644
> index 000000000000..938cb42ab7d7
> --- /dev/null
> +++ b/mm/page_owner_iter.c
> @@ -0,0 +1,124 @@

[ ... ]

> +static struct bpf_iter_reg page_owner_iter_reg_info = {
> +	.target			= "page_owner",
> +	.ctx_arg_info_size	= 2,
> +	.ctx_arg_info		= {
> +		{ offsetof(struct bpf_iter__page_owner, page),
> +		  PTR_TO_BTF_ID_OR_NULL },
> +		{ offsetof(struct bpf_iter__page_owner, po),
> +		  PTR_TO_BTF_ID_OR_NULL },
> +	},
> +	.seq_info		= &page_owner_iter_seq_info,
> +};

Should page_owner_iter_reg_info set .feature = BPF_ITER_RESCHED?

Without it, bpf_seq_read() never calls cond_resched() between objects for
this target, because can_resched = bpf_iter_support_resched(seq) is false.

page_owner_next_eligible() has no scheduling point on its hit path either:

    scan->po_snap = *page_owner;
    if (!scan->po_snap.handle)
        goto ext_put_continue;
    page_ext_put(page_ext);
    return true;

cond_resched() only runs on the ext_put_continue miss path.

When hits are back to back (runs of allocated order-0 pages, which is the
normal case for anon and page cache memory) and the BPF program writes less
than the user buffer size (the commit message's own '@ = count()' example
writes nothing to the seq_file), a single read() loops up to
MAX_ITER_OBJECTS (1,000,000) times.  Each pass does page_ext_get/put, a
struct copy and a BPF program run, with nothing yielding in between.  On
!PREEMPT kernels that holds the CPU and delays RCU quiescent states for the
whole loop.

This is the situation commit cf83b2d2e2b6 ("bpf: Permit cond_resched for
some iterators") hit in the same bpf_seq_read() loop, an rcu_sched
self-detected stall under load.  The debugfs read path doesn't have this
problem because read_page_owner() returns to userspace after every record.

The requirement for the flag looks satisfied, since nothing is held across
next() -> show(): page_owner_next_eligible() drops the page_ext RCU read
lock before returning, and already calls cond_resched() itself.  The other
iterators that can safely reschedule (task, task_file, task_vma, cgroup,
kmem_cache, dmabuf, kallsyms) all set BPF_ITER_RESCHED.

Later commits in this series (54a9d79ddbf5..e52345a241ae) do not add it.


---
AI reviewed your patch. Please fix the bug or email reply why it's not a bug.
See: https://github.com/kernel-patches/vmtest/blob/master/ci/claude/README.md

CI run summary: https://github.com/kernel-patches/bpf/actions/runs/38403671988

^ permalink raw reply	[flat|nested] 14+ messages in thread

* Re: [PATCH v2 3/6] mm/page_owner: add open-coded page_owner iterator
  2026-10-13 12:21 ` [PATCH v2 3/6] mm/page_owner: add open-coded page_owner iterator Zhen Ni
  2026-10-13 12:49   ` sashiko-bot
@ 2026-10-13 13:51   ` bot+bpf-ci
  1 sibling, 0 replies; 14+ messages in thread
From: bot+bpf-ci @ 2026-10-13 13:51 UTC (permalink / raw)
  To: zhen.ni, ast, daniel, andrii, eddyz87, memxor, martin.lau, song,
	yonghong.song, jolsa, emil, ihor.solodrai, akpm, vbabka, surenb,
	mhocko, brendan.jackman, hannes, ziy, shuah
  Cc: bpf, linux-mm, linux-kselftest, linux-kernel, zhen.ni, ast,
	andrii, daniel, martin.lau, eddyz87, yonghong.song, mason,
	ihor.solodrai

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

> mm/page_owner: add open-coded page_owner iterator

TL;DR: The new page_owner kfuncs may lack KF_PERFMON, so a CAP_BPF-only
syscall program could read page_owner records and symbolized allocation stacks
that are root-only in debugfs.

> diff --git a/kernel/bpf/helpers.c b/kernel/bpf/helpers.c
> index a284f20c97d5..85ce368315f9 100644
> --- a/kernel/bpf/helpers.c
> +++ b/kernel/bpf/helpers.c

[ ... ]

> @@ -5177,6 +5177,12 @@ BTF_ID_FLAGS(func, bpf_iter_dmabuf_new, KF_ITER_NEW | KF_SLEEPABLE)
>  BTF_ID_FLAGS(func, bpf_iter_dmabuf_next, KF_ITER_NEXT | KF_RET_NULL | KF_SLEEPABLE)
>  BTF_ID_FLAGS(func, bpf_iter_dmabuf_destroy, KF_ITER_DESTROY | KF_SLEEPABLE)
>  #endif
> +#ifdef CONFIG_PAGE_OWNER
> +BTF_ID_FLAGS(func, bpf_iter_page_owner_new, KF_ITER_NEW | KF_SLEEPABLE)
> +BTF_ID_FLAGS(func, bpf_iter_page_owner_next, KF_ITER_NEXT | KF_RET_NULL | KF_SLEEPABLE)
> +BTF_ID_FLAGS(func, bpf_iter_page_owner_destroy, KF_ITER_DESTROY | KF_SLEEPABLE)
> +BTF_ID_FLAGS(func, bpf_page_owner_stack_snprint, KF_SLEEPABLE)
> +#endif
>  BTF_ID_FLAGS(func, __bpf_trap)
>  BTF_ID_FLAGS(func, bpf_strcmp, KF_PERFMON);
>  BTF_ID_FLAGS(func, bpf_strcasecmp, KF_PERFMON);

Should bpf_page_owner_stack_snprint(), and probably bpf_iter_page_owner_new(),
carry KF_PERFMON here?

These kfuncs are added to common_btf_ids, which is registered for
BPF_PROG_TYPE_UNSPEC, and none of them has KF_PERFMON.

BPF_PROG_TYPE_SYSCALL programs are always sleepable (check_attach_btf_id()
requires it), and they only need CAP_BPF (or a delegated BPF token) since
SYSCALL is not covered by is_perfmon_prog_type().

That means a CAP_BPF-only loader can walk all page_owner records and use
bpf_page_owner_stack_snprint() to copy symbolized allocation stack traces
into program memory.  This skips the allow_ptr_leaks gate that
check_ptr_to_btf_access() applies when the same data is read field by field.

The debugfs interface that exposes this information is root-only: page_owner
is 0600 and the page_owner_stacks/* files are 0400.

This tree already has KF_PERFMON (commit 88ce88e933e4 "bpf: Add KF_PERFMON
kfunc flag"), and comparable kfuncs use it, for example bpf_get_kmem_cache()
and bpf_probe_read_kernel_dynptr() in this same table.


---
AI reviewed your patch. Please fix the bug or email reply why it's not a bug.
See: https://github.com/kernel-patches/vmtest/blob/master/ci/claude/README.md

CI run summary: https://github.com/kernel-patches/bpf/actions/runs/38403671988

^ permalink raw reply	[flat|nested] 14+ messages in thread

* Re: [PATCH v2 0/6] mm/page_owner: add bpf_iter and kfuncs
  2026-10-13 12:21 [PATCH v2 0/6] mm/page_owner: add bpf_iter and kfuncs Zhen Ni
                   ` (5 preceding siblings ...)
  2026-10-13 12:21 ` [PATCH v2 6/6] selftests/bpf: add tests for the page_owner bpf_iter target Zhen Ni
@ 2026-10-13 17:17 ` Zi Yan
  6 siblings, 0 replies; 14+ messages in thread
From: Zi Yan @ 2026-10-13 17:17 UTC (permalink / raw)
  To: Zhen Ni, ast, daniel, andrii, eddyz87, memxor, martin.lau, song,
	yonghong.song, jolsa, emil, ihor.solodrai, akpm, vbabka, surenb,
	mhocko, brendan.jackman, hannes, shuah
  Cc: bpf, linux-mm, linux-kselftest, linux-kernel

On Tue Oct 13, 2026 at 8:21 AM EDT, Zhen Ni wrote:
> Hi all,
>
> Introduction
> ============
>
> page_owner records who allocated every page and is a core memory-debug
> facility, but it currently reaches userspace as the debugfs flat dump:
> slicing allocations by pid, comm, NUMA node, cgroup or stack, etc.,
> means streaming and parsing the entire file in userspace, which is
> expensive on large hosts and hard to query live.
>
> Exposing the same walk to eBPF lets a program filter and aggregate in
> the kernel and surface only the records it cares about. This series
> adds that access as a seq_file bpf_iter target and an open-coded
> iterator.
>
> Implementation
> ==============
>
> The page walk is factored into page_owner_next_eligible(), shared by
> the read path and the iterators so eligibility stays identical. Two
> kfuncs expose information that a BPF program cannot decode from the
> snapshot alone: per-page memcg info including the full cgroup path,
> and the stack trace behind a stack_depot handle. Each record is
> snapshotted under the page_ext RCU lock, so consumers read a
> consistent copy.
>
> Testing
> =======
>
> The selftest exercises the seq_file and open-coded iterators, and
> validates the stack_snprint and get_memcg_info kfuncs.
>
> Changes in v2
> =============
> - Route the zero-handle case through ext_put_continue.
> - Drop the scan->pfn++.
> - Fill page/po/pfn only when v is non-NULL.
> - Implement page_owner_seq_stop().
> - bpf_page_owner_stack_snprint() now takes the scan cursor.
> - Buffer args renamed to buf__uninit/buf__sz.
> - Removes the v1 get_nid() kfunc. The node is now stored in scan->nid.
> - bpf_page_owner_get_memcg_info() now resolves the page with
>   pfn_to_online_page() for an offline or removed pfn.
> - Buffer/info args renamed to info__uninit/buf__uninit/buf__sz.
> - Drop the get_nid kfunc coverage.
> - Enable CONFIG_PAGE_OWNER in the bpf selftests config.

Hi Zhen,

Thank you for your patches. Review focus is currently on hotfixes and
patches already in mm tree. Please ping us on this after the merge
window is close, if no one still has not looked at it by then.

Thanks.

>
> v1: https://lore.kernel.org/linux-mm/20261009112308.240769-1-zhen.ni@easystack.cn/
>
> Zhen Ni (6):
>   mm/page_owner: extract page_owner_next_eligible() from read loop
>   mm/page_owner: add bpf_iter target "page_owner"
>   mm/page_owner: add open-coded page_owner iterator
>   mm/page_owner: record the NUMA node in the scan cursor
>   mm/page_owner: add bpf_page_owner_get_memcg_info() kfunc
>   selftests/bpf: add tests for the page_owner bpf_iter target
>
>  kernel/bpf/helpers.c                          |   7 +
>  mm/Makefile                                   |   3 +
>  mm/page_owner.c                               | 219 +++++++++++-------
>  mm/page_owner.h                               |  58 +++++
>  mm/page_owner_iter.c                          | 197 ++++++++++++++++
>  tools/testing/selftests/bpf/config            |   2 +
>  .../bpf/prog_tests/page_owner_iter.c          | 193 +++++++++++++++
>  .../selftests/bpf/progs/bpf_iter_page_owner.c | 159 +++++++++++++
>  8 files changed, 758 insertions(+), 80 deletions(-)
>  create mode 100644 mm/page_owner.h
>  create mode 100644 mm/page_owner_iter.c
>  create mode 100644 tools/testing/selftests/bpf/prog_tests/page_owner_iter.c
>  create mode 100644 tools/testing/selftests/bpf/progs/bpf_iter_page_owner.c




-- 
Best Regards,
Yan, Zi


^ permalink raw reply	[flat|nested] 14+ messages in thread

end of thread, other threads:[~2026-10-13 17:17 UTC | newest]

Thread overview: 14+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-13 12:21 [PATCH v2 0/6] mm/page_owner: add bpf_iter and kfuncs Zhen Ni
2026-10-13 12:21 ` [PATCH v2 1/6] mm/page_owner: extract page_owner_next_eligible() from read loop Zhen Ni
2026-10-13 12:21 ` [PATCH v2 2/6] mm/page_owner: add bpf_iter target "page_owner" Zhen Ni
2026-10-13 13:51   ` bot+bpf-ci
2026-10-13 12:21 ` [PATCH v2 3/6] mm/page_owner: add open-coded page_owner iterator Zhen Ni
2026-10-13 12:49   ` sashiko-bot
2026-10-13 13:51   ` bot+bpf-ci
2026-10-13 12:21 ` [PATCH v2 4/6] mm/page_owner: record the NUMA node in the scan cursor Zhen Ni
2026-10-13 12:36   ` sashiko-bot
2026-10-13 12:21 ` [PATCH v2 5/6] mm/page_owner: add bpf_page_owner_get_memcg_info() kfunc Zhen Ni
2026-10-13 12:49   ` sashiko-bot
2026-10-13 12:21 ` [PATCH v2 6/6] selftests/bpf: add tests for the page_owner bpf_iter target Zhen Ni
2026-10-13 12:38   ` sashiko-bot
2026-10-13 17:17 ` [PATCH v2 0/6] mm/page_owner: add bpf_iter and kfuncs Zi Yan

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