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