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 <[email protected]>
---
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/[email protected]/
---
 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


Reply via email to