🐝

eBPF ~ カーネル拡張に吹き込んだ新たな風 ~

に公開

はじめに

ひょんなことから X(Twitter)で次の投稿を見かけ「あ、eBFP まとめとこう」と思ったのでまとめる。

https://x.com/ibukiinterpress/status/1359793040957710342

カーネル拡張に吹き込んだ新たな風

現代のクラウド環境やマイクロサービス、コンテナ化されたワークロードの台頭により、Linuxカーネルに対する要求は日々高度化している。特にパフォーマンス、観測性、セキュリティの側面において、従来のカーネル拡張手法ではもはや十分に対応できなくなってきた。これまでのカーネル拡張手法には主に二つの選択肢があった。カーネルモジュールを開発するか、ユーザー空間で完結するソリューションを構築するかだ。

カーネルモジュールは強力だが、カーネルの内部構造への深い理解が必要で、開発の難易度が高い。さらに、異なるカーネルバージョン間の互換性維持が困難で、バグがシステム全体をクラッシュさせるリスクもある。一方、ユーザー空間ソリューションは安全だが、システムコールを介したカーネルとの通信オーバーヘッドが大きく、パフォーマンスが制限される。

このジレンマを解決する技術として登場したのが、eBPF(extended Berkeley Packet Filter)だ。eBPFはLinuxカーネル内で安全にユーザープログラムを実行できる技術である。システムを再起動せずにカーネルの動作を動的に拡張でき、様々な用途に活用できる汎用性も持ち合わせている。

eBPFがもたらすパラダイムシフトは、オペレーティングシステムレベルでのプログラマビリティの向上にある。従来、OSの革新速度はアプリケーションレイヤーに比べて遅かったが、eBPFにより、中核となるOSコードを変更することなく、カーネルの機能を安全に拡張することが可能になった。まるでJavaScriptがWebブラウザの機能を拡張したように、eBPFはLinuxカーネルに新たな可能性をもたらしたのだ。

eBPFの進化

eBPFの歴史は1992年にさかのぼる。オリジナルのBPF(Berkeley Packet Filter)は、ネットワークパケットを効率的にフィルタリングするために開発された技術だった。従来のスタックベースのCSPF(CMU/Stanford Packet Filter)に比べ、BPFはレジスタベースの仮想マシンを採用し、パケットフィルタリングのパフォーマンスを大幅に向上させた。

eBPFの進化における主要なマイルストーンを以下の表にまとめる。

出来事 影響と意義
1992 オリジナルBPFの発表 ・Steven McCanneとVan Jacobsonによる論文発表
・レジスタベースの仮想マシンを使用したパケットフィルタリング
・UNIXシステムでのネットワークモニタリングに革命
1997 Linux Socket Filterとしての実装 ・Linux 2.1.75
・BSDのBPFがLinux Socket Filter (LSF)として移植
・Linuxでのパケットフィルタリング基盤の確立
2013 Alexei Starovoitov eBPFの提案 [PATCH net-next] extended BPF(メーリスアーカイブ)
2014 eBPFの搭載 ・Linux 3.18
・64ビットレジスタの導入
・任意のカーネルフックへの接続が可能に
・mapによるデータ共有メカニズムの導入
2019 BPF CO-REの提案 ・Compile Once - Run Everywhere
・異なるカーネルバージョン間での互換性向上
2021 eBPF for Windowsの発表 ・Microsoft主導でWindows環境にもeBPFを移植
・クロスプラットフォーム化への第一歩
2021-現在 eBPF Foundationの設立と発展 ・Linux財団下でのコミュニティ拡大
・標準化と互換性の推進

eBPFの採用は、Facebook や Cloudflare、Google、Netflixといった大規模テクノロジー企業を中心に進み、その後Kubernetes環境でのネットワーキングやセキュリティ用途へと広がった。現在では、クラウドネイティブ環境におけるシステム観測性、セキュリティ、ネットワーキングの標準的なソリューションとなっている。

eBPFのアーキテクチャと動作原理

eBPFは単なるパケットフィルタではなく、Linuxカーネル内に埋め込まれた仮想マシンだ。この仮想マシンは、安全にプログラムを実行するための複数のコンポーネントから構成されている。eBPFアーキテクチャの全体像は、開発フェーズとランタイムフェーズに分けることができる。


https://ebpf.io/what-is-ebpf/

