[PATCH v5 5/6] sched/fair: Use list_for_each_entry_rcu() in print_cfs_stats()

From: Aaron Tomlin

Date: Tue Aug 25 2026 - 10:16:57 EST


In print_cfs_stats(), rq->leaf_cfs_rq_list is traversed using
for_each_leaf_cfs_rq_safe(), which expands to list_for_each_entry_safe().
Although rq->leaf_cfs_rq_list is RCU-protected, list_for_each_entry_safe()
is a non-RCU iteration macro. It dereferences pointer links without
READ_ONCE() and pre-fetches the next pointer.

When a writer concurrently adds a new cfs_rq to the list using
list_add_rcu(), a reader traversing with list_for_each_entry_safe()
lacks READ_ONCE() protection. Without READ_ONCE(), the compiler is free
to re-fetch pointers or reorder instructions. As a result, the reader
can observe a newly inserted cfs_rq's pointer before its internal fields
are fully visible, leading to reading uninitialised data or
dereferencing invalid pointers.

Additionally, because print_cfs_rq() drops rq->lock during seq_file I/O,
concurrent cfs_rq list removals and re-insertions can modify
cfs_rq->next. If cfs_rq is re-inserted near the head of the list while
rq->lock is dropped, lockless readers can jump backward in the list.
Under sufficient load it could lead to unbounded list traversal inside
the RCU read-side critical section and trigger an RCU stall.

Fix this by introducing for_each_leaf_cfs_rq_rcu(), which expands to
list_for_each_entry_rcu(). This uses READ_ONCE() during list traversal.
Finally, capping print_cfs_stats() lockless list traversal with a hard
iteration ceiling to guarantee loop termination and prevent RCU stalls
under continuous list churn.

Fixes: 039ae8bcf7a5 ("sched/fair: Fix O(nr_cgroups) in the load balancing path")
Reported-by: sashiko-bot <sashiko-bot@xxxxxxxxxx>
Signed-off-by: Aaron Tomlin <atomlin@xxxxxxxxxxx>
---
kernel/sched/debug.c | 39 +++------------------------------
kernel/sched/fair.c | 33 ++++++++++++++++++++++++----
kernel/sched/sched.h | 51 ++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 83 insertions(+), 40 deletions(-)

diff --git a/kernel/sched/debug.c b/kernel/sched/debug.c
index c05ba4d8b169..8793059688d7 100644
--- a/kernel/sched/debug.c
+++ b/kernel/sched/debug.c
@@ -11,17 +11,6 @@
#include <linux/log2.h>
#include "sched.h"

-/*
- * This allows printing both to /sys/kernel/debug/sched/debug and
- * to the console
- */
-#define SEQ_printf(m, x...) \
- do { \
- if (m) \
- seq_printf(m, x); \
- else \
- pr_cont(x); \
- } while (0)

/*
* Ease the printing of nsec fields:
@@ -853,38 +842,16 @@ static void print_cfs_group_stats(struct seq_file *m, int cpu, struct task_group
#endif /* CONFIG_FAIR_GROUP_SCHED */

#ifdef CONFIG_CGROUP_SCHED
-static DEFINE_SPINLOCK(sched_debug_lock);
-static char group_path[PATH_MAX];
+DEFINE_SPINLOCK(sched_debug_lock);
+char sched_debug_group_path[PATH_MAX];

-static void task_group_path(struct task_group *tg, char *path, int plen)
+void task_group_path(struct task_group *tg, char *path, int plen)
{
if (autogroup_path(tg, path, plen))
return;

cgroup_path(tg->css.cgroup, path, plen);
}
-
-/*
- * Only 1 SEQ_printf_task_group_path() caller can use the full length
- * group_path[] for cgroup path. Other simultaneous callers will have
- * to use a shorter stack buffer. A "..." suffix is appended at the end
- * of the stack buffer so that it will show up in case the output length
- * matches the given buffer size to indicate possible path name truncation.
- */
-#define SEQ_printf_task_group_path(m, tg, fmt...) \
-{ \
- if (spin_trylock(&sched_debug_lock)) { \
- task_group_path(tg, group_path, sizeof(group_path)); \
- SEQ_printf(m, fmt, group_path); \
- spin_unlock(&sched_debug_lock); \
- } else { \
- char buf[128]; \
- char *bufend = buf + sizeof(buf) - 3; \
- task_group_path(tg, buf, bufend - buf); \
- strcpy(bufend - 1, "..."); \
- SEQ_printf(m, fmt, buf); \
- } \
-}
#endif

static void
diff --git a/kernel/sched/fair.c b/kernel/sched/fair.c
index ad367a542eb0..b85f826be450 100644
--- a/kernel/sched/fair.c
+++ b/kernel/sched/fair.c
@@ -412,6 +412,10 @@ static inline void assert_list_leaf_cfs_rq(struct rq *rq)
list_for_each_entry_safe(cfs_rq, pos, &rq->leaf_cfs_rq_list, \
leaf_cfs_rq_list)

