Nix には再配置可能なバイナリが必要
(fzakaria.com)- store ベースのパッケージマネージャである Nix は、
/nix/storeのような固定 prefix にパッケージを置くよう設計されているため、既存の Nix インストールや root 権限なしで別の場所に store を置こうとする rootless Nix 環境では制約が大きい --store /tmp/...とchroot・mount namespace を併用すると、既存の/nix/storeビルドと同じハッシュを維持でき、cache.nixos.orgのような バイナリキャッシュを引き続き利用できる- namespace なしで
local?store=/tmp/...により store prefix を変更するとハッシュが変わり、単純なhelloビルドでも依存グラフ全体の無効化や GCC の再コンパイルにつながりうる - 提案の核心は、ELF の
RUNPATHに絶対パスではなく Linux の動的リンカがサポートする$ORIGINベースの相対パスを使い、store の場所変更がハッシュや再コンパイルに波及しないようにすること - 実際に再配置可能性を阻むボトルネックは、カーネルが ELF
PT_INTERPとスクリプトの shebang で$ORIGINをサポートしていない点であり、カーネルパッチ・静的ラッパー・言語ごとの相対パス・relocatable = true;メタデータが解決策として提案されている
固定 store prefix と rootless Nix の衝突
- Nix や Guix のような store ベースのシステムは、すべてのパッケージを決められた prefix 配下に保存する
- Nix は
/nix/store - Guix は
/gnu/store
- Nix は
- この構造では、バイナリやライブラリのパスを書き換えやすい
- たとえば
/bin/bashを/nix/store/gik3rh1vz2jlgnifb9dh6vc6sxwwz9jj-bash-5.3p9/bin/bashのような完全な store パスに置き換えられる
- たとえば
- store を別の場所に置きたいケースもある
- Nix がまだインストールされていない環境
- 必要な権限がない環境
- このような状況が「rootless Nix」の問題につながる
- Nix は現在でも別の store パスを指定できるが、方法によって ハッシュを維持できるかどうかが分かれる
nix build nixpkgs#helloは/nix/store/zi2bj2hlavv8q743li2s9diqbcpmrf9b-hello-2.12.3/にインストールするnix build --store /tmp/fzakaria/store nixpkgs#helloはchrootと mount namespace を使って/tmp/fzakaria/store/nix/store/zi2bj2hlavv8q743li2s9diqbcpmrf9b-hello-2.12.3/にインストールする- どちらの場合もハッシュ
zi2bj2hlavv8q743li2s9diqbcpmrf9bは同じである
- ハッシュが同じなら、https://cache.nixos.org のような バイナリ substituter の事前計算済み derivation を利用できる
namespace なしで store を変更するときのコスト
- Bazel や Buck2 のようなツールは、自前のサンドボックス化のために namespace をすでに使っている可能性がある
- こうしたエコシステムに Nix を統合しようとすると、ネストした user namespace や mount 制限のため実用性が大きく落ちる
chrootや mount namespace なしでも代替の store prefix を指定することはできるが、ハッシュが変わるという欠点がある- 例のコマンドでは
--store 'local?store=/tmp/fzakaria/store&state=/tmp/fzakaria/state&log=/tmp/fzakaria/log'を使う - 結果の
helloパスは/tmp/fzakaria/store/qv3fhi1j9gh27fyds5n5b16yia8i6zn5-hello-2.12.3 - ハッシュは元の
zi2...ではなくqv3fhi1j9gh27fyds5n5b16yia8i6zn5に変わる
- 例のコマンドでは
- 単なる store prefix 文字列の変更が 依存グラフ全体を連鎖的に無効化する
- 別のフォルダで “Hello World” を表示したいだけでも、GCC を 4 時間コンパイルすることになりかねない
- この場合、公開キャッシュは利用できない
- この制約は現在の Nix ドキュメント にも明記されている
$ORIGIN が解決する部分と残るカーネルの制約
- 問題の原因は、store prefix が derivation 自体の一部であり、ハッシュ計算に影響することにある
- あらゆる場所で完全な store prefix を使うのではなく相対パスを使えば、ハッシュ変化を避けられる
- ELF バイナリの
RUNPATHは適用できる箇所のひとつである- 現在の
helloのRUNPATHの例は/nix/store/57iz36553175g3178pvxjij8z5rcsd4n-glibc-2.42-61/lib - Linux ローダーは、実行ファイルがあるディレクトリを意味する
$ORIGINをサポートしている - そのため
RUNPATHを$ORIGIN/../../57iz36553175g3178pvxjij8z5rcsd4n-glibc-2.42-61/libのように書ける - こうすれば store の場所が変わってもハッシュは変わらず、再コンパイルも不要になる
- 現在の
- しかし動的リンカが
RUNPATHを読む前に、Linux カーネルがまず動的リンカ自体をロードしなければならない- このパスは ELF の
PT_INTERPヘッダに保存される - 例は
/nix/store/57iz36553175g3178pvxjij8z5rcsd4n-glibc-2.42-61/lib/ld-linux-x86-64.so.2 - 現在の Linux カーネルは
PT_INTERPで$ORIGINをサポートしていない
- このパスは ELF の
- スクリプトの shebang も同じ制約を持つ
- 例は
#!/nix/store/gik3rh1vz2jlgnifb9dh6vc6sxwwz9jj-bash-5.3p9/bin/bash - カーネルは
#!を解析する際に絶対パスを想定している - shebang でも現在
$ORIGINはサポートされていない
- 例は
- カレントディレクトリ基準の相対パスは使えるが、別の場所からスクリプトを実行すると壊れるため信頼しにくい
再配置可能なバイナリへの提案
- 真に 再配置可能なバイナリを作るには、カーネルの制約を回避するか変更する必要がある
- 提案されているアプローチは 3 つある
- Linux カーネルにパッチを当てて
PT_INTERPと shebang で$ORIGINをサポートする - すべてのバイナリを小さな静的バイナリで包み、ラッパーが自分の位置を計算したうえで動的リンカを実行する
- ファイル位置も言語ごとの相対パス機能を活用するよう変更する
- Python では
__file__により自分自身を基準にファイルへアクセスできる
- Python では
- Linux カーネルにパッチを当てて
- 最も適したアプローチとしては、Linux カーネルのサポート拡張が提案されている
- NixOS マシンでは、Nix によりカーネルへパッチを当ててこのサポートを追加できる
- さらに各 derivation に、再配置可能かどうかを示す
relocatable = true;メタデータを入れる案も提案されている
1件のコメント
Lobste.rs の意見
Linux カーネルで
PT_INTERPの$ORIGINサポートが入るとよさそう。以前に静的ラッパーバイナリで試したことがあり、ほかにもいくつかの試みを見たが(良い例)、どれも優れていて巧妙なハックではあるものの、結局はハックに過ぎないセキュリティ上の含意はまだ十分に理解できていないので、整理された説明があると助かる
Solaris はこれをサポートしているようで、安全に実現する方法があるのかもしれない。根拠は見つけにくいが、
execve(2)マニュアルの ENOEXEC には、setuid/setgid プロセスイメージファイルのPT_INTERPプログラムヘッダが相対パスを持つか$ORIGINトークンを使う場合は失敗すると書かれている多くのソフトウェアは ビルド時に埋め込まれたパスや定数を持っているので、正しく動かすには結局再コンパイルが必要なのでは?
{foo}を通じてoutPathを消費するが、上位依存関係の1つをローカルストアに変えるなら再ビルドが必要になるmusl 向けの dcrt1 パッチ(rcombs 作成)がこの問題をユーザー空間で解決する
/originにマウントされたファイルシステムを作って、それが$ORIGINとして解釈されるようにはできないだろうか? そうすれば追加の構文なしで shebang と ELF の両方で動きそうだ/originを作ってマウントできるなら、単に/nixを作ってnix-daemonを実行することもできるのでは?バイナリが相対パスで、おそらく安全ではなく自己提供の ローダーを指定できるなら、セキュリティ上のリスクにならないだろうか?
libc.so.6のようなほかの動的リンクライブラリの検索パスを決める 環境変数と比べて、なぜそれがより危険だと考えるべきなのか?