AI Trajectory


Trajectory: (Expand/Collapse All)
Seq Timestamp Type Name Duration
0/0 2026/09/05 20:41 flow patch-triage
0m Args:
null

Results:
{
  "EnableConfigs": [
    "BPF_SYSCALL",
    "MEMCG",
    "DEBUG_INFO_BTF"
  ],
  "FocusSymbols": [
    "bpf_proactive_reclaim"
  ],
  "KMSANReasoning": "The patch introduces the `bpf_proactive_reclaim` BPF kfunc in `mm/bpf_memcontrol.c` and associated BPF selftests. The function accepts a pointer to `struct mem_cgroup` and a `size` parameter, checks the execution context, calls `try_to_free_mem_cgroup_pages()`, and returns a scalar `unsigned long` indicating the number of reclaimed bytes.\n\nThere are no uninitialized variables, no uninitialized struct fields, no uninitialized buffer reads, and no transfers of kernel memory structures to user space. Any potential issues (e.g., deadlock, recursion in reclaim context, refcounting, or invalid pointer dereference) fall strictly under KASAN, LOCKDEP, and standard kernel debugging facilities, not KMSAN.",
  "NeedsKMSAN": false,
  "Reasoning": "The patch introduces a new BPF kfunc, bpf_proactive_reclaim(), allowing sleepable BPF_PROG_TYPE_SYSCALL programs to trigger proactive memory cgroup reclaim via try_to_free_mem_cgroup_pages(). This modifies reachable core memory management and BPF subsystem code and warrants fuzzing.",
  "WorthFuzzing": true
}

1/1 2026/09/05 20:41 action read-patch-diff
0m Args:
null

