eBPF Verifier內(nèi)存越界實例分析
更多內(nèi)核安全、eBPF分析和實踐文章,請關(guān)注博客: ?
https://kernel-security.blog.csdn.net/
eBPF基礎(chǔ)架構(gòu)
eBPF程序分為兩部分: 用戶態(tài)和內(nèi)核態(tài)代碼。
eBPF內(nèi)核代碼:
這個代碼首先需要經(jīng)過編譯器(比如LLVM)編譯成eBPF字節(jié)碼,然后字節(jié)碼會被加載到內(nèi)核執(zhí)行。所以 這部分代碼理論上用什么語言編寫都可以,只要編譯器支持將該語言編譯為eBPF字節(jié)碼即可;
目前絕大多數(shù)工具都是用的C語言來編寫eBPF內(nèi)核代碼,包括BCC。bpftrace提供了一種易用的腳本語言來幫助用戶快速高效的使用eBPF功能,其背后的原理還是利用LLVM 將腳本轉(zhuǎn)為eBPF字節(jié)碼;
eBPF用戶態(tài)代碼:
這部分代碼負責將eBPF內(nèi)核程序加載到內(nèi)核,與eBPF MAP交互,以及接收eBPF內(nèi)核程序發(fā)送出來的數(shù)據(jù);
這個功能的本質(zhì)上是通過Linux OS提供的syscall(bpf syscall + perf_event_open syscall)完成的,因此這 部分代碼你可以用任何語言實現(xiàn)。比如BCC使用python,libbpf使用c或者c++,TRACEE使用Go等等;

eBPF數(shù)據(jù)源
性能分析大師Brendan Gregg(Intel Fellow)總結(jié)的Linux BPF Tracing Tools上展示了豐富多彩的eBPF鉤子類型,這些鉤子類型提供了可以加載BPF程序的范圍。
fentry/fexit
Tracepoints
network devices (tc/xdp)
network routes
TCP congestion algorithms
sockets (data level)
kernel functions (kprobes)
userspace functions (uprobes)
system calls

eBPF框架的發(fā)展歷程
2014年9月 引入了bpf() syscall,將eBPF引入用戶態(tài)空間。自帶迷你libbpf庫,簡單對bpf()進行了封裝,功能是將eBPF字節(jié)碼加載到內(nèi)核。
2015年2月份 Kernel 3.19 引入bpf_load.c/h文件,對上述迷你libbpf庫再進行封裝,功能是將eBPF elf二進制文件加載到內(nèi)核(目前已過時,不建議使用)。
2015年4月 BCC項目創(chuàng)建,提供了eBPF一站式編程。
1.創(chuàng)建之初,基于上述迷你libbpf庫來加載eBPF字節(jié)碼。
2.提供了Python接口。
2015年11月 Kernel 4.3 引入標準庫 libbpf
該標準庫由Huawei 2012 OS內(nèi)核實驗室的王楠提交。
2018年 為解決BCC的缺陷,CO-RE(Compile Once, Run Everywhere)的想法被提出并實現(xiàn),最后達成共識:libbpf + BTF + CO-RE代表了eBPF的未來,BCC底層實現(xiàn)逐步轉(zhuǎn)向libbpf。
eBPF可移植性痛點和解決方案
在內(nèi)核版本A上編譯的eBPF程序,無法直接在另外一個內(nèi)核版本B上運行。造成可執(zhí)行差的根本原因在于eBPF程序訪問的內(nèi)核數(shù)據(jù)結(jié)構(gòu)(內(nèi)存空間)是不穩(wěn)定的,經(jīng)常隨內(nèi)核版本更迭而變化。
目前使用BCC的方案通過在部署機器上動態(tài)編譯eBPF源代碼可以來解決移植性問題。每一次eBPF程序運行都需要進行一次編譯,而且需要在部署機器上按照上百兆大小的依賴,如編譯器和頭文件Clang/LLVM + Linux headers等。同時在Clang/LLVM編譯過程中需要消耗大量的資源(CPU/內(nèi)存),對業(yè)務(wù)性能也會造成很大影響。
解決方案(CO-RE Compile Once,Run Everywhere):
1)BTF:將內(nèi)核數(shù)據(jù)結(jié)構(gòu)信息高效壓縮和存儲(相比于DWARF,可達到超過100倍的 壓縮比)
2)LLVM/Clang編譯器:編譯eBPF代碼的時候記錄下relocation相關(guān)的信息
3)Libbpf:基于BTF和編譯器提供的信息,動態(tài)relocate數(shù)據(jù)結(jié)構(gòu)
其中BTF為重要組成部分,Linux Kernel 5.2及以上版本自帶BTF文件,低版本需要手動移植。通過分析內(nèi)核源碼,可以發(fā)現(xiàn)BTF文件的生成并不需要改動內(nèi)核,只依賴:
帶有debug info的vmlinux image
pahole
LLVM
這意味著,我們可以自己為低版本內(nèi)核生產(chǎn)BTF文件,以此讓低內(nèi)核版本支持CORE。
eBPF程序?qū)嵗治?/strong>
eBPF程序會被LLVM編譯為eBPF字節(jié)碼,eBPF字節(jié)碼需要通過eBPF Verifier的(靜態(tài))驗證后,才能真正運行。邊界檢查是eBPF Verifier的重點工作,目的是為了防止eBPF程序內(nèi)存越界訪問。
接下來通過在eBPF程序中簡單的增加、刪減print打印信息觸發(fā)不同原因的幾種邊界檢查異常導(dǎo)致驗證失敗的例子,進一步講解深層的原理。
程序?qū)嶒灜h(huán)境:
1)LLVM 11
2)Linux Kernel 5.8
3)Libbpf commit @9c44c8a
1)內(nèi)存越界:
上述代碼編譯運行后,提示Verifier失敗,然后使用objdump命令來看一下具體的字節(jié)碼,通過以下字節(jié)碼程序,可以看到Verifier失敗的原因在于第14行R6寄存器(變量pos)沒有進行邊界檢查導(dǎo)致。
Root Cause:
當eBPF Verifier走到第14行的時候嘗試去訪問array數(shù)組,但是此時數(shù)組的下標pos是來自bpf_get_smp_processor_id獲取到的unsigned int 類型的動態(tài)變量,此時Verifier無法判斷變量的具體數(shù)值,所以會保守認為可能會達到最大值,這樣的話就會超出array數(shù)組的范圍,造成內(nèi)存越界。
添加邊界檢查代碼
2)Verifier驗證機制和編譯器優(yōu)化機制不一致導(dǎo)致邊界檢查不通過
① 使用錯誤寄存器做邊界檢查:
編譯這個代碼后Verifier驗證通過,可以正常運行。但是此時如果把bpf_printk打印信息刪掉,竟然提示Verifier驗證失敗,原因是R0寄存器(變量pos)沒有通過邊界檢查,但是明明已經(jīng)加了邊界檢查代碼,怎么還會出現(xiàn)問題,這么神奇!
Root Cause:

