AI Trajectory


Trajectory: (Expand/Collapse All)
Seq Timestamp Type Name Duration
0/0 2026/06/29 11:49 flow patch-triage
0m Args:
null

Results:
{
  "EnableConfigs": [
    "ZSWAP",
    "MEMCG"
  ],
  "FocusSymbols": [
    "user_proactive_reclaim",
    "zswap_proactive_writeback",
    "zswap_shrink_one_memcg",
    "shrink_memcg"
  ],
  "Reasoning": "The patch introduces a new feature for proactive zswap writeback via the 'memory.reclaim' cgroup interface using the 'source=zswap' key. This is a functional change reachable from userspace that modifies core memory management and zswap logic, making it highly suitable for fuzzing.",
  "WorthFuzzing": true
}

1/1 2026/06/29 11:49 action read-patch-diff
0m Args:
null

Results:
{
  "PatchDiff": "commit e6ca8ee1c3b4031271fc614c2c02c1367735ba5f\nAuthor: syz-cluster \u003ctriage@syzkaller.com\u003e\nDate:   Mon Jun 29 11:49:11 2026 +0000\n\n    syz-cluster: applied patch under review\n\ndiff --git a/Documentation/admin-guide/cgroup-v2.rst b/Documentation/admin-guide/cgroup-v2.rst\nindex 993446ab66d0fb..e1f6a4729a65e6 100644\n--- a/Documentation/admin-guide/cgroup-v2.rst\n+++ b/Documentation/admin-guide/cgroup-v2.rst\n@@ -1425,9 +1425,10 @@ PAGE_SIZE multiple when read back.\n \n The following nested keys are defined.\n \n-\t  ==========            ================================\n+\t  ====================  ==================================================\n \t  swappiness            Swappiness value to reclaim with\n-\t  ==========            ================================\n+\t  source=zswap          Only perform proactive zswap writeback\n+\t  ====================  ==================================================\n \n \tSpecifying a swappiness value instructs the kernel to perform\n \tthe reclaim with that swappiness value. Note that this has the\n@@ -1437,6 +1438,19 @@ The following nested keys are defined.\n \tThe valid range for swappiness is [0-200, max], setting\n \tswappiness=max exclusively reclaims anonymous memory.\n \n+\tThe source=zswap key skips ordinary memory reclaim and\n+\twrites back pages from zswap to the backing swap device until\n+\tthe requested amount has been written or no further candidates\n+\tare found. This is useful to proactively offload cold compressed\n+\tdata from the zswap pool to the swap device. It is only available\n+\tif zswap writeback is enabled. source=zswap cannot be\n+\tcombined with swappiness; specifying both returns -EINVAL.\n+\n+\tExample::\n+\n+\t  # Writeback up to 10MB of compressed data from zswap to the backing swap\n+\t  echo \"10M source=zswap\" \u003e memory.reclaim\n+\n   memory.peak\n \tA read-write single value file which exists on non-root cgroups.\n \n@@ -1734,6 +1748,10 @@ The following nested keys are defined.\n \t  zswpwb\n \t\tNumber of pages written from zswap to swap.\n \n+\t  zswpwb_proactive_b\n+\t\tBytes of compressed data proactively written back from\n+\t\tzswap to swap via the memory.reclaim source=zswap key.\n+\n \t  zswap_incomp\n \t\tNumber of incompressible pages currently stored in zswap\n \t\twithout compression. These pages could not be compressed to\ndiff --git a/Documentation/admin-guide/mm/zswap.rst b/Documentation/admin-guide/mm/zswap.rst\nindex 2464425c783de2..b49b8c13038905 100644\n--- a/Documentation/admin-guide/mm/zswap.rst\n+++ b/Documentation/admin-guide/mm/zswap.rst\n@@ -131,7 +131,16 @@ User can enable it as follows::\n   echo Y \u003e /sys/module/zswap/parameters/shrinker_enabled\n \n This can be enabled at the boot time if ``CONFIG_ZSWAP_SHRINKER_DEFAULT_ON`` is\n-selected.\n+selected. Once enabled, the shrinker automatically writes back zswap pages to\n+backing swap during memory reclaim.\n+\n+If users want to explicitly trigger proactive zswap writeback for a specific\n+memory cgroup without invoking standard page reclaim, it can be done as follows::\n+\n+\techo \"10M source=zswap\" \u003e /sys/fs/cgroup/\u003ccgroup-name\u003e/memory.reclaim\n+\n+Both of the methods mentioned above are subject to the ``memory.zswap.writeback``\n+control. This means that ``memory.zswap.writeback`` can prevent all zswap writeback.\n \n A debugfs interface is provided for various statistic about pool size, number\n of pages stored, same-value filled pages and various counters for the reasons\ndiff --git a/include/linux/memcontrol.h b/include/linux/memcontrol.h\nindex e1f46a0016fcfd..56580b264dc426 100644\n--- a/include/linux/memcontrol.h\n+++ b/include/linux/memcontrol.h\n@@ -40,6 +40,7 @@ enum memcg_stat_item {\n \tMEMCG_ZSWAP_B,\n \tMEMCG_ZSWAPPED,\n \tMEMCG_ZSWAP_INCOMP,\n+\tMEMCG_ZSWPWB_PROACTIVE_B,\n \tMEMCG_NR_STAT,\n };\n \ndiff --git a/include/linux/zswap.h b/include/linux/zswap.h\nindex 30c193a1207e16..e5f217759894ed 100644\n--- a/include/linux/zswap.h\n+++ b/include/linux/zswap.h\n@@ -35,6 +35,7 @@ void zswap_lruvec_state_init(struct lruvec *lruvec);\n void zswap_folio_swapin(struct folio *folio);\n bool zswap_is_enabled(void);\n bool zswap_never_enabled(void);\n+int zswap_proactive_writeback(struct mem_cgroup *memcg, u64 bytes_to_writeback);\n #else\n \n struct zswap_lruvec_state {};\n@@ -69,6 +70,12 @@ static inline bool zswap_never_enabled(void)\n \treturn true;\n }\n \n+static inline int zswap_proactive_writeback(struct mem_cgroup *memcg,\n+\t\t\t\t\t    u64 bytes_to_writeback)\n+{\n+\treturn -EOPNOTSUPP;\n+}\n+\n #endif\n \n #endif /* _LINUX_ZSWAP_H */\ndiff --git a/mm/memcontrol.c b/mm/memcontrol.c\nindex d20ffc827306e9..d81c34484bca28 100644\n--- a/mm/memcontrol.c\n+++ b/mm/memcontrol.c\n@@ -433,6 +433,7 @@ static const unsigned int memcg_stat_items[] = {\n \tMEMCG_ZSWAP_B,\n \tMEMCG_ZSWAPPED,\n \tMEMCG_ZSWAP_INCOMP,\n+\tMEMCG_ZSWPWB_PROACTIVE_B,\n };\n \n #define NR_MEMCG_NODE_STAT_ITEMS ARRAY_SIZE(memcg_node_stat_items)\n@@ -1558,6 +1559,7 @@ static const struct memory_stat memory_stats[] = {\n \t{ \"zswap\",\t\t\tMEMCG_ZSWAP_B\t\t\t},\n \t{ \"zswapped\",\t\t\tMEMCG_ZSWAPPED\t\t\t},\n \t{ \"zswap_incomp\",\t\tMEMCG_ZSWAP_INCOMP\t\t},\n+\t{ \"zswpwb_proactive_b\",\t\tMEMCG_ZSWPWB_PROACTIVE_B\t},\n #endif\n \t{ \"file_mapped\",\t\tNR_FILE_MAPPED\t\t\t},\n \t{ \"file_dirty\",\t\t\tNR_FILE_DIRTY\t\t\t},\n@@ -1614,6 +1616,7 @@ static int memcg_page_state_unit(int item)\n \tswitch (item) {\n \tcase MEMCG_PERCPU_B:\n \tcase MEMCG_ZSWAP_B:\n+\tcase MEMCG_ZSWPWB_PROACTIVE_B:\n \tcase NR_SLAB_RECLAIMABLE_B:\n \tcase NR_SLAB_UNRECLAIMABLE_B:\n \t\treturn 1;\ndiff --git a/mm/vmscan.c b/mm/vmscan.c\nindex 35c3bb15ae96a3..56ed7ff48ec9df 100644\n--- a/mm/vmscan.c\n+++ b/mm/vmscan.c\n@@ -64,6 +64,7 @@\n \n #include \u003clinux/swapops.h\u003e\n #include \u003clinux/sched/sysctl.h\u003e\n+#include \u003clinux/zswap.h\u003e\n \n #include \"internal.h\"\n #include \"swap.h\"\n@@ -7855,11 +7856,13 @@ static unsigned long __node_reclaim(struct pglist_data *pgdat, gfp_t gfp_mask,\n enum {\n \tMEMORY_RECLAIM_SWAPPINESS = 0,\n \tMEMORY_RECLAIM_SWAPPINESS_MAX,\n+\tMEMORY_RECLAIM_SOURCE,\n \tMEMORY_RECLAIM_NULL,\n };\n static const match_table_t tokens = {\n \t{ MEMORY_RECLAIM_SWAPPINESS, \"swappiness=%d\"},\n \t{ MEMORY_RECLAIM_SWAPPINESS_MAX, \"swappiness=max\"},\n+\t{ MEMORY_RECLAIM_SOURCE, \"source=%s\"},\n \t{ MEMORY_RECLAIM_NULL, NULL },\n };\n \n@@ -7869,9 +7872,12 @@ int user_proactive_reclaim(char *buf,\n \tunsigned int nr_retries = MAX_RECLAIM_RETRIES;\n \tunsigned long nr_to_reclaim, nr_reclaimed = 0;\n \tint swappiness = -1;\n+\tbool zswap_writeback_only = false;\n \tchar *old_buf, *start;\n+\tchar source[16];\n \tsubstring_t args[MAX_OPT_ARGS];\n \tgfp_t gfp_mask = GFP_KERNEL;\n+\tu64 nr_bytes;\n \n \tif (!buf || (!memcg \u0026\u0026 !pgdat) || (memcg \u0026\u0026 pgdat))\n \t\treturn -EINVAL;\n@@ -7879,7 +7885,8 @@ int user_proactive_reclaim(char *buf,\n \tbuf = strstrip(buf);\n \n \told_buf = buf;\n-\tnr_to_reclaim = memparse(buf, \u0026buf) / PAGE_SIZE;\n+\tnr_bytes = memparse(buf, \u0026buf);\n+\tnr_to_reclaim = nr_bytes / PAGE_SIZE;\n \tif (buf == old_buf)\n \t\treturn -EINVAL;\n \n@@ -7899,11 +7906,26 @@ int user_proactive_reclaim(char *buf,\n \t\tcase MEMORY_RECLAIM_SWAPPINESS_MAX:\n \t\t\tswappiness = SWAPPINESS_ANON_ONLY;\n \t\t\tbreak;\n+\t\tcase MEMORY_RECLAIM_SOURCE:\n+\t\t\tif (match_strlcpy(source, \u0026args[0], sizeof(source)) \u003e= sizeof(source))\n+\t\t\t\treturn -EINVAL;\n+\t\t\t/* Only zswap is supported as a reclaim source for now. */\n+\t\t\tif (strcmp(source, \"zswap\"))\n+\t\t\t\treturn -EINVAL;\n+\t\t\tzswap_writeback_only = true;\n+\t\t\tbreak;\n \t\tdefault:\n \t\t\treturn -EINVAL;\n \t\t}\n \t}\n \n+\tif (zswap_writeback_only) {\n+\t\t/* source=zswap and swappiness are mutually exclusive. */\n+\t\tif (swappiness != -1)\n+\t\t\treturn -EINVAL;\n+\t\treturn zswap_proactive_writeback(memcg, nr_bytes);\n+\t}\n+\n \twhile (nr_reclaimed \u003c nr_to_reclaim) {\n \t\t/* Will converge on zero, but reclaim enforces a minimum */\n \t\tunsigned long batch_size = (nr_to_reclaim - nr_reclaimed) / 4;\ndiff --git a/mm/zswap.c b/mm/zswap.c\nindex 761cd699e0a3e6..d356c1739c68f9 100644\n--- a/mm/zswap.c\n+++ b/mm/zswap.c\n@@ -160,6 +160,11 @@ struct zswap_pool {\n \tchar tfm_name[CRYPTO_MAX_ALG_NAME];\n };\n \n+struct zswap_shrink_walk_arg {\n+\tunsigned long bytes_written;\n+\tbool encountered_page_in_swapcache;\n+};\n+\n /* Global LRU lists shared by all zswap pools. */\n static struct list_lru zswap_list_lru;\n \n@@ -1089,8 +1094,9 @@ static enum lru_status shrink_memcg_cb(struct list_head *item, struct list_lru_o\n \t\t\t\t       void *arg)\n {\n \tstruct zswap_entry *entry = container_of(item, struct zswap_entry, lru);\n-\tbool *encountered_page_in_swapcache = (bool *)arg;\n+\tstruct zswap_shrink_walk_arg *walk_arg = arg;\n \tswp_entry_t swpentry;\n+\tunsigned int length;\n \tenum lru_status ret = LRU_REMOVED_RETRY;\n \tint writeback_result;\n \n@@ -1133,10 +1139,11 @@ static enum lru_status shrink_memcg_cb(struct list_head *item, struct list_lru_o\n \n \t/*\n \t * Once the lru lock is dropped, the entry might get freed. The\n-\t * swpentry is copied to the stack, and entry isn't deref'd again\n-\t * until the entry is verified to still be alive in the tree.\n+\t * needed fields are copied to the stack, and entry isn't deref'd\n+\t * again until it is verified to still be alive in the tree.\n \t */\n \tswpentry = entry-\u003eswpentry;\n+\tlength = entry-\u003elength;\n \n \t/*\n \t * It's safe to drop the lock here because we return either\n@@ -1155,12 +1162,13 @@ static enum lru_status shrink_memcg_cb(struct list_head *item, struct list_lru_o\n \t\t * into the warmer region. We should terminate shrinking (if we're in the dynamic\n \t\t * shrinker context).\n \t\t */\n-\t\tif (writeback_result == -EEXIST \u0026\u0026 encountered_page_in_swapcache) {\n+\t\tif (writeback_result == -EEXIST) {\n \t\t\tret = LRU_STOP;\n-\t\t\t*encountered_page_in_swapcache = true;\n+\t\t\twalk_arg-\u003eencountered_page_in_swapcache = true;\n \t\t}\n \t} else {\n \t\tzswap_written_back_pages++;\n+\t\twalk_arg-\u003ebytes_written += length;\n \t}\n \n \treturn ret;\n@@ -1169,8 +1177,11 @@ static enum lru_status shrink_memcg_cb(struct list_head *item, struct list_lru_o\n static unsigned long zswap_shrinker_scan(struct shrinker *shrinker,\n \t\tstruct shrink_control *sc)\n {\n+\tstruct zswap_shrink_walk_arg walk_arg = {\n+\t\t.bytes_written = 0,\n+\t\t.encountered_page_in_swapcache = false,\n+\t};\n \tunsigned long shrink_ret;\n-\tbool encountered_page_in_swapcache = false;\n \n \tif (!zswap_shrinker_enabled ||\n \t\t\t!mem_cgroup_zswap_writeback_enabled(sc-\u003ememcg)) {\n@@ -1179,9 +1190,9 @@ static unsigned long zswap_shrinker_scan(struct shrinker *shrinker,\n \t}\n \n \tshrink_ret = list_lru_shrink_walk(\u0026zswap_list_lru, sc, \u0026shrink_memcg_cb,\n-\t\t\u0026encountered_page_in_swapcache);\n+\t\t\u0026walk_arg);\n \n-\tif (encountered_page_in_swapcache)\n+\tif (walk_arg.encountered_page_in_swapcache)\n \t\treturn SHRINK_STOP;\n \n \treturn shrink_ret ? shrink_ret : SHRINK_STOP;\n@@ -1275,9 +1286,31 @@ static struct shrinker *zswap_alloc_shrinker(void)\n \treturn shrinker;\n }\n \n-static int shrink_memcg(struct mem_cgroup *memcg)\n+#define NR_ZSWAP_WB_BATCH\t64UL\n+\n+/*\n+ * Scan up to @nr_to_scan pages across the per-node zswap LRUs of @memcg\n+ * and write back the reclaimable ones.\n+ *\n+ * Since the second-chance algorithm rotates referenced entries to the\n+ * LRU tail, the per-node scan is capped at the current LRU length so\n+ * each entry is scanned at most once per call. It is up to the caller\n+ * to handle retries, deciding whether to scan another memcg to complete\n+ * the full iteration, or to rescan the current memcg to drain its zswap\n+ * entries.\n+ *\n+ * Return: The number of compressed bytes written back (\u003e= 0), or -ENOENT\n+ * if @memcg has writeback disabled, is a zombie cgroup, or has empty\n+ * zswap LRUs.\n+ */\n+static long shrink_memcg(struct mem_cgroup *memcg, unsigned long nr_to_scan)\n {\n-\tint nid, shrunk = 0, scanned = 0;\n+\tstruct zswap_shrink_walk_arg walk_arg = {\n+\t\t.bytes_written = 0,\n+\t\t.encountered_page_in_swapcache = false,\n+\t};\n+\tunsigned long nr_remaining = nr_to_scan;\n+\tint nid;\n \n \tif (!mem_cgroup_zswap_writeback_enabled(memcg))\n \t\treturn -ENOENT;\n@@ -1290,23 +1323,97 @@ static int shrink_memcg(struct mem_cgroup *memcg)\n \t\treturn -ENOENT;\n \n \tfor_each_node_state(nid, N_NORMAL_MEMORY) {\n-\t\tunsigned long nr_to_walk = 1;\n+\t\tunsigned long nr_to_walk;\n+\n+\t\t/*\n+\t\t * Cap the scan at per-node LRU length so each entry is scanned\n+\t\t * at most once per call.\n+\t\t */\n+\t\tnr_to_walk = min(nr_remaining,\n+\t\t\t\t list_lru_count_one(\u0026zswap_list_lru, nid, memcg));\n+\t\tif (!nr_to_walk)\n+\t\t\tcontinue;\n+\n+\t\tnr_remaining -= nr_to_walk;\n+\t\tlist_lru_walk_one(\u0026zswap_list_lru, nid, memcg, \u0026shrink_memcg_cb,\n+\t\t\t\t  \u0026walk_arg, \u0026nr_to_walk);\n+\t\t/* Return the unused share of the budget to the pool. */\n+\t\tnr_remaining += nr_to_walk;\n \n-\t\tshrunk += list_lru_walk_one(\u0026zswap_list_lru, nid, memcg,\n-\t\t\t\t\t    \u0026shrink_memcg_cb, NULL, \u0026nr_to_walk);\n-\t\tscanned += 1 - nr_to_walk;\n+\t\tif (!nr_remaining)\n+\t\t\tbreak;\n \t}\n \n-\tif (!scanned)\n+\t/* Nothing was scanned: every LRU under @memcg was empty. */\n+\tif (nr_remaining == nr_to_scan)\n \t\treturn -ENOENT;\n \n-\treturn shrunk ? 0 : -EAGAIN;\n+\treturn walk_arg.bytes_written;\n+}\n+\n+/* Track progress of a memcg-tree writeback walk. */\n+struct zswap_shrink_state {\n+\tint scans;\n+\tint failures;\n+};\n+\n+/*\n+ * Take one step of a memcg-tree writeback walk driven by the caller's\n+ * iterator, and fold the result into @s, the retry bookkeeping shared\n+ * across steps. @memcg is the iterator's current memcg, or NULL once\n+ * it has wrapped around after a full pass over the tree.\n+ *\n+ * The function returns -EBUSY to signal the caller to abort the walk after\n+ * encountering either of the following MAX_RECLAIM_RETRIES times:\n+ * - No writeback-candidate memcgs were found in a memcg tree walk.\n+ * - Shrinking a writeback-candidate memcg failed.\n+ *\n+ * Return: The number of compressed bytes written back (\u003e= 0), or -EBUSY\n+ * when the caller should abort the walk.\n+ */\n+static long zswap_shrink_one_memcg(struct mem_cgroup *memcg,\n+\t\t\t\t   struct zswap_shrink_state *s)\n+{\n+\tlong shrunk;\n+\n+\t/*\n+\t * Reaching a NULL memcg means a full hierarchy pass completed.\n+\t * Exclude the memcg-disabled case, where it is always NULL, and\n+\t * fall through to shrink the root LRU directly.\n+\t */\n+\tif (!memcg \u0026\u0026 !mem_cgroup_disabled()) {\n+\t\t/*\n+\t\t * Continue shrinking without incrementing failures if we found\n+\t\t * candidate memcgs in the last tree walk.\n+\t\t */\n+\t\tif (!s-\u003escans \u0026\u0026 ++s-\u003efailures == MAX_RECLAIM_RETRIES)\n+\t\t\treturn -EBUSY;\n+\t\ts-\u003escans = 0;\n+\t\treturn 0;\n+\t}\n+\n+\tshrunk = shrink_memcg(memcg, NR_ZSWAP_WB_BATCH);\n+\n+\t/*\n+\t * There are no writeback-candidate pages in the memcg. With memcg\n+\t * enabled this is not an issue as long as we can find another memcg\n+\t * with pages in zswap, so skip without counting it as a candidate.\n+\t * With memcg disabled the root LRU is the only target, so we should\n+\t * abort if it has no writeback-candidate pages.\n+\t */\n+\tif (shrunk == -ENOENT)\n+\t\treturn mem_cgroup_disabled() ? -EBUSY : 0;\n+\ts-\u003escans++;\n+\n+\tif (shrunk \u003c= 0 \u0026\u0026 ++s-\u003efailures == MAX_RECLAIM_RETRIES)\n+\t\treturn -EBUSY;\n+\n+\treturn shrunk;\n }\n \n static void shrink_worker(struct work_struct *w)\n {\n-\tstruct mem_cgroup *memcg;\n-\tint ret, failures = 0, attempts = 0;\n+\tstruct zswap_shrink_state s = {};\n \tunsigned long thr;\n \n \t/* Reclaim down to the accept threshold */\n@@ -1318,11 +1425,6 @@ static void shrink_worker(struct work_struct *w)\n \t * writeback-disabled memcgs (memory.zswap.writeback=0) are not\n \t * candidates for shrinking.\n \t *\n-\t * Shrinking will be aborted if we encounter the following\n-\t * MAX_RECLAIM_RETRIES times:\n-\t * - No writeback-candidate memcgs found in a memcg tree walk.\n-\t * - Shrinking a writeback-candidate memcg failed.\n-\t *\n \t * We save iteration cursor memcg into zswap_next_shrink,\n \t * which can be modified by the offline memcg cleaner\n \t * zswap_memcg_offline_cleanup().\n@@ -1337,7 +1439,11 @@ static void shrink_worker(struct work_struct *w)\n \t * offline memcg left in zswap_next_shrink will hold the reference\n \t * until the next run of shrink_worker().\n \t */\n-\tdo {\n+\twhile (zswap_total_pages() \u003e thr) {\n+\t\tstruct mem_cgroup *memcg;\n+\t\tlong ret;\n+\n+\t\tcond_resched();\n \t\t/*\n \t\t * Start shrinking from the next memcg after zswap_next_shrink.\n \t\t * When the offline cleaner has already advanced the cursor,\n@@ -1356,37 +1462,12 @@ static void shrink_worker(struct work_struct *w)\n \t\t} while (memcg \u0026\u0026 !mem_cgroup_tryget_online(memcg));\n \t\tspin_unlock(\u0026zswap_shrink_lock);\n \n-\t\tif (!memcg) {\n-\t\t\t/*\n-\t\t\t * Continue shrinking without incrementing failures if\n-\t\t\t * we found candidate memcgs in the last tree walk.\n-\t\t\t */\n-\t\t\tif (!attempts \u0026\u0026 ++failures == MAX_RECLAIM_RETRIES)\n-\t\t\t\tbreak;\n-\n-\t\t\tattempts = 0;\n-\t\t\tgoto resched;\n-\t\t}\n-\n-\t\tret = shrink_memcg(memcg);\n+\t\tret = zswap_shrink_one_memcg(memcg, \u0026s);\n \t\t/* drop the extra reference */\n \t\tmem_cgroup_put(memcg);\n-\n-\t\t/*\n-\t\t * There are no writeback-candidate pages in the memcg.\n-\t\t * This is not an issue as long as we can find another memcg\n-\t\t * with pages in zswap. Skip this without incrementing attempts\n-\t\t * and failures.\n-\t\t */\n-\t\tif (ret == -ENOENT)\n-\t\t\tcontinue;\n-\t\t++attempts;\n-\n-\t\tif (ret \u0026\u0026 ++failures == MAX_RECLAIM_RETRIES)\n+\t\tif (ret == -EBUSY)\n \t\t\tbreak;\n-resched:\n-\t\tcond_resched();\n-\t} while (zswap_total_pages() \u003e thr);\n+\t}\n }\n \n /*********************************\n@@ -1492,7 +1573,7 @@ bool zswap_store(struct folio *folio)\n \tobjcg = get_obj_cgroup_from_folio(folio);\n \tif (objcg \u0026\u0026 !obj_cgroup_may_zswap(objcg)) {\n \t\tmemcg = get_mem_cgroup_from_objcg(objcg);\n-\t\tif (shrink_memcg(memcg)) {\n+\t\tif (shrink_memcg(memcg, num_node_state(N_NORMAL_MEMORY)) \u003c= 0) {\n \t\t\tmem_cgroup_put(memcg);\n \t\t\tgoto put_objcg;\n \t\t}\n@@ -1632,6 +1713,58 @@ int zswap_load(struct folio *folio)\n \treturn 0;\n }\n \n+int zswap_proactive_writeback(struct mem_cgroup *memcg, u64 bytes_to_writeback)\n+{\n+\tstruct zswap_shrink_state s = {};\n+\tstruct mem_cgroup *iter = NULL;\n+\tu64 bytes_written = 0;\n+\tint ret = 0;\n+\n+\tif (!memcg)\n+\t\treturn -EINVAL;\n+\tif (!mem_cgroup_zswap_writeback_enabled(memcg))\n+\t\treturn -EINVAL;\n+\tif (!bytes_to_writeback)\n+\t\treturn 0;\n+\n+\twhile (bytes_written \u003c bytes_to_writeback) {\n+\t\tlong shrunk;\n+\n+\t\tcond_resched();\n+\n+\t\tif (signal_pending(current)) {\n+\t\t\tret = -EINTR;\n+\t\t\tbreak;\n+\t\t}\n+\n+\t\t/*\n+\t\t * Use a local iterator to walk the memcg and its online descendants\n+\t\t * in a round-robin manner. Upon exiting the loop, mem_cgroup_iter_break()\n+\t\t * must be called to drop the iterator reference.\n+\t\t */\n+\t\tdo {\n+\t\t\titer = mem_cgroup_iter(memcg, iter, NULL);\n+\t\t} while (iter \u0026\u0026 !mem_cgroup_tryget_online(iter));\n+\n+\t\tshrunk = zswap_shrink_one_memcg(iter, \u0026s);\n+\t\tif (shrunk \u003e 0) {\n+\t\t\tbytes_written += shrunk;\n+\t\t\tmod_memcg_state(iter, MEMCG_ZSWPWB_PROACTIVE_B, shrunk);\n+\t\t}\n+\n+\t\t/* drop the extra reference taken by mem_cgroup_tryget_online() */\n+\t\tmem_cgroup_put(iter);\n+\n+\t\tif (shrunk == -EBUSY) {\n+\t\t\tret = -EAGAIN;\n+\t\t\tbreak;\n+\t\t}\n+\t}\n+\n+\tmem_cgroup_iter_break(memcg, iter);\n+\treturn ret;\n+}\n+\n void zswap_invalidate(swp_entry_t swp)\n {\n \tpgoff_t offset = swp_offset(swp);\ndiff --git a/tools/testing/selftests/cgroup/test_zswap.c b/tools/testing/selftests/cgroup/test_zswap.c\nindex 49b36ee791606f..dfd5f24b9d990d 100644\n--- a/tools/testing/selftests/cgroup/test_zswap.c\n+++ b/tools/testing/selftests/cgroup/test_zswap.c\n@@ -60,7 +60,12 @@ static int get_zswap_stored_pages(size_t *value)\n \n static long get_cg_wb_count(const char *cg)\n {\n-\treturn cg_read_key_long(cg, \"memory.stat\", \"zswpwb\");\n+\treturn cg_read_key_long(cg, \"memory.stat\", \"zswpwb \");\n+}\n+\n+static long get_cg_pwb_bytes(const char *cg)\n+{\n+\treturn cg_read_key_long(cg, \"memory.stat\", \"zswpwb_proactive_b \");\n }\n \n static long get_zswpout(const char *cgroup)\n@@ -355,6 +360,7 @@ static int attempt_writeback(const char *cgroup, void *arg)\n static int test_zswap_writeback_one(const char *cgroup, bool wb)\n {\n \tlong zswpwb_before, zswpwb_after;\n+\tlong pwb_bytes;\n \n \tzswpwb_before = get_cg_wb_count(cgroup);\n \tif (zswpwb_before != 0) {\n@@ -362,6 +368,12 @@ static int test_zswap_writeback_one(const char *cgroup, bool wb)\n \t\treturn -1;\n \t}\n \n+\tpwb_bytes = get_cg_pwb_bytes(cgroup);\n+\tif (pwb_bytes \u003e 0) {\n+\t\tksft_print_msg(\"zswpwb_proactive_b_before = %ld instead of 0\\n\", pwb_bytes);\n+\t\treturn -1;\n+\t}\n+\n \tif (cg_run(cgroup, attempt_writeback, (void *) \u0026wb))\n \t\treturn -1;\n \n@@ -379,6 +391,17 @@ static int test_zswap_writeback_one(const char *cgroup, bool wb)\n \t\treturn -1;\n \t}\n \n+\t/*\n+\t * attempt_writeback() does not use the proactive writeback path, so\n+\t * zswpwb_proactive_b must stay at zero regardless of whether\n+\t * writeback was enabled.\n+\t */\n+\tpwb_bytes = get_cg_pwb_bytes(cgroup);\n+\tif (pwb_bytes \u003e 0) {\n+\t\tksft_print_msg(\"zswpwb_proactive_b_after is %ld, expected 0\\n\", pwb_bytes);\n+\t\treturn -1;\n+\t}\n+\n \treturn 0;\n }\n \n@@ -770,6 +793,141 @@ static int test_zswap_incompressible(const char *root)\n \treturn ret;\n }\n \n+/*\n+ * Trigger proactive zswap writeback with the following steps:\n+ * 1. Allocate memory.\n+ * 2. Push allocated memory into zswap.\n+ * 3. Proactively write back zswap pages to swap\n+ *    using \"source=zswap\".\n+ */\n+static int proactive_writeback_workload(const char *cgroup, void *arg)\n+{\n+\tlong pagesize = sysconf(_SC_PAGESIZE);\n+\tsize_t memsize = pagesize * 1024;\n+\tchar reclaim_cmd[64];\n+\tchar buf[pagesize];\n+\tlong zswap_usage;\n+\tint ret = -1;\n+\tint rc;\n+\tchar *mem;\n+\n+\tmem = (char *)malloc(memsize);\n+\tif (!mem)\n+\t\treturn ret;\n+\n+\tfor (int i = 0; i \u003c pagesize; i++)\n+\t\tbuf[i] = i \u003c pagesize / 2 ? (char)i : 0;\n+\tfor (int i = 0; i \u003c memsize; i += pagesize)\n+\t\tmemcpy(\u0026mem[i], buf, pagesize);\n+\n+\t/* Evict allocated memory into zswap. */\n+\tif (cg_write_numeric(cgroup, \"memory.reclaim\", memsize)) {\n+\t\tksft_print_msg(\"Failed to push pages into zswap\\n\");\n+\t\tgoto out;\n+\t}\n+\n+\tzswap_usage = cg_read_long(cgroup, \"memory.zswap.current\");\n+\tif (zswap_usage \u003c= 0) {\n+\t\tksft_print_msg(\"no zswap pool to write back\\n\");\n+\t\tgoto out;\n+\t}\n+\n+\t/* Trigger proactive zswap writeback. */\n+\tsnprintf(reclaim_cmd, sizeof(reclaim_cmd), \"%ld source=zswap\", zswap_usage);\n+\trc = cg_write(cgroup, \"memory.reclaim\", reclaim_cmd);\n+\tif (rc \u0026\u0026 rc != -EAGAIN) {\n+\t\tksft_print_msg(\"proactive zswap writeback failed: %d\\n\", rc);\n+\t\tgoto out;\n+\t}\n+\n+\tret = 0;\n+out:\n+\tfree(mem);\n+\treturn ret;\n+}\n+\n+static int check_writeback_invalid_inputs(const char *cgroup)\n+{\n+\tstatic char * const bad_inputs[] = {\n+\t\t\"source=zswap\",\n+\t\t\"1M source=zswap swappiness=60\",\n+\t\t\"1M swappiness=60 source=zswap\",\n+\t\t\"1M source=zswap swappiness=max\",\n+\t\t\"1M swappiness=max source=zswap\",\n+\t};\n+\tint i, rc;\n+\n+\tfor (i = 0; i \u003c ARRAY_SIZE(bad_inputs); i++) {\n+\t\trc = cg_write(cgroup, \"memory.reclaim\", bad_inputs[i]);\n+\t\tif (rc != -EINVAL) {\n+\t\t\tksft_print_msg(\"memory.reclaim '%s': returned %d, expected %d\\n\",\n+\t\t\t\t       bad_inputs[i], rc, -EINVAL);\n+\t\t\treturn -1;\n+\t\t}\n+\t}\n+\treturn 0;\n+}\n+\n+static int test_zswap_proactive_writeback(const char *root)\n+{\n+\tlong wb_before, wb_after;\n+\tlong pwb_b_before, pwb_b_after;\n+\tlong wb_delta, pwb_b_delta;\n+\tint ret = KSFT_FAIL;\n+\tchar *test_group;\n+\n+\tif (cg_read_strcmp(root, \"memory.zswap.writeback\", \"1\"))\n+\t\treturn KSFT_SKIP;\n+\n+\ttest_group = cg_name(root, \"zswap_proactive_test\");\n+\tif (!test_group)\n+\t\treturn KSFT_FAIL;\n+\tif (cg_create(test_group))\n+\t\tgoto out;\n+\t/*\n+\t * A missing zswpwb_proactive_b stat means the kernel lacks proactive\n+\t * writeback support, so skip rather than fail.\n+\t */\n+\tif (get_cg_pwb_bytes(test_group) \u003c 0) {\n+\t\tret = KSFT_SKIP;\n+\t\tgoto out;\n+\t}\n+\tif (check_writeback_invalid_inputs(test_group))\n+\t\tgoto out;\n+\n+\tpwb_b_before = get_cg_pwb_bytes(test_group);\n+\twb_before = get_cg_wb_count(test_group);\n+\tif (pwb_b_before \u003c 0 || wb_before \u003c 0)\n+\t\tgoto out;\n+\n+\tif (cg_run(test_group, proactive_writeback_workload, NULL))\n+\t\tgoto out;\n+\n+\tpwb_b_after = get_cg_pwb_bytes(test_group);\n+\twb_after = get_cg_wb_count(test_group);\n+\tif (pwb_b_after \u003c 0 || wb_after \u003c 0)\n+\t\tgoto out;\n+\n+\tpwb_b_delta = pwb_b_after - pwb_b_before;\n+\twb_delta = wb_after - wb_before;\n+\n+\tif (pwb_b_delta \u003c= 0) {\n+\t\tksft_print_msg(\"zswpwb_proactive_b did not increase: delta=%ld\\n\",\n+\t\t\t       pwb_b_delta);\n+\t\tgoto out;\n+\t}\n+\tif (wb_delta \u003c= 0) {\n+\t\tksft_print_msg(\"zswpwb did not increase: delta=%ld\\n\", wb_delta);\n+\t\tgoto out;\n+\t}\n+\n+\tret = KSFT_PASS;\n+out:\n+\tcg_destroy(test_group);\n+\tfree(test_group);\n+\treturn ret;\n+}\n+\n #define T(x) { x, #x }\n struct zswap_test {\n \tint (*fn)(const char *root);\n@@ -783,6 +941,7 @@ struct zswap_test {\n \tT(test_no_kmem_bypass),\n \tT(test_no_invasive_cgroup_shrink),\n \tT(test_zswap_incompressible),\n+\tT(test_zswap_proactive_writeback),\n };\n #undef T\n \n"
}

