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


Reply via email to