mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
From: sashiko-bot@kernel.org
To: "Mathieu Desnoyers" <mathieu.desnoyers@efficios.com>
Cc: linux-rt-devel@lists.linux.dev, tj@kernel.org,
	hannes@cmpxchg.org, mkoutny@suse.com,
	linux-kernel@vger.kernel.org
Subject: Re: [PATCH 07/12] sched/deadline: Keep departing bandwidth across a root domain rebuild
Date: Wed, 04 Nov 2026 15:47:10 +0000	[thread overview]
Message-ID: <sashiko-outbox-197541@kernel.org> (raw)
In-Reply-To: <9a2c58b84302e5ed3765f773eca3c71c7d5ea7bb.1793804635.git.mathieu.desnoyers@efficios.com>

Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
- [High] sched/deadline: bandwidth leak and underflow in switched_to_dl() on PI boost
- [High] sched/deadline: dl_clear_root_domains() drops and resurrects departed bw
- [High] sched/deadline: exiting tasks lose or corrupt bandwidth across rd rebuild
- [High] sched/deadline: task_non_contending() uses task_cpu() instead of mask CPU
--

commit e37776f3c05c45d876702021f65c587c9806cb26
Author: Mathieu Desnoyers <mathieu.desnoyers@efficios.com>

sched/deadline: Keep departing bandwidth across a root domain rebuild

This commit tracks tasks that leave SCHED_DEADLINE or die before their
0-lag time on a departed list associated with a CPU of their root domain,
and uses a rebuild generation counter to preserve or release their
bandwidth across root domain accounting rebuilds.

> diff --git a/kernel/sched/deadline.c b/kernel/sched/deadline.c
> index 3c57e5f5fc056..8ead9b050dfcf 100644
> --- a/kernel/sched/deadline.c
> +++ b/kernel/sched/deadline.c

[ ... ]

> @@ -217,6 +223,88 @@ __dl_overflow(struct dl_bw *dl_b, unsigned long cap, u64 old_bw, u64 new_bw)
>  	       cap_scale(dl_b->bw, cap) < dl_b->total_bw - old_bw + new_bw;
>  }
>  
> +/*
> + * A task that leaves SCHED_DEADLINE, or dies, before its 0-lag time keeps
> + * its bandwidth in total_bw until then: the inactive timer removes it. A
> + * rebuild of the root domains' accounting meanwhile (dl_clear_root_domains()
> + * and dl_add_task_root_domain(), on any change of the partitions or CPU
> + * hotplug) finds no such task among the deadline tasks of the cpusets: it
> + * puts that bandwidth back from dl_departed_list, which holds them from
> + * their departure to their timer, in the root domain each left, found by
> + * its mask then (dl_task_root_domain_cpu()), or by the CPU it dies on: the
> + * CPU it last ran on may be of another one, when it was moved asleep. The
> + * task may run anywhere meanwhile, as another policy; its timer holds a
> + * reference to it.
> + *
> + * dl_bw_gen counts the rebuilds, and each task records the one that last
> + * counted its bandwidth. A rebuild that dropped it since then leaves
> + * nothing to remove: the CPU recorded was no longer active, or the task
> + * exited while the rebuild ran, which the cpuset walk skips, and was not
> + * dead yet when the list was put back. Both counts, and the list, are under
> + * dl_departed_lock, taken with interrupts disabled (under the task's rq
> + * lock or pi_lock, or by the rebuild) and before any dl_bw lock.
> + */
> +static LIST_HEAD(dl_departed_list);
> +static DEFINE_RAW_SPINLOCK(dl_departed_lock);
> +static unsigned int dl_bw_gen;

[ ... ]

> +static void dl_task_release_bw(struct task_struct *p, int cpu)
> +{
> +	struct sched_dl_entity *dl_se = &p->dl;
> +	struct dl_bw *dl_b;
> +
> +	lockdep_assert_irqs_disabled();
> +	guard(raw_spinlock)(&dl_departed_lock);
> +	if (dl_se->dl_departed_cpu >= 0) {
> +		cpu = dl_se->dl_departed_cpu;
> +		dl_se->dl_departed_cpu = -1;
> +		list_del(&dl_se->dl_departed_node);
> +	}
> +	if (dl_se->dl_bw_gen == dl_bw_gen) {
> +		dl_b = dl_bw_of(cpu);
> +		raw_spin_lock(&dl_b->lock);
> +		__dl_sub(dl_b, dl_se->dl_bw, dl_bw_cpus(cpu));
> +		raw_spin_unlock(&dl_b->lock);
> +	}
> +	dl_se->dl_bw_gen = dl_bw_gen;
> +}

