* [PATCH v9 0/6] Introduce per-CPU debugfs files
@ 2026-08-27 22:18 Aaron Tomlin
2026-08-27 22:18 ` [PATCH v9 1/6] sched: Annotate rq->rd with __rcu and update lockless readers Aaron Tomlin
` (5 more replies)
0 siblings, 6 replies; 7+ messages in thread
From: Aaron Tomlin @ 2026-08-27 22:18 UTC (permalink / raw)
To: mingo, peterz, juri.lelli, vincent.guittot
Cc: paulmck, 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 prerequisite patch that annotates struct rq's rd
(root_domain) pointer with __rcu in kernel/sched/sched.h and introduces the
rcu_dereference_root_domain() helper macro (i.e., mirroring
rcu_dereference_sched_domain()), updating readers across the scheduler
subsystem to ensure Sparse compliance, correct memory ordering, and clean
Lockdep-RCU validation under both sched_domains_mutex and RCU-sched
contexts.
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
use-after-free 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() using guard(rcu)(), bounds
traversal with a per-CPU 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 v8:
- Added guard(rcu)() to dl_task_needs_bw_move() to prevent false-positive
warnings under CONFIG_PROVE_RCU when invoked from cpuset_can_attach()
- Converted remaining direct rq->rd accessors across in add_nr_running(),
__sched_setscheduler(), and dl_task_check_affinity() to use
rcu_dereference_root_domain()
- Linked to v8: https://lore.kernel.org/lkml/20260827194014.977758-1-atomlin@atomlin.com/
Changes since v7:
- Removed incorrect rcu_dereference_protected() usages with
lockdep_is_held(&rq->__lock) in favour of a newly introduced
rcu_dereference_root_domain() helper, properly validating access under
either sched_domains_mutex or RCU-sched read-side critical sections
(Peter Zijlstra)
- Restored the comment in check_update_overutilized_status() in
kernel/sched/fair.c (Vincent Guittot)
- Linked to v7: https://lore.kernel.org/lkml/20260826224238.936456-1-atomlin@atomlin.com/
Changes since v6:
- Updated rq->rd accessors across kernel/sched/ to use
rcu_dereference_sched() for lockless readers and
rcu_dereference_protected() for lock-held paths, eliminating false
Lockdep-RCU warnings under CONFIG_PROVE_RCU and all Sparse noderef
warnings
- Removed the __printf(3, 4) attribute from the !CONFIG_CGROUP_SCHED
fallback stub of SEQ_printf_task_group_path() in kernel/sched/sched.h to
prevent -Wformat build errors when CONFIG_CGROUP_SCHED is disabled
- Expanded the commit changelog to clarify the per-CPU scope of
SCHED_DEBUG_MAX_ITER and the RCU memory safety guarantees for recycled
cfs_rq nodes
- Linked to v6: https://lore.kernel.org/lkml/20260825184637.888364-1-atomlin@atomlin.com/
Changes since v5:
- Rebased against tip/sched/core (sched-core-2026-08-17)
- Linked to v5: https://lore.kernel.org/lkml/20260825141413.868997-1-atomlin@atomlin.com/
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 | 24 +++++++----
kernel/sched/deadline.c | 77 ++++++++++++++++++----------------
kernel/sched/debug.c | 92 ++++++++++++++++++++++++-----------------
kernel/sched/fair.c | 60 ++++++++++++++++++++-------
kernel/sched/rt.c | 64 ++++++++++++++++------------
kernel/sched/sched.h | 58 +++++++++++++++++++++++++-
kernel/sched/syscalls.c | 8 ++--
kernel/sched/topology.c | 13 +++---
8 files changed, 261 insertions(+), 135 deletions(-)
base-commit: 68e37487810a3da43c48340fab7a55b3b6efdae3
--
2.55.0
^ permalink raw reply [flat|nested] 7+ messages in thread
* [PATCH v9 1/6] sched: Annotate rq->rd with __rcu and update lockless readers
2026-08-27 22:18 [PATCH v9 0/6] Introduce per-CPU debugfs files Aaron Tomlin
@ 2026-08-27 22:18 ` Aaron Tomlin
2026-08-27 22:18 ` [PATCH v9 2/6] sched/debug: Protect lockless rq->rd access in print_dl_rq() Aaron Tomlin
` (4 subsequent siblings)
5 siblings, 0 replies; 7+ messages in thread
From: Aaron Tomlin @ 2026-08-27 22:18 UTC (permalink / raw)
To: mingo, peterz, juri.lelli, vincent.guittot
Cc: paulmck, 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. For code clarity,
introduce the rcu_dereference_root_domain() helper macro to validate
access under sched_domains_mutex or active RCU-sched read-side critical
sections. While mechanically identical to
rcu_dereference_sched_domain(), defining rcu_dereference_root_domain()
preserves the natural symmetry of rq->rd and rq->sd in struct rq, while
keeping grep-ability straightforward. Update readers and accessors
across kernel/sched/ to use rcu_dereference_root_domain().
This ensures proper memory ordering, enables Sparse static analysis
validation, and avoids false Lockdep warnings under CONFIG_PROVE_RCU.
Signed-off-by: Aaron Tomlin <atomlin@atomlin.com>
---
kernel/sched/core.c | 24 ++++++++-----
kernel/sched/deadline.c | 77 ++++++++++++++++++++++-------------------
kernel/sched/fair.c | 27 +++++++++------
kernel/sched/rt.c | 64 +++++++++++++++++++---------------
kernel/sched/sched.h | 7 ++--
kernel/sched/syscalls.c | 8 ++---
kernel/sched/topology.c | 11 +++---
7 files changed, 126 insertions(+), 92 deletions(-)
diff --git a/kernel/sched/core.c b/kernel/sched/core.c
index 2e7cde033a31..9a12601d8c54 100644
--- a/kernel/sched/core.c
+++ b/kernel/sched/core.c
@@ -8547,8 +8547,10 @@ void set_rq_online(struct rq *rq)
{
if (!rq->online) {
const struct sched_class *class;
+ struct root_domain *rd;
- cpumask_set_cpu(rq->cpu, rq->rd->online);
+ rd = rcu_dereference_root_domain(rq->rd);
+ cpumask_set_cpu(rq->cpu, rd->online);
rq->online = 1;
for_each_class(class) {
@@ -8562,6 +8564,7 @@ void set_rq_offline(struct rq *rq)
{
if (rq->online) {
const struct sched_class *class;
+ struct root_domain *rd;
update_rq_clock(rq);
for_each_class(class) {
@@ -8569,7 +8572,8 @@ void set_rq_offline(struct rq *rq)
class->rq_offline(rq);
}
- cpumask_clear_cpu(rq->cpu, rq->rd->online);
+ rd = rcu_dereference_root_domain(rq->rd);
+ cpumask_clear_cpu(rq->cpu, rd->online);
rq->online = 0;
}
}
@@ -8577,10 +8581,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_root_domain(rq->rd);
+ if (rd) {
+ BUG_ON(!cpumask_test_cpu(cpu, rd->span));
set_rq_online(rq);
}
rq_unlock_irqrestore(rq, &rf);
@@ -8589,10 +8595,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_root_domain(rq->rd);
+ if (rd) {
+ BUG_ON(!cpumask_test_cpu(cpu, rd->span));
set_rq_offline(rq);
}
rq_unlock_irqrestore(rq, &rf);
@@ -9009,8 +9017,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 857dbe3519a8..507f056084bf 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_root_domain(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_root_domain(cpu_rq(i)->rd);
RCU_LOCKDEP_WARN(!rcu_read_lock_sched_held(),
"sched RCU must be held");
@@ -156,16 +156,13 @@ static inline unsigned long dl_bw_capacity(int i)
arch_scale_cpu_capacity(i) == SCHED_CAPACITY_SCALE) {
return dl_bw_cpus(i) << SCHED_CAPACITY_SHIFT;
} else {
- 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_root_domain(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_root_domain(cpu_rq(cpu)->rd);
if (rd->visit_cookie == cookie)
return true;
@@ -533,15 +530,18 @@ void init_dl_rq(struct dl_rq *dl_rq)
static inline int dl_overloaded(struct rq *rq)
{
- return atomic_read(&rq->rd->dlo_count);
+ return atomic_read(&rcu_dereference_root_domain(rq->rd)->dlo_count);
}
static inline void dl_set_overload(struct rq *rq)
{
+ struct root_domain *rd;
+
if (!rq->online)
return;
- cpumask_set_cpu(rq->cpu, rq->rd->dlo_mask);
+ rd = rcu_dereference_root_domain(rq->rd);
+ cpumask_set_cpu(rq->cpu, rd->dlo_mask);
/*
* Must be visible before the overload count is
* set (as in sched_rt.c).
@@ -549,16 +549,19 @@ static inline void dl_set_overload(struct rq *rq)
* Matched by the barrier in pull_dl_task().
*/
smp_wmb();
- atomic_inc(&rq->rd->dlo_count);
+ atomic_inc(&rd->dlo_count);
}
static inline void dl_clear_overload(struct rq *rq)
{
+ struct root_domain *rd;
+
if (!rq->online)
return;
- atomic_dec(&rq->rd->dlo_count);
- cpumask_clear_cpu(rq->cpu, rq->rd->dlo_mask);
+ rd = rcu_dereference_root_domain(rq->rd);
+ atomic_dec(&rd->dlo_count);
+ cpumask_clear_cpu(rq->cpu, rd->dlo_mask);
}
#define __node_2_pdl(node) \
@@ -699,14 +702,15 @@ static struct rq *dl_task_offline_migration(struct rq *rq, struct task_struct *p
* since p is still hanging out in the old (now moved to default) root
* domain.
*/
- dl_b = &rq->rd->dl_bw;
+ dl_b = &rcu_dereference_root_domain(rq->rd)->dl_bw;
raw_spin_lock(&dl_b->lock);
- __dl_sub(dl_b, p->dl.dl_bw, cpumask_weight(rq->rd->span));
+ __dl_sub(dl_b, p->dl.dl_bw, cpumask_weight(rcu_dereference_root_domain(rq->rd)->span));
raw_spin_unlock(&dl_b->lock);
- dl_b = &later_rq->rd->dl_bw;
+ dl_b = &rcu_dereference_root_domain(later_rq->rd)->dl_bw;
raw_spin_lock(&dl_b->lock);
- __dl_add(dl_b, p->dl.dl_bw, cpumask_weight(later_rq->rd->span));
+ __dl_add(dl_b, p->dl.dl_bw,
+ cpumask_weight(rcu_dereference_root_domain(later_rq->rd)->span));
raw_spin_unlock(&dl_b->lock);
set_task_cpu(p, later_rq->cpu);
@@ -2222,9 +2226,10 @@ static void inc_dl_deadline(struct dl_rq *dl_rq, u64 deadline)
if (dl_rq->earliest_dl.curr == 0 ||
dl_time_before(deadline, dl_rq->earliest_dl.curr)) {
if (dl_rq->earliest_dl.curr == 0)
- cpupri_set(&rq->rd->cpupri, rq->cpu, CPUPRI_HIGHER);
+ cpupri_set(&rcu_dereference_root_domain(rq->rd)->cpupri, rq->cpu,
+ CPUPRI_HIGHER);
dl_rq->earliest_dl.curr = deadline;
- cpudl_set(&rq->rd->cpudl, rq->cpu, deadline);
+ cpudl_set(&rcu_dereference_root_domain(rq->rd)->cpudl, rq->cpu, deadline);
}
}
@@ -2239,14 +2244,15 @@ static void dec_dl_deadline(struct dl_rq *dl_rq, u64 deadline)
if (!dl_rq->dl_nr_running) {
dl_rq->earliest_dl.curr = 0;
dl_rq->earliest_dl.next = 0;
- cpudl_clear(&rq->rd->cpudl, rq->cpu, rq->online);
- cpupri_set(&rq->rd->cpupri, rq->cpu, rq->rt.highest_prio.curr);
+ cpudl_clear(&rcu_dereference_root_domain(rq->rd)->cpudl, rq->cpu, rq->online);
+ cpupri_set(&rcu_dereference_root_domain(rq->rd)->cpupri, rq->cpu,
+ rq->rt.highest_prio.curr);
} else {
struct rb_node *leftmost = rb_first_cached(&dl_rq->root);
struct sched_dl_entity *entry = __node_2_dle(leftmost);
dl_rq->earliest_dl.curr = entry->deadline;
- cpudl_set(&rq->rd->cpudl, rq->cpu, entry->deadline);
+ cpudl_set(&rcu_dereference_root_domain(rq->rd)->cpudl, rq->cpu, entry->deadline);
}
}
@@ -2686,12 +2692,14 @@ static void migrate_task_rq_dl(struct task_struct *p, int new_cpu __maybe_unused
static void check_preempt_equal_dl(struct rq *rq, struct task_struct *p)
{
+ struct root_domain *rd = rcu_dereference_root_domain(rq->rd);
+
/*
* Current can't be migrated, useless to reschedule,
* let's hope p can move out.
*/
if (rq->curr->nr_cpus_allowed == 1 ||
- !cpudl_find(&rq->rd->cpudl, rq->donor, NULL))
+ !cpudl_find(&rd->cpudl, rq->donor, NULL))
return;
/*
@@ -2699,7 +2707,7 @@ static void check_preempt_equal_dl(struct rq *rq, struct task_struct *p)
* see if it is pushed or pulled somewhere else.
*/
if (p->nr_cpus_allowed != 1 &&
- cpudl_find(&rq->rd->cpudl, p, NULL))
+ cpudl_find(&rd->cpudl, p, NULL))
return;
resched_curr(rq);
@@ -2948,7 +2956,7 @@ static int find_later_rq(struct task_struct *task)
* We have to consider system topology and task affinity
* first, then we can look for a suitable CPU.
*/
- if (!cpudl_find(&task_rq(task)->rd->cpudl, task, later_mask))
+ if (!cpudl_find(&rcu_dereference_root_domain(task_rq(task)->rd)->cpudl, task, later_mask))
return -1;
/*
@@ -3232,7 +3240,7 @@ static void pull_dl_task(struct rq *this_rq)
*/
smp_rmb();
- for_each_cpu(cpu, this_rq->rd->dlo_mask) {
+ for_each_cpu(cpu, rcu_dereference_root_domain(this_rq->rd)->dlo_mask) {
if (this_cpu == cpu)
continue;
@@ -3355,10 +3363,8 @@ static void set_cpus_allowed_dl(struct task_struct *p,
bool dl_task_needs_bw_move(struct task_struct *p,
const struct cpumask *new_mask)
{
- if (!dl_task(p))
- return false;
-
- return !cpumask_intersects(task_rq(p)->rd->span, new_mask);
+ guard(rcu)();
+ return !cpumask_intersects(rcu_dereference_root_domain(task_rq(p)->rd)->span, new_mask);
}
/* Assumes rq->lock is held */
@@ -3368,9 +3374,10 @@ static void rq_online_dl(struct rq *rq)
dl_set_overload(rq);
if (rq->dl.dl_nr_running > 0)
- cpudl_set(&rq->rd->cpudl, rq->cpu, rq->dl.earliest_dl.curr);
+ cpudl_set(&rcu_dereference_root_domain(rq->rd)->cpudl, rq->cpu,
+ rq->dl.earliest_dl.curr);
else
- cpudl_clear(&rq->rd->cpudl, rq->cpu, true);
+ cpudl_clear(&rcu_dereference_root_domain(rq->rd)->cpudl, rq->cpu, true);
}
/* Assumes rq->lock is held */
@@ -3379,7 +3386,7 @@ static void rq_offline_dl(struct rq *rq)
if (rq->dl.overloaded)
dl_clear_overload(rq);
- cpudl_clear(&rq->rd->cpudl, rq->cpu, false);
+ cpudl_clear(&rcu_dereference_root_domain(rq->rd)->cpudl, rq->cpu, false);
}
void __init init_sched_dl_class(void)
@@ -3440,10 +3447,10 @@ void dl_add_task_root_domain(struct task_struct *p)
cpu = cpumask_first_and(cpu_active_mask, msk);
BUG_ON(cpu >= nr_cpu_ids);
rq = cpu_rq(cpu);
- dl_b = &rq->rd->dl_bw;
+ dl_b = &rcu_dereference_root_domain(rq->rd)->dl_bw;
raw_spin_lock(&dl_b->lock);
- __dl_add(dl_b, p->dl.dl_bw, cpumask_weight(rq->rd->span));
+ __dl_add(dl_b, p->dl.dl_bw, cpumask_weight(rcu_dereference_root_domain(rq->rd)->span));
raw_spin_unlock(&dl_b->lock);
raw_spin_unlock_irqrestore(&p->pi_lock, rf.flags);
}
@@ -3504,7 +3511,7 @@ void dl_clear_root_domain(struct root_domain *rd)
void dl_clear_root_domain_cpu(int cpu)
{
- dl_clear_root_domain(cpu_rq(cpu)->rd);
+ dl_clear_root_domain(rcu_dereference_root_domain(cpu_rq(cpu)->rd));
}
static void switched_from_dl(struct rq *rq, struct task_struct *p)
diff --git a/kernel/sched/fair.c b/kernel/sched/fair.c
index f79fcba4afec..687999312a7d 100644
--- a/kernel/sched/fair.c
+++ b/kernel/sched/fair.c
@@ -7865,13 +7865,14 @@ static inline void set_rd_overutilized(struct root_domain *rd, bool flag)
static inline void check_update_overutilized_status(struct rq *rq)
{
+ struct root_domain *rd = rcu_dereference_root_domain(rq->rd);
+
/*
* overutilized field is used for load balancing decisions only
* if energy aware scheduler is being used
*/
-
- 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 */
@@ -9500,7 +9501,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_root_domain(this_rq()->rd);
int cpu, best_energy_cpu, target = -1;
int prev_fits = -1, best_fits = -1;
unsigned long best_actual_cap = 0;
@@ -9704,7 +9705,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_root_domain(this_rq()->rd))) {
new_cpu = find_energy_efficient_cpu(p, prev_cpu);
if (new_cpu >= 0)
return new_cpu;
@@ -12690,13 +12691,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_root_domain(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_root_domain(env->dst_rq->rd), sg_overutilized);
}
update_idle_cpu_scan(env, sum_util);
@@ -12942,8 +12945,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_root_domain(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 */
@@ -14573,7 +14578,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_root_domain(this_rq->rd)) ||
this_rq->avg_idle < sd->max_newidle_lb_cost) {
update_next_balance(sd, &next_balance);
diff --git a/kernel/sched/rt.c b/kernel/sched/rt.c
index e6e5f8a2caaf..0bf3ea1e9957 100644
--- a/kernel/sched/rt.c
+++ b/kernel/sched/rt.c
@@ -338,15 +338,18 @@ static inline bool need_pull_rt_task(struct rq *rq, struct task_struct *prev)
static inline int rt_overloaded(struct rq *rq)
{
- return atomic_read(&rq->rd->rto_count);
+ return atomic_read(&rcu_dereference_root_domain(rq->rd)->rto_count);
}
static inline void rt_set_overload(struct rq *rq)
{
+ struct root_domain *rd;
+
if (!rq->online)
return;
- cpumask_set_cpu(rq->cpu, rq->rd->rto_mask);
+ rd = rcu_dereference_root_domain(rq->rd);
+ cpumask_set_cpu(rq->cpu, rd->rto_mask);
/*
* Make sure the mask is visible before we set
* the overload count. That is checked to determine
@@ -357,17 +360,20 @@ static inline void rt_set_overload(struct rq *rq)
* Matched by the barrier in pull_rt_task().
*/
smp_wmb();
- atomic_inc(&rq->rd->rto_count);
+ atomic_inc(&rd->rto_count);
}
static inline void rt_clear_overload(struct rq *rq)
{
+ struct root_domain *rd;
+
if (!rq->online)
return;
+ rd = rcu_dereference_root_domain(rq->rd);
/* the order here really doesn't matter */
- atomic_dec(&rq->rd->rto_count);
- cpumask_clear_cpu(rq->cpu, rq->rd->rto_mask);
+ atomic_dec(&rd->rto_count);
+ cpumask_clear_cpu(rq->cpu, rd->rto_mask);
}
static inline int has_pushable_tasks(struct rq *rq)
@@ -580,7 +586,7 @@ static int rt_se_boosted(struct sched_rt_entity *rt_se)
static inline const struct cpumask *sched_rt_period_mask(void)
{
- return this_rq()->rd->span;
+ return rcu_dereference_root_domain(this_rq()->rd)->span;
}
static inline
@@ -608,7 +614,7 @@ bool sched_rt_bandwidth_account(struct rt_rq *rt_rq)
static void do_balance_runtime(struct rt_rq *rt_rq)
{
struct rt_bandwidth *rt_b = sched_rt_bandwidth(rt_rq);
- struct root_domain *rd = rq_of_rt_rq(rt_rq)->rd;
+ struct root_domain *rd = rcu_dereference_root_domain(rq_of_rt_rq(rt_rq)->rd);
int i, weight;
u64 rt_period;
@@ -659,7 +665,7 @@ static void do_balance_runtime(struct rt_rq *rt_rq)
*/
static void __disable_runtime(struct rq *rq)
{
- struct root_domain *rd = rq->rd;
+ struct root_domain *rd = rcu_dereference_root_domain(rq->rd);
rt_rq_iter_t iter;
struct rt_rq *rt_rq;
@@ -1058,7 +1064,7 @@ inc_rt_prio_smp(struct rt_rq *rt_rq, int prio, int prev_prio)
return;
if (rq->online && prio < prev_prio)
- cpupri_set(&rq->rd->cpupri, rq->cpu, prio);
+ cpupri_set(&rcu_dereference_root_domain(rq->rd)->cpupri, rq->cpu, prio);
}
static void
@@ -1073,7 +1079,8 @@ dec_rt_prio_smp(struct rt_rq *rt_rq, int prio, int prev_prio)
return;
if (rq->online && rt_rq->highest_prio.curr != prev_prio)
- cpupri_set(&rq->rd->cpupri, rq->cpu, rt_rq->highest_prio.curr);
+ cpupri_set(&rcu_dereference_root_domain(rq->rd)->cpupri, rq->cpu,
+ rt_rq->highest_prio.curr);
}
static void
@@ -1575,8 +1582,10 @@ select_task_rq_rt(struct task_struct *p, int cpu, int flags)
static void check_preempt_equal_prio(struct rq *rq, struct task_struct *p)
{
+ struct root_domain *rd = rcu_dereference_root_domain(rq->rd);
+
if (rq->curr->nr_cpus_allowed == 1 ||
- !cpupri_find(&rq->rd->cpupri, rq->donor, NULL))
+ !cpupri_find(&rd->cpupri, rq->donor, NULL))
return;
/*
@@ -1584,7 +1593,7 @@ static void check_preempt_equal_prio(struct rq *rq, struct task_struct *p)
* see if it is pushed or pulled somewhere else.
*/
if (p->nr_cpus_allowed != 1 &&
- cpupri_find(&rq->rd->cpupri, p, NULL))
+ cpupri_find(&rd->cpupri, p, NULL))
return;
/*
@@ -1793,12 +1802,12 @@ static int find_lowest_rq(struct task_struct *task)
*/
if (sched_asym_cpucap_active()) {
- ret = cpupri_find_fitness(&task_rq(task)->rd->cpupri,
+ ret = cpupri_find_fitness(&rcu_dereference_root_domain(task_rq(task)->rd)->cpupri,
task, lowest_mask,
rt_task_fits_capacity);
} else {
- ret = cpupri_find(&task_rq(task)->rd->cpupri,
+ ret = cpupri_find(&rcu_dereference_root_domain(task_rq(task)->rd)->cpupri,
task, lowest_mask);
}
@@ -2189,16 +2198,17 @@ static inline void rto_start_unlock(atomic_t *v)
static void tell_cpu_to_push(struct rq *rq)
{
+ struct root_domain *rd = rcu_dereference_root_domain(rq->rd);
int cpu = -1;
/* Keep the loop going if the IPI is currently active */
- atomic_inc(&rq->rd->rto_loop_next);
+ atomic_inc(&rd->rto_loop_next);
/* Only one CPU can initiate a loop at a time */
- if (!rto_start_trylock(&rq->rd->rto_loop_start))
+ if (!rto_start_trylock(&rd->rto_loop_start))
return;
- raw_spin_lock(&rq->rd->rto_lock);
+ raw_spin_lock(&rd->rto_lock);
/*
* The rto_cpu is updated under the lock, if it has a valid CPU
@@ -2206,17 +2216,17 @@ static void tell_cpu_to_push(struct rq *rq)
* update to loop_next, and nothing needs to be done here.
* Otherwise it is finishing up and an IPI needs to be sent.
*/
- if (rq->rd->rto_cpu < 0)
- cpu = rto_next_cpu(rq->rd);
+ if (rd->rto_cpu < 0)
+ cpu = rto_next_cpu(rd);
- raw_spin_unlock(&rq->rd->rto_lock);
+ raw_spin_unlock(&rd->rto_lock);
- rto_start_unlock(&rq->rd->rto_loop_start);
+ rto_start_unlock(&rd->rto_loop_start);
if (cpu >= 0) {
/* Make sure the rd does not get freed while pushing */
- sched_get_rd(rq->rd);
- irq_work_queue_on(&rq->rd->rto_push_work, cpu);
+ sched_get_rd(rd);
+ irq_work_queue_on(&rd->rto_push_work, cpu);
}
}
@@ -2277,7 +2287,7 @@ static void pull_rt_task(struct rq *this_rq)
/* If we are the only overloaded CPU do nothing */
if (rt_overload_count == 1 &&
- cpumask_test_cpu(this_rq->cpu, this_rq->rd->rto_mask))
+ cpumask_test_cpu(this_rq->cpu, rcu_dereference_root_domain(this_rq->rd)->rto_mask))
return;
#ifdef HAVE_RT_PUSH_IPI
@@ -2287,7 +2297,7 @@ static void pull_rt_task(struct rq *this_rq)
}
#endif
- for_each_cpu(cpu, this_rq->rd->rto_mask) {
+ for_each_cpu(cpu, rcu_dereference_root_domain(this_rq->rd)->rto_mask) {
if (this_cpu == cpu)
continue;
@@ -2392,7 +2402,7 @@ static void rq_online_rt(struct rq *rq)
__enable_runtime(rq);
- cpupri_set(&rq->rd->cpupri, rq->cpu, rq->rt.highest_prio.curr);
+ cpupri_set(&rcu_dereference_root_domain(rq->rd)->cpupri, rq->cpu, rq->rt.highest_prio.curr);
}
/* Assumes rq->lock is held */
@@ -2403,7 +2413,7 @@ static void rq_offline_rt(struct rq *rq)
__disable_runtime(rq);
- cpupri_set(&rq->rd->cpupri, rq->cpu, CPUPRI_INVALID);
+ cpupri_set(&rcu_dereference_root_domain(rq->rd)->cpupri, rq->cpu, CPUPRI_INVALID);
}
/*
diff --git a/kernel/sched/sched.h b/kernel/sched/sched.h
index 26ae13c86b69..6d45e67bcdc3 100644
--- a/kernel/sched/sched.h
+++ b/kernel/sched/sched.h
@@ -1256,7 +1256,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;
@@ -2143,6 +2143,9 @@ queue_balance_callback(struct rq *rq,
rq->balance_callback = head;
}
+#define rcu_dereference_root_domain(p) \
+ rcu_dereference_all_check((p), lockdep_is_held(&sched_domains_mutex))
+
#define rcu_dereference_sched_domain(p) \
rcu_dereference_all_check((p), lockdep_is_held(&sched_domains_mutex))
@@ -3030,7 +3033,7 @@ static inline void add_nr_running(struct rq *rq, unsigned count)
}
if (prev_nr < 2 && rq->nr_running >= 2)
- set_rd_overloaded(rq->rd, 1);
+ set_rd_overloaded(rcu_dereference_root_domain(rq->rd), 1);
sched_update_tick_dependency(rq);
}
diff --git a/kernel/sched/syscalls.c b/kernel/sched/syscalls.c
index b215b0ead9a6..89a2465727fd 100644
--- a/kernel/sched/syscalls.c
+++ b/kernel/sched/syscalls.c
@@ -621,15 +621,15 @@ int __sched_setscheduler(struct task_struct *p,
#endif /* CONFIG_RT_GROUP_SCHED */
if (dl_bandwidth_enabled() && dl_policy(policy) &&
!(attr->sched_flags & SCHED_FLAG_SUGOV)) {
- cpumask_t *span = rq->rd->span;
+ struct root_domain *rd = rcu_dereference_root_domain(rq->rd);
/*
* Don't allow tasks with an affinity mask smaller than
* the entire root_domain to become SCHED_DEADLINE. We
* will also fail if there's no bandwidth available.
*/
- if (!cpumask_subset(span, p->cpus_ptr) ||
- rq->rd->dl_bw.bw == 0) {
+ if (!cpumask_subset(rd->span, p->cpus_ptr) ||
+ rd->dl_bw.bw == 0) {
retval = -EPERM;
goto unlock;
}
@@ -1127,7 +1127,7 @@ int dl_task_check_affinity(struct task_struct *p, const struct cpumask *mask)
* root_domain.
*/
guard(rcu)();
- if (!cpumask_subset(task_rq(p)->rd->span, mask))
+ if (!cpumask_subset(rcu_dereference_root_domain(task_rq(p)->rd)->span, mask))
return -EBUSY;
return 0;
diff --git a/kernel/sched/topology.c b/kernel/sched/topology.c
index 21e816ad23ee..bf83ceee23e9 100644
--- a/kernel/sched/topology.c
+++ b/kernel/sched/topology.c
@@ -413,7 +413,7 @@ static bool build_perf_domains(const struct cpumask *cpu_map)
int i;
struct perf_domain *pd = NULL, *tmp;
int cpu = cpumask_first(cpu_map);
- struct root_domain *rd = cpu_rq(cpu)->rd;
+ struct root_domain *rd = rcu_dereference_root_domain(cpu_rq(cpu)->rd);
if (!sysctl_sched_energy_aware)
goto free;
@@ -478,9 +478,8 @@ void rq_attach_root(struct rq *rq, struct root_domain *rd)
rq_lock_irqsave(rq, &rf);
- if (rq->rd) {
- old_rd = rq->rd;
-
+ old_rd = rcu_dereference_root_domain(rq->rd);
+ if (old_rd) {
if (cpumask_test_cpu(rq->cpu, old_rd->online))
set_rq_offline(rq);
@@ -3461,8 +3460,10 @@ static void partition_sched_domains_locked(int ndoms_new, cpumask_var_t doms_new
/* Build perf domains: */
for (i = 0; i < ndoms_new; i++) {
for (j = 0; j < n && !sched_energy_update; j++) {
+ int cpu = cpumask_first(doms_cur[j]);
+
if (cpumask_equal(doms_new[i], doms_cur[j]) &&
- cpu_rq(cpumask_first(doms_cur[j]))->rd->pd) {
+ rcu_dereference_root_domain(cpu_rq(cpu)->rd)->pd) {
has_eas = true;
goto match3;
}
--
2.55.0
^ permalink raw reply [flat|nested] 7+ messages in thread
* [PATCH v9 2/6] sched/debug: Protect lockless rq->rd access in print_dl_rq()
2026-08-27 22:18 [PATCH v9 0/6] Introduce per-CPU debugfs files Aaron Tomlin
2026-08-27 22:18 ` [PATCH v9 1/6] sched: Annotate rq->rd with __rcu and update lockless readers Aaron Tomlin
@ 2026-08-27 22:18 ` Aaron Tomlin
2026-08-27 22:18 ` [PATCH v9 3/6] sched/debug: Protect lockless rq->curr access in print_cpu() Aaron Tomlin
` (3 subsequent siblings)
5 siblings, 0 replies; 7+ messages in thread
From: Aaron Tomlin @ 2026-08-27 22:18 UTC (permalink / raw)
To: mingo, peterz, juri.lelli, vincent.guittot
Cc: paulmck, 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 72236db67983..61932ef7ec4f 100644
--- a/kernel/sched/debug.c
+++ b/kernel/sched/debug.c
@@ -1172,7 +1172,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_root_domain(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 bf83ceee23e9..58913dc3a8f2 100644
--- a/kernel/sched/topology.c
+++ b/kernel/sched/topology.c
@@ -495,7 +495,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 [flat|nested] 7+ messages in thread
* [PATCH v9 3/6] sched/debug: Protect lockless rq->curr access in print_cpu()
2026-08-27 22:18 [PATCH v9 0/6] Introduce per-CPU debugfs files Aaron Tomlin
2026-08-27 22:18 ` [PATCH v9 1/6] sched: Annotate rq->rd with __rcu and update lockless readers Aaron Tomlin
2026-08-27 22:18 ` [PATCH v9 2/6] sched/debug: Protect lockless rq->rd access in print_dl_rq() Aaron Tomlin
@ 2026-08-27 22:18 ` Aaron Tomlin
2026-08-27 22:18 ` [PATCH v9 4/6] sched/debug: Protect p->mm access in sched_show_numa() Aaron Tomlin
` (2 subsequent siblings)
5 siblings, 0 replies; 7+ messages in thread
From: Aaron Tomlin @ 2026-08-27 22:18 UTC (permalink / raw)
To: mingo, peterz, juri.lelli, vincent.guittot
Cc: paulmck, 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 61932ef7ec4f..3d6248c50dad 100644
--- a/kernel/sched/debug.c
+++ b/kernel/sched/debug.c
@@ -1210,7 +1210,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 [flat|nested] 7+ messages in thread
* [PATCH v9 4/6] sched/debug: Protect p->mm access in sched_show_numa()
2026-08-27 22:18 [PATCH v9 0/6] Introduce per-CPU debugfs files Aaron Tomlin
` (2 preceding siblings ...)
2026-08-27 22:18 ` [PATCH v9 3/6] sched/debug: Protect lockless rq->curr access in print_cpu() Aaron Tomlin
@ 2026-08-27 22:18 ` Aaron Tomlin
2026-08-27 22:18 ` [PATCH v9 5/6] sched/fair: Use list_for_each_entry_rcu() in print_cfs_stats() Aaron Tomlin
2026-08-27 22:18 ` [PATCH v9 6/6] sched/debug: Introduce per-CPU debugfs files Aaron Tomlin
5 siblings, 0 replies; 7+ messages in thread
From: Aaron Tomlin @ 2026-08-27 22:18 UTC (permalink / raw)
To: mingo, peterz, juri.lelli, vincent.guittot
Cc: paulmck, 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 3d6248c50dad..1b6d2a83d50f 100644
--- a/kernel/sched/debug.c
+++ b/kernel/sched/debug.c
@@ -1395,8 +1395,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 [flat|nested] 7+ messages in thread
* [PATCH v9 5/6] sched/fair: Use list_for_each_entry_rcu() in print_cfs_stats()
2026-08-27 22:18 [PATCH v9 0/6] Introduce per-CPU debugfs files Aaron Tomlin
` (3 preceding siblings ...)
2026-08-27 22:18 ` [PATCH v9 4/6] sched/debug: Protect p->mm access in sched_show_numa() Aaron Tomlin
@ 2026-08-27 22:18 ` Aaron Tomlin
2026-08-27 22:18 ` [PATCH v9 6/6] sched/debug: Introduce per-CPU debugfs files Aaron Tomlin
5 siblings, 0 replies; 7+ messages in thread
From: Aaron Tomlin @ 2026-08-27 22:18 UTC (permalink / raw)
To: mingo, peterz, juri.lelli, vincent.guittot
Cc: paulmck, 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 locklessly under
RCU using for_each_leaf_cfs_rq_safe(), which expands to
list_for_each_entry_safe(). Although rq->leaf_cfs_rq_list is modified
using list_add_rcu(), list_for_each_entry_safe() is a non-RCU iteration
macro that dereferences pointer links without READ_ONCE() and pre-fetches
the next pointer without memory ordering guarantees.
Without READ_ONCE(), the compiler is free to re-fetch pointers or reorder
instructions. As a result, a lockless 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.
Furthermore, in the core scheduler, cfs_rq structures are embedded in
struct task_group and are enqueued/dequeued dynamically on each CPU's
leaf_cfs_rq_list during task wakeups and throttling. Because this occurs
in atomic fast paths under rq->lock, deferring list deletion with an RCU
grace period or allocating dynamic proxy nodes is not feasible. While the
underlying task_group/cfs_rq memory backing each node is safely reclaimed
via call_rcu() (i.e., sched_free_group_rcu()), immediate node
re-insertion can modify cfs_rq->next while print_cfs_stats() is
executing locklessly. Under continuous list churn, lockless readers
could experience backward jumps, resulting in unbounded list traversal
inside the RCU read-side critical section and triggering an RCU CPU
stall warning.
Address this by:
1. Introducing for_each_leaf_cfs_rq_rcu(), which expands to
list_for_each_entry_rcu(). This enforces READ_ONCE() and proper
data-dependency ordering on all architectures during list
traversal
2. Using guard(rcu)() to ensure the backing task_group/cfs_rq
memory remains valid throughout traversal
3. Capping the lockless list traversal with a per-CPU
circuit-breaker ceiling. This represents a generous upper bound
for active leaf cfs_rqs on an individual core while guaranteeing
loop termination and preventing RCU stalls under list churn
4. Emitting an explicit truncation notice if the ceiling is ever
reached, ensuring transparency in debugfs
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 1b6d2a83d50f..ffc55b89801a 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:
@@ -933,38 +922,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 687999312a7d..c6b25ac2b844 100644
--- a/kernel/sched/fair.c
+++ b/kernel/sched/fair.c
@@ -416,6 +416,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)
@@ -469,6 +473,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;
@@ -15580,14 +15587,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 6d45e67bcdc3..c70807f17104 100644
--- a/kernel/sched/sched.h
+++ b/kernel/sched/sched.h
@@ -777,6 +777,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 {
@@ -3413,6 +3425,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
+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 [flat|nested] 7+ messages in thread
* [PATCH v9 6/6] sched/debug: Introduce per-CPU debugfs files
2026-08-27 22:18 [PATCH v9 0/6] Introduce per-CPU debugfs files Aaron Tomlin
` (4 preceding siblings ...)
2026-08-27 22:18 ` [PATCH v9 5/6] sched/fair: Use list_for_each_entry_rcu() in print_cfs_stats() Aaron Tomlin
@ 2026-08-27 22:18 ` Aaron Tomlin
5 siblings, 0 replies; 7+ messages in thread
From: Aaron Tomlin @ 2026-08-27 22:18 UTC (permalink / raw)
To: mingo, peterz, juri.lelli, vincent.guittot
Cc: paulmck, 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 ffc55b89801a..0c1122007543 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)
{
@@ -698,6 +699,47 @@ static const struct file_operations sched_cgroup_fops = {
};
#endif
+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;
@@ -759,6 +801,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 [flat|nested] 7+ messages in thread
end of thread, other threads:[~2026-08-27 22:18 UTC | newest]
Thread overview: 7+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-08-27 22:18 [PATCH v9 0/6] Introduce per-CPU debugfs files Aaron Tomlin
2026-08-27 22:18 ` [PATCH v9 1/6] sched: Annotate rq->rd with __rcu and update lockless readers Aaron Tomlin
2026-08-27 22:18 ` [PATCH v9 2/6] sched/debug: Protect lockless rq->rd access in print_dl_rq() Aaron Tomlin
2026-08-27 22:18 ` [PATCH v9 3/6] sched/debug: Protect lockless rq->curr access in print_cpu() Aaron Tomlin
2026-08-27 22:18 ` [PATCH v9 4/6] sched/debug: Protect p->mm access in sched_show_numa() Aaron Tomlin
2026-08-27 22:18 ` [PATCH v9 5/6] sched/fair: Use list_for_each_entry_rcu() in print_cfs_stats() Aaron Tomlin
2026-08-27 22:18 ` [PATCH v9 6/6] sched/debug: Introduce per-CPU debugfs files Aaron Tomlin
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®