由于編譯器的優(yōu)化策略,導(dǎo)致刪減bpf_printk后編譯生成的eBPF字節(jié)碼使用寄存器r1(表示pos變量)來進行邊界檢查,但是卻用r0+1(同樣表示pos變量)來訪問數(shù)組array;
相比之下,從eBPF verifier的角度來看,由于在編譯過程中,r1和r0+1的關(guān)聯(lián)性丟失了,導(dǎo)致eBPF verifier無法知道pos變量已經(jīng)通過了檢查,因此錯誤的認為pos變量沒有進行邊界檢查,不允許程序運行;
② 寄存器溢出或重新加載后,狀態(tài)丟失:
在上述邊界檢查代碼中添加一段print調(diào)試打印信息后編譯驗證又會出現(xiàn)Verifier失敗,通過排查發(fā)現(xiàn)不是已知的兩類問題,依然使用objdump查看添加后的字節(jié)碼信息。
Root Cause:

加入bpf_printk后通過字節(jié)碼可以看到,代碼先使用R0(表示pos變量)進行邊界檢查。由于當前寄存器數(shù)量不足,編譯器決定將將R0臨時保存到棧上的空間(R10-16,在eBPF字節(jié)碼中,R10存儲存放著 eBPF ??臻g的棧幀指針的地址),這樣R0就可以空閑出來,留給其他代碼使用,我們稱這種行為為寄存器溢出(register spill);
當真正需要使用pos變量的時候,編譯器會從棧上(R10-16)將之前保存的內(nèi)容取出來賦給R1(也表示pos變量),然后使用R1對數(shù)組array進行訪問。但神奇的是,當寄存器溢出發(fā)生時,pos變量的狀態(tài)丟失了,eBPF忘記了該變量曾經(jīng)進行了邊界檢查,導(dǎo)致程序無法通過驗證;
解決方案:
在源碼中加入 &= 操作符,引導(dǎo)編譯器生成理想的eBPF字節(jié)碼
array[pos?&=?MAX_SIZE?-?1]?=?1;
如果上述方法失效,無法引導(dǎo)編譯器,那么針對出錯的部分源代碼人工編寫eBPF字節(jié)碼,替代編譯器生成的字節(jié)碼
總結(jié)
eBPF 作為 Linux 內(nèi)核一項革命性的技術(shù),起源于 Linux 內(nèi)核,該技術(shù)可以安全而高效地拓展內(nèi)核的能力,但快速發(fā)展的同時,也會存在很多新鮮出爐的問題,給廣大開發(fā)者尤其是入門者帶來個很大的困擾,本文從幾個實例的角度來對問題進行分析和解答,有相關(guān)開發(fā)疑惑的同學(xué)可以參考借鑒。
本文使用 文章同步助手 同步