2/1 2026/06/29 11:49 agent patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
{
  "EnableConfigs": [
    "ZSWAP",
    "MEMCG"
  ],
  "FocusSymbols": [
    "user_proactive_reclaim",
    "zswap_proactive_writeback",
    "zswap_shrink_one_memcg",
    "shrink_memcg"
  ],
  "Reasoning": "The patch introduces a new feature for proactive zswap writeback via the 'memory.reclaim' cgroup interface using the 'source=zswap' key. This is a functional change reachable from userspace that modifies core memory management and zswap logic, making it highly suitable for fuzzing.",
  "WorthFuzzing": true
}

Instruction:
You are an expert Linux kernel maintainer.
Your job is to review a provided patch series and determine
if it makes functional changes to the kernel that should be fuzzed.

IMPORTANT: The changes have ALREADY been applied and committed as the HEAD commit in
your workspace. Do NOT rely on your internal knowledge of the kernel. You must actively
use your code access tools to examine the actual source code and confirm any assumptions.

Return WorthFuzzing=false if the patch only contains:
- Modifications to Documentation/, Kconfig files, or code comments.
- Purely decorative changes, such as logging (e.g., pr_err, printk) or tracepoints.
- Changes to numeric constants or macros that do not functionally alter execution flow.
- Code paths that are impossible to reach in virtualized environments like GCE or QEMU,
even when utilizing software-emulated hardware (e.g., usb gadget, mac80211_hwsim).

