From: Brahmajit Das <[email protected]>

The existing cgroup_iter_memcg test covers bpf_get_mem_cgroup(),
bpf_put_mem_cgroup(), bpf_mem_cgroup_flush_stats(),
bpf_mem_cgroup_page_state() and bpf_mem_cgroup_vm_events(), but
bpf_get_root_mem_cgroup(), bpf_mem_cgroup_usage(),
bpf_mem_cgroup_memory_events() and bpf_proactive_reclaim() have no
selftest coverage.

Add a memcg_kfuncs test that drives them from syscall programs against
a test cgroup:

 - usage: bpf_mem_cgroup_usage() agrees with memory.current.
 - invalid_idx: out-of-range and negative indices passed to
   bpf_mem_cgroup_memory_events(), bpf_mem_cgroup_vm_events() and
   bpf_mem_cgroup_page_state() return (unsigned long)-1.
 - memory_events: after pushing the cgroup over memory.high,
   MEMCG_HIGH is non-zero and the low/high/max/oom/oom_kill counters
   match memory.events.
 - root_memcg: bpf_get_root_mem_cgroup() can be acquired and released.
 - reclaim_args: bpf_proactive_reclaim() rejects sizes below PAGE_SIZE
   and out-of-range swappiness with -EINVAL, and accepts -1, 0,
   MAX_SWAPPINESS and SWAPPINESS_ANON_ONLY.
 - reclaim: bpf_proactive_reclaim() reclaims clean page cache and the
   result is clamped to MEMCG_CHARGE_BATCH pages.

The page cache subtests skip when the working directory cannot back
a reclaimable O_TMPFILE, as in arena_memcg.

Also add memcg_kfuncs_failure with verifier tests for a leaked
reference from bpf_get_root_mem_cgroup(), a missing NULL check on its
result, and a call to bpf_proactive_reclaim() from a non-syscall
program.

Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Brahmajit Das <[email protected]>
---
v1 -> v2:
        - Also check that negative indices are rejected by
bpf_mem_cgroup_memory_events() and bpf_mem_cgroup_vm_events() (Sashiko)

v1: https://lore.kernel.org/bpf/[email protected]/
---
 .../selftests/bpf/prog_tests/memcg_kfuncs.c   | 302 ++++++++++++++++++
 .../selftests/bpf/progs/memcg_kfuncs.c        | 106 ++++++
 .../bpf/progs/memcg_kfuncs_failure.c          |  54 ++++
 3 files changed, 462 insertions(+)
 create mode 100644 tools/testing/selftests/bpf/prog_tests/memcg_kfuncs.c
 create mode 100644 tools/testing/selftests/bpf/progs/memcg_kfuncs.c
 create mode 100644 tools/testing/selftests/bpf/progs/memcg_kfuncs_failure.c

