Skip to contentSkip to conversation

livepatch,sched: Add livepatch task switching to cond_resched()

PATCH Superseded Josh Poimboeuf 4 participants 12 messages Feb 9, 2023 – Feb 17, 2023

Subsystems (3)

v1 of 3 Superseded Latest is v3 →

lore
Share & tools
lore

Josh Poimboeuf

author maintainer
#0 lore raw

cover livepatch,sched: Add livepatch task switching to cond_resched()

Fix patching stalls caused by busy kthreads.

Josh Poimboeuf (3):
livepatch: Skip task_call_func() for current task livepatch,sched: Add livepatch task switching to cond_resched()
vhost: Fix livepatch timeouts in vhost_worker()

 drivers/vhost/vhost.c           |  3 +-
 include/linux/livepatch.h       |  1 +
 include/linux/livepatch_sched.h | 29 +++++++++++++++
 include/linux/sched.h           | 20 ++++++++---
 kernel/livepatch/transition.c   | 60 ++++++++++++++++++++++++++++++-
 kernel/sched/core.c             | 64 ++++++++++++++++++++++++++++-----
 6 files changed, 161 insertions(+), 16 deletions(-)
 create mode 100644 include/linux/livepatch_sched.h
Signature · 1 line
2.39.0
author maintainer
#1 lore raw

PATCH 1/3 livepatch: Skip task_call_func() for current task

The current task doesn't need the scheduler's protection to unwind its own stack.

---
 kernel/livepatch/transition.c | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)
kernel/livepatch/transition.c +5 -1
index f1b25ec581e0..4d1f443778f7 100644
@@ -307,7 +307,11 @@ static bool klp_try_switch_task(struct task_struct *task)
 	 * functions.  If all goes well, switch the task to the target patch
 	 * state.
 	 */
-	ret = task_call_func(task, klp_check_and_switch_task, &old_name);
+	if (task == current)
+		ret = klp_check_and_switch_task(current, &old_name);
+	else
+		ret = task_call_func(task, klp_check_and_switch_task, &old_name);
+
 	switch (ret) {
 	case 0:		/* success */
 		break;
Signature · 1 line
2.39.0
author maintainer
#3 lore raw

PATCH 2/3 livepatch,sched: Add livepatch task switching to cond_resched()

In reply to Josh Poimboeuf

There have been reports [1][2] of live patches failing to complete within a reasonable amount of time due to CPU-bound kthreads.

Fix it by patching tasks in cond_resched().

There are four different flavors of cond_resched(), depending on the kernel configuration. Hook into all of them.

A more elegant solution might be to use a preempt notifier. However, non-ORC unwinders can't unwind a preempted task reliably.

[1] https://lore.kernel.org/lkml/[email protected]/
[2] https://lkml.kernel.org/lkml/[email protected]

---
 include/linux/livepatch.h       |  1 +
 include/linux/livepatch_sched.h | 29 +++++++++++++++
 include/linux/sched.h           | 20 ++++++++---
 kernel/livepatch/transition.c   | 54 ++++++++++++++++++++++++++++
 kernel/sched/core.c             | 64 ++++++++++++++++++++++++++++-----
 5 files changed, 155 insertions(+), 13 deletions(-)
 create mode 100644 include/linux/livepatch_sched.h
include/linux/livepatch.h +1 -0
index 293e29960c6e..9b9b38e89563 100644
@@ -13,6 +13,7 @@
 #include <linux/ftrace.h>
 #include <linux/completion.h>
 #include <linux/list.h>
+#include <linux/livepatch_sched.h>
 
 #if IS_ENABLED(CONFIG_LIVEPATCH)
 
include/linux/livepatch_sched.h new file +29 -0
new file mode 100644
index 000000000000..013794fb5da0
@@ -0,0 +1,29 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+#ifndef _LINUX_LIVEPATCH_SCHED_H_
+#define _LINUX_LIVEPATCH_SCHED_H_
+
+#include <linux/jump_label.h>
+#include <linux/static_call_types.h>
+
+#ifdef CONFIG_LIVEPATCH
+
+void __klp_sched_try_switch(void);
+
+#if !defined(CONFIG_PREEMPT_DYNAMIC) || !defined(CONFIG_HAVE_PREEMPT_DYNAMIC_CALL)
+
+DECLARE_STATIC_KEY_FALSE(klp_sched_try_switch_key);
+
+static __always_inline void klp_sched_try_switch(void)
+{
+	if (static_branch_unlikely(&klp_sched_try_switch_key))
+		__klp_sched_try_switch();
+}
+
+#endif /* !CONFIG_PREEMPT_DYNAMIC || !CONFIG_HAVE_PREEMPT_DYNAMIC_CALL */
+
+#else /* !CONFIG_LIVEPATCH */
+static inline void klp_sched_try_switch(void) {}
+static inline void __klp_sched_try_switch(void) {}
+#endif /* CONFIG_LIVEPATCH */
+
+#endif /* _LINUX_LIVEPATCH_SCHED_H_ */
include/linux/sched.h +15 -5
index 4df2b3e76b30..2bc5e925a6d8 100644
@@ -36,6 +36,7 @@
 #include <linux/seqlock.h>
 #include <linux/kcsan.h>
 #include <linux/rv.h>
+#include <linux/livepatch_sched.h>
 #include <asm/kmap_size.h>
 
 /* task_struct member predeclarations (sorted alphabetically): */
@@ -2070,6 +2071,9 @@ extern int __cond_resched(void);
 
 #if defined(CONFIG_PREEMPT_DYNAMIC) && defined(CONFIG_HAVE_PREEMPT_DYNAMIC_CALL)
 
+void sched_dynamic_klp_enable(void);
+void sched_dynamic_klp_disable(void);
+
 DECLARE_STATIC_CALL(cond_resched, __cond_resched);
 
 static __always_inline int _cond_resched(void)
@@ -2078,6 +2082,7 @@ static __always_inline int _cond_resched(void)
 }
 
 #elif defined(CONFIG_PREEMPT_DYNAMIC) && defined(CONFIG_HAVE_PREEMPT_DYNAMIC_KEY)
