[PATCH v3 0/6] kcov: Suppress timer and scheduler coverage leaks

3 views
Skip to first unread message

Karl Mehltretter

unread,
Sep 14, 2026, 1:47:54 AMSep 14
to Andrew Morton, Karl Mehltretter, Andrey Konovalov, Alexander Potapenko, Dmitry Vyukov, Marco Elver, Bradley Morgan, Anna-Maria Behnsen, Frederic Weisbecker, Thomas Gleixner, Ingo Molnar, Peter Zijlstra, Juri Lelli, Vincent Guittot, Dietmar Eggemann, Steven Rostedt, Ben Segall, Mel Gorman, Valentin Schneider, K Prateek Nayak, Sebastian Andrzej Siewior, Clark Williams, linux-r...@lists.linux.dev, kasa...@googlegroups.com, linux-...@vger.kernel.org
KCOV aims to exclude interrupt and scheduler coverage so syscall coverage
stays input-dependent. Instrumented callees can still record when
uninstrumented timer and scheduler paths run with in_task() true.

With the diagnostic patch in [1] applied, CONFIG_KCOV_SELFTEST exposes
three cases on x86-64: deferred hrtimer rearm, __schedule() callees and
PREEMPT_RT wakeups. Task-context wakeups and new-task enqueue also add
scheduler coverage to ordinary syscalls.

Add a nestable KCOV_PAUSED bit and a kcov_pause guard. Use the guard for
deferred hrtimer rearm, __schedule(), the try_to_wake_up() wakeup body
and wake_up_new_task(). This suppresses their instrumented callees
without excluding those callees from task-context coverage.

Changes in v3:
- Rebase onto current mainline (22098763a10d).
- Share flag helpers between pause and context-switch suppression, with
READ_ONCE(), WRITE_ONCE() and compiler barriers (Alexander Potapenko).
- Take the try_to_wake_up() pause guard before the preemption guard, so
preemption is re-enabled before KCOV resumes.
- Clarify that guard users must be built without KCOV instrumentation.
- Add Alexander's Reviewed-by on patch 1.
- Drop the broad Fixes tags from the scheduler patches.

v3 testing:
- GCC 15.2 x86-64 full builds with KCOV, KCOV plus PREEMPT_RT, and
CONFIG_KCOV=n; full arm64, RISC-V64 and s390 builds.
- KCOV selftest: 10/10 x86-64 boots each with and without PREEMPT_RT;
3/3 s390 boots; Clang/LLVM 21.1.8 x86-64 full build and 3/3 boots.
- Affected-object builds with GCC 8.1 on x86-64; ARM32 and LoongArch64
with and without PREEMPT_RT; RISC-V32, RISC-V64 RT and arm64 RT.
- 1,200 KCOV-enabled fork() calls on x86-64 PREEMPT_RT, plus 3,600 futex
handshakes across RT PC and non-RT PC/CMP modes, with 21,609 nonempty,
nonsaturated fresh coverage probes.
- USB remote-coverage preservation: 40 disconnect/reconnect cycles each
on x86-64 and arm64, with arch/arm64/kernel/irq.o additionally excluded
from KCOV for the arm64 run. Intended USB symbols and task tracing
stayed live; no buffer saturation or detected disable race.
- scripts/checkpatch.pl --strict and git diff --check.

Default arm64 and RISC-V selftests still failed in ways consistent with
documented entry-instrumentation issues outside this series. The USB
checks used observation mode to record additional user-return coverage;
they test remote-coverage preservation, not zero-noise task coverage.
The arm64 build exclusion is not part of v3.

Three one-hour syzkaller A/B pairs were run for v2. Each baseline and
patched run used four 2-vCPU PREEMPT_RT VMs. The patched kernel completed
22-51% more executions than base. At matched execution counts, corpus size
grew 42-54% and coverage 14-19%. No run produced a report.

[1] https://lore.kernel.org/r/20260724192122.73...@gmail.com

v1: https://lore.kernel.org/r/20260807205027.31...@gmail.com
v2: https://lore.kernel.org/r/20260811154111.64...@gmail.com

Karl Mehltretter (6):
kcov: Use unsigned int for kcov_start() mode parameter
kcov: Add a kcov_pause guard
hrtimer: Pause KCOV during deferred rearm
sched/core: Pause KCOV in __schedule()
sched/core: Pause KCOV in try_to_wake_up()
sched/core: Pause KCOV in wake_up_new_task()

include/linux/hrtimer_rearm.h | 18 +++++++-
include/linux/kcov.h | 80 +++++++++++++++++++++++++++++++----
kernel/kcov.c | 7 +--
kernel/sched/core.c | 10 ++++-
4 files changed, 97 insertions(+), 18 deletions(-)

Range-diff:
1: f60b858edad9 ! 1: cbbdcfe8483b kcov: Use unsigned int for kcov_start() mode parameter
@@ Commit message
Type the parameter unsigned int, like the field and the saved copy. No
functional change.

- Assisted-by: Claude:claude-fable-5
+ Assisted-by: LLM
Signed-off-by: Karl Mehltretter <kmehlt...@gmail.com>
+ Reviewed-by: Alexander Potapenko <gli...@google.com>