eBPFのアーキテクチャは上図のように「Development」と「Runtime」の2つの主要フェーズで構成されている。開発フェーズでは、C言語などで記述されたeBPFプログラムが、CLANGコンパイラによってeBPFバイトコードに変換される(ここではあくまでC言語を例として取り上げているだけであり、C言語に限定されるものではない)。このバイトコードとマップ定義は、eBPF Goライブラリなどのユーザースペースライブラリを通じてカーネルに渡される。

ランタイムフェーズでは、まずシステムコールを介してカーネルにプログラムがロードされ、eBPF Verifierによって安全性が厳密に検証される。検証に通過したプログラムはJITコンパイラによってネイティブマシンコードに変換され、高いパフォーマンスで実行される。プログラムとユーザー空間のプロセスはeBPFマップを介してデータを双方向に共有でき、TCP/IPスタックやソケットインタフェースとも連携する。

eBPFの主要コンポーネントとその役割を以下の表で詳細に解説する。

コンポーネント 役割と機能
Verifier(検証機構) ・プログラムの安全性を静的に解析し保証
・無限ループやカーネルクラッシュを防止
・メモリアクセスの安全性を検証
・無効なポインタ操作を検出
JITコンパイラ ・eBPFバイトコードをネイティブマシンコードに変換
・アーキテクチャ固有の最適化を実施
・x86_64、ARM64、RISC-V等の多様なアーキテクチャをサポート
・実行時のパフォーマンスを大幅に向上
マップ ・eBPFプログラム間やユーザー空間との間でデータを共有
・ハッシュマップ、配列、LRUハッシュ、リングバッファなど多様な構造を提供
・統計情報や状態を保持
・永続的なデータストレージとして機能
ヘルパー関数 ・予め定義されたカーネル機能へのアクセスを提供
・パケット操作、時間取得、乱数生成などの機能
・サンドボックス内で安全に呼び出せる唯一のカーネル関数
・プログラムタイプごとに呼び出し可能な関数が制限される
プログラムタイプとフック ・eBPFプログラムがアタッチできる場所を定義
・ネットワーク(XDP、TC)、トレーシング(kprobes、tracepoints)
・セキュリティ(LSM、Seccomp)など多岐にわたる接続点
・それぞれ特有のコンテキストと機能を持つ

eBPFのVerifierは全体の安全性を担保する最も重要なコンポーネントだ。バイトコードをグラフに変換し、すべての実行パスを解析して問題がないか検証する。具体的には、無限ループの検出、メモリアクセスの安全性確認、リソース使用量の制限などを行う。このVerifierのおかげで、カーネル空間であっても安全にユーザー定義のプログラムを実行できる。

JITコンパイラはパフォーマンス最適化の要だ。初期のBPFはインタプリタ実行だったが、現代のeBPFは常にJITコンパイルされる。これにより、ネイティブにコンパイルされたカーネルコードと同等のパフォーマンスを発揮できる。さらに、定数ブラインド(Constant blinding)などの技術でJITスプレー攻撃なども防いでいる。

eBPFプログラムはカーネルの任意の場所に接続(アタッチ)でき、それぞれの接続点に応じた処理を行う。例えば、ネットワークパケットがNICに到達した瞬間に処理するXDP(eXpress Data Path)プログラム、システムコールをフィルタリングするSeccompプログラム、カーネル関数の実行をトレースするkprobesプログラムなど、多様なフックポイントが存在する。

eBPFプログラミングモデル

eBPFのプログラミングモデルは、従来のカーネルモジュール開発と比較すると大きく異なる。eBPFプログラムは基本的にイベント駆動型で、特定のフックポイントに接続されて実行される。プログラムのライフサイクルは以下の流れで構成される。

eBPFのプログラム型(enum で定義される)とフックポイントは多岐にわたる。以下の表はその主要なものを示している。

プログラム型 接続可能なフック 主な用途
XDP (eXpress Data Path) ネットワークデバイスドライバ ・超高速パケット処理
・DDoS軽減
・パケットフィルタリング
・ルーティング
Traffic Control (TC) ネットワークキュー ・トラフィック制御
・QoS実装
・パケット変換
・帯域制御
cgroup コントロールグループ ・コンテナのネットワークポリシー
・デバイスアクセス制御
・リソース制限
Socket ソケット操作 ・ソケットフィルタリング
・プロトコル解析
・コネクショントラッキング
kprobe/uprobe カーネル/ユーザー関数 ・関数トレーシング
・パフォーマンス分析
・動的デバッグ
tracepoint 静的トレースポイント ・システムコールトレース
・カーネルイベント監視
・統計情報収集
perf_event パフォーマンスイベント ・CPUパフォーマンスカウンタ監視
・ハードウェアイベント分析
LSM (Linux Security Module) セキュリティフック ・セキュリティポリシー実装
・アクセス制御
・権限チェック

