草庐IT

eBPF verifier常见错误整理 @龙蜥社区eBPF SIG

内核沉思录 2025-04-16 原文

如今eBPF程序的编写,很多都是基于bcc或者bpftrace进行,也有开发者直接基于libbpf库进行,但是不管怎样,编写的xx.bpf.c程序,在加载到内核时,都必须经过内核的verifier校验器进行各种边界和内存检查,经常会碰到各种奇奇怪怪的 verifier 报错,导致 eBPF 程序加载失败。

有些错误,开发者可能要花费大量的时间去分析并修改程序,并祈祷程序能够加载成功。特别是在低版本的内核运行低版本Clang编译器编译的eBPF程序,错误提示非常糟糕,经常找不到出错点,这就大大增加了开发难度。

为此,本文梳理了一些常见的 eBPF verifier 报错,避免更多的人走弯路,写出能成功加载的 eBPF 程序。同时,本文通过讲解 eBPF verifier 检查原理及给出示例程序,来分析为什么 eBPF verifier 会报错,使读者能够知其然知其所以然,达到融会贯通。

简介

eBPF verifier 是一个位于内核的校验器,用于验证 eBPF 程序的安全性,保证 eBPF 程序不会破坏内核,导致内核崩溃。对于一个 eBPF 程序, verifier 会对其进行两次检查( first pass 和 second pass)(暂且这么翻译)。

  • 第一次检查通过 dfs 算法检查 eBPF 程序是否为有向无环图(DAG),也就是 eBPF 程序不能回跳,比如使用了 goto、for 循环、while 循环等则有可能导致第一次检查失败。
  • 第二次检查时,verifier 会遍历 eBPF 程序的每条指令,同时保存寄存器的类型、值域等状态信息。通过保存的寄存器状态信息,verifier 可以检查 eBPF 程序内存访问的安全性,比如数组是否越界、helper 函数参数类型是否匹配等等。

第一次检查

eBPF verifier 通过 dfs 算法检查程序是否为有向无环图(DAG)。在此阶段,以下几种情况的 eBPF 程序将会被 verifier 拒绝:

  • eBPF 程序指令数超过允许的最大值;
  • 存在环,即存在指令回跳;
  • 存在 unreachable 指令;
  • 非法 jump,如 jump 到 eBPF 程序范围之外。

其中,第 1 种场景在 4.19 版本内核很少遇见,因为从该版本开始,指令数限制为 1000000 条。第 3 和第 4 两种场景,大部分开发者很少遇见。因为开发者都是使用高级语言编程,由编译器负责生成相应指令,所以一般不会产生 unreachable 指令和非法 jump。但是如果直接使用 eBPF 指令来写 eBPF 程序则有可能遇到此类问题。

存在环

因为指令回跳会增加指令分析的复杂度,所以 verifier 直接禁止出现指令回跳。下面是一个使用 for 循环引入指令回跳的场景:

SEC("kprobe/tcp_sendmsg")
int BPF_KPROBE(tcp_sendmsg, struct sock *sk)
{
    int i;
    for (i=0;i<1000;i++)
        bpf_printk("%d\n", i);
    return 0;
}

上述eBPF代码在运行时,会报 back-edge from insn 12 to 2 错误。这个报错意思是 eBPF 程序的第 12 条指令跳转到第 2 条指令,形成会跳,即存在环。具体可以看下面的指令信息:

// bpftool
int tcp_sendmsg(struct pt_regs * ctx):
; int BPF_KPROBE(tcp_sendmsg, struct sock *sk)
   0: (b7) r6 = 0
   1: (b7) r7 = 680997
; bpf_printk("%d\n", i);
   2: (63) *(u32 *)(r10 -4) = r7
   3: (bf) r1 = r10
; 
   4: (07) r1 += -4
; bpf_printk("%d\n", i);
   5: (b7) r2 = 4
   6: (bf) r3 = r6
   7: (85) call bpf_trace_printk#-57568
; for (i=0;i<1000;i++)
   8: (07) r6 += 1
   9: (bf) r1 = r6
  10: (67) r1 <<= 32
  11: (77) r1 >>= 32
; for (i=0;i<1000;i++)
  12: (55) if r1 != 0x3e8 goto pc-11
; int BPF_KPROBE(tcp_sendmsg, struct sock *sk)
  13: (b7) r0 = 0
  14: (95) exit

对于该错误的一般解决方法是:在 for 循环前面添加 #pragma unroll,进行循环展开,避免指令回跳。

