eBPF程序加载失败:数据包非法访问与bpf_trace_printk问题排查
问题分析:eBPF程序加载时"invalid access to packet"错误
问题背景
我用BCC框架开发eBPF程序,用于分析HTTP请求:拦截8000端口的TCP数据包,搜索payload中的"cmd="模式。简化后的eBPF代码如下:
#include <uapi/linux/bpf.h> #include <linux/if_ether.h> #include <linux/ip.h> #include <linux/tcp.h> #define TCP_PORT_HTTP 8000 #define MAX_ITERATIONS 100 #define MAX_CMD_LENGTH 100 int xdp_search_http_request(struct xdp_md *ctx) { void *data_end = (void *)(long)ctx->data_end; void *data = (void *)(long)ctx->data; struct ethhdr *eth = data; if ((void *)(eth + 1) > data_end) return XDP_PASS; if (eth->h_proto != htons(ETH_P_IP)) return XDP_PASS; struct iphdr *ip = (void *)eth + sizeof(*eth); if ((void *)(ip + 1) > data_end || ip->protocol != IPPROTO_TCP) return XDP_PASS; struct tcphdr *tcp = (void *)ip + ip->ihl * 4; if ((void *)(tcp + 1) > data_end || tcp->dest != htons(TCP_PORT_HTTP)) return XDP_PASS; unsigned char *payload = (unsigned char *)tcp + (tcp->doff * 4); if ((void *)(payload + 1) > data_end) return XDP_PASS; unsigned char *end = data_end; int cmd_value_start = 0; // Simple search for "cmd=" in the payload for (int i = 0; i < MAX_ITERATIONS && (payload + i + 4) <= end; i++) { if (payload[i] == 'c' && payload[i+1] == 'm' && payload[i+2] == 'd' && payload[i+3] == '=') { // Found the "cmd=" string bpf_trace_printk("Found cmd parameter in HTTP request\n"); // Move the pointer to the value part of "cmd=" cmd_value_start = i + 4; int j = 0; char cmd [100] = {0}; for (j = cmd_value_start; (j < (cmd_value_start + MAX_CMD_LENGTH)) && ((payload + j) < end) && payload[j] != ' ' && payload[j] != '&'; j++) { cmd[j - cmd_value_start] = payload[j]; } bpf_trace_printk("cmd value: %s", cmd); break; // Exit after finding the first occurrence } } return XDP_PASS; }
错误现象
加载程序时,BPF验证器提示"invalid access to packet",最终报错"Permission denied"。注释掉bpf_trace_printk("cmd value: %s", cmd);语句后,程序可正常加载运行。已确认具备必要内核能力且以特权身份运行,环境为Ubuntu 22.04(内核5.15.0-100-generic)。
问题原因
- 字符串终止符缺失风险:
bpf_trace_printk使用%s格式符时,要求字符串必须以\0结尾。当前代码中,如果从payload拷贝的内容刚好填满100字节的cmd数组,数组末尾没有预留终止符位置,验证器判定存在越界访问风险。 - 循环边界的验证器判定逻辑:拷贝循环的边界检查是逐次执行的,内核5.15的BPF验证器无法静态确认所有情况下拷贝操作都不会超出数据包边界,尤其是当
cmd_value_start接近数据包末尾时,可能触发非法访问判定。 bpf_trace_printk的安全检查限制:该函数对栈上字符串的安全性校验极为严格,哪怕是潜在的未终止字符串,都会被验证器标记为非法操作。
解决及调试方案
修复方案1:强制保证字符串终止符
修改拷贝循环,预留数组最后一个字节用于存放\0,避免字符串越界:
char cmd[MAX_CMD_LENGTH] = {0}; // 预留最后一个字节放终止符,避免填满数组时无终止位 for (j = cmd_value_start; (j < (cmd_value_start + MAX_CMD_LENGTH - 1)) && ((payload + j) < end) && payload[j] != ' ' && payload[j] != '&'; j++) { cmd[j - cmd_value_start] = payload[j]; } // 强制添加终止符,确保字符串合法 cmd[MAX_CMD_LENGTH - 1] = '\0'; bpf_trace_printk("cmd value: %s", cmd);
修复方案2:使用安全字符串拷贝辅助函数
改用内核提供的bpf_probe_read_str函数(5.15内核支持),该函数会自动处理字符串终止符和边界检查:
char cmd[MAX_CMD_LENGTH] = {0}; // 从payload安全拷贝字符串,最多读取MAX_CMD_LENGTH-1字节并自动添加终止符 bpf_probe_read_str(cmd, sizeof(cmd), payload + cmd_value_start); // 手动截断到第一个空格或&的位置(因为bpf_probe_read_str仅以\0为终止条件) for (int k = 0; k < MAX_CMD_LENGTH; k++) { if (cmd[k] == ' ' || cmd[k] == '&') { cmd[k] = '\0'; break; } } bpf_trace_printk("cmd value: %s", cmd);
调试方案
- 开启BCC调试模式,加载程序时添加
-v参数,查看验证器的详细报错信息,定位具体非法访问位置:sudo ./your_bcc_script.py -v - 使用
bpftool手动加载程序,获取更详细的验证日志:sudo bpftool prog load ./compiled_prog.o /sys/fs/bpf/test_prog - 逐步简化代码逻辑,比如固定
cmd_value_start为安全值,测试bpf_trace_printk是否正常,再逐步恢复原有逻辑,定位问题点。
内容的提问来源于stack exchange,提问作者you_know_who
相关产品推荐
相关产品推荐

