当遇上 cilium/pwru 时,我便放弃维护自己的 skbtracer 了。

挖坑

之前,学习了 eBPF Talk: 全局变量实战指南,就打算在开源项目 GitHub cilium/pwru 上一展身手:

2023 年 2 月 4 日提了 PR,很快就被合并了。

而后 2 月 24 日有人提了个 issue:

气人的是,3 月 9 日晚上跑 ./pwru --filter-dst-ip 1.1.1.1 --output-tuple 的时候,遇到同样的问题。

既然遇上了,那就不能放过它。

分析

使用 bpf_printk() 打印日志,很快就定位出有问题的代码位置:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
static __always_inline bool
config_tuple_empty() {
    if (!addr_empty(cfg->saddr) || !addr_empty(cfg->daddr)) {
        return false;
    }
    if (cfg->l4_proto || cfg->sport || cfg->dport || cfg->port) {
        return false;
    }
    return true;
}

static __always_inline bool
filter_l3_and_l4(struct sk_buff *skb) {
    if (config_tuple_empty()) {
        bpf_printk("pwru, config is empty\n");
        return true;
    }

    // ...
}

只有 --filter-dst-ip or --filter-src-ip 时,这里判断的 config 就为空。

奇怪的地方就在这里,明明指定了 --filter-dst-ip

分析过程如下(就不详细展开了):

  1. bpftool prog dump jited id ${PROG ID}
  2. bpftool prog dump xlated id ${PROG ID}
  3. llvm-objdump -S kprobepwru_bpfel.o 查看 BPF 指令。

发现真相:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
            ; if (cfg->mark && BPF_CORE_READ(skb, mark) != cfg->mark) {
          95: LdXMemW dst: r1 src: rfp off: -8 imm: 0
            ; if (cfg->mark && BPF_CORE_READ(skb, mark) != cfg->mark) {
          96: LdXMemW dst: r2 src: r6 off: 4 imm: 0
            ; if (cfg->mark && BPF_CORE_READ(skb, mark) != cfg->mark) {
          97: JNEReg dst: r1 off: 446 src: r2
            ; if (cfg->l4_proto || cfg->sport || cfg->dport || cfg->port) {
          98: LdXMemB dst: r1 src: r6 off: 41 imm: 0
            ; if (cfg->l4_proto || cfg->sport || cfg->dport || cfg->port) {
          99: JNEImm dst: r1 off: 10 imm: 0
            ; if (cfg->l4_proto || cfg->sport || cfg->dport || cfg->port) {
         100: LoadMapValue dst: r1, fd: 0 off: 0 <.rodata>
         102: LdXMemH dst: r2 src: r1 off: 42 imm: 0
            ; if (cfg->l4_proto || cfg->sport || cfg->dport || cfg->port) {
         103: JNEImm dst: r2 off: 6 imm: 0

检查 addr 的代码被编译器优化掉了。

证明:addr_empty() 有问题。

1
2
3
4
static __always_inline bool
addr_empty(union addr addr) {
    return addr.v6addr.d1 == 0 && addr.v6addr.d2 == 0;
}

填坑

尝试填坑的过程都是泪。

  1. union addr 改成 struct addr
  2. 意识到传的实参是 cfg->saddr 时,将 union addr 改成 union addr *

以上两个尝试都失败了。

回头看看 cfg 的定义:

1
2
static volatile const struct config CFG;
#define cfg (&CFG)

Emm,“天才”般的写法。

addr_empty() 改为:

1
2
#define addr_empty(addr)                                \
    ((addr).v6addr.d1 == 0 && (addr).v6addr.d2 == 0)

就阔以了。

这么改动的原因,也是尝试填坑失败的原因,如下:

  1. 将全局变量 CFG 的一部分 union addr 传给函数时,编译器认为这部分内存不是 volatile 的,所以将 addr_empty() 函数调用给优化掉了。
  2. 编译器不允许将 volatile const structc config CFG 的一部分使用指针提供给函数使用。
  3. 宏不是函数,不存在传参问题。

提了如下 PR 进行修复:

总结

要么不挖坑,要么挖坑的同时得有能力填坑。

发现是自己破坏了 pwru,心里有点难过;毕竟这是大家都可以使用的软件。

还好是自己修复的,拿回了点心理补偿。

Generate better sentence of this. “I think it’s the behaviour of clang compiler. It eliminates the addr_empty() with considering a part of global variable CFG is not volatile. But with macro, it accesses CFG directly.”

I believe the behavior of the Clang compiler is to eliminate the addr_empty() function by assuming that a non-volatile portion of the global variable CFG does not change. However, when using a macro, CFG is accessed directly.