ローダーが終了してもプログラムはアタッチされたままになり、検証器が通過してもカウントが失われる可能性があります。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間接参照パスを排除するものではありません。
{"id":"verifier-path-proof","title":"今回の検索成功は、すべてのパスが安全であることとは限らない","summary":"検証器は、到達可能なすべてのパスの型と境界を証明する必要があります。たまたまNULLでなく、境界内である実行サンプルは、別のNULL間接参照パスを排除するものではありません。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><label class=\"check\"><input type=\"checkbox\" id=\"guard\" checked><span data-i18n=\"guard\"></span></label><div class=\"control\"><label for=\"offset\" data-i18n=\"offset\"></label><output for=\"offset\" id=\"offset-value\"></output><input id=\"offset\" type=\"range\" min=\"-4\" max=\"20\" value=\"0\" step=\"4\"></div><label class=\"check\"><input type=\"checkbox\" id=\"found\" checked><span data-i18n=\"found\"></span></label><div class=\"two\"><div class=\"card\"><header data-i18n=\"nullPath\"></header><div id=\"null-path\" class=\"readout\"></div></div><div class=\"card\"><header data-i18n=\"valuePath\"></header><div id=\"value-path\" class=\"readout\"></div></div></div><div class=\"track-row\"><span data-i18n=\"map\"></span><div id=\"map\" class=\"track\"></div></div><div class=\"formula\" id=\"bounds\"></div><div class=\"card\"><header data-i18n=\"verdict\"></header><div id=\"verdict\" class=\"readout\"></div></div><div class=\"card sample\"><header data-i18n=\"sample\"></header><div id=\"sample\" class=\"readout\"></div></div><div class=\"copy\"><p id=\"explanation\" role=\"status\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"note\"></p></div><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n.readout{min-height:6.4em}.sample{margin-top:12px}.formula{min-height:4.8em}#map{display:grid;grid-template-columns:repeat(7,minmax(0,1fr))}#map .cell{min-height:40px;font-size:10px}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}function draw(){const guard=$('#guard').checked,o=+$('#offset').value,found=$('#found').checked,safe=o>=0&&o+4<=16;set('offset-value',o+' B');set('null-path',t(guard?'returned':'unsafe'));set('value-path',t(safe?'safe':'unsafe'));cells('map',[-4,0,4,8,12,16,20].map(n=>({text:String(n),cls:o===n?'gold':n>=0&&n<16?'on':'empty'})));set('bounds','0 ≤ '+o+' ∧ '+(o+4)+' ≤ 16\\naccess ['+o+','+(o+4)+')');set('verdict',t(guard&&safe?'accepted':'rejected'));set('sample',t(found?safe?'safe':'unsafe':guard?'returned':'unsafe'));set('explanation',t('note'));}$('#guard').onchange=$('#found').onchange=$('#offset').oninput=draw;draw();","audio":false,"strings":{"accepted":"これらのチェックは通過","found":"今回の実行:キーが存在","guard":"間接参照前に if (!p) return","heading":"今回の検索成功は、すべてのパスが安全であることとは限らない","intro":"NULLチェックを切り替え、4バイトアクセスのオフセットを変更し、キーが見つかる実行サンプルを選択します。実行サンプルとロード判定がなぜ異なるのかを観察してください。","map":"valueの4つの4バイトユニット;両側は範囲外","note":"オフセットを0に保ち、NULLチェックを外して、今回のキーが存在するようにします。サンプルのアクセスは安全ですが、ロードは拒否されます。検証されるのは可能なパスであり、この1回の入力を再生するだけではありません。","nullPath":"パス1:p = NULL","offset":"4バイトアクセス開始オフセット","rejected":"拒否:証明されていないパスが存在","reset":"リセット","returned":"安全な戻り値、間接参照なし","safe":"このパスでのアクセス範囲が成立","sample":"今回の実行結果","scope":"null許容マップ検索は16バイトの値を返し、4バイトアライメントのアクセス、キーは初期化済み、その他の検証条件はすべて満たされています。ここではNULLとアクセスの上下限の2種類のルールのみを示し、実際のverifierを実行したり、ビジネスロジックや並行カウントの正しさを証明したりしません。","scopeLabel":"モデル範囲と規約","unsafe":"不正アクセスの可能性がある","valuePath":"パス2:p が value を指す","verdict":"ロード時の2つの証明"}}
3. JITは解釈オーバーヘッドを低減するが、すべてのコストを消除するわけではない
JITがサポートされ有効化されている場合、BPF命令はターゲットマシンコードにコンパイルされます。他の利用可能な実行パスは、アーキテクチャとビルド構成に依存します。すべてのBPF命令に対して固定の「x86命令1~3命令に相当」と指定することはできません。呼び出し、原子操作、チェック、JITハードニングは生成コードに影響を与えます。
実行コストには、フック、マップ検索、ロック、イベントコピー、ユーザー空間での消費が含まれます。検証はロード段階で行われますが、境界の判断に使用されるソースプログラムが実行コードに残る可能性があるため、「すべての境界チェックは実行時にゼロオーバーヘッド」とは言えません。
4. Mapsは異なるセマンティクスを持つ状態オブジェクト
マップ 適したデータ 使用上の制約
ARRAY 固定インデックス設定またはカウント インデックス範囲は固定、要素はハッシュのように削除不可
HASH 疎なキーバリュー状態 容量と更新失敗の処理が必要
PERCPU_ARRAY / HASH CPUごとのカウント ユーザー空間で各CPUスロットを集計する必要があり、集計はアトミックスナップショットではない
LRU_HASH 容量制限付きの近似キャッシュ 淘汰により状態が失われるため、損失なくすべての保留中のリクエストを保存するには不向き
RINGBUF コンシューマーへのイベント出力 スペース不足時に予約/出力が失敗する可能性がある
PROG_ARRAY tail 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命令の表示も権限とカーネル構成に依存し、すべてのプログラムにエクスポート可能なマシンコードがあるとは限りません。
続きを読む:プログラムタイプ 、トレーシングとデバッグ 。