1 ポイント 投稿者 GN⁺ 3 시간 전 | 1件のコメント | WhatsAppで共有
  • 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
  • この構造では、バイナリやライブラリのパスを書き換えやすい
    • たとえば /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#hellochroot と mount namespace を使って /tmp/fzakaria/store/nix/store/zi2bj2hlavv8q743li2s9diqbcpmrf9b-hello-2.12.3/ にインストールする
    • どちらの場合もハッシュ zi2bj2hlavv8q743li2s9diqbcpmrf9b は同じである
  • ハッシュが同じなら、https://cache.nixos.org のような バイナリ substituter の事前計算済み derivation を利用できる

namespace なしで store を変更するときのコスト

  • BazelBuck2 のようなツールは、自前のサンドボックス化のために 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 は適用できる箇所のひとつである
    • 現在の helloRUNPATH の例は /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 をサポートしていない
  • スクリプトの shebang も同じ制約を持つ
    • 例は #!/nix/store/gik3rh1vz2jlgnifb9dh6vc6sxwwz9jj-bash-5.3p9/bin/bash
    • カーネルは #! を解析する際に絶対パスを想定している
    • shebang でも現在 $ORIGIN はサポートされていない
  • カレントディレクトリ基準の相対パスは使えるが、別の場所からスクリプトを実行すると壊れるため信頼しにくい

再配置可能なバイナリへの提案

  • 真に 再配置可能なバイナリを作るには、カーネルの制約を回避するか変更する必要がある
  • 提案されているアプローチは 3 つある
    • Linux カーネルにパッチを当てて PT_INTERP と shebang で $ORIGIN をサポートする
    • すべてのバイナリを小さな静的バイナリで包み、ラッパーが自分の位置を計算したうえで動的リンカを実行する
    • ファイル位置も言語ごとの相対パス機能を活用するよう変更する
      • Python では __file__ により自分自身を基準にファイルへアクセスできる
  • 最も適したアプローチとしては、Linux カーネルのサポート拡張が提案されている
    • NixOS マシンでは、Nix によりカーネルへパッチを当ててこのサポートを追加できる
  • さらに各 derivation に、再配置可能かどうかを示す relocatable = true; メタデータを入れる案も提案されている

1件のコメント

 
GN⁺ 3 시간 전
Lobste.rs の意見
  • Linux カーネルで PT_INTERP$ORIGIN サポートが入るとよさそう。以前に静的ラッパーバイナリで試したことがあり、ほかにもいくつかの試みを見たが(良い例)、どれも優れていて巧妙なハックではあるものの、結局はハックに過ぎない
    セキュリティ上の含意はまだ十分に理解できていないので、整理された説明があると助かる
    Solaris はこれをサポートしているようで、安全に実現する方法があるのかもしれない。根拠は見つけにくいが、execve(2) マニュアルの ENOEXEC には、setuid/setgid プロセスイメージファイルの PT_INTERP プログラムヘッダが相対パスを持つか $ORIGIN トークンを使う場合は失敗すると書かれている

  • 多くのソフトウェアは ビルド時に埋め込まれたパスや定数を持っているので、正しく動かすには結局再コンパイルが必要なのでは?

    • すでにそういうケースは多く、主に shebang 行でそうなっている。Nix にはビルド時にこうした値を置換するためのヘルパーが多く含まれている
    • その通りだが、この問題はローカルストアとは無関係にもともと存在していた。おそらく下位 derivation は {foo} を通じて outPath を消費するが、上位依存関係の1つをローカルストアに変えるなら再ビルドが必要になる
  • musl 向けの dcrt1 パッチ(rcombs 作成)がこの問題をユーザー空間で解決する

  • /origin にマウントされたファイルシステムを作って、それが $ORIGIN として解釈されるようにはできないだろうか? そうすれば追加の構文なしで shebang と ELF の両方で動きそうだ

    • /origin を作ってマウントできるなら、単に /nix を作って nix-daemon を実行することもできるのでは?
    • 私の考えでは、NixOS の外でも互換性を持たせるには、追加のファイルシステムやデーモンのインストール・設定なしで済ませるのが目標だと思う
  • バイナリが相対パスで、おそらく安全ではなく自己提供の ローダーを指定できるなら、セキュリティ上のリスクにならないだろうか?

    • この脅威モデルでは、何を信頼し、何を信頼しないのか? どうせそのバイナリを実行するのであれば、単に検証済みのローダーで実行したいということなのか?
    • libc.so.6 のようなほかの動的リンクライブラリの検索パスを決める 環境変数と比べて、なぜそれがより危険だと考えるべきなのか?