eBPF写入数组触发“内存范围越界”错误的排查求助
问题:eBPF验证器报错“value is outside of the allowed memory range”
尝试从用户空间读取数据并存入char数组,由于读取可能触发多次,因此记录偏移量来存储数据。已对读取长度和偏移量做了边界检查,但eBPF验证器仍报错“value is outside of the allowed memory range”。
经分析,边界检查后验证器认为len和read均处于[0,4096)区间,但event->len + read < MAX_READ_CONTENT_LENGTH的条件未被验证器识别,导致失败。
相关代码
struct ReadArgs{ int fd; uintptr_t buf; // 结构体指针的兼容处理 }; struct ReadEvent{ int eventType; int fd; int len; u8 content[MAX_READ_CONTENT_LENGTH]; }; static __always_inline int readData(struct ReadArgs* args, struct ReadEvent* event, int read){ if((void *) args->buf == NULL){ return -1; } event->fd = args->fd; if(event->len > MAX_READ_CONTENT_LENGTH){ return -1; } else { event->len &= (MAX_READ_CONTENT_LENGTH-1); } if(read > MAX_READ_CONTENT_LENGTH){ read = MAX_READ_CONTENT_LENGTH - 1; }else{ read &= (MAX_READ_CONTENT_LENGTH-1); } if(event->len + read < MAX_READ_CONTENT_LENGTH) { long res = bpf_probe_read_user(&event->content[event->len], read, (const void *) args->buf); // 此处报错 if (res < 0) { DEBUG("readData: bpf_probe_read_user return %d", res); return -1; } event->len += read; } return 0; }
验证器日志
; if((void *) args->buf == NULL){ 109: (79) r3 = *(u64 *)(r7 +8) ; R3_w=scalar() R7=map_value(off=0,ks=4,vs=16,imm=0) ; if((void *) args->buf == NULL){ 110: (15) if r3 == 0x0 goto pc+64 ; R3_w=scalar() ; event->fd = args->fd; 111: (61) r1 = *(u32 *)(r7 +0) ; R1_w=scalar(umax=4294967295,var_off=(0x0; 0xffffffff)) R7=map_value(off=0,ks=4,vs=16,imm=0) ; event->fd = args->fd; 112: (63) *(u32 *)(r6 +4) = r1 ; R1_w=scalar(umax=4294967295,var_off=(0x0; 0xffffffff)) R6=map_value(off=0,ks=8,vs=4108,imm=0) ; if(event->len > MAX_READ_CONTENT_LENGTH){ 113: (61) r2 = *(u32 *)(r6 +8) ; R2_w=scalar(umax=4294967295,var_off=(0x0; 0xffffffff)) R6=map_value(off=0,ks=8,vs=4108,imm=0) 114: (67) r2 <<= 32 ; R2_w=scalar(smax=9223372032559808512,umax=18446744069414584320,var_off=(0x0; 0xffffffff00000000),s32_min=0,s32_max=0,u32_max=0) 115: (c7) r2 s>>= 32 ; R2=scalar(smin=-2147483648,smax=2147483647) ; if(event->len > MAX_READ_CONTENT_LENGTH){ 116: (65) if r2 s> 0x1000 goto pc+58 ; R2=scalar(smin=-2147483648,smax=4096) ; int retVal = ctx->ret; 117: (bf) r1 = r8 ; R1_w=scalar(id=1) R8=scalar(id=1) 118: (67) r1 <<= 32 ; R1_w=scalar(smax=9223372032559808512,umax=18446744069414584320,var_off=(0x0; 0xffffffff00000000),s32_min=0,s32_max=0,u32_max=0) 119: (77) r1 >>= 32 ; R1_w=scalar(umax=4294967295,var_off=(0x0; 0xffffffff)) 120: (b7) r7 = 4095 ; R7_w=4095 ; if(read > MAX_READ_CONTENT_LENGTH){ 121: (25) if r1 > 0x1000 goto pc+2 ; R1_w=scalar(umax=4096,var_off=(0x0; 0x1fff)) 122: (57) r8 &= 4095 ; R8_w=scalar(umax=4095,var_off=(0x0; 0xfff)) 123: (bf) r7 = r8 ; R7=scalar(id=13,umax=4095,var_off=(0x0; 0xfff)) R8=scalar(id=13,umax=4095,var_off=(0x0; 0xfff)) ; event->len &= (MAX_READ_CONTENT_LENGTH-1); 124: (57) r2 &= 4095 ; R2_w=scalar(umax=4095,var_off=(0x0; 0xfff)) 125: (63) *(u32 *)(r6 +8) = r2 ; R2_w=scalar(umax=4095,var_off=(0x0; 0xfff)) R6=map_value(off=0,ks=8,vs=4108,imm=0) ; if(event->len + read < MAX_READ_CONTENT_LENGTH) { 126: (bf) r1 = r2 ; R1_w=scalar(id=14,umax=4095,var_off=(0x0; 0xfff)) R2_w=scalar(id=14,umax=4095,var_off=(0x0; 0xfff)) 127: (0f) r1 += r7 ; R1_w=scalar(umax=8190,var_off=(0x0; 0x1fff)) R7=scalar(id=13,umax=4095,var_off=(0x0; 0xfff)) ; if(event->len + read < MAX_READ_CONTENT_LENGTH) { 128: (25) if r1 > 0xfff goto pc+10 ; R1_w=scalar(umax=4095,var_off=(0x0; 0xfff)) ; long res = bpf_probe_read_user(&event->content[event->len], read, (const void *) args->buf); 129: (bf) r1 = r6 ; R1_w=map_value(off=0,ks=8,vs=4108,imm=0) R6=map_value(off=0,ks=8,vs=4108,imm=0) 130: (0f) r1 += r2 ; R1_w=map_value(off=0,ks=8,vs=4108,umax=4095,var_off=(0x0; 0xfff)) R2_w=scalar(id=14,umax=4095,var_off=(0x0; 0xfff)) 131: (07) r1 += 12 ; R1_w=map_value(off=12,ks=8,vs=4108,umax=4095,var_off=(0x0; 0xfff)) ; long res = bpf_probe_read_user(&event->content[event->len], read, (const void *) args->buf); 132: (bf) r2 = r7 ; R2_w=scalar(id=13,umax=4095,var_off=(0x0; 0xfff)) R7=scalar(id=13,umax=4095,var_off=(0x0; 0xfff)) 133: (85) call bpf_probe_read_user#112 invalid access to map value, value_size=4108 off=4107 size=4095 R1 max value is outside of the allowed memory range processed 154 insns (limit 1000000) max_states_per_insn 0 total_states 12 peak_states 12 mark_read 4
解决方案
核心问题是eBPF验证器的数据流分析无法直接关联event->len + read < MAX_READ_CONTENT_LENGTH和后续数组索引的边界限制。可以通过以下方式修复:
1. 显式计算剩余空间,用它限制读取长度
先算出content数组剩余的可用空间,再将读取长度截断为不超过剩余空间,验证器能直接识别这种直白的边界限制逻辑。
2. 修改后的代码示例
static __always_inline int readData(struct ReadArgs* args, struct ReadEvent* event, int read){ if((void *) args->buf == NULL){ return -1; } event->fd = args->fd; // 确保event->len在合法范围内,避免负数或越界值 if(event->len < 0 || event->len >= MAX_READ_CONTENT_LENGTH){ event->len = 0; } // 计算剩余可用空间 int remaining = MAX_READ_CONTENT_LENGTH - event->len; if(remaining <= 0){ return 0; // 无剩余空间,直接返回 } // 限制读取长度:不能为负,且不超过剩余空间和单次最大读取限制 if(read <= 0){ return 0; } if(read > remaining){ read = remaining; } if(read > MAX_READ_CONTENT_LENGTH){ read = MAX_READ_CONTENT_LENGTH; } // 此时验证器可确认event->len + read <= MAX_READ_CONTENT_LENGTH,内存访问安全 long res = bpf_probe_read_user(&event->content[event->len], read, (const void *) args->buf); if (res < 0) { DEBUG("readData: bpf_probe_read_user return %d", res); return -1; } event->len += read; return 0; }
3. 额外优化:使用无符号类型
将event->len和read改为uint32_t等无符号整数,避免验证器处理负数分支的复杂度,进一步简化数据流分析。
原理说明
eBPF验证器对remaining = MAX - current、read <= remaining这类直接的减法和比较逻辑推导更友好,能直接得出current + read <= MAX的结论,从而确认数组访问的安全性。原代码中的a + b < MAX复合条件虽然逻辑正确,但验证器无法将其关联到数组索引的边界限制,因此需要转换为更直白的剩余空间计算方式。
内容的提问来源于stack exchange,提问作者Z3ratu1
相关产品推荐
相关产品推荐

