0/16 已展开

LLM 分析

kcov:抑制 timer 与 scheduler 的覆盖率泄漏

系列概况

  • 标题:[PATCH v2 0/6] kcov: Suppress timer and scheduler coverage leaks
  • 作者:Karl Mehltretter kmehltretter@gmail.com
  • 版本:v2(共 6 个 patch)
  • 规模:4 个文件改动,约 +66 / −6 行
  • 修改文件:include/linux/hrtimer_rearm.hinclude/linux/kcov.hkernel/kcov.ckernel/sched/core.c
  • 代码统计:patch 1 净 0 行,patch 2 +38,patch 3 +13,patch 4/5/6 各 +3
  • Message-ID(首封):20260811154111.64669-1-kmehltretter@gmail.com
  • 完整性:完整,含 cover letter、6 个 patch 与 14 封回复(其中 Alexander Potapenko 已给出两个 Reviewed-by;Peter Zijlstra 提出 NAK 与替代方案)

补丁目的

KCOV 的初衷是让 syscall 覆盖率只随用户输入变化。但当前实现里,如果未插桩(KCOV)的 timer / scheduler 路径在 in_task() 为真时跑,它内部调用的已插桩 callee(如 sched_clock()、CPU capacity helper、profile_hits()kthread_is_per_cpu())仍会被记录,并被错误归到当前任务上。三个典型症状:

  • __hrtimer_rearm_deferred() 里的 timer-reprogramming 子树;
  • __schedule() 里被调用的 arch_scale_cpu_capacity() 等;
  • PREEMPT_RT 下 wakeup 路径里的 kthread_is_per_cpu()

另外 task-context 的 wakeup 和 wake_up_new_task() 也会把调度器覆盖泄漏给普通 syscall。本系列新增可嵌套的 KCOV_PAUSED 位 + kcov_pause 守卫,让这四类关键路径调用 callee 时不再产生覆盖记录,但不屏蔽 callee 自身在 task-context 的覆盖。

旧流程的问题

  • KCOV 仅把 KCOV_IN_CTXSWKCOV_MODE_DISABLED 视为不计数;其他所有模式(含 preempt_count 还未加上、已被减去的边界)都会被当作 task 上下文记录。
  • 编译器在 callee 处自动插入 __sanitizer_cov_trace_pc(),而 __schedule() / try_to_wake_up() / wake_up_new_task() / __hrtimer_rearm_deferred() 是 noinstr 但会调到大量非 noinstr 函数,于是 KCOV 看到 in_task() 为真就把边角覆盖塞给 current。
  • 结果:CONFIG_KCOV_SELFTESTx86_64 defconfig + QEMU 下 fail;syzkaller 覆盖出现大量非确定性重复路径,corpus 增长变慢。

新流程

  • 新增 KCOV_PAUSEDBIT(29))位,与 KCOV_IN_CTXSW 并列;
  • kcov_mode_enabled() 同时屏蔽这两个位;
  • check_kcov_mode() 用精确比较,带 KCOV_PAUSED 的 mode 直接被识别为关;
  • 提供基于 DEFINE_LOCK_GUARD_0guard(kcov_pause)() 作用域守卫,内部 __kcov_pause / __kcov_resume 操作 current->kcov_mode 并保存嵌套前的旧位;
  • 在四个调用点使用该 guard:deferred hrtimer rearm、__schedule()try_to_wake_up() 主体、wake_up_new_task()
  • KCOV 关闭时 guard 编译为空。
    +-----------------------+
    |  sys_read() syscall   |
    |  in_task() == true    |
    +----------+------------+
               |
               |  instrumented .text runs
               v
    +-----------------------+        guard(kcov_pause)()
    |  try_to_wake_up()     |  --->  sets KCOV_PAUSED bit
    |  noinstr entry        |        in current->kcov_mode
    +----------+------------+
               |
               |  callees (instrumented):
               |    sched_clock()
               |    arch_scale_cpu_capacity()
               |    kthread_is_per_cpu()
               v
    +-----------------------+
    |  check_kcov_mode()    |
    |  PAUSED set -> skip   |
    +----------+------------+
               |
               |  scope exit -> __kcov_resume()
               v
    +-----------------------+
    |  KCOV_PAUSED cleared  |
    |  syscall coverage OK  |
    +-----------------------+

    Same guard pattern applied at:
       - __schedule()              (patch 4)
       - try_to_wake_up() body     (patch 5)
       - wake_up_new_task()        (patch 6)
       - __hrtimer_rearm_deferred()(patch 3)

Patch 概览

  1. kcov: Use unsigned int for kcov_start() mode parameter(cleanup):把 kcov_start() 的形参从 enum kcov_mode 改成 unsigned int,与 task_struct::kcov_mode 字段及 kcov_remote_softirq_stop() 保存的原始位一致。无功能变化。
  2. kcov: Add a kcov_pause guard(feature):新增 KCOV_PAUSED 位、__kcov_pause / __kcov_resume 两个 __always_inline 内部 helper、guard(kcov_pause)() 宏,并修改 kcov_mode_enabled() 屏蔽位。#else CONFIG_KCOV 分支提供空 stub。
  3. hrtimer: Pause KCOV during deferred rearm(bugfix):在 include/linux/hrtimer_rearm.h 增加 hrtimer_rearm_deferred_kcov_paused() 包装,调用 __hrtimer_rearm_deferred()hrtick_schedule_exit() 也改用新包装。Fixes: 15dd3a948855
  4. sched/core: Pause KCOV in __schedule()(bugfix):在函数最前面加 guard(kcov_pause)(),屏蔽 sched_clock()、CPU capacity helper、profile_hits() 等 callee。Fixes: 5c9a8750a640
  5. sched/core: Pause KCOV in try_to_wake_up()(bugfix):在 guard(preempt)() 之后加 guard(kcov_pause)(),覆盖 kthread_is_per_cpu()、CPU capacity helper、SCHED_HRTICK arming 路径。Fixes: 5c9a8750a640
  6. sched/core: Pause KCOV in wake_up_new_task()(bugfix):在 wake_up_new_task() 顶部加 guard(kcov_pause)(),避免 CPU 选择和 enqueue 把覆盖泄漏给父进程。Fixes: 5c9a8750a640

