eBPFの基礎

このページの目次

ローダーが終了してもプログラムはアタッチされたままになり、検証器が通過してもカウントが失われる可能性があります。eBPFにおけるオブジェクトの寿命、メモリ安全性の証明、ビジネスロジックの正しさは、別々の問題です。

ここでは拡張BPF(eBPF)について解説します。seccompは古典的なBPFフィルタ形式と専用のシステムコールセマンティクスを使用するため、通常のeBPFプログラムタイプとは混同しないでください。

参照目標該当セクション
ロード、アタッチ、verifier第1~2節
実行コスト、マップ、イベントバッファ第3~5節
互換性と権限第6~7節

1. 寿命には複数の独立したオブジェクトが含まれる

BPFオブジェクト:コンパイル、ロード、アタッチ、継続的参照 BPF ELFとしてコンパイル コード、マップ記述、BTF、リロケーション情報 1 ユーザー空間ローダーがオブジェクトを準備 ターゲットBTFに基づいてCO-REリロケーションを実行し、マップを構築 2 カーネルがプログラムをロードして検証 型、境界、リソース、およびターゲットプログラムタイプの制約をチェック 3 サポートされる実行ポイントにアタッチ イベントトリガー後にマップまたは出力イベントを使用 4 アタッチを解除し、参照を解放 FD、リンク、ピン留めなどがオブジェクトの寿命を決定 5 ロードとアタッチは別です。FD を閉じても、即座に破棄されるとは限りません。
オブジェクトまたは動作寿命における役割
プログラムパース、リロケーション、カーネルロードを経て、必要なマップなどの参照を保持
マップ状態を独立して管理し、寿命はローダープロセスの寿命と必ずしも一致しない
attach / BPF linkプログラムをイベントソースに接続し、インターフェースに従って関連付けを管理
pinオブジェクト参照をbpffsに保存し、他のプロセスが取得できるようにする

libbpfはELFのパース、リロケーション、カーネルインターフェース呼び出しを調整しますが、ロード成功をアタッチ済みと見なしてはいけません。

ローダープロセスを閉じても、すべてのプログラムが解除されるとは限りません。ピン留めされているか、他のオブジェクトによって参照されているリソースは存在し続ける可能性があります。また、attach APIごとに寿命のルールが異なります。ツールを設計する際は、誰がリンクを保持しているか、ピン留めされているか、終了時にどのように解除するかを明確にし、「プロセス終了時に自動的にクリーンアップされる」という依存関係だけに頼ってはなりません。詳細はlibbpf概要をご覧ください。

2. Verifierは証明可能な安全条件をチェックする

検証器は制御フローに沿って抽象状態を伝播させ、レジスタおよびスタックスロット内のスカラー範囲、ポインタ型、初期化済み領域、解放が必要なリソースを追跡します。状態のマージや枝刈りなどのメカニズムを通じてパスを処理し、具体的な入力を一つずつ実行するわけではありません。

チェック項目一般的な拒否理由
型とソース通常整数をカーネルポインタとして扱い、未初期化レジスタを読み取る
メモリ境界パケット範囲が証明されていない、マップ値またはスタック境界を越える
参照とリソース追跡対象オブジェクトを取得した後、すべてのパスで解放していない
helper/kfunc契約プログラムタイプが呼び出しを許可していない、パラメータ型またはコンテキストが要件を満たしていない
制御フローと複雑さ終了が証明できない、状態探索または呼び出し/スタックの制限を超過

有界ループ、bpf_loop、またはイテレータなどのメカニズムにより表現可能な範囲は拡大しましたが、任意のループが許可されるわけではありません。プログラム指令数とverifierの探索複雑さは異なる制限です。「100万」という数字をソースコードの長さ、実行回数、およびすべてのプログラムに対する統一された上限として同時に扱ってはなりません。

BPFスタックには厳格な制限があり、一般的にサブプログラムスタックフレームの上限は512バイトです。呼び出しチェーンやtail callの組み合わせにより制約が増加します。tail callの連鎖制限は、通常のBPF-to-BPF呼び出しの深さとは異なります。バージョン間で通用する定数表を暗記するのではなく、ターゲットカーネルの検証ログを基準にしてください。

非空証明とキー範囲は別物

以下はマップ検索後の局所的なパターンです。countsの値型が__u64、キーが初期化済み__u32であると仮定します。

__u32 key = 7;
__u64 *value = bpf_map_lookup_elem(&counts, &key);
if (!value)
    return 0;
__sync_fetch_and_add(value, 1);

検索ヘルパーはマップタイプとキーの内容に基づいて結果を決定します。検証器は、キーのメモリが読み取り可能であることを確認し、valueへの間接参照前にそれがNULLでないことを確認する必要があります。if (x < 10)を任意のマップ検索が必ず成功する証明として扱ってはなりません。特にxが符号付きであり、下限がまだチェックされていない場合です。

通常の(*value)++では、共有valueに対する並行更新でカウントが失われる可能性があります。例では原子加算を使用しています。原子操作でも、ターゲット命令とアクセス制約を満たす必要があります。検証器はビジネス統計の正しさを証明するものではなく、カーネルやverifierの実装に欠陥がないことを保証するものでもありません。詳細はverifierドキュメントをご覧ください。

1回の実行でキーが存在するかどうかを、ロード時のすべてのパスの証明と分離して制御してください。このサンプルがたまたま安全であっても、NULLや境界外アクセスのパスが排除されていない限り、ロードは阻止されます。

図を準備しています
今回の検索成功は、すべてのパスが安全であることとは限らない

検証器は、到達可能なすべてのパスの型と境界を証明する必要があります。たまたまNULLでなく、境界内である実行サンプルは、別のNULL間接参照パスを排除するものではありません。

