为何不使用bpf_map_update_element时eBPF编译器行为异常?如何规避冗余更新?
我编写了一段eBPF代码,通过bpf_map_lookup_element()获取map内结构体的指针,直接访问并修改其内部字段。按逻辑来说,指针操作应该直接修改map中的数据,不需要额外调用bpf_map_update_element()同步,但注释掉这个更新操作后,验证器抛出异常,代码无法加载;只有保留该调用时,字节码才能正常通过验证。
保留bpf_map_update_element()时的字节码
int find(struct __sk_buff *skb){ 91: 7b 1a e8 ff 00 00 00 00 *(u64 *)(r10 - 24) = r1 ; if(skb == NULL) 92: 15 01 92 00 00 00 00 00 if r1 == 0 goto +146 <LBB1_23> 93: b7 01 00 00 00 00 00 00 r1 = 0 ; context_key_t key = CONTEXT_KEY; 94: 63 1a fc ff 00 00 00 00 *(u32 *)(r10 - 4) = r1 95: bf a2 00 00 00 00 00 00 r2 = r10 96: 07 02 00 00 fc ff ff ff r2 += -4 ; context_data_t * ctx = bpf_map_lookup_elem(&context_map,&key); 97: 18 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 r1 = 0 ll 99: 85 00 00 00 01 00 00 00 call 1 100: 7b 0a f0 ff 00 00 00 00 *(u64 *)(r10 - 16) = r0 ; if(ctx==NULL) 101: 15 00 89 00 00 00 00 00 if r0 == 0 goto +137 <LBB1_23> ; if(ctx->action_index<0 || ctx->action_index >= MAX_ACTION_LIST) 102: 79 a1 f0 ff 00 00 00 00 r1 = *(u64 *)(r10 - 16) 103: 69 11 06 00 00 00 00 00 r1 = *(u16 *)(r1 + 6) 104: 25 01 86 00 1f 00 00 00 if r1 > 31 goto +134 <LBB1_23> ; unsigned short offset= args->offset; 105: 27 01 00 00 90 00 00 00 r1 *= 144 ; find_t *args = &(ctx->action_argument[ctx->action_index].find_args); 106: 79 a2 f0 ff 00 00 00 00 r2 = *(u64 *)(r10 - 16) 107: 07 02 00 00 10 01 00 00 r2 += 272 ; unsigned short offset= args->offset; 108: bf 24 00 00 00 00 00 00 r4 = r2 109: 0f 14 00 00 00 00 00 00 r4 += r1 110: 79 a3 e8 ff 00 00 00 00 r3 = *(u64 *)(r10 - 24) ; void *data = (void *)(__u64)skb->data; 111: 61 30 4c 00 00 00 00 00 r0 = *(u32 *)(r3 + 76) ; void *data_end = (void*)(__u64)skb->data_end; 112: 61 33 50 00 00 00 00 00 r3 = *(u32 *)(r3 + 80) ; unsigned short offset= args->offset; 113: 7b 3a e0 ff 00 00 00 00 *(u64 *)(r10 - 32) = r3 114: 69 43 00 00 00 00 00 00 r3 = *(u16 *)(r4 + 0) Update ctx structure Use bpf_map_update_element() to update ctx structure in map
注释掉bpf_map_update_element()后的异常
验证器抛出错误:
invalid access to map value, value_size=5016 off=9437312 size=2 R5 max value is outside of the allowed memory range
对应的字节码:
int find(struct __sk_buff *skb){ 91: bf 16 00 00 00 00 00 00 r6 = r1 ; if(skb == NULL) 92: 15 06 8b 00 00 00 00 00 if r6 == 0 goto +139 <LBB1_22> 93: b7 01 00 00 00 00 00 00 r1 = 0 ; context_key_t key = CONTEXT_KEY; 94: 63 1a fc ff 00 00 00 00 *(u32 *)(r10 - 4) = r1 95: bf a2 00 00 00 00 00 00 r2 = r10 96: 07 02 00 00 fc ff ff ff r2 += -4 ; context_data_t * ctx = bpf_map_lookup_elem(&context_map,&key); 97: 18 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 r1 = 0 ll 99: 85 00 00 00 01 00 00 00 call 1 100: 7b 0a f0 ff 00 00 00 00 *(u64 *)(r10 - 16) = r0 ; if(ctx==NULL) 101: 15 00 82 00 00 00 00 00 if r0 == 0 goto +130 <LBB1_22> ; if(ctx->action_index<0 || ctx->action_index >= MAX_ACTION_LIST) 102: 79 a1 f0 ff 00 00 00 00 r1 = *(u64 *)(r10 - 16) 103: 69 11 06 00 00 00 00 00 r1 = *(u16 *)(r1 + 6) 104: 7b 1a d0 ff 00 00 00 00 *(u64 *)(r10 - 48) = r1 105: 25 01 7e 00 1f 00 00 00 if r1 > 31 goto +126 <LBB1_22> ; unsigned short offset= args->offset; 106: 79 a2 d0 ff 00 00 00 00 r2 = *(u64 *)(r10 - 48) 107: 27 02 00 00 90 00 00 00 r2 *= 144 ; find_t *args = &(ctx->action_argument[ctx->action_index].find_args); 108: 79 a5 f0 ff 00 00 00 00 r5 = *(u64 *)(r10 - 16) 109: 07 05 00 00 10 01 00 00 r5 += 272 110: 7b 5a e8 ff 00 00 00 00 *(u64 *)(r10 - 24) = r5 ; unsigned short offset= args->offset; 111: 0f 25 00 00 00 00 00 00 r5 += r2 ; void *data = (void *)(__u64)skb->data; 112: 61 69 4c 00 00 00 00 00 r9 = *(u32 *)(r6 + 76) ; void *data_end = (void*)(__u64)skb->data_end; 113: 61 61 50 00 00 00 00 00 r1 = *(u32 *)(r6 + 80) ; unsigned short offset= args->offset; 114: 7b 1a e0 ff 00 00 00 00 *(u64 *)(r10 - 32) = r1 115: 69 54 00 00 00 00 00 00 r4 = *(u16 *)(r5 + 0)
核心差异分析
保留bpf_map_update_element()时,对action_index的边界校验完成后,直接用同一个寄存器(r1)进行后续的乘法和偏移计算,eBPF验证器能跟踪到该寄存器的边界约束,认可数组访问的合法性。
注释掉更新操作后,编译器将校验后的action_index值存入栈,再读取到另一个寄存器(r2)中使用,验证器无法关联r2的边界约束,认为其可能超出map值的内存范围,从而抛出错误。
解决方案
1. 复用校验后的变量/寄存器,避免中间存储
不要将校验后的action_index存入中间变量或栈,直接使用原始的字段访问完成后续计算,让编译器复用同一个寄存器,帮助验证器跟踪边界约束。
示例代码调整:
// 原代码(会生成中间存储) unsigned short idx = ctx->action_index; if (idx >= MAX_ACTION_LIST) return TC_ACT_OK; find_t *args = &ctx->action_argument[idx].find_args; // 修改后(直接复用字段访问) if (ctx->action_index >= MAX_ACTION_LIST) return TC_ACT_OK; find_t *args = &ctx->action_argument[ctx->action_index].find_args;
2. 使用内置函数保留访问索引信息
如果必须使用中间变量,可以通过__builtin_bpf_preserve_access_index()告知验证器保留变量的边界约束信息:
unsigned short idx = ctx->action_index; __builtin_bpf_preserve_access_index(idx); // 帮助验证器跟踪idx的来源和边界 if (idx >= MAX_ACTION_LIST) return TC_ACT_OK; find_t *args = &ctx->action_argument[idx].find_args;
3. 开启编译器优化
确保编译时开启eBPF相关的优化选项(如-O2),优化后的编译器会减少不必要的寄存器/栈存储,更可能保留验证器需要的边界跟踪信息。
内容的提问来源于stack exchange,提问作者Ashwin Kumar

