Linux eBPF

最終更新: 2026-06-09 02:54:06 +0800 +0800
tags: Bpf | Kernel | Linux
原文は英語で書かれています。あなたはAI翻訳版を読んでいます。

eBPF は Linux カーネル内の検証済み実行環境です。アプリケーションはプログラムとマップをロードし、カーネルが定義したフックにプログラムをアタッチし、ファイルディスクリプターを介してデータを交換します。ネットワーク処理は依然として重要な用途ですが、トレーシング、セキュリティポリシー、性能分析は、もはやパケットキャプチャに依存しません。

パケットフィルターから Linux ソケットフィルターへ #

設計上の系譜は、ULTRIX や DIGITAL UNIX などのシステム向けに DEC が開発したパケットフィルターに始まります。プロセスはパケットフィルターデバイスを開き、インターフェイスを選択し、ioctl() で enfilter 命令列を設定して、受理されたリンク層パケットを読み取りました。フィルターはスタックマシンを使用し、複数のコンシューマーは優先度順に処理されました。

Steven McCanne と Van Jacobson は、パケットのコピーおよび解釈にかかるコストを削減するために Berkeley Packet Filter(BPF)を設計しました。1993 年の論文では、現在も認識できる次の 2 つの変更が説明されています。

  • レジスタベースの仮想マシンが、パケットを受理するかどうか、および何バイトをコピーするかを決定する。
  • 各オープン済みの /dev/bpf インスタンスが独自のフィルターとパケットバッファーを持つため、パケットキャプチャは DEC の優先度チェーンではなくデバイスインスタンスとして表現される。

現在 cBPF と呼ばれる Classic BPF には、32 ビットのアキュムレーター A、インデックスレジスター X、16 個のスクラッチワード、パケットロード命令、条件分岐、算術演算、戻り値があります。Linux はこの命令形式を Linux Socket Filtering(LSF)に採用しましたが、フィルターを /dev/bpf ではなくソケットに直接アタッチします。

c
struct sock_filter code[] = {
    BPF_STMT(BPF_LD  | BPF_B | BPF_ABS, 9), /* IPv4 protocol byte */
    BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, IPPROTO_TCP, 0, 1),
    BPF_STMT(BPF_RET | BPF_K, 0xffff),
    BPF_STMT(BPF_RET | BPF_K, 0),
};
struct sock_fprog program = {
    .len = sizeof(code) / sizeof(code[0]),
    .filter = code,
};

setsockopt(fd, SOL_SOCKET, SO_ATTACH_FILTER,
           &program, sizeof(program));

この例では、ソケットのデータが IPv4 ヘッダーから始まることを想定しているため、9 バイト目がプロトコルフィールドになります。パケットレイアウトはソケットファミリーとフックによって異なります。ソケットフィルターでは、戻り値が保持するパケットバイト数を示し、ゼロはパケットを拒否します。カーネルはインストール前に命令列を検査します。SO_LOCK_FILTER を使用すると置換または削除を防止できるため、プロセスはフィルターをアタッチしてから権限を放棄できます。libpcap と tcpdump は、高レベルのキャプチャ式をこの命令モデルにコンパイルします。

eBPF の実行モデル #

Linux の最新の BPF システムコールインターフェイスは Linux 3.18 で導入されました。eBPF は固定幅の 64 ビット命令と、11 個の 64 ビットレジスターを使用します。

  • R0 は関数またはプログラムの戻り値を保持します。
  • R1 から R5 は引数を渡し、呼び出し元保存レジスターです。
  • R6 から R9 は呼び出し先保存レジスターです。
  • R10 は呼び出しごとのスタック用の読み取り専用フレームポインターです。

エントリー時、R1 は xdp_md、__sk_buff、トレーシングコンテキストなど、プログラムタイプ固有のコンテキストを指します。プログラムはフック固有のアクションを返します。XDP プログラムはパケットを通過、破棄、リダイレクト、送信できますが、トレーシングプログラムは通常ゼロを返します。

インタープリターは受理されたバイトコードを実行できます。アーキテクチャが BPF JIT を有効にしている場合、カーネルはプログラムをネイティブ命令に変換し、sysctl ポリシーに従って生成されたイメージを強化できます。JIT コンパイルによって実行コストは変わりますが、検証要件は変わりません。

検証が安全性の境界を定義する #

プログラムをアタッチする前に、検証器は可能なパスを記号的に実行します。検証器は、レジスターとスタックスロットの型、スカラー値の範囲、ポインターの来歴、アライメント、初期化状態、参照の所有権、境界を追跡します。したがって、パケット読み取りのようなコードは、要求されたバイトが data_end より前に存在することを制御フローが証明した後でのみ受理されます。

検証では、次のような特性が強制されます。

  • 初期化されていないレジスターやスタックデータを使用しないこと。
  • スカラー値をカーネルポインターへ任意に変換しないこと。
  • プログラムタイプで許可されたコンテキストフィールドとヘルパー関数だけを使用すること。
  • メモリアクセスが境界内にあり、ポインター演算が有効であること。
  • 追跡対象の参照の取得と解放が釣り合っていること。
  • 終了を証明できるループを含め、実行が有界であること。

受理は検証器のモデルへの適合性を証明するものであり、機能的正しさを証明するものではありません。有効なプログラムでも、すべてのパケットを破棄したり、誤ったキーを集計したり、過剰なオーバーヘッドを追加したりする可能性があります。複雑度の制限により、パス探索で必要な不変条件を確立できない場合、論理的には安全なソースコードが拒否されることもあります。

