Messages in this thread Patch in this message |  | | Date | Tue, 5 May 2026 17:15:41 +0900 | | Subject | [PATCH] kcov: fix potential kcov_mode corruption under CONFIG_PREEMPT_RT | | From | Tetsuo Handa <> |
| |
Problem Description:
syzbot has reported intermittent kcov_mode corruption when running with CONFIG_PREEMPT_RT=y. Specifically, struct task_struct->kcov_mode is found to be unexpectedly modified (e.g., changed from 0 to 2) during workqueue execution, even when the work handler itself does not use KCOV.
[ 224.426704][ T5698] BUG: workqueue function defense_work_handler changed kcov_mode from 0 to 2 [ 224.441977][ C1] BUG: workqueue function usb_giveback_urb_bh changed kcov_mode from 2 to 0
Root Cause Analysis:
The issue stems from the ambiguity of in_task() under CONFIG_PREEMPT_RT. Since commit 5ff3b30ab57d ("kcov: collect coverage from interrupts"), kcov_remote_start() uses in_task() to decide whether to use a per-CPU preallocated buffer (irq_area) or a task-specific buffer (allocated via vmalloc).
In a CONFIG_PREEMPT_RT kernel, in_task() returns true even for threaded softirqs (e.g., ksoftirqd or threaded IRQ handlers), unless they are explicitly within a local_bh_disable() region at the time of the check. This leads to several critical issues:
1. Context Mismatch: A threaded interrupt handler (like usb_giveback_urb_bh()) might be identified as a "task" context. If it starts a remote KCOV session, it incorrectly modifies the kworker's task_struct->kcov_mode using the logic intended for system calls.
2. State Leakage: If the result of in_task() differs between kcov_remote_start() and kcov_remote_stop() due to the subtle state changes in RT scheduling, the cleanup code (kcov_stop()) might be skipped or applied to the wrong context. This leaves kcov_mode set to a non-zero value (e.g., KCOV_MODE_TRACE_CMP) when the worker returns to process_one_work().
3. Evidence: The message quoted in Problem Description showed the pr_err() from process_one_work() carrying a [ C1] (CPU-based) prefix instead of a [Txxx] (Task-based) prefix. This confirms that at the moment of the error, in_serving_softirq() was true, causing in_task() to be false, which contradicts the state when the work started.
Proposed Fix:
Introduce a more strict context check, in_task_really(), which explicitly excludes any softirq context (including threaded ones) by checking in_serving_softirq() regardless of the CONFIG_PREEMPT_RT configuration. This ensures that threaded interrupts always use the irq_area path and do not corrupt the task_struct state of the worker threads they happen to preempt.
Also, replace !in_task() && !in_serving_softirq() test in kcov_remote_{start,stop}() with in_hardirq() || in_nmi() test, for relying on unstable in_serving_softirq() test might break symmetry of kcov_remote_{start,stop}() calls.
Link: https://syzkaller.appspot.com/bug?extid=e6686317bd9fe911591a Analyzed-by: Google AI mode (no mail address) Signed-off-by: Tetsuo Handa <penguin-kernel@I-love.SAKURA.ne.jp> Fixes: 5ff3b30ab57d ("kcov: collect coverage from interrupts") --- kernel/kcov.c | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-)
diff --git a/kernel/kcov.c b/kernel/kcov.c index 0b369e88c7c9..f77bcd507701 100644 --- a/kernel/kcov.c +++ b/kernel/kcov.c @@ -171,6 +171,16 @@ static __always_inline bool in_softirq_really(void) return in_serving_softirq() && !in_hardirq() && !in_nmi(); } +static __always_inline bool in_task_really(void) +{ + /* Caller won't call this function if in_hardirq() || in_nmi(). */ +#ifdef CONFIG_PREEMPT_RT + return !in_serving_softirq(); +#else + return in_task(); +#endif +} + static notrace bool check_kcov_mode(enum kcov_mode needed_mode, struct task_struct *t) { unsigned int mode; @@ -869,9 +879,10 @@ void kcov_remote_start(u64 handle) int sequence; unsigned long flags; - if (WARN_ON(!kcov_check_handle(handle, true, true, true))) + /* Don't use in_task() in order to allow consistent checks in RT kernels. */ + if (in_hardirq() || in_nmi()) return; - if (!in_task() && !in_softirq_really()) + if (WARN_ON(!kcov_check_handle(handle, true, true, true))) return; local_lock_irqsave(&kcov_percpu_data.lock, flags); @@ -903,7 +914,7 @@ void kcov_remote_start(u64 handle) return; } kcov_debug("handle = %llx, context: %s\n", handle, - in_task() ? "task" : "softirq"); + in_task_really() ? "task" : "softirq"); kcov = remote->kcov; /* Put in kcov_remote_stop(). */ kcov_get(kcov); @@ -915,7 +926,7 @@ void kcov_remote_start(u64 handle) */ mode = context_unsafe(kcov->mode); sequence = kcov->sequence; - if (in_task()) { + if (in_task_really()) { size = kcov->remote_size; area = kcov_remote_area_get(size); } else { @@ -924,7 +935,7 @@ void kcov_remote_start(u64 handle) } spin_unlock(&kcov_remote_lock); - /* Can only happen when in_task(). */ + /* Can only happen when in_task_really(). */ if (!area) { local_unlock_irqrestore(&kcov_percpu_data.lock, flags); area = vmalloc(size * sizeof(unsigned long)); @@ -1024,7 +1035,8 @@ void kcov_remote_stop(void) int sequence; unsigned long flags; - if (!in_task() && !in_softirq_really()) + /* Don't use in_task() in order to allow consistent checks in RT kernels. */ + if (in_hardirq() || in_nmi()) return; local_lock_irqsave(&kcov_percpu_data.lock, flags); @@ -1069,7 +1081,7 @@ void kcov_remote_stop(void) kcov_move_area(kcov->mode, kcov->area, kcov->size, area); spin_unlock(&kcov->lock); - if (in_task()) { + if (in_task_really()) { spin_lock(&kcov_remote_lock); kcov_remote_area_put(area, size); spin_unlock(&kcov_remote_lock); -- 2.47.3
|  |