注意:在5.10内核版本是支持有限循环的,所以上述代码是可以在 5.10 内核正常运行。

第二次检查

verfier 第二次检查会遍历所有的分支,并记录寄存器状态。在此阶段,以下几种情况的 eBPF 程序将会被 verifier 拒绝:

  • 栈访问非法,如栈溢出、栈偏移为变量等;
  • helper 函数入参的参数类型不匹配;
  • 未做范围检查,可能导致内存访问越界;
  • 指针未对齐。

栈访问非法
栈访问也是开发者写代码经常碰到的问题。

1、栈限制512字节
因为 verifier 会保存栈内存的状态,所以栈的大小是有限的,目前是 512 字节。当栈内存大小超过 512 字节时,则会被 verifier 拒绝。

SEC("kprobe/tcp_sendmsg")
int BPF_KPROBE(tcp_sendmsg, struct sock *sk)
{
#define MAX_NUM (512/8)
    volatile u64 arr[MAX_NUM + 1] = {};
    arr[MAX_NUM] = 0xff;
    bpf_printk("%lld\n", arr[MAX_NUM]);
    return 0;
}

上述程序在编译阶段会报错:

Looks like the BPF stack limit of 512 bytes is exceeded. Please move large on stack variables into BPF per-cpu array map。

对于该错误,一般建议使用 map 来存储大数据。

注:因为是高级语言编程,所以这种容易检查出来的异常,编译器就直接报错了。如果直接使用 eBPF 指令,那么会由 verifier 拒绝程序的加载。

2、栈偏移仅支持常量
当访问栈时采用变量偏移,会导致无法推测寄存器的状态。所以 4.19 版本只支持常量偏移。下面是使用变量偏移的错误示例:

SEC("kprobe/tcp_sendmsg")
int BPF_KPROBE(tcp_sendmsg, struct sock *sk)
{
    u64 volatile arr[16] = {};
    arr[bpf_ktime_get_ns() & 0xf] = 0;
    return 0;
}

当执行该程序时,会报如下错误:

variable stack access var_off=(0x0; 0x78) off=-128 size=8-

对于该错误的一般解决方法是将 arr 数组保存到 map 里面。

注意:5.10内核已经支持变量类型的栈偏移。

3、helper 函数参数类型不匹配
verfier 会检查 eBPF 程序中所调用 helper 函数的参数类型,比如 bpf_map_lookup_elem helper 函数的参数类型约束定义如下:

const struct bpf_func_proto bpf_map_lookup_elem_proto = {
  .func    = bpf_map_lookup_elem,
  .gpl_only  = false,
  .pkt_access  = true,
  .ret_type  = RET_PTR_TO_MAP_VALUE_OR_NULL,
  .arg1_type  = ARG_CONST_MAP_PTR, /* const argument used as pointer to bpf_map */
  .arg2_type  = ARG_PTR_TO_MAP_KEY, /* pointer to stack used as map key */
};

所以对于 bpf_map_lookup_elem helper 函数来说,其参数 2 类型约束为 ARG_PTR_TO_MAP_KEY,其表示指向栈的指针,即第二个参数的值必须存储在栈上。下面是一个错误的示例:

struct
{
    __uint(type, BPF_MAP_TYPE_HASH);
    __type(key, struct sock *);
    __type(value, struct sockmap_val);
    __uint(max_entries, 1024);
} sockmap SEC(".maps");

struct sockmap_val
{
    int nothing;
};

SEC("tracepoint/tcp/tcp_rcv_space_adjust")
int tp__tcp_rcv_space_adjust(struct trace_event_raw_tcp_event_sk *ctx)
{
    struct sockmap_val *sv = bpf_map_lookup_elem(&sockmap, &ctx->skaddr);
    if (sv)
        bpf_printk("%d\n", sv->nothing);
    return 0;
}

该程序会报错:R2 type=ctx expected=fp, pkt, pkt_meta, map_value。因为此时传的参数 ctx->skaddrctx 类型参数,非 fp 类型。对于该问题的一般解决方法是:定义一个栈变量,将 ctx->skaddr 的值存在栈上,即 u64 skaddr = ctx->skaddr 。

4、未做范围检查
范围检查主要是用来判断内存访问是否越界。

struct
{
    __uint(type, BPF_MAP_TYPE_HASH);
    __type(key, int);
    __type(value, int);
    __uint(max_entries, 1024);
} indexmap SEC(".maps");

