sched discussion
[PATCH 0/5] kcov: suppress timer and scheduler coverage leaks
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.c、kernel/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.c 与 hrtimer_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_coverageGCC 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/5 | include/linux/kcov.h, kernel/kcov.c | KCOV_PAUSED 位 + pause/resume helpers + 掩码 |
| 2/5 | include/linux/hrtimer_rearm.h, kernel/sched/core.c | hrtimer_rearm_deferred_paused() 替换原调用,覆盖 hrtick_schedule_exit() |
| 3/5 | kernel/sched/core.c | __schedule() 入口/出口 pause/resume |
| 4/5 | kernel/sched/core.c | try_to_wake_up() 入口/出口 pause/resume |
| 5/5 | kernel/sched/core.c | wake_up_new_task() 入口/出口 pause/resume |
关键实现
- 嵌套语义:
paused是进入前是否已 PAUSED;进入时一定置位,只在最外层退出时清。
__schedule()的 task切走后 PAUSED 仍置位,新 task 的__schedule()帧恢复旧值。 - uninstrumented 调用方约束:helpers 是 inline,但调用方自身 callback 仍会被插桩。
所以 pause 点必须落在noinstr或 KCOV-disabled 文件里。hrtimer 路径靠
HAVE_NOINSTR_HACK与ARCH_WANTS_NO_INSTR兜住 GCC < 12 的旧 x86。 KCOV_MODE_DISABLED早退:kcov_mode_enabled()改为
mode & ~(KCOV_IN_CTXSW | KCOV_PAUSED),确保 PAUSED 期间所有 COV_PC 都 short-circuit。- 测试:跨 GCC 8.1/15 构建;KCOV selftest;RISC-V/LoongArch/s390 隔离无关
arch entry泄漏后通过;syzkaller A/B 五次中位数 corpus +28.8%、覆盖 +7.7%;
USBdummy_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 改动:
- patch 4/5 用
guard()接管 preempt,使 cleanup 风格统一; kcov_start()的 mode 参数由enum kcov_mode改为unsigned int,容纳
新增 flag 位(Sashiko 提示 UBSAN 误报也由此清理);- 注释措辞统一为 Bradley 建议版本。
一句话总结
通过新增可嵌套的 KCOV_PAUSED 位与 kcov_pause/resume helpers,在 hrtimer
延迟 rearm、__schedule()、try_to_wake_up()、wake_up_new_task() 这四个
看似 task 上下文但实际属于 scheduler/timer 子树的入口包裹 coverage,让 syscall
级 KCOV 重新只反映真实输入触发的边。