[PATCH v3] sched/proxy_exec: Detect cycles in proxy walks

zhidao su (Xiaomi) posted 1 patch 1 week ago
There is a newer version of this series
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(-)
[PATCH v3] sched/proxy_exec: Detect cycles in proxy walks
Posted by zhidao su (Xiaomi) 1 week ago
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
Re: [PATCH v3] sched/proxy_exec: Detect cycles in proxy walks
Posted by K Prateek Nayak 1 week ago
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
[PATCH v4] sched/proxy_exec: Detect cycles in proxy walks
Posted by zhidao su (Xiaomi) 1 week ago
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
Re: [PATCH v4] sched/proxy_exec: Detect cycles in proxy walks
Posted by K Prateek Nayak 4 days, 1 hour ago
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
[PATCH v5] sched/proxy_exec: Detect cycles in proxy walks
Posted by zhidao su (Xiaomi) 2 days, 16 hours ago
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