]> git.ipfire.org Git - thirdparty/systemd.git/blob - src/boot/bless-boot.c
Merge pull request #9437 from poettering/sd-boot-count
[thirdparty/systemd.git] / src / boot / bless-boot.c
1 /* SPDX-License-Identifier: LGPL-2.1+ */
2
3 #include <getopt.h>
4 #include <stdlib.h>
5
6 #include "alloc-util.h"
7 #include "bootspec.h"
8 #include "efivars.h"
9 #include "fd-util.h"
10 #include "fs-util.h"
11 #include "log.h"
12 #include "parse-util.h"
13 #include "path-util.h"
14 #include "util.h"
15 #include "verbs.h"
16 #include "virt.h"
17
18 static char *arg_path = NULL;
19
20 static int help(int argc, char *argv[], void *userdata) {
21
22 printf("%s [COMMAND] [OPTIONS...]\n"
23 "\n"
24 "Mark the boot process as good or bad.\n\n"
25 " -h --help Show this help\n"
26 " --version Print version\n"
27 " --path=PATH Path to the EFI System Partition (ESP)\n"
28 "\n"
29 "Commands:\n"
30 " good Mark this boot as good\n"
31 " bad Mark this boot as bad\n"
32 " indeterminate Undo any marking as good or bad\n",
33 program_invocation_short_name);
34
35 return 0;
36 }
37
38 static int parse_argv(int argc, char *argv[]) {
39 enum {
40 ARG_PATH = 0x100,
41 ARG_VERSION,
42 };
43
44 static const struct option options[] = {
45 { "help", no_argument, NULL, 'h' },
46 { "version", no_argument, NULL, ARG_VERSION },
47 { "path", required_argument, NULL, ARG_PATH },
48 {}
49 };
50
51 int c, r;
52
53 assert(argc >= 0);
54 assert(argv);
55
56 while ((c = getopt_long(argc, argv, "h", options, NULL)) >= 0)
57 switch (c) {
58
59 case 'h':
60 help(0, NULL, NULL);
61 return 0;
62
63 case ARG_VERSION:
64 return version();
65
66 case ARG_PATH:
67 r = free_and_strdup(&arg_path, optarg);
68 if (r < 0)
69 return log_oom();
70 break;
71
72 case '?':
73 return -EINVAL;
74
75 default:
76 assert_not_reached("Unknown option");
77 }
78
79 return 1;
80 }
81
82 static int acquire_esp(void) {
83 _cleanup_free_ char *np = NULL;
84 int r;
85
86 r = find_esp_and_warn(arg_path, false, &np, NULL, NULL, NULL, NULL);
87 if (r == -ENOKEY) /* find_esp_and_warn() doesn't warn in this one error case, but in all others */
88 return log_error_errno(r,
89 "Couldn't find EFI system partition. It is recommended to mount it to /boot or /efi.\n"
90 "Alternatively, use --path= to specify path to mount point.");
91 if (r < 0)
92 return r;
93
94 free_and_replace(arg_path, np);
95 log_debug("Using EFI System Partition at %s.", arg_path);
96
97 return 0;
98 }
99
100 static int parse_counter(
101 const char *path,
102 const char **p,
103 uint64_t *ret_left,
104 uint64_t *ret_done) {
105
106 uint64_t left, done;
107 const char *z, *e;
108 size_t k;
109 int r;
110
111 assert(path);
112 assert(p);
113
114 e = *p;
115 assert(e);
116 assert(*e == '+');
117
118 e++;
119
120 k = strspn(e, DIGITS);
121 if (k == 0) {
122 log_error("Can't parse empty 'tries left' counter from LoaderBootCountPath: %s", path);
123 return -EINVAL;
124 }
125
126 z = strndupa(e, k);
127 r = safe_atou64(z, &left);
128 if (r < 0)
129 return log_error_errno(r, "Failed to parse 'tries left' counter from LoaderBootCountPath: %s", path);
130
131 e += k;
132
133 if (*e == '-') {
134 e++;
135
136 k = strspn(e, DIGITS);
137 if (k == 0) { /* If there's a "-" there also needs to be at least one digit */
138 log_error("Can't parse empty 'tries done' counter from LoaderBootCountPath: %s", path);
139 return -EINVAL;
140 }
141
142 z = strndupa(e, k);
143 r = safe_atou64(z, &done);
144 if (r < 0)
145 return log_error_errno(r, "Failed to parse 'tries done' counter from LoaderBootCountPath: %s", path);
146
147 e += k;
148 } else
149 done = 0;
150
151 if (done == 0)
152 log_warning("The 'tries done' counter is currently at zero. This can't really be, after all we are running, and this boot must hence count as one. Proceeding anyway.");
153
154 *p = e;
155
156 if (ret_left)
157 *ret_left = left;
158
159 if (ret_done)
160 *ret_done = done;
161
162 return 0;
163 }
164
165 static int acquire_boot_count_path(
166 char **ret_path,
167 char **ret_prefix,
168 uint64_t *ret_left,
169 uint64_t *ret_done,
170 char **ret_suffix) {
171
172 _cleanup_free_ char *path = NULL, *prefix = NULL, *suffix = NULL;
173 const char *last, *e;
174 uint64_t left, done;
175 int r;
176
177 r = efi_get_variable_string(EFI_VENDOR_LOADER, "LoaderBootCountPath", &path);
178 if (r == -ENOENT)
179 return -EUNATCH; /* in this case, let the caller print a message */
180 if (r < 0)
181 return log_error_errno(r, "Failed to read LoaderBootCountPath EFI variable: %m");
182
183 efi_tilt_backslashes(path);
184
185 if (!path_is_normalized(path)) {
186 log_error("Path read from LoaderBootCountPath is not normalized, refusing: %s", path);
187 return -EINVAL;
188 }
189
190 if (!path_is_absolute(path)) {
191 log_error("Path read from LoaderBootCountPath is not absolute, refusing: %s", path);
192 return -EINVAL;
193 }
194
195 last = last_path_component(path);
196 e = strrchr(last, '+');
197 if (!e) {
198 log_error("Path read from LoaderBootCountPath does not contain a counter, refusing: %s", path);
199 return -EINVAL;
200 }
201
202 if (ret_prefix) {
203 prefix = strndup(path, e - path);
204 if (!prefix)
205 return log_oom();
206 }
207
208 r = parse_counter(path, &e, &left, &done);
209 if (r < 0)
210 return r;
211
212 if (ret_suffix) {
213 suffix = strdup(e);
214 if (!suffix)
215 return log_oom();
216
217 *ret_suffix = TAKE_PTR(suffix);
218 }
219
220 if (ret_path)
221 *ret_path = TAKE_PTR(path);
222 if (ret_prefix)
223 *ret_prefix = TAKE_PTR(prefix);
224 if (ret_left)
225 *ret_left = left;
226 if (ret_done)
227 *ret_done = done;
228
229 return 0;
230 }
231
232 static int make_good(const char *prefix, const char *suffix, char **ret) {
233 _cleanup_free_ char *good = NULL;
234
235 assert(prefix);
236 assert(suffix);
237 assert(ret);
238
239 /* Generate the path we'd use on good boots. This one is easy. If we are successful, we simple drop the counter
240 * pair entirely from the name. After all, we know all is good, and the logs will contain information about the
241 * tries we needed to come here, hence it's safe to drop the counters from the name. */
242
243 good = strjoin(prefix, suffix);
244 if (!good)
245 return -ENOMEM;
246
247 *ret = TAKE_PTR(good);
248 return 0;
249 }
250
251 static int make_bad(const char *prefix, uint64_t done, const char *suffix, char **ret) {
252 _cleanup_free_ char *bad = NULL;
253
254 assert(prefix);
255 assert(suffix);
256 assert(ret);
257
258 /* Generate the path we'd use on bad boots. Let's simply set the 'left' counter to zero, and keep the 'done'
259 * counter. The information might be interesting to boot loaders, after all. */
260
261 if (done == 0) {
262 bad = strjoin(prefix, "+0", suffix);
263 if (!bad)
264 return -ENOMEM;
265 } else {
266 if (asprintf(&bad, "%s+0-%" PRIu64 "%s", prefix, done, suffix) < 0)
267 return -ENOMEM;
268 }
269
270 *ret = TAKE_PTR(bad);
271 return 0;
272 }
273
274 static const char *skip_slash(const char *path) {
275 assert(path);
276 assert(path[0] == '/');
277
278 return path + 1;
279 }
280
281 static int verb_status(int argc, char *argv[], void *userdata) {
282
283 _cleanup_free_ char *path = NULL, *prefix = NULL, *suffix = NULL, *good = NULL, *bad = NULL;
284 _cleanup_close_ int fd = -1;
285 uint64_t left, done;
286 int r;
287
288 r = acquire_boot_count_path(&path, &prefix, &left, &done, &suffix);
289 if (r == -EUNATCH) { /* No boot count in place, then let's consider this a "clean" boot, as "good", "bad" or "indeterminate" don't apply. */
290 puts("clean");
291 return 0;
292 }
293 if (r < 0)
294 return r;
295
296 r = acquire_esp();
297 if (r < 0)
298 return r;
299
300 r = make_good(prefix, suffix, &good);
301 if (r < 0)
302 return log_oom();
303
304 r = make_bad(prefix, done, suffix, &bad);
305 if (r < 0)
306 return log_oom();
307
308 log_debug("Booted file: %s%s\n"
309 "The same modified for 'good': %s%s\n"
310 "The same modified for 'bad': %s%s\n",
311 arg_path, path,
312 arg_path, good,
313 arg_path, bad);
314
315 log_debug("Tries left: %" PRIu64"\n"
316 "Tries done: %" PRIu64"\n",
317 left, done);
318
319 fd = open(arg_path, O_DIRECTORY|O_CLOEXEC|O_RDONLY);
320 if (fd < 0)
321 return log_error_errno(errno, "Failed to open ESP '%s': %m", arg_path);
322
323 if (faccessat(fd, skip_slash(path), F_OK, 0) >= 0) {
324 puts("indeterminate");
325 return 0;
326 }
327 if (errno != ENOENT)
328 return log_error_errno(errno, "Failed to check if '%s' exists: %m", path);
329
330 if (faccessat(fd, skip_slash(good), F_OK, 0) >= 0) {
331 puts("good");
332 return 0;
333 }
334 if (errno != ENOENT)
335 return log_error_errno(errno, "Failed to check if '%s' exists: %m", good);
336
337 if (faccessat(fd, skip_slash(bad), F_OK, 0) >= 0) {
338 puts("bad");
339 return 0;
340 }
341 if (errno != ENOENT)
342 return log_error_errno(errno, "Failed to check if '%s' exists: %m", bad);
343
344 return log_error_errno(errno, "Couldn't determine boot state: %m");
345 }
346
347 static int verb_set(int argc, char *argv[], void *userdata) {
348 _cleanup_free_ char *path = NULL, *prefix = NULL, *suffix = NULL, *good = NULL, *bad = NULL, *parent = NULL;
349 const char *target, *source1, *source2;
350 _cleanup_close_ int fd = -1;
351 uint64_t done;
352 int r;
353
354 r = acquire_boot_count_path(&path, &prefix, NULL, &done, &suffix);
355 if (r == -EUNATCH) /* acquire_boot_count_path() won't log on its own for this specific error */
356 return log_error_errno(r, "Not booted with boot counting in effect.");
357 if (r < 0)
358 return r;
359
360 r = acquire_esp();
361 if (r < 0)
362 return r;
363
364 r = make_good(prefix, suffix, &good);
365 if (r < 0)
366 return log_oom();
367
368 r = make_bad(prefix, done, suffix, &bad);
369 if (r < 0)
370 return log_oom();
371
372 fd = open(arg_path, O_DIRECTORY|O_CLOEXEC|O_RDONLY);
373 if (fd < 0)
374 return log_error_errno(errno, "Failed to open ESP '%s': %m", arg_path);
375
376 /* Figure out what rename to what */
377 if (streq(argv[0], "good")) {
378 target = good;
379 source1 = path;
380 source2 = bad; /* Maybe this boot was previously marked as 'bad'? */
381 } else if (streq(argv[0], "bad")) {
382 target = bad;
383 source1 = path;
384 source2 = good; /* Maybe this boot was previously marked as 'good'? */
385 } else {
386 assert(streq(argv[0], "indeterminate"));
387 target = path;
388 source1 = good;
389 source2 = bad;
390 }
391
392 r = rename_noreplace(fd, skip_slash(source1), fd, skip_slash(target));
393 if (r == -EEXIST)
394 goto exists;
395 else if (r == -ENOENT) {
396
397 r = rename_noreplace(fd, skip_slash(source2), fd, skip_slash(target));
398 if (r == -EEXIST)
399 goto exists;
400 else if (r == -ENOENT) {
401
402 if (access(target, F_OK) >= 0) /* Hmm, if we can't find either source file, maybe the destination already exists? */
403 goto exists;
404
405 return log_error_errno(r, "Can't find boot counter source file for '%s': %m", target);
406 } else if (r < 0)
407 return log_error_errno(r, "Failed to rename '%s' to '%s': %m", source2, target);
408 else
409 log_debug("Successfully renamed '%s' to '%s'.", source2, target);
410
411 } else if (r < 0)
412 return log_error_errno(r, "Failed to rename '%s' to '%s': %m", source1, target);
413 else
414 log_debug("Successfully renamed '%s' to '%s'.", source1, target);
415
416 /* First, fsync() the directory these files are located in */
417 parent = dirname_malloc(path);
418 if (!parent)
419 return log_oom();
420
421 r = fsync_path_at(fd, skip_slash(parent));
422 if (r < 0)
423 log_debug_errno(errno, "Failed to synchronize image directory, ignoring: %m");
424
425 /* Secondly, syncfs() the whole file system these files are located in */
426 if (syncfs(fd) < 0)
427 log_debug_errno(errno, "Failed to synchronize ESP, ignoring: %m");
428
429 log_info("Marked boot as '%s'. (Boot attempt counter is at %" PRIu64".)", argv[0], done);
430
431 return 1;
432
433 exists:
434 log_debug("Operation already executed before, not doing anything.");
435 return 0;
436 }
437
438 int main(int argc, char *argv[]) {
439
440 static const Verb verbs[] = {
441 { "help", VERB_ANY, VERB_ANY, 0, help },
442 { "status", VERB_ANY, 1, VERB_DEFAULT, verb_status },
443 { "good", VERB_ANY, 1, VERB_MUST_BE_ROOT, verb_set },
444 { "bad", VERB_ANY, 1, VERB_MUST_BE_ROOT, verb_set },
445 { "indeterminate", VERB_ANY, 1, VERB_MUST_BE_ROOT, verb_set },
446 {}
447 };
448
449 int r;
450
451 log_parse_environment();
452 log_open();
453
454 r = parse_argv(argc, argv);
455 if (r <= 0)
456 goto finish;
457
458 if (detect_container() > 0) {
459 log_error("Marking a boot is not supported in containers.");
460 r = -EOPNOTSUPP;
461 goto finish;
462 }
463
464 if (!is_efi_boot()) {
465 log_error("Marking a boot is only supported on EFI systems.");
466 r = -EOPNOTSUPP;
467 goto finish;
468 }
469
470 r = dispatch_verb(argc, argv, verbs, NULL);
471
472 finish:
473 free(arg_path);
474
475 return r < 0 ? EXIT_FAILURE : EXIT_SUCCESS;
476 }