[PATCH v2 0/6] kcov: Suppress timer and scheduler coverage leaks
Karl Mehltretter <[email protected]>
| Newsgroups | dev.linux.lists.linux-rt-devel,org.kernel.vger.linux-kernel |
|---|---|
| Message-ID | <[email protected]> |
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 v2: - Add patch 1 to make kcov_start()'s mode parameter unsigned int. No functional change. - Rework patch 2 around a guard-only API with private current-only helpers (Bradley Morgan). - Use the guard in patches 3-6 and reword the pause comments. v2 testing: - GCC builds on x86-64, arm32, arm64, MIPS32/64, PowerPC 32/64, s390, RISC-V 32/64, LoongArch, Xtensa and UML. - PREEMPT_RT builds on x86-64, arm32, arm64, RISC-V 32/64 and LoongArch. - x86-64 builds with GCC 8.1 and Clang 22.1. Both kernels passed a KCOV selftest boot. - x86-64 CONFIG_KCOV=n build, with no KCOV or pause references. - KCOV selftest, 10/10 x86-64 boots with and without PREEMPT_RT. A fresh non-RT boot passed after the helper-only rework. - 40 dummy_hcd/g_zero remote-KCOV cycles on x86-64 and arm64. - 400 repeated fork() calls on x86-64 PREEMPT_RT. Three one-hour syzkaller A/B pairs were run. 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. With KCOV disabled, the pause sections compile away. With KCOV enabled on x86-64, GCC 15.2 grows __schedule() by 117 bytes, try_to_wake_up() by 94 bytes and wake_up_new_task() by 88 bytes relative to the base commit. [1] https://lore.kernel.org/r/[email protected] v1: https://lore.kernel.org/r/[email protected] 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 | 17 +++++++++++++-- include/linux/kcov.h | 40 ++++++++++++++++++++++++++++++++++- kernel/kcov.c | 4 ++-- kernel/sched/core.c | 11 +++++++++- 4 files changed, 66 insertions(+), 6 deletions(-) Range-diff: -: ------------- > 1: f60b858edad96 kcov: Use unsigned int for kcov_start() mode parameter 1: a67095eb565de ! 2: 4415cac41ca43 kcov: add kcov_pause()/kcov_resume() helpers @@ Metadata Author: Karl Mehltretter <[email protected]> ## Commit message ## - kcov: add kcov_pause()/kcov_resume() helpers + kcov: Add a kcov_pause guard 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 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. + kcov_mode_enabled(). The coverage callbacks need no new check because + check_kcov_mode()'s exact comparison rejects modes with KCOV_PAUSED set. - Sections nest by passing the state returned by kcov_pause() to - kcov_resume(). Both operate on current. When task KCOV is active, remote - softirq sections save and restore the complete mode, preserving the - pause state. + 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 helpers are __always_inline, and the caller must be uninstrumented: + 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 helpers are __always_inline, and guard users must be uninstrumented: inlining does not remove the caller's own coverage callbacks. - Assisted-by: Claude:claude-opus-4-8 Assisted-by: Claude:claude-fable-5 Signed-off-by: Karl Mehltretter <[email protected]> @@ include/linux/kcov.h #define _LINUX_KCOV_H +#include <linux/bits.h> ++#include <linux/cleanup.h> #include <linux/sched.h> #include <uapi/linux/kcov.h> @@ include/linux/kcov.h: do { \ } while (0) +/* -+ * Pause coverage for current. Pass the returned state to kcov_resume(). -+ * Callers must be uninstrumented. ++ * Pause coverage for current. Callers must be uninstrumented. ++ * Pass the returned state to __kcov_resume(). + */ -+static __always_inline unsigned int kcov_pause(struct task_struct *t) ++static __always_inline unsigned int __kcov_pause(void) +{ + unsigned int paused; + -+ paused = t->kcov_mode & KCOV_PAUSED; -+ t->kcov_mode |= KCOV_PAUSED; ++ paused = current->kcov_mode & KCOV_PAUSED; ++ current->kcov_mode |= KCOV_PAUSED; + return paused; +} + -+static __always_inline void kcov_resume(struct task_struct *t, unsigned int paused) ++static __always_inline void __kcov_resume(unsigned int paused) +{ + if (!paused) -+ t->kcov_mode &= ~KCOV_PAUSED; ++ current->kcov_mode &= ~KCOV_PAUSED; +} + /* See Documentation/dev-tools/kcov.rst for usage details. */ @@ include/linux/kcov.h: void __sanitizer_cov_trace_switch(kcov_u64 val, void *case 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(struct task_struct *t) { return 0; } -+static inline void kcov_resume(struct task_struct *t, unsigned int paused) {} ++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) {} +@@ include/linux/kcov.h: 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 ++ * uninstrumented. ++ */ ++DEFINE_LOCK_GUARD_0(kcov_pause, ++ _T->paused = __kcov_pause(), ++ __kcov_resume(_T->paused), ++ unsigned int paused) ++ + #endif /* _LINUX_KCOV_H */ ## kernel/kcov.c ## @@ kernel/kcov.c: static const struct file_operations kcov_fops = { 2: 2d64b45a316c8 ! 3: c891839993a1e hrtimer: pause KCOV during deferred rearm @@ Metadata Author: Karl Mehltretter <[email protected]> ## Commit message ## - hrtimer: pause KCOV during deferred rearm + hrtimer: Pause KCOV during deferred rearm 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, the interrupt selftest fails on x86_64 - defconfig under QEMU, detecting spurious coverage in - __hrtimer_rearm_deferred(). The same happens on s390, RISC-V and - LoongArch, which also enable HRTIMER_REARM_DEFERRED. + 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. - Pause in the __always_inline wrappers, including hrtick_schedule_exit(). - Call sites where task KCOV can be active are KCOV-disabled or 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. + 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. Fixes: 15dd3a948855 ("hrtimer: Push reprogramming timers into the interrupt return path") - Assisted-by: Claude:claude-opus-4-8 Assisted-by: Claude:claude-fable-5 Signed-off-by: Karl Mehltretter <[email protected]> @@ include/linux/hrtimer_rearm.h void __hrtimer_rearm_deferred(void); +/* -+ * Pause outside __hrtimer_rearm_deferred() to suppress its entry coverage. -+ * Call sites where task KCOV can be active are uninstrumented. ++ * KCOV: Pause outside __hrtimer_rearm_deferred() to suppress entry coverage. ++ * Callers with KCOV enabled for current must be uninstrumented. + */ -+static __always_inline void hrtimer_rearm_deferred_paused(void) ++static __always_inline void hrtimer_rearm_deferred_kcov_paused(void) +{ -+ unsigned int kcov_paused = kcov_pause(current); ++ guard(kcov_pause)(); + + __hrtimer_rearm_deferred(); -+ kcov_resume(current, kcov_paused); +} + /* @@ include/linux/hrtimer_rearm.h: hrtimer_rearm_deferred_user_irq(unsigned long *ti if (unlikely((*tif_work & TIF_REARM_MASK) == _TIF_HRTIMER_REARM)) { clear_thread_flag(TIF_HRTIMER_REARM); - __hrtimer_rearm_deferred(); -+ hrtimer_rearm_deferred_paused(); ++ 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; @@ include/linux/hrtimer_rearm.h: hrtimer_rearm_deferred_user_irq(unsigned long *ti { if (hrtimer_test_and_clear_rearm_deferred_tif(tif_work)) - __hrtimer_rearm_deferred(); -+ hrtimer_rearm_deferred_paused(); ++ hrtimer_rearm_deferred_kcov_paused(); } /* @@ include/linux/hrtimer_rearm.h: static __always_inline bool hrtimer_test_and_clea #else /* CONFIG_HRTIMER_REARM_DEFERRED */ static __always_inline void __hrtimer_rearm_deferred(void) { } -+static __always_inline void hrtimer_rearm_deferred_paused(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 @@ kernel/sched/core.c: static inline void hrtick_schedule_exit(struct rq *rq) if (rq->hrtick_sched & HRTICK_SCHED_REARM_HRTIMER) - __hrtimer_rearm_deferred(); -+ hrtimer_rearm_deferred_paused(); ++ hrtimer_rearm_deferred_kcov_paused(); rq->hrtick_sched = HRTICK_SCHED_NONE; } 3: dc4bdcf8adacd ! 4: 63657f2c7ef08 sched: pause KCOV in __schedule() @@ Metadata Author: Karl Mehltretter <[email protected]> ## Commit message ## - sched: pause KCOV in __schedule() + 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. @@ Commit message During preemption and schedule() calls, instrumented callees can add nondeterministic scheduler coverage to current. - With CONFIG_KCOV_SELFTEST, the interrupt selftest fails on x86_64 - defconfig under QEMU, detecting spurious coverage in - arch_scale_cpu_capacity(). On arm64 the same class of leak appears in - sched_clock(), once the separate arm64 interrupt-accounting leak is - suppressed. + 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. Its resumed - __schedule() frame restores the prior state. + 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-opus-4-8 Assisted-by: Claude:claude-fable-5 Signed-off-by: Karl Mehltretter <[email protected]> ## kernel/sched/core.c ## @@ kernel/sched/core.c: static void __sched notrace __schedule(int sched_mode) - bool is_switch = false; - unsigned long *switch_count; - unsigned long prev_state; -+ unsigned int kcov_paused; - struct rq_flags rf; struct rq *rq; int cpu; -+ /* KCOV: sched/ is uninstrumented but the __schedule() callees are not. */ -+ kcov_paused = kcov_pause(current); ++ /* Instrumented callees would leak coverage into current. */ ++ guard(kcov_pause)(); + /* Trace preemptions consistently with task switches */ trace_sched_entry_tp(sched_mode == SM_PREEMPT); -@@ kernel/sched/core.c: static void __sched notrace __schedule(int sched_mode) - raw_spin_rq_unlock_irq(rq); - } - trace_sched_exit_tp(is_switch); -+ kcov_resume(current, kcov_paused); - } - - void __noreturn do_task_dead(void) 4: b8e96cc1903de ! 5: 5cf8497b8a0ab sched: pause KCOV in try_to_wake_up() @@ Metadata Author: Karl Mehltretter <[email protected]> ## Commit message ## - sched: pause KCOV in try_to_wake_up() + 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 @@ Commit message selftest's spin. The same helpers leak into non-RT syscall wakeups such as a pipe write waking a reader. - Pause all of try_to_wake_up(). Wrapping only select_task_rq() would miss - SCHED_HRTICK arming during enqueue. + Pause the wakeup body with the kcov_pause guard. Wrapping only + select_task_rq() would miss SCHED_HRTICK arming during enqueue. Fixes: 5c9a8750a640 ("kernel: add kcov code coverage") - Assisted-by: Claude:claude-opus-4-8 + Assisted-by: Claude:claude-fable-5 Signed-off-by: Karl Mehltretter <[email protected]> ## 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; -+ /* KCOV: sched/ is uninstrumented but the wakeup callees are not. */ -+ unsigned int kcov_paused = kcov_pause(current); ++ /* Instrumented callees would leak coverage into current. */ ++ guard(kcov_pause)(); ++ wake_flags |= WF_TTWU; -@@ kernel/sched/core.c: int try_to_wake_up(struct task_struct *p, unsigned int state, int wake_flags) - if (success) - ttwu_stat(p, task_cpu(p), wake_flags); - -+ kcov_resume(current, kcov_paused); - return success; - } - + if (p == current) { 5: f91a7644a15e8 ! 6: a00870853f5a1 sched: pause KCOV in wake_up_new_task() @@ Metadata Author: Karl Mehltretter <[email protected]> ## Commit message ## - sched: pause KCOV in wake_up_new_task() + 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 @@ Commit message scheduler exclusion to new-task wakeups. Fixes: 5c9a8750a640 ("kernel: add kcov code coverage") - Assisted-by: Claude:claude-opus-4-8 + Assisted-by: Claude:claude-fable-5 Signed-off-by: Karl Mehltretter <[email protected]> ## kernel/sched/core.c ## @@ kernel/sched/core.c: void wake_up_new_task(struct task_struct *p) - { - struct rq_flags rf; struct rq *rq; -+ unsigned int kcov_paused; int wake_flags = WF_FORK; -+ /* KCOV: sched/ is uninstrumented but the wakeup callees are not. */ -+ kcov_paused = kcov_pause(current); ++ /* 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); /* -@@ kernel/sched/core.c: void wake_up_new_task(struct task_struct *p) - rq_repin_lock(rq, &rf); - } - task_rq_unlock(rq, p, &rf); -+ kcov_resume(current, kcov_paused); - } - - #ifdef CONFIG_PREEMPT_NOTIFIERS base-commit: 8ba098e6b6ff0db8edf28528d1552be261af30d4 -- 2.53.0