1 Punkte von GN⁺ 3 시간 전 | 1 Kommentare | Auf WhatsApp teilen
  • Der store-basierte Paketmanager Nix ist so konzipiert, dass Pakete unter einem festen Präfix wie /nix/store liegen. Das führt in rootless Nix-Umgebungen, in denen der Store ohne bestehende Nix-Installation oder Root-Rechte an einem anderen Ort liegen soll, zu erheblichen Einschränkungen.
  • Nutzt man --store /tmp/... zusammen mit chroot und Mount-Namespaces, bleibt derselbe Hash wie bei einem Build nach /nix/store erhalten, sodass sich Binär-Caches wie cache.nixos.org weiterverwenden lassen.
  • Ändert man das Store-Präfix ohne Namespace mit local?store=/tmp/..., verändert sich der Hash; selbst ein einfacher hello-Build kann dann den gesamten Abhängigkeitsgraphen ungültig machen und eine Neuübersetzung von GCC auslösen.
  • Der Kern des Vorschlags ist, im ELF-RUNPATH statt absoluter Pfade relative Pfade auf Basis von $ORIGIN zu verwenden, die der Linux-Dynamic-Linker unterstützt, damit sich Änderungen des Store-Orts nicht in Hashes und Rebuilds fortpflanzen.
  • Der eigentliche Engpass für echte Verschiebbarkeit ist, dass der Kernel in ELF-PT_INTERP und in Shebangs von Skripten kein $ORIGIN unterstützt; als Lösungswege werden Kernel-Patches, statische Wrapper, sprachspezifische relative Pfade und Metadaten wie relocatable = true; vorgeschlagen.

Konflikt zwischen festem Store-Präfix und rootless Nix

  • Store-basierte Systeme wie Nix und Guix speichern alle Pakete unter einem fest definierten Präfix.
    • Nix verwendet /nix/store
    • Guix verwendet /gnu/store
  • Diese Struktur erleichtert das Umschreiben von Pfaden in Binärdateien oder Bibliotheken.
    • Zum Beispiel kann /bin/bash in einen vollständigen Store-Pfad wie /nix/store/gik3rh1vz2jlgnifb9dh6vc6sxwwz9jj-bash-5.3p9/bin/bash umgeschrieben werden.
  • Es gibt aber Situationen, in denen man den Store an einem anderen Ort haben möchte.
    • Umgebungen, in denen Nix noch nicht installiert ist
    • Umgebungen ohne die nötigen Rechte
    • Daraus ergibt sich das Problem von „rootless Nix“
  • Nix kann zwar schon heute einen anderen Store-Pfad verwenden, aber je nach Methode bleibt der Hash erhalten oder eben nicht.
    • nix build nixpkgs#hello installiert nach /nix/store/zi2bj2hlavv8q743li2s9diqbcpmrf9b-hello-2.12.3/
    • nix build --store /tmp/fzakaria/store nixpkgs#hello installiert mit chroot und Mount-Namespaces nach /tmp/fzakaria/store/nix/store/zi2bj2hlavv8q743li2s9diqbcpmrf9b-hello-2.12.3/
    • In beiden Fällen ist der Hash zi2bj2hlavv8q743li2s9diqbcpmrf9b identisch.
  • Bleibt der Hash gleich, lassen sich vorberechnete Derivations eines binären Substituters wie https://cache.nixos.org nutzen.

Die Kosten eines Store-Wechsels ohne Namespace

  • Werkzeuge wie Bazel oder Buck2 verwenden für ihr eigenes Sandboxing möglicherweise bereits Namespaces.
    • Will man Nix in solche Ökosysteme integrieren, sinkt die praktische Nutzbarkeit durch verschachtelte User-Namespaces und Mount-Beschränkungen erheblich.
  • Ein alternatives Store-Präfix lässt sich auch ohne chroot und Mount-Namespaces angeben, aber dann tritt das Problem veränderter Hashes auf.
    • Das Beispielkommando verwendet --store 'local?store=/tmp/fzakaria/store&state=/tmp/fzakaria/state&log=/tmp/fzakaria/log'
    • Der resultierende hello-Pfad lautet /tmp/fzakaria/store/qv3fhi1j9gh27fyds5n5b16yia8i6zn5-hello-2.12.3
    • Der Hash ist dann nicht mehr zi2..., sondern qv3fhi1j9gh27fyds5n5b16yia8i6zn5.
  • Schon eine bloße Änderung der Zeichenkette des Store-Präfixes kann den gesamten Abhängigkeitsgraphen kaskadenartig ungültig machen.
    • Nur um aus einem anderen Ordner „Hello World“ auszugeben, könnte man am Ende vier Stunden lang GCC kompilieren.
    • Öffentliche Caches lassen sich in diesem Fall nicht nutzen.
  • Diese Einschränkung ist derzeit auch in der Nix-Dokumentation vermerkt.