+
 extern int dynamic_cond_resched(void);
 
 static __always_inline int _cond_resched(void)
@@ -2085,20 +2090,25 @@ static __always_inline int _cond_resched(void)
 	return dynamic_cond_resched();
 }
 
-#else
+#else /* !CONFIG_PREEMPTION */
 
 static inline int _cond_resched(void)
 {
+	klp_sched_try_switch();
 	return __cond_resched();
 }
 
-#endif /* CONFIG_PREEMPT_DYNAMIC */
+#endif /* PREEMPT_DYNAMIC && CONFIG_HAVE_PREEMPT_DYNAMIC_CALL */
 
-#else
+#else /* CONFIG_PREEMPTION && !CONFIG_PREEMPT_DYNAMIC */
 
-static inline int _cond_resched(void) { return 0; }
+static inline int _cond_resched(void)
+{
+	klp_sched_try_switch();
+	return 0;
+}
 
-#endif /* !defined(CONFIG_PREEMPTION) || defined(CONFIG_PREEMPT_DYNAMIC) */
+#endif /* !CONFIG_PREEMPTION || CONFIG_PREEMPT_DYNAMIC */
 
 #define cond_resched() ({			\
 	__might_resched(__FILE__, __LINE__, 0);	\
kernel/livepatch/transition.c +54 -0

84 lines not on this page · raw message

kernel/sched/core.c +56 -8

111 lines not on this page · raw message

Signature · 1 line
2.39.0
author maintainer
#7 lore raw

PATCH 3/3 vhost: Fix livepatch timeouts in vhost_worker()

In reply to Josh Poimboeuf

Livepatch timeouts were reported due to busy vhost_worker() kthreads. Now that cond_resched() can do livepatch task switching, use cond_resched() in vhost_worker(). That's the better way to conditionally call schedule() anyway.

---
 drivers/vhost/vhost.c | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)