SEC("kprobe/tcp_sendmsg")
int BPF_KPROBE(tcp_sendmsg, struct sock *sk)
{
    int map_key = 0;
    int map_val = 0;
    int array[10] = {};
    
    int *map_val_ptr = bpf_map_look_up(&indexmap, &map_key);
    if (map_val_ptr)
        map_val = *map_val_ptr;
    
    bpf_printk("array[%d] = %d\n", map_val, array[map_val]);
    return 0;
}
char LICENSE[] SEC("license") = "GPL";

上述程序会报错:math between fp pointer and register with unbounded min value is not allowed。一般解决方法是:在内存访问时,进行范围检查。

经验总结

虽然 eBPF 程序可以采用 c 语言编写,但是相比于 c 语言的非安全性,eBPF 则通过严格的检查来保证 eBPF 程序安全,与此同时也引入了大量的约束条件。

另外在写eBPF程序时,也会遇到其他的错误,比如不能去trace类似这样的函数,ip_rcv_finish_core.isra.16,它会作为perf trace事件的一个event name,而由于带了"."特殊符号,内核会检查不通过,导致运行失败。该问题在高版本内核已修复。

还有一种错误是运行时libbpf未能加载有效的btf文件,如下图所示。造成这个问题的原因是/boot 路径下的btf默认只支持elf格式。解决这类问题的方法是升级libbpf,高版本libbpf中支持两种格式,也可以通过libbpf参数指定btf路径来解决。

通过以上具体的例子介绍了常见的 verifier 报错,那么编写eBPF程序有哪些需要注意的地方呢?以下为通过实战开发经验,总结出来的一些tips(仅供参考,可能内核版本及libbpf版本不一样结果有所差异,如果需要可以自行再验证一下):

  • 两个指针不能做算术运算
  • 如果寄存器没有被写过,那么也不能读
  • call 返回时R1-R5被设置为不可读写,R0保存返回值
  • R6-R9 在call 执行中,值不会改变(保存到栈)
  • load/store 寄存器一定要是有效类型(PTR_TO_CTX,PTR_TO_MAP, PTR_TO_STACK)
  • 根据指针类型(PTR_TO_CTX,PTR_TO_MAP, PTR_TO_STACK),对访问的大小/对齐/边界进行校验
  • 只有先向对应堆栈写入数据,才能从堆栈读数据
  • 如果一个函数能够被eBPF访问,会严格检查传递的参数
  • 创建map时,没有使用的变量需要置0, value_size不能大于1<<(KMALLOC_SHIFT_MAX-1)
  • array map的元素总大小不能超过U32_MAX,也受进程RLIMIT_MEMLOCK的限制
  • && 要同一个类型,ptr && ptr , int && int
  • bpf_probe_read 的dst需要是栈空间
  • bpf_get_current_comm的dst需要是栈空间
  • bpf_map_update_elem参数都要是栈空间
  • 要对bpf_map_lookup_elem的返回做检测
  • map地址空间可以随意访问,但是其他内核空间地址,需要用bpf_probe_read

展望

即使开发者再怎么注意,也难免出现BUG和编译错误,如果当运行时出错提示如果能够更精确一些,明确告知异常代码在哪一行,那将是非常方便的。前面这些tips和错误解决方法都是经过不断的开发实践得来的经验总结,平时大家在编码时多认真review,最主要的还是多写多去用方可写出高效代码。

