Linux内核中KRETPROBES与OPTIMIZER的故障排查故事
在LKRG(Linux内核运行时守卫)的开发过程中,我发现:
- 自内核5.8版本起,KRETPROBES机制失效(已在即将发布的内核中修复)
- 自内核5.5版本起,OPTIMIZER未充分执行优化任务
背景:KPROBES与FTRACE
Linux内核提供两种强大的钩子框架:KPROBES和FTRACE。KPROBES是较老的经典框架(于2004年10月发布的2.6.9版本引入),而FTRACE是较新的接口,可能具有更小的开销。K*PROBES包括多种类型:
- KPROBES:可放置在内核几乎任何指令上
- JPROBES:基于KPROBES实现,用于无缝访问被钩函数参数(自2017年起已弃用)
- KRETPROBES:又称“返回探针”,基于KPROBES实现,允许在函数入口和返回路径执行用户自定义例程(但不能放置在任意指令上)
FTRACE最初于2008年10月9日发布的内核2.6.27中引入,其工作原理完全不同:通过在每个编译函数中注入“长NOP”指令(GCC的“-pg”选项),注册FTRACE时将“长NOP”替换为指向蹦床代码的JUMP指令。
LKRG简介
LKRG执行Linux内核的运行时完整性检查(类似于微软的PatchGuard技术),并检测各种内核漏洞利用。为实现此功能,LKRG必须在内核中放置多个钩子,其中KRETPROBES被用于满足该需求。
LKRG在FTRACE插桩函数上的KPROBE
当函数被FTRACE插桩(注入“长NOP”)且有人在其上注册KPROBES时,Linux内核使用一种称为“基于FTRACE的KPROBES”的特殊类型KPROBES。这种KPROBE利用FTRACE基础设施,与KPROBES本身关系不大,并受FTRACE规则约束(例如禁用FTRACE基础设施会使此类KPROBE失效)。
OPTIMIZER机制
Linux内核开发者进一步“优化”所有K*PROBES以使用FTRACE,主要目的是提升性能(FTRACE开销更小)。如果无法优化,则使用经典KPROBES基础设施。在现代内核中,LKRG放置的所有KRETPROBES都被转换为某种类型的FTRACE。
LKRG误报问题
ALT Linux的Vitaly Chikunov报告称,运行FTRACE压力测试器时,LKRG报告.text节损坏。我花费数周时间使LKRG检测并接受通过FTRACE进行的授权第三方内核代码修改。完成后,我发现还需要保护全局FTRACE开关(sysctl kernel.ftrace_enabled),否则LKRG的钩子可能被无意禁用,导致误报。
该功能在5.8.x内核中工作正常,但在5.9.x中失效。经调查,发现自内核5.8起,所有非优化的KRETPROBES均失效。
第一个问题:KRETPROBES失效
自内核5.8起,中断处理逻辑变更导致KRETPROBES失效。具体来说,exc_int3()调用nmi_enter(),而pre_handler_kretprobe()在调用任何注册的KPROBE前通过in_nmi()调用验证是否处于NMI中。由于exc_int3()总是调用nmi_enter(),pre_handler_kretprobe()总是检测到NMI并跳过处理。
该问题已向维护者报告并修复,补丁将回溯到稳定树(及LTS内核)。有趣的是,在5.8.x内核中未观察到问题,因为LKRG的钩子被正确优化并使用FTRACE,而非KRETPROBES。但在5.9.x中,优化失败导致使用经典的非优化KRETPROBES,从而暴露问题。
第二个问题:OPTIMIZER优化不足
未发现OPTIMIZER源代码或被钩函数本身的变化,但在生成的vmlinux二进制文件中,GCC使用INT3操作码在函数末尾生成填充:
|
|
此填充在旧内核的生成映像中不存在,但在新内核中很常见。OPTIMIZER逻辑在can_optimize()函数中失败,因为其检查是否已有断点存在:
|
|
但此处INT3是作为填充而非断点。经查,链接器默认使用INT3作为填充(变更提交:7705dc8557973d8ad8f10840f61d8ec805695e9e)。
该问题已向Linux内核开发者(KPROBES所有者)提出,Masami Hiramatsu准备了修复补丁。经验证,修复后工作正常。
结论
通过LKRG的开发工作,我们帮助识别并修复了Linux内核中的两个有趣问题。感谢所有贡献者。
—Adam