[Severity: High]
What happens when dl_rebuild_rd_accounting() runs while a SCHED_DEADLINE
task is in the middle of do_exit()?

In do_exit(), exit_signals() sets PF_EXITING and decrements signal->live to
0 near the beginning of task teardown, before exit_mm(), exit_files(),
exit_notify(), and cgroup_exit(), whereas do_task_dead() -> __schedule() ->
deactivate_task() -> dequeue_task_dl() -> task_non_contending() ->
dl_task_depart() only runs at the very end of do_exit().

If dl_rebuild_rd_accounting() runs during that window (or if the task
reaches do_task_dead() after dl_clear_root_domains() has walked
dl_departed_list, and unlinks from its cset before
dl_update_tasks_root_domain() walks it), dl_clear_root_domains() increments
dl_bw_gen without finding the task on dl_departed_list, and
dl_update_tasks_root_domain() calls css_task_iter_start(&cs->css, 0, &it)
without CSS_TASK_ITER_WITH_DEAD, which skips the task because PF_EXITING is
set and signal->live is 0.

Doesn't that cause three problems for an exiting deadline task?

1. Its bandwidth is dropped from total_bw while the task is still running
in do_exit() and before its 0-lag time.

2. When the task finally reaches do_task_dead() -> task_non_contending()
with zerolag_time >= 0, task_non_contending() unconditionally calls
dl_task_depart(p, task_cpu(p)) without checking whether p->dl.dl_bw_gen
matches dl_bw_gen. If a second rebuild runs before inactive_task_timer()
fires, dl_clear_root_domains() will add p->dl.dl_bw back into total_bw on
top of any bandwidth admitted after the first rebuild.

3. If the task is migrated while in do_exit() after the first rebuild
dropped its bandwidth (for example via set_cpus_allowed_dl() or
dl_task_offline_migration()), those paths subtract p->dl.dl_bw from the
source root domain without checking p->dl.dl_bw_gen == dl_bw_gen
(underflowing total_bw), and dl_task_offline_migration() adds p->dl.dl_bw
to later_rq->rd->dl_bw without updating p->dl.dl_bw_gen, so
dl_task_release_bw() later skips subtracting it and leaks p->dl.dl_bw in
later_rq->rd.

[ ... ]

> @@ -449,13 +537,9 @@ static void task_non_contending(struct sched_dl_entity *dl_se, bool dl_task)
>  				sub_running_bw(dl_se, dl_rq);
>  
>  			if (!dl_task || READ_ONCE(p->__state) == TASK_DEAD) {
> -				struct dl_bw *dl_b = dl_bw_of(task_cpu(p));
> -
>  				if (READ_ONCE(p->__state) == TASK_DEAD)
>  					sub_rq_bw(dl_se, &rq->dl);
> -				raw_spin_lock(&dl_b->lock);
> -				__dl_sub(dl_b, dl_se->dl_bw, dl_bw_cpus(task_cpu(p)));
> -				raw_spin_unlock(&dl_b->lock);
> +				dl_task_release_bw(p, task_cpu(p));
>  				__dl_clear_params(dl_se);
>  			}
>  		}
> @@ -464,8 +548,18 @@ static void task_non_contending(struct sched_dl_entity *dl_se, bool dl_task)
>  	}
>  
>  	dl_se->dl_non_contending = 1;
> -	if (!dl_server(dl_se))
> -		get_task_struct(dl_task_of(dl_se));
> +	if (!dl_server(dl_se)) {
> +		struct task_struct *p = dl_task_of(dl_se);
> +
> +		get_task_struct(p);
> +		/*
> +		 * Dying: its bandwidth is in the root domain of the CPU it
> +		 * runs on. switched_from_dl() records one that leaves
> +		 * SCHED_DEADLINE.
> +		 */
> +		if (READ_ONCE(p->__state) == TASK_DEAD)
> +			dl_task_depart(p, task_cpu(p));
> +	}