マップとヘルパー #

マップは、型固有のキーと値のストレージ、および並行性セマンティクスを持つカーネルオブジェクトです。ハッシュ、配列、CPU ごとのマップ、リングバッファー、LRU マップ、最長プレフィックスツリー、プログラム配列、ストレージマップは、それぞれ異なる通信パターンに対応します。ユーザー空間は bpf() コマンドを介してマップを作成・操作し、一般には libbpf がこれをラップします。

c
int map_fd = bpf_map_create(BPF_MAP_TYPE_HASH, "counts",
                            sizeof(__u32), sizeof(__u64),
                            1024, NULL);
bpf_map_update_elem(map_fd, &key, &value, BPF_ANY);

プログラムは、許可されたヘルパーまたは BPF kfunc を使用して、マップ要素の検索、時刻とプロセス ID の読み取り、パケットのリダイレクト、リングバッファーイベントの出力、フック固有のデータへのアクセスを行います。利用可能なヘルパーと引数の型はプログラムタイプによって異なります。マップ検索は NULL を返す可能性があるため、検証器はデリファレンス前の null チェックを要求します。

マップは共有状態であるため、競合は引き続きアプリケーション側の問題です。CPU ごとのマップは、カウンターの CPU 間更新を回避します。アトミック操作や struct bpf_spin_lock を利用できるのは、文書化された制約が許す場合だけであり、ユーザー空間は CPU ごとの値を明示的に集計する必要があります。

プログラムタイプとアタッチポイント #

プログラムタイプは、コンテキスト、戻り値の規約、検証器の操作、利用可能なヘルパーを選択します。アタッチタイプはさらにフックを特定します。一般的な組み合わせには次のものがあります。

領域プログラムとアタッチの例
初期パケットパスネットワークデバイス上の XDP
トラフィック制御TC ingress/egress の SCHED_CLS または SCHED_ACT
ソケットソケットフィルター、reuseport 選択、ソケット検索、sockops、cgroup ソケットフック
トレーシングトレースポイント、raw トレースポイント、kprobe/kretprobe、fentry/fexit、perf イベント
セキュリティLSM フックと cgroup ポリシーフック

フックの選択は、セマンティクスとコストの両方を決定します。XDP は、対応ドライバー上では通常のネットワークスタックのソケットバッファーが割り当てられる前に実行されるのに対し、TC プログラムは skb を認識します。fentry/fexit は BTF で記述された関数境界を使用しますが、kprobe は命令アドレスを対象とするため、実装変更の影響を受けやすくなります。

権限、ライフサイクル、所有権 #

カーネルとシステムポリシーが要求された操作を許可しない限り、ロードには権限が必要です。最新のカーネルでは CAP_BPF、CAP_PERFMON、CAP_NET_ADMIN によって権限を分離できますが、古い経路や一部の操作では依然として CAP_SYS_ADMIN が使用されます。ディストリビューションのカーネルは kernel.unprivileged_bpf_disabled によって非特権ロードを無効化でき、lockdown、LSM、名前空間、トークン、委譲ポリシーによって追加の制限が課される場合があります。単一の capability 設定を普遍的なものとみなさず、実行中のカーネルを確認してください。

プログラム、マップ、BTF オブジェクト、リンクは、ファイルディスクリプターを介して公開される参照カウント方式のカーネルオブジェクトです。最後の参照を閉じるとオブジェクトは破棄されます。BPF ファイルシステム(bpffs)にオブジェクトをピン留めすると、永続的なパスと参照が与えられます。

bash
mount -t bpf bpf /sys/fs/bpf
bpftool prog show
bpftool map show
bpftool link show

bpf_link はアタッチを生のプログラム FD から独立して表現し、対応するフックに対してより安全なライフサイクルを提供します。リンクを閉じると、別の参照またはピンによってリンクが保持されていない限り、プログラムはデタッチされます。ピン留めは、作成プロセスが終了した後も現在のブート中は存続します。再起動後は、起動コードがカーネルオブジェクトを再作成する必要があり、以前のカーネルインスタンスのオブジェクトを再オープンすることはできません。

無制限のアクセスを伴わない可観測性 #

トレーシングプログラムは、レイテンシーの測定、イベントのカウント、スタックのサンプリング、スケジューラー活動の関連付け、リングバッファーを通じた構造化レコードのエクスポートを実行できます。CO-RE リロケーションは BTF を使用して、互換性のあるカーネル型レイアウト間でフィールドアクセスを適応させます。運用ツールでは、次の点も考慮する必要があります。

  • コンシューマーがバッファーを排出できない場合のイベント損失。
  • 高頻度フックにおけるイベントごとのオーバーヘッド。
  • 適切な BTF がないカーネルでのシンボルと型の変更。
  • カーネルまたはプロセスデータを公開することによるプライバシーおよびセキュリティへの影響。

ロード済みの内容を調査するには、bpftool prog show、bpftool prog dump xlated、検証ログ、プログラムの実行時統計を使用します。可観測性コードは本番カーネルパスで実行されるため、有界なオーバーヘッドと明示的なデタッチ計画を設計に含める必要があります。

参考資料 #

コメント

GitHub で閲覧・コメントする(投稿にはログインが必要です)