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