If it modifies reachable core kernel logic, drivers, or architectures, use your code search
tools to verify the code can be executed, then return WorthFuzzing=true.

When returning WorthFuzzing=true, you MUST ALSO:
1. Extract any specific kernel functions that should be heavily fuzzed into FocusSymbols.
   Avoid listing generic hot-path functions to prevent skewed test distributions.
2. Identify any specific CONFIG_ options required to properly test this new/modified feature.
   Go and look into the Kconfig files and check for ifdefs around the code, do not make assumptions.
   Do not list too generic configs (we already have them enabled). Only list those that
   specifically cover the modified code. List them in the EnableConfigs output array,
   and DO NOT add a 'CONFIG_' prefix (e.g., return "NET_IPV4" instead of "CONFIG_NET_IPV4").
Prefer calling several tools at the same time to save round-trips.


Use set-results tool to provide results of the analysis.
It must be called exactly once before the final reply.
Ignore results of this tool.

Prompt:
For your convenience, here is the diff of the changes:
commit e6ca8ee1c3b4031271fc614c2c02c1367735ba5f
Author: syz-cluster <triage@syzkaller.com>
Date:   Mon Jun 29 11:49:11 2026 +0000

    syz-cluster: applied patch under review

diff --git a/Documentation/admin-guide/cgroup-v2.rst b/Documentation/admin-guide/cgroup-v2.rst
index 993446ab66d0fb..e1f6a4729a65e6 100644
--- a/Documentation/admin-guide/cgroup-v2.rst
+++ b/Documentation/admin-guide/cgroup-v2.rst
@@ -1425,9 +1425,10 @@ PAGE_SIZE multiple when read back.
 
 The following nested keys are defined.
 