3. JITは解釈オーバーヘッドを低減するが、すべてのコストを消除するわけではない

JITがサポートされ有効化されている場合、BPF命令はターゲットマシンコードにコンパイルされます。他の利用可能な実行パスは、アーキテクチャとビルド構成に依存します。すべてのBPF命令に対して固定の「x86命令1~3命令に相当」と指定することはできません。呼び出し、原子操作、チェック、JITハードニングは生成コードに影響を与えます。

実行コストには、フック、マップ検索、ロック、イベントコピー、ユーザー空間での消費が含まれます。検証はロード段階で行われますが、境界の判断に使用されるソースプログラムが実行コードに残る可能性があるため、「すべての境界チェックは実行時にゼロオーバーヘッド」とは言えません。

4. Mapsは異なるセマンティクスを持つ状態オブジェクト

マップ適したデータ使用上の制約
ARRAY固定インデックス設定またはカウントインデックス範囲は固定、要素はハッシュのように削除不可
HASH疎なキーバリュー状態容量と更新失敗の処理が必要
PERCPU_ARRAY / HASHCPUごとのカウントユーザー空間で各CPUスロットを集計する必要があり、集計はアトミックスナップショットではない
LRU_HASH容量制限付きの近似キャッシュ淘汰により状態が失われるため、損失なくすべての保留中のリクエストを保存するには不向き
RINGBUFコンシューマーへのイベント出力スペース不足時に予約/出力が失敗する可能性がある
PROG_ARRAYtail callのターゲット通常の関数ポインタ配列ではない

per-CPUはCPU間の共有書き込み競合を減らしますが、マップ全体が「絶対ロックフリー」であることを意味するものではなく、ユーザー空間での読み取り時の整合性を自動的に解決するものでもありません。ハッシュの構造更新とvalue内の複数フィールドの原子的一貫性も区別する必要があります。

BPFグローバル変数も状態を保持できます。libbpfは関連するデータセクションをマップとして整理することが多いため、「プログラムは完全にステートレス」というのは過度な簡略化です。より詳細なセマンティクスについてはBPF mapsをご覧ください。

5. Ring bufferとperf bufferの選択

perf event arrayは、通常、per-CPU perfバッファと組み合わせてBPFイベントを出力します。BPF ringbufは、マルチプロデューサ・シングルコンシューマの共有リングバッファを提供し、複数のringbufでシャーディングすることも可能です。両方とも可変長レコードをサポートしており、perf bufferが固定長レコードのみをサポートすると記述してはいけません。

早期の予約が完了していないと、後のイベントも待たされる可能性がある

ringbufのreserve/submitパスは、予約済みレコードへの直接書き込みを許可します。早期に予約されたレコードがまだ完了していない場合、コンシューマーが後続のレコードを読み進める妨げになります。順序は同じringbuf内の予約シーケンスに対応しますが、CPU横断的なイベントの絶対発生時刻順序とは一致せず、複数のringbuf間でのグローバル順序とも一致しません。

どちらのソリューションでもイベントが失われる可能性があります。ringbufの予約失敗はカウントし、perf bufferのlost情報は処理する必要があります。タイムスタンプ、CPU、イベント識別子、および損失カウントはデータプロトコルの一部となるべきです。共有メモリかどうか、コピーが発生するかどうかは、具体的な出力APIと消費パスによって異なり、「perfは常に2回コピー、ringbufは常にコピーなし」と単純化してはいけません。詳細はBPF ring bufferをご覧ください。

6. BTFとCO-REの互換境界

BTFは型などの情報を記述します。CO-REはロード段階でターゲットBTFを利用して、フィールドオフセット、サイズ、存在性などのリロケーションを処理します。BPF_CORE_READ(task, pid)により、フィールドの移動後もアクセスが正しく機能しますが、カーネル内部ABI全体が安定したインターフェースになるわけではありません。

互換条件CO-REのみで解決可能か
フィールドオフセットの変化ターゲットBTFによるリロケーションで処理可能
フィールドの欠落適切な存在性ブランチまたは互換パスが必要
helper、kfunc、マップ、またはattachがサポートされていないフィールドリロケーションで補完できない
フィールドのセマンティクス変更、関数の消失、権限不足プログラム設計とデプロイメントで対応が必要

プログラムは最終的にターゲットバージョンのverifierを通過する必要があります。

/sys/kernel/btf/vmlinuxには通常、実行中のカーネルBTFが提供されますが、モジュールBTFには独自の利用条件があります。CO-REではターゲットマシンごとに完全なコンパイラをインストールする必要はありませんが、ビルドとリリースでは最低限の要件、互換パス、および検証マトリックスの計画が必要です。

7. 権限と観察ツール

tracing関連プログラムのロードには通常CAP_PERFMONが必要であり、ネットワーク管理操作にはCAP_NET_ADMINが関与する場合があります。CAP_BPFはすべてのアタッチに対するパスポートではありません。非特権BPFはsysctl、プログラムタイプ、およびその他の制限の影響を受けます。LSMやlockdownなども操作をさらに制限する可能性があります。

bpftoolをインストールした後、以下のコマンドはオブジェクトを読み取り専用でチェックするために使用されます。何が見えるかは権限に依存します。

bpftool prog show
bpftool map show
bpftool link show

bpftool feature probeは、関連操作を試すことで能力をプローブするため、単なる静的バージョンテーブルの読み取りとして扱ってはなりません。JIT命令の表示も権限とカーネル構成に依存し、すべてのプログラムにエクスポート可能なマシンコードがあるとは限りません。

続きを読む:プログラムタイプ、トレーシングとデバッグ。