[PATCH (draft)] kcov: fix potential kcov_mode corruption under CONFIG_PREEMPT_RT
From: Tetsuo Handa
Date: Mon May 04 2026 - 11:31:39 EST
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 [1].
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: Debug logs show 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.
I wrote this patch's body, but patch description above was generated
using Google AI mode. I can't tell whether above explanation is correct.
Therefore, please review but don't accept yet. For now:
Link: https://syzkaller.appspot.com/bug?extid=a21650c1666eae7b2aae [1]
Analyzed-by: https://share.google/aimode/5aL6wtYxiS3koWnAx (expires in 7 days)
Not-yet-signed-off-by: Tetsuo Handa <penguin-kernel@xxxxxxxxxxxxxxxxxxx>
Fixes: 5ff3b30ab57d ("kcov: collect coverage from interrupts")
Reported-by: syzbot+3f51ad7ac3ae57a6fdcc@xxxxxxxxxxxxxxxxxxxxxxxxx
Maybe-closes: https://syzkaller.appspot.com/bug?extid=3f51ad7ac3ae57a6fdcc
---
kernel/kcov.c | 17 +++++++++++++----
1 file changed, 13 insertions(+), 4 deletions(-)
diff --git a/kernel/kcov.c b/kernel/kcov.c
index 0b369e88c7c9..c20d3c2712d7 100644
--- a/kernel/kcov.c
+++ b/kernel/kcov.c
@@ -171,6 +171,15 @@ static __always_inline bool in_softirq_really(void)
return in_serving_softirq() && !in_hardirq() && !in_nmi();
}
+static __always_inline bool in_task_really(void)
+{
+#ifdef CONFIG_PREEMPT_RT
+ return !in_serving_softirq() && !in_hardirq() && !in_nmi();
+#else
+ return in_task();
+#endif
+}
+
static notrace bool check_kcov_mode(enum kcov_mode needed_mode, struct task_struct *t)
{
unsigned int mode;
@@ -903,7 +912,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 +924,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 +933,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));
@@ -1069,7 +1078,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