サンドボックスとシステムコールフィルタ(seccomp の原理)
プロセスが乗っ取られても被害が広がらない理由が分かる。seccomp-bpf がシステムコールを許可リストで絞ってカーネル攻撃面を縮める原理を、BPF フィルタの内部動作から解説。
- 1.サンドボックスの本質は権限を足すことではなく奪うこと。seccomp-bpf はプロセスが発行できるシステムコールを呼び出し入口で検査し、許可リスト外を弾いてカーネルの到達可能な攻撃面そのものを縮小する。
- 2.フィルタは syscall 番号・引数・呼び出し元アーキを入力に取る BPF プログラムで、ALLOW/ERRNO/TRAP/KILL を返す。一度ロードすると解除できず子プロセスに継承され、性能オーバーヘッドは1回の呼び出しあたり数十ナノ秒程度に収まる。
- 3.Chrome はレンダラを、systemd や OpenSSH や Docker はサービスやコンテナを、それぞれ seccomp で閉じ込める。コード実行を奪われても open や ptrace や execve が通らなければ、脱出には別の脆弱性が追加で必要になり攻撃難度が跳ね上がる。
サンドボックスとは「やれることを奪う」設計
サンドボックスの目的は、信頼できないコード、あるいは「いつか乗っ取られる」と想定するコードを、それができる操作の集合そのものを狭めた箱に閉じ込めることです。発想を「必要な権限を付与する」ではなく「不要な権限を剥奪する」へ反転させるのが核心で、これは最小権限の原則と多層防御の極端な実装に当たります。
プロセスがカーネルに何かを依頼する唯一の入口はシステムコールです。ファイルを開く、ソケットを張る、他プロセスを覗く、新しいプロセスを起動する——すべて syscall を通ります。逆に言えば、プロセスが発行できる syscall の集合を絞れば、そのプロセスがカーネルへ及ぼせる影響の上限が決まります。Linux の syscall は 300 以上あり、その一つ一つがカーネルコードへの入口、すなわち潜在的な攻撃面です。seccomp(secure computing mode)は、この syscall 面を呼び出し時点で機械的に絞り込む仕組みです。
seccomp の二つのモード
seccomp には歴史的に二つのモードがあります。
| モード | 許可される syscall | 用途 |
|---|---|---|
| SECCOMP_MODE_STRICT | read / write / exit / sigreturn の4つのみ | 計算専用の極端な隔離。実用アプリには厳しすぎる |
| SECCOMP_MODE_FILTER(seccomp-bpf) | BPF プログラムが許可・拒否を動的に判定 | 現代の実用的サンドボックスの主役 |
STRICT モードは 2005 年に「貸し CPU」用途(信頼できないコードを計算だけさせる)として入りましたが、4 つの syscall しか通らず汎用性がありません。実務で使われるのは 2012 年に追加された FILTER モード(seccomp-bpf) で、開発者が書いた BPF プログラムが各 syscall ごとに判定を下します。以降「seccomp」と言えばこの seccomp-bpf を指します。
フィルタプログラムの入力と出力
seccomp-bpf のフィルタは、cBPF(classic BPF)バイトコードで書かれた小さなプログラムです。各 syscall がカーネルに入る直前にこのプログラムが実行され、入力として seccomp_data 構造体を受け取ります。中身は以下の固定フィールドです。
nr— システムコール番号(例__NR_open)。arch— 呼び出し元のアーキテクチャ(AUDIT_ARCH_X86_64など)。instruction_pointer— 呼び出し時のプログラムカウンタ。args[0..5]— syscall の引数 6 つ(レジスタの生値)。
フィルタはこれらを検査し、32 ビットの戻り値を返します。上位ビットがアクション、下位がデータ(返す errno 値など)です。
| 戻り値アクション | 挙動 | 典型的な使いどころ |
|---|---|---|
| SECCOMP_RET_ALLOW | syscall を通常どおり実行 | 許可リストに載った安全な syscall |
| SECCOMP_RET_ERRNO | syscall を実行せず指定の errno を返す | 禁止だがプロセスは殺したくない(EPERM 等) |
| SECCOMP_RET_KILL_PROCESS | SIGSYS でプロセス全体を即終了 | 起こり得ないはずの危険な syscall |
| SECCOMP_RET_TRAP | SIGSYS をプロセスに配送 | ハンドラで個別対処したい |
| SECCOMP_RET_TRACE | ptrace 中の tracer に判断を委ねる | デバッガ・監視ツール連携 |
| SECCOMP_RET_USER_NOTIF | ユーザー空間の監視プロセスへ通知 | syscall を別プロセスで代行・検査 |
複数のフィルタを積み重ねた場合、カーネルは全フィルタを評価し、優先度が最も高い(最も厳しい)アクションを採用します。優先度は高い順に KILL_PROCESS → KILL_THREAD → TRAP → ERRNO → USER_NOTIF → TRACE → LOG → ALLOW で、どれか一つでも KILL を返せば KILL が勝ちます。つまりフィルタは追加で緩めることはできず、積めば積むほど締まる一方向の仕組みです。
許可リスト型フィルタの擬似コード
堅牢なフィルタは許可リスト型で設計します。アプリが実際に使う syscall だけを通し、それ以外はデフォルトで拒否する形です。考え方を擬似コードで示します。
seccomp フィルタの判定(許可リスト型):
# 1. まずアーキテクチャを固定する(重要)
if data.arch != AUDIT_ARCH_X86_64:
return KILL_PROCESS
# 2. 既知の危険 syscall を明示的に潰す
if data.nr in `{ptrace, execve, mount, keyctl, ...}`:
return KILL_PROCESS
# 3. 許可リストの syscall を通す
if data.nr in `{read, write, mmap, futex, exit_group, ...}`:
return ALLOW
# 4. 引数まで見て条件付き許可(例:実行可能メモリの禁止)
if data.nr == mmap and (data.args[2] & PROT_EXEC) != 0:
return ERRNO(EPERM)
# 5. 上記いずれにも当たらない未知の syscall は拒否
return ERRNO(EPERM)
x86-64 では同一プロセスから 32 ビット ABI の syscall を発行でき、その番号体系は 64 ビットとまったく異なります。アーキを固定せず番号だけで判定すると、攻撃者は禁止したい操作を 32 ビット番号(あるいは x32 ABI)で呼び出して許可リストをすり抜けられます。フィルタの先頭で arch を確認し、想定外の ABI を即 KILL するのが定石です。これは seccomp プロファイルの典型的な落とし穴です。
なぜ syscall 入口での検査が強いのか
seccomp の強さは、検査の位置にあります。capability などの権限チェックは「syscall を実行し始めた後、その内部で権限を確認する」ため、チェックに到達するまでにカーネルコードが動きます。一方 seccomp はsyscall がカーネルの本処理に入る前に判定するため、禁止された syscall は対応するカーネルコードに一切到達しません。
これが「攻撃面の縮小」の正確な意味です。あるカーネル関数にバグがあっても、そこへ至る唯一の syscall が seccomp で塞がれていれば、そのバグは**到達不能(unreachable)**になり実質的に無害化されます。コンテナの文脈での seccomp の役割も同じで、共有カーネルという弱点を直接補強します(コンテナ分離の原理(namespace・cgroup・seccomp))。
| 観点 | capability | seccomp-bpf |
|---|---|---|
| 制限する対象 | 特権操作の「権利」 | syscall の「呼び出し」そのもの |
| 判定の粒度 | 操作カテゴリ単位(約40種) | syscall 番号+引数値+アーキ |
| 評価の場所 | 各 syscall 内部の権限チェック | syscall 入口(本処理に入る前) |
| 効果 | root 権限の過剰付与を防ぐ | 到達可能な syscall を物理的に削減 |
ロードの不可逆性と前提条件
seccomp フィルタは prctl(PR_SET_SECCOMP, ...) または seccomp(2) システムコールでロードします。重要な性質が二つあります。
第一に、一度ロードしたフィルタは解除できず、fork/clone した子にも execve 後にも継承されます。これにより、サンドボックス化したプロセスが後から自分の制約を緩めて脱出する経路を塞ぎます。Chrome のレンダラは初期化(フォント列挙やライブラリロード)を終えた直後にフィルタをロードし、以後は最小の syscall 集合だけで動きます。
第二に、非特権プロセスがフィルタをロードするには no_new_privs ビットを立てる必要があります。これは「この後 execve しても setuid 等で特権を新たに獲得しない」という約束で、これがないと「制限付きプロセスが setuid バイナリを exec して権限昇格する」抜け道が残ってしまいます。no_new_privs と seccomp は対で機能します。
拒否リスト(危険な syscall を個別に禁止)は、カーネルに新 syscall が追加されるたびに穴が開きます。原理的に堅牢なのは許可リストで、未知の syscall がデフォルトで閉じます。実務では strace -f でアプリの全 syscall を洗い出し、SCMP_ACT_LOG(libseccomp の監査モード)で本番に近い負荷をかけて漏れを潰してから本適用します。最小化しすぎると稀なエラーパスで使う syscall を落としてクラッシュするため、観測に基づく構築が必須です。
実装の現実:libseccomp と性能
生の cBPF を手書きするのは煩雑なため、実務では libseccomp ライブラリを使います。seccomp_rule_add() でルールを宣言的に追加し、ライブラリが最適化された BPF プログラムへコンパイルします。Docker のデフォルトプロファイルや systemd の SystemCallFilter= ディレクティブ、OpenSSH の特権分離(privsep)プロセスも、この層の上に構築されています。
性能面では、フィルタは1回の syscall あたり数十ナノ秒程度の追加コストで済みます。BPF プログラムは分岐の線形列で、許可リストが大きいと逐次比較で遅くなるため、libseccomp は番号で二分探索する木構造に展開して定数時間に近づけます。攻撃面を縮小する代償としては十分小さく、これが seccomp が広く常用される理由です。
適用例:Chrome と Firefox
ブラウザは「敵のコードを毎秒実行する」前提の代表格で、seccomp の最も洗練された利用例です(詳細はブラウザサンドボックスとサイト分離の原理)。
- Chrome(Linux) — レンダラ・GPU・ネットワークサービスなど役割ごとにプロセスを分け、攻撃面の広いレンダラを seccomp-bpf で締める。ファイルやネットワークへの特権操作はすべてブローカ(ブラウザプロセス)への IPC に置換し、レンダラ自身は
openやconnectを呼べない。unprivileged user namespaceによるファイルシステム隔離と二重の壁を作る。 - Firefox — 同様にコンテンツプロセスを seccomp で隔離し、ファイル I/O などは IPC でブローカに「seccomp ブローカリング」として委譲する。Windows では AppContainer 等、macOS では Seatbelt(sandbox プロファイル)と、OS ごとに等価な機構を使い分ける。
いずれも狙いは同じです。レンダラのメモリ破壊バグ(メモリ破壊系のエクスプロイト)で攻撃者が任意コード実行を得ても、得られるのは権限を奪われたプロセスの制御にすぎません。端末を掌握するには、許された数少ない syscall や IPC 経由でブローカやカーネルのバグを突くサンドボックス脱出という第二の脆弱性が追加で要り、攻撃チェーンが一段長くなります。
seccomp は syscall の「呼び出し可否」と「引数の値」しか見られません。ポインタが指すメモリの中身は検査できない(TOCTOU を避けるためあえて見ない)ため、ファイルパスの内容で許可を分けるような判定はできません。また ioctl のように一つの syscall が無数の機能を内包する場合、番号だけでは粒度が粗すぎます。seccomp は攻撃を不可能にするのではなくコストを跳ね上げる層であり、capability・user namespace・LSM(SELinux/AppArmor)と重ねて初めて実用的な防御になります。
seccomp の本質は「syscall 入口で許可リスト判定し、到達可能な攻撃面を縮小する」こと。capability(root 権限の細分化、syscall 内部で評価)と区別する。モードは STRICT(read/write/exit/sigreturn のみ)と FILTER(BPF で動的判定)の二つ。戻り値は ALLOW/ERRNO/KILL/TRAP/USER_NOTIF など、複数フィルタは最も厳しいものが勝つ。ロードは不可逆で子に継承、非特権では no_new_privs が前提。アーキ(32/64 bit ABI)固定を忘れると許可リストをすり抜けられる。Chrome/Firefox のレンダラ隔離が代表的応用。
まとめ
seccomp-bpf は、プロセスが発行できるシステムコールを呼び出し入口で許可リスト判定し、到達可能なカーネル攻撃面そのものを物理的に縮小する仕組みです。capability が「root の権利」を、seccomp が「syscall の呼び出し」を絞るという役割分担を区別することが理解の鍵で、禁止された syscall は対応するカーネルコードに一切到達しないため、そこにバグがあっても無害化されます。
フィルタは syscall 番号・引数・アーキを入力に取る BPF プログラムで、許可リスト型で設計し、アーキ固定と no_new_privs を前提に不可逆ロードします。Chrome や Firefox のレンダラ隔離、Docker や systemd のサービス制限など応用は広く、いずれも「乗っ取られても被害を封じる」最小権限の徹底です。ただし seccomp 単体は万能ではなく、capability・名前空間・LSM と重ねる多層防御の一層として位置づけるのが原理的な正解です。
セキュリティ Article
サンドボックスとシステムコールフィルタ(seccomp の原理)を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
サンドボックス
比較で見る軸
難易度: advanced / カテゴリ: セキュリティ / タグ数: 6
導入後に効く点
フィルタは syscall 番号・引数・呼び出し元アーキを入力に取る BPF プログラムで、ALLOW/ERRNO/TRAP/KILL を返す。一度ロードすると解除できず子プロセスに継承され、性能オーバーヘッドは1回の呼び出しあたり数十ナノ秒程度に収まる。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- セキュリティ
- タグ数
- 6
判断チェックリスト
- 自社の用途が「サンドボックス / seccomp」に近いか確認する。
- 強みである「サンドボックスの本質は権限を足すことではなく奪うこと。seccomp-bpf はプロセスが発行できるシステムコールを呼び出し入口で検査し、許可リスト外を弾いてカーネルの到達可能な攻撃面そのものを縮小する。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。