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

Karl Mehltretter posted 6 patches 1 week, 4 days ago
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(-)
[PATCH v3 0/6] kcov: Suppress timer and scheduler coverage leaks
Posted by Karl Mehltretter 1 week, 4 days ago
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.73080-1-kmehltretter@gmail.com

v1: https://lore.kernel.org/r/20260807205027.31972-1-kmehltretter@gmail.com
v2: https://lore.kernel.org/r/20260811154111.64669-1-kmehltretter@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 <kmehltretter@gmail.com>
    +    Reviewed-by: Alexander Potapenko <glider@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 <glider@google.com>
         Signed-off-by: Karl Mehltretter <kmehltretter@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 <kmehltretter@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 <kmehltretter@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 <kmehltretter@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 <kmehltretter@gmail.com>
     
      ## kernel/sched/core.c ##

base-commit: 22098763a10d9c1340827fcf6edab66f153b27f0
-- 
2.53.0