[Severity: High]
Should task_non_contending() use dl_task_root_domain_cpu(p) instead of
task_cpu(p) when calling dl_task_release_bw() and dl_task_depart()?

Suppose a SCHED_DEADLINE task p is running on CPU 0 in RD_0 (or has
is_migration_disabled(p) true) while it is moved to a cpuset partition in
RD_1:

  cpuset_can_attach()
    -> reserves p->dl.dl_bw in RD_1->dl_bw.total_bw
  cpuset_attach_task()
    -> set_cpus_allowed_ptr()
      -> __set_cpus_allowed_ptr_locked()
        -> do_set_cpus_allowed()
          -> set_cpus_allowed_dl()
            -> subtracts p->dl.dl_bw from RD_0->dl_bw.total_bw
            -> updates p->cpus_mask to RD_1
        -> affine_move_task()
          -> task_on_cpu(rq0, p) or is_migration_disabled(p) is true
          -> unlocks rq0 and queues migration_cpu_stop (or waits for
             migrate_enable())

Before migration_cpu_stop runs on CPU 0 (or while migration remains
disabled), p's bandwidth is already accounted in RD_1 (matching
p->cpus_mask), but task_cpu(p) is still CPU 0 in RD_0.

If p then exits into do_task_dead() -> dequeue_task_dl() ->
task_non_contending(&p->dl, true), or switches policy via
switched_from_dl() -> task_non_contending(&p->dl, false) when zerolag_time
< 0 or hrtimer_active(&dl_se->inactive_timer) is true, passing task_cpu(p)
causes dl_task_release_bw() or dl_task_depart() to target RD_0 instead of
RD_1, underflowing RD_0->dl_bw.total_bw and leaking p->dl.dl_bw in
RD_1->dl_bw.total_bw.

[ ... ]

> @@ -3542,9 +3653,37 @@ void dl_clear_root_domain(struct root_domain *rd)
>  		dl_server_add_bw(rd, i);
>  }
>  
> -void dl_clear_root_domain_cpu(int cpu)
> +/*
> + * Clear every root domain ahead of a rebuild of their accounting, which
> + * dl_add_task_root_domain() then fills with the deadline tasks of the
> + * cpusets: put back their servers, and the bandwidth of the tasks that
> + * left SCHED_DEADLINE, or died, before their 0-lag time, which no cpuset
> + * holds as a deadline task, in the root domain of the CPU recorded for
> + * each; dropped if it is no longer active.
> + */
> +void dl_clear_root_domains(void)
>  {
> -	dl_clear_root_domain(cpu_rq(cpu)->rd);
> +	struct sched_dl_entity *dl_se;
> +	u64 cookie = ++dl_cookie;
> +	struct dl_bw *dl_b;
> +	int cpu;
> +
> +	guard(raw_spinlock_irqsave)(&dl_departed_lock);
> +	dl_bw_gen++;
> +	for_each_possible_cpu(cpu) {
> +		if (!dl_bw_visited(cpu, cookie))
> +			dl_clear_root_domain(cpu_rq(cpu)->rd);
> +	}
> +	list_for_each_entry(dl_se, &dl_departed_list, dl_departed_node) {
> +		cpu = dl_se->dl_departed_cpu;
> +		if (!cpu_active(cpu))
> +			continue;
> +		dl_b = dl_bw_of(cpu);
> +		raw_spin_lock(&dl_b->lock);
> +		__dl_add(dl_b, dl_se->dl_bw, dl_bw_cpus(cpu));
> +		raw_spin_unlock(&dl_b->lock);
> +		dl_se->dl_bw_gen = dl_bw_gen;
> +	}
>  }

[Severity: High]
Can dl_clear_root_domains() prematurely drop departed bandwidth in a
multi-CPU root domain and later resurrect it?

When a task departs or dies in a root domain spanning CPUs 0 and 1,
dl_task_depart() records only a single CPU in dl_se->dl_departed_cpu (for
example, CPU 1). If CPU 1 is then taken offline via sched_cpu_deactivate(1)
before the task's 0-lag time while CPU 0 remains active in the same root
domain, dl_bw_deactivate(1) succeeds if CPU 0 has enough capacity for the
root domain's total_bw. However, when cpuset_cpu_inactive() ->
dl_rebuild_rd_accounting() -> dl_clear_root_domains() runs, cpu_active(1)
is false, so dl_clear_root_domains() skips dl_se and drops its bandwidth
from the still-active root domain before its 0-lag time.

