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