Linux内核中损坏的KRETPROBES和OPTIMIZER简史
在LKRG开发过程中,我发现:
- 自内核5.8起KRETPROBES损坏(将在即将发布的内核中修复)
- 自内核5.5起OPTIMIZER未充分执行其任务
KPROBES和FTRACE简介
Linux内核提供两个出色的钩子框架——KPROBES和FTRACE。KPROBES更早且经典——于2.6.9版本(2004年10月)引入。FTRACE是较新的接口,与K*PROBES相比可能具有更小的开销。
各种类型的K*PROBES包括:
- KPROBES:可放置在内核中几乎任何指令上
- JPROBES:使用KPROBES实现,但自2017年起已弃用
- KRETPROBES:称为“返回探针”,允许在钩子函数的入口和返回路径上轻松执行用户自定义例程
当注册KPROBE时,它会复制被探测指令,并用断点指令替换被探测指令的第一个字节。
FTRACE最初在内核2.6.27中引入,其工作原理完全不同,主要思想是基于检测每个编译函数(注入"长NOP"指令)。当在特定函数上注册FTRACE时,此类"长NOP"被替换为指向蹦床代码的JUMP指令。
Linux内核运行时防护(LKRG)
LKRG对Linux内核执行运行时完整性检查,并检测针对内核的各种漏洞利用。为了实现这些功能,LKRG必须在内核中放置各种钩子,KRETPROBES用于满足这一要求。
LKRG在FTRACE检测函数上的KPROBE
如果一个函数被FTRACE检测(注入"长NOP"),并且有人在其上注册KPROBES,会发生什么?Linux内核在这种情况下使用特殊类型的KPROBES,称为"基于FTRACE的KPROBES"。
OPTIMIZER
Linux内核开发人员更进一步,积极"优化"所有K*PROBES以使用FTRACE代替,主要原因是性能——FTRACE开销更小。
第一个问题:KRETPROBES损坏
从内核5.8开始,所有非优化的KRETPROBES都不工作。在5.8之前,当引发#DB异常时,NMI的入口没有完全执行。但从内核5.8开始,中断处理逻辑被修改。
根本原因来自以下提交:
|
|
后来被以下提交修改:
|
|
本质上,自这些提交以来,KRETPROBES就不工作了。我们有以下逻辑:
|
|
本质上,exc_int3()
调用nmi_enter()
,而pre_handler_kretprobe()
在调用任何已注册的KPROBE之前通过in_nmi()
调用验证是否不在NMI中。
此问题已报告给维护者并得到正确修复。这些补丁将被反向移植到稳定树。
第二个问题:OPTIMIZER未充分执行其任务
在分析生成的vmlinux二进制文件时,我发现GCC使用INT3操作码在钩子函数末尾生成了填充:
|
|
这种填充在旧内核的生成映像中不存在。OPTIMIZER逻辑在此失败:
|
|
然而,这里的情况并非如此。INT3_INSN_OPCODE作为填充放置在函数末尾。
我发现以下提交改变了链接器的默认填充策略:
|
|
现在INT3是链接器使用的默认填充。
通过与Linux内核开发人员讨论此问题,Masami Hiramatsu准备了适当的补丁来修复这个问题:
|
|
通过LKRG开发工作,我们帮助识别并修复了Linux内核中的两个有趣问题。