0/14 已展开

LLM 分析

kcov:屏蔽 timer 与 scheduler 子树的 coverage 泄漏

系列概况

  • 标题:[PATCH 0/5] kcov: suppress timer and scheduler coverage leaks
  • 作者:Karl Mehltretter kmehltretter@gmail.com
  • 版本:无版本号(首次发布,cover letter 引用了 v1 的 lore 链接 20260724192122.73080-1
  • 规模:5 个 patch,覆盖 include/linux/{kcov.h,hrtimer_rearm.h}kernel/kcov.ckernel/sched/core.c
  • 代码统计:4 文件改动,55 行新增,5 行删除
  • 主题 Message-ID:20260807205027.31972-1-kmehltretter@gmail.com
  • 完整性:完整5 patch 系列,附带 KCOV selftest、跨架构 GCC 构建、syzkaller A/B 数据

补丁目的

KCOV 在 syscall 入口打开时只想采集"任务上下文"覆盖。
kernel/sched/kernel/kcov.chrtimer_rearm 本身没有插桩,而它们
内部却调用了已被插桩的 callee:sched_clock()arch_scale_cpu_capacity()
profile_hits()kthread_is_per_cpu()SCHED_HRTICK arming。
in_task() 为 true、而 KCOV 又在追踪 current 时,这些 callee 会把
非确定性的 scheduler/timer 路径误记到当前 task 的 ring 里,导致 syscall
覆盖出现"输入不相关"的边,fuzzing 信噪比下降。

本系列新加一个可嵌套的 KCOV_PAUSED 位 + kcov_pause/resume helpers,
在四个"看起来像 task 上下文但其实属于调度/定时器子树"的入口暂停覆盖,
而不是把整个文件排除出 KCOV。

旧流程的问题

syscall entry        in_task()=true, kcov_mode=TRACE_PC
        |
        v
schedule() / try_to_wake_up()  (uninstrumented, noinstr)
        |
        +--> sched_clock() ---[COV_PC]--.
        +--> arch_scale_cpu_capacity()  ---[COV_PC]--+--> writes current kcov ring
        +--> profile_hits()              ---[COV_PC]--'
        +--> hrtimer_rearm_deferred()  (after HARDIRQ dropped)
                `--> still records timer reprogramming subtree
  • KCOV_IN_CTXSW 只在上下文切换瞬间翻转 task,新覆盖源又被错算成 current;
  • __no_sanitize_coverage GCC 12 之前是空,老 GCC x86 没法靠它兜底;
  • 把相关文件整个排除会损失真实任务上下文(hrtimer/timekeeping syscall)的覆盖。

新流程

新增 KCOV_PAUSED = BIT(29),与 KCOV_IN_CTXSW 一起在 kcov_mode_enabled()
里被掩掉。kcov_pause(t) 读并设置该位、返回旧暂停状态;kcov_resume(t, paused)
只在最外层退出时才清掉。helpers 是 __always_inline,要求调用方自身
uninstrumented(内联不会消除调用方自己的 callback)。remote_softirq 路径
保存/恢复完整 kcov_mode,保留 PAUSED 状态。

/* include/linux/kcov.h (patch 1/5) */
static __always_inline unsigned int kcov_pause(struct task_struct *t)
{
    unsigned int paused = t->kcov_mode & KCOV_PAUSED;
    t->kcov_mode |= KCOV_PAUSED;
    return paused;
}

static __always_inline void kcov_resume(struct task_struct *t, unsigned int paused)
{
    if (!paused)
        t->kcov_mode &= ~KCOV_PAUSED;
}

四个调用点(patch 2–5):

/* patch 2/5:hrtimer 延迟 rearm 包装 */
static __always_inline void hrtimer_rearm_deferred_paused(void)
{
    unsigned int kcov_paused = kcov_pause(current);
    __hrtimer_rearm_deferred();
    kcov_resume(current, kcov_paused);
}

/* patch 3/5:__schedule() 头/尾 */
{
    ...
    unsigned int kcov_paused = kcov_pause(current);
    /* ... schedule body ... */
    kcov_resume(current, kcov_paused);
}

/* patch 4/5:try_to_wake_up() 头/尾 */
/* patch 5/5:wake_up_new_task() 头/尾 */

Patch 概览

Patch文件改动
1/5include/linux/kcov.h, kernel/kcov.cKCOV_PAUSED 位 + pause/resume helpers + 掩码
2/5include/linux/hrtimer_rearm.h, kernel/sched/core.chrtimer_rearm_deferred_paused() 替换原调用,覆盖 hrtick_schedule_exit()
3/5kernel/sched/core.c__schedule() 入口/出口 pause/resume
4/5kernel/sched/core.ctry_to_wake_up() 入口/出口 pause/resume
5/5kernel/sched/core.cwake_up_new_task() 入口/出口 pause/resume

关键实现

  1. 嵌套语义paused 是进入前是否已 PAUSED;进入时一定置位,只在最外层退出时清。
    __schedule() 的 task切走后 PAUSED 仍置位,新 task 的 __schedule() 帧恢复旧值。
  2. uninstrumented 调用方约束:helpers 是 inline,但调用方自身 callback 仍会被插桩。
    所以 pause 点必须落在 noinstr 或 KCOV-disabled 文件里。hrtimer 路径靠
    HAVE_NOINSTR_HACKARCH_WANTS_NO_INSTR 兜住 GCC < 12 的旧 x86。
  3. KCOV_MODE_DISABLED 早退kcov_mode_enabled() 改为
    mode & ~(KCOV_IN_CTXSW | KCOV_PAUSED),确保 PAUSED 期间所有 COV_PC 都 short-circuit。
  4. 测试:跨 GCC 8.1/15 构建;KCOV selftest;RISC-V/LoongArch/s390 隔离无关
    arch entry泄漏后通过;syzkaller A/B 五次中位数 corpus +28.8%、覆盖 +7.7%;
    USB dummy_hcd/g_zero 不再出现 rearm/hrtick/wakeup 路径 PC;fork 测试不再出现
    __smp_call_single_queue()generic_exec_single()smp_call_function_single_async() PC。
    关闭 KCOV 时 pause 调用编译为空;x86-64启用 KCOV 时,__schedule() 增 117B、
    try_to_wake_up() 增 106B、wake_up_new_task() 增 88B。

序列图

                +----------------------+
                |  syscall / fork()    |
                |  kcov_mode = TRACE   |
                +----------+-----------+
                           |
                           v
              +------------+-------------+
              |  try_to_wake_up(p)       |
              |  kcov_pause(current)     |
              |  -> KCOV_PAUSED = 1      |
              +------------+-------------+
                           |
   +-----------+-----------+-----------+----------+
   v           v           v           v          v
kthread_is  select_task  SCHED_HRTICK enqueue  capacity
 _per_cpu     _rq         arming               helper
   \_____________  all callbacks short-circuit  _____________/
                           |
                           v
              kcov_resume(current, paused)
              KCOV_PAUSED restored to caller's state

类比

把 KCOV 想象成酒店大堂的签到板:每位住客(task)只能在自己的名牌下打勾。
前台(scheduler/timer)自己不出现在客人名单上,但前台里有几位"会顺手替
客人打勾"的同事(已被插桩的 callee)。一旦客人路过前台、被这些同事瞥见,
签到板上就会冒出不属于这位客人的勾。新加的"暂停挂牌"(KCOV_PAUSED)等于
让这位客人在前台窗口暂时摘掉名牌——前台内部的人看不到他,也就不会替他打勾;
离开前台后名牌自动挂回原位。

Highlight:风险与注意点

  • 必须 uninstrumented 调用方:helpers 是 inline,但调用方自身的 COV_PC仍
    会被插桩,所以新增 pause 点必须落在 noinstr/KCOV-disabled 文件里。
    x86 GCC < 12 靠 HAVE_NOINSTR_HACK,其他 arch 靠 ARCH_WANTS_NO_INSTR
  • __schedule() 跨任务:被切走的 task 在新 task 的 __schedule() 帧仍
    保持 PAUSED,入口/出口的 kcov_pause/resume 必须严格对称,否则 task 上线
    时残留 PAUSED。
  • Sashiko bot 提示:patch 4/5 把 kcov_resume 放在 goto 出口,但函数同时
    使用 guard(preempt)(),违反 cleanup 子系统"统一 scope-based 清理"的指南。
    作者答复 v2 改用 guard() 接管。
  • Bradley Morgan 给 patch 5/5 Reviewed-by,但建议注释措辞改为
    Instrumented callees would leak into current here...
  • Peter Zijlstra 公开回复:原作者 lore 引用的是旧 v1,本帖是 resend,但
    仍只有 patch 0 的 cover 而非完整版块;他要求重发完整系列以便整体评估。
  • 架构依赖__no_sanitize_coverage 在 GCC 12 之前为空,所以该方案在旧 GCC x86 上要依赖 HAVE_NOINSTR_HACK,其他受影响架构限制到 GCC 12+ / Clang。

版本变化

本次是 5/5 的首次发布(lore 链接 20260724192122.73080-1 是较早的 v1,本帖
是 resend)。作者预告的 v2 改动:

  1. patch 4/5 用 guard() 接管 preempt,使 cleanup 风格统一;
  2. kcov_start() 的 mode 参数由 enum kcov_mode 改为 unsigned int,容纳
    新增 flag 位(Sashiko 提示 UBSAN 误报也由此清理);
  3. 注释措辞统一为 Bradley 建议版本。

一句话总结

通过新增可嵌套的 KCOV_PAUSED 位与 kcov_pause/resume helpers,在 hrtimer
延迟 rearm、__schedule()try_to_wake_up()wake_up_new_task() 这四个
看似 task 上下文但实际属于 scheduler/timer 子树的入口包裹 coverage,让 syscall
级 KCOV 重新只反映真实输入触发的边。