Wenn macOS Tahoe Nix zerlegt: Es ist nicht der Installer, es ist BTM

NixmacOSLaunchDaemonTahoeDebugging

Nach dem Update auf macOS Tahoe funktionierte mein Multi-User-Nix-Setup nicht mehr. nix --version meldete "command not found", /nix war leer, und keine meiner devenv-Shells wollte starten. Mein erster Reflex war der offensichtliche — neu installieren — und dabei hätte ich beinahe 78 GB Nix Store zerstört. Das eigentliche Problem lag an einer Stelle, die ich nicht erwartet hatte: Apples Background Task Management blockierte still und heimlich System-LaunchDaemons.

Die Falle, die der Installer stellt

Der offizielle Installer produziert auf einem bereits installierten System folgende Ausgabe:

Action `encrypt_apfs_volume` errored
The keychain lacks a password for the already existing "Nix Store"
volume on disk `disk3`, consider removing the volume with
`diskutil apfs deleteVolume "Nix Store"`

Dieser Vorschlag ist gefährlich. diskutil apfs deleteVolume "Nix Store" löscht das APFS-Volume mit dem gesamten Nix Store unwiderruflich — jedes installierte Paket, jedes Profil, jede gepinnte Version. Der Installer weiß nicht, ob auf diesem Volume 0 GB oder 800 GB an Derivations liegen.

Das mentale Modell des Installers ist "Neuinstallation". Er ist kein Reparatur-Tool. Wenn diese Fehlermeldung nach einem macOS-Upgrade auftaucht, ist der richtige Schritt: stoppen und diagnostizieren — nicht dem Vorschlag folgen.

Was tatsächlich kaputt war

Ein paar Minuten Read-Only-Diagnose zeichneten ein ganz anderes Bild:

diskutil apfs list | grep -A3 "Nix Store"
# → Name: Nix Store, Mount Point: Not Mounted, 78 GB consumed
 
ls /nix
# → leer (nur der synthetic.conf-Mountpoint)
 
ls -la /Library/LaunchDaemons/org.nixos.*
# → beide plists vorhanden, korrekte Rechte, root:wheel
 
launchctl print system/org.nixos.darwin-store
# → Could not find service "org.nixos.darwin-store" in domain for system
 
security find-generic-password -s '<volume-uuid>' /Library/Keychains/System.keychain
# → Eintrag vorhanden, "Encrypted volume password"

Das Volume war intakt. Das Keychain-Passwort war intakt. Die plists waren intakt. /etc/synthetic.conf, /etc/fstab und der Nix-Block in /etc/zshrc — alles intakt. Das einzig Kaputte: launchd hatte beide Services vergessen. Ihre plists existierten auf der Platte, waren aber nicht in der System-Domäne registriert.

Der Fix dafür sollte eigentlich simpel sein:

sudo launchctl bootstrap system /Library/LaunchDaemons/org.nixos.darwin-store.plist
sudo launchctl bootstrap system /Library/LaunchDaemons/org.nixos.nix-daemon.plist

Das hat funktioniert. /nix wurde automatisch gemountet, der Daemon lief, nix --version lieferte eine Versionsnummer.

Dann habe ich rebootet, und alles war wieder kaputt.

Die Persistenz-Falle

launchctl bootstrap lädt einen Service in die aktuelle Session. Es persistiert diese Registrierung jedoch nicht — und das ist der subtile Punkt — über Reboots hinweg, so wie ich es erwartet hätte. Beim Boot durchsucht launchd zwar /Library/LaunchDaemons/ und lädt, was es findet. Aber seit Sonoma 14.6.1 ist dieser Boot-Zeit-Durchlauf durch ein separates System abgesichert: Background Task Management (BTM).

BTM betraf früher nur LaunchAgents und Helper-Apps. Seit 14.6.1 gated es auch LaunchDaemons unter /Library/LaunchDaemons/, wenn deren Executable keine Apple-Developer-Signatur trägt. Der bootstrap-Befehl umgeht BTM für die laufende Session. Die BTM-Datenbank selbst bleibt unberührt. Beim nächsten Boot gewinnt BTM.

Diagnose mit sfltool

Der Befehl, der das sichtbar gemacht hat, ist sfltool dumpbtm. Er gibt den Inhalt des BTM-Stores aus — jedes Background-Item, das macOS kennt, mitsamt Disposition. Durch grep gepiped:

sudo sfltool dumpbtm | grep -B2 -A8 -iE "nixos|darwin-store"

Ausgabe:

