[Devel] [PATCH vz7] sched/fair: cancel per-cfs_rq active_timer before task_group teardown
Pavel Tikhomirov
ptikhomirov at virtuozzo.com
Fri May 15 11:35:02 MSK 2026
Reviewed-by: Pavel Tikhomirov <ptikhomirov at virtuozzo.com>
On 4/22/26 22:32, Konstantin Khorenko wrote:
> 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 caller, sched_offline_group(), then proceeds to list_del_rcu() the
Technically the caller of sched_offline_group() right? Cause "offline"
normally does not do freeing. It's either cpu_cgroup_css_free() or
autogroup_destroy() which does actual free.
I don't see any mechanism to prevent "free" part from happening while
the timer is still armed (I don't think there is cg/css refcount taken).
So the motivation looks correct.
> task_group and schedules free_sched_group_rcu() via call_rcu(), which
> eventually kfree()s the task_group. 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.
>
> Fix this by cancelling the active_timer in unregister_fair_sched_group()
> before the teardown proceeds. The cancellation:
>
> - goes before the on_list early-return: 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 kfree(tg)
> 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: f3fec68860fb ("ve/sched: port vcpu hotslice")
> https://virtuozzo.atlassian.net/browse/PSBM-161930
> Signed-off-by: Konstantin Khorenko <khorenko at virtuozzo.com>
> ---
> kernel/sched/fair.c | 16 ++++++++++++++++
> 1 file changed, 16 insertions(+)
>
> diff --git a/kernel/sched/fair.c b/kernel/sched/fair.c
> index 552d288d81648..b5a7e9f72e6d1 100644
> --- a/kernel/sched/fair.c
> +++ b/kernel/sched/fair.c
> @@ -8544,6 +8544,22 @@ void unregister_fair_sched_group(struct task_group *tg, int cpu)
> struct rq *rq = cpu_rq(cpu);
> unsigned long flags;
>
> +#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
> + * kfree(tg) goes through this function first.
> + * Must be done outside the rq lock - the callback acquires it.
> + * 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
> +
> /*
> * Only empty task groups can be destroyed; so we can speculatively
> * check on_list without danger of it being re-added.
--
Best regards, Pavel Tikhomirov
Senior Software Developer, Virtuozzo.
More information about the Devel
mailing list