nixos/grub: re-install grub on package change

Trigger re-installation of grub files when the store path of the grub
package changes.
This is consistent with how side-effects are executed in other parts of
NixOS, e.g. systemd service management, and ensures that the actually
running grub stays in sync with the grub package in the Nix store that
is part of the system closure.
In particular, this causes security patches or dependency changes to be
taken into effect without the need to bump the package version.

Impact:
This change causes a re-installation of grub for all systems,
due to the lack of a persisted grub store path in existing state files.

Background:
So far we only re-installed grub outside of the store when its its
version, the package name, or some flags changed.
This leads to the various security patches only fixing new installations
of grub and not existing ones. This has been like this for over a decade,
but it remains unclear why the implementation decision had been to
be overly cautious with modifying boot loader side effects.

Fixes #486315

PL-135147
This commit is contained in:
Oliver Schmidt
2026-03-02 11:51:26 +01:00
parent ec853f5437
commit 41997d49da
@@ -685,12 +685,14 @@ struct(GrubState => {
efi => '$',
devices => '$',
efiMountPoint => '$',
grub => '$',
grubEfi => '$',
extraGrubInstallArgs => '@',
});
# If you add something to the state file, only add it to the end
# because it is read line-by-line.
sub readGrubState {
my $defaultGrubState = GrubState->new(name => "", version => "", efi => "", devices => "", efiMountPoint => "", extraGrubInstallArgs => () );
my $defaultGrubState = GrubState->new(name => "", version => "", efi => "", devices => "", efiMountPoint => "", grub => "", grubEfi => "", extraGrubInstallArgs => () );
open my $fh, "<", "$bootPath/grub/state" or return $defaultGrubState;
local $/ = "\n";
my $name = <$fh>;
@@ -721,8 +723,10 @@ sub readGrubState {
}
my %jsonState = %{decode_json($jsonStateLine)};
my @extraGrubInstallArgs = exists($jsonState{'extraGrubInstallArgs'}) ? @{$jsonState{'extraGrubInstallArgs'}} : ();
my $grubValue = exists($jsonState{'grub'}) ? $jsonState{'grub'} : "";
my $grubEfiValue = exists($jsonState{'grubEfi'}) ? $jsonState{'grubEfi'} : "";
close $fh;
my $grubState = GrubState->new(name => $name, version => $version, efi => $efi, devices => $devices, efiMountPoint => $efiMountPoint, extraGrubInstallArgs => \@extraGrubInstallArgs );
my $grubState = GrubState->new(name => $name, version => $version, efi => $efi, devices => $devices, efiMountPoint => $efiMountPoint, extraGrubInstallArgs => \@extraGrubInstallArgs, grub => $grubValue, grubEfi => $grubEfiValue );
return $grubState
}
@@ -734,15 +738,16 @@ my @prevExtraGrubInstallArgs = @{$prevGrubState->extraGrubInstallArgs};
my $devicesDiffer = scalar (List::Compare->new( '-u', '-a', \@deviceTargets, \@prevDeviceTargets)->get_symmetric_difference());
my $extraGrubInstallArgsDiffer = scalar (List::Compare->new( '-u', '-a', \@extraGrubInstallArgs, \@prevExtraGrubInstallArgs)->get_symmetric_difference());
my $nameDiffer = get("fullName") ne $prevGrubState->name;
my $versionDiffer = get("fullVersion") ne $prevGrubState->version;
my $efiDiffer = $efiTarget ne $prevGrubState->efi;
my $efiMountPointDiffer = $efiSysMountPoint ne $prevGrubState->efiMountPoint;
# re-installing grub once the package store path changes is necessary, because
# introducing patches or adjusting builds does not always bump the version number
my $grubStorePathsDiffer = ($grub ne $prevGrubState->grub) || ($grubEfi ne $prevGrubState->grubEfi);
if (($ENV{'NIXOS_INSTALL_GRUB'} // "") eq "1") {
warn "NIXOS_INSTALL_GRUB env var deprecated, use NIXOS_INSTALL_BOOTLOADER";
$ENV{'NIXOS_INSTALL_BOOTLOADER'} = "1";
}
my $requireNewInstall = $devicesDiffer || $extraGrubInstallArgsDiffer || $nameDiffer || $versionDiffer || $efiDiffer || $efiMountPointDiffer || (($ENV{'NIXOS_INSTALL_BOOTLOADER'} // "") eq "1");
my $requireNewInstall = $devicesDiffer || $extraGrubInstallArgsDiffer || $efiDiffer || $efiMountPointDiffer || $grubStorePathsDiffer || (($ENV{'NIXOS_INSTALL_BOOTLOADER'} // "") eq "1");
# install a symlink so that grub can detect the boot drive
my $tmpDir = File::Temp::tempdir(CLEANUP => 1) or die "Failed to create temporary space: $!";
@@ -795,7 +800,9 @@ if ($requireNewInstall != 0) {
print $fh join( ",", @deviceTargets ), "\n" or die;
print $fh $efiSysMountPoint, "\n" or die;
my %jsonState = (
extraGrubInstallArgs => \@extraGrubInstallArgs
extraGrubInstallArgs => \@extraGrubInstallArgs,
grub => $grub,
grubEfi => $grubEfi
);
my $jsonStateLine = encode_json(\%jsonState);
print $fh $jsonStateLine, "\n" or die;