eBPFプログラミングに使用できる言語は複数あるが、大きく分けて二つのアプローチがある。低レベルの直接的なアプローチと高レベルの抽象化されたアプローチだ。

プログラミングアプローチ 言語とツール 特徴
低レベルアプローチ ・C言語(libbpf)
・Rust(libbpf-rs)
・細かな制御が可能
・高いパフォーマンス
・直接的なeBPF命令の制御
・ランタイムオーバーヘッドが最小限
高レベルアプローチ ・Python(BCC)
・Go(gobpf)
・DSL(bpftrace)
・素早いプロトタイピング
・簡潔な構文
・学習曲線が緩やか?
・一部のパフォーマンスを犠牲にする

eBPFプログラミングの中でも注目すべき点が CO-RE(Compile Once - Run Everywhere)だ。これは異なるカーネルバージョン間でのeBPFプログラムの互換性問題を解決する技術である。BPF Type Format(BTF)を活用して、コンパイル時にはシンボリック再配置を生成し、ロード時に動的に解決することで、一度コンパイルしたプログラムを様々なカーネルで実行できるようになった。これはJavaの「Write Once, Run Anywhere」と同様の概念であり、eBPFの実用性と採用のハードルを大きく下げた。

eBPFの安全性と形式検証

eBPFがLinuxカーネル内で安全に実行できる鍵となるのがVerifier(検証機構)だ。eBPFプログラムはバイトコードの状態でカーネルに渡され、実行前にVerifierによる厳格な検証プロセスを経る。以下の図はeBPFプログラムのライフサイクルとVerifierの役割を示している。


https://arxiv.org/html/2410.00026v2

この図が示すように、eBPFプログラムは以下のプロセスを経る。

  1. ソースコード(bpf.c)がClangコンパイラ+LLVMによってELFバイナリ(bpf.o)に変換される(S1→S2)
  2. アプリケーションがbpf(BPF_PROG_LOAD, ...)システムコールを使ってプログラムをカーネルにロードする(S3→S4)
  3. カーネル内のVerifierがプログラムを検証し、安全でない場合は拒否(赤バツ)する
  4. 検証を通過したプログラムはJITコンパイラによってネイティブコードに変換される
  5. bpf(BPF_LINK_CREATE, ...)システムコールでプログラムをフックポイント(例:ネットワークスタック、eth0インターフェース)にアタッチする(S5)
  6. プログラム実行が終了すると、リンク(S6)とプログラム(S7)がクローズされる

Verifierの主要な検証プロセスとセキュリティガードレールを以下の表で解説する。

検証項目 検証内容と手法
パス探索 ・制御フローグラフを構築
・すべての可能な実行パスを探索
・到達不能コードを検出
・最大命令数の制限を確認
無限ループ検出 ・ループの終了条件を解析
・バウンドされたループ(反復回数が有限)のみを許可
・繰り返し回数の上限を検証
・無限ループのリスクを排除
メモリアクセス検証 ・ポインタの範囲解析
・境界外アクセスを検出
・型安全性を確認
・不正メモリ操作を防止
ヘルパー関数呼び出し ・引数型の一致を検証
・プログラム型に応じて許可された関数であるかの確認
・不正な関数呼び出しを検出
・パラメータの有効範囲をチェック
資源使用制限 ・スタックサイズ制限の確認
・マップ使用量の検証
・命令数の上限チェック
・システムリソースの保護

Verifierは常に保守的な検証を行う。つまり、プログラムが安全かどうか確実に判断できない場合は、プログラムを拒否する。例えば、条件分岐のすべての可能性を静的に解析できない場合や、ループの終了が保証できない場合は、たとえそのプログラムが実際に安全であっても拒否される。このような厳格な検証により、カーネル空間でユーザー定義コードを実行する際のリスクを最小限に抑えている。

特に重要なのはメモリアクセス検証だ。Verifierはポインタの範囲追跡を行い、eBPFプログラムがアクセスを許可されたメモリ領域のみを読み書きすることを保証する。これにより、カーネルメモリの破壊やデータ漏洩などのセキュリティリスクを防いでいる。

Verifierを通過したeBPFプログラムはJITコンパイラによってネイティブマシンコードに変換され、高いパフォーマンスで実行される。このように、eBPFはVerifierによる強力な安全性保証とJITコンパイラによる高いパフォーマンスを両立させている。

