eBPF Talk: challenge verifier
文章目录
在 eBPF Talk: binary search 中,我们使用一个朴素的 for
循环实现了一个二分查找的 eBPF 程序,但是,这个程序通不过 verifier,报错 “R3 unbounded memory access, make sure to bounds check any such access”。
在这篇文章里,将分析 verifier log,找到问题所在,并解决这个问题。
复现问题
为了简化 verifier log,将 for
循环次数从 32 次改为 1 次,源代码如下:
|
|
最终得到完整的 verifier log 如下:
|
|
分析问题
直接分析以上 verifier log。
先分析其中的
r3
register
- 第 36 条 bpf insn
r3 = 0xffff95285896e114
,r3
register 里的值是0xffff95285896e114
,该地址指向了.rodata
bpf map value 里的delay_cidrs.cidrs
数组。 - 第 38 条 bpf insn
r3 += r2
,将r3
register 里的值加上r2
register 里的值,目的是将r3
register 里的地址指向delay_cidrs.cidrs[mid]
。 - 第 39 条 bpf insn
r2 = *(u32 *)(r3 +0)
,从r3
register 里的地址上读取 4 个字节,存放到r2
register 里,目的是将r2
register 里的值设置为delay_cidrs.cidrs[mid].start
。
在第 38 条 bpf insn 中,因为 r2
register 的缘故,r3
register 的值的上限(umax=17179869176
)超出了 verifier 里预定的上限 (1<<29 = 536870912
)。
|
|
后分析其中的
r2
register
从后往前,先找出 r2
register 赋值的地方(如第 24 条 bpf insn),然后再从该条 bpf insn 开始往后分析。
- 第 24 条 bpf insn
r2 = 0xffff95285896e110
,r2
register 里的值是0xffff95285896e110
,该地址指向了.rodata
bpf map value 里的delay_cidrs_len
。 - 第 26 条 bpf insn
r2 = *(u32 *)(r2 +0)
,从r2
register 里的地址上读取 4 个字节,存放到r2
register 里,目的是将r2
register 里的值设置为delay_cidrs_len
。 - 第 27 条 bpf insn
r2 += -1
,将r2
register 里的值减去 1,目的是将r2
register 里的值设置为delay_cidrs_len - 1
,即是hi
的值。 - 第 33 条 bpf insn
r2 >>= 1
,将r2
register 里的值右移 1 位,目的是将r2
register 里的值设置为mid
的值,即mid = (lo+hi) >> 1
(因为lo
是 0,所以被优化掉了)。 - 第 34 条 bpf insn
r2 &= 2147483647
,将r2
register 里的值与2147483647 = 0x7fffffff
做与运算,目的是将r2
register 里的值确定在0x0 ~ 0x7fffffff
之间,估计是因为上一条 bpf insn 做了右移操作。(不明白为什么要这么操作。) - 第 35 条 bpf insn
r2 <<= 3
,将r2
register 里的值左移 3 位。(不明白为什么要这么操作。)
P.S. 一些环境信息:
|
|
估计得以后研究了 clang
之后,才能搞清楚为什么会有第 34、35 条 bpf insn。
解决问题
不过,没完全搞懂所有 bpf insn 也没关系,猜测 r2
register 的状态变更都源于 .rodata
bpf map value。
那么,解决办法就是使用 volatile
切断 r2
register 状态变更与 .rodata
bpf map value 的关联。
不熟悉 volatile
,不过知道可以用来:强行使用栈来保存变量。
也就是说,__u32 hi = delay_cidrs_len - 1;
变成 volatile __u32 hi = delay_cidrs_len - 1;
后,hi
变量的值就会被保存到栈上,而不是复用 r2
register 来保存。
因此,在 bpf verifier 里分析 r2
register 的状态变更时,就不会再受到 .rodata
bpf map value 的影响了。
加了 volatile
后的 verifier log 如下:
|
|
其中:
- 第 28 条 bpf insn
*(u32 *)(r10 -4) = r2
,将r2
register 里的值保存到栈上。 - 第 30 条 bpf insn
r2 = *(u32 *)(r10 -4)
,从栈上读取 4 个字节,存放到r2
register 里。
这便是 volatile
带来的变化。
小结
以上,我们分析了 verifier log,找到了问题所在,并解决了这个问题。
使用 volatile
解决了问题,但是额外增加了栈的使用,会影响性能。
不过得到一条经验:使用 static const volatile
定义的常量进行运算的时候,最好使用 volatile
做一下变量缓存,避免 verifier 分析 register 状态变更时,受到 .rodata
bpf map value 的影响。
P.S. demo 代码:eBPF binary search。
文章作者 Leon Hwang
上次更新 2024-04-03