In addition, when dl_clear_root_domains() skips an entry because
!cpu_active(cpu), the entry stays on dl_departed_list with its old
dl_bw_gen. If CPU 1 becomes active again and another rebuild runs before
inactive_task_timer() fires, dl_clear_root_domains() does not check whether
dl_se->dl_bw_gen matched the previous generation before dl_bw_gen++, so it
adds dl_se->dl_bw back into dl_bw_of(cpu) and updates dl_se->dl_bw_gen =
dl_bw_gen after that bandwidth was already dropped.

[ ... ]

> @@ -3642,6 +3786,16 @@ static void switched_from_dl(struct rq *rq, struct task_struct *p)
>  static void switched_to_dl(struct rq *rq, struct task_struct *p)
>  {
>  	cancel_inactive_timer(&p->dl);
> +	/*
> +	 * Boosted back into the class before its 0-lag time (a policy change
> +	 * released that bandwidth in sched_dl_overflow()): the inactive
> +	 * timer will not remove it, but its leaving the class again. That
> +	 * forgets the CPU recorded: moved to another root domain meanwhile,
> +	 * the task then removes the bandwidth from that one, as it did
> +	 * before departures were recorded.
> +	 */
> +	if (p->dl.dl_departed_cpu >= 0)
> +		dl_task_undepart(p);

[ ... ]

> @@ -3909,6 +4055,19 @@ int sched_dl_overflow(struct task_struct *p, int policy,
>  	}
>  	raw_spin_unlock(&dl_b->lock);
>  
> +	/*
> +	 * Back before its 0-lag time, the task still has the bandwidth it
> +	 * left with, which the new one replaces: removed from the root
> +	 * domain that keeps it (dl_task_release_bw()). A rebuild cannot
> +	 * run meanwhile: the caller holds cpuset_mutex.
> +	 */
> +	if (!err && dl_policy(policy) && !task_has_dl_policy(p)) {
> +		lockdep_assert_cpuset_lock_held();
> +		if (p->dl.dl_departed_cpu >= 0)
> +			dl_task_release_bw(p, -1);
> +		p->dl.dl_bw_gen = dl_bw_gen;
> +	}
> +
>  	return err;
>  }

[Severity: High]
Does calling dl_task_undepart(p) in switched_to_dl() when a departed task
is PI-boosted cause bandwidth leaks and cross-root-domain underflows?

When a SCHED_DEADLINE task p switches to SCHED_OTHER before its 0-lag time,
switched_from_dl() arms inactive_timer, keeps p->dl.dl_runtime and
p->dl.dl_bw set, and records the origin root domain CPU in
p->dl.dl_departed_cpu via dl_task_depart(). If p is then boosted into
dl_sched_class via rt_mutex_setprio() before the timer fires,
switched_to_dl() cancels inactive_timer and calls dl_task_undepart(p),
which resets p->dl.dl_departed_cpu to -1 and removes p from
dl_departed_list without releasing p->dl.dl_bw from the origin root domain:

1. If p is switched back to SCHED_DEADLINE via sched_setattr() while still
PI-boosted (!task_has_dl_policy(p) is true), sched_dl_overflow() adds
new_bw to dl_b->total_bw and then checks:

	if (p->dl.dl_departed_cpu >= 0)
		dl_task_release_bw(p, -1);

Because switched_to_dl() already set p->dl.dl_departed_cpu = -1 (or if p
was already PI-boosted when it earlier switched from SCHED_DEADLINE to
SCHED_OTHER, so switched_from_dl() never ran), dl_task_release_bw() is
skipped and __setparam_dl() overwrites p->dl.dl_bw with new_bw, permanently
leaking the old p->dl.dl_bw in total_bw (and in rq->dl.this_bw if p was not
queued when boosted).

2. If p migrated to another root domain (RD_B) while in SCHED_OTHER and is
then PI-boosted, dl_task_undepart(p) forgets the origin root domain (RD_A)
recorded in dl_departed_cpu. When p is later deboosted in RD_B,
switched_from_dl() releases or re-departs p->dl.dl_bw from RD_B via
dl_task_root_domain_cpu(p), underflowing RD_B->dl_bw.total_bw and leaking
the bandwidth in RD_A->dl_bw.total_bw.

