| Seq | Timestamp | Type | Name | Duration |
|---|---|---|---|---|
| 0/0 | 2026/08/19 07:44 | flow | patch-triage |
0mArgs:null Results: {
"EnableConfigs": [
"MEMCG",
"BPF_SYSCALL"
],
"FocusSymbols": [
"bpf_proactive_reclaim",
"bpf_proactive_reclaim_swappiness"
],
"KMSANReasoning": "The patch introduces two new BPF kfuncs (`bpf_proactive_reclaim` and `bpf_proactive_reclaim_swappiness`) that wrap the existing `try_to_free_mem_cgroup_pages` function to allow BPF programs to trigger proactive memory reclaim for a memory cgroup. The arguments passed to these kfuncs are scalar values and pointers verified by the BPF verifier, ensuring they are initialized. The patch does not introduce any new data structures copied to or from userspace, nor does it allocate complex structures that might be left partially uninitialized. There is no risk of uninitialized memory being leaked to userspace or used in control flow decisions. Any potential memory safety issues introduced by these changes (such as use-after-free or out-of-bounds accesses on the `mem_cgroup` pointer) would be effectively caught by KASAN and the standard BPF verifier checks. Therefore, a dedicated KMSAN fuzzing session is not required.",
"NeedsKMSAN": false,
"Reasoning": "The patch introduces two new BPF kfuncs, `bpf_proactive_reclaim` and `bpf_proactive_reclaim_swappiness`, which allow BPF programs to proactively trigger memory reclaim on a memory cgroup. This exposes core memory management and reclaim logic to BPF, making it a functional change that is highly reachable and worth fuzzing.",
"WorthFuzzing": true
} |
| 1/1 | 2026/08/19 07:44 | action | read-patch-diff |
0mArgs:null Results: {
"PatchDiff": "commit 9be5325145a070063d3909583238b497a96dd77f\nAuthor: syz-cluster \u003ctriage@syzkaller.com\u003e\nDate: Wed Aug 19 07:44:18 2026 +0000\n\n syz-cluster: applied patch under review\n\ndiff --git a/mm/bpf_memcontrol.c b/mm/bpf_memcontrol.c\nindex 716df49d76477..b0a0d4c55dc4e 100644\n--- a/mm/bpf_memcontrol.c\n+++ b/mm/bpf_memcontrol.c\n@@ -6,6 +6,7 @@\n */\n \n #include \u003clinux/memcontrol.h\u003e\n+#include \u003clinux/swap.h\u003e\n #include \u003clinux/bpf.h\u003e\n \n __bpf_kfunc_start_defs();\n@@ -159,6 +160,103 @@ __bpf_kfunc void bpf_mem_cgroup_flush_stats(struct mem_cgroup *memcg)\n \tmem_cgroup_flush_stats(memcg);\n }\n \n+/*\n+ * Reclaim must not recurse. try_to_free_mem_cgroup_pages() unconditionally\n+ * overwrites current-\u003ereclaim_state on entry and resets it to NULL on exit.\n+ * So invoking it from an in-flight reclaim would clobber the outer reclaim\n+ * state and corrupt its accounting.\n+ *\n+ * The guard is PF_MEMALLOC. Every reclaim entry point marks the current\n+ * task with it for the whole reclaim window: try_to_free_mem_cgroup_pages()\n+ * and __perform_reclaim() do so via memalloc_noreclaim_save(), and kswapd\n+ * keeps it set for its entire lifetime. A hook inside the reclaim path\n+ * (shrink_node, shrink_slab, ...) executes in the context of the\n+ * reclaiming task, where current-\u003eflags already carries the flag. The page\n+ * allocator, the memcg charging path and node_reclaim() rely on the same\n+ * flag to avoid reclaim recursion.\n+ *\n+ * In try_to_free_mem_cgroup_pages(), reclaim_state is set slightly before\n+ * PF_MEMALLOC, with only a tracepoint in between, which a sleepable BPF\n+ * program cannot attach to.\n+ * Also, PF_MEMALLOC is set in some non-reclaim contexts (e.g. direct compaction\n+ * and vmalloc), where the kfunc conservatively refuses to reclaim as well.\n+ */\n+static bool bpf_in_reclaim_context(void)\n+{\n+\treturn current-\u003eflags \u0026 PF_MEMALLOC;\n+}\n+\n+/*\n+ * Shared implementation of the proactive reclaim kfuncs: performs one\n+ * reclaim pass on @memcg with @nr_pages as the goal, allowing swap, and\n+ * @swappiness as the anon/file balance override (NULL to follow the\n+ * cgroup's own swappiness setting).\n+ */\n+static unsigned long\n+bpf_proactive_reclaim_pages(struct mem_cgroup *memcg, unsigned long nr_pages,\n+\t\t\t int *swappiness)\n+{\n+\tif (!nr_pages || unlikely(bpf_in_reclaim_context()))\n+\t\treturn 0;\n+\n+\treturn try_to_free_mem_cgroup_pages(memcg, nr_pages, GFP_KERNEL,\n+\t\t\t\t\t MEMCG_RECLAIM_MAY_SWAP |\n+\t\t\t\t\t MEMCG_RECLAIM_PROACTIVE,\n+\t\t\t\t\t swappiness);\n+}\n+\n+/**\n+ * bpf_proactive_reclaim - proactively reclaim memory from a memory\n+ * cgroup\n+ * @memcg: the target memory cgroup to reclaim from\n+ * @size: the amount of memory to reclaim, in bytes\n+ *\n+ * Trigger one proactive reclaim pass on @memcg, similar to a write to\n+ * the memory.reclaim cgroup file: pages are reclaimed according to the\n+ * cgroup's own swappiness setting and swap is allowed. Note that,\n+ * unlike memory.reclaim, this does not retry until @size is reached;\n+ * callers can invoke it again if needed.\n+ *\n+ * Return:\n+ * The number of pages actually reclaimed, or 0 if @size is smaller\n+ * than a page or the calling task is already in a reclaim/freeing\n+ * context (PF_MEMALLOC).\n+ */\n+__bpf_kfunc unsigned long bpf_proactive_reclaim(struct mem_cgroup *memcg,\n+\t\t\t\t\t\tunsigned long size)\n+{\n+\treturn bpf_proactive_reclaim_pages(memcg, size / PAGE_SIZE, NULL);\n+}\n+\n+/**\n+ * bpf_proactive_reclaim_swappiness - proactively reclaim memory from a\n+ * memory cgroup with an explicit\n+ * swappiness\n+ * @memcg: the target memory cgroup to reclaim from\n+ * @size: the amount of memory to reclaim, in bytes\n+ * @swappiness: swappiness override for this reclaim pass\n+ *\n+ * Same as bpf_proactive_reclaim(), except that the anon/file reclaim\n+ * balance is controlled by @swappiness instead of the cgroup's\n+ * swappiness setting. Valid values are [MIN_SWAPPINESS, MAX_SWAPPINESS]\n+ * and SWAPPINESS_ANON_ONLY, which restricts reclaim to anon folios.\n+ *\n+ * Return:\n+ * The number of pages actually reclaimed, or 0 if @size is smaller\n+ * than a page, @swappiness is out of range, or the calling task is\n+ * already in a reclaim/freeing context (PF_MEMALLOC).\n+ */\n+__bpf_kfunc unsigned long\n+bpf_proactive_reclaim_swappiness(struct mem_cgroup *memcg, unsigned long size,\n+\t\t\t\t int swappiness)\n+{\n+\tif (swappiness \u003c MIN_SWAPPINESS || swappiness \u003e SWAPPINESS_ANON_ONLY)\n+\t\treturn 0;\n+\n+\treturn bpf_proactive_reclaim_pages(memcg, size / PAGE_SIZE,\n+\t\t\t\t\t \u0026swappiness);\n+}\n+\n __bpf_kfunc_end_defs();\n \n BTF_KFUNCS_START(bpf_memcontrol_kfuncs)\n@@ -172,6 +270,9 @@ BTF_ID_FLAGS(func, bpf_mem_cgroup_usage)\n BTF_ID_FLAGS(func, bpf_mem_cgroup_page_state)\n BTF_ID_FLAGS(func, bpf_mem_cgroup_flush_stats, KF_SLEEPABLE)\n \n+BTF_ID_FLAGS(func, bpf_proactive_reclaim, KF_SLEEPABLE)\n+BTF_ID_FLAGS(func, bpf_proactive_reclaim_swappiness, KF_SLEEPABLE)\n+\n BTF_KFUNCS_END(bpf_memcontrol_kfuncs)\n \n static const struct btf_kfunc_id_set bpf_memcontrol_kfunc_set = {\ndiff --git a/tools/testing/selftests/bpf/prog_tests/memcg_async_reclaim.c b/tools/testing/selftests/bpf/prog_tests/memcg_async_reclaim.c\nnew file mode 100644\nindex 0000000000000..e990d1fdc79fd\n--- /dev/null\n+++ b/tools/testing/selftests/bpf/prog_tests/memcg_async_reclaim.c\n@@ -0,0 +1,479 @@\n+// SPDX-License-Identifier: GPL-2.0\n+/*\n+ * Memory controller eBPF async reclaim test\n+ */\n+\n+#include \u003ctest_progs.h\u003e\n+#include \u003csys/mman.h\u003e\n+#include \u003csys/stat.h\u003e\n+#include \u003csys/vfs.h\u003e\n+#include \u003csys/wait.h\u003e\n+#include \u003cfcntl.h\u003e\n+#include \u003csignal.h\u003e\n+#include \u003ctime.h\u003e\n+#include \u003cunistd.h\u003e\n+#include \u003cstdio.h\u003e\n+#include \u003cstdlib.h\u003e\n+#include \u003cstring.h\u003e\n+#include \u003climits.h\u003e\n+#include \u003clinux/magic.h\u003e\n+\n+#include \"cgroup_helpers.h\"\n+\n+struct bpf_args_s {\n+\tu64 high_cgroup_id;\n+\tu64 low_cgroup_id;\n+\tu64 event_delta_threshold;\n+\tu64 check_ns;\n+};\n+\n+#include \"memcg_async_reclaim.skel.h\"\n+\n+#define FILE_SIZE (32 * 1024 * 1024ul)\n+#define BUFFER_SIZE (4096)\n+#define CG_LIMIT (32 * 1024 * 1024ul)\n+#define READ_TIMES 50\n+\n+#define CG_DIR \"/memcg_async_reclaim\"\n+#define CG_HIGH_DIR CG_DIR \"/high\"\n+#define CG_LOW_DIR CG_DIR \"/low\"\n+\n+#define CHECK_PERIOD_NS (2 * 1000 * 1000ull)\n+#define EVENT_DELTA_THRESHOLD 1\n+\n+/*\n+ * The workload files must sit on a regular filesystem: with swap\n+ * disabled for the cgroup, tmpfs/ramfs pages are unevictable and would\n+ * OOM the cgroup instead of exercising reclaim; they are also charged\n+ * as anonymous memory, so they never raise the WORKINGSET_REFAULT_FILE\n+ * events the BPF program monitors. Fall back to the current directory\n+ * when /tmp is backed by such a filesystem.\n+ */\n+static const char *workload_files_dir(void)\n+{\n+\tstruct statfs st;\n+\n+\tif (!statfs(\"/tmp\", \u0026st) \u0026\u0026\n+\t (st.f_type == TMPFS_MAGIC || st.f_type == RAMFS_MAGIC))\n+\t\treturn \".\";\n+\treturn \"/tmp\";\n+}\n+\n+/*\n+ * The workload children run after test_progs hijacked stdio, so\n+ * anything they print is lost with their private copy of the hijacked\n+ * buffer. The exit status is the only diagnostics channel that reaches\n+ * the parent, so each failing step gets its own code.\n+ */\n+enum child_exit_code {\n+\tCHILD_EXIT_OK = 0,\n+\tCHILD_EXIT_JOIN_CGROUP,\n+\tCHILD_EXIT_WRITE_FILE,\n+\tCHILD_EXIT_READ_FILE,\n+\tCHILD_EXIT_TIME_FILE,\n+};\n+\n+static const char *child_exit_str(int code)\n+{\n+\tswitch (code) {\n+\tcase CHILD_EXIT_OK:\n+\t\treturn \"success\";\n+\tcase CHILD_EXIT_JOIN_CGROUP:\n+\t\treturn \"join cgroup\";\n+\tcase CHILD_EXIT_WRITE_FILE:\n+\t\treturn \"write data file\";\n+\tcase CHILD_EXIT_READ_FILE:\n+\t\treturn \"read data file\";\n+\tcase CHILD_EXIT_TIME_FILE:\n+\t\treturn \"write time file\";\n+\tdefault:\n+\t\treturn \"unknown\";\n+\t}\n+}\n+\n+static int setup_high_low_cgroups(u64 *high_cgroup_id, u64 *low_cgroup_id)\n+{\n+\tint ret;\n+\tchar limit_buf[20];\n+\n+\tret = setup_cgroup_environment();\n+\tif (!ASSERT_OK(ret, \"setup_cgroup_environment\"))\n+\t\tgoto cleanup;\n+\n+\tret = create_and_get_cgroup(CG_DIR);\n+\tif (!ASSERT_GE(ret, 0, \"create_and_get_cgroup \" CG_DIR))\n+\t\tgoto cleanup;\n+\tclose(ret);\n+\n+\tret = enable_controllers(CG_DIR, \"memory\");\n+\tif (!ASSERT_OK(ret, \"enable_controllers\"))\n+\t\tgoto cleanup;\n+\n+\tsnprintf(limit_buf, sizeof(limit_buf), \"%lu\", CG_LIMIT);\n+\tret = write_cgroup_file(CG_DIR, \"memory.max\", limit_buf);\n+\tif (!ASSERT_OK(ret, \"write_cgroup_file memory.max\"))\n+\t\tgoto cleanup;\n+\n+\t/*\n+\t * Keep the workloads from swapping out. With CONFIG_SWAP=n the\n+\t * memory.swap.max file does not exist, and no swap can happen\n+\t * anyway, so skip the write.\n+\t */\n+\tif (!access(\"/proc/swaps\", F_OK)) {\n+\t\tret = write_cgroup_file(CG_DIR, \"memory.swap.max\", \"0\");\n+\t\tif (!ASSERT_OK(ret, \"write_cgroup_file memory.swap.max\"))\n+\t\t\tgoto cleanup;\n+\t}\n+\n+\tret = create_and_get_cgroup(CG_HIGH_DIR);\n+\tif (!ASSERT_GE(ret, 0, \"create_and_get_cgroup \" CG_HIGH_DIR))\n+\t\tgoto cleanup;\n+\tclose(ret);\n+\n+\t*high_cgroup_id = get_cgroup_id(CG_HIGH_DIR);\n+\tif (!ASSERT_GT(*high_cgroup_id, 0, \"get_cgroup_id\"))\n+\t\tgoto cleanup;\n+\n+\tret = create_and_get_cgroup(CG_LOW_DIR);\n+\tif (!ASSERT_GE(ret, 0, \"create_and_get_cgroup \" CG_LOW_DIR))\n+\t\tgoto cleanup;\n+\tclose(ret);\n+\n+\t*low_cgroup_id = get_cgroup_id(CG_LOW_DIR);\n+\tif (!ASSERT_GT(*low_cgroup_id, 0, \"get_cgroup_id\"))\n+\t\tgoto cleanup;\n+\n+\treturn 0;\n+\n+cleanup:\n+\tcleanup_cgroup_environment();\n+\treturn -1;\n+}\n+\n+static int write_file(const char *filename)\n+{\n+\tint ret = -1;\n+\tsize_t written = 0;\n+\tchar *buffer;\n+\tFILE *fp;\n+\n+\tfp = fopen(filename, \"wb\");\n+\tif (!fp)\n+\t\tgoto out;\n+\n+\tbuffer = malloc(BUFFER_SIZE);\n+\tif (!buffer)\n+\t\tgoto cleanup_fp;\n+\n+\tmemset(buffer, 'A', BUFFER_SIZE);\n+\n+\twhile (written \u003c FILE_SIZE) {\n+\t\tsize_t to_write = FILE_SIZE - written \u003c BUFFER_SIZE ?\n+\t\t\t\t FILE_SIZE - written : BUFFER_SIZE;\n+\n+\t\tif (fwrite(buffer, 1, to_write, fp) != to_write)\n+\t\t\tgoto cleanup;\n+\t\twritten += to_write;\n+\t}\n+\n+\tret = 0;\n+cleanup:\n+\tfree(buffer);\n+cleanup_fp:\n+\tfclose(fp);\n+out:\n+\treturn ret;\n+}\n+\n+static int read_file(const char *filename, int iterations)\n+{\n+\tint ret = -1;\n+\tlong page_size = sysconf(_SC_PAGESIZE);\n+\tchar *map;\n+\tsize_t i;\n+\tint fd;\n+\tstruct stat sb;\n+\n+\tfd = open(filename, O_RDONLY);\n+\tif (fd == -1)\n+\t\tgoto out;\n+\n+\tif (fstat(fd, \u0026sb) == -1)\n+\t\tgoto cleanup_fd;\n+\n+\tif (sb.st_size != FILE_SIZE) {\n+\t\tfprintf(stderr, \"File size mismatch: expected %lu, got %lu\\n\",\n+\t\t\t(unsigned long)FILE_SIZE, (unsigned long)sb.st_size);\n+\t\tgoto cleanup_fd;\n+\t}\n+\n+\tmap = mmap(NULL, FILE_SIZE, PROT_READ, MAP_PRIVATE, fd, 0);\n+\tif (map == MAP_FAILED)\n+\t\tgoto cleanup_fd;\n+\n+\tfor (int iter = 0; iter \u003c iterations; iter++) {\n+\t\tfor (i = 0; i \u003c FILE_SIZE; i += page_size) {\n+\t\t\t/* access a byte to trigger page fault */\n+\t\t\tvolatile char v = map[i];\n+\t\t\t(void)v;\n+\t\t}\n+\t}\n+\n+\tif (munmap(map, FILE_SIZE) == -1)\n+\t\tgoto cleanup_fd;\n+\n+\tret = 0;\n+\n+cleanup_fd:\n+\tclose(fd);\n+out:\n+\treturn ret;\n+}\n+\n+static int real_test_child_work(const char *cgroup_path, char *data_filename,\n+\t\t\t\tchar *time_filename, int read_times)\n+{\n+\tstruct timespec start, end;\n+\tdouble elapsed;\n+\tFILE *fp;\n+\n+\tif (join_parent_cgroup(cgroup_path))\n+\t\treturn CHILD_EXIT_JOIN_CGROUP;\n+\n+\tclock_gettime(CLOCK_MONOTONIC, \u0026start);\n+\n+\tif (write_file(data_filename))\n+\t\treturn CHILD_EXIT_WRITE_FILE;\n+\n+\tif (read_file(data_filename, read_times))\n+\t\treturn CHILD_EXIT_READ_FILE;\n+\n+\tclock_gettime(CLOCK_MONOTONIC, \u0026end);\n+\n+\tif (!time_filename)\n+\t\treturn CHILD_EXIT_OK;\n+\n+\telapsed = (end.tv_sec - start.tv_sec) +\n+\t\t (end.tv_nsec - start.tv_nsec) / 1000000000.0;\n+\tprintf(\"%.6f\\n\", elapsed);\n+\n+\tfp = fopen(time_filename, \"w\");\n+\tif (!fp)\n+\t\treturn CHILD_EXIT_TIME_FILE;\n+\tfprintf(fp, \"%.6f\", elapsed);\n+\tfclose(fp);\n+\n+\treturn CHILD_EXIT_OK;\n+}\n+\n+static int get_time(char *time_filename, double *time)\n+{\n+\tint ret = -1;\n+\tFILE *fp;\n+\tchar buf[64];\n+\n+\tfp = fopen(time_filename, \"r\");\n+\tif (!ASSERT_OK_PTR(fp, \"fopen\"))\n+\t\tgoto out;\n+\n+\tif (!ASSERT_OK_PTR(fgets(buf, sizeof(buf), fp), \"fgets\"))\n+\t\tgoto cleanup;\n+\n+\tif (sscanf(buf, \"%lf\", time) != 1) {\n+\t\tPRINT_FAIL(\"sscanf %s\", buf);\n+\t\tgoto cleanup;\n+\t}\n+\n+\tret = 0;\n+cleanup:\n+\tfclose(fp);\n+out:\n+\treturn ret;\n+}\n+\n+static int\n+run_high_low_workload(double *high_elapsed, double *low_elapsed, int read_times)\n+{\n+\tchar high_data_file[PATH_MAX];\n+\tchar low_data_file[PATH_MAX];\n+\tchar high_time_file[PATH_MAX];\n+\tchar low_time_file[PATH_MAX];\n+\tconst char *dir = workload_files_dir();\n+\tpid_t high_pid = -1, low_pid = -1;\n+\tint fd, status;\n+\tint ret = -1;\n+\n+\tsnprintf(high_data_file, sizeof(high_data_file),\n+\t\t \"%s/memcg_async_high_data_XXXXXX\", dir);\n+\tsnprintf(low_data_file, sizeof(low_data_file),\n+\t\t \"%s/memcg_async_low_data_XXXXXX\", dir);\n+\tsnprintf(high_time_file, sizeof(high_time_file),\n+\t\t \"%s/memcg_async_high_time_XXXXXX\", dir);\n+\tsnprintf(low_time_file, sizeof(low_time_file),\n+\t\t \"%s/memcg_async_low_time_XXXXXX\", dir);\n+\n+\tfd = mkstemp(high_data_file);\n+\tif (!ASSERT_GE(fd, 0, \"mkstemp\"))\n+\t\tgoto cleanup;\n+\tclose(fd);\n+\n+\tfd = mkstemp(low_data_file);\n+\tif (!ASSERT_GE(fd, 0, \"mkstemp\"))\n+\t\tgoto cleanup;\n+\tclose(fd);\n+\n+\tfd = mkstemp(high_time_file);\n+\tif (!ASSERT_GE(fd, 0, \"mkstemp\"))\n+\t\tgoto cleanup;\n+\tclose(fd);\n+\n+\tfd = mkstemp(low_time_file);\n+\tif (!ASSERT_GE(fd, 0, \"mkstemp\"))\n+\t\tgoto cleanup;\n+\tclose(fd);\n+\n+\tlow_pid = fork();\n+\tif (!ASSERT_GE(low_pid, 0, \"fork low\"))\n+\t\tgoto cleanup;\n+\tif (low_pid == 0)\n+\t\texit(real_test_child_work(CG_LOW_DIR, low_data_file,\n+\t\t\t\t\t low_time_file, read_times));\n+\n+\thigh_pid = fork();\n+\tif (!ASSERT_GE(high_pid, 0, \"fork high\"))\n+\t\tgoto cleanup;\n+\tif (high_pid == 0)\n+\t\texit(real_test_child_work(CG_HIGH_DIR, high_data_file,\n+\t\t\t\t\t high_time_file, read_times));\n+\n+\tlow_pid = waitpid(low_pid, \u0026status, 0);\n+\tif (!ASSERT_GT(low_pid, 0, \"low waitpid\"))\n+\t\tgoto cleanup;\n+\t/*\n+\t * The child has been reaped and its PID can already be reused,\n+\t * so mark it to keep cleanup from signaling an unrelated process.\n+\t */\n+\tlow_pid = -1;\n+\tif (!ASSERT_TRUE(WIFEXITED(status), \"low exited\"))\n+\t\tgoto cleanup;\n+\tif (WEXITSTATUS(status) != CHILD_EXIT_OK) {\n+\t\tPRINT_FAIL(\"low child failed at: %s (exit status %d)\",\n+\t\t\t child_exit_str(WEXITSTATUS(status)),\n+\t\t\t WEXITSTATUS(status));\n+\t\tgoto cleanup;\n+\t}\n+\n+\thigh_pid = waitpid(high_pid, \u0026status, 0);\n+\tif (!ASSERT_GT(high_pid, 0, \"high waitpid\"))\n+\t\tgoto cleanup;\n+\t/* Same as above: the reaped PID must not be signaled again. */\n+\thigh_pid = -1;\n+\tif (!ASSERT_TRUE(WIFEXITED(status), \"high exited\"))\n+\t\tgoto cleanup;\n+\tif (WEXITSTATUS(status) != CHILD_EXIT_OK) {\n+\t\tPRINT_FAIL(\"high child failed at: %s (exit status %d)\",\n+\t\t\t child_exit_str(WEXITSTATUS(status)),\n+\t\t\t WEXITSTATUS(status));\n+\t\tgoto cleanup;\n+\t}\n+\n+\tif (get_time(high_time_file, high_elapsed))\n+\t\tgoto cleanup;\n+\tif (get_time(low_time_file, low_elapsed))\n+\t\tgoto cleanup;\n+\n+\tret = 0;\n+\n+cleanup:\n+\t/* On failure, make sure no child process is left behind */\n+\tif (ret) {\n+\t\tif (high_pid \u003e 0) {\n+\t\t\tkill(high_pid, SIGKILL);\n+\t\t\t(void)waitpid(high_pid, NULL, 0);\n+\t\t}\n+\t\tif (low_pid \u003e 0) {\n+\t\t\tkill(low_pid, SIGKILL);\n+\t\t\t(void)waitpid(low_pid, NULL, 0);\n+\t\t}\n+\t}\n+\tunlink(low_time_file);\n+\tunlink(high_time_file);\n+\tunlink(low_data_file);\n+\tunlink(high_data_file);\n+\treturn ret;\n+}\n+\n+static int\n+setup_bpf(u64 high_cgroup_id, u64 low_cgroup_id,\n+\t struct memcg_async_reclaim **skel_ptr)\n+{\n+\tstruct memcg_async_reclaim *skel;\n+\tstruct bpf_args_s bpf_args = {\n+\t\t.high_cgroup_id = high_cgroup_id,\n+\t\t.low_cgroup_id = low_cgroup_id,\n+\t\t.event_delta_threshold = EVENT_DELTA_THRESHOLD,\n+\t\t.check_ns = CHECK_PERIOD_NS,\n+\t};\n+\tLIBBPF_OPTS(bpf_test_run_opts, run_opts,\n+\t\t.ctx_in = \u0026bpf_args,\n+\t\t.ctx_size_in = sizeof(bpf_args));\n+\tint prog_init_fd, err;\n+\n+\tskel = memcg_async_reclaim__open_and_load();\n+\tif (!ASSERT_OK_PTR(skel, \"memcg_async_reclaim__open_and_load\"))\n+\t\treturn -1;\n+\n+\tprog_init_fd = bpf_program__fd(skel-\u003eprogs.wq_prog_init);\n+\n+\terr = bpf_prog_test_run_opts(prog_init_fd, \u0026run_opts);\n+\tif (!ASSERT_OK(err, \"bpf_prog_test_run_opts\"))\n+\t\tgoto error_out;\n+\tif (!ASSERT_EQ(run_opts.retval, 0, \"prog_init retval\"))\n+\t\tgoto error_out;\n+\n+\t*skel_ptr = skel;\n+\treturn 0;\n+\n+error_out:\n+\tmemcg_async_reclaim__destroy(skel);\n+\treturn -1;\n+}\n+\n+void test_memcg_wq_async_reclaim(void)\n+{\n+\tu64 high_cgroup_id, low_cgroup_id;\n+\tint err;\n+\tdouble high_time = 0.0, low_time = 0.0;\n+\tstruct memcg_async_reclaim *skel = NULL;\n+\n+\terr = setup_high_low_cgroups(\u0026high_cgroup_id, \u0026low_cgroup_id);\n+\tif (!ASSERT_OK(err, \"setup_high_low_cgroups reclaim\"))\n+\t\treturn;\n+\n+\terr = setup_bpf(high_cgroup_id, low_cgroup_id, \u0026skel);\n+\tif (!ASSERT_OK(err, \"setup_bpf\"))\n+\t\tgoto out;\n+\n+\terr = run_high_low_workload(\u0026high_time, \u0026low_time, READ_TIMES);\n+\tif (!ASSERT_OK(err, \"run_high_low_workload reclaim\"))\n+\t\tgoto out;\n+\n+\t/*\n+\t * The timing comparison below alone cannot distinguish a working\n+\t * reclaim from a no-op one, so require that the BPF program\n+\t * actually reclaimed pages from the low cgroup.\n+\t */\n+\tif (!ASSERT_GT(skel-\u003ebss-\u003ereclaim_calls, 0, \"reclaim_calls\"))\n+\t\tgoto out;\n+\tif (!ASSERT_GT(skel-\u003ebss-\u003ereclaimed_pages, 0, \"reclaimed_pages\"))\n+\t\tgoto out;\n+\n+\tif (high_time \u003e= low_time)\n+\t\tPRINT_FAIL(\"high cgroup not improved with async reclaim: high_time=%f low_time=%f\",\n+\t\t\t high_time, low_time);\n+\n+out:\n+\tif (skel)\n+\t\tmemcg_async_reclaim__destroy(skel);\n+\tcleanup_cgroup_environment();\n+}\ndiff --git a/tools/testing/selftests/bpf/progs/memcg_async_reclaim.c b/tools/testing/selftests/bpf/progs/memcg_async_reclaim.c\nnew file mode 100644\nindex 0000000000000..225f0bc667113\n--- /dev/null\n+++ b/tools/testing/selftests/bpf/progs/memcg_async_reclaim.c\n@@ -0,0 +1,180 @@\n+// SPDX-License-Identifier: GPL-2.0\n+\n+#include \"vmlinux.h\"\n+#include \"bpf_experimental.h\"\n+#include \u003cbpf/bpf_helpers.h\u003e\n+#include \u003cbpf/bpf_tracing.h\u003e\n+#include \u003cbpf/bpf_core_read.h\u003e\n+\n+#define CLOCK_MONOTONIC_ID\t1\n+#define PAGE_SIZE\t\t4096UL\n+#define RECLAIM_SIZE\t\t(32 * PAGE_SIZE)\n+#define RECLAIM_MAX_ITER\t32\n+\n+struct bpf_args_s {\n+\tu64 high_cgroup_id;\n+\tu64 low_cgroup_id;\n+\tu64 event_delta_threshold;\n+\tu64 check_ns;\n+};\n+\n+struct cgroup_memcg {\n+\tstruct cgroup *cgrp;\n+\tstruct mem_cgroup *memcg;\n+};\n+\n+static u64 wq_high_cgroup_id;\n+static u64 wq_low_cgroup_id;\n+\n+/* Statistics exposed to userspace through .bss, so the test can verify\n+ * that reclaim actually happened instead of relying on timing alone.\n+ */\n+u64 reclaim_calls;\n+u64 reclaimed_pages;\n+\n+static int get_cgroup_memcg_from_id(u64 cgroup_id, struct cgroup_memcg *cm)\n+{\n+\tcm-\u003ecgrp = bpf_cgroup_from_id(cgroup_id);\n+\tif (!cm-\u003ecgrp)\n+\t\treturn -1;\n+\n+\tcm-\u003ememcg = bpf_get_mem_cgroup(\u0026cm-\u003ecgrp-\u003eself);\n+\tif (!cm-\u003ememcg) {\n+\t\tbpf_cgroup_release(cm-\u003ecgrp);\n+\t\treturn -1;\n+\t}\n+\n+\treturn 0;\n+}\n+\n+static void put_cgroup_memcg(struct cgroup_memcg *cm)\n+{\n+\tbpf_put_mem_cgroup(cm-\u003ememcg);\n+\tbpf_cgroup_release(cm-\u003ecgrp);\n+}\n+\n+static int get_cgroup_event(u64 cgroup_id, u64 *val)\n+{\n+\tstruct cgroup_memcg cm;\n+\n+\tif (get_cgroup_memcg_from_id(cgroup_id, \u0026cm))\n+\t\treturn -1;\n+\tbpf_mem_cgroup_flush_stats(cm.memcg);\n+\t*val = bpf_mem_cgroup_page_state(cm.memcg,\n+\t\tbpf_core_enum_value(enum node_stat_item,\n+\t\t\t\t WORKINGSET_REFAULT_FILE));\n+\tput_cgroup_memcg(\u0026cm);\n+\n+\treturn 0;\n+}\n+\n+static bool\n+should_reclaim_cgroup(u64 cgroup_id, u64 *prev_event, u64 event_delta_threshold)\n+{\n+\tu64 cur, delta;\n+\n+\tif (get_cgroup_event(cgroup_id, \u0026cur))\n+\t\treturn false;\n+\n+\tdelta = cur - *prev_event;\n+\t*prev_event = cur;\n+\n+\treturn delta \u003e= event_delta_threshold;\n+}\n+\n+static int reclaim_cgroup(u64 cgroup_id)\n+{\n+\tstruct cgroup_memcg cm;\n+\tint i;\n+\n+\tif (get_cgroup_memcg_from_id(cgroup_id, \u0026cm))\n+\t\treturn 0;\n+\n+\treclaim_calls++;\n+\tfor (i = 0; i \u003c RECLAIM_MAX_ITER; i++) {\n+\t\tu64 nr = bpf_proactive_reclaim(cm.memcg, RECLAIM_SIZE);\n+\n+\t\tif (!nr)\n+\t\t\tbreak;\n+\t\treclaimed_pages += nr;\n+\t}\n+\n+\tput_cgroup_memcg(\u0026cm);\n+\n+\treturn 0;\n+}\n+\n+struct wq_elem {\n+\tstruct bpf_timer timer;\n+\tstruct bpf_wq work;\n+\tu64 prev_event;\n+\tu64 event_delta_threshold;\n+\tu64 check_ns;\n+};\n+\n+struct {\n+\t__uint(type, BPF_MAP_TYPE_ARRAY);\n+\t__uint(max_entries, 1);\n+\t__type(key, __u32);\n+\t__type(value, struct wq_elem);\n+} wq_map SEC(\".maps\");\n+\n+static int async_free(void *map, int *key, void *value)\n+{\n+\tstruct wq_elem *elem = value;\n+\n+\tif (should_reclaim_cgroup(wq_high_cgroup_id, \u0026elem-\u003eprev_event,\n+\t\telem-\u003eevent_delta_threshold)) {\n+\t\treclaim_cgroup(wq_low_cgroup_id);\n+\t\tbpf_wq_start(\u0026elem-\u003ework, 0);\n+\t}\n+\n+\treturn 0;\n+}\n+\n+static int wq_timer_cb(void *map, int *key, struct wq_elem *elem)\n+{\n+\tbpf_wq_start(\u0026elem-\u003ework, 0);\n+\tbpf_timer_start(\u0026elem-\u003etimer, elem-\u003echeck_ns, 0);\n+\n+\treturn 0;\n+}\n+\n+SEC(\"syscall\")\n+int wq_prog_init(struct bpf_args_s *ctx)\n+{\n+\tstruct wq_elem *elem;\n+\t__u32 key = 0;\n+\tint ret;\n+\n+\telem = bpf_map_lookup_elem(\u0026wq_map, \u0026key);\n+\tif (!elem)\n+\t\treturn -1;\n+\n+\tret = bpf_wq_init(\u0026elem-\u003ework, \u0026wq_map, 0);\n+\tif (ret)\n+\t\treturn ret;\n+\n+\tret = bpf_wq_set_callback(\u0026elem-\u003ework, async_free, 0);\n+\tif (ret)\n+\t\treturn ret;\n+\n+\tret = bpf_timer_init(\u0026elem-\u003etimer, \u0026wq_map, CLOCK_MONOTONIC_ID);\n+\tif (ret)\n+\t\treturn ret;\n+\n+\tret = bpf_timer_set_callback(\u0026elem-\u003etimer, wq_timer_cb);\n+\tif (ret)\n+\t\treturn ret;\n+\n+\telem-\u003eprev_event = 0;\n+\telem-\u003eevent_delta_threshold = ctx-\u003eevent_delta_threshold;\n+\telem-\u003echeck_ns = ctx-\u003echeck_ns;\n+\n+\twq_high_cgroup_id = ctx-\u003ehigh_cgroup_id;\n+\twq_low_cgroup_id = ctx-\u003elow_cgroup_id;\n+\n+\treturn bpf_timer_start(\u0026elem-\u003etimer, elem-\u003echeck_ns, 0);\n+}\n+\n+char LICENSE[] SEC(\"license\") = \"GPL\";\n"
} |
| 2/1 | 2026/08/19 07:44 | agent | patch-evaluator |
0mModel:best-expensive Args: null Results: {
"EnableConfigs": [
"MEMCG",
"BPF_SYSCALL"
],
"FocusSymbols": [
"bpf_proactive_reclaim",
"bpf_proactive_reclaim_swappiness"
],
"Reasoning": "The patch introduces two new BPF kfuncs, `bpf_proactive_reclaim` and `bpf_proactive_reclaim_swappiness`, which allow BPF programs to proactively trigger memory reclaim on a memory cgroup. This exposes core memory management and reclaim logic to BPF, making it a functional change that is highly reachable and worth fuzzing.",
"WorthFuzzing": true
}Instruction: You are an expert Linux kernel maintainer. Your job is to review a provided patch series and determine if it makes functional changes to the kernel that should be fuzzed. IMPORTANT: The changes have ALREADY been applied and committed as the HEAD commit in your workspace. Do NOT rely on your internal knowledge of the kernel. You must actively use your code access tools to examine the actual source code and confirm any assumptions. Return WorthFuzzing=false if the patch only contains: - Modifications to Documentation/, Kconfig files, or code comments. - Purely decorative changes, such as logging (e.g., pr_err, printk) or tracepoints. - Changes to numeric constants or macros that do not functionally alter execution flow. - Code paths that are impossible to reach in virtualized environments like GCE or QEMU, even when utilizing software-emulated hardware (e.g., usb gadget, mac80211_hwsim). - Code in vendor-specific PCIe switch, SmartNIC, or GPU drivers (e.g., mlxsw, pds_core, qed, ionic, amdgpu) that require physical PCIe hardware cards not emulated in standard QEMU. - Driver .remove, .shutdown, or pci_unregister_driver teardown callbacks (e.g., igb_remove) that are executed only during PCI hot-unplug or sysfs driver unbind operations. If it modifies reachable core kernel logic, drivers, or architectures, use your code search tools to verify the code can be executed, then return WorthFuzzing=true. When returning WorthFuzzing=true, you MUST ALSO: 1. Extract any specific kernel functions that should be heavily fuzzed into FocusSymbols. Avoid listing generic hot-path functions to prevent skewed test distributions. Prefer non-static, non-inlined API entrypoint functions over internal static helper functions (which are inlined by the compiler and do not have distinct symbol addresses). 2. Identify any specific CONFIG_ options required to properly test this new/modified feature. Go and look into the Kconfig files and check for ifdefs around the code, do not make assumptions. Also check "depends on" lines in Kconfig to include any non-standard parent subsystem configs needed for Kbuild to compile the code statically into vmlinux. List them in the EnableConfigs output array, and DO NOT add a 'CONFIG_' prefix (e.g., return "NET_IPV4" instead of "CONFIG_NET_IPV4"). Prefer calling several tools at the same time to save round-trips. Use set-results tool to provide results of the analysis. It must be called exactly once before the final reply. Ignore results of this tool. Prompt: For your convenience, here is the diff of the changes:
commit 9be5325145a070063d3909583238b497a96dd77f
Author: syz-cluster <triage@syzkaller.com>
Date: Wed Aug 19 07:44:18 2026 +0000
syz-cluster: applied patch under review
diff --git a/mm/bpf_memcontrol.c b/mm/bpf_memcontrol.c
index 716df49d76477..b0a0d4c55dc4e 100644
--- a/mm/bpf_memcontrol.c
+++ b/mm/bpf_memcontrol.c
@@ -6,6 +6,7 @@
*/
#include <linux/memcontrol.h>
+#include <linux/swap.h>
#include <linux/bpf.h>
__bpf_kfunc_start_defs();
@@ -159,6 +160,103 @@ __bpf_kfunc void bpf_mem_cgroup_flush_stats(struct mem_cgroup *memcg)
mem_cgroup_flush_stats(memcg);
}
+/*
+ * Reclaim must not recurse. try_to_free_mem_cgroup_pages() unconditionally
+ * overwrites current->reclaim_state on entry and resets it to NULL on exit.
+ * So invoking it from an in-flight reclaim would clobber the outer reclaim
+ * state and corrupt its accounting.
+ *
+ * The guard is PF_MEMALLOC. Every reclaim entry point marks the current
+ * task with it for the whole reclaim window: try_to_free_mem_cgroup_pages()
+ * and __perform_reclaim() do so via memalloc_noreclaim_save(), and kswapd
+ * keeps it set for its entire lifetime. A hook inside the reclaim path
+ * (shrink_node, shrink_slab, ...) executes in the context of the
+ * reclaiming task, where current->flags already carries the flag. The page
+ * allocator, the memcg charging path and node_reclaim() rely on the same
+ * flag to avoid reclaim recursion.
+ *
+ * In try_to_free_mem_cgroup_pages(), reclaim_state is set slightly before
+ * PF_MEMALLOC, with only a tracepoint in between, which a sleepable BPF
+ * program cannot attach to.
+ * Also, PF_MEMALLOC is set in some non-reclaim contexts (e.g. direct compaction
+ * and vmalloc), where the kfunc conservatively refuses to reclaim as well.
+ */
+static bool bpf_in_reclaim_context(void)
+{
+ return current->flags & PF_MEMALLOC;
+}
+
+/*
+ * Shared implementation of the proactive reclaim kfuncs: performs one
+ * reclaim pass on @memcg with @nr_pages as the goal, allowing swap, and
+ * @swappiness as the anon/file balance override (NULL to follow the
+ * cgroup's own swappiness setting).
+ */
+static unsigned long
+bpf_proactive_reclaim_pages(struct mem_cgroup *memcg, unsigned long nr_pages,
+ int *swappiness)
+{
+ if (!nr_pages || unlikely(bpf_in_reclaim_context()))
+ return 0;
+
+ return try_to_free_mem_cgroup_pages(memcg, nr_pages, GFP_KERNEL,
+ MEMCG_RECLAIM_MAY_SWAP |
+ MEMCG_RECLAIM_PROACTIVE,
+ swappiness);
+}
+
+/**
+ * bpf_proactive_reclaim - proactively reclaim memory from a memory
+ * cgroup
+ * @memcg: the target memory cgroup to reclaim from
+ * @size: the amount of memory to reclaim, in bytes
+ *
+ * Trigger one proactive reclaim pass on @memcg, similar to a write to
+ * the memory.reclaim cgroup file: pages are reclaimed according to the
+ * cgroup's own swappiness setting and swap is allowed. Note that,
+ * unlike memory.reclaim, this does not retry until @size is reached;
+ * callers can invoke it again if needed.
+ *
+ * Return:
+ * The number of pages actually reclaimed, or 0 if @size is smaller
+ * than a page or the calling task is already in a reclaim/freeing
+ * context (PF_MEMALLOC).
+ */
+__bpf_kfunc unsigned long bpf_proactive_reclaim(struct mem_cgroup *memcg,
+ unsigned long size)
+{
+ return bpf_proactive_reclaim_pages(memcg, size / PAGE_SIZE, NULL);
+}
+
+/**
+ * bpf_proactive_reclaim_swappiness - proactively reclaim memory from a
+ * memory cgroup with an explicit
+ * swappiness
+ * @memcg: the target memory cgroup to reclaim from
+ * @size: the amount of memory to reclaim, in bytes
+ * @swappiness: swappiness override for this reclaim pass
+ *
+ * Same as bpf_proactive_reclaim(), except that the anon/file reclaim
+ * balance is controlled by @swappiness instead of the cgroup's
+ * swappiness setting. Valid values are [MIN_SWAPPINESS, MAX_SWAPPINESS]
+ * and SWAPPINESS_ANON_ONLY, which restricts reclaim to anon folios.
+ *
+ * Return:
+ * The number of pages actually reclaimed, or 0 if @size is smaller
+ * than a page, @swappiness is out of range, or the calling task is
+ * already in a reclaim/freeing context (PF_MEMALLOC).
+ */
+__bpf_kfunc unsigned long
+bpf_proactive_reclaim_swappiness(struct mem_cgroup *memcg, unsigned long size,
+ int swappiness)
+{
+ if (swappiness < MIN_SWAPPINESS || swappiness > SWAPPINESS_ANON_ONLY)
+ return 0;
+
+ return bpf_proactive_reclaim_pages(memcg, size / PAGE_SIZE,
+ &swappiness);
+}
+
__bpf_kfunc_end_defs();
BTF_KFUNCS_START(bpf_memcontrol_kfuncs)
@@ -172,6 +270,9 @@ BTF_ID_FLAGS(func, bpf_mem_cgroup_usage)
BTF_ID_FLAGS(func, bpf_mem_cgroup_page_state)
BTF_ID_FLAGS(func, bpf_mem_cgroup_flush_stats, KF_SLEEPABLE)
+BTF_ID_FLAGS(func, bpf_proactive_reclaim, KF_SLEEPABLE)
+BTF_ID_FLAGS(func, bpf_proactive_reclaim_swappiness, KF_SLEEPABLE)
+
BTF_KFUNCS_END(bpf_memcontrol_kfuncs)
static const struct btf_kfunc_id_set bpf_memcontrol_kfunc_set = {
diff --git a/tools/testing/selftests/bpf/prog_tests/memcg_async_reclaim.c b/tools/testing/selftests/bpf/prog_tests/memcg_async_reclaim.c
new file mode 100644
index 0000000000000..e990d1fdc79fd
--- /dev/null
+++ b/tools/testing/selftests/bpf/prog_tests/memcg_async_reclaim.c
@@ -0,0 +1,479 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Memory controller eBPF async reclaim test
+ */
+
+#include <test_progs.h>
+#include <sys/mman.h>
+#include <sys/stat.h>
+#include <sys/vfs.h>
+#include <sys/wait.h>
+#include <fcntl.h>
+#include <signal.h>
+#include <time.h>
+#include <unistd.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <limits.h>
+#include <linux/magic.h>
+
+#include "cgroup_helpers.h"
+
+struct bpf_args_s {
+ u64 high_cgroup_id;
+ u64 low_cgroup_id;
+ u64 event_delta_threshold;
+ u64 check_ns;
+};
+
+#include "memcg_async_reclaim.skel.h"
+
+#define FILE_SIZE (32 * 1024 * 1024ul)
+#define BUFFER_SIZE (4096)
+#define CG_LIMIT (32 * 1024 * 1024ul)
+#define READ_TIMES 50
+
+#define CG_DIR "/memcg_async_reclaim"
+#define CG_HIGH_DIR CG_DIR "/high"
+#define CG_LOW_DIR CG_DIR "/low"
+
+#define CHECK_PERIOD_NS (2 * 1000 * 1000ull)
+#define EVENT_DELTA_THRESHOLD 1
+
+/*
+ * The workload files must sit on a regular filesystem: with swap
+ * disabled for the cgroup, tmpfs/ramfs pages are unevictable and would
+ * OOM the cgroup instead of exercising reclaim; they are also charged
+ * as anonymous memory, so they never raise the WORKINGSET_REFAULT_FILE
+ * events the BPF program monitors. Fall back to the current directory
+ * when /tmp is backed by such a filesystem.
+ */
+static const char *workload_files_dir(void)
+{
+ struct statfs st;
+
+ if (!statfs("/tmp", &st) &&
+ (st.f_type == TMPFS_MAGIC || st.f_type == RAMFS_MAGIC))
+ return ".";
+ return "/tmp";
+}
+
+/*
+ * The workload children run after test_progs hijacked stdio, so
+ * anything they print is lost with their private copy of the hijacked
+ * buffer. The exit status is the only diagnostics channel that reaches
+ * the parent, so each failing step gets its own code.
+ */
+enum child_exit_code {
+ CHILD_EXIT_OK = 0,
+ CHILD_EXIT_JOIN_CGROUP,
+ CHILD_EXIT_WRITE_FILE,
+ CHILD_EXIT_READ_FILE,
+ CHILD_EXIT_TIME_FILE,
+};
+
+static const char *child_exit_str(int code)
+{
+ switch (code) {
+ case CHILD_EXIT_OK:
+ return "success";
+ case CHILD_EXIT_JOIN_CGROUP:
+ return "join cgroup";
+ case CHILD_EXIT_WRITE_FILE:
+ return "write data file";
+ case CHILD_EXIT_READ_FILE:
+ return "read data file";
+ case CHILD_EXIT_TIME_FILE:
+ return "write time file";
+ default:
+ return "unknown";
+ }
+}
+
+static int setup_high_low_cgroups(u64 *high_cgroup_id, u64 *low_cgroup_id)
+{
+ int ret;
+ char limit_buf[20];
+
+ ret = setup_cgroup_environment();
+ if (!ASSERT_OK(ret, "setup_cgroup_environment"))
+ goto cleanup;
+
+ ret = create_and_get_cgroup(CG_DIR);
+ if (!ASSERT_GE(ret, 0, "create_and_get_cgroup " CG_DIR))
+ goto cleanup;
+ close(ret);
+
+ ret = enable_controllers(CG_DIR, "memory");
+ if (!ASSERT_OK(ret, "enable_controllers"))
+ goto cleanup;
+
+ snprintf(limit_buf, sizeof(limit_buf), "%lu", CG_LIMIT);
+ ret = write_cgroup_file(CG_DIR, "memory.max", limit_buf);
+ if (!ASSERT_OK(ret, "write_cgroup_file memory.max"))
+ goto cleanup;
+
+ /*
+ * Keep the workloads from swapping out. With CONFIG_SWAP=n the
+ * memory.swap.max file does not exist, and no swap can happen
+ * anyway, so skip the write.
+ */
+ if (!access("/proc/swaps", F_OK)) {
+ ret = write_cgroup_file(CG_DIR, "memory.swap.max", "0");
+ if (!ASSERT_OK(ret, "write_cgroup_file memory.swap.max"))
+ goto cleanup;
+ }
+
+ ret = create_and_get_cgroup(CG_HIGH_DIR);
+ if (!ASSERT_GE(ret, 0, "create_and_get_cgroup " CG_HIGH_DIR))
+ goto cleanup;
+ close(ret);
+
+ *high_cgroup_id = get_cgroup_id(CG_HIGH_DIR);
+ if (!ASSERT_GT(*high_cgroup_id, 0, "get_cgroup_id"))
+ goto cleanup;
+
+ ret = create_and_get_cgroup(CG_LOW_DIR);
+ if (!ASSERT_GE(ret, 0, "create_and_get_cgroup " CG_LOW_DIR))
+ goto cleanup;
+ close(ret);
+
+ *low_cgroup_id = get_cgroup_id(CG_LOW_DIR);
+ if (!ASSERT_GT(*low_cgroup_id, 0, "get_cgroup_id"))
+ goto cleanup;
+
+ return 0;
+
+cleanup:
+ cleanup_cgroup_environment();
+ return -1;
+}
+
+static int write_file(const char *filename)
+{
+ int ret = -1;
+ size_t written = 0;
+ char *buffer;
+ FILE *fp;
+
+ fp = fopen(filename, "wb");
+ if (!fp)
+ goto out;
+
+ buffer = malloc(BUFFER_SIZE);
+ if (!buffer)
+ goto cleanup_fp;
+
+ memset(buffer, 'A', BUFFER_SIZE);
+
+ while (written < FILE_SIZE) {
+ size_t to_write = FILE_SIZE - written < BUFFER_SIZE ?
+ FILE_SIZE - written : BUFFER_SIZE;
+
+ if (fwrite(buffer, 1, to_write, fp) != to_write)
+ goto cleanup;
+ written += to_write;
+ }
+
+ ret = 0;
+cleanup:
+ free(buffer);
+cleanup_fp:
+ fclose(fp);
+out:
+ return ret;
+}
+
+static int read_file(const char *filename, int iterations)
+{
+ int ret = -1;
+ long page_size = sysconf(_SC_PAGESIZE);
+ char *map;
+ size_t i;
+ int fd;
+ struct stat sb;
+
+ fd = open(filename, O_RDONLY);
+ if (fd == -1)
+ goto out;
+
+ if (fstat(fd, &sb) == -1)
+ goto cleanup_fd;
+
+ if (sb.st_size != FILE_SIZE) {
+ fprintf(stderr, "File size mismatch: expected %lu, got %lu\n",
+ (unsigned long)FILE_SIZE, (unsigned long)sb.st_size);
+ goto cleanup_fd;
+ }
+
+ map = mmap(NULL, FILE_SIZE, PROT_READ, MAP_PRIVATE, fd, 0);
+ if (map == MAP_FAILED)
+ goto cleanup_fd;
+
+ for (int iter = 0; iter < iterations; iter++) {
+ for (i = 0; i < FILE_SIZE; i += page_size) {
+ /* access a byte to trigger page fault */
+ volatile char v = map[i];
+ (void)v;
+ }
+ }
+
+ if (munmap(map, FILE_SIZE) == -1)
+ goto cleanup_fd;
+
+ ret = 0;
+
+cleanup_fd:
+ close(fd);
+out:
+ return ret;
+}
+
+static int real_test_child_work(const char *cgroup_path, char *data_filename,
+ char *time_filename, int read_times)
+{
+ struct timespec start, end;
+ double elapsed;
+ FILE *fp;
+
+ if (join_parent_cgroup(cgroup_path))
+ return CHILD_EXIT_JOIN_CGROUP;
+
+ clock_gettime(CLOCK_MONOTONIC, &start);
+
+ if (write_file(data_filename))
+ return CHILD_EXIT_WRITE_FILE;
+
+ if (read_file(data_filename, read_times))
+ return CHILD_EXIT_READ_FILE;
+
+ clock_gettime(CLOCK_MONOTONIC, &end);
+
+ if (!time_filename)
+ return CHILD_EXIT_OK;
+
+ elapsed = (end.tv_sec - start.tv_sec) +
+ (end.tv_nsec - start.tv_nsec) / 1000000000.0;
+ printf("%.6f\n", elapsed);
+
+ fp = fopen(time_filename, "w");
+ if (!fp)
+ return CHILD_EXIT_TIME_FILE;
+ fprintf(fp, "%.6f", elapsed);
+ fclose(fp);
+
+ return CHILD_EXIT_OK;
+}
+
+static int get_time(char *time_filename, double *time)
+{
+ int ret = -1;
+ FILE *fp;
+ char buf[64];
+
+ fp = fopen(time_filename, "r");
+ if (!ASSERT_OK_PTR(fp, "fopen"))
+ goto out;
+
+ if (!ASSERT_OK_PTR(fgets(buf, sizeof(buf), fp), "fgets"))
+ goto cleanup;
+
+ if (sscanf(buf, "%lf", time) != 1) {
+ PRINT_FAIL("sscanf %s", buf);
+ goto cleanup;
+ }
+
+ ret = 0;
+cleanup:
+ fclose(fp);
+out:
+ return ret;
+}
+
+static int
+run_high_low_workload(double *high_elapsed, double *low_elapsed, int read_times)
+{
+ char high_data_file[PATH_MAX];
+ char low_data_file[PATH_MAX];
+ char high_time_file[PATH_MAX];
+ char low_time_file[PATH_MAX];
+ const char *dir = workload_files_dir();
+ pid_t high_pid = -1, low_pid = -1;
+ int fd, status;
+ int ret = -1;
+
+ snprintf(high_data_file, sizeof(high_data_file),
+ "%s/memcg_async_high_data_XXXXXX", dir);
+ snprintf(low_data_file, sizeof(low_data_file),
+ "%s/memcg_async_low_data_XXXXXX", dir);
+ snprintf(high_time_file, sizeof(high_time_file),
+ "%s/memcg_async_high_time_XXXXXX", dir);
+ snprintf(low_time_file, sizeof(low_time_file),
+ "%s/memcg_async_low_time_XXXXXX", dir);
+
+ fd = mkstemp(high_data_file);
+ if (!ASSERT_GE(fd, 0, "mkstemp"))
+ goto cleanup;
+ close(fd);
+
+ fd = mkstemp(low_data_file);
+ if (!ASSERT_GE(fd, 0, "mkstemp"))
+ goto cleanup;
+ close(fd);
+
+ fd = mkstemp(high_time_file);
+ if (!ASSERT_GE(fd, 0, "mkstemp"))
+ goto cleanup;
+ close(fd);
+
+ fd = mkstemp(low_time_file);
+ if (!ASSERT_GE(fd, 0, "mkstemp"))
+ goto cleanup;
+ close(fd);
+
+ low_pid = fork();
+ if (!ASSERT_GE(low_pid, 0, "fork low"))
+ goto cleanup;
+ if (low_pid == 0)
+ exit(real_test_child_work(CG_LOW_DIR, low_data_file,
+ low_time_file, read_times));
+
+ high_pid = fork();
+ if (!ASSERT_GE(high_pid, 0, "fork high"))
+ goto cleanup;
+ if (high_pid == 0)
+ exit(real_test_child_work(CG_HIGH_DIR, high_data_file,
+ high_time_file, read_times));
+
+ low_pid = waitpid(low_pid, &status, 0);
+ if (!ASSERT_GT(low_pid, 0, "low waitpid"))
+ goto cleanup;
+ /*
+ * The child has been reaped and its PID can already be reused,
+ * so mark it to keep cleanup from signaling an unrelated process.
+ */
+ low_pid = -1;
+ if (!ASSERT_TRUE(WIFEXITED(status), "low exited"))
+ goto cleanup;
+ if (WEXITSTATUS(status) != CHILD_EXIT_OK) {
+ PRINT_FAIL("low child failed at: %s (exit status %d)",
+ child_exit_str(WEXITSTATUS(status)),
+ WEXITSTATUS(status));
+ goto cleanup;
+ }
+
+ high_pid = waitpid(high_pid, &status, 0);
+ if (!ASSERT_GT(high_pid, 0, "high waitpid"))
+ goto cleanup;
+ /* Same as above: the reaped PID must not be signaled again. */
+ high_pid = -1;
+ if (!ASSERT_TRUE(WIFEXITED(status), "high exited"))
+ goto cleanup;
+ if (WEXITSTATUS(status) != CHILD_EXIT_OK) {
+ PRINT_FAIL("high child failed at: %s (exit status %d)",
+ child_exit_str(WEXITSTATUS(status)),
+ WEXITSTATUS(status));
+ goto cleanup;
+ }
+
+ if (get_time(high_time_file, high_elapsed))
+ goto cleanup;
+ if (get_time(low_time_file, low_elapsed))
+ goto cleanup;
+
+ ret = 0;
+
+cleanup:
+ /* On failure, make sure no child process is left behind */
+ if (ret) {
+ if (high_pid > 0) {
+ kill(high_pid, SIGKILL);
+ (void)waitpid(high_pid, NULL, 0);
+ }
+ if (low_pid > 0) {
+ kill(low_pid, SIGKILL);
+ (void)waitpid(low_pid, NULL, 0);
+ }
+ }
+ unlink(low_time_file);
+ unlink(high_time_file);
+ unlink(low_data_file);
+ unlink(high_data_file);
+ return ret;
+}
+
+static int
+setup_bpf(u64 high_cgroup_id, u64 low_cgroup_id,
+ struct memcg_async_reclaim **skel_ptr)
+{
+ struct memcg_async_reclaim *skel;
+ struct bpf_args_s bpf_args = {
+ .high_cgroup_id = high_cgroup_id,
+ .low_cgroup_id = low_cgroup_id,
+ .event_delta_threshold = EVENT_DELTA_THRESHOLD,
+ .check_ns = CHECK_PERIOD_NS,
+ };
+ LIBBPF_OPTS(bpf_test_run_opts, run_opts,
+ .ctx_in = &bpf_args,
+ .ctx_size_in = sizeof(bpf_args));
+ int prog_init_fd, err;
+
+ skel = memcg_async_reclaim__open_and_load();
+ if (!ASSERT_OK_PTR(skel, "memcg_async_reclaim__open_and_load"))
+ return -1;
+
+ prog_init_fd = bpf_program__fd(skel->progs.wq_prog_init);
+
+ err = bpf_prog_test_run_opts(prog_init_fd, &run_opts);
+ if (!ASSERT_OK(err, "bpf_prog_test_run_opts"))
+ goto error_out;
+ if (!ASSERT_EQ(run_opts.retval, 0, "prog_init retval"))
+ goto error_out;
+
+ *skel_ptr = skel;
+ return 0;
+
+error_out:
+ memcg_async_reclaim__destroy(skel);
+ return -1;
+}
+
+void test_memcg_wq_async_reclaim(void)
+{
+ u64 high_cgroup_id, low_cgroup_id;
+ int err;
+ double high_time = 0.0, low_time = 0.0;
+ struct memcg_async_reclaim *skel = NULL;
+
+ err = setup_high_low_cgroups(&high_cgroup_id, &low_cgroup_id);
+ if (!ASSERT_OK(err, "setup_high_low_cgroups reclaim"))
+ return;
+
+ err = setup_bpf(high_cgroup_id, low_cgroup_id, &skel);
+ if (!ASSERT_OK(err, "setup_bpf"))
+ goto out;
+
+ err = run_high_low_workload(&high_time, &low_time, READ_TIMES);
+ if (!ASSERT_OK(err, "run_high_low_workload reclaim"))
+ goto out;
+
+ /*
+ * The timing comparison below alone cannot distinguish a working
+ * reclaim from a no-op one, so require that the BPF program
+ * actually reclaimed pages from the low cgroup.
+ */
+ if (!ASSERT_GT(skel->bss->reclaim_calls, 0, "reclaim_calls"))
+ goto out;
+ if (!ASSERT_GT(skel->bss->reclaimed_pages, 0, "reclaimed_pages"))
+ goto out;
+
+ if (high_time >= low_time)
+ PRINT_FAIL("high cgroup not improved with async reclaim: high_time=%f low_time=%f",
+ high_time, low_time);
+
+out:
+ if (skel)
+ memcg_async_reclaim__destroy(skel);
+ cleanup_cgroup_environment();
+}
diff --git a/tools/testing/selftests/bpf/progs/memcg_async_reclaim.c b/tools/testing/selftests/bpf/progs/memcg_async_reclaim.c
new file mode 100644
index 0000000000000..225f0bc667113
--- /dev/null
+++ b/tools/testing/selftests/bpf/progs/memcg_async_reclaim.c
@@ -0,0 +1,180 @@
+// SPDX-License-Identifier: GPL-2.0
+
+#include "vmlinux.h"
+#include "bpf_experimental.h"
+#include <bpf/bpf_helpers.h>
+#include <bpf/bpf_tracing.h>
+#include <bpf/bpf_core_read.h>
+
+#define CLOCK_MONOTONIC_ID 1
+#define PAGE_SIZE 4096UL
+#define RECLAIM_SIZE (32 * PAGE_SIZE)
+#define RECLAIM_MAX_ITER 32
+
+struct bpf_args_s {
+ u64 high_cgroup_id;
+ u64 low_cgroup_id;
+ u64 event_delta_threshold;
+ u64 check_ns;
+};
+
+struct cgroup_memcg {
+ struct cgroup *cgrp;
+ struct mem_cgroup *memcg;
+};
+
+static u64 wq_high_cgroup_id;
+static u64 wq_low_cgroup_id;
+
+/* Statistics exposed to userspace through .bss, so the test can verify
+ * that reclaim actually happened instead of relying on timing alone.
+ */
+u64 reclaim_calls;
+u64 reclaimed_pages;
+
+static int get_cgroup_memcg_from_id(u64 cgroup_id, struct cgroup_memcg *cm)
+{
+ cm->cgrp = bpf_cgroup_from_id(cgroup_id);
+ if (!cm->cgrp)
+ return -1;
+
+ cm->memcg = bpf_get_mem_cgroup(&cm->cgrp->self);
+ if (!cm->memcg) {
+ bpf_cgroup_release(cm->cgrp);
+ return -1;
+ }
+
+ return 0;
+}
+
+static void put_cgroup_memcg(struct cgroup_memcg *cm)
+{
+ bpf_put_mem_cgroup(cm->memcg);
+ bpf_cgroup_release(cm->cgrp);
+}
+
+static int get_cgroup_event(u64 cgroup_id, u64 *val)
+{
+ struct cgroup_memcg cm;
+
+ if (get_cgroup_memcg_from_id(cgroup_id, &cm))
+ return -1;
+ bpf_mem_cgroup_flush_stats(cm.memcg);
+ *val = bpf_mem_cgroup_page_state(cm.memcg,
+ bpf_core_enum_value(enum node_stat_item,
+ WORKINGSET_REFAULT_FILE));
+ put_cgroup_memcg(&cm);
+
+ return 0;
+}
+
+static bool
+should_reclaim_cgroup(u64 cgroup_id, u64 *prev_event, u64 event_delta_threshold)
+{
+ u64 cur, delta;
+
+ if (get_cgroup_event(cgroup_id, &cur))
+ return false;
+
+ delta = cur - *prev_event;
+ *prev_event = cur;
+
+ return delta >= event_delta_threshold;
+}
+
+static int reclaim_cgroup(u64 cgroup_id)
+{
+ struct cgroup_memcg cm;
+ int i;
+
+ if (get_cgroup_memcg_from_id(cgroup_id, &cm))
+ return 0;
+
+ reclaim_calls++;
+ for (i = 0; i < RECLAIM_MAX_ITER; i++) {
+ u64 nr = bpf_proactive_reclaim(cm.memcg, RECLAIM_SIZE);
+
+ if (!nr)
+ break;
+ reclaimed_pages += nr;
+ }
+
+ put_cgroup_memcg(&cm);
+
+ return 0;
+}
+
+struct wq_elem {
+ struct bpf_timer timer;
+ struct bpf_wq work;
+ u64 prev_event;
+ u64 event_delta_threshold;
+ u64 check_ns;
+};
+
+struct {
+ __uint(type, BPF_MAP_TYPE_ARRAY);
+ __uint(max_entries, 1);
+ __type(key, __u32);
+ __type(value, struct wq_elem);
+} wq_map SEC(".maps");
+
+static int async_free(void *map, int *key, void *value)
+{
+ struct wq_elem *elem = value;
+
+ if (should_reclaim_cgroup(wq_high_cgroup_id, &elem->prev_event,
+ elem->event_delta_threshold)) {
+ reclaim_cgroup(wq_low_cgroup_id);
+ bpf_wq_start(&elem->work, 0);
+ }
+
+ return 0;
+}
+
+static int wq_timer_cb(void *map, int *key, struct wq_elem *elem)
+{
+ bpf_wq_start(&elem->work, 0);
+ bpf_timer_start(&elem->timer, elem->check_ns, 0);
+
+ return 0;
+}
+
+SEC("syscall")
+int wq_prog_init(struct bpf_args_s *ctx)
+{
+ struct wq_elem *elem;
+ __u32 key = 0;
+ int ret;
+
+ elem = bpf_map_lookup_elem(&wq_map, &key);
+ if (!elem)
+ return -1;
+
+ ret = bpf_wq_init(&elem->work, &wq_map, 0);
+ if (ret)
+ return ret;
+
+ ret = bpf_wq_set_callback(&elem->work, async_free, 0);
+ if (ret)
+ return ret;
+
+ ret = bpf_timer_init(&elem->timer, &wq_map, CLOCK_MONOTONIC_ID);
+ if (ret)
+ return ret;
+
+ ret = bpf_timer_set_callback(&elem->timer, wq_timer_cb);
+ if (ret)
+ return ret;
+
+ elem->prev_event = 0;
+ elem->event_delta_threshold = ctx->event_delta_threshold;
+ elem->check_ns = ctx->check_ns;
+
+ wq_high_cgroup_id = ctx->high_cgroup_id;
+ wq_low_cgroup_id = ctx->low_cgroup_id;
+
+ return bpf_timer_start(&elem->timer, elem->check_ns, 0);
+}
+
+char LICENSE[] SEC("license") = "GPL";
|
| 3/2 | 2026/08/19 07:44 | llm | patch-evaluator |
0mModel:gemini-3.1-pro-preview Args: null Results: null Tokens: input: 10508 output: 21 thoughts: 244 Thoughts: **My Initial Assessment of the BPF Memory Reclamation Patch** Okay, so we've got a new patch introducing two BPF kfuncs: `bpf_proactive_reclaim` and `bpf_proactive_reclaim_swappiness`. The whole point seems to be giving BPF programs the ability to proactively trigger memory reclamation within a memory cgroup. That's a powerful tool, and the implementation's in `mm/bpf_memcontrol.c`. Essentially, these kfuncs are wrappers around `bpf_proactive_reclaim_pages`, which in turn calls `try_to_free_mem_cgroup_pages`. The important question is, are these kfuncs exposed to userspace? Since they're BPF kfuncs, the answer is *yes*. A BPF program can directly invoke these, which is a significant architectural decision. My gut tells me this is something we absolutely *need* to fuzz aggressively. The potential attack surface here is considerable, and even subtle bugs could lead to serious memory management issues or even privilege escalation. Now, let's dive deeper into the implementation of `bpf_proactive_reclaim` and `bpf_proactive_reclaim_swappiness`. I need to understand the specifics of how they interact with the core memory management subsystems. And I should check that `mm/bpf_memcontrol.c` is compiled based on the right configuration options, which means I should check `mm/Makefile`. We’re going to need both `CONFIG_MEMCG` and `CONFIG_BPF_SYSCALL` to be enabled, as expected. |
| 4/2 | 2026/08/19 07:44 | tool | grepper |
0mArgs:{
"Expression": "bpf_memcontrol.o"
}Results: {
"Output": "mm/Makefile=106=ifdef CONFIG_BPF_SYSCALL\nmm/Makefile:107:obj-$(CONFIG_MEMCG) += bpf_memcontrol.o\nmm/Makefile-108-endif\n"
} |
| 5/2 | 2026/08/19 07:44 | llm | patch-evaluator |
0mModel:gemini-3.1-pro-preview Args: null Results: null Tokens: input: 11185 output: 26 thoughts: 153 Thoughts: **Kernel Configuration for BPF Proactive Reclaim** Okay, so I'm looking into the configurations needed to enable and use `bpf_proactive_reclaim`. Right off the bat, I know I'll need `CONFIG_MEMCG` and `CONFIG_BPF_SYSCALL`. Those are pretty fundamental. Now, let's zoom in on `bpf_proactive_reclaim` and `bpf_proactive_reclaim_swappiness`. The fact that they're exposed as BPF kfuncs is key here, and I'll need to make sure those symbols are accessible. I'll quickly scan for any other dependencies, though I'm guessing `CONFIG_MEMCG` will be the primary requirement. `CONFIG_BPF_SYSCALL` is pretty much standard for any BPF-related work, so that's probably covered. Next, I need to verify that `try_to_free_mem_cgroup_pages` is consistently available. It would be a bit of a nightmare if that weren't the case! |
| 6/2 | 2026/08/19 07:44 | tool | grepper |
0mArgs:{
"Expression": "try_to_free_mem_cgroup_pages"
}Results: {
"Output": "include/linux/swap.h=355=unsigned long lruvec_lru_size(struct lruvec *lruvec, enum lru_list lru, int zone_idx);\n--\ninclude/linux/swap.h-364-\ninclude/linux/swap.h:365:extern unsigned long try_to_free_mem_cgroup_pages(struct mem_cgroup *memcg,\ninclude/linux/swap.h-366-\t\t\t\t\t\t unsigned long nr_pages,\n--\nmm/bpf_memcontrol.c=158=__bpf_kfunc void bpf_mem_cgroup_flush_stats(struct mem_cgroup *memcg)\n--\nmm/bpf_memcontrol.c-163-/*\nmm/bpf_memcontrol.c:164: * Reclaim must not recurse. try_to_free_mem_cgroup_pages() unconditionally\nmm/bpf_memcontrol.c-165- * overwrites current-\u003ereclaim_state on entry and resets it to NULL on exit.\n--\nmm/bpf_memcontrol.c-169- * The guard is PF_MEMALLOC. Every reclaim entry point marks the current\nmm/bpf_memcontrol.c:170: * task with it for the whole reclaim window: try_to_free_mem_cgroup_pages()\nmm/bpf_memcontrol.c-171- * and __perform_reclaim() do so via memalloc_noreclaim_save(), and kswapd\n--\nmm/bpf_memcontrol.c-177- *\nmm/bpf_memcontrol.c:178: * In try_to_free_mem_cgroup_pages(), reclaim_state is set slightly before\nmm/bpf_memcontrol.c-179- * PF_MEMALLOC, with only a tracepoint in between, which a sleepable BPF\n--\nmm/bpf_memcontrol.c=196=bpf_proactive_reclaim_pages(struct mem_cgroup *memcg, unsigned long nr_pages,\n--\nmm/bpf_memcontrol.c-201-\nmm/bpf_memcontrol.c:202:\treturn try_to_free_mem_cgroup_pages(memcg, nr_pages, GFP_KERNEL,\nmm/bpf_memcontrol.c-203-\t\t\t\t\t MEMCG_RECLAIM_MAY_SWAP |\n--\nmm/memcontrol-v1.c=1481=static int mem_cgroup_resize_max(struct mem_cgroup *memcg,\n--\nmm/memcontrol-v1.c-1521-\nmm/memcontrol-v1.c:1522:\t\tif (!try_to_free_mem_cgroup_pages(memcg, 1, GFP_KERNEL,\nmm/memcontrol-v1.c-1523-\t\t\t\tmemsw ? 0 : MEMCG_RECLAIM_MAY_SWAP, NULL)) {\n--\nmm/memcontrol-v1.c=1540=static int mem_cgroup_force_empty(struct mem_cgroup *memcg)\n--\nmm/memcontrol-v1.c-1553-\nmm/memcontrol-v1.c:1554:\t\tif (!try_to_free_mem_cgroup_pages(memcg, 1, GFP_KERNEL,\nmm/memcontrol-v1.c-1555-\t\t\t\t\t\t MEMCG_RECLAIM_MAY_SWAP, NULL))\n--\nmm/memcontrol.c=2328=static unsigned long reclaim_high(struct mem_cgroup *memcg,\n--\nmm/memcontrol.c-2343-\t\tpsi_memstall_enter(\u0026pflags);\nmm/memcontrol.c:2344:\t\tnr_reclaimed += try_to_free_mem_cgroup_pages(memcg, nr_pages,\nmm/memcontrol.c-2345-\t\t\t\t\t\t\tgfp_mask,\n--\nmm/memcontrol.c=2595=static int try_charge_memcg(struct mem_cgroup *memcg, gfp_t gfp_mask,\n--\nmm/memcontrol.c-2654-\tpsi_memstall_enter(\u0026pflags);\nmm/memcontrol.c:2655:\tnr_reclaimed = try_to_free_mem_cgroup_pages(mem_over_limit, nr_pages,\nmm/memcontrol.c-2656-\t\t\t\t\t\t gfp_mask, reclaim_options, NULL);\n--\nmm/memcontrol.c=4768=static ssize_t memory_high_write(struct kernfs_open_file *of,\n--\nmm/memcontrol.c-4802-\nmm/memcontrol.c:4803:\t\treclaimed = try_to_free_mem_cgroup_pages(memcg, nr_pages - high,\nmm/memcontrol.c-4804-\t\t\t\t\tGFP_KERNEL, MEMCG_RECLAIM_MAY_SWAP, NULL);\n--\nmm/memcontrol.c=4820=static ssize_t memory_max_write(struct kernfs_open_file *of,\n--\nmm/memcontrol.c-4854-\t\tif (nr_reclaims) {\nmm/memcontrol.c:4855:\t\t\tif (!try_to_free_mem_cgroup_pages(memcg, nr_pages - max,\nmm/memcontrol.c-4856-\t\t\t\t\tGFP_KERNEL, MEMCG_RECLAIM_MAY_SWAP, NULL))\n--\nmm/vmscan.c=6722=unsigned long mem_cgroup_shrink_node(struct mem_cgroup *memcg,\n--\nmm/vmscan.c-6761-\nmm/vmscan.c:6762:unsigned long try_to_free_mem_cgroup_pages(struct mem_cgroup *memcg,\nmm/vmscan.c-6763-\t\t\t\t\t unsigned long nr_pages,\n--\nmm/vmscan.c-6802-#else\nmm/vmscan.c:6803:unsigned long try_to_free_mem_cgroup_pages(struct mem_cgroup *memcg,\nmm/vmscan.c-6804-\t\t\t\t\t unsigned long nr_pages,\n--\nmm/vmscan.c=7866=int user_proactive_reclaim(char *buf,\n--\nmm/vmscan.c-7933-\t\t\t\t\t MEMCG_RECLAIM_PROACTIVE;\nmm/vmscan.c:7934:\t\t\treclaimed = try_to_free_mem_cgroup_pages(memcg,\nmm/vmscan.c-7935-\t\t\t\t\t\t batch_size, gfp_mask,\n"
} |
| 7/2 | 2026/08/19 07:44 | llm | patch-evaluator |
0mModel:gemini-3.1-pro-preview Args: null Results: null Tokens: input: 13244 output: 146 thoughts: 157 Thoughts: **My Assessment of the Memory Cgroup BPF Kfunc Addition** Okay, so this patch introduces a couple of new BPF kfuncs: `bpf_proactive_reclaim` and `bpf_proactive_reclaim_swappiness`. The core idea is that BPF programs will now be able to proactively trigger memory reclaim operations within a memory cgroup. This is significant because it's giving BPF direct access to core memory management logic. We're effectively exposing a crucial part of the kernel's memory management to the BPF domain, which is a big deal in terms of programmability and control. From a configuration standpoint, it looks straightforward. I need to ensure that `MEMCG` and `BPF_SYSCALL` are enabled. I'll double-check to make absolutely certain there aren't any other dependencies, but those should be the primary requirements. `CONFIG_MEMCG` is, of course, critical for the whole memory cgroup context, and `CONFIG_BPF_SYSCALL` is standard for BPF functionality. Alright, that's my understanding, I will set those results. |
| 8/2 | 2026/08/19 07:44 | tool | set-results |
0mArgs:{
"EnableConfigs": [
"MEMCG",
"BPF_SYSCALL"
],
"FocusSymbols": [
"bpf_proactive_reclaim",
"bpf_proactive_reclaim_swappiness"
],
"Reasoning": "The patch introduces two new BPF kfuncs, `bpf_proactive_reclaim` and `bpf_proactive_reclaim_swappiness`, which allow BPF programs to proactively trigger memory reclaim on a memory cgroup. This exposes core memory management and reclaim logic to BPF, making it a functional change that is highly reachable and worth fuzzing.",
"WorthFuzzing": true
}Results: {
"EnableConfigs": [
"MEMCG",
"BPF_SYSCALL"
],
"FocusSymbols": [
"bpf_proactive_reclaim",
"bpf_proactive_reclaim_swappiness"
],
"Reasoning": "The patch introduces two new BPF kfuncs, `bpf_proactive_reclaim` and `bpf_proactive_reclaim_swappiness`, which allow BPF programs to proactively trigger memory reclaim on a memory cgroup. This exposes core memory management and reclaim logic to BPF, making it a functional change that is highly reachable and worth fuzzing.",
"WorthFuzzing": true
} |
| 9/1 | 2026/08/19 07:44 | action | If |
0mArgs:{
"WorthFuzzing": true
}Results: null |
| 10/2 | 2026/08/19 07:44 | agent | kmsan-evaluator |
0mModel:best-expensive Args: null Results: {
"KMSANReasoning": "The patch introduces two new BPF kfuncs (`bpf_proactive_reclaim` and `bpf_proactive_reclaim_swappiness`) that wrap the existing `try_to_free_mem_cgroup_pages` function to allow BPF programs to trigger proactive memory reclaim for a memory cgroup. The arguments passed to these kfuncs are scalar values and pointers verified by the BPF verifier, ensuring they are initialized. The patch does not introduce any new data structures copied to or from userspace, nor does it allocate complex structures that might be left partially uninitialized. There is no risk of uninitialized memory being leaked to userspace or used in control flow decisions. Any potential memory safety issues introduced by these changes (such as use-after-free or out-of-bounds accesses on the `mem_cgroup` pointer) would be effectively caught by KASAN and the standard BPF verifier checks. Therefore, a dedicated KMSAN fuzzing session is not required.",
"NeedsKMSAN": false
}Instruction: You are an expert Linux kernel security engineer specializing in kernel memory
error detectors (KASAN and KMSAN). Your job is to review the provided patch series and
determine if the code changes justify spawning a dedicated KMSAN (KernelMemorySanitizer)
fuzzing session in addition to standard KASAN fuzzing.
CRITICAL DISTINCTION BETWEEN KASAN AND KMSAN:
- Standard KASAN kernel builds (upstream-apparmor-kasan.config) already enable
a comprehensive suite of debugging tools and sanitizers, including KASAN
(out-of-bounds accesses, use-after-free, double free, invalid free), LOCKDEP
(locking bugs and deadlocks), UB-sanitizers, and memory corruption checks.
- KMSAN (KernelMemorySanitizer) detects reads of UNINITIALIZED memory (stack, heap,
or page allocations) and kernel-to-user memory info-leaks.
Rule: THERE IS NO SENSE IN RUNNING A KMSAN SESSION IF A BUG CAN BE CAUGHT BY KASAN,
LOCKDEP, OR OTHER STANDARD BUG DETECTORS.
A dedicated KMSAN fuzzing session incurs significant resource costs. You must ONLY
set NeedsKMSAN=true if the code changes introduce or expose UNINITIALIZED MEMORY risks
that are detected ONLY by KMSAN.
Look holistically at the patch series and surrounding code. Even if no direct
uninitialized field accesses or new buffer allocations are added in the diff itself,
a patch may alter control flow, bounds checking, or data length calculations in ways
that change how the rest of the code operates on existing buffers (e.g. allowing
uninitialized stack/heap memory to be read, copied to user space, or used in control
flow). Do not hesitate to use your code access tools to inspect the surrounding code,
called functions, and callers.
Set NeedsKMSAN=true ONLY IF the patch introduces or modifies:
1. Kernel structures sent to user space (via copy_to_user, put_user, netlink skb
attributes, ioctl output arguments, socket options, or BPF buffers) where fields
or structure padding might not be fully initialized/zeroed.
2. Conditional logic or branching that depends on potentially uninitialized variables
or struct fields.
3. Allocation or initialization of complex data structures where uninitialized fields
could be read later in reachable code paths.
4. Bounds checks, lengths, or logic in a way that allows surrounding code to access
uninitialized bytes of existing buffers.
Set NeedsKMSAN=false IF:
- The code changes primarily risk out-of-bounds access, array overflows, NULL pointer
dereferences, locking deadlocks, or use-after-free bugs (these are already caught
by KASAN, LOCKDEP, or standard bug detectors).
- All stack/heap structures touched or introduced by the patch are fully zeroed
or initialized (e.g. using = {0}, memset, kzalloc) before being read or copied.
- The patch does not introduce any risk of uninitialized memory usage or info-leaks.
Use your code access tools to inspect the surrounding code if necessary, then provide
detailed KMSANReasoning contrasting KASAN vs KMSAN applicability for this patch.
Prefer calling several tools at the same time to save round-trips.
Use set-results tool to provide results of the analysis.
It must be called exactly once before the final reply.
Ignore results of this tool.
Prompt: For your convenience, here is the diff of the changes:
commit 9be5325145a070063d3909583238b497a96dd77f
Author: syz-cluster <triage@syzkaller.com>
Date: Wed Aug 19 07:44:18 2026 +0000
syz-cluster: applied patch under review
diff --git a/mm/bpf_memcontrol.c b/mm/bpf_memcontrol.c
index 716df49d76477..b0a0d4c55dc4e 100644
--- a/mm/bpf_memcontrol.c
+++ b/mm/bpf_memcontrol.c
@@ -6,6 +6,7 @@
*/
#include <linux/memcontrol.h>
+#include <linux/swap.h>
#include <linux/bpf.h>
__bpf_kfunc_start_defs();
@@ -159,6 +160,103 @@ __bpf_kfunc void bpf_mem_cgroup_flush_stats(struct mem_cgroup *memcg)
mem_cgroup_flush_stats(memcg);
}
+/*
+ * Reclaim must not recurse. try_to_free_mem_cgroup_pages() unconditionally
+ * overwrites current->reclaim_state on entry and resets it to NULL on exit.
+ * So invoking it from an in-flight reclaim would clobber the outer reclaim
+ * state and corrupt its accounting.
+ *
+ * The guard is PF_MEMALLOC. Every reclaim entry point marks the current
+ * task with it for the whole reclaim window: try_to_free_mem_cgroup_pages()
+ * and __perform_reclaim() do so via memalloc_noreclaim_save(), and kswapd
+ * keeps it set for its entire lifetime. A hook inside the reclaim path
+ * (shrink_node, shrink_slab, ...) executes in the context of the
+ * reclaiming task, where current->flags already carries the flag. The page
+ * allocator, the memcg charging path and node_reclaim() rely on the same
+ * flag to avoid reclaim recursion.
+ *
+ * In try_to_free_mem_cgroup_pages(), reclaim_state is set slightly before
+ * PF_MEMALLOC, with only a tracepoint in between, which a sleepable BPF
+ * program cannot attach to.
+ * Also, PF_MEMALLOC is set in some non-reclaim contexts (e.g. direct compaction
+ * and vmalloc), where the kfunc conservatively refuses to reclaim as well.
+ */
+static bool bpf_in_reclaim_context(void)
+{
+ return current->flags & PF_MEMALLOC;
+}
+
+/*
+ * Shared implementation of the proactive reclaim kfuncs: performs one
+ * reclaim pass on @memcg with @nr_pages as the goal, allowing swap, and
+ * @swappiness as the anon/file balance override (NULL to follow the
+ * cgroup's own swappiness setting).
+ */
+static unsigned long
+bpf_proactive_reclaim_pages(struct mem_cgroup *memcg, unsigned long nr_pages,
+ int *swappiness)
+{
+ if (!nr_pages || unlikely(bpf_in_reclaim_context()))
+ return 0;
+
+ return try_to_free_mem_cgroup_pages(memcg, nr_pages, GFP_KERNEL,
+ MEMCG_RECLAIM_MAY_SWAP |
+ MEMCG_RECLAIM_PROACTIVE,
+ swappiness);
+}
+
+/**
+ * bpf_proactive_reclaim - proactively reclaim memory from a memory
+ * cgroup
+ * @memcg: the target memory cgroup to reclaim from
+ * @size: the amount of memory to reclaim, in bytes
+ *
+ * Trigger one proactive reclaim pass on @memcg, similar to a write to
+ * the memory.reclaim cgroup file: pages are reclaimed according to the
+ * cgroup's own swappiness setting and swap is allowed. Note that,
+ * unlike memory.reclaim, this does not retry until @size is reached;
+ * callers can invoke it again if needed.
+ *
+ * Return:
+ * The number of pages actually reclaimed, or 0 if @size is smaller
+ * than a page or the calling task is already in a reclaim/freeing
+ * context (PF_MEMALLOC).
+ */
+__bpf_kfunc unsigned long bpf_proactive_reclaim(struct mem_cgroup *memcg,
+ unsigned long size)
+{
+ return bpf_proactive_reclaim_pages(memcg, size / PAGE_SIZE, NULL);
+}
+
+/**
+ * bpf_proactive_reclaim_swappiness - proactively reclaim memory from a
+ * memory cgroup with an explicit
+ * swappiness
+ * @memcg: the target memory cgroup to reclaim from
+ * @size: the amount of memory to reclaim, in bytes
+ * @swappiness: swappiness override for this reclaim pass
+ *
+ * Same as bpf_proactive_reclaim(), except that the anon/file reclaim
+ * balance is controlled by @swappiness instead of the cgroup's
+ * swappiness setting. Valid values are [MIN_SWAPPINESS, MAX_SWAPPINESS]
+ * and SWAPPINESS_ANON_ONLY, which restricts reclaim to anon folios.
+ *
+ * Return:
+ * The number of pages actually reclaimed, or 0 if @size is smaller
+ * than a page, @swappiness is out of range, or the calling task is
+ * already in a reclaim/freeing context (PF_MEMALLOC).
+ */
+__bpf_kfunc unsigned long
+bpf_proactive_reclaim_swappiness(struct mem_cgroup *memcg, unsigned long size,
+ int swappiness)
+{
+ if (swappiness < MIN_SWAPPINESS || swappiness > SWAPPINESS_ANON_ONLY)
+ return 0;
+
+ return bpf_proactive_reclaim_pages(memcg, size / PAGE_SIZE,
+ &swappiness);
+}
+
__bpf_kfunc_end_defs();
BTF_KFUNCS_START(bpf_memcontrol_kfuncs)
@@ -172,6 +270,9 @@ BTF_ID_FLAGS(func, bpf_mem_cgroup_usage)
BTF_ID_FLAGS(func, bpf_mem_cgroup_page_state)
BTF_ID_FLAGS(func, bpf_mem_cgroup_flush_stats, KF_SLEEPABLE)
+BTF_ID_FLAGS(func, bpf_proactive_reclaim, KF_SLEEPABLE)
+BTF_ID_FLAGS(func, bpf_proactive_reclaim_swappiness, KF_SLEEPABLE)
+
BTF_KFUNCS_END(bpf_memcontrol_kfuncs)
static const struct btf_kfunc_id_set bpf_memcontrol_kfunc_set = {
diff --git a/tools/testing/selftests/bpf/prog_tests/memcg_async_reclaim.c b/tools/testing/selftests/bpf/prog_tests/memcg_async_reclaim.c
new file mode 100644
index 0000000000000..e990d1fdc79fd
--- /dev/null
+++ b/tools/testing/selftests/bpf/prog_tests/memcg_async_reclaim.c
@@ -0,0 +1,479 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Memory controller eBPF async reclaim test
+ */
+
+#include <test_progs.h>
+#include <sys/mman.h>
+#include <sys/stat.h>
+#include <sys/vfs.h>
+#include <sys/wait.h>
+#include <fcntl.h>
+#include <signal.h>
+#include <time.h>
+#include <unistd.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <limits.h>
+#include <linux/magic.h>
+
+#include "cgroup_helpers.h"
+
+struct bpf_args_s {
+ u64 high_cgroup_id;
+ u64 low_cgroup_id;
+ u64 event_delta_threshold;
+ u64 check_ns;
+};
+
+#include "memcg_async_reclaim.skel.h"
+
+#define FILE_SIZE (32 * 1024 * 1024ul)
+#define BUFFER_SIZE (4096)
+#define CG_LIMIT (32 * 1024 * 1024ul)
+#define READ_TIMES 50
+
+#define CG_DIR "/memcg_async_reclaim"
+#define CG_HIGH_DIR CG_DIR "/high"
+#define CG_LOW_DIR CG_DIR "/low"
+
+#define CHECK_PERIOD_NS (2 * 1000 * 1000ull)
+#define EVENT_DELTA_THRESHOLD 1
+
+/*
+ * The workload files must sit on a regular filesystem: with swap
+ * disabled for the cgroup, tmpfs/ramfs pages are unevictable and would
+ * OOM the cgroup instead of exercising reclaim; they are also charged
+ * as anonymous memory, so they never raise the WORKINGSET_REFAULT_FILE
+ * events the BPF program monitors. Fall back to the current directory
+ * when /tmp is backed by such a filesystem.
+ */
+static const char *workload_files_dir(void)
+{
+ struct statfs st;
+
+ if (!statfs("/tmp", &st) &&
+ (st.f_type == TMPFS_MAGIC || st.f_type == RAMFS_MAGIC))
+ return ".";
+ return "/tmp";
+}
+
+/*
+ * The workload children run after test_progs hijacked stdio, so
+ * anything they print is lost with their private copy of the hijacked
+ * buffer. The exit status is the only diagnostics channel that reaches
+ * the parent, so each failing step gets its own code.
+ */
+enum child_exit_code {
+ CHILD_EXIT_OK = 0,
+ CHILD_EXIT_JOIN_CGROUP,
+ CHILD_EXIT_WRITE_FILE,
+ CHILD_EXIT_READ_FILE,
+ CHILD_EXIT_TIME_FILE,
+};
+
+static const char *child_exit_str(int code)
+{
+ switch (code) {
+ case CHILD_EXIT_OK:
+ return "success";
+ case CHILD_EXIT_JOIN_CGROUP:
+ return "join cgroup";
+ case CHILD_EXIT_WRITE_FILE:
+ return "write data file";
+ case CHILD_EXIT_READ_FILE:
+ return "read data file";
+ case CHILD_EXIT_TIME_FILE:
+ return "write time file";
+ default:
+ return "unknown";
+ }
+}
+
+static int setup_high_low_cgroups(u64 *high_cgroup_id, u64 *low_cgroup_id)
+{
+ int ret;
+ char limit_buf[20];
+
+ ret = setup_cgroup_environment();
+ if (!ASSERT_OK(ret, "setup_cgroup_environment"))
+ goto cleanup;
+
+ ret = create_and_get_cgroup(CG_DIR);
+ if (!ASSERT_GE(ret, 0, "create_and_get_cgroup " CG_DIR))
+ goto cleanup;
+ close(ret);
+
+ ret = enable_controllers(CG_DIR, "memory");
+ if (!ASSERT_OK(ret, "enable_controllers"))
+ goto cleanup;
+
+ snprintf(limit_buf, sizeof(limit_buf), "%lu", CG_LIMIT);
+ ret = write_cgroup_file(CG_DIR, "memory.max", limit_buf);
+ if (!ASSERT_OK(ret, "write_cgroup_file memory.max"))
+ goto cleanup;
+
+ /*
+ * Keep the workloads from swapping out. With CONFIG_SWAP=n the
+ * memory.swap.max file does not exist, and no swap can happen
+ * anyway, so skip the write.
+ */
+ if (!access("/proc/swaps", F_OK)) {
+ ret = write_cgroup_file(CG_DIR, "memory.swap.max", "0");
+ if (!ASSERT_OK(ret, "write_cgroup_file memory.swap.max"))
+ goto cleanup;
+ }
+
+ ret = create_and_get_cgroup(CG_HIGH_DIR);
+ if (!ASSERT_GE(ret, 0, "create_and_get_cgroup " CG_HIGH_DIR))
+ goto cleanup;
+ close(ret);
+
+ *high_cgroup_id = get_cgroup_id(CG_HIGH_DIR);
+ if (!ASSERT_GT(*high_cgroup_id, 0, "get_cgroup_id"))
+ goto cleanup;
+
+ ret = create_and_get_cgroup(CG_LOW_DIR);
+ if (!ASSERT_GE(ret, 0, "create_and_get_cgroup " CG_LOW_DIR))
+ goto cleanup;
+ close(ret);
+
+ *low_cgroup_id = get_cgroup_id(CG_LOW_DIR);
+ if (!ASSERT_GT(*low_cgroup_id, 0, "get_cgroup_id"))
+ goto cleanup;
+
+ return 0;
+
+cleanup:
+ cleanup_cgroup_environment();
+ return -1;
+}
+
+static int write_file(const char *filename)
+{
+ int ret = -1;
+ size_t written = 0;
+ char *buffer;
+ FILE *fp;
+
+ fp = fopen(filename, "wb");
+ if (!fp)
+ goto out;
+
+ buffer = malloc(BUFFER_SIZE);
+ if (!buffer)
+ goto cleanup_fp;
+
+ memset(buffer, 'A', BUFFER_SIZE);
+
+ while (written < FILE_SIZE) {
+ size_t to_write = FILE_SIZE - written < BUFFER_SIZE ?
+ FILE_SIZE - written : BUFFER_SIZE;
+
+ if (fwrite(buffer, 1, to_write, fp) != to_write)
+ goto cleanup;
+ written += to_write;
+ }
+
+ ret = 0;
+cleanup:
+ free(buffer);
+cleanup_fp:
+ fclose(fp);
+out:
+ return ret;
+}
+
+static int read_file(const char *filename, int iterations)
+{
+ int ret = -1;
+ long page_size = sysconf(_SC_PAGESIZE);
+ char *map;
+ size_t i;
+ int fd;
+ struct stat sb;
+
+ fd = open(filename, O_RDONLY);
+ if (fd == -1)
+ goto out;
+
+ if (fstat(fd, &sb) == -1)
+ goto cleanup_fd;
+
+ if (sb.st_size != FILE_SIZE) {
+ fprintf(stderr, "File size mismatch: expected %lu, got %lu\n",
+ (unsigned long)FILE_SIZE, (unsigned long)sb.st_size);
+ goto cleanup_fd;
+ }
+
+ map = mmap(NULL, FILE_SIZE, PROT_READ, MAP_PRIVATE, fd, 0);
+ if (map == MAP_FAILED)
+ goto cleanup_fd;
+
+ for (int iter = 0; iter < iterations; iter++) {
+ for (i = 0; i < FILE_SIZE; i += page_size) {
+ /* access a byte to trigger page fault */
+ volatile char v = map[i];
+ (void)v;
+ }
+ }
+
+ if (munmap(map, FILE_SIZE) == -1)
+ goto cleanup_fd;
+
+ ret = 0;
+
+cleanup_fd:
+ close(fd);
+out:
+ return ret;
+}
+
+static int real_test_child_work(const char *cgroup_path, char *data_filename,
+ char *time_filename, int read_times)
+{
+ struct timespec start, end;
+ double elapsed;
+ FILE *fp;
+
+ if (join_parent_cgroup(cgroup_path))
+ return CHILD_EXIT_JOIN_CGROUP;
+
+ clock_gettime(CLOCK_MONOTONIC, &start);
+
+ if (write_file(data_filename))
+ return CHILD_EXIT_WRITE_FILE;
+
+ if (read_file(data_filename, read_times))
+ return CHILD_EXIT_READ_FILE;
+
+ clock_gettime(CLOCK_MONOTONIC, &end);
+
+ if (!time_filename)
+ return CHILD_EXIT_OK;
+
+ elapsed = (end.tv_sec - start.tv_sec) +
+ (end.tv_nsec - start.tv_nsec) / 1000000000.0;
+ printf("%.6f\n", elapsed);
+
+ fp = fopen(time_filename, "w");
+ if (!fp)
+ return CHILD_EXIT_TIME_FILE;
+ fprintf(fp, "%.6f", elapsed);
+ fclose(fp);
+
+ return CHILD_EXIT_OK;
+}
+
+static int get_time(char *time_filename, double *time)
+{
+ int ret = -1;
+ FILE *fp;
+ char buf[64];
+
+ fp = fopen(time_filename, "r");
+ if (!ASSERT_OK_PTR(fp, "fopen"))
+ goto out;
+
+ if (!ASSERT_OK_PTR(fgets(buf, sizeof(buf), fp), "fgets"))
+ goto cleanup;
+
+ if (sscanf(buf, "%lf", time) != 1) {
+ PRINT_FAIL("sscanf %s", buf);
+ goto cleanup;
+ }
+
+ ret = 0;
+cleanup:
+ fclose(fp);
+out:
+ return ret;
+}
+
+static int
+run_high_low_workload(double *high_elapsed, double *low_elapsed, int read_times)
+{
+ char high_data_file[PATH_MAX];
+ char low_data_file[PATH_MAX];
+ char high_time_file[PATH_MAX];
+ char low_time_file[PATH_MAX];
+ const char *dir = workload_files_dir();
+ pid_t high_pid = -1, low_pid = -1;
+ int fd, status;
+ int ret = -1;
+
+ snprintf(high_data_file, sizeof(high_data_file),
+ "%s/memcg_async_high_data_XXXXXX", dir);
+ snprintf(low_data_file, sizeof(low_data_file),
+ "%s/memcg_async_low_data_XXXXXX", dir);
+ snprintf(high_time_file, sizeof(high_time_file),
+ "%s/memcg_async_high_time_XXXXXX", dir);
+ snprintf(low_time_file, sizeof(low_time_file),
+ "%s/memcg_async_low_time_XXXXXX", dir);
+
+ fd = mkstemp(high_data_file);
+ if (!ASSERT_GE(fd, 0, "mkstemp"))
+ goto cleanup;
+ close(fd);
+
+ fd = mkstemp(low_data_file);
+ if (!ASSERT_GE(fd, 0, "mkstemp"))
+ goto cleanup;
+ close(fd);
+
+ fd = mkstemp(high_time_file);
+ if (!ASSERT_GE(fd, 0, "mkstemp"))
+ goto cleanup;
+ close(fd);
+
+ fd = mkstemp(low_time_file);
+ if (!ASSERT_GE(fd, 0, "mkstemp"))
+ goto cleanup;
+ close(fd);
+
+ low_pid = fork();
+ if (!ASSERT_GE(low_pid, 0, "fork low"))
+ goto cleanup;
+ if (low_pid == 0)
+ exit(real_test_child_work(CG_LOW_DIR, low_data_file,
+ low_time_file, read_times));
+
+ high_pid = fork();
+ if (!ASSERT_GE(high_pid, 0, "fork high"))
+ goto cleanup;
+ if (high_pid == 0)
+ exit(real_test_child_work(CG_HIGH_DIR, high_data_file,
+ high_time_file, read_times));
+
+ low_pid = waitpid(low_pid, &status, 0);
+ if (!ASSERT_GT(low_pid, 0, "low waitpid"))
+ goto cleanup;
+ /*
+ * The child has been reaped and its PID can already be reused,
+ * so mark it to keep cleanup from signaling an unrelated process.
+ */
+ low_pid = -1;
+ if (!ASSERT_TRUE(WIFEXITED(status), "low exited"))
+ goto cleanup;
+ if (WEXITSTATUS(status) != CHILD_EXIT_OK) {
+ PRINT_FAIL("low child failed at: %s (exit status %d)",
+ child_exit_str(WEXITSTATUS(status)),
+ WEXITSTATUS(status));
+ goto cleanup;
+ }
+
+ high_pid = waitpid(high_pid, &status, 0);
+ if (!ASSERT_GT(high_pid, 0, "high waitpid"))
+ goto cleanup;
+ /* Same as above: the reaped PID must not be signaled again. */
+ high_pid = -1;
+ if (!ASSERT_TRUE(WIFEXITED(status), "high exited"))
+ goto cleanup;
+ if (WEXITSTATUS(status) != CHILD_EXIT_OK) {
+ PRINT_FAIL("high child failed at: %s (exit status %d)",
+ child_exit_str(WEXITSTATUS(status)),
+ WEXITSTATUS(status));
+ goto cleanup;
+ }
+
+ if (get_time(high_time_file, high_elapsed))
+ goto cleanup;
+ if (get_time(low_time_file, low_elapsed))
+ goto cleanup;
+
+ ret = 0;
+
+cleanup:
+ /* On failure, make sure no child process is left behind */
+ if (ret) {
+ if (high_pid > 0) {
+ kill(high_pid, SIGKILL);
+ (void)waitpid(high_pid, NULL, 0);
+ }
+ if (low_pid > 0) {
+ kill(low_pid, SIGKILL);
+ (void)waitpid(low_pid, NULL, 0);
+ }
+ }
+ unlink(low_time_file);
+ unlink(high_time_file);
+ unlink(low_data_file);
+ unlink(high_data_file);
+ return ret;
+}
+
+static int
+setup_bpf(u64 high_cgroup_id, u64 low_cgroup_id,
+ struct memcg_async_reclaim **skel_ptr)
+{
+ struct memcg_async_reclaim *skel;
+ struct bpf_args_s bpf_args = {
+ .high_cgroup_id = high_cgroup_id,
+ .low_cgroup_id = low_cgroup_id,
+ .event_delta_threshold = EVENT_DELTA_THRESHOLD,
+ .check_ns = CHECK_PERIOD_NS,
+ };
+ LIBBPF_OPTS(bpf_test_run_opts, run_opts,
+ .ctx_in = &bpf_args,
+ .ctx_size_in = sizeof(bpf_args));
+ int prog_init_fd, err;
+
+ skel = memcg_async_reclaim__open_and_load();
+ if (!ASSERT_OK_PTR(skel, "memcg_async_reclaim__open_and_load"))
+ return -1;
+
+ prog_init_fd = bpf_program__fd(skel->progs.wq_prog_init);
+
+ err = bpf_prog_test_run_opts(prog_init_fd, &run_opts);
+ if (!ASSERT_OK(err, "bpf_prog_test_run_opts"))
+ goto error_out;
+ if (!ASSERT_EQ(run_opts.retval, 0, "prog_init retval"))
+ goto error_out;
+
+ *skel_ptr = skel;
+ return 0;
+
+error_out:
+ memcg_async_reclaim__destroy(skel);
+ return -1;
+}
+
+void test_memcg_wq_async_reclaim(void)
+{
+ u64 high_cgroup_id, low_cgroup_id;
+ int err;
+ double high_time = 0.0, low_time = 0.0;
+ struct memcg_async_reclaim *skel = NULL;
+
+ err = setup_high_low_cgroups(&high_cgroup_id, &low_cgroup_id);
+ if (!ASSERT_OK(err, "setup_high_low_cgroups reclaim"))
+ return;
+
+ err = setup_bpf(high_cgroup_id, low_cgroup_id, &skel);
+ if (!ASSERT_OK(err, "setup_bpf"))
+ goto out;
+
+ err = run_high_low_workload(&high_time, &low_time, READ_TIMES);
+ if (!ASSERT_OK(err, "run_high_low_workload reclaim"))
+ goto out;
+
+ /*
+ * The timing comparison below alone cannot distinguish a working
+ * reclaim from a no-op one, so require that the BPF program
+ * actually reclaimed pages from the low cgroup.
+ */
+ if (!ASSERT_GT(skel->bss->reclaim_calls, 0, "reclaim_calls"))
+ goto out;
+ if (!ASSERT_GT(skel->bss->reclaimed_pages, 0, "reclaimed_pages"))
+ goto out;
+
+ if (high_time >= low_time)
+ PRINT_FAIL("high cgroup not improved with async reclaim: high_time=%f low_time=%f",
+ high_time, low_time);
+
+out:
+ if (skel)
+ memcg_async_reclaim__destroy(skel);
+ cleanup_cgroup_environment();
+}
diff --git a/tools/testing/selftests/bpf/progs/memcg_async_reclaim.c b/tools/testing/selftests/bpf/progs/memcg_async_reclaim.c
new file mode 100644
index 0000000000000..225f0bc667113
--- /dev/null
+++ b/tools/testing/selftests/bpf/progs/memcg_async_reclaim.c
@@ -0,0 +1,180 @@
+// SPDX-License-Identifier: GPL-2.0
+
+#include "vmlinux.h"
+#include "bpf_experimental.h"
+#include <bpf/bpf_helpers.h>
+#include <bpf/bpf_tracing.h>
+#include <bpf/bpf_core_read.h>
+
+#define CLOCK_MONOTONIC_ID 1
+#define PAGE_SIZE 4096UL
+#define RECLAIM_SIZE (32 * PAGE_SIZE)
+#define RECLAIM_MAX_ITER 32
+
+struct bpf_args_s {
+ u64 high_cgroup_id;
+ u64 low_cgroup_id;
+ u64 event_delta_threshold;
+ u64 check_ns;
+};
+
+struct cgroup_memcg {
+ struct cgroup *cgrp;
+ struct mem_cgroup *memcg;
+};
+
+static u64 wq_high_cgroup_id;
+static u64 wq_low_cgroup_id;
+
+/* Statistics exposed to userspace through .bss, so the test can verify
+ * that reclaim actually happened instead of relying on timing alone.
+ */
+u64 reclaim_calls;
+u64 reclaimed_pages;
+
+static int get_cgroup_memcg_from_id(u64 cgroup_id, struct cgroup_memcg *cm)
+{
+ cm->cgrp = bpf_cgroup_from_id(cgroup_id);
+ if (!cm->cgrp)
+ return -1;
+
+ cm->memcg = bpf_get_mem_cgroup(&cm->cgrp->self);
+ if (!cm->memcg) {
+ bpf_cgroup_release(cm->cgrp);
+ return -1;
+ }
+
+ return 0;
+}
+
+static void put_cgroup_memcg(struct cgroup_memcg *cm)
+{
+ bpf_put_mem_cgroup(cm->memcg);
+ bpf_cgroup_release(cm->cgrp);
+}
+
+static int get_cgroup_event(u64 cgroup_id, u64 *val)
+{
+ struct cgroup_memcg cm;
+
+ if (get_cgroup_memcg_from_id(cgroup_id, &cm))
+ return -1;
+ bpf_mem_cgroup_flush_stats(cm.memcg);
+ *val = bpf_mem_cgroup_page_state(cm.memcg,
+ bpf_core_enum_value(enum node_stat_item,
+ WORKINGSET_REFAULT_FILE));
+ put_cgroup_memcg(&cm);
+
+ return 0;
+}
+
+static bool
+should_reclaim_cgroup(u64 cgroup_id, u64 *prev_event, u64 event_delta_threshold)
+{
+ u64 cur, delta;
+
+ if (get_cgroup_event(cgroup_id, &cur))
+ return false;
+
+ delta = cur - *prev_event;
+ *prev_event = cur;
+
+ return delta >= event_delta_threshold;
+}
+
+static int reclaim_cgroup(u64 cgroup_id)
+{
+ struct cgroup_memcg cm;
+ int i;
+
+ if (get_cgroup_memcg_from_id(cgroup_id, &cm))
+ return 0;
+
+ reclaim_calls++;
+ for (i = 0; i < RECLAIM_MAX_ITER; i++) {
+ u64 nr = bpf_proactive_reclaim(cm.memcg, RECLAIM_SIZE);
+
+ if (!nr)
+ break;
+ reclaimed_pages += nr;
+ }
+
+ put_cgroup_memcg(&cm);
+
+ return 0;
+}
+
+struct wq_elem {
+ struct bpf_timer timer;
+ struct bpf_wq work;
+ u64 prev_event;
+ u64 event_delta_threshold;
+ u64 check_ns;
+};
+
+struct {
+ __uint(type, BPF_MAP_TYPE_ARRAY);
+ __uint(max_entries, 1);
+ __type(key, __u32);
+ __type(value, struct wq_elem);
+} wq_map SEC(".maps");
+
+static int async_free(void *map, int *key, void *value)
+{
+ struct wq_elem *elem = value;
+
+ if (should_reclaim_cgroup(wq_high_cgroup_id, &elem->prev_event,
+ elem->event_delta_threshold)) {
+ reclaim_cgroup(wq_low_cgroup_id);
+ bpf_wq_start(&elem->work, 0);
+ }
+
+ return 0;
+}
+
+static int wq_timer_cb(void *map, int *key, struct wq_elem *elem)
+{
+ bpf_wq_start(&elem->work, 0);
+ bpf_timer_start(&elem->timer, elem->check_ns, 0);
+
+ return 0;
+}
+
+SEC("syscall")
+int wq_prog_init(struct bpf_args_s *ctx)
+{
+ struct wq_elem *elem;
+ __u32 key = 0;
+ int ret;
+
+ elem = bpf_map_lookup_elem(&wq_map, &key);
+ if (!elem)
+ return -1;
+
+ ret = bpf_wq_init(&elem->work, &wq_map, 0);
+ if (ret)
+ return ret;
+
+ ret = bpf_wq_set_callback(&elem->work, async_free, 0);
+ if (ret)
+ return ret;
+
+ ret = bpf_timer_init(&elem->timer, &wq_map, CLOCK_MONOTONIC_ID);
+ if (ret)
+ return ret;
+
+ ret = bpf_timer_set_callback(&elem->timer, wq_timer_cb);
+ if (ret)
+ return ret;
+
+ elem->prev_event = 0;
+ elem->event_delta_threshold = ctx->event_delta_threshold;
+ elem->check_ns = ctx->check_ns;
+
+ wq_high_cgroup_id = ctx->high_cgroup_id;
+ wq_low_cgroup_id = ctx->low_cgroup_id;
+
+ return bpf_timer_start(&elem->timer, elem->check_ns, 0);
+}
+
+char LICENSE[] SEC("license") = "GPL";
|
| 11/3 | 2026/08/19 07:44 | llm | kmsan-evaluator |
0mModel:gemini-3.1-pro-preview Args: null Results: null Tokens: input: 10505 output: 234 thoughts: 648 Thoughts: **KMSAN Fuzzing Decision: NeedsKMSAN = false** Okay, let's break this down. We've got a patch that's essentially exposing `try_to_free_mem_cgroup_pages` via a BPF interface – two new kfuncs, `bpf_proactive_reclaim` and `bpf_proactive_reclaim_swappiness`. The question is, does this warrant a dedicated KMSAN fuzzing session? Looking at the code, these kfuncs take a `struct mem_cgroup *memcg`, an `unsigned long size`, and an optional `int swappiness`. Crucially, these arguments are *provided by BPF programs*, meaning the BPF verifier is going to ensure that all of the arguments are properly initialized before they're passed. The kfuncs themselves are just passing these initialized arguments on to `try_to_free_mem_cgroup_pages`. No new kernel structures are being sent to userspace, there's no data copying in either direction beyond those verified arguments, and no complex, partially-initialized data structures are being allocated. The patch also includes a selftest, which is a good sign. So, let's review the criteria. *1. Kernel structures sent to user space where fields might not be fully initialized?* No. *2. Conditional logic or branching that depends on potentially uninitialized variables or struct fields?* Absolutely not, the BPF verifier has that covered. *3. Allocation or initialization of complex data structures where uninitialized fields could be read later?* Nope, not here. *4. Bounds checks, lengths, or logic in a way that allows surrounding code to access uninitialized bytes of existing buffers?* Still no. Given all of that, this patch is basically just a BPF wrapper around an existing function. Any potential issues are going to be standard memory safety violations like use-after-free, out-of-bounds, or some sort of logic error in the wrapping itself. Those are the kinds of things that KASAN and the normal BPF verifier checks should catch, so there's no inherent uninitialized memory exposure. I'm going to set `NeedsKMSAN = false`. |
| 12/3 | 2026/08/19 07:45 | tool | set-results |
0mArgs:{
"KMSANReasoning": "The patch introduces two new BPF kfuncs (`bpf_proactive_reclaim` and `bpf_proactive_reclaim_swappiness`) that wrap the existing `try_to_free_mem_cgroup_pages` function to allow BPF programs to trigger proactive memory reclaim for a memory cgroup. The arguments passed to these kfuncs are scalar values and pointers verified by the BPF verifier, ensuring they are initialized. The patch does not introduce any new data structures copied to or from userspace, nor does it allocate complex structures that might be left partially uninitialized. There is no risk of uninitialized memory being leaked to userspace or used in control flow decisions. Any potential memory safety issues introduced by these changes (such as use-after-free or out-of-bounds accesses on the `mem_cgroup` pointer) would be effectively caught by KASAN and the standard BPF verifier checks. Therefore, a dedicated KMSAN fuzzing session is not required.",
"NeedsKMSAN": false
}Results: {
"KMSANReasoning": "The patch introduces two new BPF kfuncs (`bpf_proactive_reclaim` and `bpf_proactive_reclaim_swappiness`) that wrap the existing `try_to_free_mem_cgroup_pages` function to allow BPF programs to trigger proactive memory reclaim for a memory cgroup. The arguments passed to these kfuncs are scalar values and pointers verified by the BPF verifier, ensuring they are initialized. The patch does not introduce any new data structures copied to or from userspace, nor does it allocate complex structures that might be left partially uninitialized. There is no risk of uninitialized memory being leaked to userspace or used in control flow decisions. Any potential memory safety issues introduced by these changes (such as use-after-free or out-of-bounds accesses on the `mem_cgroup` pointer) would be effectively caught by KASAN and the standard BPF verifier checks. Therefore, a dedicated KMSAN fuzzing session is not required.",
"NeedsKMSAN": false
} |
| Total Calls | Total Tokens | Avg Tokens | Total Duration (Seconds) | Avg Duration (Seconds) |
|---|
| Total Calls | Total Duration (Seconds) | Avg Duration (Seconds) |
|---|