## kernel/kcov.c ##
@@ kernel/kcov.c: EXPORT_SYMBOL(__sanitizer_cov_trace_switch);
2: 4415cac41ca4 ! 2: 1ca1b221b7e6 kcov: Add a kcov_pause guard
@@ Commit message
runs on the previous task and kcov_finish_switch() on the one switched
in, so its lifetime is not a pause section.

- Provide a kcov_pause guard backed by internal helpers that operate on
- current. The guard saves the previous pause state and restores it at
- scope exit, so sections nest. When KCOV is enabled for current, remote
- softirq sections save and restore the complete mode, preserving the pause
- state.
+ The shared setter no longer sets KCOV_IN_CTXSW on a disabled task,
+ since its mode already fails the coverage callbacks' exact comparison.

- The helpers are __always_inline, and guard users must be uninstrumented:
- inlining does not remove the caller's own coverage callbacks.
+ Provide a kcov_pause guard backed by flag helpers shared with context
+ switch suppression. The helpers access kcov_mode with READ_ONCE() and
+ WRITE_ONCE() and use compiler barriers to keep instrumented calls inside
+ the suppressed region. The guard saves the previous pause state and
+ restores it at scope exit, so sections nest. When KCOV is enabled for
+ current, remote softirq sections save and restore the complete mode,
+ preserving the pause state.

- Assisted-by: Claude:claude-fable-5
+ With CONFIG_KCOV=y, restoring a previously clear flag still writes
+ kcov_mode even when task coverage is disabled; nested guards also take
+ that path in this case.
+
+ The helpers are __always_inline. Guard users must be built without KCOV
+ instrumentation because inlining does not remove the caller's own coverage
+ callbacks.
+
+ Assisted-by: LLM
+ Suggested-by: Alexander Potapenko <gli...@google.com>
Signed-off-by: Karl Mehltretter <kmehlt...@gmail.com>