有关eBPF verifier常见错误整理 @龙蜥社区eBPF SIG的更多相关文章

  1. ruby-on-rails - Rails 常用字符串(用于通知和错误信息等) - 2

    大约一年前,我决定确保每个包含非唯一文本的Flash通知都将从模块中的方法中获取文本。我这样做的最初原因是为了避免一遍又一遍地输入相同的字符串。如果我想更改措辞,我可以在一个地方轻松完成,而且一遍又一遍地重复同一件事而出现拼写错误的可能性也会降低。我最终得到的是这样的:moduleMessagesdefformat_error_messages(errors)errors.map{|attribute,message|"Error:#{attribute.to_s.titleize}#{message}."}enddeferror_message_could_not_find(obje

  2. ruby-on-rails - 迷你测试错误 : "NameError: uninitialized constant" - 2

    我遵循MichaelHartl的“RubyonRails教程:学习Web开发”,并创建了检查用户名和电子邮件长度有效性的测试(名称最多50个字符,电子邮件最多255个字符)。test/helpers/application_helper_test.rb的内容是:require'test_helper'classApplicationHelperTest在运行bundleexecraketest时,所有测试都通过了,但我看到以下消息在最后被标记为错误:ERROR["test_full_title_helper",ApplicationHelperTest,1.820016791]test

  3. ruby-on-rails - 如何在 Rails View 上显示错误消息? - 2

    我是rails的新手,想在form字段上应用验证。myviewsnew.html.erb.....模拟.rbclassSimulation{:in=>1..25,:message=>'Therowmustbebetween1and25'}end模拟Controller.rbclassSimulationsController我想检查模型类中row字段的整数范围,如果不在范围内则返回错误信息。我可以检查上面代码的范围,但无法返回错误消息提前致谢 最佳答案 关键是您使用的是模型表单,一种显示ActiveRecord模型实例属性的表单。c

  4. 使用 ACL 调用 upload_file 时出现 Ruby S3 "Access Denied"错误 - 2

    我正在尝试编写一个将文件上传到AWS并公开该文件的Ruby脚本。我做了以下事情:s3=Aws::S3::Resource.new(credentials:Aws::Credentials.new(KEY,SECRET),region:'us-west-2')obj=s3.bucket('stg-db').object('key')obj.upload_file(filename)这似乎工作正常,除了该文件不是公开可用的,而且我无法获得它的公共(public)URL。但是当我登录到S3时,我可以正常查看我的文件。为了使其公开可用,我将最后一行更改为obj.upload_file(file

  5. ruby-on-rails - 错误 : Error installing pg: ERROR: Failed to build gem native extension - 2

    我克隆了一个rails仓库,我现在正尝试捆绑安装背景:OSXElCapitanruby2.2.3p173(2015-08-18修订版51636)[x86_64-darwin15]rails-v在您的Gemfile中列出的或native可用的任何gem源中找不到gem'pg(>=0)ruby​​'。运行bundleinstall以安装缺少的gem。bundleinstallFetchinggemmetadatafromhttps://rubygems.org/............Fetchingversionmetadatafromhttps://rubygems.org/...Fe

  6. ruby - #之间? Cooper 的 *Beginning Ruby* 中的错误或异常 - 2

    在Cooper的书BeginningRuby中,第166页有一个我无法重现的示例。classSongincludeComparableattr_accessor:lengthdef(other)@lengthother.lengthenddefinitialize(song_name,length)@song_name=song_name@length=lengthendenda=Song.new('Rockaroundtheclock',143)b=Song.new('BohemianRhapsody',544)c=Song.new('MinuteWaltz',60)a.betwee

  7. ruby-on-rails - 每次我尝试部署时,我都会得到 - (gcloud.preview.app.deploy) 错误响应 : [4] DEADLINE_EXCEEDED - 2

    我是Google云的新手,我正在尝试对其进行首次部署。我的第一个部署是RubyonRails项目。我基本上是在关注thisguideinthegoogleclouddocumentation.唯一的区别是我使用的是我自己的项目,而不是他们提供的“helloworld”项目。这是我的app.yaml文件runtime:customvm:trueentrypoint:bundleexecrackup-p8080-Eproductionconfig.ruresources:cpu:0.5memory_gb:1.3disk_size_gb:10当我转到我的项目目录并运行gcloudprevie

  8. ruby-on-rails - Rails 5 Active Record 记录无效错误 - 2

    我有两个Rails模型,即Invoice和Invoice_details。一个Invoice_details属于Invoice,一个Invoice有多个Invoice_details。我无法使用accepts_nested_attributes_forinInvoice通过Invoice模型保存Invoice_details。我收到以下错误:(0.2ms)BEGIN(0.2ms)ROLLBACKCompleted422UnprocessableEntityin25ms(ActiveRecord:4.0ms)ActiveRecord::RecordInvalid(Validationfa

  9. arrays - 这是 Ruby 中 Array.fill 方法的错误吗? - 2

    这个问题在这里已经有了答案:Arraysmisbehaving(1个回答)关闭6年前。是否应该这样,即我误解了,还是错误?a=Array.new(3,Array.new(3))a[1].fill('g')=>[["g","g","g"],["g","g","g"],["g","g","g"]]它不应该导致:=>[[nil,nil,nil],["g","g","g"],[nil,nil,nil]]

  10. ruby-on-rails - Ruby on Rails 计数器缓存错误 - 2

    尝试在我的RoR应用程序中实现计数器缓存列时出现错误Unknownkey(s):counter_cache。我在这个问题中实现了模型关联:Modelassociationquestion这是我的迁移:classAddVideoVotesCountToVideos0Video.reset_column_informationVideo.find(:all).eachdo|p|p.update_attributes:videos_votes_count,p.video_votes.lengthendenddefself.downremove_column:videos,:video_vot

随机推荐