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