* [PATCH v5 0/6] sched/debug: Introduce per-CPU debugfs files
@ 2026-08-25 14:14 Aaron Tomlin
2026-08-25 14:14 ` [PATCH v5 1/6] sched: Annotate rq->rd with __rcu and update lockless readers Aaron Tomlin
` (6 more replies)
0 siblings, 7 replies; 8+ messages in thread
From: Aaron Tomlin @ 2026-08-25 14:14 UTC (permalink / raw)
To: mingo, peterz, juri.lelli, vincent.guittot
Cc: dietmar.eggemann, rostedt, bsegall, mgorman, vschneid,
kprateek.nayak, zhanxusheng1024, neelx, atomlin, chjohnst,
mproche, sean, steve, rishil1999, linux-kernel
Hi Peter, Juri, Ingo, Vincent,
This patch series addresses a few pre-existing memory safety and list
traversal concurrency issues in scheduler debugfs handlers, and introduces
per-CPU debugfs files under /sys/kernel/debug/sched/cpu/cpu<N>/debug.
Patch 1 introduces a new prerequisite patch that annotates struct rq's rd
(root_domain) pointer with __rcu in kernel/sched/sched.h and updates
lockless readers across the core scheduler to use rcu_dereference(),
ensuring Sparse compliance and proper memory barriers on weakly ordered
architectures.
Patch 2 fixes a use-after-free in print_dl_rq() where cpu_rq(cpu)->rd is
dereferenced locklessly to display deadline bandwidth statistics. During
CPU hot-unplug or cgroup cpuset repartitioning events,
partition_sched_domains() calls rq_attach_root() to detach the CPU from its
root_domain and schedules free_rootdomain() via call_rcu(). Without an RCU
read lock, an RCU grace period can resolve concurrently while debugfs reads
the file, allowing free_rootdomain() to execute kfree() and causing a UAF
when reading dl_bw->bw. This patch adds rcu_assign_pointer() on the writer
side in rq_attach_root() and uses guard(rcu)() with rcu_dereference() in
print_dl_rq().
Patch 3 fixes a potential use-after-free in print_cpu() where rq->curr is
dereferenced locklessly to output the running task's PID. If the task exits
concurrently and its reference count drops to zero, put_task_struct()
schedules __put_task_struct_rcu_cb() via call_rcu(). Without holding an RCU
read lock, an RCU grace period can elapse concurrently and free the task
structure via free_task(), leading to a use-after-free race condition. This
patch protects rq->curr access using rcu_dereference() inside an RCU
read-side critical section.
Patch 4 fixes both a time-of-check to time-of-use race condition and a
potential use-after-free in sched_show_numa(), where p->mm is checked
locklessly and then passed to P(mm->numa_scan_seq). If the task exits
concurrently via exit_mm(p), current->mm is set to NULL under task_lock(p)
before mmput() is called to free the struct mm_struct. Wrapping the p->mm
check and dereference in task_lock(p) eliminates both hazards.
Patch 5 fixes an RCU traversal violation in print_cfs_stats() where
rq->leaf_cfs_rq_list is traversed locklessly using
for_each_leaf_cfs_rq_safe(), which expands to list_for_each_entry_safe().
Although leaf_cfs_rq_list is modified using list_add_rcu(),
list_for_each_entry_safe() lacks READ_ONCE() and pre-fetches the next
pointer without memory barriers. Furthermore, because cfs_rq nodes are
re-linked on enqueue/dequeue without waiting for RCU grace periods,
concurrent list churn can cause backward jumps or infinite loops. This
patch introduces for_each_leaf_cfs_rq_rcu(), bounds traversal with a
circuit-breaker ceiling, and emits an explicit truncation notice if the
ceiling is reached.
Patch 6 introduces per-CPU debugfs entries under
/sys/kernel/debug/sched/cpu/cpu<N>/debug, allowing targeted inspection of
an individual CPU's runqueue on demand. If the target CPU is currently
offline, reading its file returns -ENODEV.
Changes since v4:
- Added a new prerequisite patch to annotate struct rq's rd field with
__rcu and updated lockless readers to use
rcu_dereference()/rcu_dereference_sched()
- Updated print_dl_rq() to use guard(rcu)() and rcu_dereference() on
rq->rd (Daniel Vacek and K Prateek Nayak)
- Replaced READ_ONCE(p->mm) with task_lock(p)/task_unlock(p) in
sched_show_numa() to prevent use-after-free against concurrent exit_mm()
and mmput()
- Updated print_cfs_stats() to use guard(rcu)()
- Increased SCHED_DEBUG_MAX_ITER from 1024 to 4096 and added an explicit
truncation notice
- Moved SEQ_printf() and SEQ_printf_task_group_path() to
kernel/sched/sched.h, replaced strcpy() with strscpy(), and used
IS_ENABLED(CONFIG_FAIR_GROUP_SCHED) with a typed static inline fallback
stub
- Corrected the "Fixes:" commit tag in Patch 5 to 039ae8bcf7a5 ("sched/fair:
Fix O(nr_cgroups) in the load balancing path")
- Linked to v4: https://lore.kernel.org/lkml/20260810015812.428999-1-atomlin@atomlin.com/
Changes since v3:
- Updated Patch 1 to use rcu_dereference(rq->curr) instead of READ_ONCE()
to preserve __rcu
- Added missing writer-side RCU publication barrier (rcu_assign_pointer())
in rq_attach_root() for Patch 2
- Added Patch 3 to fix a TOCTOU condition in sched_show_numa() using
READ_ONCE(p->mm)
- Added a safety iteration ceiling in print_cfs_stats() for Patch 4 to
prevent unbounded list iteration and RCU stalls under heavy
leaf_cfs_rq_list churn
- Linked to v3: https://lore.kernel.org/lkml/20260808235522.380038-1-atomlin@atomlin.com/
Changes since v2:
- Protected lockless rq->curr dereferencing in print_cpu() with
rcu_read_lock() and READ_ONCE()
- Protected lockless rq->rd dereferencing in print_dl_rq() against CPU
hot-unplug and cgroup cpuset repartitioning races
- Introduced for_each_leaf_cfs_rq_rcu() using list_for_each_entry_rcu()
for lockless leaf_cfs_rq_list iteration
- Linked to v2: https://lore.kernel.org/lkml/20260728205238.18447-1-atomlin@atomlin.com/
Changes since v1:
- Reframed commit message motivation around targeted interactive
debugging on large SMP topologies (Peter Zijlstra and Zhan Xusheng)
- Gated sched_debug_cpu_show() with a cpu_online(cpu) check
returning -ENODEV when target CPU is offline (Zhan Xusheng)
- Linked to v1: https://lore.kernel.org/lkml/20260728020309.6169-1-atomlin@atomlin.com/
Aaron Tomlin (6):
sched: Annotate rq->rd with __rcu and update lockless readers
sched/debug: Protect lockless rq->rd access in print_dl_rq()
sched/debug: Protect lockless rq->curr access in print_cpu()
sched/debug: Protect p->mm access in sched_show_numa()
sched/fair: Use list_for_each_entry_rcu() in print_cfs_stats()
sched/debug: Introduce per-CPU debugfs files
kernel/sched/core.c | 16 ++++---
kernel/sched/deadline.c | 8 ++--
kernel/sched/debug.c | 92 ++++++++++++++++++++++++-----------------
kernel/sched/fair.c | 62 +++++++++++++++++++--------
kernel/sched/sched.h | 53 +++++++++++++++++++++++-
kernel/sched/topology.c | 2 +-
6 files changed, 165 insertions(+), 68 deletions(-)
--
2.55.0
^ permalink raw reply [flat|nested] 8+ messages in thread
* [PATCH v5 1/6] sched: Annotate rq->rd with __rcu and update lockless readers
2026-08-25 14:14 [PATCH v5 0/6] sched/debug: Introduce per-CPU debugfs files Aaron Tomlin
@ 2026-08-25 14:14 ` Aaron Tomlin
2026-08-25 14:14 ` [PATCH v5 2/6] sched/debug: Protect lockless rq->rd access in print_dl_rq() Aaron Tomlin
` (5 subsequent siblings)
6 siblings, 0 replies; 8+ messages in thread
From: Aaron Tomlin @ 2026-08-25 14:14 UTC (permalink / raw)
To: mingo, peterz, juri.lelli, vincent.guittot
Cc: dietmar.eggemann, rostedt, bsegall, mgorman, vschneid,
kprateek.nayak, zhanxusheng1024, neelx, atomlin, chjohnst,
mproche, sean, steve, rishil1999, linux-kernel
The root_domain pointer rd field in struct rq is updated dynamically
using RCU, and its memory reclamation is deferred via call_rcu() in
rq_attach_root(). However, struct rq's rd field was missing the __rcu
compiler annotation, and several lockless readers across the scheduler
subsystem accessed rq->rd directly without using RCU dereference
primitives.
Add the __rcu annotation to struct rq's rd field in kernel/sched/sched.h.
Update lockless readers across kernel/sched/ to use rcu_dereference(),
rcu_dereference_sched() or rcu_access_pointer() appropriately. This
ensures proper data-dependency barriers on all architectures, enables
Sparse static analysis validation, and documents RCU read-side ownership
contracts.
Signed-off-by: Aaron Tomlin <atomlin@atomlin.com>
---
kernel/sched/core.c | 16 ++++++++++------
kernel/sched/deadline.c | 8 ++++----
kernel/sched/fair.c | 29 +++++++++++++++--------------
kernel/sched/sched.h | 2 +-
4 files changed, 30 insertions(+), 25 deletions(-)
diff --git a/kernel/sched/core.c b/kernel/sched/core.c
index 96226707c2f6..3882aa99e2f1 100644
--- a/kernel/sched/core.c
+++ b/kernel/sched/core.c
@@ -8580,10 +8580,12 @@ void set_rq_offline(struct rq *rq)
static inline void sched_set_rq_online(struct rq *rq, int cpu)
{
struct rq_flags rf;
+ struct root_domain *rd;
rq_lock_irqsave(rq, &rf);
- if (rq->rd) {
- BUG_ON(!cpumask_test_cpu(cpu, rq->rd->span));
+ rd = rcu_dereference_protected(rq->rd, lockdep_is_held(&rq->__lock));
+ if (rd) {
+ BUG_ON(!cpumask_test_cpu(cpu, rd->span));
set_rq_online(rq);
}
rq_unlock_irqrestore(rq, &rf);
@@ -8592,10 +8594,12 @@ static inline void sched_set_rq_online(struct rq *rq, int cpu)
static inline void sched_set_rq_offline(struct rq *rq, int cpu)
{
struct rq_flags rf;
+ struct root_domain *rd;
rq_lock_irqsave(rq, &rf);
- if (rq->rd) {
- BUG_ON(!cpumask_test_cpu(cpu, rq->rd->span));
+ rd = rcu_dereference_protected(rq->rd, lockdep_is_held(&rq->__lock));
+ if (rd) {
+ BUG_ON(!cpumask_test_cpu(cpu, rd->span));
set_rq_offline(rq);
}
rq_unlock_irqrestore(rq, &rf);
@@ -9012,8 +9016,8 @@ void __init sched_init(void)
#endif
rq->next_class = &idle_sched_class;
- rq->sd = NULL;
- rq->rd = NULL;
+ RCU_INIT_POINTER(rq->sd, NULL);
+ RCU_INIT_POINTER(rq->rd, NULL);
rq->cpu_capacity = SCHED_CAPACITY_SCALE;
rq->balance_callback = &balance_push_callback;
rq->active_balance = 0;
diff --git a/kernel/sched/deadline.c b/kernel/sched/deadline.c
index 0f858b98c9aa..8e1e8337aba0 100644
--- a/kernel/sched/deadline.c
+++ b/kernel/sched/deadline.c
@@ -122,12 +122,12 @@ static inline struct dl_bw *dl_bw_of(int i)
{
RCU_LOCKDEP_WARN(!rcu_read_lock_sched_held(),
"sched RCU must be held");
- return &cpu_rq(i)->rd->dl_bw;
+ return &rcu_dereference_sched(cpu_rq(i)->rd)->dl_bw;
}
static inline int dl_bw_cpus(int i)
{
- struct root_domain *rd = cpu_rq(i)->rd;
+ struct root_domain *rd = rcu_dereference_sched(cpu_rq(i)->rd);
RCU_LOCKDEP_WARN(!rcu_read_lock_sched_held(),
"sched RCU must be held");
@@ -159,13 +159,13 @@ static inline unsigned long dl_bw_capacity(int i)
RCU_LOCKDEP_WARN(!rcu_read_lock_sched_held(),
"sched RCU must be held");
- return __dl_bw_capacity(cpu_rq(i)->rd->span);
+ return __dl_bw_capacity(rcu_dereference_sched(cpu_rq(i)->rd)->span);
}
}
bool dl_bw_visited(int cpu, u64 cookie)
{
- struct root_domain *rd = cpu_rq(cpu)->rd;
+ struct root_domain *rd = rcu_dereference_sched(cpu_rq(cpu)->rd);
if (rd->visit_cookie == cookie)
return true;
diff --git a/kernel/sched/fair.c b/kernel/sched/fair.c
index d78467ec6ee1..ad367a542eb0 100644
--- a/kernel/sched/fair.c
+++ b/kernel/sched/fair.c
@@ -7739,13 +7739,10 @@ static inline void set_rd_overutilized(struct root_domain *rd, bool flag)
static inline void check_update_overutilized_status(struct rq *rq)
{
- /*
- * overutilized field is used for load balancing decisions only
- * if energy aware scheduler is being used
- */
+ struct root_domain *rd = rcu_dereference(rq->rd);
- if (!is_rd_overutilized(rq->rd) && cpu_overutilized(rq->cpu))
- set_rd_overutilized(rq->rd, 1);
+ if (rd && !is_rd_overutilized(rd) && cpu_overutilized(rq->cpu))
+ set_rd_overutilized(rd, 1);
}
/* Runqueue only has SCHED_IDLE tasks enqueued */
@@ -9358,7 +9355,7 @@ static int find_energy_efficient_cpu(struct task_struct *p, int prev_cpu)
unsigned long prev_delta = ULONG_MAX, best_delta = ULONG_MAX;
unsigned long p_util_min = uclamp_is_used() ? uclamp_eff_value(p, UCLAMP_MIN) : 0;
unsigned long p_util_max = uclamp_is_used() ? uclamp_eff_value(p, UCLAMP_MAX) : 1024;
- struct root_domain *rd = this_rq()->rd;
+ struct root_domain *rd = rcu_dereference(this_rq()->rd);
int cpu, best_energy_cpu, target = -1;
int prev_fits = -1, best_fits = -1;
unsigned long best_actual_cap = 0;
@@ -9562,7 +9559,7 @@ select_task_rq_fair(struct task_struct *p, int prev_cpu, int wake_flags)
cpumask_test_cpu(cpu, p->cpus_ptr))
return cpu;
- if (!is_rd_overutilized(this_rq()->rd)) {
+ if (!is_rd_overutilized(rcu_dereference(this_rq()->rd))) {
new_cpu = find_energy_efficient_cpu(p, prev_cpu);
if (new_cpu >= 0)
return new_cpu;
@@ -12554,13 +12551,15 @@ static inline void update_sd_lb_stats(struct lb_env *env, struct sd_lb_stats *sd
env->fbq_type = fbq_classify_group(&sds->busiest_stat);
if (!env->sd->parent) {
+ struct root_domain *rd = rcu_dereference(env->dst_rq->rd);
+
/* update overload indicator if we are at root domain */
- set_rd_overloaded(env->dst_rq->rd, sg_overloaded);
+ set_rd_overloaded(rd, sg_overloaded);
/* Update over-utilization (tipping point, U >= 0) indicator */
- set_rd_overutilized(env->dst_rq->rd, sg_overutilized);
+ set_rd_overutilized(rd, sg_overutilized);
} else if (sg_overutilized) {
- set_rd_overutilized(env->dst_rq->rd, sg_overutilized);
+ set_rd_overutilized(rcu_dereference(env->dst_rq->rd), sg_overutilized);
}
update_idle_cpu_scan(env, sum_util);
@@ -12806,8 +12805,10 @@ static struct sched_group *sched_balance_find_src_group(struct lb_env *env)
if (busiest->group_type == group_misfit_task)
goto force_balance;
- if (!is_rd_overutilized(env->dst_rq->rd) &&
- rcu_dereference_all(env->dst_rq->rd->pd))
+ struct root_domain *rd = rcu_dereference(env->dst_rq->rd);
+
+ if (rd && !is_rd_overutilized(rd) &&
+ rcu_dereference_all(rd->pd))
goto out_balanced;
/* ASYM feature bypasses nice load balance check */
@@ -14386,7 +14387,7 @@ static int sched_balance_newidle(struct rq *this_rq, struct rq_flags *rf)
if (!sd)
goto out;
- if (!get_rd_overloaded(this_rq->rd) ||
+ if (!get_rd_overloaded(rcu_dereference(this_rq->rd)) ||
this_rq->avg_idle < sd->max_newidle_lb_cost) {
update_next_balance(sd, &next_balance);
diff --git a/kernel/sched/sched.h b/kernel/sched/sched.h
index 56acf502ba26..aca352e2f4a8 100644
--- a/kernel/sched/sched.h
+++ b/kernel/sched/sched.h
@@ -1253,7 +1253,7 @@ struct rq {
int membarrier_state;
#endif
- struct root_domain *rd;
+ struct root_domain __rcu *rd;
struct sched_domain __rcu *sd;
struct balance_callback *balance_callback;
--
2.55.0
^ permalink raw reply related [flat|nested] 8+ messages in thread
* [PATCH v5 2/6] sched/debug: Protect lockless rq->rd access in print_dl_rq()
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 ` Aaron Tomlin
2026-08-25 14:14 ` [PATCH v5 3/6] sched/debug: Protect lockless rq->curr access in print_cpu() Aaron Tomlin
` (4 subsequent siblings)
6 siblings, 0 replies; 8+ messages in thread
From: Aaron Tomlin @ 2026-08-25 14:14 UTC (permalink / raw)
To: mingo, peterz, juri.lelli, vincent.guittot
Cc: dietmar.eggemann, rostedt, bsegall, mgorman, vschneid,
kprateek.nayak, zhanxusheng1024, neelx, atomlin, chjohnst,
mproche, sean, steve, rishil1999, linux-kernel
In print_dl_rq(), cpu_rq(cpu)->rd is dereferenced locklessly to display
deadline bandwidth statistics.
During CPU hot-unplug or cgroup cpuset repartitioning events,
partition_sched_domains() calls cpu_attach_domain(), which executes
rq_attach_root() to detach the CPU from its root_domain. When the
reference count of the detached root_domain drops to zero,
rq_attach_root() calls call_rcu(&old_rd->rcu, free_rootdomain) to
schedule memory teardown after an RCU grace period.
However, rq_attach_root() previously updated rq->rd using a plain C store
without an RCU publication barrier (i.e., rcu_assign_pointer()). Without a
release memory barrier on the writer side, CPU or compiler reordering could
allow the new rq->rd pointer store to become visible to other CPUs before
the initialization writes to rd->dl_bw are committed.
Furthermore, because print_dl_rq() did not hold an RCU read lock while
dereferencing cpu_rq(cpu)->rd, an RCU grace period could elapse
concurrently while debugfs is reading the file, allowing
free_rootdomain() to execute kfree(old_rd) and causing a use-after-free
race condition when print_dl_rq() reads dl_bw->bw.
Resolve this by using rcu_assign_pointer(rq->rd, rd) in rq_attach_root() to
guarantee a release memory barrier when publishing a root_domain.
Finally, fetch rq->rd using guard(rcu)() and rcu_dereference() in print_dl_rq().
Fixes: 02968ccf7b80 ("sched: add /proc/sched_debug file")
Reported-by: sashiko-bot <sashiko-bot@kernel.org>
Signed-off-by: Aaron Tomlin <atomlin@atomlin.com>
---
kernel/sched/debug.c | 3 ++-
kernel/sched/topology.c | 2 +-
2 files changed, 3 insertions(+), 2 deletions(-)
diff --git a/kernel/sched/debug.c b/kernel/sched/debug.c
index 40584b27ea0c..632d5fe9e470 100644
--- a/kernel/sched/debug.c
+++ b/kernel/sched/debug.c
@@ -1089,7 +1089,8 @@ void print_dl_rq(struct seq_file *m, int cpu, struct dl_rq *dl_rq)
SEQ_printf(m, " .%-30s: %lu\n", #x, (unsigned long)(dl_rq->x))
PU(dl_nr_running);
- dl_bw = &cpu_rq(cpu)->rd->dl_bw;
+ guard(rcu)();
+ dl_bw = &rcu_dereference(cpu_rq(cpu)->rd)->dl_bw;
SEQ_printf(m, " .%-30s: %lld\n", "dl_bw->bw", dl_bw->bw);
SEQ_printf(m, " .%-30s: %lld\n", "dl_bw->total_bw", dl_bw->total_bw);
diff --git a/kernel/sched/topology.c b/kernel/sched/topology.c
index 622e2e01974c..b411cc00029c 100644
--- a/kernel/sched/topology.c
+++ b/kernel/sched/topology.c
@@ -496,7 +496,7 @@ void rq_attach_root(struct rq *rq, struct root_domain *rd)
}
atomic_inc(&rd->refcount);
- rq->rd = rd;
+ rcu_assign_pointer(rq->rd, rd);
cpumask_set_cpu(rq->cpu, rd->span);
if (cpumask_test_cpu(rq->cpu, cpu_active_mask))
--
2.55.0
^ permalink raw reply related [flat|nested] 8+ messages in thread
* [PATCH v5 3/6] sched/debug: Protect lockless rq->curr access in print_cpu()
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 ` Aaron Tomlin
2026-08-25 14:14 ` [PATCH v5 4/6] sched/debug: Protect p->mm access in sched_show_numa() Aaron Tomlin
` (3 subsequent siblings)
6 siblings, 0 replies; 8+ messages in thread
From: Aaron Tomlin @ 2026-08-25 14:14 UTC (permalink / raw)
To: mingo, peterz, juri.lelli, vincent.guittot
Cc: dietmar.eggemann, rostedt, bsegall, mgorman, vschneid,
kprateek.nayak, zhanxusheng1024, neelx, atomlin, chjohnst,
mproche, sean, steve, rishil1999, linux-kernel
In print_cpu(), rq->curr is dereferenced locklessly to print the current
task's PID via task_pid_nr(rq->curr).
While accessing /sys/kernel/debug/sched/debug is inherently best-effort
only; rq->curr is indeed expected to change dynamically while
print_cpu() is executing. However, if the task currently running on the
CPU exits concurrently and its reference count drops to zero,
put_task_struct() calls call_rcu() to schedule
__put_task_struct_rcu_cb(). Because print_cpu() does not hold the RCU
read lock while dereferencing rq->curr, an RCU grace period can complete
concurrently and free the task structure via free_task(), creating a
potential use-after-free race condition.
Resolve this by reading rq->curr using rcu_dereference(rq->curr) inside an
RCU read-side critical section. Holding the RCU read lock delays the
invocation of __put_task_struct_rcu_cb() until after rcu_read_unlock(),
ensuring that the struct task_struct memory remains valid while being
accessed.
Fixes: 02968ccf7b80 ("sched: add /proc/sched_debug file")
Reported-by: sashiko-bot <sashiko-bot@kernel.org>
Signed-off-by: Aaron Tomlin <atomlin@atomlin.com>
---
kernel/sched/debug.c | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/kernel/sched/debug.c b/kernel/sched/debug.c
index 632d5fe9e470..27e0840ba9be 100644
--- a/kernel/sched/debug.c
+++ b/kernel/sched/debug.c
@@ -1127,7 +1127,10 @@ do { \
P(nr_switches);
P(nr_uninterruptible);
PN(next_balance);
- SEQ_printf(m, " .%-30s: %ld\n", "curr->pid", (long)(task_pid_nr(rq->curr)));
+ rcu_read_lock();
+ SEQ_printf(m, " .%-30s: %ld\n", "curr->pid",
+ (long)(task_pid_nr(rcu_dereference(rq->curr))));
+ rcu_read_unlock();
PN(clock);
PN(clock_task);
#undef P
--
2.55.0
^ permalink raw reply related [flat|nested] 8+ messages in thread
* [PATCH v5 4/6] sched/debug: Protect p->mm access in sched_show_numa()
2026-08-25 14:14 [PATCH v5 0/6] sched/debug: Introduce per-CPU debugfs files Aaron Tomlin
` (2 preceding siblings ...)
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 ` Aaron Tomlin
2026-08-25 14:14 ` [PATCH v5 5/6] sched/fair: Use list_for_each_entry_rcu() in print_cfs_stats() Aaron Tomlin
` (2 subsequent siblings)
6 siblings, 0 replies; 8+ messages in thread
From: Aaron Tomlin @ 2026-08-25 14:14 UTC (permalink / raw)
To: mingo, peterz, juri.lelli, vincent.guittot
Cc: dietmar.eggemann, rostedt, bsegall, mgorman, vschneid,
kprateek.nayak, zhanxusheng1024, neelx, atomlin, chjohnst,
mproche, sean, steve, rishil1999, linux-kernel
In sched_show_numa(), p->mm is checked locklessly and then passed to the
P(mm->numa_scan_seq) macro. This presents both a time-of-change to
time-of-use race and a potential use-after-free vulnerability.
If a task exits concurrently via exit_mm(p), another CPU can set p->mm
to NULL and call mmput(mm) to free the struct mm_struct. Dereferencing
mm->numa_scan_seq without holding task_lock(p) can access freed memory if
mmput() runs immediately after the check.
Fix this by wrapping the p->mm check and macro dereference in
task_lock(p) and task_unlock(p). In exit_mm(), current->mm is set to
NULL under task_lock(p) before mmput() is called, guaranteeing that
p->mm cannot be set to NULL or freed while task_lock(p) is held.
Fixes: b32e86b4301e ("sched/numa: Add debugging")
Reported-by: sashiko-bot <sashiko-bot@kernel.org>
Signed-off-by: Aaron Tomlin <atomlin@atomlin.com>
---
kernel/sched/debug.c | 2 ++
1 file changed, 2 insertions(+)
diff --git a/kernel/sched/debug.c b/kernel/sched/debug.c
index 27e0840ba9be..c05ba4d8b169 100644
--- a/kernel/sched/debug.c
+++ b/kernel/sched/debug.c
@@ -1312,8 +1312,10 @@ void print_numa_stats(struct seq_file *m, int node, unsigned long tsf,
static void sched_show_numa(struct task_struct *p, struct seq_file *m)
{
#ifdef CONFIG_NUMA_BALANCING
+ task_lock(p);
if (p->mm)
P(mm->numa_scan_seq);
+ task_unlock(p);
P(numa_pages_migrated);
P(numa_preferred_nid);
--
2.55.0
^ permalink raw reply related [flat|nested] 8+ messages in thread
* [PATCH v5 5/6] sched/fair: Use list_for_each_entry_rcu() in print_cfs_stats()
2026-08-25 14:14 [PATCH v5 0/6] sched/debug: Introduce per-CPU debugfs files Aaron Tomlin
` (3 preceding siblings ...)
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
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
6 siblings, 0 replies; 8+ messages in thread
From: Aaron Tomlin @ 2026-08-25 14:14 UTC (permalink / raw)
To: mingo, peterz, juri.lelli, vincent.guittot
Cc: dietmar.eggemann, rostedt, bsegall, mgorman, vschneid,
kprateek.nayak, zhanxusheng1024, neelx, atomlin, chjohnst,
mproche, sean, steve, rishil1999, linux-kernel
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
^ permalink raw reply related [flat|nested] 8+ messages in thread
* [PATCH v5 6/6] sched/debug: Introduce per-CPU debugfs files
2026-08-25 14:14 [PATCH v5 0/6] sched/debug: Introduce per-CPU debugfs files Aaron Tomlin
` (4 preceding siblings ...)
2026-08-25 14:14 ` [PATCH v5 5/6] sched/fair: Use list_for_each_entry_rcu() in print_cfs_stats() Aaron Tomlin
@ 2026-08-25 14:14 ` Aaron Tomlin
2026-08-25 16:30 ` [PATCH v5 0/6] " Aaron Tomlin
6 siblings, 0 replies; 8+ messages in thread
From: Aaron Tomlin @ 2026-08-25 14:14 UTC (permalink / raw)
To: mingo, peterz, juri.lelli, vincent.guittot
Cc: dietmar.eggemann, rostedt, bsegall, mgorman, vschneid,
kprateek.nayak, zhanxusheng1024, neelx, atomlin, chjohnst,
mproche, sean, steve, rishil1999, linux-kernel
Currently, accessing scheduler debugging details for a specific CPU
requires reading /sys/kernel/debug/sched/debug, which outputs
information for all online CPUs. When investigating a latency anomaly or
scheduling issue isolated to a specific CPU, accessing
/sys/kernel/debug/sched/cpu/cpu<N>/debug provides an immediate, targeted
view of that runqueue.
Add support for per-CPU debug files under:
/sys/kernel/debug/sched/cpu/cpu<N>/debug. Reading
/sys/kernel/debug/sched/cpu/cpu<N>/debug calls print_cpu() specifically
for CPU <N>, exposing CPU-specific runqueue details on demand. If the
target CPU is currently offline, reading its file returns -ENODEV.
Signed-off-by: Aaron Tomlin <atomlin@atomlin.com>
---
kernel/sched/debug.c | 43 +++++++++++++++++++++++++++++++++++++++++++
1 file changed, 43 insertions(+)
diff --git a/kernel/sched/debug.c b/kernel/sched/debug.c
index 8793059688d7..a3f5b105b7ea 100644
--- a/kernel/sched/debug.c
+++ b/kernel/sched/debug.c
@@ -345,6 +345,7 @@ static const struct file_operations sched_verbose_fops = {
};
static const struct seq_operations sched_debug_sops;
+static void print_cpu(struct seq_file *m, int cpu);
static int sched_debug_open(struct inode *inode, struct file *filp)
{
@@ -622,6 +623,47 @@ static void debugfs_fair_server_init(void)
}
}
+static int sched_debug_cpu_show(struct seq_file *m, void *v)
+{
+ unsigned long cpu = (unsigned long) m->private;
+
+ if (!cpu_online(cpu))
+ return -ENODEV;
+
+ print_cpu(m, cpu);
+ return 0;
+}
+
+static int sched_debug_cpu_open(struct inode *inode, struct file *filp)
+{
+ return single_open(filp, sched_debug_cpu_show, inode->i_private);
+}
+
+static const struct file_operations sched_debug_cpu_fops = {
+ .open = sched_debug_cpu_open,
+ .read = seq_read,
+ .llseek = seq_lseek,
+ .release = single_release,
+};
+
+static __init void debugfs_cpu_init(void)
+{
+ struct dentry *d_cpu_dir;
+ unsigned long cpu;
+ char buf[16];
+
+ d_cpu_dir = debugfs_create_dir("cpu", debugfs_sched);
+
+ for_each_possible_cpu(cpu) {
+ struct dentry *d_cpu;
+
+ snprintf(buf, sizeof(buf), "cpu%lu", cpu);
+ d_cpu = debugfs_create_dir(buf, d_cpu_dir);
+
+ debugfs_create_file("debug", 0444, d_cpu, (void *) cpu, &sched_debug_cpu_fops);
+ }
+}
+
static __init int sched_init_debug(void)
{
struct dentry __maybe_unused *numa, *llc;
@@ -679,6 +721,7 @@ static __init int sched_init_debug(void)
#ifdef CONFIG_SCHED_CLASS_EXT
debugfs_ext_server_init();
#endif
+ debugfs_cpu_init();
return 0;
}
--
2.55.0
^ permalink raw reply related [flat|nested] 8+ messages in thread
* Re: [PATCH v5 0/6] sched/debug: Introduce per-CPU debugfs files
2026-08-25 14:14 [PATCH v5 0/6] sched/debug: Introduce per-CPU debugfs files Aaron Tomlin
` (5 preceding siblings ...)
2026-08-25 14:14 ` [PATCH v5 6/6] sched/debug: Introduce per-CPU debugfs files Aaron Tomlin
@ 2026-08-25 16:30 ` Aaron Tomlin
6 siblings, 0 replies; 8+ messages in thread
From: Aaron Tomlin @ 2026-08-25 16:30 UTC (permalink / raw)
To: mingo, peterz, juri.lelli, vincent.guittot
Cc: dietmar.eggemann, rostedt, bsegall, mgorman, vschneid,
kprateek.nayak, zhanxusheng1024, neelx, chjohnst, mproche, sean,
steve, rishil1999, linux-kernel
On Tue, Aug 25, 2026 at 10:14:07AM -0400, Aaron Tomlin wrote:
> Hi Peter, Juri, Ingo, Vincent,
>
> This patch series addresses a few pre-existing memory safety and list
> traversal concurrency issues in scheduler debugfs handlers, and introduces
> per-CPU debugfs files under /sys/kernel/debug/sched/cpu/cpu<N>/debug.
Please ignore. I will rebase against tip/sched/core.
Kind regards,
--
Aaron Tomlin
^ permalink raw reply [flat|nested] 8+ messages in thread
end of thread, other threads:[~2026-08-25 16:31 UTC | newest]
Thread overview: 8+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
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 ` [PATCH v5 5/6] sched/fair: Use list_for_each_entry_rcu() in print_cfs_stats() Aaron Tomlin
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
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox