From: Zhen Ni <zhen.ni@easystack.cn>
To: ast@kernel.org, daniel@iogearbox.net, andrii@kernel.org,
eddyz87@gmail.com, memxor@gmail.com, martin.lau@linux.dev,
song@kernel.org, yonghong.song@linux.dev, jolsa@kernel.org,
emil@etsalapatis.com, ihor.solodrai@linux.dev,
akpm@linux-foundation.org, vbabka@kernel.org, surenb@google.com,
mhocko@suse.com, brendan.jackman@linux.dev, hannes@cmpxchg.org,
ziy@nvidia.com, shuah@kernel.org
Cc: bpf@vger.kernel.org, linux-mm@kvack.org,
linux-kselftest@vger.kernel.org, linux-kernel@vger.kernel.org,
Zhen Ni <zhen.ni@easystack.cn>
Subject: [PATCH 6/6] selftests/bpf: add tests for the page_owner bpf_iter target
Date: Fri, 9 Oct 2026 19:23:08 +0800 [thread overview]
Message-ID: <20261009112308.240769-7-zhen.ni@easystack.cn> (raw)
In-Reply-To: <20261009112308.240769-1-zhen.ni@easystack.cn>
Add a sleepable BPF program and a userspace runner covering the seq_file
and open-coded page_owner iterators and the get_nid, stack_snprint and
get_memcg_info kfuncs. 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: valid nid per page, a non-empty stack
trace, and memcg classification.
Skipped when debugfs page_owner is unavailable.
Signed-off-by: Zhen Ni <zhen.ni@easystack.cn>
---
.../bpf/prog_tests/page_owner_iter.c | 173 ++++++++++++++++++
.../selftests/bpf/progs/bpf_iter_page_owner.c | 161 ++++++++++++++++
2 files changed, 334 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/prog_tests/page_owner_iter.c b/tools/testing/selftests/bpf/prog_tests/page_owner_iter.c
new file mode 100644
index 000000000000..a621712272bc
--- /dev/null
+++ b/tools/testing/selftests/bpf/prog_tests/page_owner_iter.c
@@ -0,0 +1,173 @@
+// 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, err;
+ 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 (!f) {
+ close(iter_fd);
+ return -1;
+ }
+ while (getline(&line, &len, f) > 0)
+ count++;
+ err = ferror(f);
+ free(line);
+ fclose(f);
+ return err ? -1 : 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()) {
+ 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..fada6a314a15
--- /dev/null
+++ b/tools/testing/selftests/bpf/progs/bpf_iter_page_owner.c
@@ -0,0 +1,161 @@
+// 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(depot_stack_handle_t handle,
+ char *buf, u32 buf_size) __ksym;
+extern int bpf_page_owner_get_nid(struct page_owner_scan *scan) __ksym;
+extern int bpf_page_owner_get_memcg_info(struct page_owner_scan *scan,
+ struct memcg_info *info,
+ char *buf, u32 size) __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 (bpf_page_owner_get_nid(scan) >= 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 = bpf_page_owner_get_nid(scan);
+ if (scan->po_snap.handle)
+ stack_bytes = bpf_page_owner_stack_snprint(
+ scan->po_snap.handle,
+ 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
next prev parent reply other threads:[~2026-10-09 12:39 UTC|newest]
Thread overview: 7+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-09 11:23 [PATCH 0/6] mm/page_owner: add bpf_iter and kfuncs Zhen Ni
2026-10-09 11:23 ` [PATCH 2/6] mm/page_owner: add bpf_iter target "page_owner" Zhen Ni
2026-10-09 11:23 ` [PATCH 3/6] mm/page_owner: add open-coded page_owner iterator Zhen Ni
2026-10-09 11:23 ` [PATCH 4/6] mm/page_owner: add bpf_page_owner_get_nid() kfunc Zhen Ni
2026-10-09 11:23 ` [PATCH 5/6] mm/page_owner: add bpf_page_owner_get_memcg_info() kfunc Zhen Ni
2026-10-09 11:23 ` Zhen Ni [this message]
2026-10-09 13:48 ` [PATCH 6/6] selftests/bpf: add tests for the page_owner bpf_iter target bot+bpf-ci
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20261009112308.240769-7-zhen.ni@easystack.cn \
--to=zhen.ni@easystack.cn \
--cc=akpm@linux-foundation.org \
--cc=andrii@kernel.org \
--cc=ast@kernel.org \
--cc=bpf@vger.kernel.org \
--cc=brendan.jackman@linux.dev \
--cc=daniel@iogearbox.net \
--cc=eddyz87@gmail.com \
--cc=emil@etsalapatis.com \
--cc=hannes@cmpxchg.org \
--cc=ihor.solodrai@linux.dev \
--cc=jolsa@kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=linux-kselftest@vger.kernel.org \
--cc=linux-mm@kvack.org \
--cc=martin.lau@linux.dev \
--cc=memxor@gmail.com \
--cc=mhocko@suse.com \
--cc=shuah@kernel.org \
--cc=song@kernel.org \
--cc=surenb@google.com \
--cc=vbabka@kernel.org \
--cc=yonghong.song@linux.dev \
--cc=ziy@nvidia.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®