| Seq | Timestamp | Type | Name | Duration |
|---|---|---|---|---|
| 0/0 | 2026/08/07 21:03 | flow | patch-triage |
0mArgs:null Results: {
"EnableConfigs": [
"BPF_SYSCALL",
"BLK_CGROUP"
],
"FocusSymbols": [
"bpf_get_root_blkcg",
"bpf_get_blkcg",
"bpf_put_blkcg",
"bpf_blkcg_flush_stats",
"bpf_iter_blkg_new",
"bpf_iter_blkg_next",
"bpf_iter_blkg_destroy",
"bpf_blkg_iostat_bytes",
"bpf_blkg_iostat_ios",
"bpf_blkg_dev",
"blkcg_fill_root_iostats"
],
"KMSANReasoning": "The patch introduces BPF kfuncs to read block cgroup I/O statistics (io.stat). It does not allocate new complex structures, copy data directly to user space, or introduce logic that could expose uninitialized memory. The kfuncs return simple scalar values (u64) or pointers to existing, initialized kernel structures. The BPF iterator state is fully initialized in `bpf_iter_blkg_new`. Any potential memory safety issues, such as out-of-bounds accesses or use-after-free bugs related to RCU or cgroup references, would be effectively caught by KASAN and standard bug detectors. Therefore, a dedicated KMSAN fuzzing session is not justified.",
"NeedsKMSAN": false,
"Reasoning": "The patch introduces new BPF kfuncs for the block I/O controller (blkcg) to allow BPF programs to read a cgroup's io.stat counters. These kfuncs are reachable from BPF programs and should be fuzzed to ensure they handle various inputs and concurrent executions safely.",
"WorthFuzzing": true
} |
| 1/1 | 2026/08/07 21:03 | action | read-patch-diff |
0mArgs:null Results: {
"PatchDiff": "commit 2ca19a8a248872888de1a5be7d67838a2f2a4217\nAuthor: syz-cluster \u003ctriage@syzkaller.com\u003e\nDate: Fri Aug 7 21:03:03 2026 +0000\n\n syz-cluster: applied patch under review\n\ndiff --git a/MAINTAINERS b/MAINTAINERS\nindex 2f9472c1a0907..87c56e9555771 100644\n--- a/MAINTAINERS\n+++ b/MAINTAINERS\n@@ -6617,6 +6617,7 @@ F:\tblock/blk-cgroup.c\n F:\tblock/blk-iocost.c\n F:\tblock/blk-iolatency.c\n F:\tblock/blk-throttle.c\n+F:\tblock/bpf_blkcg.c\n F:\tinclude/linux/blk-cgroup.h\n \n CONTROL GROUP - CPUSET\ndiff --git a/block/Makefile b/block/Makefile\nindex e7bd320e3d697..572e49988c8ee 100644\n--- a/block/Makefile\n+++ b/block/Makefile\n@@ -17,6 +17,9 @@ obj-$(CONFIG_BLK_ERROR_INJECTION) += error-injection.o\n obj-$(CONFIG_BLK_DEV_BSG_COMMON) += bsg.o\n obj-$(CONFIG_BLK_DEV_BSGLIB)\t+= bsg-lib.o\n obj-$(CONFIG_BLK_CGROUP)\t+= blk-cgroup.o\n+ifdef CONFIG_BPF_SYSCALL\n+obj-$(CONFIG_BLK_CGROUP)\t+= bpf_blkcg.o\n+endif\n obj-$(CONFIG_BLK_CGROUP_RWSTAT)\t+= blk-cgroup-rwstat.o\n obj-$(CONFIG_BLK_CGROUP_FC_APPID) += blk-cgroup-fc-appid.o\n obj-$(CONFIG_BLK_DEV_THROTTLING)\t+= blk-throttle.o\ndiff --git a/block/blk-cgroup.c b/block/blk-cgroup.c\nindex d9676126c5b5d..8d538ad4e861c 100644\n--- a/block/blk-cgroup.c\n+++ b/block/blk-cgroup.c\n@@ -1086,7 +1086,7 @@ static void blkcg_rstat_flush(struct cgroup_subsys_state *css, int cpu)\n * flushing the root cgroup's stats by explicitly filling in the iostat\n * with disk level statistics.\n */\n-static void blkcg_fill_root_iostats(void)\n+void blkcg_fill_root_iostats(void)\n {\n \tstruct class_dev_iter iter;\n \tstruct device *dev;\ndiff --git a/block/blk-cgroup.h b/block/blk-cgroup.h\nindex 615390f751aa2..8c9c2a1adfaa4 100644\n--- a/block/blk-cgroup.h\n+++ b/block/blk-cgroup.h\n@@ -205,6 +205,7 @@ void blkcg_deactivate_policy(struct gendisk *disk,\n \t\t\t const struct blkcg_policy *pol);\n \n const char *blkg_dev_name(struct blkcg_gq *blkg);\n+void blkcg_fill_root_iostats(void);\n void blkcg_print_blkgs(struct seq_file *sf, struct blkcg *blkcg,\n \t\t u64 (*prfill)(struct seq_file *,\n \t\t\t\t struct blkg_policy_data *, int),\ndiff --git a/block/bpf_blkcg.c b/block/bpf_blkcg.c\nnew file mode 100644\nindex 0000000000000..48a86f07e198a\n--- /dev/null\n+++ b/block/bpf_blkcg.c\n@@ -0,0 +1,315 @@\n+// SPDX-License-Identifier: GPL-2.0-or-later\n+/*\n+ * Block I/O Controller-related BPF kfuncs and auxiliary code.\n+ *\n+ * These let a BPF program read a cgroup's io.stat counters. A program turns a\n+ * cgroup's css into a struct blkcg with bpf_get_blkcg(), flushes the stats with\n+ * bpf_blkcg_flush_stats(), then walks the cgroup's per-device blkgs with the\n+ * bpf_iter_blkg open-coded iterator, reading each device's counters with\n+ * bpf_blkg_iostat_bytes()/bpf_blkg_iostat_ios(). It mirrors the memory\n+ * controller kfuncs in mm/bpf_memcontrol.c, but adds a per-device dimension:\n+ * unlike memcg, blkcg keeps one blkg (and one io.stat line) per block device.\n+ *\n+ * This file lives in block/ because the blkcg/blkg struct layouts are private\n+ * to block/blk-cgroup.h.\n+ */\n+\n+#include \"blk-cgroup.h\"\n+\n+#include \u003clinux/bpf.h\u003e\n+#include \u003clinux/btf_ids.h\u003e\n+#include \u003clinux/preempt.h\u003e\n+#include \u003clinux/rculist.h\u003e\n+\n+__bpf_kfunc_start_defs();\n+\n+/**\n+ * bpf_get_root_blkcg - Returns a pointer to the root block cgroup\n+ *\n+ * The function has KF_ACQUIRE semantics, even though the root block cgroup is\n+ * never destroyed and doesn't require reference counting. It's safe to pass it\n+ * to bpf_put_blkcg().\n+ *\n+ * Note that the root cgroup is special: its counters are the disks' own\n+ * statistics, so they cover every cgroup's I/O rather than only the root's.\n+ * This matches what the root io.stat file prints.\n+ *\n+ * Return: A pointer to the root block cgroup.\n+ */\n+__bpf_kfunc struct blkcg *bpf_get_root_blkcg(void)\n+{\n+\t/* css_get() is not needed */\n+\treturn \u0026blkcg_root;\n+}\n+\n+/**\n+ * bpf_get_blkcg - Get a reference to a block cgroup\n+ * @css: pointer to the css structure\n+ *\n+ * It's fine to pass a css which belongs to any cgroup controller,\n+ * e.g. unified hierarchy's main css.\n+ *\n+ * Implements KF_ACQUIRE semantics.\n+ *\n+ * Return: A pointer to a blkcg structure after bumping the corresponding css's\n+ * reference counter, or NULL if the io controller is not enabled on the cgroup.\n+ */\n+__bpf_kfunc struct blkcg *bpf_get_blkcg(struct cgroup_subsys_state *css)\n+{\n+\tstruct blkcg *blkcg = NULL;\n+\n+\tif (css-\u003ess == \u0026io_cgrp_subsys)\n+\t\treturn css_tryget(css) ? css_to_blkcg(css) : NULL;\n+\n+\t/*\n+\t * Some other controller's css, or the cgroup's own one. Look up the io\n+\t * controller's css; rcu keeps it alive between the load and the tryget.\n+\t * Acquire and release rcu on one straight path, so that block/'s lock\n+\t * context analysis can follow it.\n+\t */\n+\trcu_read_lock();\n+\tcss = rcu_dereference_raw(css-\u003ecgroup-\u003esubsys[io_cgrp_id]);\n+\tif (css \u0026\u0026 css_tryget(css))\n+\t\tblkcg = css_to_blkcg(css);\n+\trcu_read_unlock();\n+\n+\treturn blkcg;\n+}\n+\n+/**\n+ * bpf_put_blkcg - Put a reference to a block cgroup\n+ * @blkcg: block cgroup to release\n+ *\n+ * Releases a previously acquired blkcg reference.\n+ * Implements KF_RELEASE semantics.\n+ */\n+__bpf_kfunc void bpf_put_blkcg(struct blkcg *blkcg)\n+{\n+\tcss_put(\u0026blkcg-\u003ecss);\n+}\n+\n+/**\n+ * bpf_blkcg_flush_stats - Flush a block cgroup's io statistics\n+ * @blkcg: block cgroup\n+ *\n+ * Call this before reading counters for up-to-date values. Sleepable.\n+ *\n+ * It does what reading the io.stat file does, which differs by cgroup. For a\n+ * non-root cgroup it folds the per-cpu deltas into the per-device aggregates\n+ * and up the cgroup tree. The root cgroup is not accounted through rstat at\n+ * all, so for it the per-device aggregates are refilled from the disks'\n+ * own statistics, which count every cgroup's I/O.\n+ *\n+ * The root branch is not self-limiting the way the rstat one is: it rereads\n+ * every disk on every call, while a second rstat flush finds nothing left to\n+ * fold. The numbers it produces are the same for every cgroup, so read them\n+ * once rather than once per cgroup of a walk.\n+ */\n+__bpf_kfunc void bpf_blkcg_flush_stats(struct blkcg *blkcg)\n+{\n+\tif (!blkcg-\u003ecss.parent)\n+\t\tblkcg_fill_root_iostats();\n+\telse\n+\t\tcss_rstat_flush(\u0026blkcg-\u003ecss);\n+}\n+\n+struct bpf_iter_blkg {\n+\t__u64 __opaque[2];\n+} __attribute__((aligned(8)));\n+\n+struct bpf_iter_blkg_kern {\n+\tstruct blkcg *blkcg;\n+\tstruct blkcg_gq *pos;\n+} __attribute__((aligned(8)));\n+\n+/**\n+ * bpf_iter_blkg_new - Start iterating a block cgroup's per-device blkgs\n+ * @it: iterator to initialize\n+ * @blkcg: block cgroup whose devices to walk\n+ *\n+ * Each yielded blkg holds one block device's counters, the same ones behind a\n+ * per-device line of the io.stat file. Offline blkgs are skipped, as the file\n+ * skips them. One case differs: the file prints no line for a blkg whose disk\n+ * is gone, while the walk still yields it, and bpf_blkg_dev() returns 0 for\n+ * it. Must be used inside an RCU read section.\n+ *\n+ * Return: 0 on success.\n+ */\n+__bpf_kfunc int bpf_iter_blkg_new(struct bpf_iter_blkg *it, struct blkcg *blkcg)\n+{\n+\tstruct bpf_iter_blkg_kern *kit = (void *)it;\n+\n+\tBUILD_BUG_ON(sizeof(struct bpf_iter_blkg_kern) \u003e sizeof(struct bpf_iter_blkg));\n+\tBUILD_BUG_ON(__alignof__(struct bpf_iter_blkg_kern) !=\n+\t\t __alignof__(struct bpf_iter_blkg));\n+\n+\tkit-\u003eblkcg = blkcg;\n+\tkit-\u003epos = NULL;\n+\treturn 0;\n+}\n+\n+/**\n+ * bpf_iter_blkg_next - Return the next online blkg of the iterated block cgroup\n+ * @it: iterator\n+ *\n+ * Return: the next online blkg, or NULL when the walk is done.\n+ */\n+__bpf_kfunc struct blkcg_gq *bpf_iter_blkg_next(struct bpf_iter_blkg *it)\n+{\n+\tstruct bpf_iter_blkg_kern *kit = (void *)it;\n+\tstruct blkcg_gq *blkg = kit-\u003epos;\n+\tstruct hlist_node *node;\n+\n+\t/* Cleared once the walk is done, see below. */\n+\tif (!kit-\u003eblkcg)\n+\t\treturn NULL;\n+\n+\tif (!blkg)\n+\t\tnode = rcu_dereference(hlist_first_rcu(\u0026kit-\u003eblkcg-\u003eblkg_list));\n+\telse\n+\t\tnode = rcu_dereference(hlist_next_rcu(\u0026blkg-\u003eblkcg_node));\n+\n+\t/* Skip offline blkgs, matching the io.stat file. */\n+\twhile (node) {\n+\t\tblkg = hlist_entry(node, struct blkcg_gq, blkcg_node);\n+\t\tif (blkg-\u003eonline) {\n+\t\t\tkit-\u003epos = blkg;\n+\t\t\treturn blkg;\n+\t\t}\n+\t\tnode = rcu_dereference(hlist_next_rcu(\u0026blkg-\u003eblkcg_node));\n+\t}\n+\n+\t/*\n+\t * Forget the list head as well. The verifier assumes that an iterator\n+\t * which returned NULL keeps returning NULL, and stops checking the\n+\t * loop for termination once it has; starting the walk over would let\n+\t * such a loop spin forever.\n+\t */\n+\tkit-\u003epos = NULL;\n+\tkit-\u003eblkcg = NULL;\n+\treturn NULL;\n+}\n+\n+/**\n+ * bpf_iter_blkg_destroy - Tear down a blkg iterator\n+ * @it: iterator\n+ */\n+__bpf_kfunc void bpf_iter_blkg_destroy(struct bpf_iter_blkg *it)\n+{\n+}\n+\n+/*\n+ * Read one counter out of @blkg's flushed io.stat aggregate. @counters is one\n+ * of the two arrays in blkg-\u003eiostat.cur; both are guarded by that struct's\n+ * seqlock, the one the io.stat file uses. Returns (u64)-1 if the counter\n+ * cannot be read.\n+ */\n+static u64 blkg_iostat_read(struct blkcg_gq *blkg, const u64 *counters,\n+\t\t\t enum blkg_iostat_type rw)\n+{\n+\tstruct blkg_iostat_set *bis = \u0026blkg-\u003eiostat;\n+\tunsigned int seq;\n+\tu64 val;\n+\n+\tif ((unsigned int)rw \u003e= BLKG_IOSTAT_NR)\n+\t\treturn (u64)-1;\n+\n+\t/*\n+\t * On 32-bit the loop below really is a seqcount retry loop. Every\n+\t * writer of blkg-\u003eiostat keeps interrupts off, so only an NMI can land\n+\t * inside an update, and then the loop would never end. These kfuncs\n+\t * are reachable from a perf event program, which does run in NMI, so\n+\t * give up rather than spin. On 64-bit the loop compiles away.\n+\t */\n+\tif (BITS_PER_LONG == 32 \u0026\u0026 in_nmi())\n+\t\treturn (u64)-1;\n+\n+\tdo {\n+\t\tseq = u64_stats_fetch_begin(\u0026bis-\u003esync);\n+\t\tval = counters[rw];\n+\t} while (u64_stats_fetch_retry(\u0026bis-\u003esync, seq));\n+\n+\treturn val;\n+}\n+\n+/**\n+ * bpf_blkg_iostat_bytes - Read a device's io.stat byte counter\n+ * @blkg: block group (one device of a block cgroup)\n+ * @rw: which counter (BLKG_IOSTAT_READ / _WRITE / _DISCARD)\n+ *\n+ * Reads the flushed aggregate, so call bpf_blkcg_flush_stats() first for\n+ * up-to-date values. The read uses the u64_stats seqlock, like the io.stat\n+ * file.\n+ *\n+ * Return: the number of bytes, or (u64)-1 if @rw is out of range or the\n+ * counter cannot be read.\n+ */\n+__bpf_kfunc u64 bpf_blkg_iostat_bytes(struct blkcg_gq *blkg,\n+\t\t\t\t enum blkg_iostat_type rw)\n+{\n+\treturn blkg_iostat_read(blkg, blkg-\u003eiostat.cur.bytes, rw);\n+}\n+\n+/**\n+ * bpf_blkg_iostat_ios - Read a device's io.stat I/O count\n+ * @blkg: block group (one device of a block cgroup)\n+ * @rw: which counter (BLKG_IOSTAT_READ / _WRITE / _DISCARD)\n+ *\n+ * Return: the number of I/Os, or (u64)-1 if @rw is out of range or the\n+ * counter cannot be read.\n+ */\n+__bpf_kfunc u64 bpf_blkg_iostat_ios(struct blkcg_gq *blkg,\n+\t\t\t\t enum blkg_iostat_type rw)\n+{\n+\treturn blkg_iostat_read(blkg, blkg-\u003eiostat.cur.ios, rw);\n+}\n+\n+/**\n+ * bpf_blkg_dev - Return a blkg's device id\n+ * @blkg: block group\n+ *\n+ * Return: the device's dev_t (use MAJOR()/MINOR() to split), or 0 if the blkg\n+ * has no disk.\n+ */\n+__bpf_kfunc u64 bpf_blkg_dev(struct blkcg_gq *blkg)\n+{\n+\tif (!blkg-\u003eq || !blkg-\u003eq-\u003edisk)\n+\t\treturn 0;\n+\n+\treturn blkg-\u003eq-\u003edisk-\u003epart0-\u003ebd_dev;\n+}\n+\n+__bpf_kfunc_end_defs();\n+\n+BTF_KFUNCS_START(bpf_blkcg_kfuncs)\n+BTF_ID_FLAGS(func, bpf_get_root_blkcg, KF_ACQUIRE | KF_RET_NULL)\n+BTF_ID_FLAGS(func, bpf_get_blkcg, KF_ACQUIRE | KF_RET_NULL | KF_RCU)\n+BTF_ID_FLAGS(func, bpf_put_blkcg, KF_RELEASE)\n+BTF_ID_FLAGS(func, bpf_blkcg_flush_stats, KF_SLEEPABLE)\n+\n+BTF_ID_FLAGS(func, bpf_iter_blkg_new, KF_ITER_NEW | KF_RCU_PROTECTED)\n+BTF_ID_FLAGS(func, bpf_iter_blkg_next, KF_ITER_NEXT | KF_RET_NULL)\n+BTF_ID_FLAGS(func, bpf_iter_blkg_destroy, KF_ITER_DESTROY)\n+\n+BTF_ID_FLAGS(func, bpf_blkg_iostat_bytes, KF_RCU)\n+BTF_ID_FLAGS(func, bpf_blkg_iostat_ios, KF_RCU)\n+BTF_ID_FLAGS(func, bpf_blkg_dev, KF_RCU)\n+BTF_KFUNCS_END(bpf_blkcg_kfuncs)\n+\n+static const struct btf_kfunc_id_set bpf_blkcg_kfunc_set = {\n+\t.owner\t\t= THIS_MODULE,\n+\t.set\t\t= \u0026bpf_blkcg_kfuncs,\n+};\n+\n+static int __init bpf_blkcg_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_blkcg_kfunc_set);\n+\tif (err)\n+\t\tpr_warn(\"error while registering bpf blkcg kfuncs: %d\\n\", err);\n+\n+\treturn err;\n+}\n+late_initcall(bpf_blkcg_init);\ndiff --git a/tools/testing/selftests/bpf/cgroup_iter_io.h b/tools/testing/selftests/bpf/cgroup_iter_io.h\nnew file mode 100644\nindex 0000000000000..f4bbaaccdf716\n--- /dev/null\n+++ b/tools/testing/selftests/bpf/cgroup_iter_io.h\n@@ -0,0 +1,17 @@\n+/* SPDX-License-Identifier: GPL-2.0 */\n+/* Copyright (c) 2025 Meta Platforms, Inc. and affiliates. */\n+#ifndef __CGROUP_ITER_IO_H\n+#define __CGROUP_ITER_IO_H\n+\n+struct io_query {\n+\t/* one device's io.stat counters */\n+\t__u64 rbytes;\n+\t__u64 wbytes;\n+\t__u64 rios;\n+\t__u64 wios;\n+\t__u64 dbytes;\n+\t__u64 dios;\n+\t__u64 dev;\t/* dev_t of the device the counters belong to */\n+};\n+\n+#endif /* __CGROUP_ITER_IO_H */\ndiff --git a/tools/testing/selftests/bpf/config b/tools/testing/selftests/bpf/config\nindex ea7044f30adc3..270e6bf9194d2 100644\n--- a/tools/testing/selftests/bpf/config\n+++ b/tools/testing/selftests/bpf/config\n@@ -1,3 +1,4 @@\n+CONFIG_BLK_CGROUP=y\n CONFIG_BLK_DEV_LOOP=y\n CONFIG_BOOTPARAM_HARDLOCKUP_PANIC=y\n CONFIG_BOOTPARAM_SOFTLOCKUP_PANIC=1\ndiff --git a/tools/testing/selftests/bpf/prog_tests/cgroup_iter_io.c b/tools/testing/selftests/bpf/prog_tests/cgroup_iter_io.c\nnew file mode 100644\nindex 0000000000000..32cda82433183\n--- /dev/null\n+++ b/tools/testing/selftests/bpf/prog_tests/cgroup_iter_io.c\n@@ -0,0 +1,310 @@\n+// SPDX-License-Identifier: GPL-2.0\n+/* Copyright (c) 2025 Meta Platforms, Inc. and affiliates. */\n+#define _GNU_SOURCE\n+#include \u003ctest_progs.h\u003e\n+#include \u003cbpf/libbpf.h\u003e\n+#include \u003cfcntl.h\u003e\n+#include \u003clinux/loop.h\u003e\n+#include \u003cstdlib.h\u003e\n+#include \u003cstring.h\u003e\n+#include \u003csys/ioctl.h\u003e\n+#include \u003csys/stat.h\u003e\n+#include \u003csys/sysmacros.h\u003e\n+#include \u003cunistd.h\u003e\n+#include \"cgroup_helpers.h\"\n+#include \"cgroup_iter_io.h\"\n+#include \"cgroup_iter_io.skel.h\"\n+\n+#define IO_SIZE (4 * 1024 * 1024)\n+\n+static int read_stats(struct bpf_link *link)\n+{\n+\tint fd, ret = 0;\n+\tssize_t bytes;\n+\n+\tfd = bpf_iter_create(bpf_link__fd(link));\n+\tif (!ASSERT_OK_FD(fd, \"bpf_iter_create\"))\n+\t\treturn 1;\n+\n+\t/* Results land in skel-\u003edata_query; the read itself returns no data. */\n+\tbytes = read(fd, NULL, 0);\n+\tif (!ASSERT_EQ(bytes, 0, \"read fd\"))\n+\t\tret = 1;\n+\n+\tclose(fd);\n+\treturn ret;\n+}\n+\n+/*\n+ * Attach a loop device to an anonymous temp file so we have a real block\n+ * device to generate cgroup-charged I/O against. Returns 0 on success, or -1\n+ * if loop devices are unavailable (non-root / no CONFIG_BLK_DEV_LOOP) so the\n+ * caller can skip.\n+ */\n+static int loop_setup(char *loop_path, size_t sz, int *ctl_fd, int *loop_fd,\n+\t\t int *back_fd)\n+{\n+\tchar back_path[] = \"/tmp/cgroup_iter_io.XXXXXX\";\n+\tint nr;\n+\n+\t*ctl_fd = *loop_fd = *back_fd = -1;\n+\n+\t*ctl_fd = open(\"/dev/loop-control\", O_RDWR | O_CLOEXEC);\n+\tif (*ctl_fd \u003c 0)\n+\t\treturn -1;\n+\n+\tnr = ioctl(*ctl_fd, LOOP_CTL_GET_FREE);\n+\tif (nr \u003c 0)\n+\t\tgoto err;\n+\tsnprintf(loop_path, sz, \"/dev/loop%d\", nr);\n+\n+\t*back_fd = mkstemp(back_path);\n+\tif (*back_fd \u003c 0)\n+\t\tgoto err;\n+\tunlink(back_path);\n+\tif (ftruncate(*back_fd, (off_t)IO_SIZE * 4))\n+\t\tgoto err;\n+\n+\t*loop_fd = open(loop_path, O_RDWR | O_CLOEXEC);\n+\tif (*loop_fd \u003c 0)\n+\t\tgoto err;\n+\tif (ioctl(*loop_fd, LOOP_SET_FD, *back_fd))\n+\t\tgoto err;\n+\n+\treturn 0;\n+err:\n+\tif (*loop_fd \u003e= 0)\n+\t\tclose(*loop_fd);\n+\tif (*back_fd \u003e= 0)\n+\t\tclose(*back_fd);\n+\tclose(*ctl_fd);\n+\t*ctl_fd = *loop_fd = *back_fd = -1;\n+\treturn -1;\n+}\n+\n+static void loop_teardown(const char *loop_path, int ctl_fd, int loop_fd,\n+\t\t\t int back_fd)\n+{\n+\tint nr = -1;\n+\n+\tif (loop_fd \u003e= 0) {\n+\t\tioctl(loop_fd, LOOP_CLR_FD, 0);\n+\t\tclose(loop_fd);\n+\t}\n+\tif (back_fd \u003e= 0)\n+\t\tclose(back_fd);\n+\tif (ctl_fd \u003e= 0) {\n+\t\tif (sscanf(loop_path, \"/dev/loop%d\", \u0026nr) == 1 \u0026\u0026 nr \u003e= 0)\n+\t\t\tioctl(ctl_fd, LOOP_CTL_REMOVE, nr);\n+\t\tclose(ctl_fd);\n+\t}\n+}\n+\n+/* O_DIRECT I/O to the loop device, charged to the current cgroup. */\n+static int do_direct_io(const char *loop_path)\n+{\n+\tvoid *buf;\n+\tint fd, ret = -1;\n+\n+\tfd = open(loop_path, O_RDWR | O_DIRECT | O_CLOEXEC);\n+\tif (fd \u003c 0)\n+\t\treturn -1;\n+\tif (posix_memalign(\u0026buf, 4096, IO_SIZE))\n+\t\tgoto out_fd;\n+\tmemset(buf, 0xab, IO_SIZE);\n+\n+\tif (pwrite(fd, buf, IO_SIZE, 0) != IO_SIZE)\n+\t\tgoto out_buf;\n+\tfsync(fd);\n+\tif (pread(fd, buf, IO_SIZE, 0) != IO_SIZE)\n+\t\tgoto out_buf;\n+\tret = 0;\n+out_buf:\n+\tfree(buf);\n+out_fd:\n+\tclose(fd);\n+\treturn ret;\n+}\n+\n+/*\n+ * Parse the io.stat line for device @dev out of the cgroup's io.stat file and\n+ * fill @out. @dev is a kernel dev_t (as returned by bpf_blkg_dev), whose\n+ * major:minor split matches how io.stat prints the device. Returns 0 if the\n+ * device's line was found.\n+ */\n+static int parse_io_stat(int cgroup_fd, __u64 dev, struct io_query *out)\n+{\n+\tunsigned int want_maj = dev \u003e\u003e 20, want_min = dev \u0026 ((1U \u003c\u003c 20) - 1);\n+\tchar buf[4096], *line, *saveptr;\n+\tint fd, n, ret = -1;\n+\n+\tfd = openat(cgroup_fd, \"io.stat\", O_RDONLY);\n+\tif (fd \u003c 0)\n+\t\treturn -1;\n+\tn = read(fd, buf, sizeof(buf) - 1);\n+\tclose(fd);\n+\tif (n \u003c= 0)\n+\t\treturn -1;\n+\tbuf[n] = '\\0';\n+\n+\tfor (line = strtok_r(buf, \"\\n\", \u0026saveptr); line;\n+\t line = strtok_r(NULL, \"\\n\", \u0026saveptr)) {\n+\t\tunsigned long long rb = 0, wb = 0, ri = 0, wi = 0, db = 0, di = 0;\n+\t\tunsigned int maj, min;\n+\n+\t\t/*\n+\t\t * The \"maj:min\" token is always present; the field block is\n+\t\t * optional (the kernel omits it for a device with no read/write\n+\t\t * I/O), so a match of \u003e= 2 is enough and absent fields stay 0.\n+\t\t */\n+\t\tif (sscanf(line,\n+\t\t\t \"%u:%u rbytes=%llu wbytes=%llu rios=%llu wios=%llu dbytes=%llu dios=%llu\",\n+\t\t\t \u0026maj, \u0026min, \u0026rb, \u0026wb, \u0026ri, \u0026wi, \u0026db, \u0026di) \u003c 2)\n+\t\t\tcontinue;\n+\t\tif (maj != want_maj || min != want_min)\n+\t\t\tcontinue;\n+\n+\t\tout-\u003erbytes = rb;\n+\t\tout-\u003ewbytes = wb;\n+\t\tout-\u003erios = ri;\n+\t\tout-\u003ewios = wi;\n+\t\tout-\u003edbytes = db;\n+\t\tout-\u003edios = di;\n+\t\tret = 0;\n+\t\tbreak;\n+\t}\n+\treturn ret;\n+}\n+\n+void test_cgroup_iter_io(void)\n+{\n+\tchar *cgroup_rel_path = \"/cgroup_iter_io_test\";\n+\tint ctl_fd = -1, loop_fd = -1, back_fd = -1;\n+\tstruct cgroup_iter_io *skel = NULL;\n+\tstruct bpf_link *link = NULL;\n+\tchar loop_path[64];\n+\tstruct io_query *q;\n+\tint cgroup_fd;\n+\n+\tcgroup_fd = cgroup_setup_and_join(cgroup_rel_path);\n+\tif (!ASSERT_OK_FD(cgroup_fd, \"cgroup_setup_and_join\"))\n+\t\treturn;\n+\n+\tif (loop_setup(loop_path, sizeof(loop_path), \u0026ctl_fd, \u0026loop_fd, \u0026back_fd)) {\n+\t\ttest__skip();\t/* needs root + CONFIG_BLK_DEV_LOOP */\n+\t\tgoto cleanup_cgroup_fd;\n+\t}\n+\n+\tskel = cgroup_iter_io__open_and_load();\n+\tif (!ASSERT_OK_PTR(skel, \"cgroup_iter_io__open_and_load\"))\n+\t\tgoto cleanup_loop;\n+\n+\t/*\n+\t * Pin the read to the loop device so the measured device is stable and\n+\t * quiesced. Convert the glibc-encoded st_rdev to the kernel dev_t\n+\t * encoding (major \u003c\u003c 20 | minor) that bpf_blkg_dev returns.\n+\t */\n+\t{\n+\t\tstruct stat lst;\n+\n+\t\tif (!ASSERT_OK(fstat(loop_fd, \u0026lst), \"fstat loop\"))\n+\t\t\tgoto cleanup_skel;\n+\t\tskel-\u003edata_query-\u003etarget_dev =\n+\t\t\t((__u64)major(lst.st_rdev) \u003c\u003c 20) | minor(lst.st_rdev);\n+\t}\n+\n+\tDECLARE_LIBBPF_OPTS(bpf_iter_attach_opts, opts);\n+\tunion bpf_iter_link_info linfo = {\n+\t\t.cgroup.cgroup_fd = cgroup_fd,\n+\t\t.cgroup.order = BPF_CGROUP_ITER_SELF_ONLY,\n+\t};\n+\topts.link_info = \u0026linfo;\n+\topts.link_info_len = sizeof(linfo);\n+\n+\tlink = bpf_program__attach_iter(skel-\u003eprogs.cgroup_io_query, \u0026opts);\n+\tif (!ASSERT_OK_PTR(link, \"bpf_program__attach_iter\"))\n+\t\tgoto cleanup_skel;\n+\n+\t/* This process is in the test cgroup, so the loop I/O is charged here. */\n+\tif (!ASSERT_OK(do_direct_io(loop_path), \"do_direct_io\"))\n+\t\tgoto cleanup_link;\n+\n+\tif (!ASSERT_OK(read_stats(link), \"read stats\"))\n+\t\tgoto cleanup_link;\n+\n+\t/*\n+\t * Weak check: we did I/O, so the numbers must be non-zero. Follows the\n+\t * pattern in cgroup_iter_memcg.\n+\t */\n+\tq = \u0026skel-\u003edata_query-\u003eio_query;\n+\tif (test__start_subtest(\"cgroup_iter_io__write\")) {\n+\t\tASSERT_GT(q-\u003ewbytes, 0, \"wbytes\");\n+\t\tASSERT_GT(q-\u003ewios, 0, \"wios\");\n+\t}\n+\tif (test__start_subtest(\"cgroup_iter_io__read\")) {\n+\t\tASSERT_GT(q-\u003erbytes, 0, \"rbytes\");\n+\t\tASSERT_GT(q-\u003erios, 0, \"rios\");\n+\t}\n+\tif (test__start_subtest(\"cgroup_iter_io__dev\"))\n+\t\tASSERT_GT(q-\u003edev, 0, \"dev\");\n+\n+\t/*\n+\t * Stronger check: the kfunc-read values must equal what the io.stat\n+\t * file reports for the same device. Refresh via the prog, then read\n+\t * the file with no I/O in between, so both flushed snapshots match\n+\t * exactly.\n+\t */\n+\tif (test__start_subtest(\"cgroup_iter_io__match\")) {\n+\t\tstruct io_query filev = {};\n+\n+\t\tif (ASSERT_OK(read_stats(link), \"read stats\") \u0026\u0026\n+\t\t ASSERT_OK(parse_io_stat(cgroup_fd, q-\u003edev, \u0026filev),\n+\t\t\t \"parse io.stat\")) {\n+\t\t\tASSERT_EQ(q-\u003erbytes, filev.rbytes, \"rbytes\");\n+\t\t\tASSERT_EQ(q-\u003ewbytes, filev.wbytes, \"wbytes\");\n+\t\t\tASSERT_EQ(q-\u003erios, filev.rios, \"rios\");\n+\t\t\tASSERT_EQ(q-\u003ewios, filev.wios, \"wios\");\n+\t\t\tASSERT_EQ(q-\u003edbytes, filev.dbytes, \"dbytes\");\n+\t\t\tASSERT_EQ(q-\u003edios, filev.dios, \"dios\");\n+\t\t}\n+\t}\n+\n+\t/*\n+\t * Separate program for the root block cgroup. Its counters do not come\n+\t * from rstat, they are refilled from the disks themselves, so this\n+\t * covers the other half of bpf_blkcg_flush_stats(). They cover every\n+\t * cgroup's I/O to the loop device, and only this test touches it, so\n+\t * they must be at or above what the test cgroup was charged.\n+\t */\n+\tif (test__start_subtest(\"cgroup_iter_io__root\")) {\n+\t\tstruct bpf_link *root_link;\n+\t\tstruct io_query *r;\n+\n+\t\tskel-\u003edata_query-\u003egot_root_blkcg = 0;\n+\t\troot_link = bpf_program__attach_iter(skel-\u003eprogs.cgroup_root_blkcg_query,\n+\t\t\t\t\t\t \u0026opts);\n+\t\tif (ASSERT_OK_PTR(root_link, \"attach root iter\")) {\n+\t\t\tif (ASSERT_OK(read_stats(root_link), \"read root stats\")) {\n+\t\t\t\tr = \u0026skel-\u003edata_query-\u003eroot_query;\n+\t\t\t\tASSERT_EQ(skel-\u003edata_query-\u003egot_root_blkcg, 1,\n+\t\t\t\t\t \"got_root_blkcg\");\n+\t\t\t\tASSERT_EQ(r-\u003edev, q-\u003edev, \"root dev\");\n+\t\t\t\tASSERT_GE(r-\u003ewbytes, q-\u003ewbytes, \"root wbytes\");\n+\t\t\t\tASSERT_GE(r-\u003ewios, q-\u003ewios, \"root wios\");\n+\t\t\t\tASSERT_GE(r-\u003erbytes, q-\u003erbytes, \"root rbytes\");\n+\t\t\t\tASSERT_GE(r-\u003erios, q-\u003erios, \"root rios\");\n+\t\t\t}\n+\t\t\tbpf_link__destroy(root_link);\n+\t\t}\n+\t}\n+\n+cleanup_link:\n+\tbpf_link__destroy(link);\n+cleanup_skel:\n+\tcgroup_iter_io__destroy(skel);\n+cleanup_loop:\n+\tloop_teardown(loop_path, ctl_fd, loop_fd, back_fd);\n+cleanup_cgroup_fd:\n+\tclose(cgroup_fd);\n+\tcleanup_cgroup_environment();\n+}\ndiff --git a/tools/testing/selftests/bpf/progs/cgroup_iter_io.c b/tools/testing/selftests/bpf/progs/cgroup_iter_io.c\nnew file mode 100644\nindex 0000000000000..b839def945086\n--- /dev/null\n+++ b/tools/testing/selftests/bpf/progs/cgroup_iter_io.c\n@@ -0,0 +1,107 @@\n+// SPDX-License-Identifier: GPL-2.0\n+/* Copyright (c) 2025 Meta Platforms, Inc. and affiliates. */\n+#include \u003cvmlinux.h\u003e\n+#include \u003cbpf/bpf_helpers.h\u003e\n+#include \u003cbpf/bpf_core_read.h\u003e\n+#include \"bpf_experimental.h\"\n+#include \"cgroup_iter_io.h\"\n+\n+char _license[] SEC(\"license\") = \"GPL\";\n+\n+/* The counters of the device named by target_dev are stored here. */\n+struct io_query io_query SEC(\".data.query\");\n+\n+/* The same device's counters read through the root block cgroup. */\n+struct io_query root_query SEC(\".data.query\");\n+\n+/* Set to 1 by cgroup_root_blkcg_query when bpf_get_root_blkcg() succeeds. */\n+__u64 got_root_blkcg SEC(\".data.query\");\n+\n+/* Device to read, set by userspace (kernel dev_t). Pinning the device keeps\n+ * the read deterministic and lets the value be compared to io.stat exactly.\n+ */\n+__u64 target_dev SEC(\".data.query\");\n+\n+/*\n+ * Flush @blkcg and copy the target device's counters into @out. Reading only\n+ * the one pinned device keeps the result deterministic: that device is\n+ * quiesced, so its counters match io.stat exactly, while picking \"any device\n+ * with I/O\" would race with backing-store writeback.\n+ */\n+static __always_inline void read_target_dev(struct blkcg *blkcg,\n+\t\t\t\t\t struct io_query *out)\n+{\n+\tstruct blkcg_gq *pos;\n+\n+\t/* io.stat needs a flush before it can be read (sleepable). */\n+\tbpf_blkcg_flush_stats(blkcg);\n+\n+\t/* The per-device blkg walk needs an RCU section. */\n+\tbpf_rcu_read_lock();\n+\tbpf_for_each(blkg, pos, blkcg) {\n+\t\tif (bpf_blkg_dev(pos) != target_dev)\n+\t\t\tcontinue;\n+\n+\t\tout-\u003edev = bpf_blkg_dev(pos);\n+\t\tout-\u003erbytes = bpf_blkg_iostat_bytes(pos, BLKG_IOSTAT_READ);\n+\t\tout-\u003ewbytes = bpf_blkg_iostat_bytes(pos, BLKG_IOSTAT_WRITE);\n+\t\tout-\u003erios = bpf_blkg_iostat_ios(pos, BLKG_IOSTAT_READ);\n+\t\tout-\u003ewios = bpf_blkg_iostat_ios(pos, BLKG_IOSTAT_WRITE);\n+\t\tout-\u003edbytes = bpf_blkg_iostat_bytes(pos, BLKG_IOSTAT_DISCARD);\n+\t\tout-\u003edios = bpf_blkg_iostat_ios(pos, BLKG_IOSTAT_DISCARD);\n+\t\tbreak;\n+\t}\n+\tbpf_rcu_read_unlock();\n+}\n+\n+SEC(\"iter.s/cgroup\")\n+int cgroup_io_query(struct bpf_iter__cgroup *ctx)\n+{\n+\tstruct cgroup *cgrp = ctx-\u003ecgroup;\n+\tstruct blkcg *blkcg;\n+\n+\t/* The last iteration has a NULL cgroup, skip it. */\n+\tif (!cgrp)\n+\t\treturn 1;\n+\n+\t/* Start fresh so a device that is not found stays all-zero. */\n+\t__builtin_memset(\u0026io_query, 0, sizeof(io_query));\n+\n+\tblkcg = bpf_get_blkcg(\u0026cgrp-\u003eself);\n+\tif (!blkcg)\n+\t\treturn 0;\n+\n+\tread_target_dev(blkcg, \u0026io_query);\n+\n+\tbpf_put_blkcg(blkcg);\n+\treturn 0;\n+}\n+\n+SEC(\"iter.s/cgroup\")\n+int cgroup_root_blkcg_query(struct bpf_iter__cgroup *ctx)\n+{\n+\tstruct cgroup *cgrp = ctx-\u003ecgroup;\n+\tstruct blkcg *blkcg;\n+\n+\t/* The last iteration has a NULL cgroup, skip it. */\n+\tif (!cgrp)\n+\t\treturn 1;\n+\n+\t__builtin_memset(\u0026root_query, 0, sizeof(root_query));\n+\n+\tblkcg = bpf_get_root_blkcg();\n+\tif (!blkcg)\n+\t\treturn 0;\n+\n+\t/*\n+\t * The root cgroup takes its numbers from the disks themselves rather\n+\t * than from rstat, so this also covers the root side of\n+\t * bpf_blkcg_flush_stats(). The counters cover every cgroup's I/O, so\n+\t * they can only be at or above what this test's own cgroup did.\n+\t */\n+\tread_target_dev(blkcg, \u0026root_query);\n+\n+\tgot_root_blkcg = 1;\n+\tbpf_put_blkcg(blkcg);\n+\treturn 0;\n+}\n"
} |
| 2/1 | 2026/08/07 21:03 | agent | patch-evaluator |
0mModel:best-expensive Args: null Results: {
"EnableConfigs": [
"BPF_SYSCALL",
"BLK_CGROUP"
],
"FocusSymbols": [
"bpf_get_root_blkcg",
"bpf_get_blkcg",
"bpf_put_blkcg",
"bpf_blkcg_flush_stats",
"bpf_iter_blkg_new",
"bpf_iter_blkg_next",
"bpf_iter_blkg_destroy",
"bpf_blkg_iostat_bytes",
"bpf_blkg_iostat_ios",
"bpf_blkg_dev",
"blkcg_fill_root_iostats"
],
"Reasoning": "The patch introduces new BPF kfuncs for the block I/O controller (blkcg) to allow BPF programs to read a cgroup's io.stat counters. These kfuncs are reachable from BPF programs and should be fuzzed to ensure they handle various inputs and concurrent executions safely.",
"WorthFuzzing": true
}Instruction: You are an expert Linux kernel maintainer. Your job is to review a provided patch series and determine if it makes functional changes to the kernel that should be fuzzed. IMPORTANT: The changes have ALREADY been applied and committed as the HEAD commit in your workspace. Do NOT rely on your internal knowledge of the kernel. You must actively use your code access tools to examine the actual source code and confirm any assumptions. Return WorthFuzzing=false if the patch only contains: - Modifications to Documentation/, Kconfig files, or code comments. - Purely decorative changes, such as logging (e.g., pr_err, printk) or tracepoints. - Changes to numeric constants or macros that do not functionally alter execution flow. - Code paths that are impossible to reach in virtualized environments like GCE or QEMU, even when utilizing software-emulated hardware (e.g., usb gadget, mac80211_hwsim). - Code in vendor-specific PCIe switch, SmartNIC, or GPU drivers (e.g., mlxsw, pds_core, qed, ionic, amdgpu) that require physical PCIe hardware cards not emulated in standard QEMU. - Driver .remove, .shutdown, or pci_unregister_driver teardown callbacks (e.g., igb_remove) that are executed only during PCI hot-unplug or sysfs driver unbind operations. If it modifies reachable core kernel logic, drivers, or architectures, use your code search tools to verify the code can be executed, then return WorthFuzzing=true. When returning WorthFuzzing=true, you MUST ALSO: 1. Extract any specific kernel functions that should be heavily fuzzed into FocusSymbols. Avoid listing generic hot-path functions to prevent skewed test distributions. Prefer non-static, non-inlined API entrypoint functions over internal static helper functions (which are inlined by the compiler and do not have distinct symbol addresses). 2. Identify any specific CONFIG_ options required to properly test this new/modified feature. Go and look into the Kconfig files and check for ifdefs around the code, do not make assumptions. Also check "depends on" lines in Kconfig to include any non-standard parent subsystem configs needed for Kbuild to compile the code statically into vmlinux. List them in the EnableConfigs output array, and DO NOT add a 'CONFIG_' prefix (e.g., return "NET_IPV4" instead of "CONFIG_NET_IPV4"). Prefer calling several tools at the same time to save round-trips. Use set-results tool to provide results of the analysis. It must be called exactly once before the final reply. Ignore results of this tool. Prompt: For your convenience, here is the diff of the changes:
commit 2ca19a8a248872888de1a5be7d67838a2f2a4217
Author: syz-cluster <triage@syzkaller.com>
Date: Fri Aug 7 21:03:03 2026 +0000
syz-cluster: applied patch under review
diff --git a/MAINTAINERS b/MAINTAINERS
index 2f9472c1a0907..87c56e9555771 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -6617,6 +6617,7 @@ F: block/blk-cgroup.c
F: block/blk-iocost.c
F: block/blk-iolatency.c
F: block/blk-throttle.c
+F: block/bpf_blkcg.c
F: include/linux/blk-cgroup.h
CONTROL GROUP - CPUSET
diff --git a/block/Makefile b/block/Makefile
index e7bd320e3d697..572e49988c8ee 100644
--- a/block/Makefile
+++ b/block/Makefile
@@ -17,6 +17,9 @@ obj-$(CONFIG_BLK_ERROR_INJECTION) += error-injection.o
obj-$(CONFIG_BLK_DEV_BSG_COMMON) += bsg.o
obj-$(CONFIG_BLK_DEV_BSGLIB) += bsg-lib.o
obj-$(CONFIG_BLK_CGROUP) += blk-cgroup.o
+ifdef CONFIG_BPF_SYSCALL
+obj-$(CONFIG_BLK_CGROUP) += bpf_blkcg.o
+endif
obj-$(CONFIG_BLK_CGROUP_RWSTAT) += blk-cgroup-rwstat.o
obj-$(CONFIG_BLK_CGROUP_FC_APPID) += blk-cgroup-fc-appid.o
obj-$(CONFIG_BLK_DEV_THROTTLING) += blk-throttle.o
diff --git a/block/blk-cgroup.c b/block/blk-cgroup.c
index d9676126c5b5d..8d538ad4e861c 100644
--- a/block/blk-cgroup.c
+++ b/block/blk-cgroup.c
@@ -1086,7 +1086,7 @@ static void blkcg_rstat_flush(struct cgroup_subsys_state *css, int cpu)
* flushing the root cgroup's stats by explicitly filling in the iostat
* with disk level statistics.
*/
-static void blkcg_fill_root_iostats(void)
+void blkcg_fill_root_iostats(void)
{
struct class_dev_iter iter;
struct device *dev;
diff --git a/block/blk-cgroup.h b/block/blk-cgroup.h
index 615390f751aa2..8c9c2a1adfaa4 100644
--- a/block/blk-cgroup.h
+++ b/block/blk-cgroup.h
@@ -205,6 +205,7 @@ void blkcg_deactivate_policy(struct gendisk *disk,
const struct blkcg_policy *pol);
const char *blkg_dev_name(struct blkcg_gq *blkg);
+void blkcg_fill_root_iostats(void);
void blkcg_print_blkgs(struct seq_file *sf, struct blkcg *blkcg,
u64 (*prfill)(struct seq_file *,
struct blkg_policy_data *, int),
diff --git a/block/bpf_blkcg.c b/block/bpf_blkcg.c
new file mode 100644
index 0000000000000..48a86f07e198a
--- /dev/null
+++ b/block/bpf_blkcg.c
@@ -0,0 +1,315 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Block I/O Controller-related BPF kfuncs and auxiliary code.
+ *
+ * These let a BPF program read a cgroup's io.stat counters. A program turns a
+ * cgroup's css into a struct blkcg with bpf_get_blkcg(), flushes the stats with
+ * bpf_blkcg_flush_stats(), then walks the cgroup's per-device blkgs with the
+ * bpf_iter_blkg open-coded iterator, reading each device's counters with
+ * bpf_blkg_iostat_bytes()/bpf_blkg_iostat_ios(). It mirrors the memory
+ * controller kfuncs in mm/bpf_memcontrol.c, but adds a per-device dimension:
+ * unlike memcg, blkcg keeps one blkg (and one io.stat line) per block device.
+ *
+ * This file lives in block/ because the blkcg/blkg struct layouts are private
+ * to block/blk-cgroup.h.
+ */
+
+#include "blk-cgroup.h"
+
+#include <linux/bpf.h>
+#include <linux/btf_ids.h>
+#include <linux/preempt.h>
+#include <linux/rculist.h>
+
+__bpf_kfunc_start_defs();
+
+/**
+ * bpf_get_root_blkcg - Returns a pointer to the root block cgroup
+ *
+ * The function has KF_ACQUIRE semantics, even though the root block cgroup is
+ * never destroyed and doesn't require reference counting. It's safe to pass it
+ * to bpf_put_blkcg().
+ *
+ * Note that the root cgroup is special: its counters are the disks' own
+ * statistics, so they cover every cgroup's I/O rather than only the root's.
+ * This matches what the root io.stat file prints.
+ *
+ * Return: A pointer to the root block cgroup.
+ */
+__bpf_kfunc struct blkcg *bpf_get_root_blkcg(void)
+{
+ /* css_get() is not needed */
+ return &blkcg_root;
+}
+
+/**
+ * bpf_get_blkcg - Get a reference to a block cgroup
+ * @css: pointer to the css structure
+ *
+ * It's fine to pass a css which belongs to any cgroup controller,
+ * e.g. unified hierarchy's main css.
+ *
+ * Implements KF_ACQUIRE semantics.
+ *
+ * Return: A pointer to a blkcg structure after bumping the corresponding css's
+ * reference counter, or NULL if the io controller is not enabled on the cgroup.
+ */
+__bpf_kfunc struct blkcg *bpf_get_blkcg(struct cgroup_subsys_state *css)
+{
+ struct blkcg *blkcg = NULL;
+
+ if (css->ss == &io_cgrp_subsys)
+ return css_tryget(css) ? css_to_blkcg(css) : NULL;
+
+ /*
+ * Some other controller's css, or the cgroup's own one. Look up the io
+ * controller's css; rcu keeps it alive between the load and the tryget.
+ * Acquire and release rcu on one straight path, so that block/'s lock
+ * context analysis can follow it.
+ */
+ rcu_read_lock();
+ css = rcu_dereference_raw(css->cgroup->subsys[io_cgrp_id]);
+ if (css && css_tryget(css))
+ blkcg = css_to_blkcg(css);
+ rcu_read_unlock();
+
+ return blkcg;
+}
+
+/**
+ * bpf_put_blkcg - Put a reference to a block cgroup
+ * @blkcg: block cgroup to release
+ *
+ * Releases a previously acquired blkcg reference.
+ * Implements KF_RELEASE semantics.
+ */
+__bpf_kfunc void bpf_put_blkcg(struct blkcg *blkcg)
+{
+ css_put(&blkcg->css);
+}
+
+/**
+ * bpf_blkcg_flush_stats - Flush a block cgroup's io statistics
+ * @blkcg: block cgroup
+ *
+ * Call this before reading counters for up-to-date values. Sleepable.
+ *
+ * It does what reading the io.stat file does, which differs by cgroup. For a
+ * non-root cgroup it folds the per-cpu deltas into the per-device aggregates
+ * and up the cgroup tree. The root cgroup is not accounted through rstat at
+ * all, so for it the per-device aggregates are refilled from the disks'
+ * own statistics, which count every cgroup's I/O.
+ *
+ * The root branch is not self-limiting the way the rstat one is: it rereads
+ * every disk on every call, while a second rstat flush finds nothing left to
+ * fold. The numbers it produces are the same for every cgroup, so read them
+ * once rather than once per cgroup of a walk.
+ */
+__bpf_kfunc void bpf_blkcg_flush_stats(struct blkcg *blkcg)
+{
+ if (!blkcg->css.parent)
+ blkcg_fill_root_iostats();
+ else
+ css_rstat_flush(&blkcg->css);
+}
+
+struct bpf_iter_blkg {
+ __u64 __opaque[2];
+} __attribute__((aligned(8)));
+
+struct bpf_iter_blkg_kern {
+ struct blkcg *blkcg;
+ struct blkcg_gq *pos;
+} __attribute__((aligned(8)));
+
+/**
+ * bpf_iter_blkg_new - Start iterating a block cgroup's per-device blkgs
+ * @it: iterator to initialize
+ * @blkcg: block cgroup whose devices to walk
+ *
+ * Each yielded blkg holds one block device's counters, the same ones behind a
+ * per-device line of the io.stat file. Offline blkgs are skipped, as the file
+ * skips them. One case differs: the file prints no line for a blkg whose disk
+ * is gone, while the walk still yields it, and bpf_blkg_dev() returns 0 for
+ * it. Must be used inside an RCU read section.
+ *
+ * Return: 0 on success.
+ */
+__bpf_kfunc int bpf_iter_blkg_new(struct bpf_iter_blkg *it, struct blkcg *blkcg)
+{
+ struct bpf_iter_blkg_kern *kit = (void *)it;
+
+ BUILD_BUG_ON(sizeof(struct bpf_iter_blkg_kern) > sizeof(struct bpf_iter_blkg));
+ BUILD_BUG_ON(__alignof__(struct bpf_iter_blkg_kern) !=
+ __alignof__(struct bpf_iter_blkg));
+
+ kit->blkcg = blkcg;
+ kit->pos = NULL;
+ return 0;
+}
+
+/**
+ * bpf_iter_blkg_next - Return the next online blkg of the iterated block cgroup
+ * @it: iterator
+ *
+ * Return: the next online blkg, or NULL when the walk is done.
+ */
+__bpf_kfunc struct blkcg_gq *bpf_iter_blkg_next(struct bpf_iter_blkg *it)
+{
+ struct bpf_iter_blkg_kern *kit = (void *)it;
+ struct blkcg_gq *blkg = kit->pos;
+ struct hlist_node *node;
+
+ /* Cleared once the walk is done, see below. */
+ if (!kit->blkcg)
+ return NULL;
+
+ if (!blkg)
+ node = rcu_dereference(hlist_first_rcu(&kit->blkcg->blkg_list));
+ else
+ node = rcu_dereference(hlist_next_rcu(&blkg->blkcg_node));
+
+ /* Skip offline blkgs, matching the io.stat file. */
+ while (node) {
+ blkg = hlist_entry(node, struct blkcg_gq, blkcg_node);
+ if (blkg->online) {
+ kit->pos = blkg;
+ return blkg;
+ }
+ node = rcu_dereference(hlist_next_rcu(&blkg->blkcg_node));
+ }
+
+ /*
+ * Forget the list head as well. The verifier assumes that an iterator
+ * which returned NULL keeps returning NULL, and stops checking the
+ * loop for termination once it has; starting the walk over would let
+ * such a loop spin forever.
+ */
+ kit->pos = NULL;
+ kit->blkcg = NULL;
+ return NULL;
+}
+
+/**
+ * bpf_iter_blkg_destroy - Tear down a blkg iterator
+ * @it: iterator
+ */
+__bpf_kfunc void bpf_iter_blkg_destroy(struct bpf_iter_blkg *it)
+{
+}
+
+/*
+ * Read one counter out of @blkg's flushed io.stat aggregate. @counters is one
+ * of the two arrays in blkg->iostat.cur; both are guarded by that struct's
+ * seqlock, the one the io.stat file uses. Returns (u64)-1 if the counter
+ * cannot be read.
+ */
+static u64 blkg_iostat_read(struct blkcg_gq *blkg, const u64 *counters,
+ enum blkg_iostat_type rw)
+{
+ struct blkg_iostat_set *bis = &blkg->iostat;
+ unsigned int seq;
+ u64 val;
+
+ if ((unsigned int)rw >= BLKG_IOSTAT_NR)
+ return (u64)-1;
+
+ /*
+ * On 32-bit the loop below really is a seqcount retry loop. Every
+ * writer of blkg->iostat keeps interrupts off, so only an NMI can land
+ * inside an update, and then the loop would never end. These kfuncs
+ * are reachable from a perf event program, which does run in NMI, so
+ * give up rather than spin. On 64-bit the loop compiles away.
+ */
+ if (BITS_PER_LONG == 32 && in_nmi())
+ return (u64)-1;
+
+ do {
+ seq = u64_stats_fetch_begin(&bis->sync);
+ val = counters[rw];
+ } while (u64_stats_fetch_retry(&bis->sync, seq));
+
+ return val;
+}
+
+/**
+ * bpf_blkg_iostat_bytes - Read a device's io.stat byte counter
+ * @blkg: block group (one device of a block cgroup)
+ * @rw: which counter (BLKG_IOSTAT_READ / _WRITE / _DISCARD)
+ *
+ * Reads the flushed aggregate, so call bpf_blkcg_flush_stats() first for
+ * up-to-date values. The read uses the u64_stats seqlock, like the io.stat
+ * file.
+ *
+ * Return: the number of bytes, or (u64)-1 if @rw is out of range or the
+ * counter cannot be read.
+ */
+__bpf_kfunc u64 bpf_blkg_iostat_bytes(struct blkcg_gq *blkg,
+ enum blkg_iostat_type rw)
+{
+ return blkg_iostat_read(blkg, blkg->iostat.cur.bytes, rw);
+}
+
+/**
+ * bpf_blkg_iostat_ios - Read a device's io.stat I/O count
+ * @blkg: block group (one device of a block cgroup)
+ * @rw: which counter (BLKG_IOSTAT_READ / _WRITE / _DISCARD)
+ *
+ * Return: the number of I/Os, or (u64)-1 if @rw is out of range or the
+ * counter cannot be read.
+ */
+__bpf_kfunc u64 bpf_blkg_iostat_ios(struct blkcg_gq *blkg,
+ enum blkg_iostat_type rw)
+{
+ return blkg_iostat_read(blkg, blkg->iostat.cur.ios, rw);
+}
+
+/**
+ * bpf_blkg_dev - Return a blkg's device id
+ * @blkg: block group
+ *
+ * Return: the device's dev_t (use MAJOR()/MINOR() to split), or 0 if the blkg
+ * has no disk.
+ */
+__bpf_kfunc u64 bpf_blkg_dev(struct blkcg_gq *blkg)
+{
+ if (!blkg->q || !blkg->q->disk)
+ return 0;
+
+ return blkg->q->disk->part0->bd_dev;
+}
+
+__bpf_kfunc_end_defs();
+
+BTF_KFUNCS_START(bpf_blkcg_kfuncs)
+BTF_ID_FLAGS(func, bpf_get_root_blkcg, KF_ACQUIRE | KF_RET_NULL)
+BTF_ID_FLAGS(func, bpf_get_blkcg, KF_ACQUIRE | KF_RET_NULL | KF_RCU)
+BTF_ID_FLAGS(func, bpf_put_blkcg, KF_RELEASE)
+BTF_ID_FLAGS(func, bpf_blkcg_flush_stats, KF_SLEEPABLE)
+
+BTF_ID_FLAGS(func, bpf_iter_blkg_new, KF_ITER_NEW | KF_RCU_PROTECTED)
+BTF_ID_FLAGS(func, bpf_iter_blkg_next, KF_ITER_NEXT | KF_RET_NULL)
+BTF_ID_FLAGS(func, bpf_iter_blkg_destroy, KF_ITER_DESTROY)
+
+BTF_ID_FLAGS(func, bpf_blkg_iostat_bytes, KF_RCU)
+BTF_ID_FLAGS(func, bpf_blkg_iostat_ios, KF_RCU)
+BTF_ID_FLAGS(func, bpf_blkg_dev, KF_RCU)
+BTF_KFUNCS_END(bpf_blkcg_kfuncs)
+
+static const struct btf_kfunc_id_set bpf_blkcg_kfunc_set = {
+ .owner = THIS_MODULE,
+ .set = &bpf_blkcg_kfuncs,
+};
+
+static int __init bpf_blkcg_init(void)
+{
+ int err;
+
+ err = register_btf_kfunc_id_set(BPF_PROG_TYPE_UNSPEC,
+ &bpf_blkcg_kfunc_set);
+ if (err)
+ pr_warn("error while registering bpf blkcg kfuncs: %d\n", err);
+
+ return err;
+}
+late_initcall(bpf_blkcg_init);
diff --git a/tools/testing/selftests/bpf/cgroup_iter_io.h b/tools/testing/selftests/bpf/cgroup_iter_io.h
new file mode 100644
index 0000000000000..f4bbaaccdf716
--- /dev/null
+++ b/tools/testing/selftests/bpf/cgroup_iter_io.h
@@ -0,0 +1,17 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/* Copyright (c) 2025 Meta Platforms, Inc. and affiliates. */
+#ifndef __CGROUP_ITER_IO_H
+#define __CGROUP_ITER_IO_H
+
+struct io_query {
+ /* one device's io.stat counters */
+ __u64 rbytes;
+ __u64 wbytes;
+ __u64 rios;
+ __u64 wios;
+ __u64 dbytes;
+ __u64 dios;
+ __u64 dev; /* dev_t of the device the counters belong to */
+};
+
+#endif /* __CGROUP_ITER_IO_H */
diff --git a/tools/testing/selftests/bpf/config b/tools/testing/selftests/bpf/config
index ea7044f30adc3..270e6bf9194d2 100644
--- a/tools/testing/selftests/bpf/config
+++ b/tools/testing/selftests/bpf/config
@@ -1,3 +1,4 @@
+CONFIG_BLK_CGROUP=y
CONFIG_BLK_DEV_LOOP=y
CONFIG_BOOTPARAM_HARDLOCKUP_PANIC=y
CONFIG_BOOTPARAM_SOFTLOCKUP_PANIC=1
diff --git a/tools/testing/selftests/bpf/prog_tests/cgroup_iter_io.c b/tools/testing/selftests/bpf/prog_tests/cgroup_iter_io.c
new file mode 100644
index 0000000000000..32cda82433183
--- /dev/null
+++ b/tools/testing/selftests/bpf/prog_tests/cgroup_iter_io.c
@@ -0,0 +1,310 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (c) 2025 Meta Platforms, Inc. and affiliates. */
+#define _GNU_SOURCE
+#include <test_progs.h>
+#include <bpf/libbpf.h>
+#include <fcntl.h>
+#include <linux/loop.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/ioctl.h>
+#include <sys/stat.h>
+#include <sys/sysmacros.h>
+#include <unistd.h>
+#include "cgroup_helpers.h"
+#include "cgroup_iter_io.h"
+#include "cgroup_iter_io.skel.h"
+
+#define IO_SIZE (4 * 1024 * 1024)
+
+static int read_stats(struct bpf_link *link)
+{
+ int fd, ret = 0;
+ ssize_t bytes;
+
+ fd = bpf_iter_create(bpf_link__fd(link));
+ if (!ASSERT_OK_FD(fd, "bpf_iter_create"))
+ return 1;
+
+ /* Results land in skel->data_query; the read itself returns no data. */
+ bytes = read(fd, NULL, 0);
+ if (!ASSERT_EQ(bytes, 0, "read fd"))
+ ret = 1;
+
+ close(fd);
+ return ret;
+}
+
+/*
+ * Attach a loop device to an anonymous temp file so we have a real block
+ * device to generate cgroup-charged I/O against. Returns 0 on success, or -1
+ * if loop devices are unavailable (non-root / no CONFIG_BLK_DEV_LOOP) so the
+ * caller can skip.
+ */
+static int loop_setup(char *loop_path, size_t sz, int *ctl_fd, int *loop_fd,
+ int *back_fd)
+{
+ char back_path[] = "/tmp/cgroup_iter_io.XXXXXX";
+ int nr;
+
+ *ctl_fd = *loop_fd = *back_fd = -1;
+
+ *ctl_fd = open("/dev/loop-control", O_RDWR | O_CLOEXEC);
+ if (*ctl_fd < 0)
+ return -1;
+
+ nr = ioctl(*ctl_fd, LOOP_CTL_GET_FREE);
+ if (nr < 0)
+ goto err;
+ snprintf(loop_path, sz, "/dev/loop%d", nr);
+
+ *back_fd = mkstemp(back_path);
+ if (*back_fd < 0)
+ goto err;
+ unlink(back_path);
+ if (ftruncate(*back_fd, (off_t)IO_SIZE * 4))
+ goto err;
+
+ *loop_fd = open(loop_path, O_RDWR | O_CLOEXEC);
+ if (*loop_fd < 0)
+ goto err;
+ if (ioctl(*loop_fd, LOOP_SET_FD, *back_fd))
+ goto err;
+
+ return 0;
+err:
+ if (*loop_fd >= 0)
+ close(*loop_fd);
+ if (*back_fd >= 0)
+ close(*back_fd);
+ close(*ctl_fd);
+ *ctl_fd = *loop_fd = *back_fd = -1;
+ return -1;
+}
+
+static void loop_teardown(const char *loop_path, int ctl_fd, int loop_fd,
+ int back_fd)
+{
+ int nr = -1;
+
+ if (loop_fd >= 0) {
+ ioctl(loop_fd, LOOP_CLR_FD, 0);
+ close(loop_fd);
+ }
+ if (back_fd >= 0)
+ close(back_fd);
+ if (ctl_fd >= 0) {
+ if (sscanf(loop_path, "/dev/loop%d", &nr) == 1 && nr >= 0)
+ ioctl(ctl_fd, LOOP_CTL_REMOVE, nr);
+ close(ctl_fd);
+ }
+}
+
+/* O_DIRECT I/O to the loop device, charged to the current cgroup. */
+static int do_direct_io(const char *loop_path)
+{
+ void *buf;
+ int fd, ret = -1;
+
+ fd = open(loop_path, O_RDWR | O_DIRECT | O_CLOEXEC);
+ if (fd < 0)
+ return -1;
+ if (posix_memalign(&buf, 4096, IO_SIZE))
+ goto out_fd;
+ memset(buf, 0xab, IO_SIZE);
+
+ if (pwrite(fd, buf, IO_SIZE, 0) != IO_SIZE)
+ goto out_buf;
+ fsync(fd);
+ if (pread(fd, buf, IO_SIZE, 0) != IO_SIZE)
+ goto out_buf;
+ ret = 0;
+out_buf:
+ free(buf);
+out_fd:
+ close(fd);
+ return ret;
+}
+
+/*
+ * Parse the io.stat line for device @dev out of the cgroup's io.stat file and
+ * fill @out. @dev is a kernel dev_t (as returned by bpf_blkg_dev), whose
+ * major:minor split matches how io.stat prints the device. Returns 0 if the
+ * device's line was found.
+ */
+static int parse_io_stat(int cgroup_fd, __u64 dev, struct io_query *out)
+{
+ unsigned int want_maj = dev >> 20, want_min = dev & ((1U << 20) - 1);
+ char buf[4096], *line, *saveptr;
+ int fd, n, ret = -1;
+
+ fd = openat(cgroup_fd, "io.stat", O_RDONLY);
+ if (fd < 0)
+ return -1;
+ n = read(fd, buf, sizeof(buf) - 1);
+ close(fd);
+ if (n <= 0)
+ return -1;
+ buf[n] = '\0';
+
+ for (line = strtok_r(buf, "\n", &saveptr); line;
+ line = strtok_r(NULL, "\n", &saveptr)) {
+ unsigned long long rb = 0, wb = 0, ri = 0, wi = 0, db = 0, di = 0;
+ unsigned int maj, min;
+
+ /*
+ * The "maj:min" token is always present; the field block is
+ * optional (the kernel omits it for a device with no read/write
+ * I/O), so a match of >= 2 is enough and absent fields stay 0.
+ */
+ if (sscanf(line,
+ "%u:%u rbytes=%llu wbytes=%llu rios=%llu wios=%llu dbytes=%llu dios=%llu",
+ &maj, &min, &rb, &wb, &ri, &wi, &db, &di) < 2)
+ continue;
+ if (maj != want_maj || min != want_min)
+ continue;
+
+ out->rbytes = rb;
+ out->wbytes = wb;
+ out->rios = ri;
+ out->wios = wi;
+ out->dbytes = db;
+ out->dios = di;
+ ret = 0;
+ break;
+ }
+ return ret;
+}
+
+void test_cgroup_iter_io(void)
+{
+ char *cgroup_rel_path = "/cgroup_iter_io_test";
+ int ctl_fd = -1, loop_fd = -1, back_fd = -1;
+ struct cgroup_iter_io *skel = NULL;
+ struct bpf_link *link = NULL;
+ char loop_path[64];
+ struct io_query *q;
+ int cgroup_fd;
+
+ cgroup_fd = cgroup_setup_and_join(cgroup_rel_path);
+ if (!ASSERT_OK_FD(cgroup_fd, "cgroup_setup_and_join"))
+ return;
+
+ if (loop_setup(loop_path, sizeof(loop_path), &ctl_fd, &loop_fd, &back_fd)) {
+ test__skip(); /* needs root + CONFIG_BLK_DEV_LOOP */
+ goto cleanup_cgroup_fd;
+ }
+
+ skel = cgroup_iter_io__open_and_load();
+ if (!ASSERT_OK_PTR(skel, "cgroup_iter_io__open_and_load"))
+ goto cleanup_loop;
+
+ /*
+ * Pin the read to the loop device so the measured device is stable and
+ * quiesced. Convert the glibc-encoded st_rdev to the kernel dev_t
+ * encoding (major << 20 | minor) that bpf_blkg_dev returns.
+ */
+ {
+ struct stat lst;
+
+ if (!ASSERT_OK(fstat(loop_fd, &lst), "fstat loop"))
+ goto cleanup_skel;
+ skel->data_query->target_dev =
+ ((__u64)major(lst.st_rdev) << 20) | minor(lst.st_rdev);
+ }
+
+ DECLARE_LIBBPF_OPTS(bpf_iter_attach_opts, opts);
+ union bpf_iter_link_info linfo = {
+ .cgroup.cgroup_fd = cgroup_fd,
+ .cgroup.order = BPF_CGROUP_ITER_SELF_ONLY,
+ };
+ opts.link_info = &linfo;
+ opts.link_info_len = sizeof(linfo);
+
+ link = bpf_program__attach_iter(skel->progs.cgroup_io_query, &opts);
+ if (!ASSERT_OK_PTR(link, "bpf_program__attach_iter"))
+ goto cleanup_skel;
+
+ /* This process is in the test cgroup, so the loop I/O is charged here. */
+ if (!ASSERT_OK(do_direct_io(loop_path), "do_direct_io"))
+ goto cleanup_link;
+
+ if (!ASSERT_OK(read_stats(link), "read stats"))
+ goto cleanup_link;
+
+ /*
+ * Weak check: we did I/O, so the numbers must be non-zero. Follows the
+ * pattern in cgroup_iter_memcg.
+ */
+ q = &skel->data_query->io_query;
+ if (test__start_subtest("cgroup_iter_io__write")) {
+ ASSERT_GT(q->wbytes, 0, "wbytes");
+ ASSERT_GT(q->wios, 0, "wios");
+ }
+ if (test__start_subtest("cgroup_iter_io__read")) {
+ ASSERT_GT(q->rbytes, 0, "rbytes");
+ ASSERT_GT(q->rios, 0, "rios");
+ }
+ if (test__start_subtest("cgroup_iter_io__dev"))
+ ASSERT_GT(q->dev, 0, "dev");
+
+ /*
+ * Stronger check: the kfunc-read values must equal what the io.stat
+ * file reports for the same device. Refresh via the prog, then read
+ * the file with no I/O in between, so both flushed snapshots match
+ * exactly.
+ */
+ if (test__start_subtest("cgroup_iter_io__match")) {
+ struct io_query filev = {};
+
+ if (ASSERT_OK(read_stats(link), "read stats") &&
+ ASSERT_OK(parse_io_stat(cgroup_fd, q->dev, &filev),
+ "parse io.stat")) {
+ ASSERT_EQ(q->rbytes, filev.rbytes, "rbytes");
+ ASSERT_EQ(q->wbytes, filev.wbytes, "wbytes");
+ ASSERT_EQ(q->rios, filev.rios, "rios");
+ ASSERT_EQ(q->wios, filev.wios, "wios");
+ ASSERT_EQ(q->dbytes, filev.dbytes, "dbytes");
+ ASSERT_EQ(q->dios, filev.dios, "dios");
+ }
+ }
+
+ /*
+ * Separate program for the root block cgroup. Its counters do not come
+ * from rstat, they are refilled from the disks themselves, so this
+ * covers the other half of bpf_blkcg_flush_stats(). They cover every
+ * cgroup's I/O to the loop device, and only this test touches it, so
+ * they must be at or above what the test cgroup was charged.
+ */
+ if (test__start_subtest("cgroup_iter_io__root")) {
+ struct bpf_link *root_link;
+ struct io_query *r;
+
+ skel->data_query->got_root_blkcg = 0;
+ root_link = bpf_program__attach_iter(skel->progs.cgroup_root_blkcg_query,
+ &opts);
+ if (ASSERT_OK_PTR(root_link, "attach root iter")) {
+ if (ASSERT_OK(read_stats(root_link), "read root stats")) {
+ r = &skel->data_query->root_query;
+ ASSERT_EQ(skel->data_query->got_root_blkcg, 1,
+ "got_root_blkcg");
+ ASSERT_EQ(r->dev, q->dev, "root dev");
+ ASSERT_GE(r->wbytes, q->wbytes, "root wbytes");
+ ASSERT_GE(r->wios, q->wios, "root wios");
+ ASSERT_GE(r->rbytes, q->rbytes, "root rbytes");
+ ASSERT_GE(r->rios, q->rios, "root rios");
+ }
+ bpf_link__destroy(root_link);
+ }
+ }
+
+cleanup_link:
+ bpf_link__destroy(link);
+cleanup_skel:
+ cgroup_iter_io__destroy(skel);
+cleanup_loop:
+ loop_teardown(loop_path, ctl_fd, loop_fd, back_fd);
+cleanup_cgroup_fd:
+ close(cgroup_fd);
+ cleanup_cgroup_environment();
+}
diff --git a/tools/testing/selftests/bpf/progs/cgroup_iter_io.c b/tools/testing/selftests/bpf/progs/cgroup_iter_io.c
new file mode 100644
index 0000000000000..b839def945086
--- /dev/null
+++ b/tools/testing/selftests/bpf/progs/cgroup_iter_io.c
@@ -0,0 +1,107 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (c) 2025 Meta Platforms, Inc. and affiliates. */
+#include <vmlinux.h>
+#include <bpf/bpf_helpers.h>
+#include <bpf/bpf_core_read.h>
+#include "bpf_experimental.h"
+#include "cgroup_iter_io.h"
+
+char _license[] SEC("license") = "GPL";
+
+/* The counters of the device named by target_dev are stored here. */
+struct io_query io_query SEC(".data.query");
+
+/* The same device's counters read through the root block cgroup. */
+struct io_query root_query SEC(".data.query");
+
+/* Set to 1 by cgroup_root_blkcg_query when bpf_get_root_blkcg() succeeds. */
+__u64 got_root_blkcg SEC(".data.query");
+
+/* Device to read, set by userspace (kernel dev_t). Pinning the device keeps
+ * the read deterministic and lets the value be compared to io.stat exactly.
+ */
+__u64 target_dev SEC(".data.query");
+
+/*
+ * Flush @blkcg and copy the target device's counters into @out. Reading only
+ * the one pinned device keeps the result deterministic: that device is
+ * quiesced, so its counters match io.stat exactly, while picking "any device
+ * with I/O" would race with backing-store writeback.
+ */
+static __always_inline void read_target_dev(struct blkcg *blkcg,
+ struct io_query *out)
+{
+ struct blkcg_gq *pos;
+
+ /* io.stat needs a flush before it can be read (sleepable). */
+ bpf_blkcg_flush_stats(blkcg);
+
+ /* The per-device blkg walk needs an RCU section. */
+ bpf_rcu_read_lock();
+ bpf_for_each(blkg, pos, blkcg) {
+ if (bpf_blkg_dev(pos) != target_dev)
+ continue;
+
+ out->dev = bpf_blkg_dev(pos);
+ out->rbytes = bpf_blkg_iostat_bytes(pos, BLKG_IOSTAT_READ);
+ out->wbytes = bpf_blkg_iostat_bytes(pos, BLKG_IOSTAT_WRITE);
+ out->rios = bpf_blkg_iostat_ios(pos, BLKG_IOSTAT_READ);
+ out->wios = bpf_blkg_iostat_ios(pos, BLKG_IOSTAT_WRITE);
+ out->dbytes = bpf_blkg_iostat_bytes(pos, BLKG_IOSTAT_DISCARD);
+ out->dios = bpf_blkg_iostat_ios(pos, BLKG_IOSTAT_DISCARD);
+ break;
+ }
+ bpf_rcu_read_unlock();
+}
+
+SEC("iter.s/cgroup")
+int cgroup_io_query(struct bpf_iter__cgroup *ctx)
+{
+ struct cgroup *cgrp = ctx->cgroup;
+ struct blkcg *blkcg;
+
+ /* The last iteration has a NULL cgroup, skip it. */
+ if (!cgrp)
+ return 1;
+
+ /* Start fresh so a device that is not found stays all-zero. */
+ __builtin_memset(&io_query, 0, sizeof(io_query));
+
+ blkcg = bpf_get_blkcg(&cgrp->self);
+ if (!blkcg)
+ return 0;
+
+ read_target_dev(blkcg, &io_query);
+
+ bpf_put_blkcg(blkcg);
+ return 0;
+}
+
+SEC("iter.s/cgroup")
+int cgroup_root_blkcg_query(struct bpf_iter__cgroup *ctx)
+{
+ struct cgroup *cgrp = ctx->cgroup;
+ struct blkcg *blkcg;
+
+ /* The last iteration has a NULL cgroup, skip it. */
+ if (!cgrp)
+ return 1;
+
+ __builtin_memset(&root_query, 0, sizeof(root_query));
+
+ blkcg = bpf_get_root_blkcg();
+ if (!blkcg)
+ return 0;
+
+ /*
+ * The root cgroup takes its numbers from the disks themselves rather
+ * than from rstat, so this also covers the root side of
+ * bpf_blkcg_flush_stats(). The counters cover every cgroup's I/O, so
+ * they can only be at or above what this test's own cgroup did.
+ */
+ read_target_dev(blkcg, &root_query);
+
+ got_root_blkcg = 1;
+ bpf_put_blkcg(blkcg);
+ return 0;
+}
|
| 3/2 | 2026/08/07 21:03 | llm | patch-evaluator |
0mModel:gemini-3.1-pro-preview Args: null Results: null Tokens: input: 12494 output: 29 thoughts: 325 Thoughts: **BPF Kfuncs for Block I/O Controller: Initial Thoughts** Okay, so we've got this patch series introducing BPF kfuncs specifically for the block I/O controller, blkcg. Interesting. The new `block/bpf_blkcg.c` file lays out the groundwork, providing kfuncs like `bpf_get_root_blkcg`, `bpf_get_blkcg`, `bpf_put_blkcg`, `bpf_blkcg_flush_stats`, and the iterator functions: `bpf_iter_blkg_new`, `bpf_iter_blkg_next`, `bpf_iter_blkg_destroy`. Plus, we get access to those crucial `bpf_blkg_iostat_bytes`, `bpf_blkg_iostat_ios`, and `bpf_blkg_dev` functions. Essentially, BPF programs can now directly read a cgroup's io.stat counters. This is going to be useful. The changes touch both the core block layer and the BPF subsystem, which means we will need to test both. Now, this screams "fuzzing target" to me. Getting BPF to reliably read blkcg data will require some serious testing. Let's make sure the configuration is right so we can build this. Let's find the relevant config options... Okay, `block/Makefile` gives us the key: ```makefile ifdef CONFIG_BPF_SYSCALL obj-$(CONFIG_BLK_CGROUP) += bpf_blkcg.o endif ``` So, we're definitely looking at needing `CONFIG_BPF_SYSCALL` and `CONFIG_BLK_CGROUP` enabled. Let's verify there are no other dependencies. I need to run a `git grep` check on `CONFIG_BLK_CGROUP` in `block/Kconfig` to see if there are any additional requirements. |
| 4/2 | 2026/08/07 21:03 | tool | grepper |
0mArgs:{
"Expression": "config BLK_CGROUP",
"PathPrefix": "block/"
}Results: {
"Output": "block/Kconfig=39=config BLK_RQ_ALLOC_TIME\n--\nblock/Kconfig-41-\nblock/Kconfig:42:config BLK_CGROUP_RWSTAT\nblock/Kconfig-43-\tbool\nblock/Kconfig-44-\nblock/Kconfig:45:config BLK_CGROUP_PUNT_BIO\nblock/Kconfig-46-\tbool\n--\nblock/Kconfig=127=config BLK_WBT_MQ\n--\nblock/Kconfig-133-\nblock/Kconfig:134:config BLK_CGROUP_IOLATENCY\nblock/Kconfig-135-\tbool \"Enable support for latency based cgroup IO protection\"\n--\nblock/Kconfig-144-\nblock/Kconfig:145:config BLK_CGROUP_FC_APPID\nblock/Kconfig-146-\tbool \"Enable support to track FC I/O Traffic across cgroup applications\"\n--\nblock/Kconfig-153-\nblock/Kconfig:154:config BLK_CGROUP_IOCOST\nblock/Kconfig-155-\tbool \"Enable support for cost model based cgroup IO controller\"\n--\nblock/Kconfig-163-\nblock/Kconfig:164:config BLK_CGROUP_IOPRIO\nblock/Kconfig-165-\tbool \"Cgroup I/O controller for assigning an I/O priority class\"\n"
} |
| 5/2 | 2026/08/07 21:03 | llm | patch-evaluator |
0mModel:gemini-3.1-pro-preview Args: null Results: null Tokens: input: 13640 output: 31 thoughts: 17 Thoughts: Okay, let's see. My current train of thought seems to be stuck on something. I'm hitting a roadblock and need to find a way around it. **Investigating Kernel Configuration**. I need to refine my search. Right now, I'm thinking I should focus on kernel configuration, and more specifically on something related to block cgroups. To be precise, I should try to search for the exact string: `config BLK_CGROUP`. Hopefully, that will lead me to the relevant settings and documentation I need. Time to get specific! |
| 6/2 | 2026/08/07 21:03 | tool | grepper |
0mError:no matches Args: {
"Expression": "^config BLK_CGROUP$",
"PathPrefix": "block/"
}Results: {
"error": "no matches"
} |
| 7/2 | 2026/08/07 21:03 | llm | patch-evaluator |
0mModel:gemini-3.1-pro-preview Args: null Results: null Tokens: input: 13820 output: 29 thoughts: 19 Thoughts: Okay, here's my summary, presented as a stream of thought: **Focusing In on Block CGroups** Alright, time to get down to brass tacks. I need to figure out how `BLK_CGROUP` is defined or configured. My immediate thought is to check the `block/Kconfig` file. That's the logical place to start – it's where kernel configuration options related to block devices typically reside. Let's see what we can find. It's time to delve into the Kconfig. I want to see how these options are described. |
| 8/2 | 2026/08/07 21:03 | tool | grepper |
0mArgs:{
"Expression": "BLK_CGROUP",
"PathPrefix": "block/Kconfig"
}Results: {
"Output": "block/Kconfig=39=config BLK_RQ_ALLOC_TIME\n--\nblock/Kconfig-41-\nblock/Kconfig:42:config BLK_CGROUP_RWSTAT\nblock/Kconfig-43-\tbool\nblock/Kconfig-44-\nblock/Kconfig:45:config BLK_CGROUP_PUNT_BIO\nblock/Kconfig-46-\tbool\n--\nblock/Kconfig=106=config BLK_DEV_THROTTLING\nblock/Kconfig-107-\tbool \"Block layer bio throttling support\"\nblock/Kconfig:108:\tdepends on BLK_CGROUP\nblock/Kconfig:109:\tselect BLK_CGROUP_RWSTAT\nblock/Kconfig-110-\thelp\n--\nblock/Kconfig=127=config BLK_WBT_MQ\n--\nblock/Kconfig-133-\nblock/Kconfig:134:config BLK_CGROUP_IOLATENCY\nblock/Kconfig-135-\tbool \"Enable support for latency based cgroup IO protection\"\nblock/Kconfig:136:\tdepends on BLK_CGROUP\nblock/Kconfig-137-\thelp\n--\nblock/Kconfig-144-\nblock/Kconfig:145:config BLK_CGROUP_FC_APPID\nblock/Kconfig-146-\tbool \"Enable support to track FC I/O Traffic across cgroup applications\"\nblock/Kconfig:147:\tdepends on BLK_CGROUP \u0026\u0026 NVME_FC\nblock/Kconfig-148-\thelp\n--\nblock/Kconfig-153-\nblock/Kconfig:154:config BLK_CGROUP_IOCOST\nblock/Kconfig-155-\tbool \"Enable support for cost model based cgroup IO controller\"\nblock/Kconfig:156:\tdepends on BLK_CGROUP\nblock/Kconfig-157-\tselect BLK_RQ_ALLOC_TIME\n--\nblock/Kconfig-163-\nblock/Kconfig:164:config BLK_CGROUP_IOPRIO\nblock/Kconfig-165-\tbool \"Cgroup I/O controller for assigning an I/O priority class\"\nblock/Kconfig:166:\tdepends on BLK_CGROUP\nblock/Kconfig-167-\thelp\n"
} |
| 9/2 | 2026/08/07 21:03 | llm | patch-evaluator |
0mModel:gemini-3.1-pro-preview Args: null Results: null Tokens: input: 14589 output: 32 thoughts: 13 Thoughts: Here's my attempt at summarizing that thought process, tailored for an expert audience: **Debugging a Kernel-Level Block I/O Issue** Okay, so I'm knee-deep in this block I/O performance issue, and I've hit a snag. I'm looking at the relevant code path, and I've encountered `BLK_CGROUP`. Now, my initial thought is, "Wait a minute... where *exactly* is `BLK_CGROUP` defined?" It's been a while since I last looked at this part of the kernel, and the cgroup block I/O integration can be tricky. I need to refresh my memory on the macros and variables in play. I need the definition to understand the control flow here. Is it a core cgroup constant? Is it a local definition within the block layer's cgroup implementation? I need to track down the header file or module that declares this constant. Without that, I'm essentially flying blind and can't accurately assess its role in this performance bottleneck. Back to the include paths! |
| 10/2 | 2026/08/07 21:03 | tool | grepper |
0mError:no matches Args: {
"Expression": "^menuconfig BLK_CGROUP$",
"PathPrefix": "block/"
}Results: {
"error": "no matches"
} |
| 11/2 | 2026/08/07 21:03 | llm | patch-evaluator |
0mModel:gemini-3.1-pro-preview Args: null Results: null Tokens: input: 14875 output: 29 thoughts: 18 Thoughts: Okay, here's my summary, presented as a stream of thought: **Kconfig Keyword Hunt: BLK_CGROUP** Right, time to get to work. I need to figure out the current status of `BLK_CGROUP` within the kernel configuration. I know it's related to cgroup block device control, but I need a definitive picture. So, I need to do a full search. I'm going to scan *all* the Kconfig files. This means a comprehensive search across the entire kernel configuration space. No cherry-picking, I want the full context. Let's see what the Kconfig files have to say about this. That search should give me the information I need. I'll need to pay close attention to dependencies and default values once I have the results. |
| 12/2 | 2026/08/07 21:03 | tool | grepper |
0mArgs:{
"Expression": "^config BLK_CGROUP$",
"PathPrefix": ""
}Results: {
"Output": "init/Kconfig=1099=config MEMCG_V1\n--\ninit/Kconfig-1115-\ninit/Kconfig:1116:config BLK_CGROUP\ninit/Kconfig-1117-\tbool \"IO controller\"\n"
} |
| 13/2 | 2026/08/07 21:03 | llm | patch-evaluator |
0mModel:gemini-3.1-pro-preview Args: null Results: null Tokens: input: 15170 output: 31 thoughts: 19 Thoughts: Okay, here's my take on those thoughts, tailored for an expert audience: **Tracing a Configuration Dependency** Right, I'm onto something...the setting I'm after is probably in `init/Kconfig`. Classic place for core configuration options. Time to dig in and understand its dependencies. No point just flipping a switch blindly; gotta figure out the rationale behind it being set up the way it is. I need to see *why* it's configured the way it is - are there specific features, kernel versions, or hardware configurations that trigger its inclusion? Is it gated on some esoteric menu path? I'll need to reverse-engineer its implications within the broader architecture. Okay, let's fire up the editor and start tracing... |
| 14/2 | 2026/08/07 21:03 | tool | grepper |
0mArgs:{
"Expression": "config BLK_CGROUP",
"PathPrefix": "init/Kconfig"
}Results: {
"Output": "init/Kconfig=1099=config MEMCG_V1\n--\ninit/Kconfig-1115-\ninit/Kconfig:1116:config BLK_CGROUP\ninit/Kconfig-1117-\tbool \"IO controller\"\n"
} |
| 15/2 | 2026/08/07 21:03 | llm | patch-evaluator |
0mModel:gemini-3.1-pro-preview Args: null Results: null Tokens: input: 15454 output: 33 thoughts: 18 Thoughts: Here's my attempt at summarizing the thought process, tailored for an expert audience: **Diving into the Kernel Configuration Landscape** Okay, so I'm trying to get a handle on this particular aspect of the kernel configuration. I'm thinking, `init/Kconfig` is the place to start, specifically around line 1116. That area likely deals with the configuration options or a particular module I'm interested in. I should check that section for the relevant definitions and dependencies. Let's see what gems lie in wait in that portion of `Kconfig`. |
| 16/2 | 2026/08/07 21:03 | tool | read-file |
0mArgs:{
"File": "init/Kconfig",
"FirstLine": 1110,
"LineCount": 20
}Results: {
"Contents": "1110:\t\t Please note that feature set of the legacy memory controller is likely\n1111:\t\t going to shrink due to deprecation process. New deployments with v1\n1112:\t\t controller are highly discouraged.\n1113:\t\n1114:\t\t Say N if unsure.\n1115:\t\n1116:\tconfig BLK_CGROUP\n1117:\t\tbool \"IO controller\"\n1118:\t\tdepends on BLOCK\n1119:\t\tdefault n\n1120:\t\thelp\n1121:\t\tGeneric block IO controller cgroup interface. This is the common\n1122:\t\tcgroup interface which should be used by various IO controlling\n1123:\t\tpolicies.\n1124:\t\n1125:\t\tCurrently, CFQ IO scheduler uses it to recognize task groups and\n1126:\t\tcontrol disk bandwidth allocation (proportional time slice allocation)\n1127:\t\tto such task groups. It is also used by bio throttling logic in\n1128:\t\tblock layer to implement upper limit in IO rates on a device.\n1129:\t\n"
} |
| 17/2 | 2026/08/07 21:03 | llm | patch-evaluator |
0mModel:gemini-3.1-pro-preview Args: null Results: null Tokens: input: 15914 output: 237 thoughts: 230 Thoughts: **Analyzing the Block cgroup BPF Implementation** Okay, so I'm looking at this `BLK_CGROUP` stuff, and it's pretty clear that it has a dependency on `BLOCK`. That's not unexpected, but good to keep in mind. I need to ensure that the kernel config includes `BPF_SYSCALL` along with `BLK_CGROUP` – these are my base requirements. Now, let's nail down the critical pieces. I need to understand the new additions and changes. My focus right now is to identify the main symbols that define the BPF integration for block cgroups. From `block/bpf_blkcg.c`, these are the key functions I'll need to understand: `bpf_get_root_blkcg`, `bpf_get_blkcg`, `bpf_put_blkcg`, `bpf_blkcg_flush_stats`, `bpf_iter_blkg_new`, `bpf_iter_blkg_next`, `bpf_iter_blkg_destroy`, `bpf_blkg_iostat_bytes`, `bpf_blkg_iostat_ios`, and `bpf_blkg_dev`. These clearly handle things like root cgroup access, cgroup management, stats flushing, and iteration, as well as accessing I/O statistics and the device itself. It looks like the `blkcg_fill_root_iostats` function in `block/blk-cgroup.c` has also been changed to be non-static. I need to check this out, as non-static likely indicates it's used more broadly now. I'll make sure to add all of these as "FocusSymbols" in my notes, to track them as I dive deeper. |
| 18/2 | 2026/08/07 21:03 | tool | set-results |
0mArgs:{
"EnableConfigs": [
"BPF_SYSCALL",
"BLK_CGROUP"
],
"FocusSymbols": [
"bpf_get_root_blkcg",
"bpf_get_blkcg",
"bpf_put_blkcg",
"bpf_blkcg_flush_stats",
"bpf_iter_blkg_new",
"bpf_iter_blkg_next",
"bpf_iter_blkg_destroy",
"bpf_blkg_iostat_bytes",
"bpf_blkg_iostat_ios",
"bpf_blkg_dev",
"blkcg_fill_root_iostats"
],
"Reasoning": "The patch introduces new BPF kfuncs for the block I/O controller (blkcg) to allow BPF programs to read a cgroup's io.stat counters. These kfuncs are reachable from BPF programs and should be fuzzed to ensure they handle various inputs and concurrent executions safely.",
"WorthFuzzing": true
}Results: {
"EnableConfigs": [
"BPF_SYSCALL",
"BLK_CGROUP"
],
"FocusSymbols": [
"bpf_get_root_blkcg",
"bpf_get_blkcg",
"bpf_put_blkcg",
"bpf_blkcg_flush_stats",
"bpf_iter_blkg_new",
"bpf_iter_blkg_next",
"bpf_iter_blkg_destroy",
"bpf_blkg_iostat_bytes",
"bpf_blkg_iostat_ios",
"bpf_blkg_dev",
"blkcg_fill_root_iostats"
],
"Reasoning": "The patch introduces new BPF kfuncs for the block I/O controller (blkcg) to allow BPF programs to read a cgroup's io.stat counters. These kfuncs are reachable from BPF programs and should be fuzzed to ensure they handle various inputs and concurrent executions safely.",
"WorthFuzzing": true
} |
| 19/1 | 2026/08/07 21:03 | action | If |
0mArgs:{
"WorthFuzzing": true
}Results: null |
| 20/2 | 2026/08/07 21:03 | agent | kmsan-evaluator |
0mModel:best-expensive Args: null Results: {
"KMSANReasoning": "The patch introduces BPF kfuncs to read block cgroup I/O statistics (io.stat). It does not allocate new complex structures, copy data directly to user space, or introduce logic that could expose uninitialized memory. The kfuncs return simple scalar values (u64) or pointers to existing, initialized kernel structures. The BPF iterator state is fully initialized in `bpf_iter_blkg_new`. Any potential memory safety issues, such as out-of-bounds accesses or use-after-free bugs related to RCU or cgroup references, would be effectively caught by KASAN and standard bug detectors. Therefore, a dedicated KMSAN fuzzing session is not justified.",
"NeedsKMSAN": false
}Instruction: You are an expert Linux kernel security engineer specializing in kernel memory
error detectors (KASAN and KMSAN). Your job is to review the provided patch series and
determine if the code changes justify spawning a dedicated KMSAN (KernelMemorySanitizer)
fuzzing session in addition to standard KASAN fuzzing.
CRITICAL DISTINCTION BETWEEN KASAN AND KMSAN:
- Standard KASAN kernel builds (upstream-apparmor-kasan.config) already enable
a comprehensive suite of debugging tools and sanitizers, including KASAN
(out-of-bounds accesses, use-after-free, double free, invalid free), LOCKDEP
(locking bugs and deadlocks), UB-sanitizers, and memory corruption checks.
- KMSAN (KernelMemorySanitizer) detects reads of UNINITIALIZED memory (stack, heap,
or page allocations) and kernel-to-user memory info-leaks.
Rule: THERE IS NO SENSE IN RUNNING A KMSAN SESSION IF A BUG CAN BE CAUGHT BY KASAN,
LOCKDEP, OR OTHER STANDARD BUG DETECTORS.
A dedicated KMSAN fuzzing session incurs significant resource costs. You must ONLY
set NeedsKMSAN=true if the code changes introduce or expose UNINITIALIZED MEMORY risks
that are detected ONLY by KMSAN.
Look holistically at the patch series and surrounding code. Even if no direct
uninitialized field accesses or new buffer allocations are added in the diff itself,
a patch may alter control flow, bounds checking, or data length calculations in ways
that change how the rest of the code operates on existing buffers (e.g. allowing
uninitialized stack/heap memory to be read, copied to user space, or used in control
flow). Do not hesitate to use your code access tools to inspect the surrounding code,
called functions, and callers.
Set NeedsKMSAN=true ONLY IF the patch introduces or modifies:
1. Kernel structures sent to user space (via copy_to_user, put_user, netlink skb
attributes, ioctl output arguments, socket options, or BPF buffers) where fields
or structure padding might not be fully initialized/zeroed.
2. Conditional logic or branching that depends on potentially uninitialized variables
or struct fields.
3. Allocation or initialization of complex data structures where uninitialized fields
could be read later in reachable code paths.
4. Bounds checks, lengths, or logic in a way that allows surrounding code to access
uninitialized bytes of existing buffers.
Set NeedsKMSAN=false IF:
- The code changes primarily risk out-of-bounds access, array overflows, NULL pointer
dereferences, locking deadlocks, or use-after-free bugs (these are already caught
by KASAN, LOCKDEP, or standard bug detectors).
- All stack/heap structures touched or introduced by the patch are fully zeroed
or initialized (e.g. using = {0}, memset, kzalloc) before being read or copied.
- The patch does not introduce any risk of uninitialized memory usage or info-leaks.
Use your code access tools to inspect the surrounding code if necessary, then provide
detailed KMSANReasoning contrasting KASAN vs KMSAN applicability for this patch.
Prefer calling several tools at the same time to save round-trips.
Use set-results tool to provide results of the analysis.
It must be called exactly once before the final reply.
Ignore results of this tool.
Prompt: For your convenience, here is the diff of the changes:
commit 2ca19a8a248872888de1a5be7d67838a2f2a4217
Author: syz-cluster <triage@syzkaller.com>
Date: Fri Aug 7 21:03:03 2026 +0000
syz-cluster: applied patch under review
diff --git a/MAINTAINERS b/MAINTAINERS
index 2f9472c1a0907..87c56e9555771 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -6617,6 +6617,7 @@ F: block/blk-cgroup.c
F: block/blk-iocost.c
F: block/blk-iolatency.c
F: block/blk-throttle.c
+F: block/bpf_blkcg.c
F: include/linux/blk-cgroup.h
CONTROL GROUP - CPUSET
diff --git a/block/Makefile b/block/Makefile
index e7bd320e3d697..572e49988c8ee 100644
--- a/block/Makefile
+++ b/block/Makefile
@@ -17,6 +17,9 @@ obj-$(CONFIG_BLK_ERROR_INJECTION) += error-injection.o
obj-$(CONFIG_BLK_DEV_BSG_COMMON) += bsg.o
obj-$(CONFIG_BLK_DEV_BSGLIB) += bsg-lib.o
obj-$(CONFIG_BLK_CGROUP) += blk-cgroup.o
+ifdef CONFIG_BPF_SYSCALL
+obj-$(CONFIG_BLK_CGROUP) += bpf_blkcg.o
+endif
obj-$(CONFIG_BLK_CGROUP_RWSTAT) += blk-cgroup-rwstat.o
obj-$(CONFIG_BLK_CGROUP_FC_APPID) += blk-cgroup-fc-appid.o
obj-$(CONFIG_BLK_DEV_THROTTLING) += blk-throttle.o
diff --git a/block/blk-cgroup.c b/block/blk-cgroup.c
index d9676126c5b5d..8d538ad4e861c 100644
--- a/block/blk-cgroup.c
+++ b/block/blk-cgroup.c
@@ -1086,7 +1086,7 @@ static void blkcg_rstat_flush(struct cgroup_subsys_state *css, int cpu)
* flushing the root cgroup's stats by explicitly filling in the iostat
* with disk level statistics.
*/
-static void blkcg_fill_root_iostats(void)
+void blkcg_fill_root_iostats(void)
{
struct class_dev_iter iter;
struct device *dev;
diff --git a/block/blk-cgroup.h b/block/blk-cgroup.h
index 615390f751aa2..8c9c2a1adfaa4 100644
--- a/block/blk-cgroup.h
+++ b/block/blk-cgroup.h
@@ -205,6 +205,7 @@ void blkcg_deactivate_policy(struct gendisk *disk,
const struct blkcg_policy *pol);
const char *blkg_dev_name(struct blkcg_gq *blkg);
+void blkcg_fill_root_iostats(void);
void blkcg_print_blkgs(struct seq_file *sf, struct blkcg *blkcg,
u64 (*prfill)(struct seq_file *,
struct blkg_policy_data *, int),
diff --git a/block/bpf_blkcg.c b/block/bpf_blkcg.c
new file mode 100644
index 0000000000000..48a86f07e198a
--- /dev/null
+++ b/block/bpf_blkcg.c
@@ -0,0 +1,315 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Block I/O Controller-related BPF kfuncs and auxiliary code.
+ *
+ * These let a BPF program read a cgroup's io.stat counters. A program turns a
+ * cgroup's css into a struct blkcg with bpf_get_blkcg(), flushes the stats with
+ * bpf_blkcg_flush_stats(), then walks the cgroup's per-device blkgs with the
+ * bpf_iter_blkg open-coded iterator, reading each device's counters with
+ * bpf_blkg_iostat_bytes()/bpf_blkg_iostat_ios(). It mirrors the memory
+ * controller kfuncs in mm/bpf_memcontrol.c, but adds a per-device dimension:
+ * unlike memcg, blkcg keeps one blkg (and one io.stat line) per block device.
+ *
+ * This file lives in block/ because the blkcg/blkg struct layouts are private
+ * to block/blk-cgroup.h.
+ */
+
+#include "blk-cgroup.h"
+
+#include <linux/bpf.h>
+#include <linux/btf_ids.h>
+#include <linux/preempt.h>
+#include <linux/rculist.h>
+
+__bpf_kfunc_start_defs();
+
+/**
+ * bpf_get_root_blkcg - Returns a pointer to the root block cgroup
+ *
+ * The function has KF_ACQUIRE semantics, even though the root block cgroup is
+ * never destroyed and doesn't require reference counting. It's safe to pass it
+ * to bpf_put_blkcg().
+ *
+ * Note that the root cgroup is special: its counters are the disks' own
+ * statistics, so they cover every cgroup's I/O rather than only the root's.
+ * This matches what the root io.stat file prints.
+ *
+ * Return: A pointer to the root block cgroup.
+ */
+__bpf_kfunc struct blkcg *bpf_get_root_blkcg(void)
+{
+ /* css_get() is not needed */
+ return &blkcg_root;
+}
+
+/**
+ * bpf_get_blkcg - Get a reference to a block cgroup
+ * @css: pointer to the css structure
+ *
+ * It's fine to pass a css which belongs to any cgroup controller,
+ * e.g. unified hierarchy's main css.
+ *
+ * Implements KF_ACQUIRE semantics.
+ *
+ * Return: A pointer to a blkcg structure after bumping the corresponding css's
+ * reference counter, or NULL if the io controller is not enabled on the cgroup.
+ */
+__bpf_kfunc struct blkcg *bpf_get_blkcg(struct cgroup_subsys_state *css)
+{
+ struct blkcg *blkcg = NULL;
+
+ if (css->ss == &io_cgrp_subsys)
+ return css_tryget(css) ? css_to_blkcg(css) : NULL;
+
+ /*
+ * Some other controller's css, or the cgroup's own one. Look up the io
+ * controller's css; rcu keeps it alive between the load and the tryget.
+ * Acquire and release rcu on one straight path, so that block/'s lock
+ * context analysis can follow it.
+ */
+ rcu_read_lock();
+ css = rcu_dereference_raw(css->cgroup->subsys[io_cgrp_id]);
+ if (css && css_tryget(css))
+ blkcg = css_to_blkcg(css);
+ rcu_read_unlock();
+
+ return blkcg;
+}
+
+/**
+ * bpf_put_blkcg - Put a reference to a block cgroup
+ * @blkcg: block cgroup to release
+ *
+ * Releases a previously acquired blkcg reference.
+ * Implements KF_RELEASE semantics.
+ */
+__bpf_kfunc void bpf_put_blkcg(struct blkcg *blkcg)
+{
+ css_put(&blkcg->css);
+}
+
+/**
+ * bpf_blkcg_flush_stats - Flush a block cgroup's io statistics
+ * @blkcg: block cgroup
+ *
+ * Call this before reading counters for up-to-date values. Sleepable.
+ *
+ * It does what reading the io.stat file does, which differs by cgroup. For a
+ * non-root cgroup it folds the per-cpu deltas into the per-device aggregates
+ * and up the cgroup tree. The root cgroup is not accounted through rstat at
+ * all, so for it the per-device aggregates are refilled from the disks'
+ * own statistics, which count every cgroup's I/O.
+ *
+ * The root branch is not self-limiting the way the rstat one is: it rereads
+ * every disk on every call, while a second rstat flush finds nothing left to
+ * fold. The numbers it produces are the same for every cgroup, so read them
+ * once rather than once per cgroup of a walk.
+ */
+__bpf_kfunc void bpf_blkcg_flush_stats(struct blkcg *blkcg)
+{
+ if (!blkcg->css.parent)
+ blkcg_fill_root_iostats();
+ else
+ css_rstat_flush(&blkcg->css);
+}
+
+struct bpf_iter_blkg {
+ __u64 __opaque[2];
+} __attribute__((aligned(8)));
+
+struct bpf_iter_blkg_kern {
+ struct blkcg *blkcg;
+ struct blkcg_gq *pos;
+} __attribute__((aligned(8)));
+
+/**
+ * bpf_iter_blkg_new - Start iterating a block cgroup's per-device blkgs
+ * @it: iterator to initialize
+ * @blkcg: block cgroup whose devices to walk
+ *
+ * Each yielded blkg holds one block device's counters, the same ones behind a
+ * per-device line of the io.stat file. Offline blkgs are skipped, as the file
+ * skips them. One case differs: the file prints no line for a blkg whose disk
+ * is gone, while the walk still yields it, and bpf_blkg_dev() returns 0 for
+ * it. Must be used inside an RCU read section.
+ *
+ * Return: 0 on success.
+ */
+__bpf_kfunc int bpf_iter_blkg_new(struct bpf_iter_blkg *it, struct blkcg *blkcg)
+{
+ struct bpf_iter_blkg_kern *kit = (void *)it;
+
+ BUILD_BUG_ON(sizeof(struct bpf_iter_blkg_kern) > sizeof(struct bpf_iter_blkg));
+ BUILD_BUG_ON(__alignof__(struct bpf_iter_blkg_kern) !=
+ __alignof__(struct bpf_iter_blkg));
+
+ kit->blkcg = blkcg;
+ kit->pos = NULL;
+ return 0;
+}
+
+/**
+ * bpf_iter_blkg_next - Return the next online blkg of the iterated block cgroup
+ * @it: iterator
+ *
+ * Return: the next online blkg, or NULL when the walk is done.
+ */
+__bpf_kfunc struct blkcg_gq *bpf_iter_blkg_next(struct bpf_iter_blkg *it)
+{
+ struct bpf_iter_blkg_kern *kit = (void *)it;
+ struct blkcg_gq *blkg = kit->pos;
+ struct hlist_node *node;
+
+ /* Cleared once the walk is done, see below. */
+ if (!kit->blkcg)
+ return NULL;
+
+ if (!blkg)
+ node = rcu_dereference(hlist_first_rcu(&kit->blkcg->blkg_list));
+ else
+ node = rcu_dereference(hlist_next_rcu(&blkg->blkcg_node));
+
+ /* Skip offline blkgs, matching the io.stat file. */
+ while (node) {
+ blkg = hlist_entry(node, struct blkcg_gq, blkcg_node);
+ if (blkg->online) {
+ kit->pos = blkg;
+ return blkg;
+ }
+ node = rcu_dereference(hlist_next_rcu(&blkg->blkcg_node));
+ }
+
+ /*
+ * Forget the list head as well. The verifier assumes that an iterator
+ * which returned NULL keeps returning NULL, and stops checking the
+ * loop for termination once it has; starting the walk over would let
+ * such a loop spin forever.
+ */
+ kit->pos = NULL;
+ kit->blkcg = NULL;
+ return NULL;
+}
+
+/**
+ * bpf_iter_blkg_destroy - Tear down a blkg iterator
+ * @it: iterator
+ */
+__bpf_kfunc void bpf_iter_blkg_destroy(struct bpf_iter_blkg *it)
+{
+}
+
+/*
+ * Read one counter out of @blkg's flushed io.stat aggregate. @counters is one
+ * of the two arrays in blkg->iostat.cur; both are guarded by that struct's
+ * seqlock, the one the io.stat file uses. Returns (u64)-1 if the counter
+ * cannot be read.
+ */
+static u64 blkg_iostat_read(struct blkcg_gq *blkg, const u64 *counters,
+ enum blkg_iostat_type rw)
+{
+ struct blkg_iostat_set *bis = &blkg->iostat;
+ unsigned int seq;
+ u64 val;
+
+ if ((unsigned int)rw >= BLKG_IOSTAT_NR)
+ return (u64)-1;
+
+ /*
+ * On 32-bit the loop below really is a seqcount retry loop. Every
+ * writer of blkg->iostat keeps interrupts off, so only an NMI can land
+ * inside an update, and then the loop would never end. These kfuncs
+ * are reachable from a perf event program, which does run in NMI, so
+ * give up rather than spin. On 64-bit the loop compiles away.
+ */
+ if (BITS_PER_LONG == 32 && in_nmi())
+ return (u64)-1;
+
+ do {
+ seq = u64_stats_fetch_begin(&bis->sync);
+ val = counters[rw];
+ } while (u64_stats_fetch_retry(&bis->sync, seq));
+
+ return val;
+}
+
+/**
+ * bpf_blkg_iostat_bytes - Read a device's io.stat byte counter
+ * @blkg: block group (one device of a block cgroup)
+ * @rw: which counter (BLKG_IOSTAT_READ / _WRITE / _DISCARD)
+ *
+ * Reads the flushed aggregate, so call bpf_blkcg_flush_stats() first for
+ * up-to-date values. The read uses the u64_stats seqlock, like the io.stat
+ * file.
+ *
+ * Return: the number of bytes, or (u64)-1 if @rw is out of range or the
+ * counter cannot be read.
+ */
+__bpf_kfunc u64 bpf_blkg_iostat_bytes(struct blkcg_gq *blkg,
+ enum blkg_iostat_type rw)
+{
+ return blkg_iostat_read(blkg, blkg->iostat.cur.bytes, rw);
+}
+
+/**
+ * bpf_blkg_iostat_ios - Read a device's io.stat I/O count
+ * @blkg: block group (one device of a block cgroup)
+ * @rw: which counter (BLKG_IOSTAT_READ / _WRITE / _DISCARD)
+ *
+ * Return: the number of I/Os, or (u64)-1 if @rw is out of range or the
+ * counter cannot be read.
+ */
+__bpf_kfunc u64 bpf_blkg_iostat_ios(struct blkcg_gq *blkg,
+ enum blkg_iostat_type rw)
+{
+ return blkg_iostat_read(blkg, blkg->iostat.cur.ios, rw);
+}
+
+/**
+ * bpf_blkg_dev - Return a blkg's device id
+ * @blkg: block group
+ *
+ * Return: the device's dev_t (use MAJOR()/MINOR() to split), or 0 if the blkg
+ * has no disk.
+ */
+__bpf_kfunc u64 bpf_blkg_dev(struct blkcg_gq *blkg)
+{
+ if (!blkg->q || !blkg->q->disk)
+ return 0;
+
+ return blkg->q->disk->part0->bd_dev;
+}
+
+__bpf_kfunc_end_defs();
+
+BTF_KFUNCS_START(bpf_blkcg_kfuncs)
+BTF_ID_FLAGS(func, bpf_get_root_blkcg, KF_ACQUIRE | KF_RET_NULL)
+BTF_ID_FLAGS(func, bpf_get_blkcg, KF_ACQUIRE | KF_RET_NULL | KF_RCU)
+BTF_ID_FLAGS(func, bpf_put_blkcg, KF_RELEASE)
+BTF_ID_FLAGS(func, bpf_blkcg_flush_stats, KF_SLEEPABLE)
+
+BTF_ID_FLAGS(func, bpf_iter_blkg_new, KF_ITER_NEW | KF_RCU_PROTECTED)
+BTF_ID_FLAGS(func, bpf_iter_blkg_next, KF_ITER_NEXT | KF_RET_NULL)
+BTF_ID_FLAGS(func, bpf_iter_blkg_destroy, KF_ITER_DESTROY)
+
+BTF_ID_FLAGS(func, bpf_blkg_iostat_bytes, KF_RCU)
+BTF_ID_FLAGS(func, bpf_blkg_iostat_ios, KF_RCU)
+BTF_ID_FLAGS(func, bpf_blkg_dev, KF_RCU)
+BTF_KFUNCS_END(bpf_blkcg_kfuncs)
+
+static const struct btf_kfunc_id_set bpf_blkcg_kfunc_set = {
+ .owner = THIS_MODULE,
+ .set = &bpf_blkcg_kfuncs,
+};
+
+static int __init bpf_blkcg_init(void)
+{
+ int err;
+
+ err = register_btf_kfunc_id_set(BPF_PROG_TYPE_UNSPEC,
+ &bpf_blkcg_kfunc_set);
+ if (err)
+ pr_warn("error while registering bpf blkcg kfuncs: %d\n", err);
+
+ return err;
+}
+late_initcall(bpf_blkcg_init);
diff --git a/tools/testing/selftests/bpf/cgroup_iter_io.h b/tools/testing/selftests/bpf/cgroup_iter_io.h
new file mode 100644
index 0000000000000..f4bbaaccdf716
--- /dev/null
+++ b/tools/testing/selftests/bpf/cgroup_iter_io.h
@@ -0,0 +1,17 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/* Copyright (c) 2025 Meta Platforms, Inc. and affiliates. */
+#ifndef __CGROUP_ITER_IO_H
+#define __CGROUP_ITER_IO_H
+
+struct io_query {
+ /* one device's io.stat counters */
+ __u64 rbytes;
+ __u64 wbytes;
+ __u64 rios;
+ __u64 wios;
+ __u64 dbytes;
+ __u64 dios;
+ __u64 dev; /* dev_t of the device the counters belong to */
+};
+
+#endif /* __CGROUP_ITER_IO_H */
diff --git a/tools/testing/selftests/bpf/config b/tools/testing/selftests/bpf/config
index ea7044f30adc3..270e6bf9194d2 100644
--- a/tools/testing/selftests/bpf/config
+++ b/tools/testing/selftests/bpf/config
@@ -1,3 +1,4 @@
+CONFIG_BLK_CGROUP=y
CONFIG_BLK_DEV_LOOP=y
CONFIG_BOOTPARAM_HARDLOCKUP_PANIC=y
CONFIG_BOOTPARAM_SOFTLOCKUP_PANIC=1
diff --git a/tools/testing/selftests/bpf/prog_tests/cgroup_iter_io.c b/tools/testing/selftests/bpf/prog_tests/cgroup_iter_io.c
new file mode 100644
index 0000000000000..32cda82433183
--- /dev/null
+++ b/tools/testing/selftests/bpf/prog_tests/cgroup_iter_io.c
@@ -0,0 +1,310 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (c) 2025 Meta Platforms, Inc. and affiliates. */
+#define _GNU_SOURCE
+#include <test_progs.h>
+#include <bpf/libbpf.h>
+#include <fcntl.h>
+#include <linux/loop.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/ioctl.h>
+#include <sys/stat.h>
+#include <sys/sysmacros.h>
+#include <unistd.h>
+#include "cgroup_helpers.h"
+#include "cgroup_iter_io.h"
+#include "cgroup_iter_io.skel.h"
+
+#define IO_SIZE (4 * 1024 * 1024)
+
+static int read_stats(struct bpf_link *link)
+{
+ int fd, ret = 0;
+ ssize_t bytes;
+
+ fd = bpf_iter_create(bpf_link__fd(link));
+ if (!ASSERT_OK_FD(fd, "bpf_iter_create"))
+ return 1;
+
+ /* Results land in skel->data_query; the read itself returns no data. */
+ bytes = read(fd, NULL, 0);
+ if (!ASSERT_EQ(bytes, 0, "read fd"))
+ ret = 1;
+
+ close(fd);
+ return ret;
+}
+
+/*
+ * Attach a loop device to an anonymous temp file so we have a real block
+ * device to generate cgroup-charged I/O against. Returns 0 on success, or -1
+ * if loop devices are unavailable (non-root / no CONFIG_BLK_DEV_LOOP) so the
+ * caller can skip.
+ */
+static int loop_setup(char *loop_path, size_t sz, int *ctl_fd, int *loop_fd,
+ int *back_fd)
+{
+ char back_path[] = "/tmp/cgroup_iter_io.XXXXXX";
+ int nr;
+
+ *ctl_fd = *loop_fd = *back_fd = -1;
+
+ *ctl_fd = open("/dev/loop-control", O_RDWR | O_CLOEXEC);
+ if (*ctl_fd < 0)
+ return -1;
+
+ nr = ioctl(*ctl_fd, LOOP_CTL_GET_FREE);
+ if (nr < 0)
+ goto err;
+ snprintf(loop_path, sz, "/dev/loop%d", nr);
+
+ *back_fd = mkstemp(back_path);
+ if (*back_fd < 0)
+ goto err;
+ unlink(back_path);
+ if (ftruncate(*back_fd, (off_t)IO_SIZE * 4))
+ goto err;
+
+ *loop_fd = open(loop_path, O_RDWR | O_CLOEXEC);
+ if (*loop_fd < 0)
+ goto err;
+ if (ioctl(*loop_fd, LOOP_SET_FD, *back_fd))
+ goto err;
+
+ return 0;
+err:
+ if (*loop_fd >= 0)
+ close(*loop_fd);
+ if (*back_fd >= 0)
+ close(*back_fd);
+ close(*ctl_fd);
+ *ctl_fd = *loop_fd = *back_fd = -1;
+ return -1;
+}
+
+static void loop_teardown(const char *loop_path, int ctl_fd, int loop_fd,
+ int back_fd)
+{
+ int nr = -1;
+
+ if (loop_fd >= 0) {
+ ioctl(loop_fd, LOOP_CLR_FD, 0);
+ close(loop_fd);
+ }
+ if (back_fd >= 0)
+ close(back_fd);
+ if (ctl_fd >= 0) {
+ if (sscanf(loop_path, "/dev/loop%d", &nr) == 1 && nr >= 0)
+ ioctl(ctl_fd, LOOP_CTL_REMOVE, nr);
+ close(ctl_fd);
+ }
+}
+
+/* O_DIRECT I/O to the loop device, charged to the current cgroup. */
+static int do_direct_io(const char *loop_path)
+{
+ void *buf;
+ int fd, ret = -1;
+
+ fd = open(loop_path, O_RDWR | O_DIRECT | O_CLOEXEC);
+ if (fd < 0)
+ return -1;
+ if (posix_memalign(&buf, 4096, IO_SIZE))
+ goto out_fd;
+ memset(buf, 0xab, IO_SIZE);
+
+ if (pwrite(fd, buf, IO_SIZE, 0) != IO_SIZE)
+ goto out_buf;
+ fsync(fd);
+ if (pread(fd, buf, IO_SIZE, 0) != IO_SIZE)
+ goto out_buf;
+ ret = 0;
+out_buf:
+ free(buf);
+out_fd:
+ close(fd);
+ return ret;
+}
+
+/*
+ * Parse the io.stat line for device @dev out of the cgroup's io.stat file and
+ * fill @out. @dev is a kernel dev_t (as returned by bpf_blkg_dev), whose
+ * major:minor split matches how io.stat prints the device. Returns 0 if the
+ * device's line was found.
+ */
+static int parse_io_stat(int cgroup_fd, __u64 dev, struct io_query *out)
+{
+ unsigned int want_maj = dev >> 20, want_min = dev & ((1U << 20) - 1);
+ char buf[4096], *line, *saveptr;
+ int fd, n, ret = -1;
+
+ fd = openat(cgroup_fd, "io.stat", O_RDONLY);
+ if (fd < 0)
+ return -1;
+ n = read(fd, buf, sizeof(buf) - 1);
+ close(fd);
+ if (n <= 0)
+ return -1;
+ buf[n] = '\0';
+
+ for (line = strtok_r(buf, "\n", &saveptr); line;
+ line = strtok_r(NULL, "\n", &saveptr)) {
+ unsigned long long rb = 0, wb = 0, ri = 0, wi = 0, db = 0, di = 0;
+ unsigned int maj, min;
+
+ /*
+ * The "maj:min" token is always present; the field block is
+ * optional (the kernel omits it for a device with no read/write
+ * I/O), so a match of >= 2 is enough and absent fields stay 0.
+ */
+ if (sscanf(line,
+ "%u:%u rbytes=%llu wbytes=%llu rios=%llu wios=%llu dbytes=%llu dios=%llu",
+ &maj, &min, &rb, &wb, &ri, &wi, &db, &di) < 2)
+ continue;
+ if (maj != want_maj || min != want_min)
+ continue;
+
+ out->rbytes = rb;
+ out->wbytes = wb;
+ out->rios = ri;
+ out->wios = wi;
+ out->dbytes = db;
+ out->dios = di;
+ ret = 0;
+ break;
+ }
+ return ret;
+}
+
+void test_cgroup_iter_io(void)
+{
+ char *cgroup_rel_path = "/cgroup_iter_io_test";
+ int ctl_fd = -1, loop_fd = -1, back_fd = -1;
+ struct cgroup_iter_io *skel = NULL;
+ struct bpf_link *link = NULL;
+ char loop_path[64];
+ struct io_query *q;
+ int cgroup_fd;
+
+ cgroup_fd = cgroup_setup_and_join(cgroup_rel_path);
+ if (!ASSERT_OK_FD(cgroup_fd, "cgroup_setup_and_join"))
+ return;
+
+ if (loop_setup(loop_path, sizeof(loop_path), &ctl_fd, &loop_fd, &back_fd)) {
+ test__skip(); /* needs root + CONFIG_BLK_DEV_LOOP */
+ goto cleanup_cgroup_fd;
+ }
+
+ skel = cgroup_iter_io__open_and_load();
+ if (!ASSERT_OK_PTR(skel, "cgroup_iter_io__open_and_load"))
+ goto cleanup_loop;
+
+ /*
+ * Pin the read to the loop device so the measured device is stable and
+ * quiesced. Convert the glibc-encoded st_rdev to the kernel dev_t
+ * encoding (major << 20 | minor) that bpf_blkg_dev returns.
+ */
+ {
+ struct stat lst;
+
+ if (!ASSERT_OK(fstat(loop_fd, &lst), "fstat loop"))
+ goto cleanup_skel;
+ skel->data_query->target_dev =
+ ((__u64)major(lst.st_rdev) << 20) | minor(lst.st_rdev);
+ }
+
+ DECLARE_LIBBPF_OPTS(bpf_iter_attach_opts, opts);
+ union bpf_iter_link_info linfo = {
+ .cgroup.cgroup_fd = cgroup_fd,
+ .cgroup.order = BPF_CGROUP_ITER_SELF_ONLY,
+ };
+ opts.link_info = &linfo;
+ opts.link_info_len = sizeof(linfo);
+
+ link = bpf_program__attach_iter(skel->progs.cgroup_io_query, &opts);
+ if (!ASSERT_OK_PTR(link, "bpf_program__attach_iter"))
+ goto cleanup_skel;
+
+ /* This process is in the test cgroup, so the loop I/O is charged here. */
+ if (!ASSERT_OK(do_direct_io(loop_path), "do_direct_io"))
+ goto cleanup_link;
+
+ if (!ASSERT_OK(read_stats(link), "read stats"))
+ goto cleanup_link;
+
+ /*
+ * Weak check: we did I/O, so the numbers must be non-zero. Follows the
+ * pattern in cgroup_iter_memcg.
+ */
+ q = &skel->data_query->io_query;
+ if (test__start_subtest("cgroup_iter_io__write")) {
+ ASSERT_GT(q->wbytes, 0, "wbytes");
+ ASSERT_GT(q->wios, 0, "wios");
+ }
+ if (test__start_subtest("cgroup_iter_io__read")) {
+ ASSERT_GT(q->rbytes, 0, "rbytes");
+ ASSERT_GT(q->rios, 0, "rios");
+ }
+ if (test__start_subtest("cgroup_iter_io__dev"))
+ ASSERT_GT(q->dev, 0, "dev");
+
+ /*
+ * Stronger check: the kfunc-read values must equal what the io.stat
+ * file reports for the same device. Refresh via the prog, then read
+ * the file with no I/O in between, so both flushed snapshots match
+ * exactly.
+ */
+ if (test__start_subtest("cgroup_iter_io__match")) {
+ struct io_query filev = {};
+
+ if (ASSERT_OK(read_stats(link), "read stats") &&
+ ASSERT_OK(parse_io_stat(cgroup_fd, q->dev, &filev),
+ "parse io.stat")) {
+ ASSERT_EQ(q->rbytes, filev.rbytes, "rbytes");
+ ASSERT_EQ(q->wbytes, filev.wbytes, "wbytes");
+ ASSERT_EQ(q->rios, filev.rios, "rios");
+ ASSERT_EQ(q->wios, filev.wios, "wios");
+ ASSERT_EQ(q->dbytes, filev.dbytes, "dbytes");
+ ASSERT_EQ(q->dios, filev.dios, "dios");
+ }
+ }
+
+ /*
+ * Separate program for the root block cgroup. Its counters do not come
+ * from rstat, they are refilled from the disks themselves, so this
+ * covers the other half of bpf_blkcg_flush_stats(). They cover every
+ * cgroup's I/O to the loop device, and only this test touches it, so
+ * they must be at or above what the test cgroup was charged.
+ */
+ if (test__start_subtest("cgroup_iter_io__root")) {
+ struct bpf_link *root_link;
+ struct io_query *r;
+
+ skel->data_query->got_root_blkcg = 0;
+ root_link = bpf_program__attach_iter(skel->progs.cgroup_root_blkcg_query,
+ &opts);
+ if (ASSERT_OK_PTR(root_link, "attach root iter")) {
+ if (ASSERT_OK(read_stats(root_link), "read root stats")) {
+ r = &skel->data_query->root_query;
+ ASSERT_EQ(skel->data_query->got_root_blkcg, 1,
+ "got_root_blkcg");
+ ASSERT_EQ(r->dev, q->dev, "root dev");
+ ASSERT_GE(r->wbytes, q->wbytes, "root wbytes");
+ ASSERT_GE(r->wios, q->wios, "root wios");
+ ASSERT_GE(r->rbytes, q->rbytes, "root rbytes");
+ ASSERT_GE(r->rios, q->rios, "root rios");
+ }
+ bpf_link__destroy(root_link);
+ }
+ }
+
+cleanup_link:
+ bpf_link__destroy(link);
+cleanup_skel:
+ cgroup_iter_io__destroy(skel);
+cleanup_loop:
+ loop_teardown(loop_path, ctl_fd, loop_fd, back_fd);
+cleanup_cgroup_fd:
+ close(cgroup_fd);
+ cleanup_cgroup_environment();
+}
diff --git a/tools/testing/selftests/bpf/progs/cgroup_iter_io.c b/tools/testing/selftests/bpf/progs/cgroup_iter_io.c
new file mode 100644
index 0000000000000..b839def945086
--- /dev/null
+++ b/tools/testing/selftests/bpf/progs/cgroup_iter_io.c
@@ -0,0 +1,107 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (c) 2025 Meta Platforms, Inc. and affiliates. */
+#include <vmlinux.h>
+#include <bpf/bpf_helpers.h>
+#include <bpf/bpf_core_read.h>
+#include "bpf_experimental.h"
+#include "cgroup_iter_io.h"
+
+char _license[] SEC("license") = "GPL";
+
+/* The counters of the device named by target_dev are stored here. */
+struct io_query io_query SEC(".data.query");
+
+/* The same device's counters read through the root block cgroup. */
+struct io_query root_query SEC(".data.query");
+
+/* Set to 1 by cgroup_root_blkcg_query when bpf_get_root_blkcg() succeeds. */
+__u64 got_root_blkcg SEC(".data.query");
+
+/* Device to read, set by userspace (kernel dev_t). Pinning the device keeps
+ * the read deterministic and lets the value be compared to io.stat exactly.
+ */
+__u64 target_dev SEC(".data.query");
+
+/*
+ * Flush @blkcg and copy the target device's counters into @out. Reading only
+ * the one pinned device keeps the result deterministic: that device is
+ * quiesced, so its counters match io.stat exactly, while picking "any device
+ * with I/O" would race with backing-store writeback.
+ */
+static __always_inline void read_target_dev(struct blkcg *blkcg,
+ struct io_query *out)
+{
+ struct blkcg_gq *pos;
+
+ /* io.stat needs a flush before it can be read (sleepable). */
+ bpf_blkcg_flush_stats(blkcg);
+
+ /* The per-device blkg walk needs an RCU section. */
+ bpf_rcu_read_lock();
+ bpf_for_each(blkg, pos, blkcg) {
+ if (bpf_blkg_dev(pos) != target_dev)
+ continue;
+
+ out->dev = bpf_blkg_dev(pos);
+ out->rbytes = bpf_blkg_iostat_bytes(pos, BLKG_IOSTAT_READ);
+ out->wbytes = bpf_blkg_iostat_bytes(pos, BLKG_IOSTAT_WRITE);
+ out->rios = bpf_blkg_iostat_ios(pos, BLKG_IOSTAT_READ);
+ out->wios = bpf_blkg_iostat_ios(pos, BLKG_IOSTAT_WRITE);
+ out->dbytes = bpf_blkg_iostat_bytes(pos, BLKG_IOSTAT_DISCARD);
+ out->dios = bpf_blkg_iostat_ios(pos, BLKG_IOSTAT_DISCARD);
+ break;
+ }
+ bpf_rcu_read_unlock();
+}
+
+SEC("iter.s/cgroup")
+int cgroup_io_query(struct bpf_iter__cgroup *ctx)
+{
+ struct cgroup *cgrp = ctx->cgroup;
+ struct blkcg *blkcg;
+
+ /* The last iteration has a NULL cgroup, skip it. */
+ if (!cgrp)
+ return 1;
+
+ /* Start fresh so a device that is not found stays all-zero. */
+ __builtin_memset(&io_query, 0, sizeof(io_query));
+
+ blkcg = bpf_get_blkcg(&cgrp->self);
+ if (!blkcg)
+ return 0;
+
+ read_target_dev(blkcg, &io_query);
+
+ bpf_put_blkcg(blkcg);
+ return 0;
+}
+
+SEC("iter.s/cgroup")
+int cgroup_root_blkcg_query(struct bpf_iter__cgroup *ctx)
+{
+ struct cgroup *cgrp = ctx->cgroup;
+ struct blkcg *blkcg;
+
+ /* The last iteration has a NULL cgroup, skip it. */
+ if (!cgrp)
+ return 1;
+
+ __builtin_memset(&root_query, 0, sizeof(root_query));
+
+ blkcg = bpf_get_root_blkcg();
+ if (!blkcg)
+ return 0;
+
+ /*
+ * The root cgroup takes its numbers from the disks themselves rather
+ * than from rstat, so this also covers the root side of
+ * bpf_blkcg_flush_stats(). The counters cover every cgroup's I/O, so
+ * they can only be at or above what this test's own cgroup did.
+ */
+ read_target_dev(blkcg, &root_query);
+
+ got_root_blkcg = 1;
+ bpf_put_blkcg(blkcg);
+ return 0;
+}
|
| 21/3 | 2026/08/07 21:03 | llm | kmsan-evaluator |
0mModel:gemini-3.1-pro-preview Args: null Results: null Tokens: input: 12491 output: 169 thoughts: 1364 Thoughts: **KMSAN Assessment: Block Cgroup BPF Kfuncs** Okay, let's break this down. My initial thought is to determine if this patch series warrants a dedicated KMSAN fuzzing session. The patch focuses on introducing BPF kfuncs to read block cgroup (blkcg) I/O statistics, which is interesting. First, I need to evaluate the potential for uninitialized memory risks. We're adding functions like `bpf_get_root_blkcg`, `bpf_get_blkcg`, `bpf_put_blkcg`, and others, all dealing with reading data from existing kernel structures like `struct blkcg`, `struct blkcg_gq`, and `struct blkg_iostat_set`. The critical question is, are these kfuncs returning uninitialized values to the BPF programs? The kfuncs return either `u64` values or pointers to kernel structures. BPF programs then read these returned values. I need to pinpoint if there are any instances where the kfuncs are pulling uninitialized fields and passing them back to BPF programs. Specifically, I'm looking at `bpf_blkg_iostat_bytes` and `bpf_blkg_iostat_ios`, which read from `blkg->iostat.cur.bytes` and `blkg->iostat.cur.ios`, both arrays of `u64`. Also, `bpf_blkg_dev` that returns `blkg->q->disk->part0->bd_dev` which is a `dev_t` – either `u32` or `u64`. The patch also has a selftest, so I'll check that. Now, let's dig into `bpf_iter_blkg_new` and `bpf_iter_blkg_next`. Standard BPF iterator implementation. The verifier in BPF ensures proper initialization of `bpf_iter_blkg_new`. It initializes `kit->blkcg` and `kit->pos`. No issues here as the iterator state is managed by the BPF infrastructure. Okay, next concern: are there any instances of uninitialized memory being copied to user space? BPF programs read counters and store them in global variables (`io_query`, `root_query`), which are then accessed by user space via the BPF skeleton. These counters are extracted from `blkg->iostat.cur.bytes` and `blkg->iostat.cur.ios`, but these are updated by the block layer, and should be fully initialized. Then, are there any direct transmissions of kernel structures to user space? No, this patch only exposes BPF kfuncs, which are then called by BPF programs. These kfuncs return `u64` or pointers. There are no `copy_to_user` calls, and no complex data structures are allocated and sent directly to user space. The iterator state is a 16-byte structure, which is fully initialized during `bpf_iter_blkg_new`. Furthermore, I have to consider conditional logic that might depend on uninitialized variables. This is not the case because the variables are either pointers from the BPF context or read from existing initialized kernel structures. Therefore, the likelihood of this patch introducing uninitialized memory usage or information leaks is extremely low. KASAN and standard tools should suffice to detect any potential out-of-bounds accesses or use-after-free bugs stemming from incorrect `css_tryget` or RCU usage, if any. I still want to double check `blkg_iostat_read` though: Looking at the code, it's just reading `u64` values from `counters[rw]`. The bounds are checked against `BLKG_IOSTAT_NR`, so it's safe. My conclusion? **NeedsKMSAN = false.** The patch introduces BPF kfuncs to read block cgroup I/O statistics, but it doesn't do anything that creates a strong need for KMSAN. There's no creation of new complex structures, no direct data copy to user space, and no logic that could expose uninitialized memory. Standard memory safety tools such as KASAN are enough. |
| 22/3 | 2026/08/07 21:03 | tool | set-results |
0mArgs:{
"KMSANReasoning": "The patch introduces BPF kfuncs to read block cgroup I/O statistics (io.stat). It does not allocate new complex structures, copy data directly to user space, or introduce logic that could expose uninitialized memory. The kfuncs return simple scalar values (u64) or pointers to existing, initialized kernel structures. The BPF iterator state is fully initialized in `bpf_iter_blkg_new`. Any potential memory safety issues, such as out-of-bounds accesses or use-after-free bugs related to RCU or cgroup references, would be effectively caught by KASAN and standard bug detectors. Therefore, a dedicated KMSAN fuzzing session is not justified.",
"NeedsKMSAN": false
}Results: {
"KMSANReasoning": "The patch introduces BPF kfuncs to read block cgroup I/O statistics (io.stat). It does not allocate new complex structures, copy data directly to user space, or introduce logic that could expose uninitialized memory. The kfuncs return simple scalar values (u64) or pointers to existing, initialized kernel structures. The BPF iterator state is fully initialized in `bpf_iter_blkg_new`. Any potential memory safety issues, such as out-of-bounds accesses or use-after-free bugs related to RCU or cgroup references, would be effectively caught by KASAN and standard bug detectors. Therefore, a dedicated KMSAN fuzzing session is not justified.",
"NeedsKMSAN": false
} |
| Total Calls | Total Tokens | Avg Tokens | Total Duration (Seconds) | Avg Duration (Seconds) |
|---|
| Total Calls | Total Duration (Seconds) | Avg Duration (Seconds) |
|---|