[Devel] [PATCH vz9 v2] sched/fair: cancel per-cfs_rq active_timer before task_group teardown

Konstantin Khorenko khorenko at virtuozzo.com
Wed Apr 22 23:50:13 MSK 2026


The per-cfs_rq active_timer (CONFIG_CFS_CPULIMIT) is armed by
dec_nr_active_cfs_rqs() to defer the tg->nr_cpus_active decrement
when a task goes to sleep.  Its callback sched_cfs_active_timer()
dereferences cfs_rq->tg.

When a task group is destroyed, unregister_fair_sched_group() tears
down the per-CPU cfs_rq structures but never cancels the active_timer.
The teardown chain is:

    sched_destroy_group()
      call_rcu()
        sched_unregister_group_rcu()
          sched_unregister_group()
            unregister_fair_sched_group()
            call_rcu()
              sched_free_group_rcu()
                sched_free_group()
                  kmem_cache_free(task_group_cache, tg)

If the timer fires during or after that sequence, the callback
performs atomic_dec() through a dangling cfs_rq->tg pointer.  All
three relevant objects - cfs_rq, sched_entity, and task_group - live
in the kmalloc-1k slab cache, so once the slot is reused the
atomic_dec() lands on an arbitrary kernel address and silently
corrupts memory.

This was observed as a hard lockup and NULL-pointer oops in
enqueue_task_fair() during task wakeup: the se->parent pointer at
offset 128 in a sched_entity was corrupted because it shares the same
slab offset as cfs_rq->skip (also offset 128), a classic cross-type
UAF in a shared slab cache.

Fix this by cancelling the active_timer in unregister_fair_sched_group()
before the teardown proceeds.  The cancellation:

  - goes before the on_list early-continue: active_timer state is
    independent of leaf-list membership, and the timer is always
    initialized in init_cfs_rq_runtime(), so hrtimer_cancel() is safe
    unconditionally;

  - stays outside the rq lock: the callback itself takes that lock,
    so calling hrtimer_cancel() under it would deadlock.

hrtimer_cancel() blocks until a running callback has fully returned
from its fn() (base->running is cleared only after fn() completes),
so after this call neither a pending nor an in-flight callback can
still be racing with the teardown.  Since the only path to
kmem_cache_free(task_group_cache) goes through
unregister_fair_sched_group() first, this closes the UAF on its own;
no additional serialization of the atomic_dec() against the teardown
is needed.

Fixes: 831465734a10 ("sched: Port CONFIG_CFS_CPULIMIT feature")
https://virtuozzo.atlassian.net/browse/VSTOR-126785

Feature: sched: ability to limit number of CPUs available to a CT
Signed-off-by: Konstantin Khorenko <khorenko at virtuozzo.com>
---
 kernel/sched/fair.c | 17 +++++++++++++++++
 1 file changed, 17 insertions(+)

diff --git a/kernel/sched/fair.c b/kernel/sched/fair.c
index ab2a890cccd4e..202d9b0e08fb5 100644
--- a/kernel/sched/fair.c
+++ b/kernel/sched/fair.c
@@ -13023,6 +13023,23 @@ void unregister_fair_sched_group(struct task_group *tg)
 	destroy_cfs_bandwidth(tg_cfs_bandwidth(tg));
 
 	for_each_possible_cpu(cpu) {
+#ifdef CONFIG_CFS_CPULIMIT
+		/*
+		 * Cancel the per-cfs_rq active_timer before the tg/cfs_rq
+		 * memory can be freed.  The callback dereferences cfs_rq->tg,
+		 * so failing to cancel would leave a use-after-free window
+		 * once the tg is freed via the RCU callback that follows
+		 * this teardown.  hrtimer_cancel() blocks until a running
+		 * callback has fully returned, which is sufficient on its
+		 * own: the only path to kmem_cache_free(task_group_cache)
+		 * goes through this function first.
+		 * Must be done outside the rq lock - the callback acquires
+		 * it.  The active_timer is always initialized in
+		 * init_cfs_rq_runtime(), so hrtimer_cancel() is safe
+		 * regardless of the on_list state below.
+		 */
+		hrtimer_cancel(&tg->cfs_rq[cpu]->active_timer);
+#endif
 		if (tg->se[cpu])
 			remove_entity_load_avg(tg->se[cpu]);
 
-- 
2.43.0



More information about the Devel mailing list