lkml.org 
[lkml]   [2026]   [May]   [5]   [last100]   RSS Feed
Views: [wrap][no wrap]   [headers]  [forward] 
 
Messages in this thread
Patch in this message
/
Date
Subject[PATCH] kcov: fix potential kcov_mode corruption under CONFIG_PREEMPT_RT
From
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


\
 
 \ /
  Last update: 2026-05-05 10:18    [W:0.062 / U:3.870 seconds]
©2003-2020 Jasper Spaans|hosted at Digital Ocean and my Meterkast|Read the blog