* Re: [PATCH v2] sched/proxy_exec: Break cyclic proxy chains by deactivating blocked tasks
2026-07-14 15:21 ` [PATCH v2] sched/proxy_exec: Break cyclic proxy chains by deactivating blocked tasks soolaugust
@ 2026-07-14 20:46 ` K Prateek Nayak
2026-07-15 2:37 ` soolaugust
2026-07-17 10:52 ` [PATCH v3] sched/proxy_exec: Detect cycles in proxy walks zhidao su (Xiaomi)
2 siblings, 0 replies; 17+ messages in thread
From: K Prateek Nayak @ 2026-07-14 20:46 UTC (permalink / raw)
To: soolaugust, John Stultz
Cc: Peter Zijlstra, Ingo Molnar, Juri Lelli, Vincent Guittot,
Dietmar Eggemann, Steven Rostedt, Ben Segall, Mel Gorman,
Valentin Schneider, linux-kernel
Hello Zhidao,
On 7/14/2026 8:51 PM, soolaugust@gmail.com wrote:
> diff --git a/kernel/sched/core.c b/kernel/sched/core.c
> index 2e7cde033a319..cff1a3b58660b 100644
> --- a/kernel/sched/core.c
> +++ b/kernel/sched/core.c
> @@ -6724,6 +6724,8 @@ static bool try_to_block_task(struct rq *rq, struct task_struct *p,
> }
>
> #ifdef CONFIG_SCHED_PROXY_EXEC
> +#define MAX_PROXY_CHAIN_DEPTH 1024
I'm not really a fan of the arbitrary MAX_PROXY_CHAIN_DEPTH.
> +
> static inline void proxy_set_task_cpu(struct task_struct *p, int cpu)
> {
> unsigned int wake_cpu;
> @@ -6873,6 +6875,7 @@ find_proxy_task(struct rq *rq, struct task_struct *donor, struct rq_flags *rf)
> int this_cpu = cpu_of(rq);
> struct task_struct *p;
> int owner_cpu;
> + int chain_depth = 0;
>
> /* Follow blocked_on chain. */
> for (p = donor; p->is_blocked; p = owner) {
> @@ -6905,6 +6908,13 @@ find_proxy_task(struct rq *rq, struct task_struct *donor, struct rq_flags *rf)
> return NULL;
> }
>
> + if (++chain_depth > MAX_PROXY_CHAIN_DEPTH) {
> + WARN_ONCE(1, "sched/pe: proxy chain depth exceeded %d, pid %d\n",
> + MAX_PROXY_CHAIN_DEPTH, p->pid);
> + __clear_task_blocked_on(p, NULL);
> + goto deactivate;
> + }
> +
> if (task_current(rq, p))
> curr_in_chain = true;
>
> @@ -6923,6 +6933,13 @@ find_proxy_task(struct rq *rq, struct task_struct *donor, struct rq_flags *rf)
> goto deactivate;
> }
>
> + if (owner == donor) {
So let us take a step back. Say you have a genuine case of:
A -> B -> C -> D
^ |
| |
+---------+
donor = A
Cycle is somewhere in-between the chain
Now, we have a "p->blocked_donor" back link that we can probably
leverage to detect this cycle except we don't clear a
p->blocked_donor link when find_proxy_task() returns idle or NULL
so we can have stale p->blocked_donor links for queued tasks but
they are so far harmless.
Me goes and thinks ...
Since p->blocked_donor isn't used for chain migration yet, does
something like below help your deadlock case?
(Prepared on top of tip:sched/core; Lightly tested)
diff --git a/kernel/sched/core.c b/kernel/sched/core.c
index 2e7cde033a31..fa1d9443f744 100644
--- a/kernel/sched/core.c
+++ b/kernel/sched/core.c
@@ -6874,6 +6874,9 @@ find_proxy_task(struct rq *rq, struct task_struct *donor, struct rq_flags *rf)
struct task_struct *p;
int owner_cpu;
+ /* Increment proxy_pick_seq such that last 4 bits are always set. */
+ rq->proxy_pick_seq += 0x10;
+
/* Follow blocked_on chain. */
for (p = donor; p->is_blocked; p = owner) {
/* if its PROXY_WAKING, do return migration or run if current */
@@ -6990,12 +6993,28 @@ find_proxy_task(struct rq *rq, struct task_struct *donor, struct rq_flags *rf)
*/
return proxy_resched_idle(rq);
}
+
+ /* Cyclic deadlock detection. */
+ if (((u64)owner->blocked_donor) & 0xF) {
+ u64 pick_seq = (u64)owner->blocked_donor;
+
+ /*
+ * Owner was already seen during this find_proxy_task() loop.
+ * Deactivate the task since there is a loop in proxy-chain.
+ */
+ if (rq->proxy_pick_seq == pick_seq) {
+ __clear_task_blocked_on(p, NULL);
+ goto deactivate;
+ }
+ }
+
/*
* OK, now we're absolutely sure @owner is on this
* rq, therefore holding @rq->lock is sufficient to
* guarantee its existence, as per ttwu_remote().
*/
owner->blocked_donor = p;
+ p->blocked_donor = (void *)rq->proxy_pick_seq;
}
WARN_ON_ONCE(owner && !owner->on_rq);
return owner;
@@ -9057,6 +9076,9 @@ void __init sched_init(void)
raw_spin_lock_init(&rq->cpu_epoch_lock);
rq->cpu_epoch_next = jiffies;
#endif
+#ifdef CONFIG_SCHED_PROXY_EXEC
+ rq->proxy_pick_seq = 0xf;
+#endif
zalloc_cpumask_var_node(&rq->scratch_mask, GFP_KERNEL, cpu_to_node(i));
}
diff --git a/kernel/sched/sched.h b/kernel/sched/sched.h
index 26ae13c86b69..452cbcd3a8c5 100644
--- a/kernel/sched/sched.h
+++ b/kernel/sched/sched.h
@@ -1154,6 +1154,7 @@ struct rq {
#ifdef CONFIG_SCHED_PROXY_EXEC
struct task_struct __rcu *donor; /* Scheduling context */
struct task_struct __rcu *curr; /* Execution context */
+ u64 proxy_pick_seq; /* find_proxy_task() seq */
#else
union {
struct task_struct __rcu *donor; /* Scheduler context */
---
base-commit: 04998aa54848f15332202d0bea008d2ca1ed1713
This assumes task_struct pointers are 4-bytes aligned.
Only rq->curr->blocked_donor matters currently so it should be okay
to clobber the rest of the chain. If this is okay, perhaps we can
just stash the pick_seq copy in task_struct.
Thoughts?
> + WARN_ONCE(1, "sched/pe: deadlock cycle detected, pid %d\n",
> + p->pid);
> + __clear_task_blocked_on(p, NULL);
> + goto deactivate;
> + }
> +
> if (!READ_ONCE(owner->on_rq) || owner->se.sched_delayed) {
> /* XXX Don't handle blocked owners/delayed dequeue yet */
> if (curr_in_chain)
--
Thanks and Regards,
Prateek
^ permalink raw reply [flat|nested] 17+ messages in thread* Re: [PATCH v2] sched/proxy_exec: Break cyclic proxy chains by deactivating blocked tasks
2026-07-14 15:21 ` [PATCH v2] sched/proxy_exec: Break cyclic proxy chains by deactivating blocked tasks soolaugust
2026-07-14 20:46 ` K Prateek Nayak
@ 2026-07-15 2:37 ` soolaugust
2026-07-15 3:01 ` K Prateek Nayak
2026-07-17 10:52 ` [PATCH v3] sched/proxy_exec: Detect cycles in proxy walks zhidao su (Xiaomi)
2 siblings, 1 reply; 17+ messages in thread
From: soolaugust @ 2026-07-15 2:37 UTC (permalink / raw)
To: K Prateek Nayak, John Stultz
Cc: Peter Zijlstra, Ingo Molnar, Juri Lelli, Vincent Guittot,
Dietmar Eggemann, Steven Rostedt, Ben Segall, Mel Gorman,
Valentin Schneider, linux-kernel
Thanks Prateek.
> I'm not really a fan of the arbitrary MAX_PROXY_CHAIN_DEPTH.
Agreed. It was only a guard rail, not a real fix. I'll drop it in v3
and switch to a proper cycle check.
> This only catches cycles that come back to the original donor.
Right. The owner == donor check only catches cycles that include the
original donor, so it misses loops in the middle of the chain, like
your A -> B -> C -> D -> B example.
> Maybe we can use the blocked_donor backlink as a visited marker?
The basic idea makes sense, but I'd rather not overload blocked_donor.
That field is already the donor-stack backlink, and keeping it as a
plain task pointer seems easier to reason about as the proxy-exec model
grows to more primitives.
So for v3 I'll likely use an explicit per-pick seq marker instead.
> We should probably think about other lock types too.
Yes, agreed. That's another reason to keep this at the proxy
wait-for-chain level rather than make it mutex-specific. The current
tree still only has mutex-backed blocked_on, but the design is meant to
cover other blocking primitives as well.
Thanks for the review. I'll rework v3 accordingly.
^ permalink raw reply [flat|nested] 17+ messages in thread
* Re: [PATCH v2] sched/proxy_exec: Break cyclic proxy chains by deactivating blocked tasks
2026-07-15 2:37 ` soolaugust
@ 2026-07-15 3:01 ` K Prateek Nayak
2026-07-15 8:50 ` zhidao su
0 siblings, 1 reply; 17+ messages in thread
From: K Prateek Nayak @ 2026-07-15 3:01 UTC (permalink / raw)
To: soolaugust, John Stultz
Cc: Peter Zijlstra, Ingo Molnar, Juri Lelli, Vincent Guittot,
Dietmar Eggemann, Steven Rostedt, Ben Segall, Mel Gorman,
Valentin Schneider, linux-kernel
Hello Zhidao,
On 7/15/2026 8:07 AM, soolaugust@gmail.com wrote:
> Thanks Prateek.
>
>> I'm not really a fan of the arbitrary MAX_PROXY_CHAIN_DEPTH.
>
> Agreed. It was only a guard rail, not a real fix. I'll drop it in v3
> and switch to a proper cycle check.
>
>> This only catches cycles that come back to the original donor.
>
> Right. The owner == donor check only catches cycles that include the
> original donor, so it misses loops in the middle of the chain, like
> your A -> B -> C -> D -> B example.
>
>> Maybe we can use the blocked_donor backlink as a visited marker?
>
> The basic idea makes sense, but I'd rather not overload blocked_donor.
> That field is already the donor-stack backlink, and keeping it as a
> plain task pointer seems easier to reason about as the proxy-exec model
> grows to more primitives.
>
> So for v3 I'll likely use an explicit per-pick seq marker instead.
Ack! Sample code was just a PoC to see if it is good enough to solve the
hard lockup problem :-)
Btw, I realized you'll need to reset this marker on migration, else
there is a rare chance of a stale p->blocked_donor pick_seq from an old
pick CPU colliding with the pick_seq after migration.
For my PoC, this was a good enough solution:
diff --git a/kernel/sched/sched.h b/kernel/sched/sched.h
index 452cbcd3a8c5..f5180913c038 100644
--- a/kernel/sched/sched.h
+++ b/kernel/sched/sched.h
@@ -2400,6 +2400,7 @@ static inline void __set_task_cpu(struct task_struct *p, unsigned int cpu)
smp_wmb();
WRITE_ONCE(task_thread_info(p)->cpu, cpu);
p->wake_cpu = cpu;
+ p->blocked_donor = NULL;
rseq_sched_set_ids_changed(p);
#endif /* CONFIG_SMP */
}
---
You can make this fit as per your v3 scheme.
>> We should probably think about other lock types too.
>
> Yes, agreed. That's another reason to keep this at the proxy
> wait-for-chain level rather than make it mutex-specific. The current
> tree still only has mutex-backed blocked_on, but the design is meant to
> cover other blocking primitives as well.
Ack! I think eventually, blocked_on becomes a struct with a lock_type
enum + void* pointer which allows for interpreting the pointer based
on the lock_type.
I agree p->blocked_donor has other uses with the extended series so it
is best to use a separate variable to track the seq_count if we go down
that route.
I'll let John comment if it is a good idea or not.
>
> Thanks for the review. I'll rework v3 accordingly.
Thanks a ton.
--
Thanks and Regards,
Prateek
^ permalink raw reply [flat|nested] 17+ messages in thread
* Re: [PATCH v2] sched/proxy_exec: Break cyclic proxy chains by deactivating blocked tasks
2026-07-15 3:01 ` K Prateek Nayak
@ 2026-07-15 8:50 ` zhidao su
0 siblings, 0 replies; 17+ messages in thread
From: zhidao su @ 2026-07-15 8:50 UTC (permalink / raw)
To: K Prateek Nayak, John Stultz
Cc: Peter Zijlstra, Ingo Molnar, Juri Lelli, Vincent Guittot,
Dietmar Eggemann, Steven Rostedt, Ben Segall, Mel Gorman,
Valentin Schneider, linux-kernel
> Btw, I realized you'll need to reset this marker on migration, else
> there is a rare chance of a stale p->blocked_donor pick_seq from an old
> pick CPU colliding with the pick_seq after migration.
Good catch. I think v3 should clear the marker when we migrate the task,
so the pick state is only meaningful for a single walk on a single CPU.
That avoids stale state from an old CPU colliding with a later walk.
> You can make this fit as per your v3 scheme.
I’m leaning toward keeping this proxy-local instead of putting it in a
generic CPU switch helper.
> Ack! I think eventually, blocked_on becomes a struct with a lock_type
> enum + void* pointer which allows for interpreting the pointer based
> on the lock_type.
Makes sense, and that’s another reason I’d keep the walk marker separate
from the blocked_on / blocked_donor representation itself.
> I agree p->blocked_donor has other uses with the extended series so it
> is best to use a separate variable to track the seq_count if we go down
> that route.
Right, that was my thinking as well.
^ permalink raw reply [flat|nested] 17+ messages in thread
* [PATCH v3] sched/proxy_exec: Detect cycles in proxy walks
2026-07-14 15:21 ` [PATCH v2] sched/proxy_exec: Break cyclic proxy chains by deactivating blocked tasks soolaugust
2026-07-14 20:46 ` K Prateek Nayak
2026-07-15 2:37 ` soolaugust
@ 2026-07-17 10:52 ` zhidao su (Xiaomi)
2026-07-17 11:50 ` K Prateek Nayak
2 siblings, 1 reply; 17+ messages in thread
From: zhidao su (Xiaomi) @ 2026-07-17 10:52 UTC (permalink / raw)
To: K Prateek Nayak, John Stultz
Cc: Peter Zijlstra, Ingo Molnar, Juri Lelli, Vincent Guittot,
Dietmar Eggemann, Steven Rostedt, Ben Segall, Mel Gorman,
Valentin Schneider, linux-kernel, zhidao su (Xiaomi)
find_proxy_task() can keep walking the same blocked_on chain if the chain
contains a cycle. This happens with a simple A->B->A mutex deadlock under
proxy execution, and can leave the CPU spinning in __schedule() with the rq
lock held.
Mark each task visited during one proxy walk with the rq's pick sequence.
If the walk sees the same task again in the same pick, break the cycle by
clearing the blocked_on state at the detection point and deactivating that
task.
Clear the marker when proxy execution migrates a blocked task, so a later
walk on another CPU cannot match stale state.
Tested with a PE cycle reproducer in virtme-ng:
buggy kernel: vng timed out without returning
fixed kernel: WARN_ONCE "sched/pe: deadlock cycle detected"
Signed-off-by: zhidao su (Xiaomi) <soolaugust@gmail.com>
---
Changes since v2:
- Drop the proxy-chain depth limit.
- Detect cycles anywhere in the proxy walk, not only cycles back to the
original donor.
- Use a separate per-pick marker instead of overloading blocked_donor.
- Clear the marker on proxy-exec blocked-task migration.
include/linux/sched.h | 4 ++++
kernel/fork.c | 4 ++++
kernel/sched/core.c | 34 ++++++++++++++++++++++++++++++++--
kernel/sched/sched.h | 1 +
4 files changed, 41 insertions(+), 2 deletions(-)
diff --git a/include/linux/sched.h b/include/linux/sched.h
index 968b18a7f4702..b83a4e775d736 100644
--- a/include/linux/sched.h
+++ b/include/linux/sched.h
@@ -1256,6 +1256,10 @@ struct task_struct {
* under preempt_disable().
*/
struct task_struct *blocked_donor;
+#ifdef CONFIG_SCHED_PROXY_EXEC
+ int proxy_pick_cpu;
+ u64 proxy_pick_seq; /* find_proxy_task() seq */
+#endif
#ifdef CONFIG_DETECT_HUNG_TASK_BLOCKER
/*
diff --git a/kernel/fork.c b/kernel/fork.c
index 13e38e89a1f30..2219cce602c62 100644
--- a/kernel/fork.c
+++ b/kernel/fork.c
@@ -2242,6 +2242,10 @@ __latent_entropy struct task_struct *copy_process(
p->blocked_on = NULL; /* not blocked yet */
p->blocked_donor = NULL; /* nobody is boosting p yet */
+#ifdef CONFIG_SCHED_PROXY_EXEC
+ p->proxy_pick_cpu = -1;
+ p->proxy_pick_seq = 0;
+#endif
#ifdef CONFIG_BCACHE
p->sequential_io = 0;
diff --git a/kernel/sched/core.c b/kernel/sched/core.c
index 2e7cde033a319..3db4e6c2b786a 100644
--- a/kernel/sched/core.c
+++ b/kernel/sched/core.c
@@ -6724,6 +6724,7 @@ static bool try_to_block_task(struct rq *rq, struct task_struct *p,
}
#ifdef CONFIG_SCHED_PROXY_EXEC
+
static inline void proxy_set_task_cpu(struct task_struct *p, int cpu)
{
unsigned int wake_cpu;
@@ -6830,6 +6831,10 @@ static void proxy_migrate_task(struct rq *rq, struct rq_flags *rf,
deactivate_task(rq, p, DEQUEUE_NOCLOCK);
proxy_set_task_cpu(p, target_cpu);
+#ifdef CONFIG_SCHED_PROXY_EXEC
+ WRITE_ONCE(p->proxy_pick_cpu, -1);
+ WRITE_ONCE(p->proxy_pick_seq, 0);
+#endif
proxy_release_rq_lock(rq, rf);
@@ -6839,14 +6844,14 @@ static void proxy_migrate_task(struct rq *rq, struct rq_flags *rf,
}
/*
- * Find runnable lock owner to proxy for mutex blocked donor
+ * Find runnable lock owner to proxy for a blocked donor
*
* Follow the blocked-on relation:
*
* ,-> task
* | | blocked-on
* | v
- * blocked_donor | mutex
+ * blocked_donor | blocking primitive
* | | owner
* | v
* `-- task
@@ -6874,6 +6879,10 @@ find_proxy_task(struct rq *rq, struct task_struct *donor, struct rq_flags *rf)
struct task_struct *p;
int owner_cpu;
+ rq->proxy_pick_seq++;
+ if (unlikely(!rq->proxy_pick_seq))
+ rq->proxy_pick_seq++;
+
/* Follow blocked_on chain. */
for (p = donor; p->is_blocked; p = owner) {
/* if its PROXY_WAKING, do return migration or run if current */
@@ -6905,6 +6914,16 @@ find_proxy_task(struct rq *rq, struct task_struct *donor, struct rq_flags *rf)
return NULL;
}
+ if (READ_ONCE(p->proxy_pick_cpu) == this_cpu &&
+ READ_ONCE(p->proxy_pick_seq) == rq->proxy_pick_seq) {
+ WARN_ONCE(1, "sched/pe: deadlock cycle detected, pid %d\n",
+ p->pid);
+ __clear_task_blocked_on(p, NULL);
+ goto deactivate;
+ }
+ WRITE_ONCE(p->proxy_pick_cpu, this_cpu);
+ WRITE_ONCE(p->proxy_pick_seq, rq->proxy_pick_seq);
+
if (task_current(rq, p))
curr_in_chain = true;
@@ -6990,6 +7009,14 @@ find_proxy_task(struct rq *rq, struct task_struct *donor, struct rq_flags *rf)
*/
return proxy_resched_idle(rq);
}
+
+ if (READ_ONCE(owner->proxy_pick_cpu) == this_cpu &&
+ READ_ONCE(owner->proxy_pick_seq) == rq->proxy_pick_seq) {
+ WARN_ONCE(1, "sched/pe: deadlock cycle detected, pid %d\n",
+ p->pid);
+ __clear_task_blocked_on(p, NULL);
+ goto deactivate;
+ }
/*
* OK, now we're absolutely sure @owner is on this
* rq, therefore holding @rq->lock is sufficient to
@@ -9057,6 +9084,9 @@ void __init sched_init(void)
raw_spin_lock_init(&rq->cpu_epoch_lock);
rq->cpu_epoch_next = jiffies;
#endif
+#ifdef CONFIG_SCHED_PROXY_EXEC
+ rq->proxy_pick_seq = 0;
+#endif
zalloc_cpumask_var_node(&rq->scratch_mask, GFP_KERNEL, cpu_to_node(i));
}
diff --git a/kernel/sched/sched.h b/kernel/sched/sched.h
index 26ae13c86b699..f63c024d160e8 100644
--- a/kernel/sched/sched.h
+++ b/kernel/sched/sched.h
@@ -1154,6 +1154,7 @@ struct rq {
#ifdef CONFIG_SCHED_PROXY_EXEC
struct task_struct __rcu *donor; /* Scheduling context */
struct task_struct __rcu *curr; /* Execution context */
+ u64 proxy_pick_seq; /* find_proxy_task() seq */
#else
union {
struct task_struct __rcu *donor; /* Scheduler context */
base-commit: 04998aa54848f15332202d0bea008d2ca1ed1713
--
2.43.0
^ permalink raw reply [flat|nested] 17+ messages in thread* Re: [PATCH v3] sched/proxy_exec: Detect cycles in proxy walks
2026-07-17 10:52 ` [PATCH v3] sched/proxy_exec: Detect cycles in proxy walks zhidao su (Xiaomi)
@ 2026-07-17 11:50 ` K Prateek Nayak
2026-07-17 12:55 ` [PATCH v4] " zhidao su (Xiaomi)
0 siblings, 1 reply; 17+ messages in thread
From: K Prateek Nayak @ 2026-07-17 11:50 UTC (permalink / raw)
To: zhidao su (Xiaomi), John Stultz
Cc: Peter Zijlstra, Ingo Molnar, Juri Lelli, Vincent Guittot,
Dietmar Eggemann, Steven Rostedt, Ben Segall, Mel Gorman,
Valentin Schneider, linux-kernel
Hello Zhidao,
On 7/17/2026 4:22 PM, zhidao su (Xiaomi) wrote:
> find_proxy_task() can keep walking the same blocked_on chain if the chain
> contains a cycle. This happens with a simple A->B->A mutex deadlock under
> proxy execution, and can leave the CPU spinning in __schedule() with the rq
> lock held.
>
> Mark each task visited during one proxy walk with the rq's pick sequence.
> If the walk sees the same task again in the same pick, break the cycle by
> clearing the blocked_on state at the detection point and deactivating that
> task.
>
> Clear the marker when proxy execution migrates a blocked task, so a later
> walk on another CPU cannot match stale state.
>
> Tested with a PE cycle reproducer in virtme-ng:
>
> buggy kernel: vng timed out without returning
> fixed kernel: WARN_ONCE "sched/pe: deadlock cycle detected"
>
> Signed-off-by: zhidao su (Xiaomi) <soolaugust@gmail.com>
> ---
> Changes since v2:
> - Drop the proxy-chain depth limit.
> - Detect cycles anywhere in the proxy walk, not only cycles back to the
> original donor.
> - Use a separate per-pick marker instead of overloading blocked_donor.
> - Clear the marker on proxy-exec blocked-task migration.
>
> include/linux/sched.h | 4 ++++
> kernel/fork.c | 4 ++++
> kernel/sched/core.c | 34 ++++++++++++++++++++++++++++++++--
> kernel/sched/sched.h | 1 +
> 4 files changed, 41 insertions(+), 2 deletions(-)
>
> diff --git a/include/linux/sched.h b/include/linux/sched.h
> index 968b18a7f4702..b83a4e775d736 100644
> --- a/include/linux/sched.h
> +++ b/include/linux/sched.h
> @@ -1256,6 +1256,10 @@ struct task_struct {
> * under preempt_disable().
> */
> struct task_struct *blocked_donor;
> +#ifdef CONFIG_SCHED_PROXY_EXEC
> + int proxy_pick_cpu;
This adds 8-bytes to task_struct for not much benefit.
I think resetting proxy_pick_seq on migration / enqueue is much
simpler than tracking 2 things. Not to mention you need to write to
"proxy_pick_cpu" at every iteration and compare it too.
> + u64 proxy_pick_seq; /* find_proxy_task() seq */
> +#endif
>
> #ifdef CONFIG_DETECT_HUNG_TASK_BLOCKER
> /*
> diff --git a/kernel/fork.c b/kernel/fork.c
> index 13e38e89a1f30..2219cce602c62 100644
> --- a/kernel/fork.c
> +++ b/kernel/fork.c
> @@ -2242,6 +2242,10 @@ __latent_entropy struct task_struct *copy_process(
>
> p->blocked_on = NULL; /* not blocked yet */
> p->blocked_donor = NULL; /* nobody is boosting p yet */
> +#ifdef CONFIG_SCHED_PROXY_EXEC
> + p->proxy_pick_cpu = -1;
> + p->proxy_pick_seq = 0;
It should be okay to inherit these values from parent. The task
still needs to be placed after the fork which grabs the rq_lock
and next pick would increment the seq number anyways.
> +#endif
>
> #ifdef CONFIG_BCACHE
> p->sequential_io = 0;
> diff --git a/kernel/sched/core.c b/kernel/sched/core.c
> index 2e7cde033a319..3db4e6c2b786a 100644
> --- a/kernel/sched/core.c
> +++ b/kernel/sched/core.c
> @@ -6724,6 +6724,7 @@ static bool try_to_block_task(struct rq *rq, struct task_struct *p,
> }
>
> #ifdef CONFIG_SCHED_PROXY_EXEC
> +
> static inline void proxy_set_task_cpu(struct task_struct *p, int cpu)
> {
> unsigned int wake_cpu;
> @@ -6830,6 +6831,10 @@ static void proxy_migrate_task(struct rq *rq, struct rq_flags *rf,
>
> deactivate_task(rq, p, DEQUEUE_NOCLOCK);
> proxy_set_task_cpu(p, target_cpu);
> +#ifdef CONFIG_SCHED_PROXY_EXEC
> + WRITE_ONCE(p->proxy_pick_cpu, -1);
> + WRITE_ONCE(p->proxy_pick_seq, 0);
> +#endif
>
> proxy_release_rq_lock(rq, rf);
>
> @@ -6839,14 +6844,14 @@ static void proxy_migrate_task(struct rq *rq, struct rq_flags *rf,
> }
>
> /*
> - * Find runnable lock owner to proxy for mutex blocked donor
> + * Find runnable lock owner to proxy for a blocked donor
> *
> * Follow the blocked-on relation:
> *
> * ,-> task
> * | | blocked-on
> * | v
> - * blocked_donor | mutex
> + * blocked_donor | blocking primitive
> * | | owner
> * | v
> * `-- task
> @@ -6874,6 +6879,10 @@ find_proxy_task(struct rq *rq, struct task_struct *donor, struct rq_flags *rf)
> struct task_struct *p;
> int owner_cpu;
>
> + rq->proxy_pick_seq++;
> + if (unlikely(!rq->proxy_pick_seq))
> + rq->proxy_pick_seq++;
Can't we simply initialize "rq->proxy_pick_seq" to 1 in sched_init()?
I don't think we need to worry about a u64 wraparound.
> +
> /* Follow blocked_on chain. */
> for (p = donor; p->is_blocked; p = owner) {
> /* if its PROXY_WAKING, do return migration or run if current */
> @@ -6905,6 +6914,16 @@ find_proxy_task(struct rq *rq, struct task_struct *donor, struct rq_flags *rf)
> return NULL;
> }
>
> + if (READ_ONCE(p->proxy_pick_cpu) == this_cpu &&
> + READ_ONCE(p->proxy_pick_seq) == rq->proxy_pick_seq) {
> + WARN_ONCE(1, "sched/pe: deadlock cycle detected, pid %d\n",
> + p->pid);
> + __clear_task_blocked_on(p, NULL);
> + goto deactivate;
> + }
> + WRITE_ONCE(p->proxy_pick_cpu, this_cpu);
> + WRITE_ONCE(p->proxy_pick_seq, rq->proxy_pick_seq);
> +
Why do we need READ_ONCE() / WRITE_ONCE()?
All the checks happen on the same CPU under rq_lock. There is no chance
of a concurrent update here. Any time we drop the lock, we do a re-pick
and that should be sufficient to prevent any collision.
> if (task_current(rq, p))
> curr_in_chain = true;
>
> @@ -6990,6 +7009,14 @@ find_proxy_task(struct rq *rq, struct task_struct *donor, struct rq_flags *rf)
> */
> return proxy_resched_idle(rq);
> }
> +
> + if (READ_ONCE(owner->proxy_pick_cpu) == this_cpu &&
> + READ_ONCE(owner->proxy_pick_seq) == rq->proxy_pick_seq) {
> + WARN_ONCE(1, "sched/pe: deadlock cycle detected, pid %d\n",
> + p->pid);
> + __clear_task_blocked_on(p, NULL);
> + goto deactivate;
> + }
> /*
> * OK, now we're absolutely sure @owner is on this
> * rq, therefore holding @rq->lock is sufficient to
> @@ -9057,6 +9084,9 @@ void __init sched_init(void)
> raw_spin_lock_init(&rq->cpu_epoch_lock);
> rq->cpu_epoch_next = jiffies;
> #endif
> +#ifdef CONFIG_SCHED_PROXY_EXEC
> + rq->proxy_pick_seq = 0;
> +#endif
>
> zalloc_cpumask_var_node(&rq->scratch_mask, GFP_KERNEL, cpu_to_node(i));
> }
> diff --git a/kernel/sched/sched.h b/kernel/sched/sched.h
> index 26ae13c86b699..f63c024d160e8 100644
> --- a/kernel/sched/sched.h
> +++ b/kernel/sched/sched.h
> @@ -1154,6 +1154,7 @@ struct rq {
> #ifdef CONFIG_SCHED_PROXY_EXEC
> struct task_struct __rcu *donor; /* Scheduling context */
> struct task_struct __rcu *curr; /* Execution context */
> + u64 proxy_pick_seq; /* find_proxy_task() seq */
> #else
> union {
> struct task_struct __rcu *donor; /* Scheduler context */
>
> base-commit: 04998aa54848f15332202d0bea008d2ca1ed1713
--
Thanks and Regards,
Prateek
^ permalink raw reply [flat|nested] 17+ messages in thread* [PATCH v4] sched/proxy_exec: Detect cycles in proxy walks
2026-07-17 11:50 ` K Prateek Nayak
@ 2026-07-17 12:55 ` zhidao su (Xiaomi)
2026-07-21 3:41 ` K Prateek Nayak
0 siblings, 1 reply; 17+ messages in thread
From: zhidao su (Xiaomi) @ 2026-07-17 12:55 UTC (permalink / raw)
To: K Prateek Nayak, John Stultz
Cc: Peter Zijlstra, Ingo Molnar, Juri Lelli, Vincent Guittot,
Dietmar Eggemann, Steven Rostedt, Ben Segall, Mel Gorman,
Valentin Schneider, linux-kernel, zhidao su (Xiaomi)
find_proxy_task() can keep walking the same blocked_on chain if the chain
contains a cycle. This happens with a simple A->B->A mutex deadlock under
proxy execution, and can leave the CPU spinning in __schedule() with the rq
lock held.
Use the rq pick sequence as a per-walk marker. Mark each task visited by
the current proxy walk. If the walk sees the same marker again, break the
cycle by clearing the blocked_on state at the detection point and
deactivating that task.
The marker is only consumed while holding the rq lock. Clear it when a task
is activated, so stale state from an earlier pick or another rq is not
carried into the next queued lifetime.
Tested with a PE cycle reproducer in virtme-ng:
buggy kernel: vng timed out without returning
fixed kernel: WARN_ONCE "sched/pe: deadlock cycle detected"
Signed-off-by: zhidao su (Xiaomi) <soolaugust@gmail.com>
---
Changes since v3:
- Use a single per-task proxy_pick_seq marker instead of storing both CPU
and sequence.
- Clear stale proxy-walk state from activate_task() rather than from the
proxy migration path only.
- Drop fork-time marker initialization.
- Drop READ_ONCE()/WRITE_ONCE() around marker accesses; the walk is under
rq lock.
- Drop the u64 wraparound guard.
include/linux/sched.h | 3 +++
kernel/sched/core.c | 28 ++++++++++++++++++++++++++--
kernel/sched/sched.h | 1 +
3 files changed, 30 insertions(+), 2 deletions(-)
diff --git a/include/linux/sched.h b/include/linux/sched.h
index 968b18a7f4702..b1c46e21bc268 100644
--- a/include/linux/sched.h
+++ b/include/linux/sched.h
@@ -1256,6 +1256,9 @@ struct task_struct {
* under preempt_disable().
*/
struct task_struct *blocked_donor;
+#ifdef CONFIG_SCHED_PROXY_EXEC
+ u64 proxy_pick_seq;
+#endif
#ifdef CONFIG_DETECT_HUNG_TASK_BLOCKER
/*
diff --git a/kernel/sched/core.c b/kernel/sched/core.c
index 2e7cde033a319..1a9d3decd66e0 100644
--- a/kernel/sched/core.c
+++ b/kernel/sched/core.c
@@ -2221,6 +2221,9 @@ void activate_task(struct rq *rq, struct task_struct *p, int flags)
if (task_on_rq_migrating(p))
flags |= ENQUEUE_MIGRATED;
+#ifdef CONFIG_SCHED_PROXY_EXEC
+ p->proxy_pick_seq = 0;
+#endif
enqueue_task(rq, p, flags);
WRITE_ONCE(p->on_rq, TASK_ON_RQ_QUEUED);
@@ -6724,6 +6727,7 @@ static bool try_to_block_task(struct rq *rq, struct task_struct *p,
}
#ifdef CONFIG_SCHED_PROXY_EXEC
+
static inline void proxy_set_task_cpu(struct task_struct *p, int cpu)
{
unsigned int wake_cpu;
@@ -6839,14 +6843,14 @@ static void proxy_migrate_task(struct rq *rq, struct rq_flags *rf,
}
/*
- * Find runnable lock owner to proxy for mutex blocked donor
+ * Find runnable lock owner to proxy for a blocked donor
*
* Follow the blocked-on relation:
*
* ,-> task
* | | blocked-on
* | v
- * blocked_donor | mutex
+ * blocked_donor | blocking primitive
* | | owner
* | v
* `-- task
@@ -6874,6 +6878,8 @@ find_proxy_task(struct rq *rq, struct task_struct *donor, struct rq_flags *rf)
struct task_struct *p;
int owner_cpu;
+ rq->proxy_pick_seq++;
+
/* Follow blocked_on chain. */
for (p = donor; p->is_blocked; p = owner) {
/* if its PROXY_WAKING, do return migration or run if current */
@@ -6905,6 +6911,14 @@ find_proxy_task(struct rq *rq, struct task_struct *donor, struct rq_flags *rf)
return NULL;
}
+ if (p->proxy_pick_seq == rq->proxy_pick_seq) {
+ WARN_ONCE(1, "sched/pe: deadlock cycle detected, pid %d\n",
+ p->pid);
+ __clear_task_blocked_on(p, NULL);
+ goto deactivate;
+ }
+ p->proxy_pick_seq = rq->proxy_pick_seq;
+
if (task_current(rq, p))
curr_in_chain = true;
@@ -6990,6 +7004,13 @@ find_proxy_task(struct rq *rq, struct task_struct *donor, struct rq_flags *rf)
*/
return proxy_resched_idle(rq);
}
+
+ if (owner->proxy_pick_seq == rq->proxy_pick_seq) {
+ WARN_ONCE(1, "sched/pe: deadlock cycle detected, pid %d\n",
+ p->pid);
+ __clear_task_blocked_on(p, NULL);
+ goto deactivate;
+ }
/*
* OK, now we're absolutely sure @owner is on this
* rq, therefore holding @rq->lock is sufficient to
@@ -9057,6 +9078,9 @@ void __init sched_init(void)
raw_spin_lock_init(&rq->cpu_epoch_lock);
rq->cpu_epoch_next = jiffies;
#endif
+#ifdef CONFIG_SCHED_PROXY_EXEC
+ rq->proxy_pick_seq = 1;
+#endif
zalloc_cpumask_var_node(&rq->scratch_mask, GFP_KERNEL, cpu_to_node(i));
}
diff --git a/kernel/sched/sched.h b/kernel/sched/sched.h
index 26ae13c86b699..26d8d2788e0cd 100644
--- a/kernel/sched/sched.h
+++ b/kernel/sched/sched.h
@@ -1154,6 +1154,7 @@ struct rq {
#ifdef CONFIG_SCHED_PROXY_EXEC
struct task_struct __rcu *donor; /* Scheduling context */
struct task_struct __rcu *curr; /* Execution context */
+ u64 proxy_pick_seq;
#else
union {
struct task_struct __rcu *donor; /* Scheduler context */
base-commit: 04998aa54848f15332202d0bea008d2ca1ed1713
--
2.43.0
^ permalink raw reply [flat|nested] 17+ messages in thread* Re: [PATCH v4] sched/proxy_exec: Detect cycles in proxy walks
2026-07-17 12:55 ` [PATCH v4] " zhidao su (Xiaomi)
@ 2026-07-21 3:41 ` K Prateek Nayak
2026-07-22 12:03 ` [PATCH v5] " zhidao su (Xiaomi)
0 siblings, 1 reply; 17+ messages in thread
From: K Prateek Nayak @ 2026-07-21 3:41 UTC (permalink / raw)
To: zhidao su (Xiaomi), John Stultz
Cc: Peter Zijlstra, Ingo Molnar, Juri Lelli, Vincent Guittot,
Dietmar Eggemann, Steven Rostedt, Ben Segall, Mel Gorman,
Valentin Schneider, linux-kernel
Hello Zhidao,
On 7/17/2026 6:25 PM, zhidao su (Xiaomi) wrote:
> diff --git a/kernel/sched/core.c b/kernel/sched/core.c
> index 2e7cde033a319..1a9d3decd66e0 100644
> --- a/kernel/sched/core.c
> +++ b/kernel/sched/core.c
> @@ -2221,6 +2221,9 @@ void activate_task(struct rq *rq, struct task_struct *p, int flags)
> if (task_on_rq_migrating(p))
> flags |= ENQUEUE_MIGRATED;
>
> +#ifdef CONFIG_SCHED_PROXY_EXEC
> + p->proxy_pick_seq = 0;
> +#endif
nit.
I think it would be better if we have a sched_proxy_enqueue_task()
wrapper around this to avoid sprinkling #ifdef in otherwise what is a
clean function.
Also you can bail out early for !sched_proxy_exec() to avoid this write
for users who disable proxy execution at runtime.
> enqueue_task(rq, p, flags);
>
> WRITE_ONCE(p->on_rq, TASK_ON_RQ_QUEUED);
> @@ -6724,6 +6727,7 @@ static bool try_to_block_task(struct rq *rq, struct task_struct *p,
> }
>
> #ifdef CONFIG_SCHED_PROXY_EXEC
> +
> static inline void proxy_set_task_cpu(struct task_struct *p, int cpu)
> {
> unsigned int wake_cpu;
> @@ -6839,14 +6843,14 @@ static void proxy_migrate_task(struct rq *rq, struct rq_flags *rf,
> }
>
> /*
> - * Find runnable lock owner to proxy for mutex blocked donor
> + * Find runnable lock owner to proxy for a blocked donor
> *
> * Follow the blocked-on relation:
> *
> * ,-> task
> * | | blocked-on
> * | v
> - * blocked_donor | mutex
> + * blocked_donor | blocking primitive
> * | | owner
> * | v
> * `-- task
> @@ -6874,6 +6878,8 @@ find_proxy_task(struct rq *rq, struct task_struct *donor, struct rq_flags *rf)
> struct task_struct *p;
> int owner_cpu;
>
> + rq->proxy_pick_seq++;
> +
> /* Follow blocked_on chain. */
> for (p = donor; p->is_blocked; p = owner) {
> /* if its PROXY_WAKING, do return migration or run if current */
> @@ -6905,6 +6911,14 @@ find_proxy_task(struct rq *rq, struct task_struct *donor, struct rq_flags *rf)
> return NULL;
> }
>
> + if (p->proxy_pick_seq == rq->proxy_pick_seq) {
> + WARN_ONCE(1, "sched/pe: deadlock cycle detected, pid %d\n",
You can use pr_warn_once().
> + p->pid);
> + __clear_task_blocked_on(p, NULL);
> + goto deactivate;
> + }
> + p->proxy_pick_seq = rq->proxy_pick_seq;
> +
Do we need to do this here too? ...
> if (task_current(rq, p))
> curr_in_chain = true;
>
> @@ -6990,6 +7004,13 @@ find_proxy_task(struct rq *rq, struct task_struct *donor, struct rq_flags *rf)
> */
> return proxy_resched_idle(rq);
> }
> +
> + if (owner->proxy_pick_seq == rq->proxy_pick_seq) {
> + WARN_ONCE(1, "sched/pe: deadlock cycle detected, pid %d\n",
> + p->pid);
> + __clear_task_blocked_on(p, NULL);
> + goto deactivate;
> + }
... This should be sufficient right? You can just set
owner->proxy_pick_seq = rq->proxy_pick_seq;
below once owner is finalized instead of setting "p->proxy_pick_seq"
very early above.
> /*
> * OK, now we're absolutely sure @owner is on this
> * rq, therefore holding @rq->lock is sufficient to
Apart from those, the patch looks good. With the comments addressed,
feel free to include:
Reviewed-by: K Prateek Nayak <kprateek.nayak@amd.com>
--
Thanks and Regards,
Prateek
^ permalink raw reply [flat|nested] 17+ messages in thread* [PATCH v5] sched/proxy_exec: Detect cycles in proxy walks
2026-07-21 3:41 ` K Prateek Nayak
@ 2026-07-22 12:03 ` zhidao su (Xiaomi)
2026-08-12 21:29 ` John Stultz
0 siblings, 1 reply; 17+ messages in thread
From: zhidao su (Xiaomi) @ 2026-07-22 12:03 UTC (permalink / raw)
To: K Prateek Nayak, John Stultz
Cc: Peter Zijlstra, Ingo Molnar, Juri Lelli, Vincent Guittot,
Dietmar Eggemann, Steven Rostedt, Ben Segall, Mel Gorman,
Valentin Schneider, linux-kernel, zhidao su (Xiaomi)
find_proxy_task() can keep walking the same blocked_on chain if the chain
contains a cycle. A simple A->B->A deadlock can leave the CPU spinning in
__schedule() with rq->lock held.
Use the rq pick sequence as a per-walk marker. Mark each task visited by
the current walk. If the walk sees the same marker again, break the cycle
by clearing blocked_on at the detection point and deactivating that task.
The marker is only consumed while holding rq->lock. Clear it when a task is
activated, so stale state from an earlier pick or another rq is not carried
into the next queued lifetime.
Tested with a PE cycle reproducer in virtme-ng:
buggy kernel: vng timed out without returning
fixed kernel: WARN_ONCE "sched/pe: deadlock cycle detected"
Reviewed-by: K Prateek Nayak <kprateek.nayak@amd.com>
Signed-off-by: zhidao su (Xiaomi) <soolaugust@gmail.com>
---
Changes since v4:
- Use sched_proxy_enqueue_task() to clear stale proxy-walk state when a
task is activated.
- Stop marking the donor early; only mark the finalized owner.
- Use pr_warn_once() for the cycle warning.
include/linux/sched.h | 3 +++
kernel/sched/core.c | 19 +++++++++++++++++--
kernel/sched/sched.h | 11 +++++++++++
3 files changed, 31 insertions(+), 2 deletions(-)
diff --git a/include/linux/sched.h b/include/linux/sched.h
index 968b18a7f4702..b1c46e21bc268 100644
--- a/include/linux/sched.h
+++ b/include/linux/sched.h
@@ -1256,6 +1256,9 @@ struct task_struct {
* under preempt_disable().
*/
struct task_struct *blocked_donor;
+#ifdef CONFIG_SCHED_PROXY_EXEC
+ u64 proxy_pick_seq;
+#endif
#ifdef CONFIG_DETECT_HUNG_TASK_BLOCKER
/*
diff --git a/kernel/sched/core.c b/kernel/sched/core.c
index 2e7cde033a319..c1bb4f98dd64a 100644
--- a/kernel/sched/core.c
+++ b/kernel/sched/core.c
@@ -2221,6 +2221,7 @@ void activate_task(struct rq *rq, struct task_struct *p, int flags)
if (task_on_rq_migrating(p))
flags |= ENQUEUE_MIGRATED;
+ sched_proxy_enqueue_task(p);
enqueue_task(rq, p, flags);
WRITE_ONCE(p->on_rq, TASK_ON_RQ_QUEUED);
@@ -6724,6 +6725,7 @@ static bool try_to_block_task(struct rq *rq, struct task_struct *p,
}
#ifdef CONFIG_SCHED_PROXY_EXEC
+
static inline void proxy_set_task_cpu(struct task_struct *p, int cpu)
{
unsigned int wake_cpu;
@@ -6839,14 +6841,14 @@ static void proxy_migrate_task(struct rq *rq, struct rq_flags *rf,
}
/*
- * Find runnable lock owner to proxy for mutex blocked donor
+ * Find runnable lock owner to proxy for a blocked donor
*
* Follow the blocked-on relation:
*
* ,-> task
* | | blocked-on
* | v
- * blocked_donor | mutex
+ * blocked_donor | blocking primitive
* | | owner
* | v
* `-- task
@@ -6874,6 +6876,8 @@ find_proxy_task(struct rq *rq, struct task_struct *donor, struct rq_flags *rf)
struct task_struct *p;
int owner_cpu;
+ rq->proxy_pick_seq++;
+
/* Follow blocked_on chain. */
for (p = donor; p->is_blocked; p = owner) {
/* if its PROXY_WAKING, do return migration or run if current */
@@ -6990,6 +6994,14 @@ find_proxy_task(struct rq *rq, struct task_struct *donor, struct rq_flags *rf)
*/
return proxy_resched_idle(rq);
}
+
+ if (owner->proxy_pick_seq == rq->proxy_pick_seq) {
+ pr_warn_once("sched/pe: deadlock cycle detected, pid %d\n",
+ p->pid);
+ __clear_task_blocked_on(p, NULL);
+ goto deactivate;
+ }
+ owner->proxy_pick_seq = rq->proxy_pick_seq;
/*
* OK, now we're absolutely sure @owner is on this
* rq, therefore holding @rq->lock is sufficient to
@@ -9057,6 +9069,9 @@ void __init sched_init(void)
raw_spin_lock_init(&rq->cpu_epoch_lock);
rq->cpu_epoch_next = jiffies;
#endif
+#ifdef CONFIG_SCHED_PROXY_EXEC
+ rq->proxy_pick_seq = 1;
+#endif
zalloc_cpumask_var_node(&rq->scratch_mask, GFP_KERNEL, cpu_to_node(i));
}
diff --git a/kernel/sched/sched.h b/kernel/sched/sched.h
index 26ae13c86b699..d5d9de509dfdb 100644
--- a/kernel/sched/sched.h
+++ b/kernel/sched/sched.h
@@ -1154,6 +1154,7 @@ struct rq {
#ifdef CONFIG_SCHED_PROXY_EXEC
struct task_struct __rcu *donor; /* Scheduling context */
struct task_struct __rcu *curr; /* Execution context */
+ u64 proxy_pick_seq;
#else
union {
struct task_struct __rcu *donor; /* Scheduler context */
@@ -3092,6 +3093,16 @@ static inline void __block_task(struct rq *rq, struct task_struct *p)
extern void activate_task(struct rq *rq, struct task_struct *p, int flags);
extern void deactivate_task(struct rq *rq, struct task_struct *p, int flags);
+static inline void sched_proxy_enqueue_task(struct task_struct *p)
+{
+#ifdef CONFIG_SCHED_PROXY_EXEC
+ if (!sched_proxy_exec())
+ return;
+
+ p->proxy_pick_seq = 0;
+#endif
+}
+
extern void wakeup_preempt(struct rq *rq, struct task_struct *p, int flags);
/*
base-commit: 04998aa54848f15332202d0bea008d2ca1ed1713
--
2.43.0
^ permalink raw reply [flat|nested] 17+ messages in thread* Re: [PATCH v5] sched/proxy_exec: Detect cycles in proxy walks
2026-07-22 12:03 ` [PATCH v5] " zhidao su (Xiaomi)
@ 2026-08-12 21:29 ` John Stultz
2026-08-13 4:09 ` zhidao su
0 siblings, 1 reply; 17+ messages in thread
From: John Stultz @ 2026-08-12 21:29 UTC (permalink / raw)
To: zhidao su (Xiaomi)
Cc: K Prateek Nayak, Peter Zijlstra, Ingo Molnar, Juri Lelli,
Vincent Guittot, Dietmar Eggemann, Steven Rostedt, Ben Segall,
Mel Gorman, Valentin Schneider, linux-kernel, Suleiman Souhlal
On Wed, Jul 22, 2026 at 5:03 AM zhidao su (Xiaomi) <soolaugust@gmail.com> wrote:
>
> find_proxy_task() can keep walking the same blocked_on chain if the chain
> contains a cycle. A simple A->B->A deadlock can leave the CPU spinning in
> __schedule() with rq->lock held.
>
> Use the rq pick sequence as a per-walk marker. Mark each task visited by
> the current walk. If the walk sees the same marker again, break the cycle
> by clearing blocked_on at the detection point and deactivating that task.
>
> The marker is only consumed while holding rq->lock. Clear it when a task is
> activated, so stale state from an earlier pick or another rq is not carried
> into the next queued lifetime.
>
> Tested with a PE cycle reproducer in virtme-ng:
>
> buggy kernel: vng timed out without returning
> fixed kernel: WARN_ONCE "sched/pe: deadlock cycle detected"
>
> Reviewed-by: K Prateek Nayak <kprateek.nayak@amd.com>
> Signed-off-by: zhidao su (Xiaomi) <soolaugust@gmail.com>
Hey! Zhidao,
Sorry for not having responded on this for awhile. In part its
because I'm still maybe partial to the simplicity of the
MAX_PROXY_CHAIN_DEPTH appraoch (no structure additions to task or rq
needed!).
But I also trust K Prateek's taste more then my own, so I've sort of
backed off to see what takes shape here.
I've not seen the cycle detection as super urgent, as mutex lock
cycles are not currently allowable in-kernel, and while the behavior
in this problematic case does shift with proxy (system hang vs task
stalls), it didn't seem like this was an actual problem except for
problematic out of tree drivers. That said, now that Suleiman has a
first draft for proxy-enabled futexes, this definitely raises the
urgency, as userland would be controlling the locking, so cycle
detection would be critical.
So I'm definitely eager to see this move forward. Though I still
think a simple (++count > MAX_PROXY_CHAIN_DEPTH) check would be
simpler and just as effective.
One small thought below..
> @@ -6990,6 +6994,14 @@ find_proxy_task(struct rq *rq, struct task_struct *donor, struct rq_flags *rf)
> */
> return proxy_resched_idle(rq);
> }
> +
> + if (owner->proxy_pick_seq == rq->proxy_pick_seq) {
> + pr_warn_once("sched/pe: deadlock cycle detected, pid %d\n",
> + p->pid);
> + __clear_task_blocked_on(p, NULL);
> + goto deactivate;
> + }
So, the only thought I have here is that the deactivate here will
deactivate p, not the donor. This will cause the next cycle to follow
the same chain path, until it hits the deactivated task and then it
will call proxy_enqueue_on_owner() and pick_again. The downside is
since you're always acting effectively on a leaf (sort of an
inacturate term in the loop case), you have to basically
deactivate/enqueue_on_owner the whole chain before we migth pick
something runnable.
If instead the donor was first to be deactivated (working root to
leaf), the next pick_again cycle could potentially pick a task out of
the chain and run it.
This is maybe a little moot, since once you detect and deactivate a
task *in-the-loop*, the proxy_enqueue_on_owner() code will have to
work leaf->root. But if the loop is at the end of a chain it might
help allow non-chain tasks to be picked a little faster.
Bit of a tangent, but this does make me wonder if we should eventually
try to do the proxy_enqueue_on_owner() back down the entire chain
following the blocked_donor (similar to what we do for
proxy_migrate_task) to try to reduce the amount of pick_again work we
do.
thanks
-john
^ permalink raw reply [flat|nested] 17+ messages in thread* Re: [PATCH v5] sched/proxy_exec: Detect cycles in proxy walks
2026-08-12 21:29 ` John Stultz
@ 2026-08-13 4:09 ` zhidao su
0 siblings, 0 replies; 17+ messages in thread
From: zhidao su @ 2026-08-13 4:09 UTC (permalink / raw)
To: John Stultz
Cc: K Prateek Nayak, Peter Zijlstra, Ingo Molnar, Juri Lelli,
Vincent Guittot, Dietmar Eggemann, Steven Rostedt, Ben Segall,
Mel Gorman, Valentin Schneider, linux-kernel, Suleiman Souhlal
Hey John,
Thanks for the reply, and agreed that the proxy futex work makes this
urgent rather than a theoretical edge case.
I tested the "deactivate the donor" idea on v31 (7.2-rc4, with the
sleeping_owner / blocked_on_lock rework), using a kernel-mutex reproducer
with a tail+cycle: D -> A -> B -> C -> A, where D waits on A but is not
in the cycle.
deactivate p (v5): 1 detection, stable
deactivate owner: 1 detection, stable
deactivate donor: unstable - 4 runs gave 1 / hang / 1 / 2
When the picked donor is D (outside the cycle), deactivating it leaves
the A->B->C->A cycle intact, so the next pick_again re-walks it and can
hang again. At detection time p and owner are always cycle members (the
check only fires once owner is already marked); the donor is only
guaranteed to be blocked.
So I'd rather keep deactivating the detection point, or switch to owner
if that reads better, but not the donor.
You're right that after one node is deactivated, the remaining blocked
tasks still get enqueued one at a time (N-1 for an N-node cycle). If
futex makes long cycles common, doing the whole blocked_donor chain in
one pass like proxy_migrate_task() would be the real win - I can
prototype that as a follow-up.
-zhidao
^ permalink raw reply [flat|nested] 17+ messages in thread