-	  ==========            ================================
+	  ====================  ==================================================
 	  swappiness            Swappiness value to reclaim with
-	  ==========            ================================
+	  source=zswap          Only perform proactive zswap writeback
+	  ====================  ==================================================
 
 	Specifying a swappiness value instructs the kernel to perform
 	the reclaim with that swappiness value. Note that this has the
@@ -1437,6 +1438,19 @@ The following nested keys are defined.
 	The valid range for swappiness is [0-200, max], setting
 	swappiness=max exclusively reclaims anonymous memory.
 
+	The source=zswap key skips ordinary memory reclaim and
+	writes back pages from zswap to the backing swap device until
+	the requested amount has been written or no further candidates
+	are found. This is useful to proactively offload cold compressed
+	data from the zswap pool to the swap device. It is only available
+	if zswap writeback is enabled. source=zswap cannot be
+	combined with swappiness; specifying both returns -EINVAL.
+
+	Example::
+
+	  # Writeback up to 10MB of compressed data from zswap to the backing swap
+	  echo "10M source=zswap" > memory.reclaim
+
   memory.peak
 	A read-write single value file which exists on non-root cgroups.
 
@@ -1734,6 +1748,10 @@ The following nested keys are defined.
 	  zswpwb
 		Number of pages written from zswap to swap.
 
+	  zswpwb_proactive_b
+		Bytes of compressed data proactively written back from
+		zswap to swap via the memory.reclaim source=zswap key.
+
 	  zswap_incomp
 		Number of incompressible pages currently stored in zswap
 		without compression. These pages could not be compressed to
diff --git a/Documentation/admin-guide/mm/zswap.rst b/Documentation/admin-guide/mm/zswap.rst
index 2464425c783de2..b49b8c13038905 100644
--- a/Documentation/admin-guide/mm/zswap.rst
+++ b/Documentation/admin-guide/mm/zswap.rst
@@ -131,7 +131,16 @@ User can enable it as follows::
   echo Y > /sys/module/zswap/parameters/shrinker_enabled
 
 This can be enabled at the boot time if ``CONFIG_ZSWAP_SHRINKER_DEFAULT_ON`` is
-selected.
+selected. Once enabled, the shrinker automatically writes back zswap pages to
+backing swap during memory reclaim.
+
+If users want to explicitly trigger proactive zswap writeback for a specific
+memory cgroup without invoking standard page reclaim, it can be done as follows::
+
+	echo "10M source=zswap" > /sys/fs/cgroup/<cgroup-name>/memory.reclaim
+
+Both of the methods mentioned above are subject to the ``memory.zswap.writeback``
+control. This means that ``memory.zswap.writeback`` can prevent all zswap writeback.
 
 A debugfs interface is provided for various statistic about pool size, number
 of pages stored, same-value filled pages and various counters for the reasons
diff --git a/include/linux/memcontrol.h b/include/linux/memcontrol.h
index e1f46a0016fcfd..56580b264dc426 100644
--- a/include/linux/memcontrol.h
+++ b/include/linux/memcontrol.h
@@ -40,6 +40,7 @@ enum memcg_stat_item {
 	MEMCG_ZSWAP_B,
 	MEMCG_ZSWAPPED,
 	MEMCG_ZSWAP_INCOMP,
+	MEMCG_ZSWPWB_PROACTIVE_B,
 	MEMCG_NR_STAT,
 };
 
diff --git a/include/linux/zswap.h b/include/linux/zswap.h
index 30c193a1207e16..e5f217759894ed 100644
--- a/include/linux/zswap.h
+++ b/include/linux/zswap.h
@@ -35,6 +35,7 @@ void zswap_lruvec_state_init(struct lruvec *lruvec);
 void zswap_folio_swapin(struct folio *folio);
 bool zswap_is_enabled(void);
 bool zswap_never_enabled(void);
+int zswap_proactive_writeback(struct mem_cgroup *memcg, u64 bytes_to_writeback);
 #else
 
 struct zswap_lruvec_state {};
@@ -69,6 +70,12 @@ static inline bool zswap_never_enabled(void)
 	return true;
 }
 
+static inline int zswap_proactive_writeback(struct mem_cgroup *memcg,
+					    u64 bytes_to_writeback)
+{
+	return -EOPNOTSUPP;
+}
+
 #endif
 
 #endif /* _LINUX_ZSWAP_H */
diff --git a/mm/memcontrol.c b/mm/memcontrol.c
index d20ffc827306e9..d81c34484bca28 100644
--- a/mm/memcontrol.c
+++ b/mm/memcontrol.c
@@ -433,6 +433,7 @@ static const unsigned int memcg_stat_items[] = {
 	MEMCG_ZSWAP_B,
 	MEMCG_ZSWAPPED,
 	MEMCG_ZSWAP_INCOMP,
+	MEMCG_ZSWPWB_PROACTIVE_B,
 };
 
 #define NR_MEMCG_NODE_STAT_ITEMS ARRAY_SIZE(memcg_node_stat_items)