Results:
{
  "PatchDiff": "commit e71a3219429423e5fd3664f52cdb8943c1849bbe\nAuthor: syz-cluster \u003ctriage@syzkaller.com\u003e\nDate:   Sat Sep 5 20:41:22 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..92f35ba66309e 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,74 @@ __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() overwrites\n+ * current-\u003ereclaim_state, so a nested call would corrupt the outer\n+ * reclaim state. Reclaim windows are marked with PF_MEMALLOC;\n+ * reclaim_state is also checked because it is installed slightly\n+ * before PF_MEMALLOC.\n+ */\n+static bool bpf_in_reclaim_context(void)\n+{\n+\treturn (current-\u003eflags \u0026 PF_MEMALLOC) || current-\u003ereclaim_state;\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, clamped to\n+ *         MEMCG_CHARGE_BATCH (64 pages)\n+ *\n+ * Trigger one proactive reclaim pass on @memcg, similar to a write to\n+ * memory.reclaim, but without retrying until @size is reached.\n+ *\n+ * @size is clamped so that one call is a bounded unit of work, matching\n+ * the memory.high workqueue fallback in high_work_func(). To reclaim\n+ * more, call this kfunc repeatedly instead of passing a larger @size.\n+ *\n+ * This kfunc is restricted to BPF_PROG_TYPE_SYSCALL to ensure it runs\n+ * in a clean process context. The SYSCALL program can schedule the\n+ * actual reclaim work via bpf_wq or timers, which also execute in\n+ * safe process context (workqueue, task_work).\n+ *\n+ * When reclaim is driven from a bpf_wq, call this kfunc once per\n+ * callback and requeue the same work item for the next batch rather\n+ * than looping inside the callback: a long-running callback stalls\n+ * other work on the shared workqueue, and because lru_lock is held with\n+ * interrupts disabled the resulting contention also delays IPI\n+ * handling. Give each target memcg its own bpf_wq item, so that\n+ * reclaiming one memcg neither serializes behind nor piles up on top of\n+ * another. Deciding whether to submit the next batch is up to the BPF\n+ * program, which can stop at any point, e.g. once the target cgroup is\n+ * dying.\n+ *\n+ * Must not be called with a filesystem lock held: the reclaim path\n+ * may deadlock on it via filesystem shrinkers.\n+ *\n+ * Return: The amount of memory reclaimed, in bytes, or 0 if @size is\n+ * smaller than a page or the task is already in a reclaim context.\n+ */\n+__bpf_kfunc unsigned long bpf_proactive_reclaim(struct mem_cgroup *memcg,\n+\t\t\t\t\t\tunsigned long size)\n+{\n+\tunsigned long nr_reclaimed;\n+\tunsigned long nr_pages;\n+\n+\tif (size \u003c PAGE_SIZE || unlikely(bpf_in_reclaim_context()))\n+\t\treturn 0;\n+\n+\tnr_pages = min(size / PAGE_SIZE, (unsigned long)MEMCG_CHARGE_BATCH);\n+\n+\tnr_reclaimed = try_to_free_mem_cgroup_pages(memcg, nr_pages,\n+\t\t\t\t\t\t    GFP_KERNEL,\n+\t\t\t\t\t\t    MEMCG_RECLAIM_MAY_SWAP |\n+\t\t\t\t\t\t    MEMCG_RECLAIM_PROACTIVE,\n+\t\t\t\t\t\t    NULL);\n+\n+\treturn nr_reclaimed * PAGE_SIZE;\n+}\n+\n __bpf_kfunc_end_defs();\n \n BTF_KFUNCS_START(bpf_memcontrol_kfuncs)\n@@ -171,22 +240,44 @@ BTF_ID_FLAGS(func, bpf_mem_cgroup_memory_events)\n 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_KFUNCS_END(bpf_memcontrol_kfuncs)\n \n+/*\n+ * Proactive reclaim needs a clean process context, so it is restricted\n+ * to BPF_PROG_TYPE_SYSCALL. The bpf_wq and task_work callbacks that a\n+ * SYSCALL program schedules run as the same program type, so they can\n+ * still invoke it; generic sleepable programs (e.g. fentry on reclaim\n+ * paths, inode_rmdir) cannot.\n+ */\n+BTF_KFUNCS_START(bpf_memcontrol_reclaim_kfuncs)\n+BTF_ID_FLAGS(func, bpf_proactive_reclaim, KF_SLEEPABLE)\n+BTF_KFUNCS_END(bpf_memcontrol_reclaim_kfuncs)\n+\n static const struct btf_kfunc_id_set bpf_memcontrol_kfunc_set = {\n \t.owner          = THIS_MODULE,\n \t.set            = \u0026bpf_memcontrol_kfuncs,\n };\n \n+static const struct btf_kfunc_id_set bpf_memcontrol_reclaim_kfunc_set = {\n+\t.owner          = THIS_MODULE,\n+\t.set            = \u0026bpf_memcontrol_reclaim_kfuncs,\n+};\n+\n static int __init bpf_memcontrol_init(void)\n {\n \tint err;\n \n \terr = register_btf_kfunc_id_set(BPF_PROG_TYPE_UNSPEC,\n \t\t\t\t\t\u0026bpf_memcontrol_kfunc_set);\n-\tif (err)\n+\tif (err) {\n \t\tpr_warn(\"error while registering bpf memcontrol kfuncs: %d\", err);\n+\t\treturn err;\n+\t}\n+\n+\terr = register_btf_kfunc_id_set(BPF_PROG_TYPE_SYSCALL,\n+\t\t\t\t\t\u0026bpf_memcontrol_reclaim_kfunc_set);\n+\tif (err)\n+\t\tpr_warn(\"error registering bpf reclaim kfuncs: %d\", err);\n \n \treturn err;\n }\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..65f5006844636\n--- /dev/null\n+++ b/tools/testing/selftests/bpf/prog_tests/memcg_async_reclaim.c\n@@ -0,0 +1,686 @@\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 {\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 CG_DYING_DIR \"/memcg_async_reclaim_dying\"\n+#define CG_DYING_TRIGGER_DIR CG_DYING_DIR \"/trigger\"\n+#define CG_DYING_TARGET_DIR CG_DYING_DIR \"/target\"\n+\n+#define CHECK_PERIOD_NS (2 * 1000 * 1000ull)\n+#define EVENT_DELTA_THRESHOLD 1\n+\n+/*\n+ * Timing for the dying test: after the target cgroup is removed, give\n+ * in-flight reclaim passes time to drain, then wait for a reclaim round\n+ * to hit the removed target. The keepalive reader keeps the trigger\n+ * cgroup refaulting, and the timer fires every CHECK_PERIOD_NS, so\n+ * such a round must show up within a few timer periods.\n+ */\n+#define DYING_SETTLE_US (200 * 1000)\n+#define DYING_POLL_ITERS 500\n+#define DYING_POLL_INTERVAL_US (10 * 1000)\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+/*\n+ * The dying test needs an empty reclaim target plus a cgroup that keeps\n+ * refaulting while the target is removed, so reclaim rounds keep\n+ * starting and run into the removed target. The two have to be separate\n+ * cgroups: the target must hold no processes to be removed, and v2's\n+ * no-internal-process constraint keeps the refaulting workload out of\n+ * any parent that has domain children.\n+ */\n+static int setup_dying_cgroups(u64 *trigger_cgroup_id, u64 *target_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_DYING_DIR);\n+\tif (!ASSERT_GE(ret, 0, \"create_and_get_cgroup \" CG_DYING_DIR))\n+\t\tgoto cleanup;\n+\tclose(ret);\n+\n+\tret = enable_controllers(CG_DYING_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_DYING_DIR, \"memory.max\", limit_buf);\n+\tif (!ASSERT_OK(ret, \"write_cgroup_file memory.max\"))\n+\t\tgoto cleanup;\n+\n+\t/* See the matching write in setup_high_low_cgroups(). */\n+\tif (!access(\"/proc/swaps\", F_OK)) {\n+\t\tret = write_cgroup_file(CG_DYING_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_DYING_TRIGGER_DIR);\n+\tif (!ASSERT_GE(ret, 0, \"create_and_get_cgroup \" CG_DYING_TRIGGER_DIR))\n+\t\tgoto cleanup;\n+\tclose(ret);\n+\n+\t*trigger_cgroup_id = get_cgroup_id(CG_DYING_TRIGGER_DIR);\n+\tif (!ASSERT_GT(*trigger_cgroup_id, 0, \"get_cgroup_id\"))\n+\t\tgoto cleanup;\n+\n+\tret = create_and_get_cgroup(CG_DYING_TARGET_DIR);\n+\tif (!ASSERT_GE(ret, 0, \"create_and_get_cgroup \" CG_DYING_TARGET_DIR))\n+\t\tgoto cleanup;\n+\tclose(ret);\n+\n+\t*target_cgroup_id = get_cgroup_id(CG_DYING_TARGET_DIR);\n+\tif (!ASSERT_GT(*target_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+\tpid_t wait_ret;\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\t_exit(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\t_exit(real_test_child_work(CG_HIGH_DIR, high_data_file,\n+\t\t\t\t\t  high_time_file, read_times));\n+\n+\twait_ret = waitpid(low_pid, \u0026status, 0);\n+\tif (!ASSERT_GT(wait_ret, 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+\twait_ret = waitpid(high_pid, \u0026status, 0);\n+\tif (!ASSERT_GT(wait_ret, 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 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 = \u0026args,\n+\t\t.ctx_size_in = sizeof(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_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 memory 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_bytes, 0, \"reclaimed_bytes\"))\n+\t\tgoto out;\n+\n+\tif (high_time \u003e= low_time)\n+\t\tPRINT_FAIL(\"high cgroup not improved: high=%f low=%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+}\n+\n+/*\n+ * Keep refaults flowing through the trigger cgroup so reclaim rounds\n+ * keep being triggered while the target cgroup is being removed. The\n+ * child joins the trigger cgroup and writes the data file there, so\n+ * that the file pages are charged to the trigger cgroup and actually\n+ * come under its memory limit; then it re-reads the file in a loop\n+ * until it is killed.\n+ */\n+static pid_t spawn_keepalive_reader(const char *data_file)\n+{\n+\tpid_t pid = fork();\n+\n+\tif (pid != 0)\n+\t\treturn pid;\n+\n+\tif (join_parent_cgroup(CG_DYING_TRIGGER_DIR))\n+\t\t_exit(CHILD_EXIT_JOIN_CGROUP);\n+\tif (write_file(data_file))\n+\t\t_exit(CHILD_EXIT_WRITE_FILE);\n+\tfor (;;) {\n+\t\tif (read_file(data_file, READ_TIMES))\n+\t\t\t_exit(CHILD_EXIT_READ_FILE);\n+\t}\n+}\n+\n+/*\n+ * Remove the reclaim target while the BPF program keeps running and\n+ * verify that reclaim stops on the dying/removed cgroup instead of\n+ * reclaiming from it.\n+ *\n+ * The target stays empty; the workload lives in the trigger cgroup and\n+ * only keeps refaults flowing so that reclaim rounds keep starting,\n+ * both before and after the target is removed. reclaim_calls growing\n+ * while the target is alive proves that rounds really run (the kfunc\n+ * returns 0 on the empty target, but the call is still counted), and\n+ * after the removal the skip counters must grow while reclaim_calls\n+ * and reclaimed_bytes stay frozen.\n+ */\n+void test_memcg_async_reclaim_dying(void)\n+{\n+\tu64 trigger_cgroup_id, target_cgroup_id;\n+\tu64 calls_before, bytes_before;\n+\tchar data_file[PATH_MAX] = \"\";\n+\tstruct memcg_async_reclaim *skel = NULL;\n+\tpid_t reader_pid = -1;\n+\tint err, fd, i;\n+\n+\terr = setup_dying_cgroups(\u0026trigger_cgroup_id, \u0026target_cgroup_id);\n+\tif (!ASSERT_OK(err, \"setup_dying_cgroups\"))\n+\t\treturn;\n+\n+\terr = setup_bpf(trigger_cgroup_id, target_cgroup_id, \u0026skel);\n+\tif (!ASSERT_OK(err, \"setup_bpf\"))\n+\t\tgoto out;\n+\n+\tsnprintf(data_file, sizeof(data_file),\n+\t\t \"%s/memcg_async_dying_XXXXXX\", workload_files_dir());\n+\tfd = mkstemp(data_file);\n+\tif (!ASSERT_GE(fd, 0, \"mkstemp\"))\n+\t\tgoto out;\n+\tclose(fd);\n+\n+\treader_pid = spawn_keepalive_reader(data_file);\n+\tif (!ASSERT_GT(reader_pid, 0, \"fork keepalive reader\"))\n+\t\tgoto out;\n+\n+\t/* Wait for reclaim rounds to reach the live target cgroup. */\n+\tfor (i = 0; i \u003c DYING_POLL_ITERS; i++) {\n+\t\tif (skel-\u003ebss-\u003ereclaim_calls \u003e 0)\n+\t\t\tbreak;\n+\t\tusleep(DYING_POLL_INTERVAL_US);\n+\t}\n+\tif (!ASSERT_GT(skel-\u003ebss-\u003ereclaim_calls, 0, \"reclaim_calls\"))\n+\t\tgoto out;\n+\n+\tremove_cgroup(CG_DYING_TARGET_DIR);\n+\n+\t/* Let reclaim passes that were already in flight drain. */\n+\tusleep(DYING_SETTLE_US);\n+\n+\tcalls_before = skel-\u003ebss-\u003ereclaim_calls;\n+\tbytes_before = skel-\u003ebss-\u003ereclaimed_bytes;\n+\n+\t/* Wait for reclaim rounds to hit the removed cgroup. */\n+\tfor (i = 0; i \u003c DYING_POLL_ITERS; i++) {\n+\t\tif (skel-\u003ebss-\u003ereclaim_target_gone ||\n+\t\t    skel-\u003ebss-\u003ereclaim_skipped_dying)\n+\t\t\tbreak;\n+\t\tusleep(DYING_POLL_INTERVAL_US);\n+\t}\n+\n+\tif (!skel-\u003ebss-\u003ereclaim_target_gone \u0026\u0026\n+\t    !skel-\u003ebss-\u003ereclaim_skipped_dying) {\n+\t\tPRINT_FAIL(\"no reclaim round hit the removed cgroup (gone=%llu, dying=%llu)\",\n+\t\t\t   (unsigned long long)skel-\u003ebss-\u003ereclaim_target_gone,\n+\t\t\t   (unsigned long long)skel-\u003ebss-\u003ereclaim_skipped_dying);\n+\t\tgoto out;\n+\t}\n+\n+\t/*\n+\t * reclaim_skipped_dying shows that the CSS_DYING/CSS_ONLINE check\n+\t * caught the cgroup mid-teardown. Whether it is hit is timing\n+\t * dependent, because the cgroup may already be fully released, so\n+\t * only the combined skip count above is asserted.\n+\t */\n+\tprintf(\"memcg_async_reclaim_dying: skips on removed cgroup: gone=%llu, dying=%llu\\n\",\n+\t       (unsigned long long)skel-\u003ebss-\u003ereclaim_target_gone,\n+\t       (unsigned long long)skel-\u003ebss-\u003ereclaim_skipped_dying);\n+\n+\t/* Nothing may have been reclaimed from the removed target. */\n+\tif (!ASSERT_EQ(skel-\u003ebss-\u003ereclaim_calls, calls_before, \"reclaim_calls\"))\n+\t\tgoto out;\n+\tif (!ASSERT_EQ(skel-\u003ebss-\u003ereclaimed_bytes, bytes_before,\n+\t\t       \"reclaimed_bytes\"))\n+\t\tgoto out;\n+\n+out:\n+\tif (reader_pid \u003e 0) {\n+\t\tkill(reader_pid, SIGKILL);\n+\t\t(void)waitpid(reader_pid, NULL, 0);\n+\t}\n+\tif (data_file[0])\n+\t\tunlink(data_file);\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..e6839ade472bb\n--- /dev/null\n+++ b/tools/testing/selftests/bpf/progs/memcg_async_reclaim.c\n@@ -0,0 +1,259 @@\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+/*\n+ * One reclaim round targets RECLAIM_MAX_ITER batches of RECLAIM_SIZE\n+ * each. Each bpf_wq callback reclaims a single batch and requeues the\n+ * same work item for the next one, so no callback runs longer than one\n+ * bounded reclaim pass.\n+ */\n+#define RECLAIM_SIZE\t\t(32 * PAGE_SIZE)\n+#define RECLAIM_MAX_ITER\t32\n+\n+struct bpf_args {\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+/*\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_bytes;\n+/*\n+ * Reclaim attempts skipped because the target cgroup is dying or has\n+ * been removed. reclaim_skipped_dying counts lookups that still found\n+ * the cgroup while it is being torn down, reclaim_target_gone counts\n+ * lookups that found nothing. The test removes the target cgroup while\n+ * reclaim is running and checks that reclaim stops via these counters.\n+ */\n+u64 reclaim_skipped_dying;\n+u64 reclaim_target_gone;\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+/*\n+ * A cgroup is dying once it has been offlined (CSS_ONLINE cleared) or\n+ * CSS_DYING has been raised, mirroring cgroup_is_dead()/css_is_dying()\n+ * in include/linux/cgroup.h. bpf_cgroup_from_id() can still hand back\n+ * such a cgroup, because it only fails once the last reference has been\n+ * dropped, so reclaim has to check these flags instead of relying on\n+ * the lookup failing.\n+ *\n+ * CSS_ONLINE and CSS_DYING come from vmlinux.h: the kernel defines them\n+ * in an anonymous enum, so bpf_core_enum_value() has no enum type to\n+ * bind to, and redeclaring them locally would clash with the vmlinux.h\n+ * enumerators. vmlinux.h is generated from the running kernel's BTF, so\n+ * the values already match the target kernel.\n+ */\n+static bool cgroup_is_dying(struct cgroup *cgrp)\n+{\n+\tunsigned int flags = cgrp-\u003eself.flags;\n+\n+\treturn (flags \u0026 CSS_DYING) || !(flags \u0026 CSS_ONLINE);\n+}\n+\n+/*\n+ * Reclaim one batch from the target cgroup. Returns the number of\n+ * bytes reclaimed, or 0 if the cgroup is dying or gone or nothing was\n+ * reclaimed.\n+ */\n+static u64 reclaim_cgroup(u64 cgroup_id, u64 size)\n+{\n+\tstruct cgroup_memcg cm;\n+\tu64 nr = 0;\n+\n+\tif (get_cgroup_memcg_from_id(cgroup_id, \u0026cm)) {\n+\t\treclaim_target_gone++;\n+\t\treturn 0;\n+\t}\n+\n+\tif (cgroup_is_dying(cm.cgrp)) {\n+\t\treclaim_skipped_dying++;\n+\t\tput_cgroup_memcg(\u0026cm);\n+\t\treturn 0;\n+\t}\n+\n+\treclaim_calls++;\n+\tnr = bpf_proactive_reclaim(cm.memcg, size);\n+\treclaimed_bytes += nr;\n+\n+\tput_cgroup_memcg(\u0026cm);\n+\n+\treturn nr;\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+\t/*\n+\t * Bytes still to reclaim in the current round, carried across\n+\t * requeues. 0 means no round is in progress; the timer path\n+\t * starts a new round by resetting it, requeued work only looks\n+\t * at it.\n+\t */\n+\tu64 remaining;\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 reclaim_work_fn(void *map, int *key, void *value)\n+{\n+\tstruct wq_elem *elem = value;\n+\tu64 nr, size;\n+\n+\tif (!elem-\u003eremaining) {\n+\t\t/*\n+\t\t * Timer-triggered entry: start a new round only when the\n+\t\t * high cgroup refaults enough. Requeued entries skip this\n+\t\t * check and only look at remaining, so the refault delta\n+\t\t * is consumed once per round.\n+\t\t */\n+\t\tif (!should_reclaim_cgroup(wq_high_cgroup_id, \u0026elem-\u003eprev_event,\n+\t\t\telem-\u003eevent_delta_threshold))\n+\t\t\treturn 0;\n+\t\telem-\u003eremaining = RECLAIM_MAX_ITER * RECLAIM_SIZE;\n+\t}\n+\n+\t/* One bounded reclaim pass per callback */\n+\tsize = elem-\u003eremaining \u003c RECLAIM_SIZE ? elem-\u003eremaining : RECLAIM_SIZE;\n+\tnr = reclaim_cgroup(wq_low_cgroup_id, size);\n+\tif (!nr) {\n+\t\telem-\u003eremaining = 0;\n+\t\treturn 0;\n+\t}\n+\n+\t/* try_to_free_mem_cgroup_pages() may reclaim more than requested */\n+\tif (nr \u003e= elem-\u003eremaining)\n+\t\telem-\u003eremaining = 0;\n+\telse\n+\t\telem-\u003eremaining -= nr;\n+\n+\t/* Requeue the same work item for the next batch */\n+\tif (elem-\u003eremaining)\n+\t\tbpf_wq_start(\u0026elem-\u003ework, 0);\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 *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, reclaim_work_fn, 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-\u003eremaining = 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/09/05 20:41 agent patch-evaluator
0m Model:
core

Args:
null

Results:
{
  "EnableConfigs": [
    "BPF_SYSCALL",
    "MEMCG",
    "DEBUG_INFO_BTF"
  ],
  "FocusSymbols": [
    "bpf_proactive_reclaim"
  ],
  "Reasoning": "The patch introduces a new BPF kfunc, bpf_proactive_reclaim(), allowing sleepable BPF_PROG_TYPE_SYSCALL programs to trigger proactive memory cgroup reclaim via try_to_free_mem_cgroup_pages(). This modifies reachable core memory management and BPF subsystem code and warrants fuzzing.",
  "WorthFuzzing": true
}

Instruction:
You are an expert Linux kernel maintainer and security engineer.
Your job is to review a provided patch series and evaluate whether it warrants fuzzing with syzkaller.

IMPORTANT: The changes have ALREADY been applied and committed as the HEAD commit in
your workspace. Do NOT rely on internal assumptions. You must actively use your code access
tools to inspect the actual source code, callers, and surrounding context.

================================================================================
1. CORE TRIAGE PHILOSOPHY
================================================================================
The goal of patch fuzzing is to discover crashes, regressions, exposed latent bugs,
and newly triggered assertions introduced by the patch series.

- REACHABILITY IS THE PRIMARY GATE:
  Fuzzing can only discover bugs in code that can actually execute in standard virtualized
  environments (GCE or QEMU, utilizing software-emulated devices like USB gadgets, netdev, tun/tap).
  If the modified code is structurally unreachable (see Section 2), it MUST NOT be fuzzed,
  regardless of whether it adds assertions or complex logic.

- DO NOT BLINDLY TRUST "NO FUNCTIONAL CHANGE" (NFCI) OR "REFACTORING" CLAIMS:
  Patch authors routinely label changes as "cleanups", "refactorings", or state
  "No functional change intended". Do NOT take these claims at face value.
  Code refactorings that rearrange logic, introduce helper functions, or alter state management
  in core subsystems frequently introduce subtle semantic shifts or uncover latent kernel bugs.
  If reachable executable code is modified or refactored, it MUST be fuzzed.

- NEW OR MODIFIED ASSERTIONS IN REACHABLE CODE MUST BE FUZZED:
  When a patch introduces or modifies runtime checks or assertions (e.g., WARN_ON*, VM_WARN_ON*,
  BUG_ON*, lockdep_assert*) in reachable code paths, it enforces new or stricter invariants.
  Even if the author believes the invariant always holds, fuzzing is essential to verify whether
  an unusual sequence of operations can violate it.

================================================================================
2. WHEN TO RETURN WorthFuzzing=false (NEGATIVE CRITERIA)
================================================================================
Return WorthFuzzing=false ONLY IF all modified code falls strictly into one or more of these categories:

- Non-kernel and non-executable changes:
  * Modifications to Documentation/, comments, or spelling fixes.
  * User-space directories, self-tests, samples, or scripts (e.g., tools/, samples/, scripts/, usr/)
    that do not affect the compiled kernel image (vmlinux) or kernel modules.
  * Purely decorative logging (e.g., message strings in pr_err, printk, dev_info) or tracepoints
    that do not alter control flow or data structures.
  * Build system or Kconfig changes that do not alter compiled C logic.
- Structurally unreachable hardware:
  * Vendor-specific PCIe switches, SmartNICs, or GPU drivers (e.g., mlxsw, pds_core, qed,
    ionic, amdgpu) requiring physical ASIC/PCIe cards not emulated in standard QEMU.
- Unreachable execution paths:
  * Driver teardown callbacks (.remove, .shutdown, pci_unregister_driver) executed only during
    physical PCI hot-unplug or manual sysfs driver unbinding.
  * Code paths exclusive to architectures other than the target architecture.

================================================================================
3. WHEN TO RETURN WorthFuzzing=true (POSITIVE CRITERIA)
================================================================================
Return WorthFuzzing=true whenever the patch touches reachable executable code, including:
- Core Subsystems:
  * Any logic modifications in memory management (mm/), synchronization/locking (kernel/locking/),
    BPF, scheduler, core networking, VFS, or syscall handling.
- Refactorings and Code Cleanups:
  * Any restructuring of reachable data structures, helper abstractions, or algorithm flows.
- Runtime Assertions and Defensive Checks:
  * Any introduction or alteration of assertions (WARN_ON*, VM_WARN_ON*, BUG_ON*, etc.) in reachable paths.
- Reachable Drivers and Protocols:
  * Drivers accessible via virtual buses (virtio, USB gadget, loopback, netlink, binder, sockets, etc.).

================================================================================
4. EXTRACTING FocusSymbols (PREVENTING DILUTION)
================================================================================
When WorthFuzzing=true, you must extract specific kernel functions into FocusSymbols to guide the fuzzer:

- AVOID UBIQUITOUS LIFECYCLE HOT-PATHS:
  Do NOT list generic, ubiquitous functions called by almost every program in the corpus
  (including, but not limited to: general memory allocators and deallocators, page fault
  and trap handlers, or core synchronization primitives; this is not an exhaustive list).
  Listing ubiquitous functions causes the fuzzer to classify thousands of unrelated tests as "focused",
  which severely dilutes fuzzing effort away from the actual changes.

- TARGET SPECIFIC FEATURE LOGIC AND ENTRYPOINTS:
  List functions that specifically implement the logic being added or altered, or direct API entrypoints
  for the subsystem feature under review.

- HANDLING STATIC INLINE FUNCTIONS IN HEADERS (.h):
  Compiler-inlined static functions (such as static inlines in mm/*.h or include/linux/*.h) lack
  distinct symbol addresses in vmlinux and cannot be targeted directly by symbol coverage filters.
  If the changes are primarily in static inline helpers, identify non-static, feature-specific caller
  functions in .c files that exercise them (avoiding ubiquitous lifecycle wrappers).

================================================================================
5. IDENTIFYING EnableConfigs
================================================================================
Identify any specific CONFIG_ options required to properly compile and reach the modified code:
- Inspect Kconfig files and #ifdef guards; do not make assumptions.
- Check "depends on" lines in Kconfig to include any non-standard parent subsystem configs needed.
- Strip any '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:
Target architecture: amd64

For your convenience, here is the diff of the changes:
commit e71a3219429423e5fd3664f52cdb8943c1849bbe
Author: syz-cluster <triage@syzkaller.com>
Date:   Sat Sep 5 20:41:22 2026 +0000

    syz-cluster: applied patch under review

diff --git a/mm/bpf_memcontrol.c b/mm/bpf_memcontrol.c
index 716df49d76477..92f35ba66309e 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,74 @@ __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() overwrites
+ * current->reclaim_state, so a nested call would corrupt the outer
+ * reclaim state. Reclaim windows are marked with PF_MEMALLOC;
+ * reclaim_state is also checked because it is installed slightly
+ * before PF_MEMALLOC.
+ */
+static bool bpf_in_reclaim_context(void)
+{
+	return (current->flags & PF_MEMALLOC) || current->reclaim_state;
+}
+
+/**
+ * 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, clamped to
+ *         MEMCG_CHARGE_BATCH (64 pages)
+ *
+ * Trigger one proactive reclaim pass on @memcg, similar to a write to
+ * memory.reclaim, but without retrying until @size is reached.
+ *
+ * @size is clamped so that one call is a bounded unit of work, matching
+ * the memory.high workqueue fallback in high_work_func(). To reclaim
+ * more, call this kfunc repeatedly instead of passing a larger @size.
+ *
+ * This kfunc is restricted to BPF_PROG_TYPE_SYSCALL to ensure it runs
+ * in a clean process context. The SYSCALL program can schedule the
+ * actual reclaim work via bpf_wq or timers, which also execute in
+ * safe process context (workqueue, task_work).
+ *
+ * When reclaim is driven from a bpf_wq, call this kfunc once per
+ * callback and requeue the same work item for the next batch rather
+ * than looping inside the callback: a long-running callback stalls
+ * other work on the shared workqueue, and because lru_lock is held with
+ * interrupts disabled the resulting contention also delays IPI
+ * handling. Give each target memcg its own bpf_wq item, so that
+ * reclaiming one memcg neither serializes behind nor piles up on top of
+ * another. Deciding whether to submit the next batch is up to the BPF
+ * program, which can stop at any point, e.g. once the target cgroup is
+ * dying.
+ *
+ * Must not be called with a filesystem lock held: the reclaim path
+ * may deadlock on it via filesystem shrinkers.
+ *
+ * Return: The amount of memory reclaimed, in bytes, or 0 if @size is
+ * smaller than a page or the task is already in a reclaim context.
+ */
+__bpf_kfunc unsigned long bpf_proactive_reclaim(struct mem_cgroup *memcg,
+						unsigned long size)
+{
+	unsigned long nr_reclaimed;
+	unsigned long nr_pages;
+
+	if (size < PAGE_SIZE || unlikely(bpf_in_reclaim_context()))
+		return 0;
+
+	nr_pages = min(size / PAGE_SIZE, (unsigned long)MEMCG_CHARGE_BATCH);
+
+	nr_reclaimed = try_to_free_mem_cgroup_pages(memcg, nr_pages,
+						    GFP_KERNEL,
+						    MEMCG_RECLAIM_MAY_SWAP |
+						    MEMCG_RECLAIM_PROACTIVE,
+						    NULL);
+
+	return nr_reclaimed * PAGE_SIZE;
+}
+
 __bpf_kfunc_end_defs();
 
 BTF_KFUNCS_START(bpf_memcontrol_kfuncs)
@@ -171,22 +240,44 @@ BTF_ID_FLAGS(func, bpf_mem_cgroup_memory_events)
 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_KFUNCS_END(bpf_memcontrol_kfuncs)
 
+/*
+ * Proactive reclaim needs a clean process context, so it is restricted
+ * to BPF_PROG_TYPE_SYSCALL. The bpf_wq and task_work callbacks that a
+ * SYSCALL program schedules run as the same program type, so they can
+ * still invoke it; generic sleepable programs (e.g. fentry on reclaim
+ * paths, inode_rmdir) cannot.
+ */
+BTF_KFUNCS_START(bpf_memcontrol_reclaim_kfuncs)
+BTF_ID_FLAGS(func, bpf_proactive_reclaim, KF_SLEEPABLE)
+BTF_KFUNCS_END(bpf_memcontrol_reclaim_kfuncs)
+
 static const struct btf_kfunc_id_set bpf_memcontrol_kfunc_set = {
 	.owner          = THIS_MODULE,
 	.set            = &bpf_memcontrol_kfuncs,
 };
 
+static const struct btf_kfunc_id_set bpf_memcontrol_reclaim_kfunc_set = {
+	.owner          = THIS_MODULE,
+	.set            = &bpf_memcontrol_reclaim_kfuncs,
+};
+
 static int __init bpf_memcontrol_init(void)
 {
 	int err;
 
 	err = register_btf_kfunc_id_set(BPF_PROG_TYPE_UNSPEC,
 					&bpf_memcontrol_kfunc_set);
-	if (err)
+	if (err) {
 		pr_warn("error while registering bpf memcontrol kfuncs: %d", err);
+		return err;
+	}
+
+	err = register_btf_kfunc_id_set(BPF_PROG_TYPE_SYSCALL,
+					&bpf_memcontrol_reclaim_kfunc_set);
+	if (err)
+		pr_warn("error registering bpf reclaim kfuncs: %d", err);
 
 	return err;
 }
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..65f5006844636
--- /dev/null
+++ b/tools/testing/selftests/bpf/prog_tests/memcg_async_reclaim.c
@@ -0,0 +1,686 @@
+// 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 {
+	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 CG_DYING_DIR "/memcg_async_reclaim_dying"
+#define CG_DYING_TRIGGER_DIR CG_DYING_DIR "/trigger"
+#define CG_DYING_TARGET_DIR CG_DYING_DIR "/target"
+
+#define CHECK_PERIOD_NS (2 * 1000 * 1000ull)
+#define EVENT_DELTA_THRESHOLD 1
+
+/*
+ * Timing for the dying test: after the target cgroup is removed, give
+ * in-flight reclaim passes time to drain, then wait for a reclaim round
+ * to hit the removed target. The keepalive reader keeps the trigger
+ * cgroup refaulting, and the timer fires every CHECK_PERIOD_NS, so
+ * such a round must show up within a few timer periods.
+ */
+#define DYING_SETTLE_US (200 * 1000)
+#define DYING_POLL_ITERS 500
+#define DYING_POLL_INTERVAL_US (10 * 1000)
+
+/*
+ * 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;
+}
+
+/*
+ * The dying test needs an empty reclaim target plus a cgroup that keeps
+ * refaulting while the target is removed, so reclaim rounds keep
+ * starting and run into the removed target. The two have to be separate
+ * cgroups: the target must hold no processes to be removed, and v2's
+ * no-internal-process constraint keeps the refaulting workload out of
+ * any parent that has domain children.
+ */
+static int setup_dying_cgroups(u64 *trigger_cgroup_id, u64 *target_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_DYING_DIR);
+	if (!ASSERT_GE(ret, 0, "create_and_get_cgroup " CG_DYING_DIR))
+		goto cleanup;
+	close(ret);
+
+	ret = enable_controllers(CG_DYING_DIR, "memory");
+	if (!ASSERT_OK(ret, "enable_controllers"))
+		goto cleanup;
+
+	snprintf(limit_buf, sizeof(limit_buf), "%lu", CG_LIMIT);
+	ret = write_cgroup_file(CG_DYING_DIR, "memory.max", limit_buf);
+	if (!ASSERT_OK(ret, "write_cgroup_file memory.max"))
+		goto cleanup;
+
+	/* See the matching write in setup_high_low_cgroups(). */
+	if (!access("/proc/swaps", F_OK)) {
+		ret = write_cgroup_file(CG_DYING_DIR, "memory.swap.max", "0");
+		if (!ASSERT_OK(ret, "write_cgroup_file memory.swap.max"))
+			goto cleanup;
+	}
+
+	ret = create_and_get_cgroup(CG_DYING_TRIGGER_DIR);
+	if (!ASSERT_GE(ret, 0, "create_and_get_cgroup " CG_DYING_TRIGGER_DIR))
+		goto cleanup;
+	close(ret);
+
+	*trigger_cgroup_id = get_cgroup_id(CG_DYING_TRIGGER_DIR);
+	if (!ASSERT_GT(*trigger_cgroup_id, 0, "get_cgroup_id"))
+		goto cleanup;
+
+	ret = create_and_get_cgroup(CG_DYING_TARGET_DIR);
+	if (!ASSERT_GE(ret, 0, "create_and_get_cgroup " CG_DYING_TARGET_DIR))
+		goto cleanup;
+	close(ret);
+
+	*target_cgroup_id = get_cgroup_id(CG_DYING_TARGET_DIR);
+	if (!ASSERT_GT(*target_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;
+	pid_t wait_ret;
+	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));
+
+	wait_ret = waitpid(low_pid, &status, 0);
+	if (!ASSERT_GT(wait_ret, 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;
+	}
+
+	wait_ret = waitpid(high_pid, &status, 0);
+	if (!ASSERT_GT(wait_ret, 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 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 = &args,
+		.ctx_size_in = sizeof(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_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 memory from the low cgroup.
+	 */
+	if (!ASSERT_GT(skel->bss->reclaim_calls, 0, "reclaim_calls"))
+		goto out;
+	if (!ASSERT_GT(skel->bss->reclaimed_bytes, 0, "reclaimed_bytes"))
+		goto out;
+
+	if (high_time >= low_time)
+		PRINT_FAIL("high cgroup not improved: high=%f low=%f",
+			   high_time, low_time);
+
+out:
+	if (skel)
+		memcg_async_reclaim__destroy(skel);
+	cleanup_cgroup_environment();
+}
+
+/*
+ * Keep refaults flowing through the trigger cgroup so reclaim rounds
+ * keep being triggered while the target cgroup is being removed. The
+ * child joins the trigger cgroup and writes the data file there, so
+ * that the file pages are charged to the trigger cgroup and actually
+ * come under its memory limit; then it re-reads the file in a loop
+ * until it is killed.
+ */
+static pid_t spawn_keepalive_reader(const char *data_file)
+{
+	pid_t pid = fork();
+
+	if (pid != 0)
+		return pid;
+
+	if (join_parent_cgroup(CG_DYING_TRIGGER_DIR))
+		_exit(CHILD_EXIT_JOIN_CGROUP);
+	if (write_file(data_file))
+		_exit(CHILD_EXIT_WRITE_FILE);
+	for (;;) {
+		if (read_file(data_file, READ_TIMES))
+			_exit(CHILD_EXIT_READ_FILE);
+	}
+}
+
+/*
+ * Remove the reclaim target while the BPF program keeps running and
+ * verify that reclaim stops on the dying/removed cgroup instead of
+ * reclaiming from it.
+ *
+ * The target stays empty; the workload lives in the trigger cgroup and
+ * only keeps refaults flowing so that reclaim rounds keep starting,
+ * both before and after the target is removed. reclaim_calls growing
+ * while the target is alive proves that rounds really run (the kfunc
+ * returns 0 on the empty target, but the call is still counted), and
+ * after the removal the skip counters must grow while reclaim_calls
+ * and reclaimed_bytes stay frozen.
+ */
+void test_memcg_async_reclaim_dying(void)
+{
+	u64 trigger_cgroup_id, target_cgroup_id;
+	u64 calls_before, bytes_before;
+	char data_file[PATH_MAX] = "";
+	struct memcg_async_reclaim *skel = NULL;
+	pid_t reader_pid = -1;
+	int err, fd, i;
+
+	err = setup_dying_cgroups(&trigger_cgroup_id, &target_cgroup_id);
+	if (!ASSERT_OK(err, "setup_dying_cgroups"))
+		return;
+
+	err = setup_bpf(trigger_cgroup_id, target_cgroup_id, &skel);
+	if (!ASSERT_OK(err, "setup_bpf"))
+		goto out;
+
+	snprintf(data_file, sizeof(data_file),
+		 "%s/memcg_async_dying_XXXXXX", workload_files_dir());
+	fd = mkstemp(data_file);
+	if (!ASSERT_GE(fd, 0, "mkstemp"))
+		goto out;
+	close(fd);
+
+	reader_pid = spawn_keepalive_reader(data_file);
+	if (!ASSERT_GT(reader_pid, 0, "fork keepalive reader"))
+		goto out;
+
+	/* Wait for reclaim rounds to reach the live target cgroup. */
+	for (i = 0; i < DYING_POLL_ITERS; i++) {
+		if (skel->bss->reclaim_calls > 0)
+			break;
+		usleep(DYING_POLL_INTERVAL_US);
+	}
+	if (!ASSERT_GT(skel->bss->reclaim_calls, 0, "reclaim_calls"))
+		goto out;
+
+	remove_cgroup(CG_DYING_TARGET_DIR);
+
+	/* Let reclaim passes that were already in flight drain. */
+	usleep(DYING_SETTLE_US);
+
+	calls_before = skel->bss->reclaim_calls;
+	bytes_before = skel->bss->reclaimed_bytes;
+
+	/* Wait for reclaim rounds to hit the removed cgroup. */
+	for (i = 0; i < DYING_POLL_ITERS; i++) {
+		if (skel->bss->reclaim_target_gone ||
+		    skel->bss->reclaim_skipped_dying)
+			break;
+		usleep(DYING_POLL_INTERVAL_US);
+	}
+
+	if (!skel->bss->reclaim_target_gone &&
+	    !skel->bss->reclaim_skipped_dying) {
+		PRINT_FAIL("no reclaim round hit the removed cgroup (gone=%llu, dying=%llu)",
+			   (unsigned long long)skel->bss->reclaim_target_gone,
+			   (unsigned long long)skel->bss->reclaim_skipped_dying);
+		goto out;
+	}
+
+	/*
+	 * reclaim_skipped_dying shows that the CSS_DYING/CSS_ONLINE check
+	 * caught the cgroup mid-teardown. Whether it is hit is timing
+	 * dependent, because the cgroup may already be fully released, so
+	 * only the combined skip count above is asserted.
+	 */
+	printf("memcg_async_reclaim_dying: skips on removed cgroup: gone=%llu, dying=%llu\n",
+	       (unsigned long long)skel->bss->reclaim_target_gone,
+	       (unsigned long long)skel->bss->reclaim_skipped_dying);
+
+	/* Nothing may have been reclaimed from the removed target. */
+	if (!ASSERT_EQ(skel->bss->reclaim_calls, calls_before, "reclaim_calls"))
+		goto out;
+	if (!ASSERT_EQ(skel->bss->reclaimed_bytes, bytes_before,
+		       "reclaimed_bytes"))
+		goto out;
+
+out:
+	if (reader_pid > 0) {
+		kill(reader_pid, SIGKILL);
+		(void)waitpid(reader_pid, NULL, 0);
+	}
+	if (data_file[0])
+		unlink(data_file);
+	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..e6839ade472bb
--- /dev/null
+++ b/tools/testing/selftests/bpf/progs/memcg_async_reclaim.c
@@ -0,0 +1,259 @@
+// 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
+/*
+ * One reclaim round targets RECLAIM_MAX_ITER batches of RECLAIM_SIZE
+ * each. Each bpf_wq callback reclaims a single batch and requeues the
+ * same work item for the next one, so no callback runs longer than one
+ * bounded reclaim pass.
+ */
+#define RECLAIM_SIZE		(32 * PAGE_SIZE)
+#define RECLAIM_MAX_ITER	32
+
+struct bpf_args {
+	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_bytes;
+/*
+ * Reclaim attempts skipped because the target cgroup is dying or has
+ * been removed. reclaim_skipped_dying counts lookups that still found
+ * the cgroup while it is being torn down, reclaim_target_gone counts
+ * lookups that found nothing. The test removes the target cgroup while
+ * reclaim is running and checks that reclaim stops via these counters.
+ */
+u64 reclaim_skipped_dying;
+u64 reclaim_target_gone;
+
+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;
+}
+
+/*
+ * A cgroup is dying once it has been offlined (CSS_ONLINE cleared) or
+ * CSS_DYING has been raised, mirroring cgroup_is_dead()/css_is_dying()
+ * in include/linux/cgroup.h. bpf_cgroup_from_id() can still hand back
+ * such a cgroup, because it only fails once the last reference has been
+ * dropped, so reclaim has to check these flags instead of relying on
+ * the lookup failing.
+ *
+ * CSS_ONLINE and CSS_DYING come from vmlinux.h: the kernel defines them
+ * in an anonymous enum, so bpf_core_enum_value() has no enum type to
+ * bind to, and redeclaring them locally would clash with the vmlinux.h
+ * enumerators. vmlinux.h is generated from the running kernel's BTF, so
+ * the values already match the target kernel.
+ */
+static bool cgroup_is_dying(struct cgroup *cgrp)
+{
+	unsigned int flags = cgrp->self.flags;
+
+	return (flags & CSS_DYING) || !(flags & CSS_ONLINE);
+}
+
+/*
+ * Reclaim one batch from the target cgroup. Returns the number of
+ * bytes reclaimed, or 0 if the cgroup is dying or gone or nothing was
+ * reclaimed.
+ */
+static u64 reclaim_cgroup(u64 cgroup_id, u64 size)
+{
+	struct cgroup_memcg cm;
+	u64 nr = 0;
+
+	if (get_cgroup_memcg_from_id(cgroup_id, &cm)) {
+		reclaim_target_gone++;
+		return 0;
+	}
+
+	if (cgroup_is_dying(cm.cgrp)) {
+		reclaim_skipped_dying++;
+		put_cgroup_memcg(&cm);
+		return 0;
+	}
+
+	reclaim_calls++;
+	nr = bpf_proactive_reclaim(cm.memcg, size);
+	reclaimed_bytes += nr;
+
+	put_cgroup_memcg(&cm);
+
+	return nr;
+}
+
+struct wq_elem {
+	struct bpf_timer timer;
+	struct bpf_wq work;
+	u64 prev_event;
+	u64 event_delta_threshold;
+	u64 check_ns;
+	/*
+	 * Bytes still to reclaim in the current round, carried across
+	 * requeues. 0 means no round is in progress; the timer path
+	 * starts a new round by resetting it, requeued work only looks
+	 * at it.
+	 */
+	u64 remaining;
+};
+
+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 reclaim_work_fn(void *map, int *key, void *value)
+{
+	struct wq_elem *elem = value;
+	u64 nr, size;
+
+	if (!elem->remaining) {
+		/*
+		 * Timer-triggered entry: start a new round only when the
+		 * high cgroup refaults enough. Requeued entries skip this
+		 * check and only look at remaining, so the refault delta
+		 * is consumed once per round.
+		 */
+		if (!should_reclaim_cgroup(wq_high_cgroup_id, &elem->prev_event,
+			elem->event_delta_threshold))
+			return 0;
+		elem->remaining = RECLAIM_MAX_ITER * RECLAIM_SIZE;
+	}
+
+	/* One bounded reclaim pass per callback */
+	size = elem->remaining < RECLAIM_SIZE ? elem->remaining : RECLAIM_SIZE;
+	nr = reclaim_cgroup(wq_low_cgroup_id, size);
+	if (!nr) {
+		elem->remaining = 0;
+		return 0;
+	}
+
+	/* try_to_free_mem_cgroup_pages() may reclaim more than requested */
+	if (nr >= elem->remaining)
+		elem->remaining = 0;
+	else
+		elem->remaining -= nr;
+
+	/* Requeue the same work item for the next batch */
+	if (elem->remaining)
+		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 *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, reclaim_work_fn, 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->remaining = 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/09/05 20:41 llm patch-evaluator
0m Model:
gemini-3.7-flash

Args:
null

Results:
null

Tokens:
							input: 14889
							output: 27
							thoughts: 112
						

Thoughts:
**Analyzing the Potential for Fuzzing: A Strategic Assessment**

Okay, let's break this down. My initial thought is to figure out if this new `bpf_proactive_reclaim` kfunc is even a good target for fuzzing, and if so, how to approach it. First, I need to get a handle on the specifics of this patch.

Right, the patch introduces the `bpf_proactive_reclaim` kfunc within `mm/bpf_memcontrol.c`. Now, I need to check the codebase to understand what *enables* this functionality. I'll need to examine `mm/bpf_memcontrol.c` itself, and quickly dive into the relevant `Makefile` and `Kconfig` files within the `mm` directory. That will tell me which configuration options trigger the inclusion of `mm/bpf_memcontrol.c`, thus also informing what configurations I'll need to enable during fuzzing to even make this code path active. Once I know what triggers the inclusion of the new code, I'll then determine which symbols are good focus points to extract.



4/2 2026/09/05 20:41 tool grepper
0m Args:
{
  "Expression": "bpf_memcontrol",
  "PathPrefix": "mm/"
}

Results:
{
  "Output": "mm/Makefile=106=ifdef CONFIG_BPF_SYSCALL\nmm/Makefile:107:obj-$(CONFIG_MEMCG) += bpf_memcontrol.o\nmm/Makefile-108-endif\n--\nmm/bpf_memcontrol.c=231=__bpf_kfunc_end_defs();\nmm/bpf_memcontrol.c-232-\nmm/bpf_memcontrol.c:233:BTF_KFUNCS_START(bpf_memcontrol_kfuncs)\nmm/bpf_memcontrol.c-234-BTF_ID_FLAGS(func, bpf_get_root_mem_cgroup, KF_ACQUIRE | KF_RET_NULL)\n--\nmm/bpf_memcontrol.c=242=BTF_ID_FLAGS(func, bpf_mem_cgroup_flush_stats, KF_SLEEPABLE)\nmm/bpf_memcontrol.c:243:BTF_KFUNCS_END(bpf_memcontrol_kfuncs)\nmm/bpf_memcontrol.c-244-\n--\nmm/bpf_memcontrol.c-251- */\nmm/bpf_memcontrol.c:252:BTF_KFUNCS_START(bpf_memcontrol_reclaim_kfuncs)\nmm/bpf_memcontrol.c-253-BTF_ID_FLAGS(func, bpf_proactive_reclaim, KF_SLEEPABLE)\nmm/bpf_memcontrol.c:254:BTF_KFUNCS_END(bpf_memcontrol_reclaim_kfuncs)\nmm/bpf_memcontrol.c-255-\nmm/bpf_memcontrol.c:256:static const struct btf_kfunc_id_set bpf_memcontrol_kfunc_set = {\nmm/bpf_memcontrol.c-257-\t.owner          = THIS_MODULE,\nmm/bpf_memcontrol.c:258:\t.set            = \u0026bpf_memcontrol_kfuncs,\nmm/bpf_memcontrol.c-259-};\nmm/bpf_memcontrol.c-260-\nmm/bpf_memcontrol.c:261:static const struct btf_kfunc_id_set bpf_memcontrol_reclaim_kfunc_set = {\nmm/bpf_memcontrol.c-262-\t.owner          = THIS_MODULE,\nmm/bpf_memcontrol.c:263:\t.set            = \u0026bpf_memcontrol_reclaim_kfuncs,\nmm/bpf_memcontrol.c-264-};\nmm/bpf_memcontrol.c-265-\nmm/bpf_memcontrol.c:266:static int __init bpf_memcontrol_init(void)\nmm/bpf_memcontrol.c-267-{\n--\nmm/bpf_memcontrol.c-270-\terr = register_btf_kfunc_id_set(BPF_PROG_TYPE_UNSPEC,\nmm/bpf_memcontrol.c:271:\t\t\t\t\t\u0026bpf_memcontrol_kfunc_set);\nmm/bpf_memcontrol.c-272-\tif (err) {\n--\nmm/bpf_memcontrol.c-277-\terr = register_btf_kfunc_id_set(BPF_PROG_TYPE_SYSCALL,\nmm/bpf_memcontrol.c:278:\t\t\t\t\t\u0026bpf_memcontrol_reclaim_kfunc_set);\nmm/bpf_memcontrol.c-279-\tif (err)\n--\nmm/bpf_memcontrol.c-283-}\nmm/bpf_memcontrol.c:284:late_initcall(bpf_memcontrol_init);\n"
}

5/2 2026/09/05 20:41 llm patch-evaluator
0m Model:
gemini-3.7-flash

Args:
null

Results:
null

Tokens:
							input: 16222
							output: 37
							thoughts: 18
						

Thoughts:
Here's my attempt at summarizing the thought process, tailored for an expert audience:

**Diving into `mm/bpf_memcontrol.c`**

Okay, time to take a look at the implementation. Let's start by digging into `mm/bpf_memcontrol.c`. I need to refresh my memory on how the BPF integration with memory control is actually implemented. I'm especially curious about how the hooks are set up and how the BPF programs interact with the memcg data structures. I'm expecting to see calls related to `memcg_css_`, `mem_cgroup_`, and probably some interaction with the per-cpu stats. I'm keen to see the specific points of injection: allocation paths, reclaim, migration, etc. This is where the magic happens, so let's see how the BPF hooks into these critical memory management subsystems. Ultimately, I'm trying to grok the overhead introduced and, more importantly, the level of control afforded by this integration. Time to get my hands dirty.



6/2 2026/09/05 20:41 tool read-file
0m Args:
{
  "File": "mm/bpf_memcontrol.c",
  "FirstLine": 150,
  "LineCount": 80
}

Results:
{
  "Contents": " 150:\t}\n 151:\t\n 152:\t/**\n 153:\t * bpf_mem_cgroup_flush_stats - Flush memory cgroup's statistics\n 154:\t * @memcg: memory cgroup\n 155:\t *\n 156:\t * Propagate memory cgroup's statistics up the cgroup tree.\n 157:\t */\n 158:\t__bpf_kfunc void bpf_mem_cgroup_flush_stats(struct mem_cgroup *memcg)\n 159:\t{\n 160:\t\tmem_cgroup_flush_stats(memcg);\n 161:\t}\n 162:\t\n 163:\t/*\n 164:\t * Reclaim must not recurse: try_to_free_mem_cgroup_pages() overwrites\n 165:\t * current-\u003ereclaim_state, so a nested call would corrupt the outer\n 166:\t * reclaim state. Reclaim windows are marked with PF_MEMALLOC;\n 167:\t * reclaim_state is also checked because it is installed slightly\n 168:\t * before PF_MEMALLOC.\n 169:\t */\n 170:\tstatic bool bpf_in_reclaim_context(void)\n 171:\t{\n 172:\t\treturn (current-\u003eflags \u0026 PF_MEMALLOC) || current-\u003ereclaim_state;\n 173:\t}\n 174:\t\n 175:\t/**\n 176:\t * bpf_proactive_reclaim - proactively reclaim memory from a memory\n 177:\t *                         cgroup\n 178:\t * @memcg: the target memory cgroup to reclaim from\n 179:\t * @size:  the amount of memory to reclaim, in bytes, clamped to\n 180:\t *         MEMCG_CHARGE_BATCH (64 pages)\n 181:\t *\n 182:\t * Trigger one proactive reclaim pass on @memcg, similar to a write to\n 183:\t * memory.reclaim, but without retrying until @size is reached.\n 184:\t *\n 185:\t * @size is clamped so that one call is a bounded unit of work, matching\n 186:\t * the memory.high workqueue fallback in high_work_func(). To reclaim\n 187:\t * more, call this kfunc repeatedly instead of passing a larger @size.\n 188:\t *\n 189:\t * This kfunc is restricted to BPF_PROG_TYPE_SYSCALL to ensure it runs\n 190:\t * in a clean process context. The SYSCALL program can schedule the\n 191:\t * actual reclaim work via bpf_wq or timers, which also execute in\n 192:\t * safe process context (workqueue, task_work).\n 193:\t *\n 194:\t * When reclaim is driven from a bpf_wq, call this kfunc once per\n 195:\t * callback and requeue the same work item for the next batch rather\n 196:\t * than looping inside the callback: a long-running callback stalls\n 197:\t * other work on the shared workqueue, and because lru_lock is held with\n 198:\t * interrupts disabled the resulting contention also delays IPI\n 199:\t * handling. Give each target memcg its own bpf_wq item, so that\n 200:\t * reclaiming one memcg neither serializes behind nor piles up on top of\n 201:\t * another. Deciding whether to submit the next batch is up to the BPF\n 202:\t * program, which can stop at any point, e.g. once the target cgroup is\n 203:\t * dying.\n 204:\t *\n 205:\t * Must not be called with a filesystem lock held: the reclaim path\n 206:\t * may deadlock on it via filesystem shrinkers.\n 207:\t *\n 208:\t * Return: The amount of memory reclaimed, in bytes, or 0 if @size is\n 209:\t * smaller than a page or the task is already in a reclaim context.\n 210:\t */\n 211:\t__bpf_kfunc unsigned long bpf_proactive_reclaim(struct mem_cgroup *memcg,\n 212:\t\t\t\t\t\t\tunsigned long size)\n 213:\t{\n 214:\t\tunsigned long nr_reclaimed;\n 215:\t\tunsigned long nr_pages;\n 216:\t\n 217:\t\tif (size \u003c PAGE_SIZE || unlikely(bpf_in_reclaim_context()))\n 218:\t\t\treturn 0;\n 219:\t\n 220:\t\tnr_pages = min(size / PAGE_SIZE, (unsigned long)MEMCG_CHARGE_BATCH);\n 221:\t\n 222:\t\tnr_reclaimed = try_to_free_mem_cgroup_pages(memcg, nr_pages,\n 223:\t\t\t\t\t\t\t    GFP_KERNEL,\n 224:\t\t\t\t\t\t\t    MEMCG_RECLAIM_MAY_SWAP |\n 225:\t\t\t\t\t\t\t    MEMCG_RECLAIM_PROACTIVE,\n 226:\t\t\t\t\t\t\t    NULL);\n 227:\t\n 228:\t\treturn nr_reclaimed * PAGE_SIZE;\n 229:\t}\n"
}

7/2 2026/09/05 20:41 llm patch-evaluator
0m Model:
gemini-3.7-flash

Args:
null

Results:
null

Tokens:
							input: 17851
							output: 36
							thoughts: 301
						

Thoughts:
**My Initial Assessment of the BPF Proactive Reclaim Patch**

Okay, let's break this down systematically. My first thought is to understand the scope of this change. It appears we're introducing a new BPF kfunc, `bpf_proactive_reclaim()`, within `mm/bpf_memcontrol.c`. That in itself is interesting. This kfunc, callable from `BPF_PROG_TYPE_SYSCALL` – and crucially, from a sleepable context – then proceeds to call `try_to_free_mem_cgroup_pages()`.

Next, I need to consider its applicability. Is this something that's even *relevant* to standard virtualization setups like QEMU? The answer, I quickly assess, is a definite yes. The BPF subsystem is entirely reachable through syscalls like `bpf(BPF_PROG_LOAD)` and `bpf(BPF_PROG_TEST_RUN)`, and memory cgroups are, of course, equally accessible. Given that both memory management and BPF kfuncs are core subsystems, this area is screaming for fuzzing. It's a prime target for finding subtle, potentially exploitable vulnerabilities.

Now, I'm thinking about the prerequisites. Looking at `mm/Makefile`, the inclusion of `bpf_memcontrol.o` hinges on `CONFIG_BPF_SYSCALL` and, implicitly, `CONFIG_MEMCG`. Furthermore, `DEBUG_INFO_BTF` is usually a necessary ingredient for BTF kfuncs, which are heavily related to BPF kfunc support, as well as `BPF_SYSCALL` and, again, `MEMCG`. I'll need to double-check the Kconfig options around BTF, kfuncs, `BPF_SYSCALL`, and `MEMCG` to make sure the build environment is set correctly, but this all seems quite plausible.



8/2 2026/09/05 20:41 tool grepper
0m Args:
{
  "Expression": "register_btf_kfunc_id_set",
  "PathPrefix": "kernel/bpf/"
}

Results:
{
  "Output": "kernel/bpf/arena.c=1131=static int __init kfunc_init(void)\nkernel/bpf/arena.c-1132-{\nkernel/bpf/arena.c:1133:\treturn register_btf_kfunc_id_set(BPF_PROG_TYPE_UNSPEC, \u0026common_kfunc_set);\nkernel/bpf/arena.c-1134-}\n--\nkernel/bpf/btf.c=8919=static int btf_populate_kfunc_set(struct btf *btf, enum btf_kfunc_hook hook,\n--\nkernel/bpf/btf.c-8965-\tset = tab-\u003esets[hook];\nkernel/bpf/btf.c:8966:\t/* Warn when register_btf_kfunc_id_set is called twice for the same hook\nkernel/bpf/btf.c-8967-\t * for module sets.\n--\nkernel/bpf/btf.c=9190=u32 *btf_kfunc_is_modify_return(const struct btf *btf, u32 kfunc_btf_id,\n--\nkernel/bpf/btf.c-9198-\nkernel/bpf/btf.c:9199:static int __register_btf_kfunc_id_set(enum btf_kfunc_hook hook,\nkernel/bpf/btf.c-9200-\t\t\t\t       const struct btf_kfunc_id_set *kset)\n--\nkernel/bpf/btf.c-9225-/* This function must be invoked only from initcalls/module init functions */\nkernel/bpf/btf.c:9226:int register_btf_kfunc_id_set(enum bpf_prog_type prog_type,\nkernel/bpf/btf.c-9227-\t\t\t      const struct btf_kfunc_id_set *kset)\n--\nkernel/bpf/btf.c-9239-\thook = bpf_prog_type_to_kfunc_hook(prog_type);\nkernel/bpf/btf.c:9240:\treturn __register_btf_kfunc_id_set(hook, kset);\nkernel/bpf/btf.c-9241-}\nkernel/bpf/btf.c:9242:EXPORT_SYMBOL_GPL(register_btf_kfunc_id_set);\nkernel/bpf/btf.c-9243-\n--\nkernel/bpf/btf.c=9245=int register_btf_fmodret_id_set(const struct btf_kfunc_id_set *kset)\nkernel/bpf/btf.c-9246-{\nkernel/bpf/btf.c:9247:\treturn __register_btf_kfunc_id_set(BTF_KFUNC_HOOK_FMODRET, kset);\nkernel/bpf/btf.c-9248-}\n--\nkernel/bpf/cpumask.c=515=static int __init cpumask_kfunc_init(void)\n--\nkernel/bpf/cpumask.c-525-\tret = bpf_mem_alloc_init(\u0026bpf_cpumask_ma, sizeof(struct bpf_cpumask), false);\nkernel/bpf/cpumask.c:526:\tret = ret ?: register_btf_kfunc_id_set(BPF_PROG_TYPE_TRACING, \u0026cpumask_kfunc_set);\nkernel/bpf/cpumask.c:527:\tret = ret ?: register_btf_kfunc_id_set(BPF_PROG_TYPE_STRUCT_OPS, \u0026cpumask_kfunc_set);\nkernel/bpf/cpumask.c:528:\tret = ret ?: register_btf_kfunc_id_set(BPF_PROG_TYPE_SYSCALL, \u0026cpumask_kfunc_set);\nkernel/bpf/cpumask.c-529-\treturn  ret ?: register_btf_id_dtor_kfuncs(cpumask_dtors,\n--\nkernel/bpf/crypto.c=379=static int __init crypto_kfunc_init(void)\n--\nkernel/bpf/crypto.c-388-\nkernel/bpf/crypto.c:389:\tret = register_btf_kfunc_id_set(BPF_PROG_TYPE_SCHED_CLS, \u0026crypt_kfunc_set);\nkernel/bpf/crypto.c:390:\tret = ret ?: register_btf_kfunc_id_set(BPF_PROG_TYPE_SCHED_ACT, \u0026crypt_kfunc_set);\nkernel/bpf/crypto.c:391:\tret = ret ?: register_btf_kfunc_id_set(BPF_PROG_TYPE_XDP, \u0026crypt_kfunc_set);\nkernel/bpf/crypto.c:392:\tret = ret ?: register_btf_kfunc_id_set(BPF_PROG_TYPE_SYSCALL,\nkernel/bpf/crypto.c-393-\t\t\t\t\t       \u0026crypt_init_kfunc_set);\n--\nkernel/bpf/helpers.c=4982=static int __init kfunc_init(void)\n--\nkernel/bpf/helpers.c-4997-\nkernel/bpf/helpers.c:4998:\tret = register_btf_kfunc_id_set(BPF_PROG_TYPE_TRACING, \u0026generic_kfunc_set);\nkernel/bpf/helpers.c:4999:\tret = ret ?: register_btf_kfunc_id_set(BPF_PROG_TYPE_SCHED_CLS, \u0026generic_kfunc_set);\nkernel/bpf/helpers.c:5000:\tret = ret ?: register_btf_kfunc_id_set(BPF_PROG_TYPE_XDP, \u0026generic_kfunc_set);\nkernel/bpf/helpers.c:5001:\tret = ret ?: register_btf_kfunc_id_set(BPF_PROG_TYPE_STRUCT_OPS, \u0026generic_kfunc_set);\nkernel/bpf/helpers.c:5002:\tret = ret ?: register_btf_kfunc_id_set(BPF_PROG_TYPE_SYSCALL, \u0026generic_kfunc_set);\nkernel/bpf/helpers.c:5003:\tret = ret ?: register_btf_kfunc_id_set(BPF_PROG_TYPE_CGROUP_SKB, \u0026generic_kfunc_set);\nkernel/bpf/helpers.c-5004-\tret = ret ?: register_btf_id_dtor_kfuncs(generic_dtors,\n--\nkernel/bpf/helpers.c-5006-\t\t\t\t\t\t  THIS_MODULE);\nkernel/bpf/helpers.c:5007:\treturn ret ?: register_btf_kfunc_id_set(BPF_PROG_TYPE_UNSPEC, \u0026common_kfunc_set);\nkernel/bpf/helpers.c-5008-}\n--\nkernel/bpf/map_iter.c=230=static int init_subsystem(void)\nkernel/bpf/map_iter.c-231-{\nkernel/bpf/map_iter.c:232:\treturn register_btf_kfunc_id_set(BPF_PROG_TYPE_UNSPEC, \u0026bpf_map_iter_kfunc_set);\nkernel/bpf/map_iter.c-233-}\n--\nkernel/bpf/rqspinlock.c=759=static __init int rqspinlock_register_kfuncs(void)\nkernel/bpf/rqspinlock.c-760-{\nkernel/bpf/rqspinlock.c:761:\treturn register_btf_kfunc_id_set(BPF_PROG_TYPE_UNSPEC, \u0026rqspinlock_kfunc_set);\nkernel/bpf/rqspinlock.c-762-}\n"
}

9/2 2026/09/05 20:41 llm patch-evaluator
0m Model:
gemini-3.7-flash

Args:
null

Results:
null

Tokens:
							input: 20625
							output: 27
							thoughts: 17
						

Thoughts:
Okay, here's my attempt at summarizing those thoughts from a first-person, expert perspective, focusing on the implied reasoning:

**Checking Kconfig for Crucial Kernel Features**

Right, let's dive in. I need to quickly ascertain the kernel configuration state. Specifically, I'm thinking about whether `MEMCG` (memory cgroups) is enabled. It's almost a given that it's important for this kind of work, but I always want to confirm. It's likely involved in resource management for whatever I'm troubleshooting or developing. Then, I need to verify `BPF_SYSCALL` as well, because I'm betting I'll be touching eBPF at some point, either for instrumentation or for filtering. Checking these in Kconfig will let me know whether I need to recompile the kernel, or if these are already enabled on the system I am working with. Making sure these are set correctly is a foundational step before digging into the details. Time to get the lay of the land.



10/2 2026/09/05 20:41 tool grepper
0m Args:
{
  "Expression": "config MEMCG",
  "PathPrefix": "init/Kconfig"
}

Results:
{
  "Output": "init/Kconfig=1067=config CGROUP_FAVOR_DYNMODS\n--\ninit/Kconfig-1076-\ninit/Kconfig:1077:config MEMCG\ninit/Kconfig-1078-\tbool \"Memory controller\"\n--\ninit/Kconfig-1085-\ninit/Kconfig:1086:config MEMCG_NMI_UNSAFE\ninit/Kconfig-1087-\tbool\n--\ninit/Kconfig-1092-\ninit/Kconfig:1093:config MEMCG_NMI_SAFETY_REQUIRES_ATOMIC\ninit/Kconfig-1094-\tbool\n--\ninit/Kconfig-1099-\ninit/Kconfig:1100:config MEMCG_V1\ninit/Kconfig-1101-\tbool \"Legacy cgroup v1 memory controller\"\n"
}

11/2 2026/09/05 20:41 llm patch-evaluator
0m Model:
gemini-3.7-flash

Args:
null

Results:
null

Tokens:
							input: 21113
							output: 29
							thoughts: 29
						

Thoughts:
Here's my take on the thought process, as if it were my own:

**Investigating BPF Syscall Configuration**

Alright, I need to pinpoint where the `BPF_SYSCALL` configuration option is defined. Knowing the kernel's Kconfig structure like the back of my hand, my first instinct is to check either `init/Kconfig` or `kernel/bpf/Kconfig`. Those are the two most logical places to start looking for a BPF-related setting. It's almost certain it'll be in one of those, or perhaps cross-referenced between them. This is the starting point for understanding how the BPF syscall feature is enabled or disabled at build time.



12/2 2026/09/05 20:41 tool grepper
0m Error:
no matches

Args:
{
  "Expression": "config BPF_SYSCALL",
  "PathPrefix": "init/"
}

Results:
{
  "error": "no matches"
}

13/2 2026/09/05 20:41 llm patch-evaluator
0m Model:
gemini-3.7-flash

Args:
null

Results:
null

Tokens:
							input: 21334
							output: 29
							thoughts: 12
						

Thoughts:
Here's my attempt at summarizing the thought process, tailored for an expert audience:

**Hunting for BPF_SYSCALL in the Config**

Okay, so I need to track down the configuration option for `BPF_SYSCALL`. I'm going to start by assuming it's likely a kernel configuration parameter, probably under one of the broader BPF or security-related sections. Since it's a syscall-related feature, I can almost guarantee its existence. I need to figure out how it's defined and what dependencies it might have. My first instinct is to use the standard tools; a `grep` through the kernel's `Kconfig` files should do the trick, maybe starting with a wildcard search like `Kconfig* BPF_SYSCALL`. Or perhaps I will use 'find' and 'grep' commands to search. Depending on the size of the codebase, I might need to refine that search if it's too broad initially. I'll need to pay close attention to the context around that option – what other features does it enable or disable? Are there any prerequisites I need to be aware of? And more importantly, what's the default value and what are the implications of enabling or disabling it? I'll probably also check relevant `Makefile`s to understand how this config option translates into build flags and ultimately, how it affects the generated kernel code. Ideally, I'll find a concise description of what the option actually *does* along the way. That's the priority right now, finding its definition.



14/2 2026/09/05 20:41 tool grepper
0m Args:
{
  "Expression": "config BPF_SYSCALL",
  "PathPrefix": "kernel/"
}

Results:
{
  "Output": "kernel/bpf/Kconfig=29=menu \"BPF subsystem\"\nkernel/bpf/Kconfig-30-\nkernel/bpf/Kconfig:31:config BPF_SYSCALL\nkernel/bpf/Kconfig-32-\tbool \"Enable bpf() system call\"\n"
}

15/2 2026/09/05 20:41 llm patch-evaluator
0m Model:
gemini-3.7-flash

Args:
null

Results:
null

Tokens:
							input: 21783
							output: 33
							thoughts: 19
						

Thoughts:
Okay, here's my take on those thoughts, framed as an expert's internal monologue:

**Investigating the DEBUG_INFO_BTF Dependency for BTF Kfuncs**

Alright, time to get my hands dirty. I'm taking a look at how `DEBUG_INFO_BTF` ties into BTF kfuncs. My immediate question is: *is* it a hard dependency? I need to understand if `DEBUG_INFO_BTF` is strictly *required* for the proper functionality and generation of BTF information related to kfuncs, or if there's some level of flexibility here. This has implications for potential optimizations and how we can approach tooling. I've got a mental checklist already: Is it necessary for the *creation* of kfunc BTF, or just for more detailed debugging information? Does it impact runtime performance or only compilation time? I need to dig into the relevant code paths and build processes. Gotta start with the BTF generation and symbol table analysis and see where that bit of information plugs in. Essentially, I need to figure out the precise scope of this dependency. It's time to fire up the tools and trace the flow.



16/2 2026/09/05 20:41 tool grepper
0m Args:
{
  "Expression": "register_btf_kfunc_id_set",
  "PathPrefix": "include/"
}

Results:
{
  "Output": "include/linux/btf.h=588=u32 *btf_kfunc_is_modify_return(const struct btf *btf, u32 kfunc_btf_id,\ninclude/linux/btf.h-589-\t\t\t\tconst struct bpf_prog *prog);\ninclude/linux/btf.h:590:int register_btf_kfunc_id_set(enum bpf_prog_type prog_type,\ninclude/linux/btf.h-591-\t\t\t      const struct btf_kfunc_id_set *s);\n--\ninclude/linux/btf.h=648=static inline u32 *btf_kfunc_id_set_contains(const struct btf *btf,\n--\ninclude/linux/btf.h-654-}\ninclude/linux/btf.h:655:static inline int register_btf_kfunc_id_set(enum bpf_prog_type prog_type,\ninclude/linux/btf.h-656-\t\t\t\t\t    const struct btf_kfunc_id_set *s)\n"
}

17/2 2026/09/05 20:41 llm patch-evaluator
0m Model:
gemini-3.7-flash

Args:
null

Results:
null

Tokens:
							input: 22361
							output: 36
							thoughts: 69
						

Thoughts:
Here's my take on those thoughts:

**Navigating the BTF Maze**

Okay, so I'm digging into something related to BTF (Berkeley eBPF Type Format) and trying to figure out a conditional compilation path. My intuition is telling me that the relevant code might hinge on `CONFIG_BPF_SYSCALL` and `CONFIG_DEBUG_INFO_BTF` being defined. So, I need to verify that and see how they interact. My mental map points me towards `include/linux/btf.h` as the likely location, and since these sorts of conditionals often cluster together, I'm going to quickly scan lines roughly between 580 and 660. Hopefully, it's laid out in a way that's easy to read and I can quickly ascertain whether my hunch is right and what the logic flow is. I'm expecting to find either an `#if` block or something similar that addresses those specific configurations. This should be a quick check, hopefully.



18/2 2026/09/05 20:41 tool read-file
0m Args:
{
  "File": "include/linux/btf.h",
  "FirstLine": 580,
  "LineCount": 80
}

Results:
{
  "Contents": " 580:\t\n 581:\tconst char *btf_name_by_offset(const struct btf *btf, u32 offset);\n 582:\tconst char *btf_str_by_offset(const struct btf *btf, u32 offset);\n 583:\tstruct btf *btf_parse_vmlinux(void);\n 584:\tstruct btf *bpf_prog_get_target_btf(const struct bpf_prog *prog);\n 585:\tu32 *btf_kfunc_flags(const struct btf *btf, u32 kfunc_btf_id, const struct bpf_prog *prog);\n 586:\tint btf_kfunc_check_flag(const struct btf *btf, u32 kfunc_btf_id, u32 flag);\n 587:\tbool btf_kfunc_is_allowed(const struct btf *btf, u32 kfunc_btf_id, const struct bpf_prog *prog);\n 588:\tu32 *btf_kfunc_is_modify_return(const struct btf *btf, u32 kfunc_btf_id,\n 589:\t\t\t\t\tconst struct bpf_prog *prog);\n 590:\tint register_btf_kfunc_id_set(enum bpf_prog_type prog_type,\n 591:\t\t\t\t      const struct btf_kfunc_id_set *s);\n 592:\tint register_btf_fmodret_id_set(const struct btf_kfunc_id_set *kset);\n 593:\ts32 btf_find_dtor_kfunc(struct btf *btf, u32 btf_id);\n 594:\tint register_btf_id_dtor_kfuncs(const struct btf_id_dtor_kfunc *dtors, u32 add_cnt,\n 595:\t\t\t\t\tstruct module *owner);\n 596:\tstruct btf_struct_meta *btf_find_struct_meta(const struct btf *btf, u32 btf_id);\n 597:\tbool btf_is_projection_of(const char *pname, const char *tname);\n 598:\tbool btf_is_prog_ctx_type(struct bpf_verifier_log *log, const struct btf *btf,\n 599:\t\t\t\t   const struct btf_type *t, enum bpf_prog_type prog_type,\n 600:\t\t\t\t   int arg);\n 601:\tint get_kern_ctx_btf_id(struct bpf_verifier_log *log, enum bpf_prog_type prog_type);\n 602:\tbool btf_types_are_same(const struct btf *btf1, u32 id1,\n 603:\t\t\t\tconst struct btf *btf2, u32 id2);\n 604:\tint btf_check_iter_arg(struct btf *btf, const struct btf_type *func, int arg_idx);\n 605:\t\n 606:\tstatic inline bool btf_type_is_struct_ptr(struct btf *btf, const struct btf_type *t)\n 607:\t{\n 608:\t\tif (!btf_type_is_ptr(t))\n 609:\t\t\treturn false;\n 610:\t\n 611:\t\tt = btf_type_skip_modifiers(btf, t-\u003etype, NULL);\n 612:\t\n 613:\t\treturn btf_type_is_struct(t);\n 614:\t}\n 615:\t#else\n 616:\tstatic inline const struct btf_type *btf_type_by_id(const struct btf *btf,\n 617:\t\t\t\t\t\t\t    u32 type_id)\n 618:\t{\n 619:\t\treturn NULL;\n 620:\t}\n 621:\t\n 622:\tstatic inline void btf_set_base_btf(struct btf *btf, const struct btf *base_btf)\n 623:\t{\n 624:\t}\n 625:\t\n 626:\tstatic inline int btf_relocate(void *log, struct btf *btf, const struct btf *base_btf,\n 627:\t\t\t\t       __u32 **map_ids)\n 628:\t{\n 629:\t\treturn -EOPNOTSUPP;\n 630:\t}\n 631:\t\n 632:\tstatic inline int btf_field_iter_init(struct btf_field_iter *it, struct btf_type *t,\n 633:\t\t\t\t\t      enum btf_field_iter_kind iter_kind)\n 634:\t{\n 635:\t\treturn -EOPNOTSUPP;\n 636:\t}\n 637:\t\n 638:\tstatic inline __u32 *btf_field_iter_next(struct btf_field_iter *it)\n 639:\t{\n 640:\t\treturn NULL;\n 641:\t}\n 642:\t\n 643:\tstatic inline const char *btf_name_by_offset(const struct btf *btf,\n 644:\t\t\t\t\t\t     u32 offset)\n 645:\t{\n 646:\t\treturn NULL;\n 647:\t}\n 648:\tstatic inline u32 *btf_kfunc_id_set_contains(const struct btf *btf,\n 649:\t\t\t\t\t\t     u32 kfunc_btf_id,\n 650:\t\t\t\t\t\t     struct bpf_prog *prog)\n 651:\t\n 652:\t{\n 653:\t\treturn NULL;\n 654:\t}\n 655:\tstatic inline int register_btf_kfunc_id_set(enum bpf_prog_type prog_type,\n 656:\t\t\t\t\t\t    const struct btf_kfunc_id_set *s)\n 657:\t{\n 658:\t\treturn 0;\n 659:\t}\n"
}

19/2 2026/09/05 20:41 llm patch-evaluator
0m Model:
gemini-3.7-flash

Args:
null

Results:
null

Tokens:
							input: 24274
							output: 36
							thoughts: 25
						

Thoughts:
Here's my take on that code snippet:

**Diving into `btf.h`'s Conditional Compilation**

Okay, so I'm getting ready to poke around in the `include/linux/btf.h` file, specifically the `#ifdef` blocks. I want to see what's being conditionally compiled and why. This is likely where the core of the BTF (BPF Type Format) definitions and related structures are declared, or maybe it sets up different configurations based on kernel features or compile-time options. I'm expecting to find a lot of essential data structures and possibly some macro definitions that really drive the BTF handling at the lowest levels. I'll need to figure out the conditions that trigger these different configurations and how they impact the overall BTF implementation. Time to dig in!



20/2 2026/09/05 20:41 tool read-file
0m Args:
{
  "File": "include/linux/btf.h",
  "FirstLine": 540,
  "LineCount": 40
}

Results:
{
  "Contents": " 540:\tstruct bpf_struct_ops;\n 541:\tint __register_bpf_struct_ops(struct bpf_struct_ops *st_ops);\n 542:\tconst struct bpf_struct_ops_desc *bpf_struct_ops_find_value(struct btf *btf, u32 value_id);\n 543:\tconst struct bpf_struct_ops_desc *bpf_struct_ops_find(struct btf *btf, u32 type_id);\n 544:\t#else\n 545:\tstatic inline const struct bpf_struct_ops_desc *bpf_struct_ops_find(struct btf *btf, u32 type_id)\n 546:\t{\n 547:\t\treturn NULL;\n 548:\t}\n 549:\t#endif\n 550:\t\n 551:\tenum btf_field_iter_kind {\n 552:\t\tBTF_FIELD_ITER_IDS,\n 553:\t\tBTF_FIELD_ITER_STRS,\n 554:\t};\n 555:\t\n 556:\tstruct btf_field_desc {\n 557:\t\t/* once-per-type offsets */\n 558:\t\tint t_off_cnt, t_offs[2];\n 559:\t\t/* member struct size, or zero, if no members */\n 560:\t\tint m_sz;\n 561:\t\t/* repeated per-member offsets */\n 562:\t\tint m_off_cnt, m_offs[1];\n 563:\t};\n 564:\t\n 565:\tstruct btf_field_iter {\n 566:\t\tstruct btf_field_desc desc;\n 567:\t\tvoid *p;\n 568:\t\tint m_idx;\n 569:\t\tint off_idx;\n 570:\t\tint vlen;\n 571:\t};\n 572:\t\n 573:\t#ifdef CONFIG_BPF_SYSCALL\n 574:\tconst struct btf_type *btf_type_by_id(const struct btf *btf, u32 type_id);\n 575:\tvoid btf_set_base_btf(struct btf *btf, const struct btf *base_btf);\n 576:\tint btf_relocate(struct btf *btf, const struct btf *base_btf, __u32 **map_ids);\n 577:\tint btf_field_iter_init(struct btf_field_iter *it, struct btf_type *t,\n 578:\t\t\t\tenum btf_field_iter_kind iter_kind);\n 579:\t__u32 *btf_field_iter_next(struct btf_field_iter *it);\n"
}

21/2 2026/09/05 20:41 llm patch-evaluator
0m Model:
gemini-3.7-flash

Args:
null

Results:
null

Tokens:
							input: 25245
							output: 33
							thoughts: 40
						

Thoughts:
Okay, here's my take on those thoughts, framed as an expert's internal monologue:

**Investigating BTF and Debug Information Requirements**

Right, so I need to clarify something about the BTF (BPF Type Format) and kfuncs (kernel functions). I'm pretty sure I have a handle on this, but let's just make absolutely sure, and double check my assumption regarding the `DEBUG_INFO_BTF` configuration option. I need to know if it's strictly *required* for BTF kfunc functionality within the kernel itself.

My gut feeling is that it isn't. BTF support has been evolving, and I have a nagging feeling that the core functionality might have decoupled from requiring this particular debugging information, especially given recent optimization efforts. However, let's not rely on intuition alone. The definitive answer is always within the code. Time to dig into the kernel source, specifically `kernel/bpf/btf.c`. That's the logical place to start – it's the heart of the BTF implementation. I'll need to carefully examine the code in `btf.c`, pay close attention to the relevant compilation flags and conditional statements, and pinpoint any dependencies or error handling related to `DEBUG_INFO_BTF`. Only then can I be certain whether the option is strictly a requirement or more of an optional enhancement for the BTF kfunc capabilities.



22/2 2026/09/05 20:41 tool grepper
0m Args:
{
  "Expression": "CONFIG_DEBUG_INFO_BTF",
  "PathPrefix": "kernel/bpf/"
}

Results:
{
  "Output": "kernel/bpf/Makefile=43=ifeq ($(CONFIG_SYSFS),y)\nkernel/bpf/Makefile:44:obj-$(CONFIG_DEBUG_INFO_BTF) += sysfs_btf.o\nkernel/bpf/Makefile-45-endif\n--\nkernel/bpf/btf.c=6401=static struct btf *btf_parse_base(struct btf_verifier_env *env, const char *name,\n--\nkernel/bpf/btf.c-6406-\nkernel/bpf/btf.c:6407:\tif (!IS_ENABLED(CONFIG_DEBUG_INFO_BTF))\nkernel/bpf/btf.c-6408-\t\treturn ERR_PTR(-ENOENT);\n--\nkernel/bpf/btf.c=6487=__u32 btf_relocate_id(const struct btf *btf, __u32 id)\n--\nkernel/bpf/btf.c-6493-\nkernel/bpf/btf.c:6494:#ifdef CONFIG_DEBUG_INFO_BTF_MODULES\nkernel/bpf/btf.c-6495-\nkernel/bpf/btf.c=6496=static struct btf *btf_parse_module(const char *module_name, const void *data,\n--\nkernel/bpf/btf.c-6591-\nkernel/bpf/btf.c:6592:#endif /* CONFIG_DEBUG_INFO_BTF_MODULES */\nkernel/bpf/btf.c-6593-\n--\nkernel/bpf/btf.c=8502=enum {\n--\nkernel/bpf/btf.c-8505-\nkernel/bpf/btf.c:8506:#ifdef CONFIG_DEBUG_INFO_BTF_MODULES\nkernel/bpf/btf.c-8507-struct btf_module {\n--\nkernel/bpf/btf.c=8644=fs_initcall(btf_module_init);\nkernel/bpf/btf.c:8645:#endif /* CONFIG_DEBUG_INFO_BTF_MODULES */\nkernel/bpf/btf.c-8646-\nkernel/bpf/btf.c=8647=struct module *btf_try_get_module(const struct btf *btf)\n--\nkernel/bpf/btf.c-8649-\tstruct module *res = NULL;\nkernel/bpf/btf.c:8650:#ifdef CONFIG_DEBUG_INFO_BTF_MODULES\nkernel/bpf/btf.c-8651-\tstruct btf_module *btf_mod, *tmp;\n--\nkernel/bpf/btf.c=8677=struct btf *btf_get_module_btf(const struct module *module)\nkernel/bpf/btf.c-8678-{\nkernel/bpf/btf.c:8679:#ifdef CONFIG_DEBUG_INFO_BTF_MODULES\nkernel/bpf/btf.c-8680-\tstruct btf_module *btf_mod, *tmp;\n--\nkernel/bpf/btf.c-8690-\nkernel/bpf/btf.c:8691:#ifdef CONFIG_DEBUG_INFO_BTF_MODULES\nkernel/bpf/btf.c-8692-\tmutex_lock(\u0026btf_module_mutex);\n--\nkernel/bpf/btf.c=8707=static int check_btf_kconfigs(const struct module *module, const char *feature)\nkernel/bpf/btf.c-8708-{\nkernel/bpf/btf.c:8709:\tif (!module \u0026\u0026 IS_ENABLED(CONFIG_DEBUG_INFO_BTF)) {\nkernel/bpf/btf.c-8710-\t\tpr_err(\"missing vmlinux BTF, cannot register %s\\n\", feature);\n--\nkernel/bpf/btf.c-8712-\t}\nkernel/bpf/btf.c:8713:\tif (module \u0026\u0026 IS_ENABLED(CONFIG_DEBUG_INFO_BTF_MODULES))\nkernel/bpf/btf.c-8714-\t\tpr_warn(\"missing module BTF, cannot register %s\\n\", feature);\n--\nkernel/bpf/btf.c=8854=static int btf_check_kfunc_name(struct btf *btf, const char *func_name, u32 kind)\nkernel/bpf/btf.c-8855-{\nkernel/bpf/btf.c:8856:#ifdef CONFIG_DEBUG_INFO_BTF_MODULES\nkernel/bpf/btf.c-8857-\tstruct btf_module *btf_mod, *tmp;\n--\nkernel/bpf/btf.c-8870-\nkernel/bpf/btf.c:8871:#ifdef CONFIG_DEBUG_INFO_BTF_MODULES\nkernel/bpf/btf.c-8872-\tguard(mutex)(\u0026btf_module_mutex);\n--\nkernel/bpf/btf.c=9519=static struct bpf_cand_cache *populate_cand_cache(struct bpf_cand_cache *cands,\n--\nkernel/bpf/btf.c-9546-\nkernel/bpf/btf.c:9547:#ifdef CONFIG_DEBUG_INFO_BTF_MODULES\nkernel/bpf/btf.c-9548-static void __purge_cand_cache(struct btf *btf, struct bpf_cand_cache **cache,\n--\nkernel/bpf/verifier.c=2836=int bpf_add_kfunc_call(struct bpf_verifier_env *env, u32 func_id, u16 offset)\n--\nkernel/bpf/verifier.c-2852-\t\tif (!btf_vmlinux) {\nkernel/bpf/verifier.c:2853:\t\t\tverbose(env, \"calling kernel function is not supported without CONFIG_DEBUG_INFO_BTF\\n\");\nkernel/bpf/verifier.c-2854-\t\t\treturn -ENOTSUPP;\n--\nkernel/bpf/verifier.c=6190=static int check_ptr_to_map_access(struct bpf_verifier_env *env,\n--\nkernel/bpf/verifier.c-6204-\tif (!btf_vmlinux) {\nkernel/bpf/verifier.c:6205:\t\tverbose(env, \"map_ptr access not supported without CONFIG_DEBUG_INFO_BTF\\n\");\nkernel/bpf/verifier.c-6206-\t\treturn -ENOTSUPP;\n--\nkernel/bpf/verifier.c=18848=static int check_pseudo_btf_id(struct bpf_verifier_env *env,\n--\nkernel/bpf/verifier.c-18864-\t\tif (!btf_vmlinux) {\nkernel/bpf/verifier.c:18865:\t\t\tverbose(env, \"kernel is missing BTF, make sure CONFIG_DEBUG_INFO_BTF=y is specified in Kconfig.\\n\");\nkernel/bpf/verifier.c-18866-\t\t\treturn -EINVAL;\n--\nkernel/bpf/verifier.c=20756=struct btf *bpf_get_btf_vmlinux(void)\n--\nkernel/bpf/verifier.c-20760-\nkernel/bpf/verifier.c:20761:\tif (!btf \u0026\u0026 IS_ENABLED(CONFIG_DEBUG_INFO_BTF)) {\nkernel/bpf/verifier.c-20762-\t\tmutex_lock(\u0026btf_vmlinux_lock);\n"
}

23/2 2026/09/05 20:41 llm patch-evaluator
0m Model:
gemini-3.7-flash

Args:
null

Results:
null

Tokens:
							input: 27568
							output: 29
							thoughts: 56
						

Thoughts:
Okay, let's break this down.

**My Initial Assessment**

Alright, I'm looking at a few kernel configurations – `CONFIG_BPF_SYSCALL`, `CONFIG_MEMCG`, and `CONFIG_DEBUG_INFO_BTF`. My immediate thought is, "What's the relationship between these?" It looks like I might be trying to understand how these feature flags interact. My intuition tells me that `CONFIG_DEBUG_INFO_BTF` is the most complex one and possibly has dependencies, let me check the `lib/Kconfig.debug` file to see what the dependencies are.



24/2 2026/09/05 20:41 tool grepper
0m Args:
{
  "Expression": "config DEBUG_INFO_BTF",
  "PathPrefix": "lib/"
}

Results:
{
  "Output": "lib/Kconfig.debug=378=config DEBUG_INFO_SPLIT\n--\nlib/Kconfig.debug-397-\nlib/Kconfig.debug:398:config DEBUG_INFO_BTF\nlib/Kconfig.debug-399-\tbool \"Generate BTF type information\"\n--\nlib/Kconfig.debug=419=config PAHOLE_HAS_LANG_EXCLUDE\n--\nlib/Kconfig.debug-427-\nlib/Kconfig.debug:428:config DEBUG_INFO_BTF_MODULES\nlib/Kconfig.debug-429-\tbool \"Generate BTF type information for kernel modules\"\n"
}

25/2 2026/09/05 20:41 llm patch-evaluator
0m Model:
gemini-3.7-flash

Args:
null

Results:
null

Tokens:
							input: 27953
							output: 34
							thoughts: 28
						

Thoughts:
Here's my summary, as though I'm thinking through this process:

**Diving into Kconfig.debug Dependencies**

Okay, so I need to understand the dependencies for something related to the debug configuration. Time to head over to `lib/Kconfig.debug`. Specifically, I'm going to zero in on lines 390 to 410. That's usually a good range to start with when I'm chasing down dependencies in a Kconfig file. Let's see what the `depends on` clauses reveal there. Hopefully, it's straightforward, and I can quickly trace what conditions need to be met for this debug option to be enabled. This is where the real work begins, I need to understand what other options are impacted and if they are relevant to this current focus.



26/2 2026/09/05 20:41 tool read-file
0m Args:
{
  "File": "lib/Kconfig.debug",
  "FirstLine": 395,
  "LineCount": 20
}

Results:
{
  "Contents": " 395:\t\t  to know about the .dwo files and include them.\n 396:\t\t  Incompatible with older versions of ccache.\n 397:\t\n 398:\tconfig DEBUG_INFO_BTF\n 399:\t\tbool \"Generate BTF type information\"\n 400:\t\tdepends on !DEBUG_INFO_SPLIT \u0026\u0026 !DEBUG_INFO_REDUCED\n 401:\t\tdepends on !GCC_PLUGIN_RANDSTRUCT || COMPILE_TEST\n 402:\t\tdepends on BPF_SYSCALL\n 403:\t\tdepends on PAHOLE_VERSION \u003e= 122\n 404:\t\t# pahole uses elfutils, which does not have support for Hexagon relocations\n 405:\t\tdepends on !HEXAGON\n 406:\t\thelp\n 407:\t\t  Generate deduplicated BTF type information from DWARF debug info.\n 408:\t\t  Turning this on requires pahole v1.22 or later, which will convert\n 409:\t\t  DWARF type info into equivalent deduplicated BTF type info.\n 410:\t\n 411:\tconfig PAHOLE_HAS_BTF_TAG\n 412:\t\tdef_bool PAHOLE_VERSION \u003e= 123\n 413:\t\tdepends on CC_IS_CLANG\n 414:\t\thelp\n"
}

27/2 2026/09/05 20:41 llm patch-evaluator
0m Model:
gemini-3.7-flash

Args:
null

Results:
null

Tokens:
							input: 28528
							output: 34
							thoughts: 37
						

Thoughts:
Okay, here's my interpretation of those thoughts, summarized from a first-person expert perspective:

**Kernel Configuration Dependencies: BPF and Beyond**

Alright, so I'm thinking about the kernel configuration options, specifically `CONFIG_BPF`. This keeps popping up, and it's a critical foundational element. I know `BPF_SYSCALL` relies on `BPF`, which immediately suggests a dependency. I need to get a clear picture of how these options interact and what other related options exist. Best to dive into the core configuration definitions. Let me check the `kernel/bpf/Kconfig` file. That's where all the details about BPF configuration should be laid out. Understanding the dependency chain here is key to a robust and efficient kernel build.



28/2 2026/09/05 20:41 tool read-file
0m Args:
{
  "File": "kernel/bpf/Kconfig",
  "FirstLine": 25,
  "LineCount": 25
}

Results:
{
  "Contents": "  25:\t# default for kernels that were compiled with BPF JIT support.\n  26:\tconfig ARCH_WANT_DEFAULT_BPF_JIT\n  27:\t\tbool\n  28:\t\n  29:\tmenu \"BPF subsystem\"\n  30:\t\n  31:\tconfig BPF_SYSCALL\n  32:\t\tbool \"Enable bpf() system call\"\n  33:\t\tselect BPF\n  34:\t\tselect IRQ_WORK\n  35:\t\tselect NEED_TASKS_RCU\n  36:\t\tselect TASKS_TRACE_RCU\n  37:\t\tselect BINARY_PRINTF\n  38:\t\tselect NET_SOCK_MSG if NET\n  39:\t\tselect NET_XGRESS if NET\n  40:\t\tselect PAGE_POOL if NET\n  41:\t\tdefault n\n  42:\t\thelp\n  43:\t\t  Enable the bpf() system call that allows to manipulate BPF programs\n  44:\t\t  and maps via file descriptors.\n  45:\t\n  46:\tconfig BPF_JIT\n  47:\t\tbool \"Enable BPF Just In Time compiler\"\n  48:\t\tdepends on BPF\n  49:\t\tdepends on HAVE_CBPF_JIT || HAVE_EBPF_JIT\n"
}

29/2 2026/09/05 20:41 llm patch-evaluator
0m Model:
gemini-3.7-flash

Args:
null

Results:
null

Tokens:
							input: 29090
							output: 134
							thoughts: 859
						

Thoughts:
**Fuzzing Analysis: BPF-Based Proactive Memory Reclaim**

Okay, let's break this down. My analysis indicates a new avenue for memory reclaim within the kernel, leveraging BPF capabilities. The key configurations involved are `BPF_SYSCALL`, `MEMCG`, and `DEBUG_INFO_BTF`. These are essential for the functionality.

The primary focus is undeniably `bpf_proactive_reclaim`. This function, now exposed as a BPF kfunc, is *the* focal point. It's the new entry point for proactive memory reclamation triggered from userspace via the `bpf()` syscall, specifically using a `BPF_PROG_TYPE_SYSCALL` program or potentially through workqueue callbacks. This is a crucial distinction and a significant addition. It calls core reclaim logic. There's also recursion protection, `bpf_in_reclaim_context`, to keep things stable.

`bpf_memcontrol_init` is also present, but because it's an initialization routine, it's not a suitable target for syscall fuzzing. `bpf_in_reclaim_context` is static and likely inlined, so it's not the primary entry point either.

Essentially, `bpf_proactive_reclaim` provides a userspace-initiated mechanism to proactively reclaim memory within a memcg, which is a big deal from a security perspective. It's a BPF kfunc calling the core reclaim logic, so it's a good place to start digging.

Therefore, `WorthFuzzing` is definitely `true` here. The configuration requirements are `BPF_SYSCALL`, `MEMCG`, and `DEBUG_INFO_BTF`. And the prime `FocusSymbol` is unequivocally `bpf_proactive_reclaim`. It's non-static, a kfunc, and the core of this new functionality, so it's exactly what we want. The tests in `tools/testing/selftests/...` are for userspace which are also not a focus point for the kernel code.

I'm confident in these findings. Let's get the results set.



30/2 2026/09/05 20:42 tool set-results
0m Args:
{
  "EnableConfigs": [
    "BPF_SYSCALL",
    "MEMCG",
    "DEBUG_INFO_BTF"
  ],
  "FocusSymbols": [
    "bpf_proactive_reclaim"
  ],
  "Reasoning": "The patch introduces a new BPF kfunc, bpf_proactive_reclaim(), allowing sleepable BPF_PROG_TYPE_SYSCALL programs to trigger proactive memory cgroup reclaim via try_to_free_mem_cgroup_pages(). This modifies reachable core memory management and BPF subsystem code and warrants fuzzing.",
  "WorthFuzzing": true
}

Results:
{
  "EnableConfigs": [
    "BPF_SYSCALL",
    "MEMCG",
    "DEBUG_INFO_BTF"
  ],
  "FocusSymbols": [
    "bpf_proactive_reclaim"
  ],
  "Reasoning": "The patch introduces a new BPF kfunc, bpf_proactive_reclaim(), allowing sleepable BPF_PROG_TYPE_SYSCALL programs to trigger proactive memory cgroup reclaim via try_to_free_mem_cgroup_pages(). This modifies reachable core memory management and BPF subsystem code and warrants fuzzing.",
  "WorthFuzzing": true
}

31/1 2026/09/05 20:42 action If
0m Args:
{
  "WorthFuzzing": true
}

Results:
null

32/2 2026/09/05 20:42 agent kmsan-evaluator
0m Model:
core

Args:
null

Results:
{
  "KMSANReasoning": "The patch introduces the `bpf_proactive_reclaim` BPF kfunc in `mm/bpf_memcontrol.c` and associated BPF selftests. The function accepts a pointer to `struct mem_cgroup` and a `size` parameter, checks the execution context, calls `try_to_free_mem_cgroup_pages()`, and returns a scalar `unsigned long` indicating the number of reclaimed bytes.\n\nThere are no uninitialized variables, no uninitialized struct fields, no uninitialized buffer reads, and no transfers of kernel memory structures to user space. Any potential issues (e.g., deadlock, recursion in reclaim context, refcounting, or invalid pointer dereference) fall strictly under KASAN, LOCKDEP, and standard kernel debugging facilities, not KMSAN.",
  "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:
Target architecture: amd64

For your convenience, here is the diff of the changes:
commit e71a3219429423e5fd3664f52cdb8943c1849bbe
Author: syz-cluster <triage@syzkaller.com>
Date:   Sat Sep 5 20:41:22 2026 +0000

    syz-cluster: applied patch under review

diff --git a/mm/bpf_memcontrol.c b/mm/bpf_memcontrol.c
index 716df49d76477..92f35ba66309e 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,74 @@ __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() overwrites
+ * current->reclaim_state, so a nested call would corrupt the outer
+ * reclaim state. Reclaim windows are marked with PF_MEMALLOC;
+ * reclaim_state is also checked because it is installed slightly
+ * before PF_MEMALLOC.
+ */
+static bool bpf_in_reclaim_context(void)
+{
+	return (current->flags & PF_MEMALLOC) || current->reclaim_state;
+}
+
+/**
+ * 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, clamped to
+ *         MEMCG_CHARGE_BATCH (64 pages)
+ *
+ * Trigger one proactive reclaim pass on @memcg, similar to a write to
+ * memory.reclaim, but without retrying until @size is reached.
+ *
+ * @size is clamped so that one call is a bounded unit of work, matching
+ * the memory.high workqueue fallback in high_work_func(). To reclaim
+ * more, call this kfunc repeatedly instead of passing a larger @size.
+ *
+ * This kfunc is restricted to BPF_PROG_TYPE_SYSCALL to ensure it runs
+ * in a clean process context. The SYSCALL program can schedule the
+ * actual reclaim work via bpf_wq or timers, which also execute in
+ * safe process context (workqueue, task_work).
+ *
+ * When reclaim is driven from a bpf_wq, call this kfunc once per
+ * callback and requeue the same work item for the next batch rather
+ * than looping inside the callback: a long-running callback stalls
+ * other work on the shared workqueue, and because lru_lock is held with
+ * interrupts disabled the resulting contention also delays IPI
+ * handling. Give each target memcg its own bpf_wq item, so that
+ * reclaiming one memcg neither serializes behind nor piles up on top of
+ * another. Deciding whether to submit the next batch is up to the BPF
+ * program, which can stop at any point, e.g. once the target cgroup is
+ * dying.
+ *
+ * Must not be called with a filesystem lock held: the reclaim path
+ * may deadlock on it via filesystem shrinkers.
+ *
+ * Return: The amount of memory reclaimed, in bytes, or 0 if @size is
+ * smaller than a page or the task is already in a reclaim context.
+ */
+__bpf_kfunc unsigned long bpf_proactive_reclaim(struct mem_cgroup *memcg,
+						unsigned long size)
+{
+	unsigned long nr_reclaimed;
+	unsigned long nr_pages;
+
+	if (size < PAGE_SIZE || unlikely(bpf_in_reclaim_context()))
+		return 0;
+
+	nr_pages = min(size / PAGE_SIZE, (unsigned long)MEMCG_CHARGE_BATCH);
+
+	nr_reclaimed = try_to_free_mem_cgroup_pages(memcg, nr_pages,
+						    GFP_KERNEL,
+						    MEMCG_RECLAIM_MAY_SWAP |
+						    MEMCG_RECLAIM_PROACTIVE,
+						    NULL);
+
+	return nr_reclaimed * PAGE_SIZE;
+}
+
 __bpf_kfunc_end_defs();
 
 BTF_KFUNCS_START(bpf_memcontrol_kfuncs)
@@ -171,22 +240,44 @@ BTF_ID_FLAGS(func, bpf_mem_cgroup_memory_events)
 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_KFUNCS_END(bpf_memcontrol_kfuncs)
 
+/*
+ * Proactive reclaim needs a clean process context, so it is restricted
+ * to BPF_PROG_TYPE_SYSCALL. The bpf_wq and task_work callbacks that a
+ * SYSCALL program schedules run as the same program type, so they can
+ * still invoke it; generic sleepable programs (e.g. fentry on reclaim
+ * paths, inode_rmdir) cannot.
+ */
+BTF_KFUNCS_START(bpf_memcontrol_reclaim_kfuncs)
+BTF_ID_FLAGS(func, bpf_proactive_reclaim, KF_SLEEPABLE)
+BTF_KFUNCS_END(bpf_memcontrol_reclaim_kfuncs)
+
 static const struct btf_kfunc_id_set bpf_memcontrol_kfunc_set = {
 	.owner          = THIS_MODULE,
 	.set            = &bpf_memcontrol_kfuncs,
 };
 
+static const struct btf_kfunc_id_set bpf_memcontrol_reclaim_kfunc_set = {
+	.owner          = THIS_MODULE,
+	.set            = &bpf_memcontrol_reclaim_kfuncs,
+};
+
 static int __init bpf_memcontrol_init(void)
 {
 	int err;
 
 	err = register_btf_kfunc_id_set(BPF_PROG_TYPE_UNSPEC,
 					&bpf_memcontrol_kfunc_set);
-	if (err)
+	if (err) {
 		pr_warn("error while registering bpf memcontrol kfuncs: %d", err);
+		return err;
+	}
+
+	err = register_btf_kfunc_id_set(BPF_PROG_TYPE_SYSCALL,
+					&bpf_memcontrol_reclaim_kfunc_set);
+	if (err)
+		pr_warn("error registering bpf reclaim kfuncs: %d", err);
 
 	return err;
 }
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..65f5006844636
--- /dev/null
+++ b/tools/testing/selftests/bpf/prog_tests/memcg_async_reclaim.c
@@ -0,0 +1,686 @@
+// 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 {
+	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 CG_DYING_DIR "/memcg_async_reclaim_dying"
+#define CG_DYING_TRIGGER_DIR CG_DYING_DIR "/trigger"
+#define CG_DYING_TARGET_DIR CG_DYING_DIR "/target"
+
+#define CHECK_PERIOD_NS (2 * 1000 * 1000ull)
+#define EVENT_DELTA_THRESHOLD 1
+
+/*
+ * Timing for the dying test: after the target cgroup is removed, give
+ * in-flight reclaim passes time to drain, then wait for a reclaim round
+ * to hit the removed target. The keepalive reader keeps the trigger
+ * cgroup refaulting, and the timer fires every CHECK_PERIOD_NS, so
+ * such a round must show up within a few timer periods.
+ */
+#define DYING_SETTLE_US (200 * 1000)
+#define DYING_POLL_ITERS 500
+#define DYING_POLL_INTERVAL_US (10 * 1000)
+
+/*
+ * 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;
+}
+
+/*
+ * The dying test needs an empty reclaim target plus a cgroup that keeps
+ * refaulting while the target is removed, so reclaim rounds keep
+ * starting and run into the removed target. The two have to be separate
+ * cgroups: the target must hold no processes to be removed, and v2's
+ * no-internal-process constraint keeps the refaulting workload out of
+ * any parent that has domain children.
+ */
+static int setup_dying_cgroups(u64 *trigger_cgroup_id, u64 *target_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_DYING_DIR);
+	if (!ASSERT_GE(ret, 0, "create_and_get_cgroup " CG_DYING_DIR))
+		goto cleanup;
+	close(ret);
+
+	ret = enable_controllers(CG_DYING_DIR, "memory");
+	if (!ASSERT_OK(ret, "enable_controllers"))
+		goto cleanup;
+
+	snprintf(limit_buf, sizeof(limit_buf), "%lu", CG_LIMIT);
+	ret = write_cgroup_file(CG_DYING_DIR, "memory.max", limit_buf);
+	if (!ASSERT_OK(ret, "write_cgroup_file memory.max"))
+		goto cleanup;
+
+	/* See the matching write in setup_high_low_cgroups(). */
+	if (!access("/proc/swaps", F_OK)) {
+		ret = write_cgroup_file(CG_DYING_DIR, "memory.swap.max", "0");
+		if (!ASSERT_OK(ret, "write_cgroup_file memory.swap.max"))
+			goto cleanup;
+	}
+
+	ret = create_and_get_cgroup(CG_DYING_TRIGGER_DIR);
+	if (!ASSERT_GE(ret, 0, "create_and_get_cgroup " CG_DYING_TRIGGER_DIR))
+		goto cleanup;
+	close(ret);
+
+	*trigger_cgroup_id = get_cgroup_id(CG_DYING_TRIGGER_DIR);
+	if (!ASSERT_GT(*trigger_cgroup_id, 0, "get_cgroup_id"))
+		goto cleanup;
+
+	ret = create_and_get_cgroup(CG_DYING_TARGET_DIR);
+	if (!ASSERT_GE(ret, 0, "create_and_get_cgroup " CG_DYING_TARGET_DIR))
+		goto cleanup;
+	close(ret);
+
+	*target_cgroup_id = get_cgroup_id(CG_DYING_TARGET_DIR);
+	if (!ASSERT_GT(*target_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;
+	pid_t wait_ret;
+	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));
+
+	wait_ret = waitpid(low_pid, &status, 0);
+	if (!ASSERT_GT(wait_ret, 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;
+	}
+
+	wait_ret = waitpid(high_pid, &status, 0);
+	if (!ASSERT_GT(wait_ret, 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 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 = &args,
+		.ctx_size_in = sizeof(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_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 memory from the low cgroup.
+	 */
+	if (!ASSERT_GT(skel->bss->reclaim_calls, 0, "reclaim_calls"))
+		goto out;
+	if (!ASSERT_GT(skel->bss->reclaimed_bytes, 0, "reclaimed_bytes"))
+		goto out;
+
+	if (high_time >= low_time)
+		PRINT_FAIL("high cgroup not improved: high=%f low=%f",
+			   high_time, low_time);
+
+out:
+	if (skel)
+		memcg_async_reclaim__destroy(skel);
+	cleanup_cgroup_environment();
+}
+
+/*
+ * Keep refaults flowing through the trigger cgroup so reclaim rounds
+ * keep being triggered while the target cgroup is being removed. The
+ * child joins the trigger cgroup and writes the data file there, so
+ * that the file pages are charged to the trigger cgroup and actually
+ * come under its memory limit; then it re-reads the file in a loop
+ * until it is killed.
+ */
+static pid_t spawn_keepalive_reader(const char *data_file)
+{
+	pid_t pid = fork();
+
+	if (pid != 0)
+		return pid;
+
+	if (join_parent_cgroup(CG_DYING_TRIGGER_DIR))
+		_exit(CHILD_EXIT_JOIN_CGROUP);
+	if (write_file(data_file))
+		_exit(CHILD_EXIT_WRITE_FILE);
+	for (;;) {
+		if (read_file(data_file, READ_TIMES))
+			_exit(CHILD_EXIT_READ_FILE);
+	}
+}
+
+/*
+ * Remove the reclaim target while the BPF program keeps running and
+ * verify that reclaim stops on the dying/removed cgroup instead of
+ * reclaiming from it.
+ *
+ * The target stays empty; the workload lives in the trigger cgroup and
+ * only keeps refaults flowing so that reclaim rounds keep starting,
+ * both before and after the target is removed. reclaim_calls growing
+ * while the target is alive proves that rounds really run (the kfunc
+ * returns 0 on the empty target, but the call is still counted), and
+ * after the removal the skip counters must grow while reclaim_calls
+ * and reclaimed_bytes stay frozen.
+ */
+void test_memcg_async_reclaim_dying(void)
+{
+	u64 trigger_cgroup_id, target_cgroup_id;
+	u64 calls_before, bytes_before;
+	char data_file[PATH_MAX] = "";
+	struct memcg_async_reclaim *skel = NULL;
+	pid_t reader_pid = -1;
+	int err, fd, i;
+
+	err = setup_dying_cgroups(&trigger_cgroup_id, &target_cgroup_id);
+	if (!ASSERT_OK(err, "setup_dying_cgroups"))
+		return;
+
+	err = setup_bpf(trigger_cgroup_id, target_cgroup_id, &skel);
+	if (!ASSERT_OK(err, "setup_bpf"))
+		goto out;
+
+	snprintf(data_file, sizeof(data_file),
+		 "%s/memcg_async_dying_XXXXXX", workload_files_dir());
+	fd = mkstemp(data_file);
+	if (!ASSERT_GE(fd, 0, "mkstemp"))
+		goto out;
+	close(fd);
+
+	reader_pid = spawn_keepalive_reader(data_file);
+	if (!ASSERT_GT(reader_pid, 0, "fork keepalive reader"))
+		goto out;
+
+	/* Wait for reclaim rounds to reach the live target cgroup. */
+	for (i = 0; i < DYING_POLL_ITERS; i++) {
+		if (skel->bss->reclaim_calls > 0)
+			break;
+		usleep(DYING_POLL_INTERVAL_US);
+	}
+	if (!ASSERT_GT(skel->bss->reclaim_calls, 0, "reclaim_calls"))
+		goto out;
+
+	remove_cgroup(CG_DYING_TARGET_DIR);
+
+	/* Let reclaim passes that were already in flight drain. */
+	usleep(DYING_SETTLE_US);
+
+	calls_before = skel->bss->reclaim_calls;
+	bytes_before = skel->bss->reclaimed_bytes;
+
+	/* Wait for reclaim rounds to hit the removed cgroup. */
+	for (i = 0; i < DYING_POLL_ITERS; i++) {
+		if (skel->bss->reclaim_target_gone ||
+		    skel->bss->reclaim_skipped_dying)
+			break;
+		usleep(DYING_POLL_INTERVAL_US);
+	}
+
+	if (!skel->bss->reclaim_target_gone &&
+	    !skel->bss->reclaim_skipped_dying) {
+		PRINT_FAIL("no reclaim round hit the removed cgroup (gone=%llu, dying=%llu)",
+			   (unsigned long long)skel->bss->reclaim_target_gone,
+			   (unsigned long long)skel->bss->reclaim_skipped_dying);
+		goto out;
+	}
+
+	/*
+	 * reclaim_skipped_dying shows that the CSS_DYING/CSS_ONLINE check
+	 * caught the cgroup mid-teardown. Whether it is hit is timing
+	 * dependent, because the cgroup may already be fully released, so
+	 * only the combined skip count above is asserted.
+	 */
+	printf("memcg_async_reclaim_dying: skips on removed cgroup: gone=%llu, dying=%llu\n",
+	       (unsigned long long)skel->bss->reclaim_target_gone,
+	       (unsigned long long)skel->bss->reclaim_skipped_dying);
+
+	/* Nothing may have been reclaimed from the removed target. */
+	if (!ASSERT_EQ(skel->bss->reclaim_calls, calls_before, "reclaim_calls"))
+		goto out;
+	if (!ASSERT_EQ(skel->bss->reclaimed_bytes, bytes_before,
+		       "reclaimed_bytes"))
+		goto out;
+
+out:
+	if (reader_pid > 0) {
+		kill(reader_pid, SIGKILL);
+		(void)waitpid(reader_pid, NULL, 0);
+	}
+	if (data_file[0])
+		unlink(data_file);
+	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..e6839ade472bb
--- /dev/null
+++ b/tools/testing/selftests/bpf/progs/memcg_async_reclaim.c
@@ -0,0 +1,259 @@
+// 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
+/*
+ * One reclaim round targets RECLAIM_MAX_ITER batches of RECLAIM_SIZE
+ * each. Each bpf_wq callback reclaims a single batch and requeues the
+ * same work item for the next one, so no callback runs longer than one
+ * bounded reclaim pass.
+ */
+#define RECLAIM_SIZE		(32 * PAGE_SIZE)
+#define RECLAIM_MAX_ITER	32
+
+struct bpf_args {
+	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_bytes;
+/*
+ * Reclaim attempts skipped because the target cgroup is dying or has
+ * been removed. reclaim_skipped_dying counts lookups that still found
+ * the cgroup while it is being torn down, reclaim_target_gone counts
+ * lookups that found nothing. The test removes the target cgroup while
+ * reclaim is running and checks that reclaim stops via these counters.
+ */
+u64 reclaim_skipped_dying;
+u64 reclaim_target_gone;
+
+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;
+}
+
+/*
+ * A cgroup is dying once it has been offlined (CSS_ONLINE cleared) or
+ * CSS_DYING has been raised, mirroring cgroup_is_dead()/css_is_dying()
+ * in include/linux/cgroup.h. bpf_cgroup_from_id() can still hand back
+ * such a cgroup, because it only fails once the last reference has been
+ * dropped, so reclaim has to check these flags instead of relying on
+ * the lookup failing.
+ *
+ * CSS_ONLINE and CSS_DYING come from vmlinux.h: the kernel defines them
+ * in an anonymous enum, so bpf_core_enum_value() has no enum type to
+ * bind to, and redeclaring them locally would clash with the vmlinux.h
+ * enumerators. vmlinux.h is generated from the running kernel's BTF, so
+ * the values already match the target kernel.
+ */
+static bool cgroup_is_dying(struct cgroup *cgrp)
+{
+	unsigned int flags = cgrp->self.flags;
+
+	return (flags & CSS_DYING) || !(flags & CSS_ONLINE);
+}
+
+/*
+ * Reclaim one batch from the target cgroup. Returns the number of
+ * bytes reclaimed, or 0 if the cgroup is dying or gone or nothing was
+ * reclaimed.
+ */
+static u64 reclaim_cgroup(u64 cgroup_id, u64 size)
+{
+	struct cgroup_memcg cm;
+	u64 nr = 0;
+
+	if (get_cgroup_memcg_from_id(cgroup_id, &cm)) {
+		reclaim_target_gone++;
+		return 0;
+	}
+
+	if (cgroup_is_dying(cm.cgrp)) {
+		reclaim_skipped_dying++;
+		put_cgroup_memcg(&cm);
+		return 0;
+	}
+
+	reclaim_calls++;
+	nr = bpf_proactive_reclaim(cm.memcg, size);
+	reclaimed_bytes += nr;
+
+	put_cgroup_memcg(&cm);
+
+	return nr;
+}
+
+struct wq_elem {
+	struct bpf_timer timer;
+	struct bpf_wq work;
+	u64 prev_event;
+	u64 event_delta_threshold;
+	u64 check_ns;
+	/*
+	 * Bytes still to reclaim in the current round, carried across
+	 * requeues. 0 means no round is in progress; the timer path
+	 * starts a new round by resetting it, requeued work only looks
+	 * at it.
+	 */
+	u64 remaining;
+};
+
+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 reclaim_work_fn(void *map, int *key, void *value)
+{
+	struct wq_elem *elem = value;
+	u64 nr, size;
+
+	if (!elem->remaining) {
+		/*
+		 * Timer-triggered entry: start a new round only when the
+		 * high cgroup refaults enough. Requeued entries skip this
+		 * check and only look at remaining, so the refault delta
+		 * is consumed once per round.
+		 */
+		if (!should_reclaim_cgroup(wq_high_cgroup_id, &elem->prev_event,
+			elem->event_delta_threshold))
+			return 0;
+		elem->remaining = RECLAIM_MAX_ITER * RECLAIM_SIZE;
+	}
+
+	/* One bounded reclaim pass per callback */
+	size = elem->remaining < RECLAIM_SIZE ? elem->remaining : RECLAIM_SIZE;
+	nr = reclaim_cgroup(wq_low_cgroup_id, size);
+	if (!nr) {
+		elem->remaining = 0;
+		return 0;
+	}
+
+	/* try_to_free_mem_cgroup_pages() may reclaim more than requested */
+	if (nr >= elem->remaining)
+		elem->remaining = 0;
+	else
+		elem->remaining -= nr;
+
+	/* Requeue the same work item for the next batch */
+	if (elem->remaining)
+		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 *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, reclaim_work_fn, 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->remaining = 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";

33/3 2026/09/05 20:42 llm kmsan-evaluator
0m Model:
gemini-3.7-flash

Args:
null

Results:
null

Tokens:
							input: 14101
							output: 195
							thoughts: 1083
						

Thoughts:
**KMSAN Analysis of BPF Proactive Reclaim Kfunc**

Alright, let's break down this patch. I see a new kfunc, `bpf_proactive_reclaim`, added in `mm/bpf_memcontrol.c`, along with its selftests. The core of it involves this function calling `try_to_free_mem_cgroup_pages`.

I need to make sure this doesn't introduce any uninitialized memory issues. So, let's step through this methodically. First, there aren't any kernel structures getting sent to user space directly. It returns a simple `unsigned long` scalar representing the number of reclaimed bytes, which is good. Second, the code flow is straightforward; there's no conditional logic based on uninitialized variables. The key variables, `nr_pages` and `nr_reclaimed`, are directly initialized with the results of calculations and function calls, respectively. Third, no new complex data structures are allocated. Finally, there's no risk of any kind of buffer overruns that might expose uninitialized memory.

Looking deeper at the code, everything hinges on `try_to_free_mem_cgroup_pages`, `bpf_in_reclaim_context`, and the initialization within `mm/bpf_memcontrol.c`. This kfunc leverages an existing, well-vetted mm internal function. The function is quite simple, and it's passing in the `memcg` pointer and the size value. The return value is just an `unsigned long`.

The parameters passed to `try_to_free_mem_cgroup_pages` are `struct mem_cgroup *`, `unsigned long nr_pages`, `gfp_t gfp_mask`, `unsigned int reclaim_options`, and `nodemask_t *nodemask` (where it's passed as NULL). There is no reading of uninitialized stack or heap memory. Nothing here leaks uninitialized memory.

The potential issues I can see would be classic memory management errors like locking issues, recursion, sleep-in-atomic context problems, concurrency issues, refcount errors, or deadlocks. However, those are the types of bugs that KASAN, LOCKDEP, PROVE_LOCKING, and the other standard kernel detectors are designed to catch. KMSAN isn't needed here.

So, according to my analysis and these guidelines:

*   **NeedsKMSAN: false**
*   **KMSANReasoning:** The patch introduces the `bpf_proactive_reclaim` kfunc, which is added to `mm/bpf_memcontrol.c` along with corresponding selftests. This kfunc takes a memcg pointer and a size scalar, does some basic validation, then invokes `try_to_free_mem_cgroup_pages()`, returning an `unsigned long` representing reclaimed bytes. There are no heap or stack allocations with uninitialized fields, no copies to user space of uninitialized data, and no info leaks. Any potential memory management issues, locking, sleepable contexts, or pointer validity problems would be identified by KASAN and LOCKDEP.

Let's proceed to `set-results`.



34/3 2026/09/05 20:42 tool set-results
0m Args:
{
  "KMSANReasoning": "The patch introduces the `bpf_proactive_reclaim` BPF kfunc in `mm/bpf_memcontrol.c` and associated BPF selftests. The function accepts a pointer to `struct mem_cgroup` and a `size` parameter, checks the execution context, calls `try_to_free_mem_cgroup_pages()`, and returns a scalar `unsigned long` indicating the number of reclaimed bytes.\n\nThere are no uninitialized variables, no uninitialized struct fields, no uninitialized buffer reads, and no transfers of kernel memory structures to user space. Any potential issues (e.g., deadlock, recursion in reclaim context, refcounting, or invalid pointer dereference) fall strictly under KASAN, LOCKDEP, and standard kernel debugging facilities, not KMSAN.",
  "NeedsKMSAN": false
}

Results:
{
  "KMSANReasoning": "The patch introduces the `bpf_proactive_reclaim` BPF kfunc in `mm/bpf_memcontrol.c` and associated BPF selftests. The function accepts a pointer to `struct mem_cgroup` and a `size` parameter, checks the execution context, calls `try_to_free_mem_cgroup_pages()`, and returns a scalar `unsigned long` indicating the number of reclaimed bytes.\n\nThere are no uninitialized variables, no uninitialized struct fields, no uninitialized buffer reads, and no transfers of kernel memory structures to user space. Any potential issues (e.g., deadlock, recursion in reclaim context, refcounting, or invalid pointer dereference) fall strictly under KASAN, LOCKDEP, and standard kernel debugging facilities, not KMSAN.",
  "NeedsKMSAN": false
}

LLM Calls Summary:
Total Calls Total Tokens Avg Tokens Total Duration (Seconds) Avg Duration (Seconds)
Tool Calls Summary:
Total Calls Total Duration (Seconds) Avg Duration (Seconds)