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