7月21日、Farid Zakaria氏が「Linux kernel will support $ORIGIN, sort of」と題した記事を公開した。LinuxカーネルがeBPFを介して$ORIGINをサポートする仕組みがどのように実現されるかを詳述した記事だ。
Zakaria氏がLinuxカーネルメーリングリストにパッチを送ったのは今年6月のこと。VFSサブシステムに$ORIGINサポートを直接追加しようとしたその提案に、VFSメンテナのChristian Brauner氏が応答し、全く別のアプローチを提案してきた。しかもBrauner氏はその後休暇中に最初のドラフト実装を書き上げ、パッチシリーズとして提出。近日中にLinuxの-nextブランチに入る見通しだ。この話が注目を集めているのは、技術的な面白さだけではない。Nixのような再現性重視のパッケージマネージャが長年抱えてきた根本的な制約に、カーネルレベルの解決策が現れつつあるからだ。
そもそも$ORIGINとは何か
$ORIGINは、ELFバイナリの動的リンク時に使われる特殊変数で、「実行中のバイナリ自身が置かれているディレクトリ」を表す。共有ライブラリの検索パス(RPATH)を$ORIGIN/libのように相対指定することで、バイナリをどこに移動させてもパスが壊れない——いわゆるリロケータブル(再配置可能)なバイナリを実現するための仕組みだ。
この$ORIGINはRPATHでは古くから使える。しかしZakaria氏がNixで取り組んでいたのは、それをさらに一歩進めた問題だ。ELFのPT_INTERPセクション(ダイナミックリンカのパスを指定する箇所)やシェバン(#!)には$ORIGINが使えない。これがNixのリロケータブルバイナリ実現を阻む壁のひとつになっていた。
Nixはパッケージを/nix/store以下のハッシュ付きパスに配置する設計上、バイナリがどのパスに展開されるか実行前に確定しない。そのためインタープリタのパスをハードコードできず、$ORIGINによる相対解決が本質的に必要な仕組みだ。
eBPFとbinfmt_miscの組み合わせ
Brauner氏が提案したのは「eBPFを使ったプログラマブルなインタープリタ選択」だった。eBPFはCのサブセットで書いたプログラムをカーネル内で実行できる仕組みで、ロード前に検証器が安全性を確認し、JITコンパイルでネイティブ命令に変換される。ネットワーク・トレーシング・セキュリティの領域で広く使われているが、今回はバイナリ実行パスへの適用という珍しいユースケースだ。
binfmt_miscはLinuxカーネルが任意のバイナリ形式(スクリプトや異アーキテクチャのELFなど)を実行するためのインタープリタ登録機構だ。QEMUがx86以外のELFをそのまま実行できるのも、この仕組みによる。今回のパッチはこのbinfmt_miscにeBPFプログラムをフックできるようにするものだ。
具体的には、以下のようなeBPFプログラムで$ORIGINサポートを実装できる:
SEC("struct_ops.s/match")
bool BPF_PROG(nix_match, struct linux_binprm *bprm)
{
return !bpf_strncmp(bprm->buf, 4, "\x7f" "ELF");
}
SEC("struct_ops.s/load")
int BPF_PROG(nix_load, struct linux_binprm *bprm)
{
char path[256];
long n;
n = bpf_path_d_path(&bprm->file->f_path, path, sizeof(path));
if (n < 0)
return n;
return bpf_binprm_set_interp(bprm, path, sizeof(path));
}
登録はわずか2行だ:
> bpftool struct_ops register nix_origin.bpf.o /sys/fs/bpf
> echo ':origin:B::::nix:' > /proc/sys/fs/binfmt_misc/register
nix_matchでELFファイルを検出し、nix_loadでバイナリのパスを取得してインタープリタを動的に決定する。ファイルシステム上のどこにバイナリが置かれていても、そのパスを基準にした相対インタープリタ解決——$ORIGIN相当の動作——が実現する仕組みだ。
なおJohn Ericson氏も議論に加わり、NixやBuck・Bazelといったビルドシステムにとって固定されないインタープリタがなぜ有用かを説明している。
残る課題:プロセスIDの汚染
従来のbinfmt_miscには根本的な問題がある。登録されたインタープリタがプロセスそのものになってしまう点だ。実行したいバイナリはインタープリタの引数に格下げされる。この影響は次の形で現れる:
argv[0]や/proc/<pid>/cmdlineにインタープリタのパスが表示される/proc/self/exeがインタープリタを指す(リロケータブルプログラムが自身の場所を取得するために使う/proc/self/exeが壊れる)
これに対してBrauner氏は2つのディスパッチモードを追加するパッチシリーズを提出した。Zakaria氏が特に注目するのはローダー置換モード(Lフラグ)だ。このモードではカーネルがマッチしたバイナリをメインイメージとしてそのまま実行し、PT_INTERPに指定されたローダーだけを登録済みのものに差し替える。binfmt_miscがプロセスを「ハイジャック」するのではなく、単純にPT_INTERPを書き換えるだけになるため、argv[0]や/proc/self/exeが正しくバイナリ本体を指したまま、標準のダイナミックリンカをそのまま使える。これがZakaria氏の想定するNix統合にとって重要な点だ。
Nixへの統合計画
Zakaria氏はカーネルの-nextブランチへのマージとタグ付きリリースを追跡しつつ、NixOSモジュールとして$ORIGINサポートをブート時に登録する仕組みをアップストリームに送る計画だ。
実装は全ELFファイルに適用するのではなく、新たに提案するPT_INTERP_NIXセグメントを持つバイナリだけにeBPFハンドラを適用する方針だ(PT_INTERP_NIXは既存のELF仕様にはなく、Nix向けに新たに定義するセグメントとして議論されている)。既存のNixバイナリとの後方互換性を維持しつつ、明示的にオプトインしたバイナリだけがリロケータブル化される設計になっている。
詳細はLinux kernel will support $ORIGIN, sort ofを参照していただきたい。