+#define for_each_leaf_cfs_rq_rcu(rq, cfs_rq) \
+ list_for_each_entry_rcu(cfs_rq, &(rq)->leaf_cfs_rq_list, \
+ leaf_cfs_rq_list)
+
/* Do the two (enqueued) entities belong to the same group ? */
static inline struct cfs_rq *
is_same_group(struct sched_entity *se, struct sched_entity *pse)
@@ -497,6 +501,9 @@ static inline void assert_list_leaf_cfs_rq(struct rq *rq)
#define for_each_leaf_cfs_rq_safe(rq, cfs_rq, pos) \
for (cfs_rq = &rq->cfs, pos = NULL; cfs_rq; cfs_rq = pos)

+#define for_each_leaf_cfs_rq_rcu(rq, cfs_rq) \
+ for (cfs_rq = &rq->cfs; cfs_rq; cfs_rq = NULL)
+
static inline struct sched_entity *parent_entity(struct sched_entity *se)
{
return NULL;
@@ -15400,14 +15407,32 @@ DEFINE_SCHED_CLASS(fair) = {
#endif
};

+#define SCHED_DEBUG_MAX_ITER 4096
+#define SCHED_DEBUG_TRUNCATED_MSG \
+ "stats truncated at " __stringify(SCHED_DEBUG_MAX_ITER) " iterations\n"
+
void print_cfs_stats(struct seq_file *m, int cpu)
{
- struct cfs_rq *cfs_rq, *pos;
+ struct cfs_rq *cfs_rq;
+ int max_iter = SCHED_DEBUG_MAX_ITER;

- rcu_read_lock();
- for_each_leaf_cfs_rq_safe(cpu_rq(cpu), cfs_rq, pos)
+ guard(rcu)();
+ for_each_leaf_cfs_rq_rcu(cpu_rq(cpu), cfs_rq) {
+ if (--max_iter < 0) {
+ SEQ_printf(m, "\n");
+ if (IS_ENABLED(CONFIG_FAIR_GROUP_SCHED)) {
+ SEQ_printf_task_group_path(m, cfs_rq_tg(cfs_rq),
+ "cfs_rq[%d]:%s ... "
+ SCHED_DEBUG_TRUNCATED_MSG,
+ cpu);
+ } else {
+ SEQ_printf(m, "cfs_rq[%d]: "
+ SCHED_DEBUG_TRUNCATED_MSG, cpu);
+ }
+ break;
+ }
print_cfs_rq(m, cpu, cfs_rq);
- rcu_read_unlock();
+ }
}

#ifdef CONFIG_NUMA_BALANCING
diff --git a/kernel/sched/sched.h b/kernel/sched/sched.h
index aca352e2f4a8..e25b3fd35972 100644
--- a/kernel/sched/sched.h
+++ b/kernel/sched/sched.h
@@ -774,6 +774,18 @@ struct cfs_rq {
#endif /* CONFIG_FAIR_GROUP_SCHED */
};

+#ifdef CONFIG_FAIR_GROUP_SCHED
+static inline struct task_group *cfs_rq_tg(struct cfs_rq *cfs_rq)
+{
+ return cfs_rq->tg;
+}
+#else
+static inline struct task_group *cfs_rq_tg(struct cfs_rq *cfs_rq)
+{
+ return NULL;
+}
+#endif
+
#ifdef CONFIG_SCHED_CLASS_EXT
/* scx_rq->flags, protected by the rq lock */
enum scx_rq_flags {
@@ -3393,6 +3405,45 @@ extern struct sched_entity *__pick_root_entity(struct cfs_rq *cfs_rq);
extern struct sched_entity *__pick_first_entity(struct cfs_rq *cfs_rq);
extern struct sched_entity *__pick_last_entity(struct cfs_rq *cfs_rq);

+/*
+ * This allows printing both to /sys/kernel/debug/sched/debug and
+ * to the console
+ */
+#define SEQ_printf(m, x...) \
+do { \
+ if (m) \
+ seq_printf(m, x); \
+ else \
+ pr_cont(x); \
+} while (0)
+
+#ifdef CONFIG_CGROUP_SCHED
+extern spinlock_t sched_debug_lock;
+extern char sched_debug_group_path[PATH_MAX];
+extern void task_group_path(struct task_group *tg, char *path, int plen);
+
+#define SEQ_printf_task_group_path(m, tg, fmt...) \
+{ \
+ if (spin_trylock(&sched_debug_lock)) { \
+ task_group_path(tg, sched_debug_group_path, sizeof(sched_debug_group_path)); \
+ SEQ_printf(m, fmt, sched_debug_group_path); \
+ spin_unlock(&sched_debug_lock); \
+ } else { \
+ char buf[128]; \
+ char *bufend = buf + sizeof(buf) - 3; \
+ task_group_path(tg, buf, bufend - buf); \
+ strscpy(bufend - 1, "...", sizeof("...")); \
+ SEQ_printf(m, fmt, buf); \
+ } \
+}
+#else
+static inline void __printf(3, 4)
+SEQ_printf_task_group_path(struct seq_file *m, struct task_group *tg,
+ const char *fmt, ...)
+{
+}
+#endif
+
extern bool sched_debug_verbose;

extern void print_cfs_stats(struct seq_file *m, int cpu);
--
2.55.0