## include/linux/kcov.h ##
@@ include/linux/kcov.h: enum kcov_mode {
-#define KCOV_IN_CTXSW (1 << 30)
+#define KCOV_IN_CTXSW BIT(30)
+#define KCOV_PAUSED BIT(29)
++
++static __always_inline bool kcov_mode_enabled(unsigned int mode)
++{
++ return (mode & ~(KCOV_IN_CTXSW | KCOV_PAUSED)) != KCOV_MODE_DISABLED;
++}

void kcov_task_init(struct task_struct *t);
void kcov_task_exit(struct task_struct *t);
-@@ include/linux/kcov.h: do { \
- (t)->kcov_mode &= ~KCOV_IN_CTXSW; \
- } while (0)

+-#define kcov_prepare_switch(t) \
+-do { \
+- (t)->kcov_mode |= KCOV_IN_CTXSW; \
+-} while (0)
++static __always_inline unsigned int
++__kcov_set_flag(struct task_struct *t, unsigned int flag)
++{
++ unsigned int mode = READ_ONCE(t->kcov_mode);
++ unsigned int prev_flag = mode & flag;
++
++ if (!prev_flag && kcov_mode_enabled(mode)) {
++ WRITE_ONCE(t->kcov_mode, mode | flag);
++ barrier();
++ }
++ return prev_flag;
++}
+
+-#define kcov_finish_switch(t) \
+-do { \
+- (t)->kcov_mode &= ~KCOV_IN_CTXSW; \
+-} while (0)
++static __always_inline void
++__kcov_restore_flag(struct task_struct *t, unsigned int flag,
++ unsigned int prev_flag)
++{
++ if (!prev_flag) {
++ barrier();
++ WRITE_ONCE(t->kcov_mode, READ_ONCE(t->kcov_mode) & ~flag);
++ }
++}
++
++static __always_inline void kcov_prepare_switch(struct task_struct *t)
++{
++ __kcov_set_flag(t, KCOV_IN_CTXSW);
++}
++
++static __always_inline void kcov_finish_switch(struct task_struct *t)
++{
++ __kcov_restore_flag(t, KCOV_IN_CTXSW, 0);
++}
++
+/*
-+ * Pause coverage for current. Callers must be uninstrumented.
++ * Pause coverage for current. Callers must be built without KCOV
++ * instrumentation.
+ * Pass the returned state to __kcov_resume().
+ */
+static __always_inline unsigned int __kcov_pause(void)
+{
-+ unsigned int paused;
-+
-+ paused = current->kcov_mode & KCOV_PAUSED;
-+ current->kcov_mode |= KCOV_PAUSED;
-+ return paused;
++ return __kcov_set_flag(current, KCOV_PAUSED);
+}
+
+static __always_inline void __kcov_resume(unsigned int paused)
+{
-+ if (!paused)
-+ current->kcov_mode &= ~KCOV_PAUSED;
++ __kcov_restore_flag(current, KCOV_PAUSED, paused);
+}
-+
+
/* See Documentation/dev-tools/kcov.rst for usage details. */
void kcov_remote_start(u64 handle);
- void kcov_remote_stop(void);
@@ include/linux/kcov.h: void __sanitizer_cov_trace_switch(kcov_u64 val, void *cases);

static inline void kcov_task_init(struct task_struct *t) {}
@@ include/linux/kcov.h: static inline void kcov_remote_start_usb_softirq(u64 id) {
+ * guard(kcov_pause)();
+ *
+ * pauses coverage for current until the end of the scope. Callers must be
-+ * uninstrumented.
++ * built without KCOV instrumentation.
+ */
+DEFINE_LOCK_GUARD_0(kcov_pause,
+ _T->paused = __kcov_pause(),
@@ include/linux/kcov.h: static inline void kcov_remote_start_usb_softirq(u64 id) {

## kernel/kcov.c ##
@@ kernel/kcov.c: static const struct file_operations kcov_fops = {
+ * collecting coverage and copies all collected coverage into the kcov area.
+ */

- static inline bool kcov_mode_enabled(unsigned int mode)
- {
+-static inline bool kcov_mode_enabled(unsigned int mode)
+-{
- return (mode & ~KCOV_IN_CTXSW) != KCOV_MODE_DISABLED;
-+ return (mode & ~(KCOV_IN_CTXSW | KCOV_PAUSED)) != KCOV_MODE_DISABLED;
- }
-
+-}
+-
static void kcov_remote_softirq_start(struct task_struct *t)
+ __must_hold(&kcov_percpu_data.lock)
+ {
3: c891839993a1 ! 3: 238cf03aa786 hrtimer: Pause KCOV during deferred rearm
@@ Commit message
__no_sanitize_coverage, which is empty before GCC 12. Tested with GCC 8.1
and 15 on x86_64.

+ A later patch also pauses __schedule(); keeping hrtick_schedule_exit()
+ guarded here makes the deferred-rearm fix independent of that scheduler
+ change.
+
Fixes: 15dd3a948855 ("hrtimer: Push reprogramming timers into the interrupt return path")
- Assisted-by: Claude:claude-fable-5
+ Assisted-by: LLM
Signed-off-by: Karl Mehltretter <kmehlt...@gmail.com>

## include/linux/hrtimer_rearm.h ##
@@
- #define _LINUX_HRTIMER_REARM_H

#ifdef CONFIG_HRTIMER_REARM_DEFERRED
+ #include <linux/irqflags.h>
+#include <linux/kcov.h>
+ #include <linux/lockdep.h>
+ #include <linux/preempt.h>
#include <linux/thread_info.h>

void __hrtimer_rearm_deferred(void);

+/*
+ * KCOV: Pause outside __hrtimer_rearm_deferred() to suppress entry coverage.
-+ * Callers with KCOV enabled for current must be uninstrumented.
++ * Callers with KCOV enabled for current must be built without KCOV
++ * instrumentation.
+ */
+static __always_inline void hrtimer_rearm_deferred_kcov_paused(void)
+{
4: 63657f2c7ef0 ! 4: 55c364e62fb8 sched/core: Pause KCOV in __schedule()
@@ Metadata
## Commit message ##
sched/core: Pause KCOV in __schedule()

- kernel/sched/ is not instrumented, but callees such as sched_clock(),
- architecture CPU-capacity helpers and profile_hits() are.
+ kernel/sched/ is built without KCOV instrumentation, but callees such
+ as sched_clock(), architecture CPU-capacity helpers and profile_hits() are.

During preemption and schedule() calls, instrumented callees can add
nondeterministic scheduler coverage to current.
@@ Commit message
KCOV_PAUSED remains set while a task is switched out. The guard in its
resumed __schedule() frame restores the prior state.

- Fixes: 5c9a8750a640 ("kernel: add kcov code coverage")
- Assisted-by: Claude:claude-fable-5
+ Assisted-by: LLM
Signed-off-by: Karl Mehltretter <kmehlt...@gmail.com>

## kernel/sched/core.c ##
5: 5cf8497b8a0a ! 5: 2b6c6355917c sched/core: Pause KCOV in try_to_wake_up()
@@ Metadata
## Commit message ##
sched/core: Pause KCOV in try_to_wake_up()

- try_to_wake_up() is uninstrumented, but it calls instrumented helpers
- such as kthread_is_per_cpu(), CPU capacity helpers and SCHED_HRTICK
- arming. They can record into current while in_task() is true.
+ try_to_wake_up() is built without KCOV instrumentation, but it calls
+ instrumented helpers such as kthread_is_per_cpu(), CPU capacity helpers
+ and SCHED_HRTICK arming. They can record into current while in_task() is
+ true.

CONFIG_KCOV_SELFTEST exposes this under PREEMPT_RT. The interrupt
selftest fails on x86_64 in kthread_is_per_cpu(): RT runs the timer
@@ Commit message
selftest's spin. The same helpers leak into non-RT syscall wakeups such
as a pipe write waking a reader.

- Pause the wakeup body with the kcov_pause guard. Wrapping only
- select_task_rq() would miss SCHED_HRTICK arming during enqueue.
+ Pause the wakeup body with the kcov_pause guard. Take it before the
+ preemption guard so preemption is re-enabled while KCOV remains paused.
+ Wrapping only select_task_rq() would miss SCHED_HRTICK arming during
+ enqueue.

- Fixes: 5c9a8750a640 ("kernel: add kcov code coverage")
- Assisted-by: Claude:claude-fable-5
+ Assisted-by: LLM
Signed-off-by: Karl Mehltretter <kmehlt...@gmail.com>

## kernel/sched/core.c ##
-@@ kernel/sched/core.c: int try_to_wake_up(struct task_struct *p, unsigned int state, int wake_flags)
- guard(preempt)();
- int cpu, success = 0;
-
+@@ kernel/sched/core.c: bool ttwu_state_match(struct task_struct *p, unsigned int state, int *success)
+ */
+ int try_to_wake_up(struct task_struct *p, unsigned int state, int wake_flags)
+ {
+ /* Instrumented callees would leak coverage into current. */
+ guard(kcov_pause)();
-+
- wake_flags |= WF_TTWU;
+ guard(preempt)();
+ int cpu, success = 0;

- if (p == current) {
6: a00870853f5a ! 6: 417784f395a8 sched/core: Pause KCOV in wake_up_new_task()
@@ Metadata
## Commit message ##
sched/core: Pause KCOV in wake_up_new_task()

- wake_up_new_task() is uninstrumented, but CPU selection and enqueue call
- instrumented helpers. During a KCOV-enabled fork, they can record
- scheduler, hrtimer and clockevent coverage into the parent.
+ wake_up_new_task() is built without KCOV instrumentation, but CPU
+ selection and enqueue call instrumented helpers. During a KCOV-enabled
+ fork, they can record scheduler, hrtimer and clockevent coverage into the
+ parent.

The paths depend on runqueue and CPU state, so coverage varies between
identical forks. Pause KCOV for the whole function, extending the
scheduler exclusion to new-task wakeups.

- Fixes: 5c9a8750a640 ("kernel: add kcov code coverage")
- Assisted-by: Claude:claude-fable-5
+ Assisted-by: LLM
Signed-off-by: Karl Mehltretter <kmehlt...@gmail.com>

## kernel/sched/core.c ##

base-commit: 22098763a10d9c1340827fcf6edab66f153b27f0
--
2.53.0

Karl Mehltretter

unread,
Sep 14, 2026, 1:48:00 AMSep 14
to Andrew Morton, Karl Mehltretter, Andrey Konovalov, Alexander Potapenko, Dmitry Vyukov, Marco Elver, Bradley Morgan, Anna-Maria Behnsen, Frederic Weisbecker, Thomas Gleixner, Ingo Molnar, Peter Zijlstra, Juri Lelli, Vincent Guittot, Dietmar Eggemann, Steven Rostedt, Ben Segall, Mel Gorman, Valentin Schneider, K Prateek Nayak, Sebastian Andrzej Siewior, Clark Williams, linux-r...@lists.linux.dev, kasa...@googlegroups.com, linux-...@vger.kernel.org
task_struct::kcov_mode usually holds an enum kcov_mode value, but it can
also carry flag bits such as KCOV_IN_CTXSW, so the field is unsigned int.

kcov_remote_softirq_stop() passes the saved raw value through
kcov_start()'s enum kcov_mode parameter before kcov_start() stores it
back into the unsigned int field. The parameter type therefore does not
match the values it receives.

Type the parameter unsigned int, like the field and the saved copy. No
functional change.

Assisted-by: LLM
Signed-off-by: Karl Mehltretter <kmehlt...@gmail.com>
Reviewed-by: Alexander Potapenko <gli...@google.com>
---
kernel/kcov.c | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/kernel/kcov.c b/kernel/kcov.c
index 35420f0ac524..79dabbad5a38 100644
--- a/kernel/kcov.c
+++ b/kernel/kcov.c
@@ -350,7 +350,7 @@ EXPORT_SYMBOL(__sanitizer_cov_trace_switch);
#endif /* ifdef CONFIG_KCOV_ENABLE_COMPARISONS */

static void kcov_start(struct task_struct *t, struct kcov *kcov,
- unsigned int size, void *area, enum kcov_mode mode,
+ unsigned int size, void *area, unsigned int mode,
int sequence)
{
kcov_debug("t = %px, size = %u, area = %px\n", t, size, area);
--
2.53.0

Karl Mehltretter

unread,
Sep 14, 2026, 1:48:02 AMSep 14
to Andrew Morton, Karl Mehltretter, Andrey Konovalov, Alexander Potapenko, Dmitry Vyukov, Marco Elver, Bradley Morgan, Anna-Maria Behnsen, Frederic Weisbecker, Thomas Gleixner, Ingo Molnar, Peter Zijlstra, Juri Lelli, Vincent Guittot, Dietmar Eggemann, Steven Rostedt, Ben Segall, Mel Gorman, Valentin Schneider, K Prateek Nayak, Sebastian Andrzej Siewior, Clark Williams, linux-r...@lists.linux.dev, kasa...@googlegroups.com, linux-...@vger.kernel.org
Interrupt-return work can run after HARDIRQ_OFFSET is dropped, when
in_task() is true. KCOV then attributes instrumented callees to the
interrupted task.

Add a KCOV_PAUSED bit next to KCOV_IN_CTXSW and mask both in
kcov_mode_enabled(). The coverage callbacks need no new check because
check_kcov_mode()'s exact comparison rejects modes with KCOV_PAUSED set.

The context switch suppression keeps its own bit: kcov_prepare_switch()
runs on the previous task and kcov_finish_switch() on the one switched
in, so its lifetime is not a pause section.

The shared setter no longer sets KCOV_IN_CTXSW on a disabled task,
since its mode already fails the coverage callbacks' exact comparison.

Provide a kcov_pause guard backed by flag helpers shared with context
switch suppression. The helpers access kcov_mode with READ_ONCE() and
WRITE_ONCE() and use compiler barriers to keep instrumented calls inside
the suppressed region. The guard saves the previous pause state and
restores it at scope exit, so sections nest. When KCOV is enabled for
current, remote softirq sections save and restore the complete mode,
preserving the pause state.

With CONFIG_KCOV=y, restoring a previously clear flag still writes
kcov_mode even when task coverage is disabled; nested guards also take
that path in this case.

The helpers are __always_inline. Guard users must be built without KCOV
instrumentation because inlining does not remove the caller's own coverage
callbacks.

Assisted-by: LLM
Suggested-by: Alexander Potapenko <gli...@google.com>
Signed-off-by: Karl Mehltretter <kmehlt...@gmail.com>
---
include/linux/kcov.h | 80 +++++++++++++++++++++++++++++++++++++++-----
kernel/kcov.c | 5 ---
2 files changed, 71 insertions(+), 14 deletions(-)

diff --git a/include/linux/kcov.h b/include/linux/kcov.h
index 895b761b2db1..41833b434c40 100644
--- a/include/linux/kcov.h
+++ b/include/linux/kcov.h
@@ -2,6 +2,8 @@
#ifndef _LINUX_KCOV_H
#define _LINUX_KCOV_H

+#include <linux/bits.h>
+#include <linux/cleanup.h>
#include <linux/sched.h>
#include <uapi/linux/kcov.h>

@@ -23,20 +25,64 @@ enum kcov_mode {
KCOV_MODE_TRACE_CMP = 3,
};

-#define KCOV_IN_CTXSW (1 << 30)
+#define KCOV_IN_CTXSW BIT(30)
+#define KCOV_PAUSED BIT(29)
+
+static __always_inline bool kcov_mode_enabled(unsigned int mode)
+{
+ return (mode & ~(KCOV_IN_CTXSW | KCOV_PAUSED)) != KCOV_MODE_DISABLED;
+}

void kcov_task_init(struct task_struct *t);
void kcov_task_exit(struct task_struct *t);

-#define kcov_prepare_switch(t) \
-do { \
- (t)->kcov_mode |= KCOV_IN_CTXSW; \
-} while (0)
+static __always_inline unsigned int
+__kcov_set_flag(struct task_struct *t, unsigned int flag)
+{
+ unsigned int mode = READ_ONCE(t->kcov_mode);
+ unsigned int prev_flag = mode & flag;
+
+ if (!prev_flag && kcov_mode_enabled(mode)) {
+ WRITE_ONCE(t->kcov_mode, mode | flag);
+ barrier();
+ }
+ return prev_flag;
+}

-#define kcov_finish_switch(t) \
-do { \
- (t)->kcov_mode &= ~KCOV_IN_CTXSW; \
-} while (0)
+static __always_inline void
+__kcov_restore_flag(struct task_struct *t, unsigned int flag,
+ unsigned int prev_flag)
+{
+ if (!prev_flag) {
+ barrier();
+ WRITE_ONCE(t->kcov_mode, READ_ONCE(t->kcov_mode) & ~flag);
+ }
+}
+
+static __always_inline void kcov_prepare_switch(struct task_struct *t)
+{
+ __kcov_set_flag(t, KCOV_IN_CTXSW);
+}
+
+static __always_inline void kcov_finish_switch(struct task_struct *t)
+{
+ __kcov_restore_flag(t, KCOV_IN_CTXSW, 0);
+}
+
+/*
+ * Pause coverage for current. Callers must be built without KCOV
+ * instrumentation.
+ * Pass the returned state to __kcov_resume().
+ */
+static __always_inline unsigned int __kcov_pause(void)
+{
+ return __kcov_set_flag(current, KCOV_PAUSED);
+}
+
+static __always_inline void __kcov_resume(unsigned int paused)
+{
+ __kcov_restore_flag(current, KCOV_PAUSED, paused);
+}

/* See Documentation/dev-tools/kcov.rst for usage details. */
void kcov_remote_start(u64 handle);
@@ -93,6 +139,8 @@ void __sanitizer_cov_trace_switch(kcov_u64 val, void *cases);

static inline void kcov_task_init(struct task_struct *t) {}
static inline void kcov_task_exit(struct task_struct *t) {}
+static inline unsigned int __kcov_pause(void) { return 0; }
+static inline void __kcov_resume(unsigned int paused) {}
static inline void kcov_prepare_switch(struct task_struct *t) {}
static inline void kcov_finish_switch(struct task_struct *t) {}
static inline void kcov_remote_start(u64 handle) {}
@@ -107,4 +155,18 @@ static inline void kcov_remote_start_usb_softirq(u64 id) {}
static inline void kcov_remote_stop_softirq(void) {}

#endif /* CONFIG_KCOV */
+
+/*
+ * Scope-based KCOV pause:
+ *
+ * guard(kcov_pause)();
+ *
+ * pauses coverage for current until the end of the scope. Callers must be
+ * built without KCOV instrumentation.
+ */
+DEFINE_LOCK_GUARD_0(kcov_pause,
+ _T->paused = __kcov_pause(),
+ __kcov_resume(_T->paused),
+ unsigned int paused)
+
#endif /* _LINUX_KCOV_H */
diff --git a/kernel/kcov.c b/kernel/kcov.c
index 79dabbad5a38..faccbd3bd2b8 100644
--- a/kernel/kcov.c
+++ b/kernel/kcov.c
@@ -830,11 +830,6 @@ static const struct file_operations kcov_fops = {
* collecting coverage and copies all collected coverage into the kcov area.
*/

-static inline bool kcov_mode_enabled(unsigned int mode)
-{
- return (mode & ~KCOV_IN_CTXSW) != KCOV_MODE_DISABLED;
-}
-
static void kcov_remote_softirq_start(struct task_struct *t)
__must_hold(&kcov_percpu_data.lock)
{
--
2.53.0

Karl Mehltretter

unread,
Sep 14, 2026, 1:48:03 AMSep 14
to Andrew Morton, Karl Mehltretter, Andrey Konovalov, Alexander Potapenko, Dmitry Vyukov, Marco Elver, Bradley Morgan, Anna-Maria Behnsen, Frederic Weisbecker, Thomas Gleixner, Ingo Molnar, Peter Zijlstra, Juri Lelli, Vincent Guittot, Dietmar Eggemann, Steven Rostedt, Ben Segall, Mel Gorman, Valentin Schneider, K Prateek Nayak, Sebastian Andrzej Siewior, Clark Williams, linux-r...@lists.linux.dev, kasa...@googlegroups.com, linux-...@vger.kernel.org
Deferred hrtimer rearm can run after HARDIRQ_OFFSET is dropped. in_task()
is then true, so KCOV attributes the instrumented timer-reprogramming
subtree to current.

With CONFIG_KCOV_SELFTEST added to x86_64 defconfig, the interrupt
selftest fails under QEMU, detecting spurious coverage in
__hrtimer_rearm_deferred(). The same happens on s390, RISC-V and LoongArch,
which also enable HRTIMER_REARM_DEFERRED.

Excluding the involved files instead would cost their coverage on real
task-context paths, e.g. the hrtimer and timekeeping syscalls.

Take the kcov_pause guard in an __always_inline wrapper around
__hrtimer_rearm_deferred(). Use it at all call sites, including
hrtick_schedule_exit(). Callers that may run with KCOV enabled for current
are built without KCOV instrumentation or marked noinstr. HAVE_NOINSTR_HACK
covers pre-GCC-12 x86. The other affected architectures restrict KCOV to
GCC 12 or Clang through ARCH_WANTS_NO_INSTR. This avoids relying on
__no_sanitize_coverage, which is empty before GCC 12. Tested with GCC 8.1
and 15 on x86_64.

A later patch also pauses __schedule(); keeping hrtick_schedule_exit()
guarded here makes the deferred-rearm fix independent of that scheduler
change.

Fixes: 15dd3a948855 ("hrtimer: Push reprogramming timers into the interrupt return path")
Assisted-by: LLM
Signed-off-by: Karl Mehltretter <kmehlt...@gmail.com>
---
This patch depends on patch 2, "kcov: Add a kcov_pause guard".

include/linux/hrtimer_rearm.h | 18 ++++++++++++++++--
kernel/sched/core.c | 2 +-
2 files changed, 17 insertions(+), 3 deletions(-)

diff --git a/include/linux/hrtimer_rearm.h b/include/linux/hrtimer_rearm.h
index 17a81826bd9a..cd935bab3da3 100644
--- a/include/linux/hrtimer_rearm.h
+++ b/include/linux/hrtimer_rearm.h
@@ -6,12 +6,25 @@

#ifdef CONFIG_HRTIMER_REARM_DEFERRED
#include <linux/irqflags.h>
+#include <linux/kcov.h>
#include <linux/lockdep.h>
#include <linux/preempt.h>
#include <linux/thread_info.h>

void __hrtimer_rearm_deferred(void);

+/*
+ * KCOV: Pause outside __hrtimer_rearm_deferred() to suppress entry coverage.
+ * Callers with KCOV enabled for current must be built without KCOV
+ * instrumentation.
+ */
+static __always_inline void hrtimer_rearm_deferred_kcov_paused(void)
+{
+ guard(kcov_pause)();
+
+ __hrtimer_rearm_deferred();
+}
+
/*
* This is purely CPU local, so check the TIF bit first to avoid the overhead of
* the atomic test_and_clear_bit() operation for the common case where the bit
@@ -43,7 +56,7 @@ hrtimer_rearm_deferred_user_irq(unsigned long *tif_work, const unsigned long tif
*/
if (unlikely((*tif_work & TIF_REARM_MASK) == _TIF_HRTIMER_REARM)) {
clear_thread_flag(TIF_HRTIMER_REARM);
- __hrtimer_rearm_deferred();
+ hrtimer_rearm_deferred_kcov_paused();
/* Don't go into the loop if HRTIMER_REARM was the only flag */
*tif_work &= ~TIF_HRTIMER_REARM;
return !*tif_work;
@@ -55,7 +68,7 @@ hrtimer_rearm_deferred_user_irq(unsigned long *tif_work, const unsigned long tif
static __always_inline void hrtimer_rearm_deferred_tif(unsigned long tif_work)
{
if (hrtimer_test_and_clear_rearm_deferred_tif(tif_work))
- __hrtimer_rearm_deferred();
+ hrtimer_rearm_deferred_kcov_paused();
}

/*
@@ -78,6 +91,7 @@ static __always_inline bool hrtimer_test_and_clear_rearm_deferred(void)

#else /* CONFIG_HRTIMER_REARM_DEFERRED */
static __always_inline void __hrtimer_rearm_deferred(void) { }
+static __always_inline void hrtimer_rearm_deferred_kcov_paused(void) { }
static __always_inline void hrtimer_rearm_deferred(void) { }
static __always_inline void hrtimer_rearm_deferred_tif(unsigned long tif_work) { }
static __always_inline bool
diff --git a/kernel/sched/core.c b/kernel/sched/core.c
index 7885ff76e69f..ae97c63842a4 100644
--- a/kernel/sched/core.c
+++ b/kernel/sched/core.c
@@ -1020,7 +1020,7 @@ static inline void hrtick_schedule_exit(struct rq *rq)
}

if (rq->hrtick_sched & HRTICK_SCHED_REARM_HRTIMER)
- __hrtimer_rearm_deferred();
+ hrtimer_rearm_deferred_kcov_paused();

rq->hrtick_sched = HRTICK_SCHED_NONE;
}
--
2.53.0

Karl Mehltretter

unread,
Sep 14, 2026, 1:48:04 AMSep 14
to Andrew Morton, Karl Mehltretter, Andrey Konovalov, Alexander Potapenko, Dmitry Vyukov, Marco Elver, Bradley Morgan, Anna-Maria Behnsen, Frederic Weisbecker, Thomas Gleixner, Ingo Molnar, Peter Zijlstra, Juri Lelli, Vincent Guittot, Dietmar Eggemann, Steven Rostedt, Ben Segall, Mel Gorman, Valentin Schneider, K Prateek Nayak, Sebastian Andrzej Siewior, Clark Williams, linux-r...@lists.linux.dev, kasa...@googlegroups.com, linux-...@vger.kernel.org
kernel/sched/ is built without KCOV instrumentation, but callees such
as sched_clock(), architecture CPU-capacity helpers and profile_hits() are.

During preemption and schedule() calls, instrumented callees can add
nondeterministic scheduler coverage to current.

With CONFIG_KCOV_SELFTEST added to x86_64 defconfig, the interrupt
selftest fails under QEMU, detecting spurious coverage in
arch_scale_cpu_capacity().

Annotating each callee would spread exclusions across architectures.
Pause across __schedule() instead, extending the scheduler exclusion to
its callees.

KCOV_PAUSED remains set while a task is switched out. The guard in its
resumed __schedule() frame restores the prior state.

Assisted-by: LLM
Signed-off-by: Karl Mehltretter <kmehlt...@gmail.com>
---
kernel/sched/core.c | 3 +++
1 file changed, 3 insertions(+)

diff --git a/kernel/sched/core.c b/kernel/sched/core.c
index ae97c63842a4..54b5c5383fe6 100644
--- a/kernel/sched/core.c
+++ b/kernel/sched/core.c
@@ -7116,6 +7116,9 @@ static void __sched notrace __schedule(int sched_mode)
struct rq *rq;
int cpu;

+ /* Instrumented callees would leak coverage into current. */
+ guard(kcov_pause)();
+
/* Trace preemptions consistently with task switches */
trace_sched_entry_tp(sched_mode == SM_PREEMPT);

--
2.53.0

Karl Mehltretter

unread,
Sep 14, 2026, 1:48:05 AMSep 14
to Andrew Morton, Karl Mehltretter, Andrey Konovalov, Alexander Potapenko, Dmitry Vyukov, Marco Elver, Bradley Morgan, Anna-Maria Behnsen, Frederic Weisbecker, Thomas Gleixner, Ingo Molnar, Peter Zijlstra, Juri Lelli, Vincent Guittot, Dietmar Eggemann, Steven Rostedt, Ben Segall, Mel Gorman, Valentin Schneider, K Prateek Nayak, Sebastian Andrzej Siewior, Clark Williams, linux-r...@lists.linux.dev, kasa...@googlegroups.com, linux-...@vger.kernel.org
try_to_wake_up() is built without KCOV instrumentation, but it calls
instrumented helpers such as kthread_is_per_cpu(), CPU capacity helpers
and SCHED_HRTICK arming. They can record into current while in_task() is
true.

CONFIG_KCOV_SELFTEST exposes this under PREEMPT_RT. The interrupt
selftest fails on x86_64 in kthread_is_per_cpu(): RT runs the timer
softirq in a thread, so the wakeup runs in task context during the
selftest's spin. The same helpers leak into non-RT syscall wakeups such
as a pipe write waking a reader.

Pause the wakeup body with the kcov_pause guard. Take it before the
preemption guard so preemption is re-enabled while KCOV remains paused.
Wrapping only select_task_rq() would miss SCHED_HRTICK arming during
enqueue.

Assisted-by: LLM
Signed-off-by: Karl Mehltretter <kmehlt...@gmail.com>
---
kernel/sched/core.c | 2 ++
1 file changed, 2 insertions(+)

diff --git a/kernel/sched/core.c b/kernel/sched/core.c
index 54b5c5383fe6..e3b50bbdf3a9 100644
--- a/kernel/sched/core.c
+++ b/kernel/sched/core.c
@@ -4266,6 +4266,8 @@ bool ttwu_state_match(struct task_struct *p, unsigned int state, int *success)
*/
int try_to_wake_up(struct task_struct *p, unsigned int state, int wake_flags)
{
+ /* Instrumented callees would leak coverage into current. */
+ guard(kcov_pause)();
guard(preempt)();
int cpu, success = 0;

--
2.53.0

Karl Mehltretter

unread,
Sep 14, 2026, 1:48:07 AMSep 14
to Andrew Morton, Karl Mehltretter, Andrey Konovalov, Alexander Potapenko, Dmitry Vyukov, Marco Elver, Bradley Morgan, Anna-Maria Behnsen, Frederic Weisbecker, Thomas Gleixner, Ingo Molnar, Peter Zijlstra, Juri Lelli, Vincent Guittot, Dietmar Eggemann, Steven Rostedt, Ben Segall, Mel Gorman, Valentin Schneider, K Prateek Nayak, Sebastian Andrzej Siewior, Clark Williams, linux-r...@lists.linux.dev, kasa...@googlegroups.com, linux-...@vger.kernel.org
wake_up_new_task() is built without KCOV instrumentation, but CPU
selection and enqueue call instrumented helpers. During a KCOV-enabled
fork, they can record scheduler, hrtimer and clockevent coverage into the
parent.

The paths depend on runqueue and CPU state, so coverage varies between
identical forks. Pause KCOV for the whole function, extending the
scheduler exclusion to new-task wakeups.

Assisted-by: LLM
Signed-off-by: Karl Mehltretter <kmehlt...@gmail.com>
---
kernel/sched/core.c | 3 +++
1 file changed, 3 insertions(+)

diff --git a/kernel/sched/core.c b/kernel/sched/core.c
index e3b50bbdf3a9..a669f8d99653 100644
--- a/kernel/sched/core.c
+++ b/kernel/sched/core.c
@@ -4962,6 +4962,9 @@ void wake_up_new_task(struct task_struct *p)
struct rq *rq;
int wake_flags = WF_FORK;

+ /* Instrumented callees would leak coverage into current. */
+ guard(kcov_pause)();
+
raw_spin_lock_irqsave(&p->pi_lock, rf.flags);
WRITE_ONCE(p->__state, TASK_RUNNING);
/*
--
2.53.0

Alexander Potapenko

unread,
Sep 14, 2026, 10:43:14 AMSep 14
to Karl Mehltretter, Andrew Morton, Andrey Konovalov, Dmitry Vyukov, Marco Elver, Bradley Morgan, Anna-Maria Behnsen, Frederic Weisbecker, Thomas Gleixner, Ingo Molnar, Peter Zijlstra, Juri Lelli, Vincent Guittot, Dietmar Eggemann, Steven Rostedt, Ben Segall, Mel Gorman, Valentin Schneider, K Prateek Nayak, Sebastian Andrzej Siewior, Clark Williams, linux-r...@lists.linux.dev, kasa...@googlegroups.com, linux-...@vger.kernel.org
Here and in other patches, I believe Peter expected different wording.
How about "Callees instrumented with KCOV"?



> + guard(kcov_pause)();
> +
> /* Trace preemptions consistently with task switches */
> trace_sched_entry_tp(sched_mode == SM_PREEMPT);
>
> --
> 2.53.0
>


--
Alexander Potapenko
Software Engineer

Google Germany GmbH
Erika-Mann-Straße, 33
80636 München

Geschäftsführer: Paul Manicle, Liana Sebastian
Registergericht und -nummer: Hamburg, HRB 86891
Sitz der Gesellschaft: Hamburg
Reply all
Reply to author
Forward
0 new messages