また、eBPFのセキュリティモデルは多層防御の原則に基づいている。仮にVerifierにバグがあったとしても、追加の保護メカニズムにより、カーネルの完全性を保護している。

セキュリティメカニズム 機能と役割
定数ブラインド ・JITスプレー攻撃の防止
・コード内の定数を難読化
・実行可能コードの挿入を防止
JITハードニング ・JITコンパイルされたコードの保護
・実行コードの読み取り専用化
・コード改ざんの防止
特権分離 ・非特権ユーザーの利用制限
・CAP_BPF権限による保護
・特権昇格攻撃の緩和
Spectre緩和策 ・投機的実行攻撃対策
・メモリアクセスのマスキング
・サイドチャネル攻撃の防止

近年では、eBPFの安全性をさらに高めるための形式検証的アプローチも進んでいる。特にJITコンパイラの正当性証明や、Verifierの完全性検証について活発な研究が行われている。

主要な応用領域

eBPFの汎用性の高さから、様々な領域で応用が広がっている。以下にその主要な応用領域を解説する。

ネットワーキング

ネットワーキングはeBPFが最も強力な効果を発揮する分野の一つだ。特にXDP(eXpress Data Path)は、パケットがデバイスドライバに到達した時点で処理できるため、従来のカーネルネットワークスタックを経由せずに超高速なパケット処理が可能になる。


https://www.researchgate.net/publication/339084847_Fast_Packet_Processing_with_eBPF_and_XDP_Concepts_Code_Challenges_and_Applications

ネットワーキング応用 機能と事例
パケットフィルタリング ・Suricataのパケットフィルタエンジン
・Cloudflareの大規模DDoS防御
・Facebookのトラフィックフィルタリング
・毎秒数百万パケットの処理が可能
ロードバランシング ・KatranによるL4ロードバランサー
・Facebookのスケーラブルな接続配分
・マイクロサービス間の通信最適化
・一貫性のあるハッシュベースの分散
ネットワークポリシー ・CiliumによるKubernetesネットワークポリシー
・マイクロサービス間の通信制御
・L7レベルのAPI認識フィルタリング
・アプリケーション層プロトコル制御
トラフィックシェーピング ・TCプログラムによる帯域制御
・QoSの動的実装
・優先度に基づくトラフィック配分
・トラフィックエンジニアリング

この表はeBPFのネットワーキング応用の主要な分野と、それぞれの代表的な機能や実装事例を示している。

XDPとTCの組み合わせにより、エッジからコアまでのネットワークスタック全体をプログラマブルにすることが可能だ。これにより、従来はハードウェアアプライアンスや専用ミドルウェアで実現していた機能を、汎用サーバー上のソフトウェアで実現できるようになった。

観測性(Observability)

システムの状態や挙動を可視化するための観測性は、近年のマイクロサービスアーキテクチャの普及とともに重要性が増している。eBPFは低オーバーヘッドで高精度な観測性を提供する理想的なツールだ。


https://www.brendangregg.com/blog/2019-07-15/bpf-performance-tools-book.html

観測性応用 機能と事例
パフォーマンスモニタリング ・bpftrace/BCCによるシステム分析
・CPUプロファイリング
・I/Oレイテンシ測定
・メモリ使用状況追跡
分散トレーシング ・Pixieによるアプリケーション監視
・マイクロサービス間通信の追跡
・サービスメッシュの可視化
・リクエストフローのエンドツーエンド分析
コンテナ監視 ・CiliumのHubbleによるコンテナ通信観測
・Kubernetes環境のネットワークフロー分析
・コンテナメトリクス収集
・異常検出とトラブルシューティング
カスタムメトリクス ・カーネルイベントからのメトリクス生成
・アプリケーション特有のパフォーマンス指標
・Prometheusとの統合
・リアルタイムダッシュボード構築

eBPFベースの観測ツールの特筆すべき点は、アプリケーションの変更やリコンパイルなしに、深いレベルの洞察を得られることだ。これは従来のAPM(Application Performance Management)ツールやプロファイラとは一線を画する特長である。

Brendan Greggが提唱した「USE Method」(Utilization, Saturation, Errors)などのパフォーマンスモニタリング手法も、eBPFツールによって格段に実装しやすくなった。

セキュリティ

eBPFはセキュリティ分野でも革命的な進歩をもたらしている。カーネルレベルでの可視性と制御を提供することで、従来のセキュリティソリューションが持つ多くの制限を克服している。