关键实现

/* include/linux/kcov.h (patch 2) */
#define KCOV_IN_CTXSW BIT(30)
#define KCOV_PAUSED   BIT(29)

static __always_inline unsigned int __kcov_pause(void)
{
    unsigned int paused = current->kcov_mode & KCOV_PAUSED;
    current->kcov_mode |= KCOV_PAUSED;
    return paused;
}

static __always_inline void __kcov_resume(unsigned int paused)
{
    if (!paused)
        current->kcov_mode &= ~KCOV_PAUSED;
}

DEFINE_LOCK_GUARD_0(kcov_pause,
    _T->paused = __kcov_pause(),
    __kcov_resume(_T->paused),
    unsigned int paused)
/* kernel/kcov.c (patch 2) */
static inline bool kcov_mode_enabled(unsigned int mode)
{
    return (mode & ~(KCOV_IN_CTXSW | KCOV_PAUSED)) != KCOV_MODE_DISABLED;
}
/* kernel/sched/core.c (patch 5) */
int try_to_wake_up(struct task_struct *p, unsigned int state, int wake_flags)
{
    guard(preempt)();
    int cpu, success = 0;

    /* Instrumented callees would leak coverage into current. */
    guard(kcov_pause)();
    ...
}
    Nesting semantics:
      outer guard:  paused = 0 -> save 0, set PAUSED
      inner guard:  paused = 1 -> save 1, PAUSED already set
      inner exit :  resumed=1 -> no clear
      outer exit :  resumed=0 -> clear PAUSED
    kcov_remote_softirq_start/stop() saves/restores full mode
    so KCOV_PAUSED survives remote-softirq round trips.

kcov_remote_softirq_start / stop() 仍保存并恢复完整 kcov_mode(含 KCOV_PAUSED),因此嵌套的 pause 段在 remote softirq 里行为正确。

类比

把 KCOV 想成小区门口的摄像头:本意是拍下居民(用户 syscall)进出大门时走过的路径。但调度器 / timer 像物业维修工,他们进出时也会经过同一条走廊,结果被摄像头误拍成居民行踪。

本系列做的事是给物业工人挂一块临时工牌KCOV_PAUSED 位),进出时摄像头主动忽略;这条工牌只在工作时间佩戴,离开走廊就自动取下(guard 作用域结束),且可以多层嵌套(班长罩工人、工头罩班长)。

Highlight:风险与注意点

  • Peter Zijlstra 强烈反对当前写法:他 NAK patch 3–6,认为既然 kernel/sched/ 是 noinstr,被 KCOV 注入本就不该发生,根因应是 KCOV 在 noinstr 区段发出了插桩 —— "kcov is broken and needs to die"。Karl 澄清 "instrumented" 特指 KCOV 插桩,承认 __hrtimer_rearm_deferred() 是普通 .text,问题是 __irq_exit_rcu() 在减去 HARDIRQ_OFFSET 之后才调到它。
  • Peter 提出替代方案(patch 15/16):直接修 kernel/softirq.csoftirq_handle_begin / end,让从 hardirq 退到 softirq 的路径显式覆盖 preempt_count;这样 KCOV 不再有机会被错误唤醒。Karl 同意这是更干净的根因修复方向。
  • 架构差异:patch 3 依赖 HAVE_NOINSTR_HACK 覆盖 GCC < 12 的 x86,其它架构用 ARCH_WANTS_NO_INSTR 限制 KCOV 到 GCC ≥ 12 或 Clang;否则旧 GCC 没有 __no_sanitize_coverage 支持。Karl 已在 GCC 8.1 / 15 / Clang 22.1 上测过。
  • 路径选择疑问:patch 5 仅在 try_to_wake_up() 主体上加 guard,而不是 select_task_rq(),以避免漏掉 SCHED_HRTICK arming;后续可能需要把 guard(kcov_pause)() 移到 guard(preempt)() 之前(v3 Karl 自己提了)。
  • 代码位移变化:guard 内联 + KCOV 关闭时空展开,导致不同 build 之间 __schedule() 多 117 字节、try_to_wake_up() 多 94 字节、wake_up_new_task() 多 88 字节,要留意回归脚本基线。
  • Sashiko 静态分析器报 "kcov_mode store 可能被重排 / 消除":Karl 认为在当前调用点不成立,但要继续观察。

版本变化

  • v1 → v2
    • 新增 patch 1(cleanup:mode 参数 unsigned int);
    • patch 2 改为 guard-only API,内部 __kcov_pause / __kcov_resume 仅操作 current(参考 Bradley Morgan 的意见);
    • patch 3–6 改用 guard(kcov_pause)() 而非显式 kcov_pause() / kcov_resume() 对;
    • 改写所有 pause 注释。

一句话总结

本系列通过新增可嵌套的 KCOV_PAUSED 位和 guard(kcov_pause)(),抑制 __schedule()try_to_wake_up()wake_up_new_task() 与 deferred hrtimer rearm 对 KCOV 覆盖的污染;但维护者认为更彻底的修法是修正 softirq 边界上的 preempt_count 状态。