WARNING: THIS SITE IS A MIRROR OF GITHUB.COM / IT CANNOT LOGIN OR REGISTER ACCOUNTS / THE CONTENTS ARE PROVIDED AS-IS / THIS SITE ASSUMES NO RESPONSIBILITY FOR ANY DISPLAYED CONTENT OR LINKS / IF YOU FOUND SOMETHING MAY NOT GOOD FOR EVERYONE, CONTACT ADMIN AT ilovescratch@foxmail.com
Skip to content

Commit d288378

Browse files
committed
feat: add boot-counting support to lanzaboote
See https://systemd.io/AUTOMATIC_BOOT_ASSESSMENT/ Boot counting is controlled with a single option, `boot.lanzaboote.bootCounting.initialTries`, if this option is set to a non-zero value, new boot entries will be created with a counter added, set to the value specified in that option. If the option is set to zero, which is also the default, then no boot-counting counters will be added. Already existing entries will not be modified, so boot counting only applies to new boot entries.
1 parent 1f075a9 commit d288378

File tree

7 files changed

+255
-23
lines changed

7 files changed

+255
-23
lines changed

nix/modules/lanzaboote.nix

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,18 @@ in
126126
'';
127127
};
128128

129+
bootCounting = {
130+
initialTries = lib.mkOption {
131+
type = lib.types.ints.u32;
132+
default = 0;
133+
description = ''
134+
The number of boot counting tries to set for new boot entries.
135+
Setting this to zero, disables boot counting.
136+
See https://systemd.io/AUTOMATIC_BOOT_ASSESSMENT/
137+
'';
138+
};
139+
};
140+
129141
installCommand = lib.mkOption {
130142
type = lib.types.str;
131143
readOnly = true;
@@ -143,15 +155,17 @@ in
143155
--systemd-boot-loader-config ${loaderConfigFile} \
144156
--public-key ${cfg.publicKeyFile} \
145157
--private-key ${cfg.privateKeyFile} \
146-
--configuration-limit ${toString configurationLimit}'';
158+
--configuration-limit ${toString configurationLimit} \
159+
--bootcounting-initial-tries ${toString cfg.bootCounting.initialTries}'';
147160
defaultText = lib.literalExpression ''
148161
''${lib.getExe cfg.package} install \
149162
--system ''${config.boot.kernelPackages.stdenv.hostPlatform.system} \
150163
--systemd ''${config.systemd.package} \
151164
--systemd-boot-loader-config ''${loaderConfigFile} \
152165
--public-key ''${cfg.publicKeyFile} \
153166
--private-key ''${cfg.privateKeyFile} \
154-
--configuration-limit ''${toString configurationLimit}'';
167+
--configuration-limit ''${toString configurationLimit} \
168+
--bootcounting-initial-tries ''${toString cfg.bootCounting.initialTries}'';
155169
};
156170

