From 93cda47a1f2049c70074367445e6fb2bfb928154 Mon Sep 17 00:00:00 2001 From: r-vdp Date: Mon, 15 Dec 2025 16:12:51 +0100 Subject: [PATCH] nixos/switchable-system: introduce a standard pre-switch check to prevent switching under certain conditions This commit introduces "switch inhibitors" which are derivations that prevent a switch of a system to a new configuration if those derivations don't have the same hash in both configurations. This means that we can for instance add the systemd and dbus derivations such that users will be instructed to reboot their system when those derivations have changed instead of switching. This feature should be used sparingly, but it can make NixOS more robust by avoiding users switching to a configuration that can make their system unstable (like major updates of systemd, or new versions of dbus since the dbus and dbus-broker daemons cannot be restarted). The user can still force the switch by setting an env var. --- nixos/modules/services/system/dbus.nix | 8 + .../system/activation/switchable-system.nix | 141 ++++++++++++++---- nixos/modules/system/boot/systemd.nix | 4 + 3 files changed, 128 insertions(+), 25 deletions(-) diff --git a/nixos/modules/services/system/dbus.nix b/nixos/modules/services/system/dbus.nix index 3751fa5e57fd..a0edb9336208 100644 --- a/nixos/modules/services/system/dbus.nix +++ b/nixos/modules/services/system/dbus.nix @@ -174,6 +174,10 @@ in permissions = "u+rx,g+rx,o-rx"; }; + system.switch.inhibitors = [ + cfg.dbusPackage + ]; + systemd.services.dbus = { aliases = [ # hack aiding to prevent dbus from restarting when switching from dbus-broker back to dbus @@ -212,6 +216,10 @@ in cfg.brokerPackage ]; + system.switch.inhibitors = [ + cfg.brokerPackage + ]; + # Just to be sure we don't restart through the unit alias systemd.services.dbus.reloadIfChanged = true; systemd.user.services.dbus.reloadIfChanged = true; diff --git a/nixos/modules/system/activation/switchable-system.nix b/nixos/modules/system/activation/switchable-system.nix index a2e804e8a788..380b52b6825d 100644 --- a/nixos/modules/system/activation/switchable-system.nix +++ b/nixos/modules/system/activation/switchable-system.nix @@ -16,38 +16,129 @@ '') ]; - options.system.switch.enable = lib.mkOption { - type = lib.types.bool; - default = true; - description = '' - Whether to include the capability to switch configurations. + options.system.switch = { + enable = lib.mkOption { + type = lib.types.bool; + default = true; + description = '' + Whether to include the capability to switch configurations. - Disabling this makes the system unable to be reconfigured via `nixos-rebuild`. + Disabling this makes the system unable to be reconfigured via `nixos-rebuild`. - This is good for image based appliances where updates are handled - outside the image. Reducing features makes the image lighter and - slightly more secure. - ''; + This is good for image based appliances where updates are handled + outside the image. Reducing features makes the image lighter and + slightly more secure. + ''; + }; + + inhibitors = lib.mkOption { + type = lib.types.listOf lib.types.pathInStore; + default = [ ]; + description = '' + List of derivations that will prevent switching into a configuration when + they change. + This can be manually overridden on the command line if required. + ''; + }; }; config = lib.mkIf config.system.switch.enable { # Use a subshell so we can source makeWrapper's setup hook without # affecting the rest of activatableSystemBuilderCommands. - system.activatableSystemBuilderCommands = '' - ( - source ${pkgs.buildPackages.makeWrapper}/nix-support/setup-hook + system = { + activatableSystemBuilderCommands = '' + ( + source ${pkgs.buildPackages.makeWrapper}/nix-support/setup-hook - mkdir $out/bin - ln -sf ${lib.getExe pkgs.switch-to-configuration-ng} $out/bin/switch-to-configuration - wrapProgram $out/bin/switch-to-configuration \ - --set OUT $out \ - --set TOPLEVEL ''${!toplevelVar} \ - --set DISTRO_ID ${lib.escapeShellArg config.system.nixos.distroId} \ - --set INSTALL_BOOTLOADER ${lib.escapeShellArg config.system.build.installBootLoader} \ - --set PRE_SWITCH_CHECK ${lib.escapeShellArg config.system.preSwitchChecksScript} \ - --set LOCALE_ARCHIVE ${config.i18n.glibcLocales}/lib/locale/locale-archive \ - --set SYSTEMD ${config.systemd.package} - ) - ''; + mkdir $out/bin + ln -sf ${lib.getExe pkgs.switch-to-configuration-ng} $out/bin/switch-to-configuration + wrapProgram $out/bin/switch-to-configuration \ + --set OUT $out \ + --set TOPLEVEL ''${!toplevelVar} \ + --set DISTRO_ID ${lib.escapeShellArg config.system.nixos.distroId} \ + --set INSTALL_BOOTLOADER ${lib.escapeShellArg config.system.build.installBootLoader} \ + --set PRE_SWITCH_CHECK ${lib.escapeShellArg config.system.preSwitchChecksScript} \ + --set LOCALE_ARCHIVE ${config.i18n.glibcLocales}/lib/locale/locale-archive \ + --set SYSTEMD ${config.systemd.package} + ) + ''; + + systemBuilderCommands = '' + ln -s ${config.system.build.inhibitSwitch} $out/switch-inhibitors + ''; + + build.inhibitSwitch = pkgs.writeTextFile { + name = "switch-inhibitors"; + text = lib.concatMapStringsSep "\n" (drv: drv.outPath) config.system.switch.inhibitors; + }; + + preSwitchChecks.switchInhibitors = + let + realpath = lib.getExe' pkgs.coreutils "realpath"; + sha256sum = lib.getExe' pkgs.coreutils "sha256sum"; + diff = lib.getExe' pkgs.diffutils "diff"; + in + # bash + '' + incoming="''${1-}" + action="''${2-}" + + if [ "$action" == "boot" ]; then + echo "Not checking switch inhibitors (action = $action)" + exit + fi + + echo -n "Checking switch inhibitors..." + + booted_inhibitors="$(${realpath} /run/booted-system)/switch-inhibitors" + booted_inhibitors_sha="$( + if [ -f "$booted_inhibitors" ]; then + ${sha256sum} - < "$booted_inhibitors" + else + echo 'none' + fi + )" + + if [ "$booted_inhibitors_sha" == "none" ]; then + echo + echo "The previous configuration did not specify switch inhibitors, nothing to check." + exit + fi + + new_inhibitors="$(${realpath} "$incoming")/switch-inhibitors" + new_inhibitors_sha="$( + if [ -f "$new_inhibitors" ]; then + ${sha256sum} - < "$new_inhibitors" + else + echo 'none' + fi + )" + + if [ "$new_inhibitors_sha" == "none" ]; then + echo + echo "The new configuration does not specify switch inhibitors, nothing to check." + exit + fi + + if [ "$new_inhibitors_sha" != "$booted_inhibitors_sha" ]; then + echo + echo "Found diff in switch inhibitors:" + echo + ${diff} --color "$booted_inhibitors" "$new_inhibitors" + echo + echo "The new configuration contains changes to packages that were" + echo "listed as switch inhibitors." + echo + echo "If you really want to switch into this configuration directly, then" + echo "you can set NIXOS_NO_CHECK=1 to ignore these pre-switch checks." + echo + echo "WARNING: doing so might cause the switch to fail or your system to become unstable." + echo + exit 1 + else + echo " done" + fi + ''; + }; }; } diff --git a/nixos/modules/system/boot/systemd.nix b/nixos/modules/system/boot/systemd.nix index 7fb5c2259062..25bd4c624bfe 100644 --- a/nixos/modules/system/boot/systemd.nix +++ b/nixos/modules/system/boot/systemd.nix @@ -567,6 +567,10 @@ in ); }; + system.switch.inhibitors = [ + cfg.package + ]; + environment.systemPackages = [ cfg.package ]; environment.etc =