[PATCH v3 02/24] sched: Refuse SCHED_FLAG_KEEP_PARAMS that would change the policy or the class
From: Mathieu Desnoyers
Date: Thu Nov 05 2026 - 14:52:43 EST
SCHED_FLAG_KEEP_PARAMS keeps the parameters of a task:
__sched_setscheduler() skips __setscheduler_params(), which is what sets
p->policy, and the sched_class assignment with it. A request that asks
for another policy is then carried out by half, and succeeds:
- a class change, SCHED_DEADLINE to SCHED_OTHER, sets DEQUEUE_CLASS:
sched_change_begin() runs switched_from_dl() on a task that stays in
the class, and sched_change_end() switched_to_dl(). A sleeping task
has its bandwidth taken off its runqueue there, and taken off again
when it does leave: this_bw underflows;
- a policy change within the class, SCHED_OTHER to SCHED_BATCH, returns
0 and leaves the task where it was;
- a task that priority inheritance boosted into the deadline class is
of that class already, and a request for SCHED_DEADLINE finds
prev_class == next_class: sched_dl_overflow() admits it and adds its
bandwidth, which the skipped __setscheduler_params() never gives to
the task. It keeps SCHED_NORMAL and a dl_bw of 0, so nothing releases
that bandwidth, and total_bw of the root domain holds it for good.
Refuse a request for another policy or another class, before
sched_dl_overflow() reserves bandwidth, since nothing gives back what it
reserves: a refusal after it would leak the runtime that sched_setattr()
fills in from the task's slice, up to 100ms with a custom one, into
total_bw. A refused request also leaves sched_reset_on_fork as it was.
A SCHED_OTHER owner of a PTHREAD_PRIO_INHERIT mutex with a slice of
40ms, boosted by a deadline waiter blocked on it, leaves 0.4 of a CPU
behind at every such call: total_bw 419424 -> 838854 and there to stay,
in 8-CPU VMs of v7.4-rc1. With this change the call is refused and
total_bw is as it was. The selftest added later in this series has the
case, params_keep_boosted.
The class change was found by Juri Lelli from a report of Tejun Heo, and
refused, after the reservation, by his "sched: Reject
SCHED_FLAG_KEEP_PARAMS when it would change scheduling class", since
dropped from tip. The policy change and the boosted case were found by
the Sashiko AI review of the first posting of this series.
Link: https://lore.kernel.org/all/2b5fb3f22bca6aa383be8f4781923392@xxxxxxxxxx/
Link: https://lore.kernel.org/all/20261103-b4-sched-dl-bw-fixes-v1-3-dd761fe005b4@xxxxxxxxxx/
Fixes: a509a7cd7974 ("sched/uclamp: Extend sched_setattr() to support utilization clamping")
Reviewed-by: Juri Lelli <juri.lelli@xxxxxxxxxx>
Cc: stable@xxxxxxxxxxxxxxx
Signed-off-by: Mathieu Desnoyers <mathieu.desnoyers@xxxxxxxxxxxx>
---
kernel/sched/syscalls.c | 40 +++++++++++++++++++++++++++++-----------
1 file changed, 29 insertions(+), 11 deletions(-)
diff --git a/kernel/sched/syscalls.c b/kernel/sched/syscalls.c
index 2af550f92ac6..6819f0fea3c5 100644
--- a/kernel/sched/syscalls.c
+++ b/kernel/sched/syscalls.c
@@ -647,17 +647,6 @@ int __sched_setscheduler(struct task_struct *p,
goto recheck;
}
- /*
- * If setscheduling to SCHED_DEADLINE (or changing the parameters
- * of a SCHED_DEADLINE task) we need to check if enough bandwidth
- * is available.
- */
- if ((dl_policy(policy) || dl_task(p)) && sched_dl_overflow(p, policy, attr)) {
- retval = -EBUSY;
- goto unlock;
- }
-
- p->sched_reset_on_fork = reset_on_fork;
oldprio = p->prio;
newprio = __normal_prio(policy, attr->sched_priority, attr->sched_nice);
@@ -677,9 +666,38 @@ int __sched_setscheduler(struct task_struct *p,
prev_class = p->sched_class;
next_class = __setscheduler_class(policy, newprio);
+ /*
+ * SCHED_FLAG_KEEP_PARAMS prevents the policy and the class change
+ * from actually happening: it skips __setscheduler_params() and the
+ * sched_class assignment. Reject the contradictory requests that
+ * ask for either, rather than carry out half of what they ask.
+ *
+ * A class change with the policy kept is one of them: the class of a
+ * policy depends on the priority that priority inheritance leaves,
+ * and on whether a BPF scheduler is in charge.
+ */
+ if ((attr->sched_flags & SCHED_FLAG_KEEP_PARAMS) &&
+ (policy != p->policy || prev_class != next_class)) {
+ retval = -EINVAL;
+ goto unlock;
+ }
+
if (prev_class != next_class)
queue_flags |= DEQUEUE_CLASS;
+ /*
+ * If setscheduling to SCHED_DEADLINE (or changing the parameters
+ * of a SCHED_DEADLINE task) we need to check if enough bandwidth
+ * is available. Last of the checks: nothing gives back the
+ * bandwidth it reserves.
+ */
+ if ((dl_policy(policy) || dl_task(p)) && sched_dl_overflow(p, policy, attr)) {
+ retval = -EBUSY;
+ goto unlock;
+ }
+
+ p->sched_reset_on_fork = reset_on_fork;
+
scx_prepare_setscheduler(p, policy);
scoped_guard (sched_change, p, queue_flags) {
--
2.47.3