drivers/vhost/vhost.c +1 -2
index cbe72bfd2f1f..424c0c939f57 100644
@@ -363,8 +363,7 @@ static int vhost_worker(void *data)
 			kcov_remote_start_common(dev->kcov_handle);
 			work->fn(work);
 			kcov_remote_stop();
-			if (need_resched())
-				schedule();
+			cond_resched();
 		}
 	}
 	kthread_unuse_mm(dev->mm);
Signature · 1 line
2.39.0
Tested-by on the v1 series
#9 lore raw
In reply to Josh Poimboeuf
Quoted 6 lines · Josh Poimboeuf

Fix patching stalls caused by busy kthreads.

Josh Poimboeuf (3):
livepatch: Skip task_call_func() for current task livepatch,sched: Add livepatch task switching to cond_resched()
vhost: Fix livepatch timeouts in vhost_worker()

This is working perfectly in my testing. The busy vhost threads get switched immediately. Thanks for working up these patches!

Thanks,
Seth

maintainer
#10 lore raw
In reply to Josh Poimboeuf
Quoted 6 lines · Josh Poimboeuf

Fix patching stalls caused by busy kthreads.

Josh Poimboeuf (3):
livepatch: Skip task_call_func() for current task livepatch,sched: Add livepatch task switching to cond_resched()
vhost: Fix livepatch timeouts in vhost_worker()

Seems reasonable, you want me to take them through the sched tree?

author maintainer
#11 lore raw
Quoted 9 lines · Peter Zijlstra
Quoted 6 lines · Josh Poimboeuf

Fix patching stalls caused by busy kthreads.

Josh Poimboeuf (3):
livepatch: Skip task_call_func() for current task livepatch,sched: Add livepatch task switching to cond_resched()
vhost: Fix livepatch timeouts in vhost_worker()

Seems reasonable, you want me to take them through the sched tree?

That would be great, though first I'd like to get an ack from at least another livepatch maintainer.

Signature · 1 line
Josh
maintainer
#4 lore raw
In reply to Josh Poimboeuf
Quoted 14 lines · Josh Poimboeuf

There have been reports [1][2] of live patches failing to complete within a reasonable amount of time due to CPU-bound kthreads.

Fix it by patching tasks in cond_resched().

There are four different flavors of cond_resched(), depending on the kernel configuration. Hook into all of them.

A more elegant solution might be to use a preempt notifier. However, non-ORC unwinders can't unwind a preempted task reliably.

include/linux/sched.h static __always_inline int _cond_resched(void) · line 2098

Quoted 10 lines
include/linux/sched.h +2 -1
@@ -2085,20 +2090,25 @@ static __always_inline int _cond_resched(void)
 	return dynamic_cond_resched();
 }
 
-#else
+#else /* !CONFIG_PREEMPTION */
 
 static inline int _cond_resched(void)
 {
+	klp_sched_try_switch();
 	return __cond_resched();

My only concern is if it might cause any performance problems.

On one hand, cond_resched() is used in code paths that are slow on its own. Also it will do nothing most of the time.

On the other hand, cond_resched() is typically used in cycles. One cycle might be fast. The code might be slow because there are too many cycles. Repeating the same failing test might prolong the time significantly.

An idea is to try the switch only when it was not done during a real schedule. Something like:

static inline int _cond_resched(void)

{
	int scheduled;

	scheduled = __cond_resched();
	if (scheduled)
		klp_sched_try_switch();

	return scheduled();
}

But it would make it less reliable/predictable. Also it won't work in configurations when cond_resched() is always a nop.

I am probably too careful. We might keep it simple until any real life problems are reported.

kernel/livepatch/transition.c static void klp_complete_transition(void) · line 100

Quoted 8 lines
kernel/livepatch/transition.c +2 -0
@@ -76,6 +96,8 @@ static void klp_complete_transition(void)
 		 klp_transition_patch->mod->name,
 		 klp_target_state == KLP_PATCHED ? "patching" : "unpatching");
 
+	klp_cond_resched_disable();
+

Nit: Strictly speaking, this is not needed when klp_complete_transition()
is called from klp_cancel_transition(). In this case, klp_cond_resched_enable() was not called. So it might be moved into klp_try_complete_transition().