3. If p is sleeping when PI-boosted and the waiter times out or is
interrupted while p is still sleeping (!task_on_rq_queued(p)), deboosting p
calls switched_from_dl() with p->dl.dl_non_contending == 0, which enters:

	} else if (p->dl.dl_runtime && !dl_entity_is_special(&p->dl)) {
		dl_task_release_bw(p, dl_task_root_domain_cpu(p));
		released = true;
	}

and releases p->dl.dl_bw immediately before p's 0-lag time.

4. If dl_clear_root_domains() runs while p is PI-boosted (so p is not on
dl_departed_list) and p is deboosted before dl_add_task_root_domain(p)
visits it, dl_add_task_root_domain(p) sees !dl_task(p) and returns early,
dropping p's bandwidth before its 0-lag time.

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/cover.1793804635.git.mathieu.desnoyers@efficios.com?part=7

  reply	other threads:[~2026-11-04 15:47 UTC|newest]

Thread overview: 23+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-11-04 15:10 [PATCH 00/12] sched/deadline: bandwidth accounting fixes and selftests Mathieu Desnoyers
2026-11-04 15:10 ` [PATCH 01/12] sched: Reject SCHED_FLAG_KEEP_PARAMS before reserving deadline bandwidth Mathieu Desnoyers
2026-11-04 15:30   ` sashiko-bot
2026-11-04 15:52     ` Mathieu Desnoyers
2026-11-04 15:10 ` [PATCH 02/12] sched/deadline: Do not divide by zero active CPUs of a root domain Mathieu Desnoyers
2026-11-04 15:10 ` [PATCH 03/12] sched/deadline: Do not BUG() on a deadline task with no active CPU allowed Mathieu Desnoyers
2026-11-04 15:39   ` sashiko-bot
2026-11-04 15:10 ` [PATCH 04/12] sched/deadline: Do not lose a throttled task switched out and back Mathieu Desnoyers
2026-11-04 15:42   ` sashiko-bot
2026-11-04 15:10 ` [PATCH 05/12] sched/deadline: Do not release a departing sleeper's bandwidth twice Mathieu Desnoyers
2026-11-04 15:33   ` sashiko-bot
2026-11-04 15:10 ` [PATCH 06/12] selftests/sched: Add a test of SCHED_DEADLINE policy round trips Mathieu Desnoyers
2026-11-04 15:10 ` [PATCH 07/12] sched/deadline: Keep departing bandwidth across a root domain rebuild Mathieu Desnoyers
2026-11-04 15:47   ` sashiko-bot [this message]
2026-11-04 15:10 ` [PATCH 08/12] selftests/sched: Test SCHED_DEADLINE " Mathieu Desnoyers
2026-11-04 15:10 ` [PATCH 09/12] sched/deadline: Do not take a waking task's bandwidth off a runqueue it left Mathieu Desnoyers
2026-11-04 15:45   ` sashiko-bot
2026-11-04 15:10 ` [PATCH 10/12] sched/debug: Show the deadline bandwidth of each runqueue Mathieu Desnoyers
2026-11-04 15:10 ` [PATCH 11/12] sched/deadline: Rebuild the root domains' accounting without cpusets too Mathieu Desnoyers
2026-11-04 15:45   ` sashiko-bot
2026-11-04 15:10 ` [PATCH 12/12] selftests/sched: Add a test of SCHED_DEADLINE bandwidth accounting Mathieu Desnoyers
2026-11-04 15:52   ` sashiko-bot
2026-11-04 20:24 ` [PATCH 00/12] sched/deadline: bandwidth accounting fixes and selftests Mathieu Desnoyers

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=sashiko-outbox-197541@kernel.org \
    --to=sashiko-bot@kernel.org \
    --cc=hannes@cmpxchg.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-rt-devel@lists.linux.dev \
    --cc=mathieu.desnoyers@efficios.com \
    --cc=mkoutny@suse.com \
    --cc=sashiko-reviews@lists.linux.dev \
    --cc=tj@kernel.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

all inboxes | Powered by JetHome®