Was $ORIGIN löst – und welche Kernel-Grenzen bleiben

  • Die Ursache des Problems ist, dass das Store-Präfix Teil der Derivation selbst ist und damit die Hash-Berechnung beeinflusst.
  • Verwendet man nicht überall das vollständige Store-Präfix, sondern relative Pfade, lassen sich Hash-Änderungen vermeiden.
  • Ein möglicher Ansatzpunkt ist der RUNPATH von ELF-Binärdateien.
    • Das aktuelle RUNPATH-Beispiel von hello ist /nix/store/57iz36553175g3178pvxjij8z5rcsd4n-glibc-2.42-61/lib
    • Der Linux-Loader unterstützt $ORIGIN, also das Verzeichnis der ausführbaren Datei.
    • Daher könnte RUNPATH als $ORIGIN/../../57iz36553175g3178pvxjij8z5rcsd4n-glibc-2.42-61/lib geschrieben werden.
    • Dann würde sich der Hash bei einem anderen Store-Ort nicht ändern, und ein Rebuild wäre nicht nötig.
  • Bevor der Dynamic Linker aber RUNPATH lesen kann, muss der Linux-Kernel zuerst den Dynamic Linker selbst laden.
    • Dieser Pfad steht im PT_INTERP-Header von ELF.
    • Ein Beispiel ist /nix/store/57iz36553175g3178pvxjij8z5rcsd4n-glibc-2.42-61/lib/ld-linux-x86-64.so.2
    • Der Linux-Kernel unterstützt derzeit kein $ORIGIN in PT_INTERP.
  • Für Shebangs in Skripten gilt dieselbe Einschränkung.
    • Ein Beispiel ist #!/nix/store/gik3rh1vz2jlgnifb9dh6vc6sxwwz9jj-bash-5.3p9/bin/bash
    • Beim Parsen von #! erwartet der Kernel einen absoluten Pfad.
    • Auch in Shebangs wird $ORIGIN derzeit nicht unterstützt.
  • Relative Pfade auf Basis des aktuellen Arbeitsverzeichnisses wären zwar möglich, brechen aber, sobald ein Skript von einem anderen Ort aus gestartet wird, und sind deshalb unzuverlässig.

Vorschläge auf dem Weg zu verschiebbaren Binärdateien

  • Um wirklich verschiebbare Binärdateien zu erhalten, muss man die Kernel-Beschränkungen umgehen oder ändern.
  • Vorgeschlagen werden drei Ansätze.
    • Den Linux-Kernel patchen, sodass $ORIGIN in PT_INTERP und Shebangs unterstützt wird
    • Alle Binärdateien in kleine statische Binärdateien einhüllen, wobei der Wrapper seine eigene Position bestimmt und dann den Dynamic Linker startet
    • Auch Dateipfade so umstellen, dass sprachspezifische Funktionen für relative Pfade genutzt werden
      • In Python kann man etwa über __file__ relativ zur eigenen Datei auf weitere Dateien zugreifen.
  • Als am besten geeigneter Ansatz wird eine Erweiterung der Linux-Kernel-Unterstützung vorgeschlagen.
    • Auf NixOS-Rechnern könnte man den Kernel mit Nix patchen und diese Unterstützung ergänzen.
  • Zusätzlich wird vorgeschlagen, jeder Derivation Metadaten wie relocatable = true; mitzugeben, um anzugeben, ob sie verschiebbar ist.

1 Kommentare

 
GN⁺ 3 시간 전
Lobste.rs-Kommentare
  • Es wäre gut, wenn der Linux-Kernel $ORIGIN-Unterstützung in PT_INTERP bekäme. Ich habe das früher einmal mit einem statischen Wrapper-Binary ausprobiert und auch mehrere andere Versuche gesehen (gutes Beispiel); alles großartige und clevere Hacks, aber am Ende eben Hacks.
    Die sicherheitsrelevanten Implikationen habe ich noch nicht wirklich verstanden, daher wäre eine gut aufbereitete Erklärung hilfreich.
    Solaris scheint das zu unterstützen, also gibt es vielleicht einen sicheren Weg dafür. Schwer zu belegen, aber im ENOEXEC-Abschnitt des execve(2)-Handbuchs steht, dass es fehlschlägt, wenn der PT_INTERP-Programm-Header einer setuid/setgid-Prozess-Image-Datei einen relativen Pfad enthält oder das Token $ORIGIN verwendet.

  • Viel Software enthält zur Build-Zeit fest eingetragene Pfade oder Konstanten; müsste man sie für korrektes Verhalten am Ende nicht doch neu kompilieren?

    • Das ist ohnehin schon oft so, vor allem bei Shebang-Zeilen. Nix enthält viele Hilfswerkzeuge, die solche Werte zur Build-Zeit ersetzen.
    • Stimmt, aber dieses Problem gab es unabhängig vom lokalen Store ohnehin schon. Wahrscheinlich konsumieren abgeleitete Derivations outPath über {foo}, und wenn man eine der oberen Abhängigkeiten auf einen lokalen Store umstellt, muss man neu bauen.
  • Ein dcrt1-Patch für musl (von rcombs) löst dieses Problem im User Space.

  • Könnte man nicht ein auf /origin eingehängtes Dateisystem erstellen, das als $ORIGIN aufgelöst wird? Dann müsste es ohne zusätzliche Syntax sowohl für Shebangs als auch für ELF funktionieren.

    • Wenn man /origin erstellen und mounten kann, könnte man dann nicht einfach /nix erstellen und nix-daemon starten?
    • Ich denke, das Ziel ist, auch außerhalb von NixOS kompatibel zu sein, also ohne Installation oder Konfiguration zusätzlicher Dateisysteme oder Daemons.
  • Wäre es nicht ein Sicherheitsrisiko, wenn ein Binary über einen relativen Pfad einen möglicherweise unsicheren und selbst mitgelieferten Loader angeben könnte?

    • Was genau wird in diesem Bedrohungsmodell als vertrauenswürdig bzw. nicht vertrauenswürdig angesehen? Wenn man das Binary ohnehin ausführen will, geht es dann nur darum, es mit einem verifizierten Loader auszuführen?
    • Warum sollte das als weniger sicher gelten als Umgebungsvariablen, die den Suchpfad für andere dynamisch gelinkte Bibliotheken wie libc.so.6 festlegen?