Flags: [ legacy ] (0x1)
Disposition: [enabled, disallowed, notified] (0x9)
Identifier: 16.org.nixos.nix-daemon
URL: file:///Library/LaunchDaemons/org.nixos.nix-daemon.plist
Executable Path: /bin/sh
Parent Identifier: Unknown Developer

Und ein identischer Block für org.nixos.darwin-store.

Zwei Dinge fallen auf:

  1. disallowed. Die Services sind in BTM registriert, aber BTM verhindert aktiv, dass sie beim Boot geladen werden. Genau dieser Mechanismus lässt manuelle launchctl bootstrap-Fixes über Reboots hinweg verdampfen.
  2. Executable Path: /bin/sh und Parent Identifier: Unknown Developer. Die plists rufen /bin/sh -c "..." auf, statt ein signiertes Binary direkt zu starten. BTM identifiziert den Service über seinen Entry Point — es sieht /bin/sh, es sieht keine Developer-Signatur, und behandelt das Ganze als untrusted generisches Shell-Skript.

Deshalb steht in den entsprechenden Einträgen in den Systemeinstellungen auch nirgends "Nix". Sie erscheinen als zwei "sh"-Einträge mit dem Untertitel "Item from unidentified developer".

Der tatsächliche Fix

  1. Systemeinstellungen → Allgemein → Anmeldeobjekte & Erweiterungen öffnen.
  2. Zu "Im Hintergrund erlauben" scrollen.
  3. Zwei Einträge mit Namen sh und Untertitel "Item from unidentified developer" suchen. (Möglicherweise liegen ein oder zwei benachbarte sh-Einträge plus ein unrelated shutdown-gpg-agent — letzteren ignorieren.)
  4. Beide auf EIN setzen.

Keine launchctl-Zeremonie nötig. Die Toggles schreiben direkt in den BTM-Store, und der nächste Reboot respektiert die neue Disposition.

Wenn die Toggles bereits auf EIN stehen, BTM aber trotzdem disallowed meldet — das kann passieren, wenn UI-State und BTM-State nach einem OS-Upgrade auseinanderdriften — einmal aus- und wieder einschalten, um das Schreiben zu erzwingen.

Was ich dem Ich von damals sagen würde

  • Wenn der nix-installer bei einer "Reinstallation" fehlschlägt, stoppen. Er ist kein Reparatur-Tool. Seine Fehlermeldungen gehen davon aus, dass du eine frische Installation willst, und die Vorschläge spiegeln das wider. Lies sie als "so würde ich von vorne anfangen", nicht als "so reparierst du dein System".
  • Diagnose vor Destruktion. diskutil apfs list, ls /nix, launchctl print, security find-generic-password — alles read-only, alles aussagekräftig. Fünf Minuten Probing haben mir gezeigt, dass das Volume in Ordnung war, die Keychain in Ordnung war und nur die launchd-Registrierung fehlte.
  • launchctl bootstrap ist ein Session-Fix, kein Persistenz-Fix. Wenn ein bootstrap funktioniert, aber den Reboot nicht überlebt, ist BTM fast sicher beteiligt. Die "versuch's nochmal mit mehr sudo"-Phase überspringen und direkt zu sfltool dumpbtm greifen.
  • sfltool dumpbtm ist das Diagnose-Tool für diese gesamte Problemkategorie. Quasi undokumentiert, selten erwähnt — und genau das, was man braucht, wenn ein LaunchDaemon, der autostarten sollte, es nicht tut.
  • Apples Trust-Perimeter wird immer weiter gezogen. Was früher ein relativ offenes Gebiet war (System-LaunchDaemons in /Library/LaunchDaemons/), wird heute durch BTM abgesichert. Jedes Third-Party-Tool, das auf einen Script-gewrappten Daemon angewiesen ist — Nix, Homebrew-Services, eigene Launch-Skripte, gpg-agent-Shutdown-Hooks — ist ein Kandidat für genau dieses Problem nach einem größeren macOS-Update.

Das Deprimierende daran: Das wird wieder passieren. Jedes macOS-Major-Release zieht eine weitere Sicherheits-Grenze, und das Symptom sieht immer nach "das Tool ist kaputt" aus, bevor sich herausstellt: "Das OS hat beschlossen, dem Tool nicht mehr zu trauen." Ein kurzes Incident-Log — was war kaputt, was war die Diagnose, wie sah der Fix aus — macht jede Runde zu einem schnelleren Recovery als die vorherige.

Quellen

Alle Artikel