157171
extraEfiSysMountPoints = lib.mkOption {

nix/tests/default.nix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ in
2525
export-efivars = runTest ./lanzaboote/export-efivars.nix;
2626
export-efivars-tpm = runTest ./lanzaboote/export-efivars-tpm.nix;
2727
extra-efi-partitions = runTest ./lanzaboote/extra-efi-partitions.nix;
28+
boot-counting = runTest ./lanzaboote/boot-counting.nix;
2829

2930
systemd-pcrlock = runTest ./lanzaboote/systemd-pcrlock.nix;
3031
systemd-measured-uki = runTest ./lanzaboote/systemd-measured-uki.nix;
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
let
2+
sortKey = "mySpecialSortKey";
3+
sortKeySpectator = "spectatorSortKey";
4+
sortKeyBad = "allthewayontop";
5+
in
6+
{
7+
name = "lanzaboote boot counting";
8+
9+
nodes = {
10+
machine = {
11+
imports = [ ./common/lanzaboote.nix ];
12+
13+
boot.lanzaboote = {
14+
inherit sortKey;
15+
bootCounting.initialTries = 2;
16+
};
17+
18+
# Boot is successful if multi-user is reached
19+
systemd.targets.boot-complete.after = [ "multi-user.target" ];
20+
21+
specialisation = {
22+
# This specialisation is ordered first, but it will fail to boot successfully
23+
bad.configuration =
24+
{ lib, ... }:
25+
{
26+
boot.lanzaboote.sortKey = lib.mkForce sortKeyBad;
27+
28+
systemd.services."failing" = {
29+
script = "exit 1";
30+
requiredBy = [ "boot-complete.target" ];
31+
before = [ "boot-complete.target" ];
32+
serviceConfig.Type = "oneshot";
33+
};
34+
};
35+
36+
# This specialisation should be ordered at the bottom, and we should never boot it
37+
spectator.configuration =
38+
{ lib, ... }:
39+
{
40+
boot.lanzaboote.sortKey = lib.mkForce sortKeySpectator;
41+
};
42+
# Specialisation with boot counting turned off, this should not matter
43+
spectator2.configuration =
44+
{ lib, ... }:
45+
{
46+
boot.lanzaboote = {
47+
sortKey = lib.mkForce sortKeySpectator;
48+
bootCounting.initialTries = lib.mkForce 0;
49+
};
50+
};
51+
};
52+
};
53+
};
54+
55+
testScript =
56+
{ nodes, ... }:
57+
let
58+
orig = nodes.machine.system.build.toplevel;
59+
bad = nodes.machine.specialisation.bad.configuration.system.build.toplevel;
60+
in
61+
(import ./common/image-helper.nix { inherit (nodes) machine; })
62+
+
63+
# python
64+
''
65+
orig = "${orig}"
66+
bad = "${bad}"
67+
68+
def check_current_system(system_path:str):
69+
current_sys = machine.succeed('readlink -f /run/current-system').strip()
70+
print(f'current system: {current_sys}')
71+
machine.succeed(f'test "{current_sys}" = "{system_path}"')
72+
73+
def check_boot_entry(
74+
generation_counter:int,
75+
specialisation:str|None,
76+
boot_counter:int=0,
77+
bad_counter:int=0
78+
):
79+
regex = rf"^/boot/EFI/Linux/nixos-generation-{generation_counter}"
80+
81+
if specialisation:
82+
regex += f"-specialisation-{specialisation}"
83+
84+
regex += r"-[0-9a-z]{52}"
85+
86+
if boot_counter != 0 or bad_counter != 0:
87+
regex += rf"\+{boot_counter}"
88+
if bad_counter != 0:
89+
regex += rf"-{bad_counter}"
90+
91+
regex += r"\.efi$"
92+
93+
find_command = rf"find /boot/EFI/Linux/ -regextype posix-extended -regex '{regex}' -type f | grep -q '.'"
94+
95+
machine.succeed(find_command)
96+
97+
machine.start()
98+
machine.wait_for_unit("multi-user.target")
99+
# Ensure we booted using an entry with counters enabled
100+
machine.succeed(
101+
"test -e /sys/firmware/efi/efivars/LoaderBootCountPath-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f"
102+
)
103+
print(machine.succeed("bootctl list"))
104+
check_current_system(bad)
105+
check_boot_entry(generation_counter=1, specialisation=None, boot_counter=2)
106+
check_boot_entry(generation_counter=1, specialisation="spectator", boot_counter=2)
107+
check_boot_entry(generation_counter=1, specialisation="bad", boot_counter=1, bad_counter=1)
108+
machine.shutdown()
109+
110+
machine.start()
111+
machine.wait_for_unit("multi-user.target")
112+
print(machine.succeed("bootctl list"))
113+
check_current_system(bad)
114+
check_boot_entry(generation_counter=1, specialisation=None, boot_counter=2)
115+
check_boot_entry(generation_counter=1, specialisation="spectator", boot_counter=2)
116+
check_boot_entry(generation_counter=1, specialisation="bad", boot_counter=0, bad_counter=2)
117+
machine.shutdown()
118+
119+
# Should boot back into original configuration
120+
machine.start()
121+
check_current_system(orig)
122+
machine.wait_for_unit("multi-user.target")
123+
machine.wait_for_unit("systemd-bless-boot.service")
124+
print(machine.succeed("bootctl list"))
125+
check_boot_entry(generation_counter=1, specialisation=None)
126+
check_boot_entry(generation_counter=1, specialisation="spectator", boot_counter=2)
127+
check_boot_entry(generation_counter=1, specialisation="bad", boot_counter=0, bad_counter=2)
128+
machine.shutdown()
129+
'';
130+
}

rust/tool/Cargo.lock

Lines changed: 33 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

rust/tool/systemd/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ serde_json = "1.0.145"
1717
sha2 = "0.10.9"
1818
tempfile = "3.23.0"
1919
nix = { version = "0.30.1", default-features = false, features = [ "fs" ] }
20+
regex = "1.12.2"
2021

2122
[dev-dependencies]
2223
assert_cmd = "2.1.1"

rust/tool/systemd/src/cli.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@ struct InstallCommand {
5454
#[arg(long, default_value_t = 1)]
5555
configuration_limit: usize,
5656

57+
/// Initial number of boot counting tries, set to zero to disable boot counting
58+
#[arg(long, default_value_t = 0)]
59+
bootcounting_initial_tries: u32,
60+
5761
/// EFI system partition mountpoint (e.g. efiSysMountPoint)
5862
esp: PathBuf,
5963

@@ -102,6 +106,7 @@ fn install(args: InstallCommand) -> Result<()> {
102106
args.systemd_boot_loader_config,
103107
local_signer,
104108
args.configuration_limit,
109+
args.bootcounting_initial_tries,
105110
args.esp,
106111
args.generations,
107112
)

0 commit comments

Comments
 (0)