diff --git a/tools/testing/selftests/bpf/prog_tests/memcg_kfuncs.c 
b/tools/testing/selftests/bpf/prog_tests/memcg_kfuncs.c
new file mode 100644
index 000000000000..4022d97dc1a9
--- /dev/null
+++ b/tools/testing/selftests/bpf/prog_tests/memcg_kfuncs.c
@@ -0,0 +1,302 @@
+// SPDX-License-Identifier: GPL-2.0
+#define _GNU_SOURCE
+#include <test_progs.h>
+#include <fcntl.h>
+#include <linux/magic.h>
+#include <sys/mman.h>
+#include <sys/vfs.h>
+#include <unistd.h>
+
+#include "cgroup_helpers.h"
+#include "memcg_kfuncs.skel.h"
+#include "memcg_kfuncs_failure.skel.h"
+
+#define CG_PATH                "/memcg_kfuncs"
+
+/* From mm/internal.h, not exposed in BTF. */
+#define MAX_SWAPPINESS         200
+#define SWAPPINESS_ANON_ONLY   (MAX_SWAPPINESS + 1)
+
+/* bpf_proactive_reclaim() clamps @size to MEMCG_CHARGE_BATCH pages. */
+#define MEMCG_CHARGE_BATCH     64
+
+#define ANON_SIZE              (4 * 1024 * 1024)
+#define USAGE_SLACK            (1 * 1024 * 1024)
+#define PAGECACHE_SIZE         (32 * 1024 * 1024)
+#define HIGH_HEADROOM          (1 * 1024 * 1024)
+#define RECLAIM_RETRIES                16
+
+static long page_size;
+
+static int run_prog(struct bpf_program *prog)
+{
+       LIBBPF_OPTS(bpf_test_run_opts, opts);
+       int err;
+
+       err = bpf_prog_test_run_opts(bpf_program__fd(prog), &opts);
+       if (!ASSERT_OK(err, "bpf_prog_test_run_opts"))
+               return -1;
+       if (!ASSERT_EQ(opts.retval, 0, "retval"))
+               return -1;
+       return 0;
+}
+
+static int read_memcg_ulong(const char *file, unsigned long *val)
+{
+       char buf[64];
+
+       if (read_cgroup_file(CG_PATH, file, buf, sizeof(buf)))
+               return -1;
+       *val = strtoul(buf, NULL, 10);
+       return 0;
+}
+
+static int read_memory_event(const char *key, unsigned long *val)
+{
+       char buf[512], name[64], *line, *saveptr;
+       unsigned long v;
+
+       if (read_cgroup_file(CG_PATH, "memory.events", buf, sizeof(buf)))
+               return -1;
+
+       for (line = strtok_r(buf, "\n", &saveptr); line;
+            line = strtok_r(NULL, "\n", &saveptr)) {
+               if (sscanf(line, "%63s %lu", name, &v) != 2)
+                       continue;
+               if (!strcmp(name, key)) {
+                       *val = v;
+                       return 0;
+               }
+       }
+       return -1;
+}
+
+/*
+ * Fill the page cache with @size bytes of clean, reclaimable pages by
+ * reading a sparse temp file. Returns the fd, which must stay open, or
+ * -EOPNOTSUPP if the working directory cannot back such a file, -1 on error.
+ */
+static int alloc_pagecache(size_t size)
+{
+       struct statfs stfs;
+       char buf[4096];
+       size_t off;
+       int fd;
+
+       fd = open(".", O_TMPFILE | O_RDWR | O_EXCL, 0600);
+       if (fd < 0)
+               return errno == EOPNOTSUPP ? -EOPNOTSUPP : -1;
+       /* tmpfs hands out shmem pages, which are not reclaimable without swap 
*/
+       if (fstatfs(fd, &stfs) || stfs.f_type == TMPFS_MAGIC) {
+               close(fd);
+               return -EOPNOTSUPP;
+       }
+       if (ftruncate(fd, size))
+               goto err;
+       for (off = 0; off < size; off += sizeof(buf))
+               if (read(fd, buf, sizeof(buf)) < 0)
+                       goto err;
+       return fd;
+err:
+       close(fd);
+       return -1;
+}
+
+static int alloc_pagecache_or_skip(size_t size)
+{
+       int fd;
+
+       fd = alloc_pagecache(size);
+       if (fd == -EOPNOTSUPP) {
+               test__skip();
+               return -1;
+       }
+       ASSERT_OK_FD(fd, "alloc_pagecache");
+       return fd;
+}
+
+static void test_usage(struct memcg_kfuncs *skel)
+{
+       unsigned long before, after, lo, hi;
+       void *map;
+
+       map = mmap(NULL, ANON_SIZE, PROT_READ | PROT_WRITE,
+                  MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
+       if (!ASSERT_NEQ(map, MAP_FAILED, "mmap anon"))
+               return;
+       memset(map, 1, ANON_SIZE);
+
+       if (!ASSERT_OK(read_memcg_ulong("memory.current", &before), "read 
before"))
+               goto out;
+       if (run_prog(skel->progs.memcg_query))
+               goto out;
+       if (!ASSERT_OK(read_memcg_ulong("memory.current", &after), "read 
after"))
+               goto out;
+
+       /*
+        * bpf_mem_cgroup_usage() and memory.current read the same counter,
+        * but the cgroup may still be charged or uncharged in between.
+        */
+       lo = (before < after ? before : after) - USAGE_SLACK;
+       hi = (before < after ? after : before) + USAGE_SLACK;
+       ASSERT_GE(skel->bss->usage, ANON_SIZE, "usage covers anon");
+       ASSERT_GE(skel->bss->usage, lo, "usage lower bound");
+       ASSERT_LE(skel->bss->usage, hi, "usage upper bound");
+out:
+       munmap(map, ANON_SIZE);
+}
+
+static void test_invalid_idx(struct memcg_kfuncs *skel)
+{
+       if (run_prog(skel->progs.memcg_query))
+               return;
+
+       ASSERT_EQ(skel->bss->bad_memory_event, -1UL, "bad memory event");
+       ASSERT_EQ(skel->bss->bad_vm_event, -1UL, "bad vm event");
+       ASSERT_EQ(skel->bss->bad_page_state, -1UL, "bad page state");
+       ASSERT_EQ(skel->bss->neg_memory_event, -1UL, "negative memory event");
+       ASSERT_EQ(skel->bss->neg_vm_event, -1UL, "negative vm event");
+}
+
+static void test_memory_events(struct memcg_kfuncs *skel)
+{
+       unsigned long cur, val;
+       char buf[64];
+       int fd;
+
+       if (!ASSERT_OK(read_memcg_ulong("memory.current", &cur), 
"memory.current"))
+               return;
+
+       /*
+        * Push the cgroup over memory.high to raise MEMCG_HIGH. Every charge
+        * above the limit reclaims, so only overshoot it slightly.
+        */
+       snprintf(buf, sizeof(buf), "%lu", cur + HIGH_HEADROOM);
+       if (!ASSERT_OK(write_cgroup_file(CG_PATH, "memory.high", buf), "set 
memory.high"))
+               return;
+       fd = alloc_pagecache_or_skip(2 * HIGH_HEADROOM);
+       write_cgroup_file(CG_PATH, "memory.high", "max");
+       if (fd < 0)
+               return;
+
+       if (run_prog(skel->progs.memcg_query))
+               goto out;
+
+       ASSERT_GT(skel->bss->ev_high, 0, "high event raised");
+
+       if (ASSERT_OK(read_memory_event("low", &val), "read low"))
+               ASSERT_EQ(skel->bss->ev_low, val, "low");
+       if (ASSERT_OK(read_memory_event("high", &val), "read high"))
+               ASSERT_EQ(skel->bss->ev_high, val, "high");
+       if (ASSERT_OK(read_memory_event("max", &val), "read max"))
+               ASSERT_EQ(skel->bss->ev_max, val, "max");
+       if (ASSERT_OK(read_memory_event("oom", &val), "read oom"))
+               ASSERT_EQ(skel->bss->ev_oom, val, "oom");
+       if (ASSERT_OK(read_memory_event("oom_kill", &val), "read oom_kill"))
+               ASSERT_EQ(skel->bss->ev_oom_kill, val, "oom_kill");
+out:
+       close(fd);
+}
+
+static void test_root_memcg(struct memcg_kfuncs *skel)
+{
+       if (run_prog(skel->progs.root_memcg_query))
+               return;
+
+       ASSERT_GT(skel->bss->root_usage, 0, "root usage");
+}
+
+static long do_reclaim(struct memcg_kfuncs *skel, unsigned long size,
+                      int swappiness)
+{
+       skel->bss->reclaim_size = size;
+       skel->bss->reclaim_swappiness = swappiness;
+       skel->bss->reclaim_ret = 0;
+       if (run_prog(skel->progs.memcg_reclaim))
+               return LONG_MIN;
+       return skel->bss->reclaim_ret;
+}
+
+static void test_reclaim_args(struct memcg_kfuncs *skel)
+{
+       ASSERT_EQ(do_reclaim(skel, 0, -1), -EINVAL, "size 0");
+       ASSERT_EQ(do_reclaim(skel, page_size - 1, -1), -EINVAL, "size < 
PAGE_SIZE");
+       ASSERT_EQ(do_reclaim(skel, page_size, -2), -EINVAL, "swappiness -2");
+       ASSERT_EQ(do_reclaim(skel, page_size, SWAPPINESS_ANON_ONLY + 1), 
-EINVAL,
+                 "swappiness too large");
+
+       ASSERT_GE(do_reclaim(skel, page_size, -1), 0, "swappiness -1");
+       ASSERT_GE(do_reclaim(skel, page_size, 0), 0, "swappiness 0");
+       ASSERT_GE(do_reclaim(skel, page_size, MAX_SWAPPINESS), 0, "swappiness 
max");
+       ASSERT_GE(do_reclaim(skel, page_size, SWAPPINESS_ANON_ONLY), 0,
+                 "swappiness anon only");
+}
+
+static void test_reclaim(struct memcg_kfuncs *skel)
+{
+       long ret = 0;
+       int fd, i;
+
+       fd = alloc_pagecache_or_skip(PAGECACHE_SIZE);
+       if (fd < 0)
+               return;
+
+       /* Ask for more than one batch to check the clamp. */
+       for (i = 0; i < RECLAIM_RETRIES && ret <= 0; i++)
+               ret = do_reclaim(skel, 4 * MEMCG_CHARGE_BATCH * page_size, 0);
+
+       ASSERT_GT(ret, 0, "reclaimed");
+       ASSERT_LE(ret, MEMCG_CHARGE_BATCH * page_size, "reclaim clamped");
+       ASSERT_EQ(ret % page_size, 0, "reclaim page aligned");
+
+       close(fd);
+}
+
+void test_memcg_kfuncs(void)
+{
+       struct memcg_kfuncs *skel = NULL;
+       unsigned long cur;
+       int cgroup_fd;
+
+       page_size = sysconf(_SC_PAGESIZE);
+
+       cgroup_fd = cgroup_setup_and_join(CG_PATH);
+       if (!ASSERT_OK_FD(cgroup_fd, "cgroup_setup_and_join"))
+               return;
+
+       if (read_memcg_ulong("memory.current", &cur)) {
+               test__skip();
+               goto out;
+       }
+
+       skel = memcg_kfuncs__open_and_load();
+       if (!ASSERT_OK_PTR(skel, "memcg_kfuncs__open_and_load"))
+               goto out;
+
+       skel->bss->cgid = get_cgroup_id(CG_PATH);
+       if (!ASSERT_NEQ(skel->bss->cgid, 0, "get_cgroup_id"))
+               goto out;
+
+       if (test__start_subtest("usage"))
+               test_usage(skel);
+       if (test__start_subtest("invalid_idx"))
+               test_invalid_idx(skel);
+       if (test__start_subtest("memory_events"))
+               test_memory_events(skel);
+       if (test__start_subtest("root_memcg"))
+               test_root_memcg(skel);
+       if (test__start_subtest("reclaim_args"))
+               test_reclaim_args(skel);
+       if (test__start_subtest("reclaim"))
+               test_reclaim(skel);
+
+out:
+       memcg_kfuncs__destroy(skel);
+       close(cgroup_fd);
+       cleanup_cgroup_environment();
+}
+
+void test_memcg_kfuncs_failure(void)
+{
+       RUN_TESTS(memcg_kfuncs_failure);
+}
diff --git a/tools/testing/selftests/bpf/progs/memcg_kfuncs.c 
b/tools/testing/selftests/bpf/progs/memcg_kfuncs.c
new file mode 100644
index 000000000000..5e2ea19a072d
--- /dev/null
+++ b/tools/testing/selftests/bpf/progs/memcg_kfuncs.c
@@ -0,0 +1,106 @@
+// SPDX-License-Identifier: GPL-2.0
+#include <vmlinux.h>
+#include <bpf/bpf_helpers.h>
+#include <bpf/bpf_core_read.h>
+
+char _license[] SEC("license") = "GPL";
+
+struct cgroup *bpf_cgroup_from_id(u64 cgid) __ksym;
+void bpf_cgroup_release(struct cgroup *cgrp) __ksym;
+
+/* in */
+u64 cgid;
+unsigned long reclaim_size;
+int reclaim_swappiness;
+
+/* out */
+unsigned long usage;
+unsigned long ev_low, ev_high, ev_max, ev_oom, ev_oom_kill;
+unsigned long bad_memory_event;
+unsigned long bad_vm_event;
+unsigned long bad_page_state;
+unsigned long neg_memory_event;
+unsigned long neg_vm_event;
+unsigned long root_usage;
+long reclaim_ret;
+
+static struct mem_cgroup *memcg_from_cgid(void)
+{
+       struct mem_cgroup *memcg;
+       struct cgroup *cgrp;
+
+       cgrp = bpf_cgroup_from_id(cgid);
+       if (!cgrp)
+               return NULL;
+
+       memcg = bpf_get_mem_cgroup(&cgrp->self);
+       bpf_cgroup_release(cgrp);
+       return memcg;
+}
+
+SEC("syscall")
+int memcg_query(void *ctx)
+{
+       struct mem_cgroup *memcg;
+
+       memcg = memcg_from_cgid();
+       if (!memcg)
+               return 1;
+
+       usage = bpf_mem_cgroup_usage(memcg);
+
+       ev_low = bpf_mem_cgroup_memory_events(memcg,
+               bpf_core_enum_value(enum memcg_memory_event, MEMCG_LOW));
+       ev_high = bpf_mem_cgroup_memory_events(memcg,
+               bpf_core_enum_value(enum memcg_memory_event, MEMCG_HIGH));
+       ev_max = bpf_mem_cgroup_memory_events(memcg,
+               bpf_core_enum_value(enum memcg_memory_event, MEMCG_MAX));
+       ev_oom = bpf_mem_cgroup_memory_events(memcg,
+               bpf_core_enum_value(enum memcg_memory_event, MEMCG_OOM));
+       ev_oom_kill = bpf_mem_cgroup_memory_events(memcg,
+               bpf_core_enum_value(enum memcg_memory_event, MEMCG_OOM_KILL));
+
+       /* Out-of-range indices must be rejected with (unsigned long)-1. */
+       bad_memory_event = bpf_mem_cgroup_memory_events(memcg,
+               bpf_core_enum_value(enum memcg_memory_event, 
MEMCG_NR_MEMORY_EVENTS));
+       bad_vm_event = bpf_mem_cgroup_vm_events(memcg,
+               bpf_core_enum_value(enum vm_event_item, NR_VM_EVENT_ITEMS));
+       bad_page_state = bpf_mem_cgroup_page_state(memcg, -1);
+       /* Negative indices must not bypass the bounds checks. */
+       neg_memory_event = bpf_mem_cgroup_memory_events(memcg, -1);
+       neg_vm_event = bpf_mem_cgroup_vm_events(memcg, -1);
+
+       bpf_put_mem_cgroup(memcg);
+       return 0;
+}
+
+SEC("syscall")
+int root_memcg_query(void *ctx)
+{
+       struct mem_cgroup *memcg;
+
+       memcg = bpf_get_root_mem_cgroup();
+       if (!memcg)
+               return 1;
+
+       root_usage = bpf_mem_cgroup_usage(memcg);
+
+       bpf_put_mem_cgroup(memcg);
+       return 0;
+}
+
+SEC("syscall")
+int memcg_reclaim(void *ctx)
+{
+       struct mem_cgroup *memcg;
+
+       memcg = memcg_from_cgid();
+       if (!memcg)
+               return 1;
+
+       reclaim_ret = bpf_proactive_reclaim(memcg, reclaim_size,
+                                           reclaim_swappiness);
+
+       bpf_put_mem_cgroup(memcg);
+       return 0;
+}
diff --git a/tools/testing/selftests/bpf/progs/memcg_kfuncs_failure.c 
b/tools/testing/selftests/bpf/progs/memcg_kfuncs_failure.c
new file mode 100644
index 000000000000..3fe9f5a14707
--- /dev/null
+++ b/tools/testing/selftests/bpf/progs/memcg_kfuncs_failure.c
@@ -0,0 +1,54 @@
+// SPDX-License-Identifier: GPL-2.0
+#include <vmlinux.h>
+#include <bpf/bpf_helpers.h>
+
+#include "bpf_misc.h"
+
+char _license[] SEC("license") = "GPL";
+
+SEC("syscall")
+__failure __msg("Unreleased reference")
+int root_memcg_leak(void *ctx)
+{
+       struct mem_cgroup *memcg;
+
+       memcg = bpf_get_root_mem_cgroup();
+       if (!memcg)
+               return 0;
+
+       /* Missing bpf_put_mem_cgroup(). */
+       return 0;
+}
+
+SEC("syscall")
+__failure __msg("Possibly NULL pointer passed to trusted R1")
+int root_memcg_no_null_check(void *ctx)
+{
+       struct mem_cgroup *memcg;
+
+       memcg = bpf_get_root_mem_cgroup();
+       /* bpf_get_root_mem_cgroup() is KF_RET_NULL. */
+       bpf_mem_cgroup_usage(memcg);
+       bpf_put_mem_cgroup(memcg);
+       return 0;
+}
+
+SEC("iter.s/cgroup")
+__failure __msg("calling kernel function bpf_proactive_reclaim is not allowed")
+int proactive_reclaim_not_syscall(struct bpf_iter__cgroup *ctx)
+{
+       struct cgroup *cgrp = ctx->cgroup;
+       struct mem_cgroup *memcg;
+
+       if (!cgrp)
+               return 0;
+
+       memcg = bpf_get_mem_cgroup(&cgrp->self);
+       if (!memcg)
+               return 0;
+
+       /* Only BPF_PROG_TYPE_SYSCALL may reclaim. */
+       bpf_proactive_reclaim(memcg, 4096, -1);
+       bpf_put_mem_cgroup(memcg);
+       return 0;
+}
-- 
2.56.0


Reply via email to