More important thing, thinking loudly:

We need to make sure that no task is in the middle klp_cond_resched_disable() when we modify anything that is used there.

We seem to be on the safe side in klp_complete_transition(). We are here only when all tasks have TIF_PATCH_PENDING cleared. In this case, __klp_sched_try_switch() just returns. Also it calls klp_synchronize_transition() so that all tasks finish the critical part in __klp_sched_try_switch() before any new transition starts.

But it is not the case in klp_reverse_transition(). It modifies klp_target_state() when __klp_sched_try_switch might be in the middle of klp_check_stack() and it might give wrong result.

klp_reverse_transition() already solves similar race with klp_update_patch_state() by clearing all TIF_PATCH_PENDING flags and calling klp_synchronize_transition(). We just need to do it earlier. Something like:

kernel/livepatch/transition.c +10 -6
@@ -642,14 +642,11 @@ void klp_reverse_transition(void)
 		 klp_target_state == KLP_PATCHED ? "patching to unpatching" :
 						   "unpatching to patching");
 
-	klp_transition_patch->enabled = !klp_transition_patch->enabled;
-
-	klp_target_state = !klp_target_state;
 
 	/*
 	 * Clear all TIF_PATCH_PENDING flags to prevent races caused by
-	 * klp_update_patch_state() running in parallel with
-	 * klp_start_transition().
+	 * klp_update_patch_state() and __klp_sched_try_switch()
+	 * running in parallel.
 	 */
 	read_lock(&tasklist_lock);
 	for_each_process_thread(g, task)
@@ -659,9 +656,16 @@ void klp_reverse_transition(void)
 	for_each_possible_cpu(cpu)
 		clear_tsk_thread_flag(idle_task(cpu), TIF_PATCH_PENDING);
 
-	/* Let any remaining calls to klp_update_patch_state() complete */
+	/*
+	 * Make sure that both klp_update_patch_state() and
+	 * __klp_sched_try_switch() see the TIF_PATCH_PENDING cleared.
+	 */
 	klp_synchronize_transition();
 
+	klp_transition_patch->enabled = !klp_transition_patch->enabled;
+
+	klp_target_state = !klp_target_state;
+
 	klp_start_transition();
 }
 

kernel/livepatch/transition.c static void klp_complete_transition(void) · line 103

