From: Aaron Tomlin <atomlin@atomlin.com>
To: mingo@redhat.com, peterz@infradead.org, juri.lelli@redhat.com,
vincent.guittot@linaro.org
Cc: dietmar.eggemann@arm.com, rostedt@goodmis.org,
bsegall@google.com, mgorman@suse.de, vschneid@redhat.com,
kprateek.nayak@amd.com, zhanxusheng1024@gmail.com,
neelx@suse.com, atomlin@atomlin.com, chjohnst@mail.com,
mproche@mail.com, sean@ashe.io, steve@abita.co,
rishil1999@outlook.com, linux-kernel@vger.kernel.org
Subject: [PATCH v5 5/6] sched/fair: Use list_for_each_entry_rcu() in print_cfs_stats()
Date: Tue, 25 Aug 2026 10:14:12 -0400 [thread overview]
Message-ID: <20260825141413.868997-6-atomlin@atomlin.com> (raw)
In-Reply-To: <20260825141413.868997-1-atomlin@atomlin.com>
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@kernel.org>
Signed-off-by: Aaron Tomlin <atomlin@atomlin.com>
---
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
next prev parent reply other threads:[~2026-08-25 14:14 UTC|newest]
Thread overview: 8+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-08-25 14:14 [PATCH v5 0/6] sched/debug: Introduce per-CPU debugfs files Aaron Tomlin
2026-08-25 14:14 ` [PATCH v5 1/6] sched: Annotate rq->rd with __rcu and update lockless readers Aaron Tomlin
2026-08-25 14:14 ` [PATCH v5 2/6] sched/debug: Protect lockless rq->rd access in print_dl_rq() Aaron Tomlin
2026-08-25 14:14 ` [PATCH v5 3/6] sched/debug: Protect lockless rq->curr access in print_cpu() Aaron Tomlin
2026-08-25 14:14 ` [PATCH v5 4/6] sched/debug: Protect p->mm access in sched_show_numa() Aaron Tomlin
2026-08-25 14:14 ` Aaron Tomlin [this message]
2026-08-25 14:14 ` [PATCH v5 6/6] sched/debug: Introduce per-CPU debugfs files Aaron Tomlin
2026-08-25 16:30 ` [PATCH v5 0/6] " Aaron Tomlin
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260825141413.868997-6-atomlin@atomlin.com \
--to=atomlin@atomlin.com \
--cc=bsegall@google.com \
--cc=chjohnst@mail.com \
--cc=dietmar.eggemann@arm.com \
--cc=juri.lelli@redhat.com \
--cc=kprateek.nayak@amd.com \
--cc=linux-kernel@vger.kernel.org \
--cc=mgorman@suse.de \
--cc=mingo@redhat.com \
--cc=mproche@mail.com \
--cc=neelx@suse.com \
--cc=peterz@infradead.org \
--cc=rishil1999@outlook.com \
--cc=rostedt@goodmis.org \
--cc=sean@ashe.io \
--cc=steve@abita.co \
--cc=vincent.guittot@linaro.org \
--cc=vschneid@redhat.com \
--cc=zhanxusheng1024@gmail.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox