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