03 - eBPF:核心可程式化的雲原生基石¶
這是整個學習計畫最進階的章節。閱讀本章前,請確認你已具備: - Linux 基礎(行程 (Process)、系統呼叫 (System Call)、檔案描述符 (File Descriptor)) - 容器 (Container) 基礎(命名空間 (Namespace)、控制群組 (cgroup)) - Kubernetes (K8s) 基礎(Pod、Service、網路策略 (NetworkPolicy)、CNI)
本章以建立觀念為主軸,但同時提供清晰的實作路徑,讓你能從「會用工具」一路走到「會寫程式」。
目錄¶
1. 為什麼需要 eBPF¶
1.1 傳統的困境:想在核心做事,代價很高¶
作業系統分成兩個世界:使用者空間 (User Space) 與 核心空間 (Kernel Space)。我們平常寫的程式(包含容器裡跑的應用程式)都活在使用者空間,看不到、也碰不到核心內部正在發生的事——封包怎麼轉送、行程怎麼排程、檔案怎麼開啟。
如果你想在核心 (Kernel) 層級做事(例如攔截每一個網路封包、監看每一次檔案開啟),傳統上你只有兩條路,而且都很痛:
| 做法 | 優點 | 致命缺點 |
|---|---|---|
| 寫核心模組 (Kernel Module) | 功能完整、效能高 | 一個 bug 就讓整台機器當機 (Kernel Panic);每次核心升級可能要重編譯;難維護、難審查 |
| 改核心原始碼後重新編譯 | 完全客製 | 要說服整個 Linux 社群接受你的修改,曠日廢時(常以「年」為單位) |
| 在使用者空間用既有介面 | 安全 | 資料要在核心與使用者空間之間反覆複製,效能差、且只能拿到核心「願意給」的有限資訊 |
簡單說:核心很強大,但傳統上「對外封閉」。 想擴充它的行為,要嘛冒著當機風險,要嘛慢得令人絕望。
1.2 eBPF 的破局:核心裡的沙箱¶
eBPF (extended Berkeley Packet Filter) 徹底改變了這個局面。它的核心理念是:
讓你在「不改核心原始碼、不裝核心模組」的前提下,安全地把自己的程式「注入」到核心內部執行。(官方定義見 ebpf.io:What is eBPF?)
1.3 最好的比喻:核心裡的 JavaScript¶
理解 eBPF 最快的方式,是拿瀏覽器與 JavaScript 來類比:
| 瀏覽器世界 | eBPF 世界 | 共通概念 |
|---|---|---|
| 瀏覽器引擎 (Browser Engine) | Linux 核心 (Kernel) | 龐大、不能隨便改的執行環境 |
| JavaScript | eBPF 程式 | 你寫的、可動態載入的小程式 |
| 網頁事件(點擊、載入) | 核心事件(收封包、開檔案、系統呼叫) | 事件觸發 (Event-driven) |
| JS 沙箱 (Sandbox) | eBPF 驗證器 (Verifier) + 沙箱 | 保證注入的程式不會搞垮宿主 |
就像你不需要為了讓網頁互動而「重新編譯瀏覽器」,你只要寫一段 JavaScript 掛上事件即可;eBPF 讓你不需要為了擴充核心行為而「重新編譯核心」,你只要寫一段 eBPF 程式掛上掛載點 (Hook Point) 即可。
而且 eBPF 程式是安全的:在它被允許執行前,核心裡的驗證器 (Verifier) 會嚴格審查它(下一節詳述),確保它不會無窮迴圈、不會存取非法記憶體、不會搞垮系統。這正是 eBPF 與傳統核心模組最大的差異——核心模組信任你,eBPF 不信任你,所以它先驗證你。(詳見核心官方文件 eBPF Verifier)
1.4 為什麼這在雲原生 (Cloud Native) 如此重要¶
雲原生環境的特徵是:高度動態、規模龐大、多租戶、強調可觀測性與零信任安全。
- 一台節點上可能跑著數十甚至上百個 Pod,網路連線瞬息萬變——傳統
iptables規則會膨脹到數萬條,效能崩潰。 - 安全團隊需要即時看到「哪個容器執行了什麼系統呼叫」,但又不能為了監控而拖垮應用程式。
- 平台團隊想要無侵入式 (Zero-instrumentation) 的可觀測性:不改一行應用程式碼,就能拿到 HTTP / gRPC / SQL 層級的指標。
eBPF 剛好同時滿足這三點:它在核心內運作,所以看得到一切;它是事件驅動且 JIT 編譯,所以夠快;它有驗證器把關,所以夠安全。 這就是為什麼 Cilium、Falco、Pixie、Tetragon 等雲原生明星專案,底層全都是 eBPF。
動手練習 1:用一句話向同事解釋「為什麼不直接寫核心模組就好」。提示:從「當機風險」與「維護成本」兩個角度切入。
2. eBPF 運作原理¶
2.1 全貌:從原始碼到核心內執行¶
flowchart TD
A["eBPF 原始碼<br/>(C 語言)"] -->|"clang/LLVM 編譯"| B["eBPF Bytecode<br/>(位元組碼)"]
B -->|"bpf() 系統呼叫載入"| C{"驗證器 (Verifier)<br/>安全檢查"}
C -->|"不通過 → 拒絕載入"| X["錯誤回傳"]
C -->|"通過"| D["JIT 編譯器<br/>(Just-In-Time)"]
D --> E["原生機器碼<br/>(Native Code)"]
E -->|"附掛到"| F["掛載點 (Hook Point)<br/>kprobe / tracepoint / XDP ..."]
F -->|"事件觸發時執行"| G["eBPF 程式運行"]
G <-->|"讀寫資料 / 與使用者空間溝通"| H["映射 (Maps)"]
H <--> I["使用者空間程式<br/>(loader / agent)"]
整個生命週期:寫 C → 編成位元組碼 → 經 bpf() 系統呼叫載入 → 驗證器審查 → JIT 編成原生機器碼 → 掛到掛載點 → 事件發生時執行 → 透過映射與使用者空間交換資料。 這個流程是 eBPF 子系統的官方總覽,完整定義可參見 Linux 核心 BPF 文件首頁。
2.2 程式類型 (Program Types)¶
eBPF 程式不是萬用的——你寫的程式必須宣告屬於哪一種類型,類型決定了:它能掛在哪種掛載點、能呼叫哪些輔助函式 (Helper Functions)、能拿到什麼樣的上下文 (Context)。完整的程式類型清單與其對應的掛載介面,可見 Linux 核心文件:Program Types and ELF Sections 與 eBPF Docs 的 Program Types 索引。
| 程式類型 | 典型用途 | 上下文 (Context) |
|---|---|---|
BPF_PROG_TYPE_KPROBE |
動態追蹤核心函式 | 暫存器狀態 (pt_regs) |
BPF_PROG_TYPE_TRACEPOINT |
追蹤核心靜態追蹤點 (Tracepoint) | 追蹤點專屬結構 |
BPF_PROG_TYPE_XDP |
最高速封包處理(網卡驅動層) | xdp_md(封包指標) |
BPF_PROG_TYPE_SCHED_CLS |
tc 流量控制 / 整形 | __sk_buff(socket buffer) |
BPF_PROG_TYPE_CGROUP_SKB |
cgroup 層級網路過濾 | __sk_buff |
BPF_PROG_TYPE_SOCKET_FILTER |
socket 層封包過濾 | __sk_buff |
BPF_PROG_TYPE_PERF_EVENT |
效能事件取樣(profiling) | 效能事件資料 |
2.3 映射 (Maps):eBPF 的記憶體與通訊管道¶
eBPF 程式本身是無狀態、短命的——每次事件觸發、跑完就結束。那狀態存哪裡?跨次事件如何累積資料?核心內的程式如何把結果送回使用者空間?
答案是 映射 (Maps)。映射是核心管理的鍵值資料結構 (Key-Value Store),同時被核心內的 eBPF 程式與使用者空間程式存取,扮演兩個世界之間的橋樑。
常見的映射類型(完整類型清單見 核心文件:BPF maps):
| 映射類型 | 用途 |
|---|---|
BPF_MAP_TYPE_HASH |
雜湊表,任意鍵值查找(如:依 PID 累計次數)。核心文件:HASH map |
BPF_MAP_TYPE_ARRAY |
陣列,索引固定 |
BPF_MAP_TYPE_PERCPU_HASH / PERCPU_ARRAY |
每 CPU 各自一份獨立的值,讀寫不需加鎖、避免跨 CPU 競爭,效能極高。核心文件:PERCPU_HASH、核心文件:PERCPU_ARRAY |
BPF_MAP_TYPE_PERF_EVENT_ARRAY |
把事件串流送往使用者空間(舊式,per-CPU 緩衝,可能有跨 CPU 事件順序錯亂與記憶體使用率較差的問題) |
BPF_MAP_TYPE_RINGBUF |
環形緩衝區 (Ring Buffer),核心 5.8 引入,多生產者單消費者 (MPSC),解決了 PERF_EVENT_ARRAY 的記憶體效率與跨 CPU 事件排序問題,是現代主流的事件串流方式。核心文件:BPF ring buffer |
BPF_MAP_TYPE_LRU_HASH |
雜湊表容量滿時自動淘汰最久未用 (Least Recently Used) 的項目。核心文件:HASH map(含 LRU 變體) |
2.4 驗證器 (Verifier):安全的守門員¶
這是 eBPF「安全」的關鍵。當你透過 bpf() 系統呼叫載入程式時,驗證器 (Verifier) 會在程式真正執行之前,以靜態分析窮舉所有可能的執行路徑,確保(詳細規則見核心官方文件 eBPF Verifier):
- 一定會結束:控制流程圖 (CFG) 不能含有迴圈,確保程式必定終止;核心 5.3 起放寬為允許有界迴圈 (Bounded Loops)——驗證器會展開模擬每一輪迭代的狀態,確認迴圈一定會在有限步驟內結束(LWN:Bounded loops in BPF for the 5.3 kernel)。
- 不會非法存取記憶體:每次解參考指標前,必須先檢查邊界(這就是為什麼你常被迫寫
if (ptr + 1 > data_end) return;)。 - 不會洩漏核心記憶體:在非特權模式下,不允許對指標做可能外洩核心位址的指標運算。
- 只用允許的輔助函式:依程式類型限制可呼叫的 helper,且若 helper 被標記為
gpl_only,程式的LICENSE必須宣告為 GPL 相容授權,否則載入會被拒絕(核心文件:BPF licensing)。 - 指令數量與複雜度受限:避免拖垮核心。核心 5.2 之前硬上限是 4096 條指令且複雜度上限 128K;5.2 之後的變更只放寬了特權 (root) 程式——複雜度上限改為單純的
BPF_COMPLEXITY_LIMIT_INSNS,約 100 萬條指令等級,此時純粹看驗證器能否在合理時間內窮舉完所有路徑。但非特權程式的 4096 條指令上限至今仍然存在,並未被移除(見 Linux 核心文件:BPF Design Q&A 中「BPF_MAXINSNS (4096)... the maximum number of instructions that the unprivileged bpf program can have」;變更歷史見 Linux 核心 commit:bpf: increase complexity limit and maximum program size)。
心法:驗證器不是你的敵人,是你的安全帶。 初學時被它擋下會很挫折,但它擋下的每一個錯誤,在傳統核心模組裡都可能是一次 Kernel Panic。
但安全帶也會有瑕疵:驗證器本身是複雜的靜態分析程式碼,一樣可能出錯。例如 CVE-2026-31413 就是驗證器對
BPF_OR常數運算元的純量分析錯誤(maybe_fork_scalars()誤用了BPF_AND的推導邏輯),導致驗證器認定的值與執行期實際值不一致,可被利用做越界的 map 存取(CVSS 7.8;CVE 官方記錄(NVD)、核心修補 commit)。這類問題通常需要載入非特權 eBPF 程式的能力才能觸發,也是許多 K8s 節點預設停用非特權 BPF(kernel.unprivileged_bpf_disabled=1,見 核心文件:BPF Design Q&A)或限制CAP_BPF授予對象的原因。
2.5 JIT 編譯:跑得跟原生一樣快¶
通過驗證後,JIT (Just-In-Time) 編譯器 會把與架構無關的 eBPF 位元組碼,翻譯成當下 CPU 的原生機器碼 (Native Machine Code)。eBPF 的暫存器與指令格式刻意設計成與現代 CPU(x86-64、ARM64 等)的暫存器/呼叫慣例相近,讓 JIT 多半能做到指令一對一映射,目前官方支援 x86-64/x86-32、arm64、arm32、ppc64/ppc32、s390x、mips64/mips32、sparc64、riscv64/riscv32、loongarch64、parisc(32/64 位元,核心 6.6 起新增)、arc(ARCv2)等架構——ppc32 與 mips32 也各自擁有獨立的 eBPF JIT 實作(而非僅有較舊的 classic BPF JIT),分別可見核心原始碼 arch/powerpc/net/bpf_jit_comp32.c、arch/mips/net/bpf_jit_comp32.c、arch/parisc/net/;arc 的 eBPF JIT 支援可見 arch/arc/net/ 及 Kconfig 中 select HAVE_EBPF_JIT if ISA_ARCV2,官方架構支援表見核心原始碼 Documentation/features/core/eBPF-JIT/arch-support.txt;相對地 sparc32 至今仍只有 classic BPF JIT、未支援 eBPF,故未列入。核心原始碼各架構下的 bpf_jit_comp*.c 為權威來源;概念說明見核心文件:Classic BPF vs eBPF。所以 eBPF 程式雖然是「動態載入的腳本」,執行效能卻接近原生編譯的核心程式碼,沒有直譯器的開銷。
2.6 掛載點 (Hook Points):程式掛在哪裡¶
eBPF 程式要「綁」到某個事件來源才能被觸發。常見掛載點:
| 掛載點 | 觸發時機 | 領域 |
|---|---|---|
| kprobe / kretprobe | 進入 / 離開任一核心函式時 | 追蹤(動態) |
| uprobe / uretprobe | 進入 / 離開使用者空間函式時(如追 libc、追 Go 函式) | 追蹤(動態) |
| tracepoint | 核心預先埋好的靜態追蹤點(穩定、跨版本) | 追蹤(靜態) |
| XDP (eXpress Data Path) | 封包經 DMA 進記憶體後、核心配置 sk_buff 與進入網路堆疊之前;只處理 ingress(進站)方向 |
網路(最高速) |
| tc (Traffic Control) | 核心網路堆疊的流量控制層,此時封包已有完整 sk_buff,可看到更豐富的協定中介資訊;支援 ingress 與 egress(出站)雙向 |
網路 |
| cgroup hooks | 某個 cgroup 的行程做網路 / socket 操作時 | 網路 / 安全 |
| LSM (Linux Security Module) | 核心安全決策點,程式回傳值可直接決定該操作被允許還是以 -EPERM 拒絕(核心 5.7 引入,需 CONFIG_BPF_LSM=y) |
安全 |
kprobe vs tracepoint 怎麼選? kprobe 能掛任何核心函式,彈性最大,但函式名稱可能隨核心版本改變(不穩定);tracepoint 是核心刻意提供的穩定介面,跨版本可靠,優先用 tracepoint,沒有合適的再退而用 kprobe。
XDP vs tc 怎麼選? XDP 掛在驅動層、早於
sk_buff配置,延遲最低、最適合「越早丟棄惡意/不需要的封包越好」的場景(如 DDoS 防護),但只能處理 ingress;tc 掛在網路堆疊內,雖然延遲略高,卻能看到完整封包中介資訊、可串接多個分類器、支援 egress,適合更複雜的策略控制。tc 的現代附掛方式:tcx。 傳統 tc BPF 程式要透過 netlink 掛在
clsactqdisc 上,多個程式共存時的執行順序與卸載時機不易管理。核心 6.6 引入的 tcx,提供以bpf_link為基礎的輕量多程式附掛機制:支援用BPF_F_BEFORE/BPF_F_AFTER明確指定執行順序、行程結束或 fd 關閉時自動卸載,不必再操作 qdisc。傳統tc/classifier/action附掛型態已被標示為過時,tcx 是目前建議的做法,Cilium 等專案已預設偵測核心支援後改用 tcx。詳見 eBPF Docs:BPF_PROG_TYPE_SCHED_CLS。動手練習 2:畫出「一個封包從網卡到應用程式」的路徑,並標出 XDP 與 tc 分別攔截在哪一段。思考:為什麼 DDoS 防護要用 XDP 而不是 tc?(提示:越早丟棄惡意封包,浪費的 CPU 越少。)
3. eBPF 的三大應用領域¶
eBPF 的應用千變萬化,但收斂起來就是三大支柱(這個分類方式也是 ebpf.io 官方對 eBPF 應用領域的劃分):
mindmap
root((eBPF))
可觀測性 Observability
無侵入式追蹤
效能分析 Profiling
指標 / Tracing
工具:bcc / bpftrace / Pixie
網路 Networking
高速封包處理 XDP
負載平衡
取代 kube-proxy
工具:Cilium / Katran
安全 Security
系統呼叫監控
執行期偵測
LSM 強制策略
工具:Falco / Tetragon
3.1 可觀測性 (Observability)¶
eBPF 最成熟、最容易上手的領域。因為程式直接跑在核心內,它能無侵入 (Zero-instrumentation) 地看到一切:每一次系統呼叫、每一個封包、每一次函式呼叫——完全不需要改應用程式碼。
- 抓出「誰在 fork 一堆短命行程」、「誰在狂開檔案」、「哪個連線延遲爆高」。
- 持續效能剖析 (Continuous Profiling):用極低開銷取樣 CPU 堆疊,找出熱點。
3.2 網路 (Networking)¶
eBPF 在網路領域是顛覆性的。透過 XDP 與 tc 掛載點,可以在封包處理的最早期就做轉送、過濾、負載平衡——速度遠超傳統 iptables。Cilium、Facebook 的 Katran(L4 負載平衡器)都建立在此之上(詳見第 5 節)。
3.3 安全 (Security)¶
eBPF 能即時觀察核心層的安全事件(誰執行了什麼程式、開了什麼檔、發了什麼連線),甚至透過 LSM hook 直接允許或拒絕某個操作。相較傳統稽核 (auditd),eBPF 的開銷低、可程式化、上下文更豐富。Falco、Tetragon 是代表作。
4. 學習路徑(由淺入深)¶
學 eBPF 最忌諱一開始就埋頭寫 C。正確的順序是:先當「使用者」用工具建立直覺 → 再當「開發者」寫程式。
flowchart LR
A["第 1 階段<br/>用 bpftrace 單行程式"] --> B["第 2 階段<br/>用 bcc 工具集"]
B --> C["第 3 階段<br/>libbpf + CO-RE 寫 C"]
C --> D["第 4 階段<br/>cilium/ebpf 寫 Go"]
style A fill:#d4f4dd
style B fill:#d4f4dd
style C fill:#fff3cd
style D fill:#fff3cd
4.1 第 1 階段:用 bpftrace 建立直覺¶
bpftrace 是一套高階追蹤語言(語法像 awk),讓你用一行就完成原本要寫一大段 C 的追蹤工作,內部以 LLVM 把腳本編譯成 eBPF 位元組碼。最適合探索與建立直覺。
# 安裝(以 Ubuntu/Debian 為例)
sudo apt-get update && sudo apt-get install -y bpftrace
# 範例 1:列出所有 tracepoint 與 kprobe(看看有哪些掛載點可用)
sudo bpftrace -l 'tracepoint:syscalls:*' | head
# 範例 2:統計每個行程觸發了幾次 execve(誰在狂開新程式?)
sudo bpftrace -e 'tracepoint:syscalls:sys_enter_execve { @[comm] = count(); }'
# 範例 3:每秒印出系統呼叫總數(觀察系統負載)
sudo bpftrace -e 'tracepoint:raw_syscalls:sys_enter { @ = count(); }
interval:s:1 { print(@); clear(@); }'
# 範例 4:畫出 read() 回傳大小的直方圖(資料分佈一目了然)
sudo bpftrace -e 'tracepoint:syscalls:sys_exit_read { @bytes = hist(args->ret); }'
動手練習 3:用 bpftrace 寫一行程式,統計每個指令名稱 (comm) 各自開啟了幾次檔案(提示:掛
tracepoint:syscalls:sys_enter_openat,用@[comm] = count())。
4.2 第 2 階段:用 bcc 工具集¶
bcc (BPF Compiler Collection) 內附數十支「即裝即用」的生產級工具。每支都是一個 eBPF 程式的完整範例,用法本身就是最好的教材。
# 安裝
sudo apt-get install -y bpfcc-tools linux-headers-$(uname -r)
# 工具通常以 -bpfcc 結尾(Ubuntu 套件命名)
sudo execsnoop-bpfcc # 即時顯示每一個被執行的新程式(含完整指令列)
sudo opensnoop-bpfcc # 即時顯示每一次檔案開啟(誰開了什麼檔)
sudo tcpconnect-bpfcc # 即時顯示每一個 TCP 主動連線(連到哪去了?)
sudo tcpaccept-bpfcc # 即時顯示每一個 TCP 被動接受的連線
sudo biolatency-bpfcc # 區塊裝置 I/O 延遲直方圖
sudo runqlat-bpfcc # 排程器執行佇列延遲(CPU 爭用程度)
這些工具的對應關係,正好涵蓋三大領域:
| 工具 | 看到什麼 | 領域 |
|---|---|---|
execsnoop |
誰執行了什麼程式 | 安全 / 觀測 |
opensnoop |
誰開了什麼檔 | 安全 / 觀測 |
tcpconnect |
誰連到哪裡 | 網路 / 安全 |
biolatency |
磁碟慢不慢 | 觀測 / 效能 |
動手練習 4:開兩個終端機。一個跑
sudo execsnoop-bpfcc,另一個隨意執行幾個指令(如ls、date)。觀察 execsnoop 即時捕捉到的輸出,理解「核心事件即時可見」的威力。
4.3 第 3 階段:寫程式 — libbpf + CO-RE(現代主流)¶
當工具滿足不了需求,就得自己寫。現代寫法的關鍵字是 CO-RE (Compile Once - Run Everywhere,一次編譯、到處執行)。
為什麼需要 CO-RE? 早期 bcc 的做法是在目標機器上即時編譯(把 C 原始碼字串內嵌進工具、執行期呼叫 Clang/LLVM)——所以每台機器都要裝完整編譯器工具鏈與核心標頭檔,部署笨重、編譯期耗資源、且編譯錯誤要到執行期才會發現。CO-RE 改變了這點(參考:Andrii Nakryiko, BPF CO-RE reference guide):
- 編譯時 Clang 會把「我要存取
task_struct的pid欄位」這類意圖記錄成 BTF (BPF Type Format) 重定位資訊,而非寫死欄位偏移量。BTF 本身由核心 4.18 引入(核心文件:BPF Type Format)。 - 載入時 libbpf 讀取目標機器當前核心的 BTF(
/sys/kernel/btf/vmlinux),比對並重新計算實際欄位偏移,在載入前動態調整存取位址——即使目標核心的結構體佈局與編譯時不同也能正確運作。 - 結果:一個編譯好的
.o檔,可以搬到不同核心版本的機器上直接跑,目標機器不需要編譯器、不需要核心標頭檔。
這就是今天 Cilium、Tetragon、Pixie 等主流專案採用的方式。
下面是一個最小可運行的 libbpf + CO-RE 骨架,功能:追蹤每一次 execve 並印出 PID 與指令名稱。
核心側程式 minimal.bpf.c(在核心內執行):
// minimal.bpf.c — 在核心空間執行的 eBPF 程式
#include "vmlinux.h" // 由 BTF 產生,含所有核心型別定義
#include <bpf/bpf_helpers.h>
char LICENSE[] SEC("license") = "GPL"; // 必須宣告授權,否則無法用 GPL helper
// 定義一個環形緩衝區 (Ring Buffer) 映射,用來把事件送回使用者空間
struct {
__uint(type, BPF_MAP_TYPE_RINGBUF);
__uint(max_entries, 256 * 1024); // 緩衝區大小:256 KB
} events SEC(".maps");
// 事件資料結構
struct event {
int pid;
char comm[16];
};
// 掛到 execve 系統呼叫的進入點 (tracepoint)
SEC("tracepoint/syscalls/sys_enter_execve")
int handle_execve(void *ctx)
{
// 從環形緩衝區預留一塊空間
struct event *e = bpf_ringbuf_reserve(&events, sizeof(*e), 0);
if (!e)
return 0; // 預留失敗就放棄(緩衝區滿了)
// bpf_get_current_pid_tgid() 回傳 64 位元值:高 32 位是 TGID(即一般認知的 PID),低 32 位是個別執行緒 ID
// 參考:https://docs.ebpf.io/linux/helper-function/bpf_get_current_pid_tgid/
e->pid = bpf_get_current_pid_tgid() >> 32;
bpf_get_current_comm(&e->comm, sizeof(e->comm));
bpf_ringbuf_submit(e, 0); // 提交事件給使用者空間
return 0;
}
使用者側載入器(概念流程,以 libbpf 為例):
// minimal.c — 使用者空間載入器(關鍵步驟)
// 1. minimal_bpf__open() 開啟編譯好的 eBPF 物件
// 2. minimal_bpf__load() 載入核心(此時驗證器會審查)
// 3. minimal_bpf__attach() 附掛到 tracepoint
// 4. ring_buffer__poll() 輪詢環形緩衝區、處理事件
// (實務上 .bpf.c 會用 bpftool 產生 skeleton 標頭檔,大幅簡化以上樣板)
典型建置流程(完整流程說明見 核心文件:libbpf Overview):
# 1. 產生 vmlinux.h(從當前核心的 BTF 萃取所有型別)
bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h
# 2. 用 clang 把 .bpf.c 編成 eBPF 物件檔(注意 -target bpf)
clang -O2 -g -target bpf -c minimal.bpf.c -o minimal.bpf.o
# 3. 產生 skeleton 標頭(讓使用者側程式好寫)
bpftool gen skeleton minimal.bpf.o > minimal.skel.h
# 4. 編譯使用者側並連結 libbpf,即可執行
強烈建議直接從官方範本起步:libbpf/libbpf-bootstrap 把上面所有樣板都準備好了。
4.4 第 4 階段:用 Go 寫 — cilium/ebpf¶
如果你的世界是雲原生(K8s 控制器、Operator、agent 多半用 Go),那麼 cilium/ebpf(官方文件站 ebpf-go.dev)是純 Go 實作、無 CGO 依賴的主流函式庫,由 Cilium 與 Cloudflare 共同維護,Cilium 本身的 Go 程式碼也使用它來載入與附掛 eBPF 程式。
// main.go — 用 cilium/ebpf 載入並附掛 eBPF 程式(精簡骨架)
package main
import (
"log"
"os"
"os/signal"
"github.com/cilium/ebpf/link"
"github.com/cilium/ebpf/ringbuf"
"github.com/cilium/ebpf/rlimit"
)
// 用 go:generate 搭配 bpf2go,把 minimal.bpf.c 編譯並產生 Go 綁定
//go:generate go run github.com/cilium/ebpf/cmd/bpf2go bpf minimal.bpf.c
func main() {
// 移除 MEMLOCK 上限(舊核心 < 5.11 需要;5.11+ 已改用 cgroup 記憶體核算,此呼叫會是 no-op)
// 參考:https://ebpf-go.dev/concepts/rlimit/
if err := rlimit.RemoveMemlock(); err != nil {
log.Fatal(err)
}
// 載入 bpf2go 產生的 eBPF 物件(objs 含 program 與 map)
objs := bpfObjects{}
if err := loadBpfObjects(&objs, nil); err != nil {
log.Fatalf("載入 eBPF 物件失敗: %v", err)
}
defer objs.Close()
// 把 eBPF 程式附掛到 execve tracepoint
tp, err := link.Tracepoint("syscalls", "sys_enter_execve", objs.HandleExecve, nil)
if err != nil {
log.Fatalf("附掛 tracepoint 失敗: %v", err)
}
defer tp.Close()
// 開啟環形緩衝區讀取器,從核心讀回事件
rd, err := ringbuf.NewReader(objs.Events)
if err != nil {
log.Fatalf("開啟 ringbuf 失敗: %v", err)
}
defer rd.Close()
// 收到 Ctrl-C 時優雅結束
stop := make(chan os.Signal, 1)
signal.Notify(stop, os.Interrupt)
log.Println("開始監聽 execve 事件... (按 Ctrl-C 結束)")
go func() {
for {
record, err := rd.Read() // 阻塞讀取下一筆事件
if err != nil {
return
}
log.Printf("收到事件,長度 %d bytes", len(record.RawSample))
// 實務上會把 record.RawSample 解析成事件結構
}
}()
<-stop
log.Println("收到結束訊號,清理中...")
}
動手練習 5:clone
libbpf/libbpf-bootstrap,建置並執行其中的minimal範例。觀察它如何只用幾十行就完成載入、附掛、讀取事件。接著嘗試把追蹤目標從execve改成openat。
5. eBPF 與 Kubernetes¶
這是本章與你雲原生學習主線最關鍵的交會點。
5.1 核心痛點:iptables 撐不住雲原生規模¶
傳統 K8s 預設用 kube-proxy + iptables 模式來實作 Service 的負載平衡與轉送。問題在於:
- iptables 規則是線性比對的鏈結,封包路由的複雜度是
O(N)(N 為規則數)。每新增一個 Service / Endpoint,規則就增加。 - 大型叢集動輒數千個 Service、數萬條規則——實務上規模來到約 5000 個 Service(對應數萬條規則)時效能就會明顯惡化,封包每次轉送都要從頭掃這串長鏈,延遲與 CPU 隨規模線性惡化。
- 規則更新需要整批重載 (atomic replace),在高變動環境下成本高昂。
現況補充:K8s 社群也意識到此問題。
kube-proxy的 nftables 模式已於 1.33 版 GA,用近似O(1)的映射結構解決了同樣的效能問題(Kubernetes 官方部落格:NFTables mode for kube-proxy);不過 iptables 目前仍是上游預設模式。- IPVS 模式的棄用走多版本漸進式時程,依照官方 KEP-5495:1.35 起印出棄用警告(功能仍完整可用)、1.37 引入
KubeProxyIPVSfeature gate(預設true)、1.40 預設翻成false、1.43 才真正移除程式碼(pkg/proxy/ipvs)、1.46 清掉 feature gate。社群建議及早改用 nftables 以避免屆時被迫遷移。留意 AWS 的 EKS 1.35 版本說明截至目前(2026-07)仍寫著「will be removed in Kubernetes 1.36」,而 1.36 已於 2026-04 發布且並未移除 IPVS,此說法已被實際發布時程證偽,與上游 KEP-5495(程式碼要到 1.43 才移除)也不一致——實際時程請一律以上游 KEP-5495 追蹤進度為準,而非 AWS 文件的這句敘述。- 但 nftables/IPVS 都只解決了「Service 轉送」這一項問題,並未涵蓋 eBPF 在身分型網路策略、L7 可視性、無侵入式可觀測性上的能力——這正是 Cilium 等 eBPF 方案除了取代 kube-proxy 之外仍有價值的原因。
flowchart TD
subgraph 傳統["傳統:kube-proxy + iptables"]
P1["封包進入"] --> R["線性掃描<br/>數萬條 iptables 規則 O(n)"] --> T1["轉送到 Pod"]
end
subgraph eBPF["eBPF:Cilium"]
P2["封包進入"] --> H["eBPF 雜湊表查找<br/>O(1)"] --> T2["轉送到 Pod"]
end
eBPF 用雜湊表 (Hash Map) 做查找,複雜度從 O(n) 降到接近 O(1),且更新單一條目不需重載整體。這就是 eBPF 取代 iptables 的根本優勢。
5.2 Cilium:eBPF 在 K8s 的旗艦專案¶
Cilium 已於 2023 年 10 月畢業成為 CNCF Graduated 專案,用 eBPF 全面重寫了 Kubernetes 的網路、安全與可觀測性層。CNCF 官方公告當時稱其為「僅次於 Kubernetes、CNCF commit 數第二活躍的專案」(CNCF 公告)——這是畢業當下(2023 年)的排名快照,非持續更新的即時數據:
| Cilium 能力 | eBPF 怎麼做到 | 取代了什麼 |
|---|---|---|
| CNI(容器網路) | 用 eBPF 在核心內處理 Pod 間封包轉送 | 傳統 bridge/overlay |
| 取代 kube-proxy | 用 eBPF cgroup hook 在 socket 層(connect/sendmsg 等)做服務轉譯,搭配 tc 層的封包級負載平衡 |
kube-proxy + iptables。詳見 Cilium 官方文件:Kubernetes Without kube-proxy |
| 網路策略 (NetworkPolicy) | 在 eBPF 層依身分 (Identity)(由 label 推導出的數值 ID)而非 IP 強制策略,查表方式做策略判斷 | iptables 規則。詳見 Cilium 官方文件:eBPF Datapath 介紹 |
| L7 感知策略 | eBPF 解析 HTTP / gRPC / Kafka,做應用層管控 | 須額外 sidecar |
| 可觀測性 (Hubble) | eBPF 觀測所有網路流並彙整 | 須額外監控堆疊 |
Hubble 是 Cilium 的可觀測性元件,讓你即時看到「哪個 Pod 跟哪個 Pod 講話、用什麼協定、有沒有被策略擋下」——服務地圖一目了然。
版本現況:Cilium 1.20.0 已於 2026 年 7 月發布(超過 2,660 個 commit),Gateway API 支援由 v1.4 提升到 v1.6.1(對應本教材第 1 章 5.3 節談到的 TCPRoute/UDPRoute GA),並支援 Kubernetes v1.36。升級前請注意官方列出的重大變更項目(legacy Mutual Authentication、Envoy Go extensions、
cilium.io/v2alpha1 CiliumNodeConfig等)。目前最新修補版為 Cilium 1.20.1(以 bug fix 與文件修訂為主,未變更 Gateway API / Kubernetes 版本支援範圍)。
# 用 Helm 安裝 Cilium 並啟用「取代 kube-proxy」模式(概念示意)
helm install cilium cilium/cilium --namespace kube-system \
--set kubeProxyReplacement=true \
--set hubble.relay.enabled=true \
--set hubble.ui.enabled=true
# 檢查 Cilium 狀態
cilium status
# 觀察即時網路流(需先 cilium hubble enable)
hubble observe --follow
5.3 其他重要的雲原生 eBPF 專案¶
| 專案 | 領域 | 一句話說明 |
|---|---|---|
| Cilium | 網路 / 安全 / 觀測 | eBPF 版的 CNI 與服務網格,kube-proxy 替代品 |
| Tetragon | 安全 | Cilium 旗下,執行期安全觀測與強制執行 (Enforcement) |
| Falco | 安全 | CNCF 執行期威脅偵測,可用 eBPF 作為事件來源 |
| Pixie | 可觀測性 | 用 eBPF 自動抓 K8s 應用層遙測,免改程式碼 |
| Parca / Pyroscope | 可觀測性 | eBPF 持續效能剖析 (Continuous Profiling) |
動手練習 6:用
kind或minikube建一個本地叢集,安裝 Cilium 並啟用 Hubble UI。部署兩個有互相通訊的 Pod,在 Hubble UI 觀察它們之間的網路流。接著套用一條CiliumNetworkPolicy阻擋其中一個方向,觀察流被擋下的事件。
6. 環境需求¶
6.1 核心版本¶
eBPF 的功能與核心版本強相關。各功能登場的大致里程碑(詳見各功能對應的核心官方文件):
| 核心版本 | 重要里程碑 |
|---|---|
| 3.18 (2014) | eBPF 首次併入主線 |
| 4.x 系列 | kprobe、tracepoint、XDP、Maps 等陸續成熟 |
| 4.18 | BTF 引入,CO-RE 的基礎 |
| 5.2 | 特權程式的指令/複雜度上限放寬至約 100 萬條等級;非特權程式維持 4096 條上限(詳見 2.4 節驗證器規則 5) |
| 5.3 | 有界迴圈 (Bounded Loops) 開放 |
| 5.7 | BPF LSM 引入 |
| 5.8 | CAP_BPF / CAP_PERFMON 權限拆分、Ring Buffer 映射 引入 |
實務建議:做 CO-RE 與現代開發,以核心 5.4+(理想 5.8+)且啟用 BTF(CONFIG_DEBUG_INFO_BTF=y)為基準。(截至 2026 年中,主線核心已進入 7.x 系列——Linux 7.0 於 2026 年 4 月發布;上述 5.4+/5.8+ 只是「CO-RE 可用」的最低基準,新專案沒有理由不用更新的 LTS 核心。)
近期已知的 eBPF 相關核心安全公告(提醒:eBPF 的攻擊面包含驗證器、maps、helper 三處,以下各對應一處): - CVE-2026-63830(CVSS 9.4,Critical,2026-07-19 揭露):
sk_msg的sg.copybitmap 是 scatterlist entry 的「所有權狀態」標記,但 sockmap/TLS 的 transform 路徑在 move、copy、split、compactmsg->sg.data[]entries 時,沒有同步搬動對應的sg.copybit,導致外部(page cache)所擁有的記憶體頁被誤判為可由 BPF 修改,可寫入原本唯讀的 page cache 內容。屬於本節分類中的 maps/helper 攻擊面,是目前這份 CVE 清單中嚴重度最高的一筆;修補見核心 commit406e8a651a7b。 - CVE-2026-64548(CVSS 8.4,2026-07-27 揭露):同樣是 sockmap 攻擊面,bpf_msg_push_data()在 scatterlist ring 空間不足、走進 copy-fallback 配置路徑時,以copy + len計算配置大小——但len是 BPF 程式可完全控制的ARG_ANYTHING參數,且兩者皆為 u32,未做溢位檢查就相加,可構造len使總和整數溢位成一個很小的值,造成配置的緩衝區過小,後續memcpy寫入超出邊界,導致 heap 記憶體毀損。修補是在配置前加一行溢位檢查if (unlikely(copy + len < copy)) return -EINVAL;(核心 commit0c0a8ed85349,"bpf, sockmap: reject overflowing copy + len in bpf_msg_push_data()")。與上面 CVE-2026-63830 同屬 sockmap/scatterlist 機制出錯的案例,只是這次錯在整數溢位而非 bit 同步。 - CVE-2025-39913(CVSS 7.8,2025-10 揭露):同屬 sockmap 攻擊面,比上面兩筆 2026 年案例早了約九個月。tcp_bpf_send_verdict()在為訊息「corking」(透過bpf_msg_cork_bytes()觸發)配置psock->cork失敗時(例如以 fault injection 模擬記憶體不足),沒有呼叫sk_msg_free()回復先前sk_msg_alloc()對sk->sk_forward_alloc所做的變更,導致該sk_msg物件的參照計數與記憶體追蹤狀態不一致,形成 Use-After-Free,可能被本地攻擊者用於權限提升或核心資訊洩漏。與上面 CVE-2026-63830、CVE-2026-64548 同屬 sockmap/cork 生命週期管理出錯的類型,只是這次錯在配置失敗後的清理路徑,而非 scatterlist 的搬移或溢位計算。 - CVE-2026-23359(CVSS 7.8,2026-03-25 揭露):BPF_MAP_TYPE_DEVMAP的 XDP 廣播轉送路徑中,get_upper_ifindexes()在收集裝置的「上層 (upper)」介面索引時未做邊界檢查,呼叫端原本假設上層裝置數量不超過MAX_NEST_DEV(8)並依此配置陣列空間——但當一張裝置疊了超過 8 個上層裝置(例如疊了大量 macvlan 介面)、且掛上帶BPF_F_BROADCAST | BPF_F_EXCLUDE_INGRESSflag 的 XDP 程式時,收到封包觸發轉送就會寫出堆疊陣列邊界,造成 stack out-of-bounds write。修補為get_upper_ifindexes()加上max容量參數,超出時回傳-EOVERFLOW並中止轉送。與下面 CVE-2026-64545 同屬 XDP 導向 (redirect) 路徑出錯的案例,只是這次錯在邊界檢查而非 NULL 檢查(且觸發需要CAP_BPF+CAP_NET_ADMIN,一般僅 root 可達,風險相對較低)。 - CVE-2026-43030(CVSS 7.8,2026-05-01 揭露):驗證器的regsafe()在比對指向封包 (packet) 的指標狀態時,當舊狀態rold->reg->range == BEYOND_PKT_END而目前狀態rcur->reg->range == N時可能誤判為安全,導致某些其實帶有有效封包範圍、理應被探索的狀態被跳過驗證。屬於驗證器的狀態剪枝 (state pruning) 邏輯出錯。 - CVE-2026-43070(CVSS 7.8,2026-05-05 揭露):BPF_END(位元組序轉換)指令會就地改變暫存器的純量值,但驗證器沒有同步重置該暫存器的id,導致後續以 ID 為基礎的邊界追蹤 (range tracking) 誤把兩個暫存器視為同源,可能被利用構造越界記憶體存取。屬於驗證器純量追蹤出錯的案例。 - CVE-2026-43321(CVSS 7.8,2026-05-08 揭露):compute_insn_live_regs()在計算 live register 時,漏掉標記gotox rX(間接跳躍)指令實際用到的暫存器rX,導致驗證器對該暫存器的存活分析不完整。屬於驗證器的存活分析 (liveness analysis) 出錯。 - CVE-2026-45838(CVSS 5.5,2026-05-27 揭露):cgroup_storage_get_next_key()用list_next_entry()判斷是否走到 list 尾端,但該巨集在最後一個元素時並不會回傳NULL,而是透過container_of()繞回 list head,導致原本的NULL檢查永遠不會命中,使函式把 list head 誤當成合法的 map entry 讀取,將核心內部的struct欄位(而非真正的storage->key)外洩給使用者空間。屬於本節分類中的 maps/helper 攻擊面。 - CVE-2026-52910(CVSS 6.4,2026-06-19 揭露):SO_REUSEPORT的 classic BPF(cBPF)選核程式與 eBPF 選核程式,在移除路徑上被同一份程式碼處理,但兩者生命週期管理方式不同——eBPF prog 透過bpf_prog_put()走參照計數、於 RCU grace period 後才真正釋放,cBPF prog 卻在sk_reuseport_prog_free()中被立即釋放。當另一顆 CPU 正在reuseport_select_sock()內、於 RCU read section 中讀取同一份 cBPF 程式做封包選核時,程式已被釋放,形成 Use-After-Free(fuzzing 以 KASAN 抓到 vmalloc-out-of-bounds 讀取)。修補改為 cBPF prog 比照 eBPF 走法,等 RCU grace period 後才釋放。屬於本節分類中的 maps/helper 生命週期管理類型,與上面 CVE-2025-39913 同屬 reuseport/cork 物件釋放時機出錯的案例。 - CVE-2026-53031(CVSS 7.8,2026-06-24 揭露):BPF arena(見 2.5 節相關概念)的arena_alloc_pages()直接收下一個未經檢查的純int node_id,並沿整條記憶體配置鏈往下傳遞,完全沒有邊界檢查。屬於本節分類中的 maps/helper 攻擊面。 - CVE-2026-53033(CVSS 6.4,Ubuntu priority High,2026-06-24 揭露):unix_stream_bpf_update_proto()中,sockmap 附掛在 AF_UNIX socket 上的 iterator 程式所讀取的peer指標,在該 socket 從TCP_ESTABLISHED轉換到TCP_CLOSE的過程中可能變成過期指標,形成 Use-After-Free。屬於 maps/helper 攻擊面,與前述 sockmap 系列問題(CVE-2026-63830、CVE-2026-64548)同屬 sockmap 生命週期管理類型。 - CVE-2026-53036(CVSS 7.8,2026-06-24 揭露):arm64 平台的 BPF JIT 編譯器,check_imm巨集用來驗證分支位移 (branch displacement) 是否落在合法的有號 N 位元範圍內時有差一 (off-by-one) 錯誤,可能讓超出範圍的值通過檢查,經由位元遮罩效應把原本的前向分支 (forward branch) 變成後向分支 (backward branch),影響B.cond、CBZ/CBNZ等指令的編碼正確性。屬於 JIT 編譯器層級的邊界檢查出錯,不同於前述多屬驗證器或 maps/helper 的案例。 - CVE-2026-53081(CVSS 6.4,Ubuntu priority High,2026-06-24 揭露):驗證器的狀態剪枝機制在比對兩個帶有BPF_ADD_CONST標記的純量暫存器時,沒有確認雙方的 base id 是否一致——例如舊狀態的R3 = R2 + 10(base id A)與目前狀態的R3 = R4 + 10(base id C)彼此無關,卻可能被誤判為等價狀態而剪枝放行。屬於驗證器狀態剪枝出錯,與前述 CVE-2026-43030 同屬此類但錯在不同暫存器追蹤機制。 - CVE-2026-53085(CVSS 6.4,Ubuntu priority High,2026-06-24 揭露):開放編碼 (open-coded) 的task_vmaiterator 在讀取任務的記憶體映射前,沒有正確取得mm_struct的參照計數(該結構未標記SLAB_TYPESAFE_BY_RCU),當任務並發結束 (exit) 時可能被提前釋放,形成 Use-After-Free。屬於 maps/helper(iterator)攻擊面。 - CVE-2026-53090(CVSS 6.4,Ubuntu priority High,2026-06-24 揭露):驗證器對 subprogram 內的ld_abs/ld_ind(直接讀取封包資料)指令,只模擬了成功路徑,沒有同時模擬封包資料讀取失敗時、程式會直接回傳 0 給呼叫端的失敗路徑,導致該路徑的狀態分析不完整。屬於驗證器路徑分析出錯。上述 CVE-2026-53031/53033/53036/53081/53085/53090 與下面的 CVE-2026-53095 同屬 2026-06-24 同一批公告的核心 BPF 修補,建議一併留意。 - CVE-2026-64036(CVSS 7.8):
css_rstat_updated()這個 BPF kfunc 沒有驗證呼叫端傳入的 CPU 編號合法性,持有CAP_BPF+CAP_PERFMON即可觸發越界的 per-CPU 記憶體存取;已在 6.18.34 / 7.0.11 / 7.1 等版本修補。這正是 6.2 節「CAP_BPF拆分權限」立意雖好,但拆出的能力組合仍可能被濫用的實例。 - CVE-2026-64192:在CONFIG_BPF_LSM=y但 BPF LSM 未於開機時初始化的系統上,建立BPF_MAP_TYPE_INODE_STORAGEmap 會讓 inode 安全 blob 的偏移量停留在錯誤的預設值,導致記憶體別名進而破壞 RCU 回呼指標,清理階段觸發 Kernel Panic;修補後改為在建立當下直接拒絕該 map 類型(影響 5.10 至 7.1.4,修補見 7.2-rc2 起)。 - CVE-2026-64545(CVSS 7.5,2026-07-27 揭露):XDP 導向函式xdp_master_redirect()(net/core/filter.c)在接收裝置沒有 upper-master adjacency 時缺少 NULL 檢查,當 bond slave 裝置在釋放過程中收到XDP_TX流量,就可能觸發 NULL pointer dereference 造成 Kernel Panic(阻斷服務);與 2.6 節、3.2 節討論的 XDP 高速封包處理路徑直接相關。修補見核心 commite82d8cc4321c。 - CVE-2026-63864(CVSS 8.4,2026-07-20 揭露):驗證器的visit_tailcall_insn()忽略了自己的錯誤回傳值,導致 tail call 指令的堆疊存活性(stack liveness)分析可能出錯,讓驗證器誤判為安全而放行本應拒絕的程式;持有CAP_BPF(或系統允許非特權 BPF)即可觸發。修補見核心 commit6bd96e40f31d與945816e63c86。與上面 CVE-2026-31413(見 2.4 節)同屬「驗證器邏輯本身出錯」的類型,只是這次錯在 tail call 而非純量運算。 - CVE-2026-53095(CVSS 5.5,2026-06-24 揭露):uprobe類型的程式被允許修改struct pt_regs,但uprobe底層實際的程式類型其實與kprobe共用(都是BPF_PROG_TYPE_KPROBE),導致freplace程式可以附掛到監控核心函式的kprobe程式上,間接繼承本該只屬於uprobe的pt_regs寫入權限,在核心函式執行期間竄改其參數。修補方式是在freplace附掛時比對雙方的kprobe_write_ctx是否一致,不一致就拒絕附掛(核心 commit611fe4b79af7,"bpf: Fix abuse of kprobe_write_ctx via freplace")。屬於驗證器/附掛檢查邏輯出錯的另一個案例,與 CVE-2026-31413、CVE-2026-63864 同屬一類,只是這次錯在「附掛檢查」而非純量分析或堆疊分析。 - CVE-2026-74364(CVSS 7.1,2026-08-15 揭露):BPF 的「獨佔 (exclusive) map」機制原本保證某些 map 只能被單一程式存取,但這類 map 可以被當成 inner map 塞進一個非獨佔的 map-in-map 外層 map,執行期經由外層 map 存取時,原本的相容性/獨佔性檢查完全沒被檢查到,形同繞過。修補見核心 commit9a3c3c49c333。屬於本節分類中的 maps/helper 攻擊面,與下一筆 CVE-2026-74360 同屬「map 獨佔保證被繞過」的同類問題,只是繞過的入口不同(map-in-map vs. iterator)。 - CVE-2026-74360(CVSS 未公布,Ubuntu priority Medium,2026-08-15 揭露):bpf_map_elemiterator 在bpf_iter_attach_map()「附掛當下」就直接綁定目標 map,而非透過程式本身去參照,導致上一筆提到的獨佔性檢查完全沒有機會被執行——且此 iterator 還把 map value 以可寫緩衝區的形式暴露給使用者。修補見核心 commit3c56ee343f94。與上一筆同屬 maps/helper 的獨佔保證繞過類型。 - CVE-2026-74371(CVSS 7.8,2026-08-15 揭露):BPF_PROG_QUERY(含 cgroup BPF query)在處理較舊、較小的bpf_attr結構(相容舊版 userspace)時,核心仍無條件把query.revision寫回使用者提供的緩衝區,若呼叫端傳入的緩衝區不夠大,就會造成越界寫入。修補見核心 commit21c4b99b27f3。屬於本節分類中的 maps/helper(BPF syscall query 介面)攻擊面。 - CVE-2026-74363(CVSS 7.8,2026-08-15 揭露):bpffs(BPF 檔案系統)的 use-after-free——併發的unlinkat()釋放掉一個 inode 的最後一個參照後,destroy_inode()立即釋放該 inode,但另一個 task 可能仍在 RCU read mode 下走訪同一條路徑,形成 UAF(此問題源自先前為了避免 RCU context 警告所做的修改)。修補把清理邏輯拆成兩段:bpf_destroy_inode()保留可阻塞的操作,新增bpf_free_inode()專門做 RCU-safe 的延遲釋放。修補見核心 commitb93c55b4932d。是這份清單中少數涉及 bpffs 檔案系統層(而非驗證器/maps/helper 三分類本身)的案例。 - CVE-2026-74400(CVSS 未公布,Ubuntu priority Medium,2026-08-15 揭露):bpf_set_dentry_xattr()/bpf_remove_dentry_xattr()這兩個 helper 在收到 negative dentry(例如來自security_inode_create()的呼叫路徑)時,d_inode(dentry)會回傳NULL,但程式碼沒檢查就直接inode_lock(inode),造成 NULL pointer dereference;相關權限檢查函式中原本用WARN_ON處理同一情況,在啟用panic_on_warn的系統上等於也能被觸發成阻斷服務。修補改為對 NULL inode 直接回傳-EINVAL。修補見核心 commit07410646f6ff。 - CVE-2026-74382(CVSS 未公布,Ubuntu priority Medium,2026-08-15 揭露):這筆不在驗證器/maps/helper 三分類內,而是 TC(traffic control)子系統的cls_bpf——cls_bpf_offload_cmd()在 offload rollback 失敗時,會用相同參數遞迴呼叫自己;若 rollback 本身持續失敗(不限 netdevsim 測試驅動,任何tc_setup_cb_replace()連續失敗兩次的網卡驅動都可能觸發),就會無窮遞迴耗盡核心堆疊,造成 stack overflow / 阻斷服務。修補改為只允許一次 rollback,失敗就直接回傳原始錯誤,不再遞迴。修補見核心 commit27db54b90bcc。提醒:這說明 eBPF 的攻擊面不只驗證器/maps/helper 三處,tc(cls_bpf)這類與 eBPF 整合的核心子系統本身的控制流程,也可能是問題來源。這幾個案例的教育意義:eBPF 的「安全」是核心持續攻防的動態結果,不是一次性保證——生產環境務必訂閱 Linux 核心官方 CVE 公告(linux-cve-announce 郵件論壇) 或發行版的安全公告,並優先選用仍在收到安全更新的 LTS 核心。
# 檢查核心版本
uname -r
# 檢查是否啟用 BTF(CO-RE 的前提);存在此檔代表有 BTF
ls -l /sys/kernel/btf/vmlinux
# 檢查核心設定中與 eBPF 相關的選項
grep -E 'CONFIG_BPF|CONFIG_DEBUG_INFO_BTF' /boot/config-$(uname -r)
6.2 權限:從 root 走向 CAP_BPF¶
早期載入 eBPF 程式幾乎都需要 root(或 CAP_SYS_ADMIN)——權限太大,不符合最小權限原則。
核心 5.8 起引入了專屬能力 CAP_BPF,把原本綁在 CAP_SYS_ADMIN 上的 eBPF 權限拆出來,可搭配(參考:Linux capabilities(7) man page):
CAP_BPF:基本的 eBPF 操作(建立 map、做大部分bpf()系統呼叫)。注意單獨持有CAP_BPF仍無法載入大多數類型的程式。CAP_PERFMON:載入追蹤類程式(kprobe、tracepoint、perf event)所需,需與CAP_BPF搭配使用。CAP_NET_ADMIN:載入網路類(XDP / tc)程式所需,同樣需與CAP_BPF搭配。
這讓你能給一個 agent 剛好夠用的權限組合,而不是整個 root。
6.3 在容器 / Kubernetes 裡跑 eBPF 的注意事項¶
在 K8s 中部署 eBPF agent(如 Cilium、Falco、Tetragon),通常以 DaemonSet(每個節點一份)形式運行,並需注意:
- 特權或精細能力:Pod 通常需要
CAP_BPF/CAP_PERFMON/CAP_NET_ADMIN,或在受控下使用privileged: true。以 Cilium agent 為例,其預設能力清單包含NET_ADMIN、SYS_ADMIN、SYS_RESOURCE、IPC_LOCK等多項能力(完整清單見 Cilium 原始碼install/kubernetes/cilium/values.yaml中securityContext.capabilities.ciliumAgent段落所列出的預設值,額外還包含NET_RAW、SYS_MODULE、DAC_OVERRIDE、FOWNER、SETGID、SETUID、SYSLOG、CHOWN、KILL)。 - 掛載核心檔案系統:常需把宿主的
/sys/kernel/debug(debugfs)、/sys/fs/bpf(bpffs)掛進容器——eBPF 程式與 map 可以「釘選 (Pin)」到 bpffs 路徑,讓多個行程或重啟後仍能找到、複用同一份物件(eBPF Docs:Pinning)。 - eBPF 是節點層級、非命名空間化的:容器的隔離靠的是命名空間 (Namespace)、cgroup 等核心機制,但所有容器共用同一個核心;eBPF 程式作用於整個核心,而非單一容器的命名空間。意即一個節點上的 eBPF agent 看得到該節點上所有容器——這是它強大(全域可觀測)也需謹慎(權限邊界、最小權限原則更重要)之處。
- 託管叢集 (EKS/GKE/AKS) 的核心限制:你無法自選節點核心版本,得確認雲商提供的核心已啟用 BTF 並支援你需要的功能。這對前一章
02-eks學到的 AWS 環境尤其相關——選用節點 AMI 時要留意核心版本。
動手練習 7:檢視 Cilium 或 Falco 的官方 DaemonSet manifest,找出它宣告了哪些
securityContext.capabilities與volumeMounts(尤其是bpf-maps、sys-kernel-debug),對照本節說明,理解「為什麼它需要這些」。
7. 學習資源¶
| 資源 | 類型 | 說明 |
|---|---|---|
| ebpf.io | 官方入口 | eBPF 基金會官網,概念、生態系、文件總匯,最佳起點 |
| 《Learning eBPF》— Liz Rice | 書籍 | 由淺入深的入門經典,O'Reilly 出版(2023 年 3 月) |
| Cilium 官方文件 | 文件 | K8s 上實戰 eBPF 網路 / 安全 / Hubble 的權威來源 |
| libbpf-bootstrap | 範本 | libbpf + CO-RE 的官方起手範本,寫 C 必看 |
| cilium/ebpf | 函式庫 | Go 開發者寫 eBPF 的主流函式庫,含豐富範例 |
| bcc / bpftrace | 工具 | 觀測階段的必備工具集與單行追蹤語言 |
| Brendan Gregg 的網站與著作 | 進階 | 效能分析與 eBPF 追蹤的大師級資源 |
建議學習順序:ebpf.io 建立全貌 → 《Learning eBPF》系統性打底 → bpftrace/bcc 動手玩 → libbpf-bootstrap 或 cilium/ebpf 寫第一支程式 → Cilium 文件落地到 K8s。
8. 本章檢核點 (Checklist)¶
完成本章後,你應該能勾選以下每一項:
觀念理解 - [ ] 能說明「為什麼不直接寫核心模組」,並從當機風險與維護成本說明 eBPF 的優勢 - [ ] 能用「核心裡的 JavaScript」比喻向他人解釋 eBPF - [ ] 能說明驗證器 (Verifier) 的角色,以及它如何保證 eBPF 程式的安全 - [ ] 能解釋 JIT 編譯為何讓 eBPF 兼具「動態載入」與「原生效能」 - [ ] 能說出映射 (Maps) 的用途,以及它如何連接核心與使用者空間
掛載點與類型 - [ ] 能區分 kprobe、uprobe、tracepoint、XDP、tc、cgroup、LSM 各自的觸發時機 - [ ] 知道何時該優先選 tracepoint 而非 kprobe(穩定性) - [ ] 能說明 eBPF 三大應用領域:可觀測性、網路、安全
動手實作 - [ ] 已用 bpftrace 跑過至少 3 個單行程式 - [ ] 已用 bcc 工具(execsnoop / opensnoop / tcpconnect)實際觀測過系統 - [ ] 理解 CO-RE (Compile Once - Run Everywhere) 解決了什麼問題,以及 BTF 的角色 - [ ] 已建置並執行 libbpf-bootstrap 的 minimal 範例(或 cilium/ebpf 範例)
Kubernetes 整合 - [ ] 能說明為什麼 iptables 在大規模叢集會成為瓶頸,以及 eBPF 如何改善 - [ ] 知道 Cilium 用 eBPF 實作了哪些能力(CNI、取代 kube-proxy、NetworkPolicy、Hubble) - [ ] 認識 Tetragon、Falco、Pixie 各屬於哪個應用領域 - [ ] (進階)已在本地叢集安裝 Cilium 並用 Hubble 觀察過網路流
環境與部署 - [ ] 能檢查自己機器的核心版本與 BTF 是否啟用 - [ ] 知道 CAP_BPF 相較於 root 的意義(最小權限) - [ ] 理解在容器 / K8s 裡跑 eBPF 的注意事項(DaemonSet、能力、掛載 bpffs/debugfs、非命名空間化、託管叢集核心限制)
下一步:把本章學到的 eBPF 觀念,連回
01-kubernetes的網路策略與02-eks的節點選型——當你在 EKS 上選擇 Cilium 作為 CNI 時,你會清楚知道底層每一個封包,正由一段你能理解的 eBPF 程式在核心內處理。這就是雲原生最深的一層。