Nix braucht verschiebbare Binärdateien
(fzakaria.com)- Der store-basierte Paketmanager Nix ist so konzipiert, dass Pakete unter einem festen Präfix wie
/nix/storeliegen. 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 mitchrootund Mount-Namespaces, bleibt derselbe Hash wie bei einem Build nach/nix/storeerhalten, sodass sich Binär-Caches wiecache.nixos.orgweiterverwenden lassen. - Ändert man das Store-Präfix ohne Namespace mit
local?store=/tmp/..., verändert sich der Hash; selbst ein einfacherhello-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-
RUNPATHstatt absoluter Pfade relative Pfade auf Basis von$ORIGINzu 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_INTERPund in Shebangs von Skripten kein$ORIGINunterstützt; als Lösungswege werden Kernel-Patches, statische Wrapper, sprachspezifische relative Pfade und Metadaten wierelocatable = 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
- Nix verwendet
- Diese Struktur erleichtert das Umschreiben von Pfaden in Binärdateien oder Bibliotheken.
- Zum Beispiel kann
/bin/bashin einen vollständigen Store-Pfad wie/nix/store/gik3rh1vz2jlgnifb9dh6vc6sxwwz9jj-bash-5.3p9/bin/bashumgeschrieben werden.
- Zum Beispiel kann
- 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#helloinstalliert nach/nix/store/zi2bj2hlavv8q743li2s9diqbcpmrf9b-hello-2.12.3/nix build --store /tmp/fzakaria/store nixpkgs#helloinstalliert mitchrootund Mount-Namespaces nach/tmp/fzakaria/store/nix/store/zi2bj2hlavv8q743li2s9diqbcpmrf9b-hello-2.12.3/- In beiden Fällen ist der Hash
zi2bj2hlavv8q743li2s9diqbcpmrf9bidentisch.
- 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
chrootund 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..., sondernqv3fhi1j9gh27fyds5n5b16yia8i6zn5.
- Das Beispielkommando verwendet
- 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
RUNPATHvon ELF-Binärdateien.- Das aktuelle
RUNPATH-Beispiel vonhelloist/nix/store/57iz36553175g3178pvxjij8z5rcsd4n-glibc-2.42-61/lib - Der Linux-Loader unterstützt
$ORIGIN, also das Verzeichnis der ausführbaren Datei. - Daher könnte
RUNPATHals$ORIGIN/../../57iz36553175g3178pvxjij8z5rcsd4n-glibc-2.42-61/libgeschrieben werden. - Dann würde sich der Hash bei einem anderen Store-Ort nicht ändern, und ein Rebuild wäre nicht nötig.
- Das aktuelle
- Bevor der Dynamic Linker aber
RUNPATHlesen 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
$ORIGINinPT_INTERP.
- Dieser Pfad steht im
- 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
$ORIGINderzeit nicht unterstützt.
- Ein Beispiel ist
- 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
$ORIGINinPT_INTERPund 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.
- In Python kann man etwa über
- Den Linux-Kernel patchen, sodass
- 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
Lobste.rs-Kommentare
Es wäre gut, wenn der Linux-Kernel
$ORIGIN-Unterstützung inPT_INTERPbekä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 derPT_INTERP-Programm-Header einer setuid/setgid-Prozess-Image-Datei einen relativen Pfad enthält oder das Token$ORIGINverwendet.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?
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
/origineingehängtes Dateisystem erstellen, das als$ORIGINaufgelöst wird? Dann müsste es ohne zusätzliche Syntax sowohl für Shebangs als auch für ELF funktionieren./originerstellen und mounten kann, könnte man dann nicht einfach/nixerstellen undnix-daemonstarten?Wäre es nicht ein Sicherheitsrisiko, wenn ein Binary über einen relativen Pfad einen möglicherweise unsicheren und selbst mitgelieferten Loader angeben könnte?
libc.so.6festlegen?