Linux内核中KRETPROBES与OPTIMIZER故障解析

本文深入分析了Linux内核5.8版本中KRETPROBES机制的故障原因,以及OPTIMIZER在代码优化中的不足。通过LKRG开发过程中的实际案例,揭示了中断处理逻辑变更和编译器填充指令对内核钩子的影响。

Linux内核中损坏的KRETPROBES和OPTIMIZER简史

在LKRG开发过程中,我发现:

  • KRETPROBES自内核5.8起损坏(将在即将发布的内核中修复)
  • OPTIMIZER自内核5.5起未能充分执行任务

KPROBES和FTRACE简介

Linux内核提供两个出色的钩子框架 - KPROBES和FTRACE。KPROBES更早且经典 - 于2.6.9版本(2004年10月)引入。FTRACE是较新的接口,与K*PROBES相比可能具有更小的开销。

各种类型的K*PROBES包括:

  • KPROBES - 可放置在内核中的几乎任何指令上
  • JPROBES - 使用KPROBES实现,但自2017年起已弃用
  • KRETPROBES - 称为"返回探针",允许在挂钩函数的入口和返回路径上轻松执行用户自己的例程

FTRACE最初在内核2.6.27中引入,其工作原理完全不同,主要基于检测每个编译函数(注入"长NOP"指令)。

Linux内核运行时防护(LKRG)

LKRG对Linux内核执行运行时完整性检查,并检测针对内核的各种漏洞利用。为实现此功能,LKRG必须在内核中放置各种钩子,使用KRETPROBES来满足该要求。

LKRG在FTRACE检测函数上的KPROBE

当函数被FTRACE检测并且有人在其上注册KPROBES时,Linux内核在这种情况下使用特殊类型的KPROBES,称为"基于FTRACE的KPROBES"。

OPTIMIZER

Linux内核开发人员更进一步,积极"优化"所有K*PROBES以使用FTRACE代替。主要原因是性能 - FTRACE的开销更小。

LKRG报告误报

ALT Linux的Vitaly Chikunov报告说,当他运行FTRACE压力测试器时,LKRG报告.text部分损坏。

第一个问题 - KRETPROBES损坏

从内核5.8开始,所有非优化的KRETPROBES都不工作。根本原因来自提交0d00449c7a28a1514595630735df383dec606812,后来由提交8edd7e37aed8b9df938a63f0b0259c70569ce3d2修改。

问题逻辑:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
asm_exc_int3() -> exc_int3():
                    |
    ----------------|
    |
    v
...
nmi_enter();
...
if (!do_int3(regs))
       |
  -----|
  |
  v
do_int3() -> kprobe_int3_handler():
                    |
    ----------------|
    |
    v
...
if (!p->pre_handler || !p->pre_handler(p, regs))
                             |
    -------------------------|
    |
    v
...
pre_handler_kretprobe():
...
    if (unlikely(in_nmi())) {
        rp->nmissed++;
        return 0;
    }

本质上,exc_int3()调用nmi_enter(),而pre_handler_kretprobe()在调用任何已注册的KPROBE之前通过in_nmi()调用验证是否不在NMI中。

第二个问题 - OPTIMIZER未能充分执行工作

在生成vmlinux二进制文件时,GCC使用INT3操作码在挂钩函数末尾生成填充:

1
2
3
4
5
6
...
ffffffff8130528b:       41 bd f0 ff ff ff       mov    $0xfffffff0,%r13d
ffffffff81305291:       e9 fe fe ff ff          jmpq   ffffffff81305194
ffffffff81305296:       cc                      int3
ffffffff81305297:       cc                      int3
...

OPTIMIZER逻辑在此失败:

1
2
3
/* Another subsystem puts a breakpoint */
if (insn.opcode.bytes[0] == INT3_INSN_OPCODE)
    return 0;

然而,这里的情况并非如此。INT3_INSN_OPCODE作为填充放置在函数末尾。

原因是链接器现在使用INT3作为默认填充,如提交7705dc8557973d8ad8f10840f61d8ec805695e9e所示。

通过LKRG开发工作,我们帮助识别并修复了Linux内核中的两个有趣问题。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计