セキュリティ応用 機能と事例
ランタイム保護 ・FalcoによるLinuxシステム監視
・異常な挙動やセキュリティ違反の検出
・ファイルアクセス制御
・プロセス実行管理
コンテナセキュリティ ・Ciliumによるコンテナ間通信の制御
・マイクロサービスのセグメンテーション
・権限昇格検出
・コンテナエスケープ防止
システムコール監査 ・Tetragonによるシステムコールトレース
・セキュリティイベントのリアルタイム検出
・フォレンジック分析
・コンプライアンス監視
侵入検知・防止 ・Traceeによる高度な脅威検出
・ゼロデイ攻撃の検出
・フィルインジェクション検出
・異常なネットワーク通信の遮断

eBPFを用いたセキュリティソリューションの特長は、従来のセキュリティツールが直面していた「可視性 vs. パフォーマンス」のトレードオフを大幅に改善している点だ。カーネルレベルで直接動作するため、ユーザー空間のエージェントが持つオーバーヘッドを削減しつつ、より深いレベルの可視性と制御を実現している。

また、eBPFプログラムはイベント駆動型であるため、継続的なポーリングなどによるCPU消費を防ぎ、必要なときだけ実行される効率的な設計になっている。これにより、本番環境でも常時稼働可能なセキュリティモニタリングが実現した。

ストレージとI/O

ストレージとI/Oの最適化もeBPFの新たな応用領域だ。特に高速なNVMeデバイスの普及により、カーネルストレージスタックのオーバーヘッドが目立つようになり、eBPFを用いた最適化手法が注目されている。


https://www.usenix.org/conference/osdi22/presentation/zhong

ストレージ応用 機能と事例
ストレージI/O最適化 ・XRPフレームワーク(上図)によるNVMeドライバフック
・不要なストレージスタック処理のバイパス
・カスタムインデックス検索
・ストレージエンジン最適化
ファイルシステム拡張 ・ExtFUSEによるユーザー空間ファイルシステム拡張
・ファイルアクセスパターン分析
・カスタムキャッシュ戦略
・ファイルシステムセキュリティ強化
ブロックI/Oトレース ・ブロックI/Oレイテンシのリアルタイム測定
・アプリケーション別I/Oパターン解析
・ストレージボトルネック特定
・I/Oスケジューラの動的調整
キャッシュ最適化 ・メモリキャッシュの動的管理
・アプリケーション特有のプリフェッチ戦略
・キャッシュヒット率の最適化
・ページキャッシュのカスタム制御

XRPのような研究プロジェクトは、ストレージスタックのオーバーヘッドを大幅に削減し、NVMeデバイスの本来の性能を引き出すことに成功している。これは特にデータベースやキーバリューストアのような、ストレージI/Oが重要なアプリケーションにとって大きな意味を持つ。

IoTと組み込みシステム

リソースが制限されたIoT環境でもeBPFの活用が進んでいる。特に軽量化されたeBPF実装が、組み込みLinuxやマイクロコントローラ向けOSに採用されつつある。

IoT応用 機能と事例
軽量eBPF実装 ・RIOT OSのrBPF実装
・マイクロコントローラ向け軽量VM
・形式検証されたJIT
・メモリフットプリントの最小化
エッジコンピューティング ・エッジデバイスでのデータフィルタリング
・ローカル処理による通信量削減
・センサーデータの前処理
・クラウドとエッジの連携
IoTセキュリティ ・リソース制約環境でのセキュリティモニタリング
・異常検知
・デバイス間通信の監視
・リモート攻撃の検出
フェムトコンテナ ・IoTデバイス上での安全な機能拡張
・OTAプログラミング
・リソースアイソレーション
・動的機能更新

IoT環境では、デバイスの計算能力とメモリが限られているため、フル機能のeBPFよりも機能を制限した軽量実装が採用されることが多い。例えば、RIOT OSのrBPFは組み込みデバイス向けに最適化され、形式検証されたJITコンパイラによって安全性と効率性を両立している。

まとめ

eBPFはLinuxのエコシステムにおける「レゴブロック」のようなものだ。基本的なコンポーネントとしてカーネルに組み込まれていながら、それを組み合わせることで無限の可能性が広がる。

話変わって、「ガンジーが助走つけて殴るレベル」を蜂業界のコンテキストに引き入れると、「クマバチが助走して刺すレベル」と言うことができる。しらんけど。

Discussion