Quoted 3 lines
kernel/livepatch/transition.c +0 -0
 	if (klp_transition_patch->replace && klp_target_state == KLP_PATCHED) {
 		klp_unpatch_replaced_patches(klp_transition_patch);
 		klp_discard_nops(klp_transition_patch);

Otherwise, the patch looks fine. I looked at it primary from the livepatching code side.

Best Regards,
Petr

maintainer Reviewed-by on v1 3/3
#8 lore raw

Re: [PATCH 3/3] vhost: Fix livepatch timeouts in vhost_worker()

In reply to Josh Poimboeuf
Quoted 8 lines · Josh Poimboeuf

Livepatch timeouts were reported due to busy vhost_worker() kthreads. Now that cond_resched() can do livepatch task switching, use cond_resched() in vhost_worker(). That's the better way to conditionally call schedule() anyway.

Best Regards,
Petr

author maintainer
#5 lore raw
In reply to Petr Mladek
Quoted 7 lines · Petr Mladek

Review

Quoted 4 lines

static inline int _cond_resched(void)
{

(quoted diff) +1 -0
+	klp_sched_try_switch();
 	return __cond_resched();

My only concern is if it might cause any performance problems.

Quoted 7 lines

On one hand, cond_resched() is used in code paths that are slow on its own. Also it will do nothing most of the time.

On the other hand, cond_resched() is typically used in cycles. One cycle might be fast. The code might be slow because there are too many cycles. Repeating the same failing test might prolong the time significantly.

Yes, but it should hopefully be very rare to patch a function in the call stack of a kthread loop. In general it's a good idea for the patch author to avoid that.

Quoted 14 lines

An idea is to try the switch only when it was not done during a real schedule. Something like:

static inline int _cond_resched(void)

{
	int scheduled;

	scheduled = __cond_resched();
	if (scheduled)
		klp_sched_try_switch();

	return scheduled();
}
Quoted 5 lines

But it would make it less reliable/predictable. Also it won't work in configurations when cond_resched() is always a nop.

I am probably too careful. We might keep it simple until any real life problems are reported.

If we can get away with it, I much prefer the simple unconditional klp_sched_try_switch() because of the predictability and quickness with which the kthread gets patched.

Quoted 6 lines
Quoted 6 lines
kernel/livepatch/transition.c +0 -0
@@ -76,6 +96,8 @@ static void klp_complete_transition(void)
 		 klp_transition_patch->mod->name,
 		 klp_target_state == KLP_PATCHED ? "patching" : "unpatching");
 
Quoted 7 lines

kernel/livepatch/transition.c static void klp_complete_transition(void) · line 100

Quoted 2 lines
kernel/livepatch/transition.c +2 -0
+	klp_cond_resched_disable();
+

Nit: Strictly speaking, this is not needed when klp_complete_transition()
is called from klp_cancel_transition(). In this case, klp_cond_resched_enable() was not called. So it might be moved into klp_try_complete_transition().

Argh, I always forget about that pesky klp_cancel_transition().

Quoted 11 lines

More important thing, thinking loudly:

We need to make sure that no task is in the middle klp_cond_resched_disable() when we modify anything that is used there.

We seem to be on the safe side in klp_complete_transition(). We are here only when all tasks have TIF_PATCH_PENDING cleared. In this case, __klp_sched_try_switch() just returns. Also it calls klp_synchronize_transition() so that all tasks finish the critical part in __klp_sched_try_switch() before any new transition starts.

Quoted 8 lines

But it is not the case in klp_reverse_transition(). It modifies klp_target_state() when __klp_sched_try_switch might be in the middle of klp_check_stack() and it might give wrong result.

klp_reverse_transition() already solves similar race with klp_update_patch_state() by clearing all TIF_PATCH_PENDING flags and calling klp_synchronize_transition(). We just need to do it earlier. Something like:

Yes! Thanks, I can always count on you to find the race conditions ;-)

This highlights the similarities between klp_target_state(current) and __klp_sched_try_switch(), they both access TIF_PATCH_PENDING out-of-band.

Also, I'll update the comment in klp_copy_process(). It should be safe for with __klp_sched_try_switch() for the same reason as klp_update_patch_state(current): they all only work on 'current'.

Signature · 1 line
Josh
maintainer
#6 lore raw
Quoted 11 lines · Josh Poimboeuf
Quoted 10 lines · Petr Mladek

Review

Quoted 4 lines

static inline int _cond_resched(void)
{

(quoted diff) +1 -0
+	klp_sched_try_switch();
 	return __cond_resched();

My only concern is if it might cause any performance problems.

On one hand, cond_resched() is used in code paths that are slow on its own. Also it will do nothing most of the time.

Quoted 8 lines
Quoted 4 lines

On the other hand, cond_resched() is typically used in cycles. One cycle might be fast. The code might be slow because there are too many cycles. Repeating the same failing test might prolong the time significantly.

Yes, but it should hopefully be very rare to patch a function in the call stack of a kthread loop. In general it's a good idea for the patch author to avoid that.

I do not have any data about it.

Quoted 17 lines
Quoted 17 lines

An idea is to try the switch only when it was not done during a real schedule. Something like:

static inline int _cond_resched(void)

{
	int scheduled;

	scheduled = __cond_resched();
	if (scheduled)
		klp_sched_try_switch();

	return scheduled();
}

But it would make it less reliable/predictable. Also it won't work in configurations when cond_resched() is always a nop.

Quoted 6 lines
Quoted 2 lines

I am probably too careful. We might keep it simple until any real life problems are reported.

If we can get away with it, I much prefer the simple unconditional klp_sched_try_switch() because of the predictability and quickness with which the kthread gets patched.

I am fine with keeping it simple now. We could always change it when it causes problems.

I primary wanted to point out the potential problem and check what others think about it.

Best Regards,
Petr