@@ -1558,6 +1559,7 @@ static const struct memory_stat memory_stats[] = {
 	{ "zswap",			MEMCG_ZSWAP_B			},
 	{ "zswapped",			MEMCG_ZSWAPPED			},
 	{ "zswap_incomp",		MEMCG_ZSWAP_INCOMP		},
+	{ "zswpwb_proactive_b",		MEMCG_ZSWPWB_PROACTIVE_B	},
 #endif
 	{ "file_mapped",		NR_FILE_MAPPED			},
 	{ "file_dirty",			NR_FILE_DIRTY			},
@@ -1614,6 +1616,7 @@ static int memcg_page_state_unit(int item)
 	switch (item) {
 	case MEMCG_PERCPU_B:
 	case MEMCG_ZSWAP_B:
+	case MEMCG_ZSWPWB_PROACTIVE_B:
 	case NR_SLAB_RECLAIMABLE_B:
 	case NR_SLAB_UNRECLAIMABLE_B:
 		return 1;
diff --git a/mm/vmscan.c b/mm/vmscan.c
index 35c3bb15ae96a3..56ed7ff48ec9df 100644
--- a/mm/vmscan.c
+++ b/mm/vmscan.c
@@ -64,6 +64,7 @@
 
 #include <linux/swapops.h>
 #include <linux/sched/sysctl.h>
+#include <linux/zswap.h>
 
 #include "internal.h"
 #include "swap.h"
@@ -7855,11 +7856,13 @@ static unsigned long __node_reclaim(struct pglist_data *pgdat, gfp_t gfp_mask,
 enum {
 	MEMORY_RECLAIM_SWAPPINESS = 0,
 	MEMORY_RECLAIM_SWAPPINESS_MAX,
+	MEMORY_RECLAIM_SOURCE,
 	MEMORY_RECLAIM_NULL,
 };
 static const match_table_t tokens = {
 	{ MEMORY_RECLAIM_SWAPPINESS, "swappiness=%d"},
 	{ MEMORY_RECLAIM_SWAPPINESS_MAX, "swappiness=max"},
+	{ MEMORY_RECLAIM_SOURCE, "source=%s"},
 	{ MEMORY_RECLAIM_NULL, NULL },
 };
 
@@ -7869,9 +7872,12 @@ int user_proactive_reclaim(char *buf,
 	unsigned int nr_retries = MAX_RECLAIM_RETRIES;
 	unsigned long nr_to_reclaim, nr_reclaimed = 0;
 	int swappiness = -1;
+	bool zswap_writeback_only = false;
 	char *old_buf, *start;
+	char source[16];
 	substring_t args[MAX_OPT_ARGS];
 	gfp_t gfp_mask = GFP_KERNEL;
+	u64 nr_bytes;
 
 	if (!buf || (!memcg && !pgdat) || (memcg && pgdat))
 		return -EINVAL;
@@ -7879,7 +7885,8 @@ int user_proactive_reclaim(char *buf,
 	buf = strstrip(buf);
 
 	old_buf = buf;
-	nr_to_reclaim = memparse(buf, &buf) / PAGE_SIZE;
+	nr_bytes = memparse(buf, &buf);
+	nr_to_reclaim = nr_bytes / PAGE_SIZE;
 	if (buf == old_buf)
 		return -EINVAL;
 
@@ -7899,11 +7906,26 @@ int user_proactive_reclaim(char *buf,
 		case MEMORY_RECLAIM_SWAPPINESS_MAX:
 			swappiness = SWAPPINESS_ANON_ONLY;
 			break;
+		case MEMORY_RECLAIM_SOURCE:
+			if (match_strlcpy(source, &args[0], sizeof(source)) >= sizeof(source))
+				return -EINVAL;
+			/* Only zswap is supported as a reclaim source for now. */
+			if (strcmp(source, "zswap"))
+				return -EINVAL;
+			zswap_writeback_only = true;
+			break;
 		default:
 			return -EINVAL;
 		}
 	}
 
+	if (zswap_writeback_only) {
+		/* source=zswap and swappiness are mutually exclusive. */
+		if (swappiness != -1)
+			return -EINVAL;
+		return zswap_proactive_writeback(memcg, nr_bytes);
+	}
+
 	while (nr_reclaimed < nr_to_reclaim) {
 		/* Will converge on zero, but reclaim enforces a minimum */
 		unsigned long batch_size = (nr_to_reclaim - nr_reclaimed) / 4;
diff --git a/mm/zswap.c b/mm/zswap.c
index 761cd699e0a3e6..d356c1739c68f9 100644
--- a/mm/zswap.c
+++ b/mm/zswap.c
@@ -160,6 +160,11 @@ struct zswap_pool {
 	char tfm_name[CRYPTO_MAX_ALG_NAME];
 };
 
+struct zswap_shrink_walk_arg {
+	unsigned long bytes_written;
+	bool encountered_page_in_swapcache;
+};
+
 /* Global LRU lists shared by all zswap pools. */
 static struct list_lru zswap_list_lru;
 
@@ -1089,8 +1094,9 @@ static enum lru_status shrink_memcg_cb(struct list_head *item, struct list_lru_o
 				       void *arg)
 {
 	struct zswap_entry *entry = container_of(item, struct zswap_entry, lru);
-	bool *encountered_page_in_swapcache = (bool *)arg;
+	struct zswap_shrink_walk_arg *walk_arg = arg;
 	swp_entry_t swpentry;
+	unsigned int length;
 	enum lru_status ret = LRU_REMOVED_RETRY;
 	int writeback_result;
 
@@ -1133,10 +1139,11 @@ static enum lru_status shrink_memcg_cb(struct list_head *item, struct list_lru_o
 
 	/*
 	 * Once the lru lock is dropped, the entry might get freed. The
-	 * swpentry is copied to the stack, and entry isn't deref'd again
-	 * until the entry is verified to still be alive in the tree.
+	 * needed fields are copied to the stack, and entry isn't deref'd
+	 * again until it is verified to still be alive in the tree.
 	 */
 	swpentry = entry->swpentry;
+	length = entry->length;
 
 	/*
 	 * It's safe to drop the lock here because we return either
@@ -1155,12 +1162,13 @@ static enum lru_status shrink_memcg_cb(struct list_head *item, struct list_lru_o
 		 * into the warmer region. We should terminate shrinking (if we're in the dynamic
 		 * shrinker context).
 		 */
-		if (writeback_result == -EEXIST && encountered_page_in_swapcache) {
+		if (writeback_result == -EEXIST) {
 			ret = LRU_STOP;
-			*encountered_page_in_swapcache = true;
+			walk_arg->encountered_page_in_swapcache = true;
 		}
 	} else {
 		zswap_written_back_pages++;
+		walk_arg->bytes_written += length;
 	}
 
 	return ret;
@@ -1169,8 +1177,11 @@ static enum lru_status shrink_memcg_cb(struct list_head *item, struct list_lru_o
 static unsigned long zswap_shrinker_scan(struct shrinker *shrinker,
 		struct shrink_control *sc)
 {
+	struct zswap_shrink_walk_arg walk_arg = {
+		.bytes_written = 0,
+		.encountered_page_in_swapcache = false,
+	};
 	unsigned long shrink_ret;
-	bool encountered_page_in_swapcache = false;
 
 	if (!zswap_shrinker_enabled ||
 			!mem_cgroup_zswap_writeback_enabled(sc->memcg)) {
@@ -1179,9 +1190,9 @@ static unsigned long zswap_shrinker_scan(struct shrinker *shrinker,
 	}
 
 	shrink_ret = list_lru_shrink_walk(&zswap_list_lru, sc, &shrink_memcg_cb,
-		&encountered_page_in_swapcache);
+		&walk_arg);
 
-	if (encountered_page_in_swapcache)
+	if (walk_arg.encountered_page_in_swapcache)
 		return SHRINK_STOP;
 
 	return shrink_ret ? shrink_ret : SHRINK_STOP;
@@ -1275,9 +1286,31 @@ static struct shrinker *zswap_alloc_shrinker(void)
 	return shrinker;
 }
 
-static int shrink_memcg(struct mem_cgroup *memcg)
+#define NR_ZSWAP_WB_BATCH	64UL
+
+/*
+ * Scan up to @nr_to_scan pages across the per-node zswap LRUs of @memcg
+ * and write back the reclaimable ones.
+ *
+ * Since the second-chance algorithm rotates referenced entries to the
+ * LRU tail, the per-node scan is capped at the current LRU length so
+ * each entry is scanned at most once per call. It is up to the caller
+ * to handle retries, deciding whether to scan another memcg to complete
+ * the full iteration, or to rescan the current memcg to drain its zswap
+ * entries.
+ *
+ * Return: The number of compressed bytes written back (>= 0), or -ENOENT
+ * if @memcg has writeback disabled, is a zombie cgroup, or has empty
+ * zswap LRUs.
+ */
+static long shrink_memcg(struct mem_cgroup *memcg, unsigned long nr_to_scan)
 {
-	int nid, shrunk = 0, scanned = 0;
+	struct zswap_shrink_walk_arg walk_arg = {
+		.bytes_written = 0,
+		.encountered_page_in_swapcache = false,
+	};
+	unsigned long nr_remaining = nr_to_scan;
+	int nid;
 
 	if (!mem_cgroup_zswap_writeback_enabled(memcg))
 		return -ENOENT;
@@ -1290,23 +1323,97 @@ static int shrink_memcg(struct mem_cgroup *memcg)
 		return -ENOENT;
 
 	for_each_node_state(nid, N_NORMAL_MEMORY) {
-		unsigned long nr_to_walk = 1;
+		unsigned long nr_to_walk;
+
+		/*
+		 * Cap the scan at per-node LRU length so each entry is scanned
+		 * at most once per call.
+		 */
+		nr_to_walk = min(nr_remaining,
+				 list_lru_count_one(&zswap_list_lru, nid, memcg));
+		if (!nr_to_walk)
+			continue;
+
+		nr_remaining -= nr_to_walk;
+		list_lru_walk_one(&zswap_list_lru, nid, memcg, &shrink_memcg_cb,
+				  &walk_arg, &nr_to_walk);
+		/* Return the unused share of the budget to the pool. */
+		nr_remaining += nr_to_walk;
 
-		shrunk += list_lru_walk_one(&zswap_list_lru, nid, memcg,
-					    &shrink_memcg_cb, NULL, &nr_to_walk);
-		scanned += 1 - nr_to_walk;
+		if (!nr_remaining)
+			break;
 	}
 
-	if (!scanned)
+	/* Nothing was scanned: every LRU under @memcg was empty. */
+	if (nr_remaining == nr_to_scan)
 		return -ENOENT;
 
-	return shrunk ? 0 : -EAGAIN;
+	return walk_arg.bytes_written;
+}
+
+/* Track progress of a memcg-tree writeback walk. */
+struct zswap_shrink_state {
+	int scans;
+	int failures;
+};
+
+/*
+ * Take one step of a memcg-tree writeback walk driven by the caller's
+ * iterator, and fold the result into @s, the retry bookkeeping shared
+ * across steps. @memcg is the iterator's current memcg, or NULL once
+ * it has wrapped around after a full pass over the tree.
+ *
+ * The function returns -EBUSY to signal the caller to abort the walk after
+ * encountering either of the following MAX_RECLAIM_RETRIES times:
+ * - No writeback-candidate memcgs were found in a memcg tree walk.
+ * - Shrinking a writeback-candidate memcg failed.
+ *
+ * Return: The number of compressed bytes written back (>= 0), or -EBUSY
+ * when the caller should abort the walk.
+ */
+static long zswap_shrink_one_memcg(struct mem_cgroup *memcg,
+				   struct zswap_shrink_state *s)
+{
+	long shrunk;
+
+	/*
+	 * Reaching a NULL memcg means a full hierarchy pass completed.
+	 * Exclude the memcg-disabled case, where it is always NULL, and
+	 * fall through to shrink the root LRU directly.
+	 */
+	if (!memcg && !mem_cgroup_disabled()) {
+		/*
+		 * Continue shrinking without incrementing failures if we found
+		 * candidate memcgs in the last tree walk.
+		 */
+		if (!s->scans && ++s->failures == MAX_RECLAIM_RETRIES)
+			return -EBUSY;
+		s->scans = 0;
+		return 0;
+	}
+
+	shrunk = shrink_memcg(memcg, NR_ZSWAP_WB_BATCH);
+
+	/*
+	 * There are no writeback-candidate pages in the memcg. With memcg
+	 * enabled this is not an issue as long as we can find another memcg
+	 * with pages in zswap, so skip without counting it as a candidate.
+	 * With memcg disabled the root LRU is the only target, so we should
+	 * abort if it has no writeback-candidate pages.
+	 */
+	if (shrunk == -ENOENT)
+		return mem_cgroup_disabled() ? -EBUSY : 0;
+	s->scans++;
+
+	if (shrunk <= 0 && ++s->failures == MAX_RECLAIM_RETRIES)
+		return -EBUSY;
+
+	return shrunk;
 }
 
 static void shrink_worker(struct work_struct *w)
 {
-	struct mem_cgroup *memcg;
-	int ret, failures = 0, attempts = 0;
+	struct zswap_shrink_state s = {};
 	unsigned long thr;
 
 	/* Reclaim down to the accept threshold */
@@ -1318,11 +1425,6 @@ static void shrink_worker(struct work_struct *w)
 	 * writeback-disabled memcgs (memory.zswap.writeback=0) are not
 	 * candidates for shrinking.
 	 *
-	 * Shrinking will be aborted if we encounter the following
-	 * MAX_RECLAIM_RETRIES times:
-	 * - No writeback-candidate memcgs found in a memcg tree walk.
-	 * - Shrinking a writeback-candidate memcg failed.
-	 *
 	 * We save iteration cursor memcg into zswap_next_shrink,
 	 * which can be modified by the offline memcg cleaner
 	 * zswap_memcg_offline_cleanup().
@@ -1337,7 +1439,11 @@ static void shrink_worker(struct work_struct *w)
 	 * offline memcg left in zswap_next_shrink will hold the reference
 	 * until the next run of shrink_worker().
 	 */
-	do {
+	while (zswap_total_pages() > thr) {
+		struct mem_cgroup *memcg;
+		long ret;
+
+		cond_resched();
 		/*
 		 * Start shrinking from the next memcg after zswap_next_shrink.
 		 * When the offline cleaner has already advanced the cursor,
@@ -1356,37 +1462,12 @@ static void shrink_worker(struct work_struct *w)
 		} while (memcg && !mem_cgroup_tryget_online(memcg));
 		spin_unlock(&zswap_shrink_lock);
 
-		if (!memcg) {
-			/*
-			 * Continue shrinking without incrementing failures if
-			 * we found candidate memcgs in the last tree walk.
-			 */
-			if (!attempts && ++failures == MAX_RECLAIM_RETRIES)
-				break;
-
-			attempts = 0;
-			goto resched;
-		}
-
-		ret = shrink_memcg(memcg);
+		ret = zswap_shrink_one_memcg(memcg, &s);
 		/* drop the extra reference */
 		mem_cgroup_put(memcg);
-
-		/*
-		 * There are no writeback-candidate pages in the memcg.
-		 * This is not an issue as long as we can find another memcg
-		 * with pages in zswap. Skip this without incrementing attempts
-		 * and failures.
-		 */
-		if (ret == -ENOENT)
-			continue;
-		++attempts;
-
-		if (ret && ++failures == MAX_RECLAIM_RETRIES)
+		if (ret == -EBUSY)
 			break;
-resched:
-		cond_resched();
-	} while (zswap_total_pages() > thr);
+	}
 }
 
 /*********************************
@@ -1492,7 +1573,7 @@ bool zswap_store(struct folio *folio)
 	objcg = get_obj_cgroup_from_folio(folio);
 	if (objcg && !obj_cgroup_may_zswap(objcg)) {
 		memcg = get_mem_cgroup_from_objcg(objcg);
-		if (shrink_memcg(memcg)) {
+		if (shrink_memcg(memcg, num_node_state(N_NORMAL_MEMORY)) <= 0) {
 			mem_cgroup_put(memcg);
 			goto put_objcg;
 		}
@@ -1632,6 +1713,58 @@ int zswap_load(struct folio *folio)
 	return 0;
 }
 
+int zswap_proactive_writeback(struct mem_cgroup *memcg, u64 bytes_to_writeback)
+{
+	struct zswap_shrink_state s = {};
+	struct mem_cgroup *iter = NULL;
+	u64 bytes_written = 0;
+	int ret = 0;
+
+	if (!memcg)
+		return -EINVAL;
+	if (!mem_cgroup_zswap_writeback_enabled(memcg))
+		return -EINVAL;
+	if (!bytes_to_writeback)
+		return 0;
+
+	while (bytes_written < bytes_to_writeback) {
+		long shrunk;
+
+		cond_resched();
+
+		if (signal_pending(current)) {
+			ret = -EINTR;
+			break;
+		}
+
+		/*
+		 * Use a local iterator to walk the memcg and its online descendants
+		 * in a round-robin manner. Upon exiting the loop, mem_cgroup_iter_break()
+		 * must be called to drop the iterator reference.
+		 */
+		do {
+			iter = mem_cgroup_iter(memcg, iter, NULL);
+		} while (iter && !mem_cgroup_tryget_online(iter));
+
+		shrunk = zswap_shrink_one_memcg(iter, &s);
+		if (shrunk > 0) {
+			bytes_written += shrunk;
+			mod_memcg_state(iter, MEMCG_ZSWPWB_PROACTIVE_B, shrunk);
+		}
+
+		/* drop the extra reference taken by mem_cgroup_tryget_online() */
+		mem_cgroup_put(iter);
+
+		if (shrunk == -EBUSY) {
+			ret = -EAGAIN;
+			break;
+		}
+	}
+
+	mem_cgroup_iter_break(memcg, iter);
+	return ret;
+}
+
 void zswap_invalidate(swp_entry_t swp)
 {
 	pgoff_t offset = swp_offset(swp);
diff --git a/tools/testing/selftests/cgroup/test_zswap.c b/tools/testing/selftests/cgroup/test_zswap.c
index 49b36ee791606f..dfd5f24b9d990d 100644
--- a/tools/testing/selftests/cgroup/test_zswap.c
+++ b/tools/testing/selftests/cgroup/test_zswap.c
@@ -60,7 +60,12 @@ static int get_zswap_stored_pages(size_t *value)
 
 static long get_cg_wb_count(const char *cg)
 {
-	return cg_read_key_long(cg, "memory.stat", "zswpwb");
+	return cg_read_key_long(cg, "memory.stat", "zswpwb ");
+}
+
+static long get_cg_pwb_bytes(const char *cg)
+{
+	return cg_read_key_long(cg, "memory.stat", "zswpwb_proactive_b ");
 }
 
 static long get_zswpout(const char *cgroup)
@@ -355,6 +360,7 @@ static int attempt_writeback(const char *cgroup, void *arg)
 static int test_zswap_writeback_one(const char *cgroup, bool wb)
 {
 	long zswpwb_before, zswpwb_after;
+	long pwb_bytes;
 
 	zswpwb_before = get_cg_wb_count(cgroup);
 	if (zswpwb_before != 0) {
@@ -362,6 +368,12 @@ static int test_zswap_writeback_one(const char *cgroup, bool wb)
 		return -1;
 	}
 
+	pwb_bytes = get_cg_pwb_bytes(cgroup);
+	if (pwb_bytes > 0) {
+		ksft_print_msg("zswpwb_proactive_b_before = %ld instead of 0\n", pwb_bytes);
+		return -1;
+	}
+
 	if (cg_run(cgroup, attempt_writeback, (void *) &wb))
 		return -1;
 
@@ -379,6 +391,17 @@ static int test_zswap_writeback_one(const char *cgroup, bool wb)
 		return -1;
 	}
 
+	/*
+	 * attempt_writeback() does not use the proactive writeback path, so
+	 * zswpwb_proactive_b must stay at zero regardless of whether
+	 * writeback was enabled.
+	 */
+	pwb_bytes = get_cg_pwb_bytes(cgroup);
+	if (pwb_bytes > 0) {
+		ksft_print_msg("zswpwb_proactive_b_after is %ld, expected 0\n", pwb_bytes);
+		return -1;
+	}
+
 	return 0;
 }
 
@@ -770,6 +793,141 @@ static int test_zswap_incompressible(const char *root)
 	return ret;
 }
 
+/*
+ * Trigger proactive zswap writeback with the following steps:
+ * 1. Allocate memory.
+ * 2. Push allocated memory into zswap.
+ * 3. Proactively write back zswap pages to swap
+ *    using "source=zswap".
+ */
+static int proactive_writeback_workload(const char *cgroup, void *arg)
+{
+	long pagesize = sysconf(_SC_PAGESIZE);
+	size_t memsize = pagesize * 1024;
+	char reclaim_cmd[64];
+	char buf[pagesize];
+	long zswap_usage;
+	int ret = -1;
+	int rc;
+	char *mem;
+
+	mem = (char *)malloc(memsize);
+	if (!mem)
+		return ret;
+
+	for (int i = 0; i < pagesize; i++)
+		buf[i] = i < pagesize / 2 ? (char)i : 0;
+	for (int i = 0; i < memsize; i += pagesize)
+		memcpy(&mem[i], buf, pagesize);
+
+	/* Evict allocated memory into zswap. */
+	if (cg_write_numeric(cgroup, "memory.reclaim", memsize)) {
+		ksft_print_msg("Failed to push pages into zswap\n");
+		goto out;
+	}
+
+	zswap_usage = cg_read_long(cgroup, "memory.zswap.current");
+	if (zswap_usage <= 0) {
+		ksft_print_msg("no zswap pool to write back\n");
+		goto out;
+	}
+
+	/* Trigger proactive zswap writeback. */
+	snprintf(reclaim_cmd, sizeof(reclaim_cmd), "%ld source=zswap", zswap_usage);
+	rc = cg_write(cgroup, "memory.reclaim", reclaim_cmd);
+	if (rc && rc != -EAGAIN) {
+		ksft_print_msg("proactive zswap writeback failed: %d\n", rc);
+		goto out;
+	}
+
+	ret = 0;
+out:
+	free(mem);
+	return ret;
+}
+
+static int check_writeback_invalid_inputs(const char *cgroup)
+{
+	static char * const bad_inputs[] = {
+		"source=zswap",
+		"1M source=zswap swappiness=60",
+		"1M swappiness=60 source=zswap",
+		"1M source=zswap swappiness=max",
+		"1M swappiness=max source=zswap",
+	};
+	int i, rc;
+
+	for (i = 0; i < ARRAY_SIZE(bad_inputs); i++) {
+		rc = cg_write(cgroup, "memory.reclaim", bad_inputs[i]);
+		if (rc != -EINVAL) {
+			ksft_print_msg("memory.reclaim '%s': returned %d, expected %d\n",
+				       bad_inputs[i], rc, -EINVAL);
+			return -1;
+		}
+	}
+	return 0;
+}
+
+static int test_zswap_proactive_writeback(const char *root)
+{
+	long wb_before, wb_after;
+	long pwb_b_before, pwb_b_after;
+	long wb_delta, pwb_b_delta;
+	int ret = KSFT_FAIL;
+	char *test_group;
+
+	if (cg_read_strcmp(root, "memory.zswap.writeback", "1"))
+		return KSFT_SKIP;
+
+	test_group = cg_name(root, "zswap_proactive_test");
+	if (!test_group)
+		return KSFT_FAIL;
+	if (cg_create(test_group))
+		goto out;
+	/*
+	 * A missing zswpwb_proactive_b stat means the kernel lacks proactive
+	 * writeback support, so skip rather than fail.
+	 */
+	if (get_cg_pwb_bytes(test_group) < 0) {
+		ret = KSFT_SKIP;
+		goto out;
+	}
+	if (check_writeback_invalid_inputs(test_group))
+		goto out;
+
+	pwb_b_before = get_cg_pwb_bytes(test_group);
+	wb_before = get_cg_wb_count(test_group);
+	if (pwb_b_before < 0 || wb_before < 0)
+		goto out;
+
+	if (cg_run(test_group, proactive_writeback_workload, NULL))
+		goto out;
+
+	pwb_b_after = get_cg_pwb_bytes(test_group);
+	wb_after = get_cg_wb_count(test_group);
+	if (pwb_b_after < 0 || wb_after < 0)
+		goto out;
+
+	pwb_b_delta = pwb_b_after - pwb_b_before;
+	wb_delta = wb_after - wb_before;
+
+	if (pwb_b_delta <= 0) {
+		ksft_print_msg("zswpwb_proactive_b did not increase: delta=%ld\n",
+			       pwb_b_delta);
+		goto out;
+	}
+	if (wb_delta <= 0) {
+		ksft_print_msg("zswpwb did not increase: delta=%ld\n", wb_delta);
+		goto out;
+	}
+
+	ret = KSFT_PASS;
+out:
+	cg_destroy(test_group);
+	free(test_group);
+	return ret;
+}
+
 #define T(x) { x, #x }
 struct zswap_test {
 	int (*fn)(const char *root);
@@ -783,6 +941,7 @@ struct zswap_test {
 	T(test_no_kmem_bypass),
 	T(test_no_invasive_cgroup_shrink),
 	T(test_zswap_incompressible),
+	T(test_zswap_proactive_writeback),
 };
 #undef T
 

3/2 2026/06/29 11:49 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 11134
							output: 27
							thoughts: 414
						

Thoughts:
**Analyzing the Proactive Zswap Writeback Patch**

Okay, so we have this new patch, and it looks like it's introducing proactive zswap writeback through the `memory.reclaim` interface, specifically with `source=zswap`. This is interesting; let's break down what's going on here.

The patch touches a number of files. It's updating the documentation (`Documentation/admin-guide/cgroup-v2.rst`, `Documentation/admin-guide/mm/zswap.rst`) to reflect this new capability, which is standard practice, of course. We're also seeing changes in the header files (`include/linux/memcontrol.h`, `include/linux/zswap.h`) and core implementation files (`mm/memcontrol.c`, `mm/vmscan.c`, `mm/zswap.c`). There's even a test suite addition (`tools/testing/selftests/cgroup/test_zswap.c`), which is great – we need thorough testing for a feature like this.

The meat of the change seems to be in `mm/vmscan.c`. It's modifying `user_proactive_reclaim` to parse the `source=zswap` option. If `zswap_writeback_only` is enabled, it calls `zswap_proactive_writeback`. Then `mm/zswap.c` implements `zswap_proactive_writeback` itself, which, in turn, iterates through memcgs and calls `zswap_shrink_one_memcg`. The `zswap_shrink_one_memcg` then calls `shrink_memcg`. The `shrink_memcg` function has been altered to accept a `nr_to_scan` argument and to return the number of compressed bytes written back. This is all clearly functional, introducing a way to proactively trigger writeback of zswap pages.

From a functional perspective, this is a significant addition. It provides a new mechanism to control zswap behavior through memcgs. Because it's exposed through the `memory.reclaim` interface, accessible from userspace, we absolutely need to consider it a target for fuzzing. Any new userspace interaction is a potential attack surface, so this needs to be really robust.

Now, let's think about the configuration options. `CONFIG_ZSWAP` is obviously a must-have for any of this to work, and `CONFIG_MEMCG` is required because it uses the `memory.reclaim` interface. It's a bit early, but I should probably check `mm/zswap.c` itself for any other relevant configurations.



4/2 2026/06/29 11:49 tool grepper
0m Args:
{
  "Expression": "CONFIG_ZSWAP",
  "PathPrefix": "mm/"
}

Results:
{
  "Output": "mm/Makefile=78=obj-$(CONFIG_SWAP)\t+= page_io.o swap_state.o swapfile.o\nmm/Makefile:79:obj-$(CONFIG_ZSWAP)\t+= zswap.o\nmm/Makefile-80-obj-$(CONFIG_HAS_DMA)\t+= dmapool.o\n--\nmm/memcontrol.c=554=static const unsigned int memcg_vm_event_stat[] = {\n--\nmm/memcontrol.c-570-#endif\nmm/memcontrol.c:571:#ifdef CONFIG_ZSWAP\nmm/memcontrol.c-572-\tZSWPIN,\n--\nmm/memcontrol.c=1547=static const struct memory_stat memory_stats[] = {\n--\nmm/memcontrol.c-1557-\t{ \"shmem\",\t\t\tNR_SHMEM\t\t\t},\nmm/memcontrol.c:1558:#ifdef CONFIG_ZSWAP\nmm/memcontrol.c-1559-\t{ \"zswap\",\t\t\tMEMCG_ZSWAP_B\t\t\t},\n--\nmm/memcontrol.c=4160=mem_cgroup_css_alloc(struct cgroup_subsys_state *parent_css)\n--\nmm/memcontrol.c-4173-\tmemcg1_soft_limit_reset(memcg);\nmm/memcontrol.c:4174:#ifdef CONFIG_ZSWAP\nmm/memcontrol.c-4175-\tmemcg-\u003ezswap_max = PAGE_COUNTER_MAX;\n--\nmm/memcontrol.c=5792=static struct cftype swap_files[] = {\n--\nmm/memcontrol.c-5826-\nmm/memcontrol.c:5827:#ifdef CONFIG_ZSWAP\nmm/memcontrol.c-5828-/**\n--\nmm/memcontrol.c=6004=static struct cftype zswap_files[] = {\n--\nmm/memcontrol.c-6022-};\nmm/memcontrol.c:6023:#endif /* CONFIG_ZSWAP */\nmm/memcontrol.c-6024-\nmm/memcontrol.c=6025=static int __init mem_cgroup_swap_init(void)\n--\nmm/memcontrol.c-6033-#endif\nmm/memcontrol.c:6034:#ifdef CONFIG_ZSWAP\nmm/memcontrol.c-6035-\tWARN_ON(cgroup_add_dfl_cftypes(\u0026memory_cgrp_subsys, zswap_files));\n--\nmm/vmstat.c=1187=const char * const vmstat_text[] = {\n--\nmm/vmstat.c-1450-#endif\nmm/vmstat.c:1451:#ifdef CONFIG_ZSWAP\nmm/vmstat.c-1452-\t[I(ZSWPIN)]\t\t\t\t= \"zswpin\",\n--\nmm/zswap.c=86=static int zswap_setup(void);\n--\nmm/zswap.c-88-/* Enable/disable zswap */\nmm/zswap.c:89:static DEFINE_STATIC_KEY_MAYBE(CONFIG_ZSWAP_DEFAULT_ON, zswap_ever_enabled);\nmm/zswap.c:90:static bool zswap_enabled = IS_ENABLED(CONFIG_ZSWAP_DEFAULT_ON);\nmm/zswap.c-91-static int zswap_enabled_param_set(const char *,\n--\nmm/zswap.c=97=module_param_cb(enabled, \u0026zswap_enabled_param_ops, \u0026zswap_enabled, 0644);\n--\nmm/zswap.c-99-/* Crypto compressor to use */\nmm/zswap.c:100:static char *zswap_compressor = CONFIG_ZSWAP_COMPRESSOR_DEFAULT;\nmm/zswap.c-101-static int zswap_compressor_param_set(const char *,\n--\nmm/zswap.c=121=static bool zswap_shrinker_enabled = IS_ENABLED(\nmm/zswap.c:122:\t\tCONFIG_ZSWAP_SHRINKER_DEFAULT_ON);\nmm/zswap.c-123-module_param_named(shrinker_enabled, zswap_shrinker_enabled, bool, 0644);\n--\nmm/zswap.c=130=bool zswap_never_enabled(void)\nmm/zswap.c-131-{\nmm/zswap.c:132:\treturn !static_branch_maybe(CONFIG_ZSWAP_DEFAULT_ON, \u0026zswap_ever_enabled);\nmm/zswap.c-133-}\n--\nmm/zswap.c=349=static struct zswap_pool *__zswap_pool_create_fallback(void)\n--\nmm/zswap.c-351-\tif (!crypto_has_acomp(zswap_compressor, 0, 0) \u0026\u0026\nmm/zswap.c:352:\t    strcmp(zswap_compressor, CONFIG_ZSWAP_COMPRESSOR_DEFAULT)) {\nmm/zswap.c-353-\t\tpr_err(\"compressor %s not available, using default %s\\n\",\nmm/zswap.c:354:\t\t       zswap_compressor, CONFIG_ZSWAP_COMPRESSOR_DEFAULT);\nmm/zswap.c-355-\t\tparam_free_charp(\u0026zswap_compressor);\nmm/zswap.c:356:\t\tzswap_compressor = CONFIG_ZSWAP_COMPRESSOR_DEFAULT;\nmm/zswap.c-357-\t}\n"
}

5/2 2026/06/29 11:49 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 13557
							output: 33
							thoughts: 17
						

Thoughts:
Okay, let's dive into this. Here's my thought process summarized, assuming I'm already pretty familiar with the Linux kernel and memory management:

**Checking Zswap Configuration Options**

Alright, I need to get a handle on the zswap configuration possibilities. My first instinct, as always, is to go directly to the source. The header file should contain the key definitions. So, let me open up `include/linux/zswap.h`. That's where I expect to find the relevant macros, structures, and likely some inline comments explaining the different compile-time and runtime parameters associated with zswap. I'm looking for things like buffer sizes, compression algorithms, thresholds, and any tunable parameters that directly influence zswap's behavior. I anticipate finding the preprocessor definitions that control whether the feature is compiled in, along with any default values that are used. The documentation within that header should be my starting point for understanding how this feature is currently designed. From there, I can begin to reason about how to tune or modify the feature.



6/2 2026/06/29 11:49 tool grepper
0m Args:
{
  "Expression": "CONFIG_ZSWAP",
  "PathPrefix": "include/linux/zswap.h"
}

Results:
{
  "Output": "include/linux/zswap.h=10=extern atomic_long_t zswap_stored_pages;\ninclude/linux/zswap.h-11-\ninclude/linux/zswap.h:12:#ifdef CONFIG_ZSWAP\ninclude/linux/zswap.h-13-\n"
}

7/2 2026/06/29 11:49 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 13909
							output: 29
							thoughts: 175
						

Thoughts:
**Analyzing the ZSWAP and MEMCG Configuration Changes**

Okay, so I'm looking at this patch and immediately, my brain zeroes in on the key players: `ZSWAP` and `MEMCG`. We're dealing with memory management and control groups, specifically how zswap interacts with them. My initial instinct is to identify the critical symbols altered by this patch.

Right, the patch hits `mm/vmscan.c` with a change to `user_proactive_reclaim`. Then, it introduces `zswap_proactive_writeback` in `mm/zswap.c`. This suggests a new proactive writeback strategy specifically for zswap. Furthermore, it touches `shrink_memcg` and adds `zswap_shrink_one_memcg`, also in `mm/zswap.c`. This looks like a refinement of how we shrink memory control groups when using zswap.

So, the focus symbols are: `user_proactive_reclaim`, `zswap_proactive_writeback`, `zswap_shrink_one_memcg`, and `shrink_memcg`. Now, the first thing I need to do is quickly check if `user_proactive_reclaim` is static or exported. That will give me a better understanding of the scope of the change and if it's visible outside its original source file.



8/2 2026/06/29 11:49 tool grepper
0m Args:
{
  "Expression": "user_proactive_reclaim",
  "PathPrefix": "mm/"
}

Results:
{
  "Output": "mm/internal.h=631=extern void reclaim_throttle(pg_data_t *pgdat, enum vmscan_throttle_state reason);\nmm/internal.h:632:int user_proactive_reclaim(char *buf,\nmm/internal.h-633-\t\t\t   struct mem_cgroup *memcg, pg_data_t *pgdat);\n--\nmm/memcontrol.c=4992=static ssize_t memory_reclaim(struct kernfs_open_file *of, char *buf,\n--\nmm/memcontrol.c-4997-\nmm/memcontrol.c:4998:\tret = user_proactive_reclaim(buf, memcg, NULL);\nmm/memcontrol.c-4999-\tif (ret)\n--\nmm/vmscan.c=7862=static const match_table_t tokens = {\n--\nmm/vmscan.c-7868-\nmm/vmscan.c:7869:int user_proactive_reclaim(char *buf,\nmm/vmscan.c-7870-\t\t\t   struct mem_cgroup *memcg, pg_data_t *pgdat)\n--\nmm/vmscan.c=8031=static ssize_t reclaim_store(struct device *dev,\n--\nmm/vmscan.c-8036-\nmm/vmscan.c:8037:\tret = user_proactive_reclaim((char *)buf, NULL, NODE_DATA(nid));\nmm/vmscan.c-8038-\treturn ret ? -EAGAIN : count;\n"
}

9/2 2026/06/29 11:49 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 14808
							output: 27
							thoughts: 361
						

Thoughts:
**Analyzing the Memcg Memory Reclaim Flow**

Okay, so I'm looking at how memory reclaim is triggered and how it interacts with zswap and memory cgroups. I see that `user_proactive_reclaim` is the key function here, called from `memory_reclaim` in `mm/memcontrol.c` and also from `reclaim_store` in `mm/vmscan.c`. `memory_reclaim` is, of course, the handler for the `memory.reclaim` cgroup file, so that’s the main entry point for user-initiated reclaim.

My immediate focus is on how `user_proactive_reclaim` relates to zswap, specifically the interaction between `zswap_proactive_writeback`, `zswap_shrink_one_memcg`, and the general `shrink_memcg` function. I need to understand the conditions under which these are called.

The code mentions `CONFIG_ZSWAP` and `MEMCG` as relevant configurations. I'm wondering if just `CONFIG_ZSWAP` is sufficient to enable the proactive zswap writeback functionality, or if I need `CONFIG_ZSWAP_SHRINKER_DEFAULT_ON`. The patch text provides some context, stating that zswap writeback needs to be enabled for this, and that you cannot mix the `zswap` source with swappiness.

Digging into `mm/zswap.c`, I find the `zswap_proactive_writeback` function. I see it checks `mem_cgroup_zswap_writeback_enabled(memcg)`. I'll confirm how that is set up. Let's see... and, it looks like this check *doesn't* depend on `CONFIG_ZSWAP_SHRINKER_DEFAULT_ON`. So, it seems like having both `ZSWAP` and `MEMCG` enabled is enough. I can now be confident that `ZSWAP` and `MEMCG` are the correct configs to be looking at.

Now I'll just double-check, is it truly `CONFIG_MEMCG`, or should I be looking for something else?



10/2 2026/06/29 11:49 tool grepper
0m Args:
{
  "Expression": "config MEMCG",
  "PathPrefix": "init/Kconfig"
}

Results:
{
  "Output": "init/Kconfig=1063=config CGROUP_FAVOR_DYNMODS\n--\ninit/Kconfig-1072-\ninit/Kconfig:1073:config MEMCG\ninit/Kconfig-1074-\tbool \"Memory controller\"\n--\ninit/Kconfig-1081-\ninit/Kconfig:1082:config MEMCG_NMI_UNSAFE\ninit/Kconfig-1083-\tbool\n--\ninit/Kconfig-1088-\ninit/Kconfig:1089:config MEMCG_NMI_SAFETY_REQUIRES_ATOMIC\ninit/Kconfig-1090-\tbool\n--\ninit/Kconfig-1095-\ninit/Kconfig:1096:config MEMCG_V1\ninit/Kconfig-1097-\tbool \"Legacy cgroup v1 memory controller\"\n"
}

11/2 2026/06/29 11:49 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 15891
							output: 28
							thoughts: 27
						

Thoughts:
Okay, here's my summary, presented as a stream of thought:

**Confirming and Refining My Understanding**

Yes, that's right, `MEMCG` is definitely the relevant term we're dealing with. It's the core concept. Now, before I proceed further with the analysis, I just want to quickly make absolutely sure about the next configuration option that’s mentioned. Let me just double check: is `CONFIG_ZSWAP` the precise configuration option we're looking at here? I need to verify that specific name to ensure accuracy in my work.



12/2 2026/06/29 11:49 tool grepper
0m Args:
{
  "Expression": "config ZSWAP",
  "PathPrefix": "mm/Kconfig"
}

Results:
{
  "Output": "mm/Kconfig=12=menuconfig SWAP\n--\nmm/Kconfig-21-\nmm/Kconfig:22:config ZSWAP\nmm/Kconfig-23-\tbool \"Compressed cache for swap pages\"\n--\nmm/Kconfig-34-\nmm/Kconfig:35:config ZSWAP_DEFAULT_ON\nmm/Kconfig-36-\tbool \"Enable the compressed cache for swap pages by default\"\n--\nmm/Kconfig-44-\nmm/Kconfig:45:config ZSWAP_SHRINKER_DEFAULT_ON\nmm/Kconfig-46-\tbool \"Shrink the zswap pool on memory pressure\"\n--\nmm/Kconfig=59=choice\n--\nmm/Kconfig-76-\nmm/Kconfig:77:config ZSWAP_COMPRESSOR_DEFAULT_DEFLATE\nmm/Kconfig-78-\tbool \"Deflate\"\n--\nmm/Kconfig-82-\nmm/Kconfig:83:config ZSWAP_COMPRESSOR_DEFAULT_LZO\nmm/Kconfig-84-\tbool \"LZO\"\n--\nmm/Kconfig-88-\nmm/Kconfig:89:config ZSWAP_COMPRESSOR_DEFAULT_842\nmm/Kconfig-90-\tbool \"842\"\n--\nmm/Kconfig-94-\nmm/Kconfig:95:config ZSWAP_COMPRESSOR_DEFAULT_LZ4\nmm/Kconfig-96-\tbool \"LZ4\"\n--\nmm/Kconfig-100-\nmm/Kconfig:101:config ZSWAP_COMPRESSOR_DEFAULT_LZ4HC\nmm/Kconfig-102-\tbool \"LZ4HC\"\n--\nmm/Kconfig-106-\nmm/Kconfig:107:config ZSWAP_COMPRESSOR_DEFAULT_ZSTD\nmm/Kconfig-108-\tbool \"zstd\"\n--\nmm/Kconfig=112=endchoice\nmm/Kconfig-113-\nmm/Kconfig:114:config ZSWAP_COMPRESSOR_DEFAULT\nmm/Kconfig-115-       string\n"
}

13/2 2026/06/29 11:49 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 16607
							output: 140
							thoughts: 122
						

Thoughts:
Okay, let's break this down.

**Initial Assessment: ZSWAP Patch for Proactive Writeback**

Alright, so the feedback confirms my initial hunch – `ZSWAP` is definitely the right direction here. And this patch... yes, it's screaming for some serious fuzzing. The fact that it adds a new userspace-accessible feature to `memory.reclaim` – that proactive zswap writeback, in effect – changes the game. This has the potential to introduce interesting edge cases and vulnerabilities, I can see that immediately.

Now, my focus needs to hone in on those key symbols: `user_proactive_reclaim`, `zswap_proactive_writeback`, `zswap_shrink_one_memcg`, and `shrink_memcg`. They're the critical functions to dissect and understand the interactions within that area of the code. I need to understand exactly how the userspace interaction influences the reclamation process. What are the preconditions, and what are the potential failure modes?

Also, the configs are another point to explore. I need to make sure I am fully aware of the interactions between `ZSWAP` and `MEMCG`. They are going to have a lot of importance in the testing of the patches.

Alright, time to set some concrete testing goals and get some results in. Let's call a set-results and establish some parameters.



14/2 2026/06/29 11:49 tool set-results
0m Args:
{
  "EnableConfigs": [
    "ZSWAP",
    "MEMCG"
  ],
  "FocusSymbols": [
    "user_proactive_reclaim",
    "zswap_proactive_writeback",
    "zswap_shrink_one_memcg",
    "shrink_memcg"
  ],
  "Reasoning": "The patch introduces a new feature for proactive zswap writeback via the 'memory.reclaim' cgroup interface using the 'source=zswap' key. This is a functional change reachable from userspace that modifies core memory management and zswap logic, making it highly suitable for fuzzing.",
  "WorthFuzzing": true
}

Results:
{
  "EnableConfigs": [
    "ZSWAP",
    "MEMCG"
  ],
  "FocusSymbols": [
    "user_proactive_reclaim",
    "zswap_proactive_writeback",
    "zswap_shrink_one_memcg",
    "shrink_memcg"
  ],
  "Reasoning": "The patch introduces a new feature for proactive zswap writeback via the 'memory.reclaim' cgroup interface using the 'source=zswap' key. This is a functional change reachable from userspace that modifies core memory management and zswap logic, making it highly suitable for fuzzing.",
  "WorthFuzzing": true
}

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)