Branch data Line data Source code
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 : 0 : STATIC_DESTRUCTOR_REGISTER(arg_path, strv_freep);
22 : :
23 : 0 : static int help(int argc, char *argv[], void *userdata) {
24 : :
25 : 0 : 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 : 0 : return 0;
39 : : }
40 : :
41 : 0 : 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 [ # # ]: 0 : assert(argc >= 0);
57 [ # # ]: 0 : assert(argv);
58 : :
59 [ # # ]: 0 : while ((c = getopt_long(argc, argv, "h", options, NULL)) >= 0)
60 [ # # # # : 0 : switch (c) {
# ]
61 : :
62 : 0 : case 'h':
63 : 0 : help(0, NULL, NULL);
64 : 0 : return 0;
65 : :
66 : 0 : case ARG_VERSION:
67 : 0 : return version();
68 : :
69 : 0 : case ARG_PATH:
70 : 0 : r = strv_extend(&arg_path, optarg);
71 [ # # ]: 0 : if (r < 0)
72 : 0 : return log_oom();
73 : 0 : break;
74 : :
75 : 0 : case '?':
76 : 0 : return -EINVAL;
77 : :
78 : 0 : default:
79 : 0 : assert_not_reached("Unknown option");
80 : : }
81 : :
82 : 0 : return 1;
83 : : }
84 : :
85 : 0 : static int acquire_path(void) {
86 : 0 : _cleanup_free_ char *esp_path = NULL, *xbootldr_path = NULL;
87 : : char **a;
88 : : int r;
89 : :
90 [ # # ]: 0 : if (!strv_isempty(arg_path))
91 : 0 : return 0;
92 : :
93 : 0 : r = find_esp_and_warn(NULL, false, &esp_path, NULL, NULL, NULL, NULL);
94 [ # # # # ]: 0 : if (r < 0 && r != -ENOKEY) /* ENOKEY means not found, and is the only error the function won't log about on its own */
95 : 0 : return r;
96 : :
97 : 0 : r = find_xbootldr_and_warn(NULL, false, &xbootldr_path, NULL);
98 [ # # # # ]: 0 : if (r < 0 && r != -ENOKEY)
99 : 0 : return r;
100 : :
101 [ # # # # ]: 0 : if (!esp_path && !xbootldr_path)
102 [ # # ]: 0 : 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 [ # # ]: 0 : if (esp_path)
107 : 0 : a = strv_new(esp_path, xbootldr_path);
108 : : else
109 : 0 : a = strv_new(xbootldr_path);
110 [ # # ]: 0 : if (!a)
111 : 0 : return log_oom();
112 : :
113 : 0 : strv_free_and_replace(arg_path, a);
114 : :
115 [ # # ]: 0 : if (DEBUG_LOGGING) {
116 : 0 : _cleanup_free_ char *j;
117 : :
118 : 0 : j = strv_join(arg_path, ":");
119 [ # # ]: 0 : log_debug("Using %s as boot loader drop-in search path.", j);
120 : : }
121 : :
122 : 0 : return 0;
123 : : }
124 : :
125 : 0 : 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 [ # # ]: 0 : assert(path);
137 [ # # ]: 0 : assert(p);
138 : :
139 : 0 : e = *p;
140 [ # # ]: 0 : assert(e);
141 [ # # ]: 0 : assert(*e == '+');
142 : :
143 : 0 : e++;
144 : :
145 : 0 : k = strspn(e, DIGITS);
146 [ # # ]: 0 : if (k == 0)
147 [ # # ]: 0 : return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
148 : : "Can't parse empty 'tries left' counter from LoaderBootCountPath: %s",
149 : : path);
150 : :
151 : 0 : z = strndupa(e, k);
152 : 0 : r = safe_atou64(z, &left);
153 [ # # ]: 0 : if (r < 0)
154 [ # # ]: 0 : return log_error_errno(r, "Failed to parse 'tries left' counter from LoaderBootCountPath: %s", path);
155 : :
156 : 0 : e += k;
157 : :
158 [ # # ]: 0 : if (*e == '-') {
159 : 0 : e++;
160 : :
161 : 0 : k = strspn(e, DIGITS);
162 [ # # ]: 0 : if (k == 0) /* If there's a "-" there also needs to be at least one digit */
163 [ # # ]: 0 : return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
164 : : "Can't parse empty 'tries done' counter from LoaderBootCountPath: %s",
165 : : path);
166 : :
167 : 0 : z = strndupa(e, k);
168 : 0 : r = safe_atou64(z, &done);
169 [ # # ]: 0 : if (r < 0)
170 [ # # ]: 0 : return log_error_errno(r, "Failed to parse 'tries done' counter from LoaderBootCountPath: %s", path);
171 : :
172 : 0 : e += k;
173 : : } else
174 : 0 : done = 0;
175 : :
176 [ # # ]: 0 : if (done == 0)
177 [ # # ]: 0 : 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 : 0 : *p = e;
180 : :
181 [ # # ]: 0 : if (ret_left)
182 : 0 : *ret_left = left;
183 : :
184 [ # # ]: 0 : if (ret_done)
185 : 0 : *ret_done = done;
186 : :
187 : 0 : return 0;
188 : : }
189 : :
190 : 0 : 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 : 0 : _cleanup_free_ char *path = NULL, *prefix = NULL, *suffix = NULL;
198 : : const char *last, *e;
199 : : uint64_t left, done;
200 : : int r;
201 : :
202 : 0 : r = efi_get_variable_string(EFI_VENDOR_LOADER, "LoaderBootCountPath", &path);
203 [ # # ]: 0 : if (r == -ENOENT)
204 : 0 : return -EUNATCH; /* in this case, let the caller print a message */
205 [ # # ]: 0 : if (r < 0)
206 [ # # ]: 0 : return log_error_errno(r, "Failed to read LoaderBootCountPath EFI variable: %m");
207 : :
208 : 0 : efi_tilt_backslashes(path);
209 : :
210 [ # # ]: 0 : if (!path_is_normalized(path))
211 [ # # ]: 0 : return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
212 : : "Path read from LoaderBootCountPath is not normalized, refusing: %s",
213 : : path);
214 : :
215 [ # # ]: 0 : if (!path_is_absolute(path))
216 [ # # ]: 0 : return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
217 : : "Path read from LoaderBootCountPath is not absolute, refusing: %s",
218 : : path);
219 : :
220 : 0 : last = last_path_component(path);
221 : 0 : e = strrchr(last, '+');
222 [ # # ]: 0 : if (!e)
223 [ # # ]: 0 : return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
224 : : "Path read from LoaderBootCountPath does not contain a counter, refusing: %s",
225 : : path);
226 : :
227 [ # # ]: 0 : if (ret_prefix) {
228 : 0 : prefix = strndup(path, e - path);
229 [ # # ]: 0 : if (!prefix)
230 : 0 : return log_oom();
231 : : }
232 : :
233 : 0 : r = parse_counter(path, &e, &left, &done);
234 [ # # ]: 0 : if (r < 0)
235 : 0 : return r;
236 : :
237 [ # # ]: 0 : if (ret_suffix) {
238 : 0 : suffix = strdup(e);
239 [ # # ]: 0 : if (!suffix)
240 : 0 : return log_oom();
241 : :
242 : 0 : *ret_suffix = TAKE_PTR(suffix);
243 : : }
244 : :
245 [ # # ]: 0 : if (ret_path)
246 : 0 : *ret_path = TAKE_PTR(path);
247 [ # # ]: 0 : if (ret_prefix)
248 : 0 : *ret_prefix = TAKE_PTR(prefix);
249 [ # # ]: 0 : if (ret_left)
250 : 0 : *ret_left = left;
251 [ # # ]: 0 : if (ret_done)
252 : 0 : *ret_done = done;
253 : :
254 : 0 : return 0;
255 : : }
256 : :
257 : 0 : static int make_good(const char *prefix, const char *suffix, char **ret) {
258 : 0 : _cleanup_free_ char *good = NULL;
259 : :
260 [ # # ]: 0 : assert(prefix);
261 [ # # ]: 0 : assert(suffix);
262 [ # # ]: 0 : 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 : 0 : good = strjoin(prefix, suffix);
269 [ # # ]: 0 : if (!good)
270 : 0 : return -ENOMEM;
271 : :
272 : 0 : *ret = TAKE_PTR(good);
273 : 0 : return 0;
274 : : }
275 : :
276 : 0 : static int make_bad(const char *prefix, uint64_t done, const char *suffix, char **ret) {
277 : 0 : _cleanup_free_ char *bad = NULL;
278 : :
279 [ # # ]: 0 : assert(prefix);
280 [ # # ]: 0 : assert(suffix);
281 [ # # ]: 0 : 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 [ # # ]: 0 : if (done == 0) {
287 : 0 : bad = strjoin(prefix, "+0", suffix);
288 [ # # ]: 0 : if (!bad)
289 : 0 : return -ENOMEM;
290 : : } else {
291 [ # # ]: 0 : if (asprintf(&bad, "%s+0-%" PRIu64 "%s", prefix, done, suffix) < 0)
292 : 0 : return -ENOMEM;
293 : : }
294 : :
295 : 0 : *ret = TAKE_PTR(bad);
296 : 0 : return 0;
297 : : }
298 : :
299 : 0 : static const char *skip_slash(const char *path) {
300 [ # # ]: 0 : assert(path);
301 [ # # ]: 0 : assert(path[0] == '/');
302 : :
303 : 0 : return path + 1;
304 : : }
305 : :
306 : 0 : static int verb_status(int argc, char *argv[], void *userdata) {
307 : 0 : _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 : 0 : r = acquire_boot_count_path(&path, &prefix, &left, &done, &suffix);
313 [ # # ]: 0 : 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 : 0 : puts("clean");
315 : 0 : return 0;
316 : : }
317 [ # # ]: 0 : if (r < 0)
318 : 0 : return r;
319 : :
320 : 0 : r = acquire_path();
321 [ # # ]: 0 : if (r < 0)
322 : 0 : return r;
323 : :
324 : 0 : r = make_good(prefix, suffix, &good);
325 [ # # ]: 0 : if (r < 0)
326 : 0 : return log_oom();
327 : :
328 : 0 : r = make_bad(prefix, done, suffix, &bad);
329 [ # # ]: 0 : if (r < 0)
330 : 0 : return log_oom();
331 : :
332 [ # # ]: 0 : 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 [ # # ]: 0 : log_debug("Tries left: %" PRIu64"\n"
340 : : "Tries done: %" PRIu64"\n",
341 : : left, done);
342 : :
343 [ # # # # ]: 0 : STRV_FOREACH(p, arg_path) {
344 [ # # # ]: 0 : _cleanup_close_ int fd = -1;
345 : :
346 : 0 : fd = open(*p, O_DIRECTORY|O_CLOEXEC|O_RDONLY);
347 [ # # ]: 0 : if (fd < 0) {
348 [ # # ]: 0 : if (errno == ENOENT)
349 : 0 : continue;
350 : :
351 [ # # ]: 0 : return log_error_errno(errno, "Failed to open $BOOT partition '%s': %m", *p);
352 : : }
353 : :
354 [ # # ]: 0 : if (faccessat(fd, skip_slash(path), F_OK, 0) >= 0) {
355 : 0 : puts("indeterminate");
356 : 0 : return 0;
357 : : }
358 [ # # ]: 0 : if (errno != ENOENT)
359 [ # # ]: 0 : return log_error_errno(errno, "Failed to check if '%s' exists: %m", path);
360 : :
361 [ # # ]: 0 : if (faccessat(fd, skip_slash(good), F_OK, 0) >= 0) {
362 : 0 : puts("good");
363 : 0 : return 0;
364 : : }
365 : :
366 [ # # ]: 0 : if (errno != ENOENT)
367 [ # # ]: 0 : return log_error_errno(errno, "Failed to check if '%s' exists: %m", good);
368 : :
369 [ # # ]: 0 : if (faccessat(fd, skip_slash(bad), F_OK, 0) >= 0) {
370 : 0 : puts("bad");
371 : 0 : return 0;
372 : : }
373 [ # # ]: 0 : if (errno != ENOENT)
374 [ # # ]: 0 : 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 [ # # ]: 0 : return log_error_errno(SYNTHETIC_ERRNO(EBUSY), "Couldn't determine boot state: %m");
380 : : }
381 : :
382 : 0 : static int verb_set(int argc, char *argv[], void *userdata) {
383 : 0 : _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 : 0 : r = acquire_boot_count_path(&path, &prefix, NULL, &done, &suffix);
390 [ # # ]: 0 : if (r == -EUNATCH) /* acquire_boot_count_path() won't log on its own for this specific error */
391 [ # # ]: 0 : return log_error_errno(r, "Not booted with boot counting in effect.");
392 [ # # ]: 0 : if (r < 0)
393 : 0 : return r;
394 : :
395 : 0 : r = acquire_path();
396 [ # # ]: 0 : if (r < 0)
397 : 0 : return r;
398 : :
399 : 0 : r = make_good(prefix, suffix, &good);
400 [ # # ]: 0 : if (r < 0)
401 : 0 : return log_oom();
402 : :
403 : 0 : r = make_bad(prefix, done, suffix, &bad);
404 [ # # ]: 0 : if (r < 0)
405 : 0 : return log_oom();
406 : :
407 : : /* Figure out what rename to what */
408 [ # # ]: 0 : if (streq(argv[0], "good")) {
409 : 0 : target = good;
410 : 0 : source1 = path;
411 : 0 : source2 = bad; /* Maybe this boot was previously marked as 'bad'? */
412 [ # # ]: 0 : } else if (streq(argv[0], "bad")) {
413 : 0 : target = bad;
414 : 0 : source1 = path;
415 : 0 : source2 = good; /* Maybe this boot was previously marked as 'good'? */
416 : : } else {
417 [ # # ]: 0 : assert(streq(argv[0], "indeterminate"));
418 : 0 : target = path;
419 : 0 : source1 = good;
420 : 0 : source2 = bad;
421 : : }
422 : :
423 [ # # # # ]: 0 : STRV_FOREACH(p, arg_path) {
424 [ # # # # ]: 0 : _cleanup_close_ int fd = -1;
425 : :
426 : 0 : fd = open(*p, O_DIRECTORY|O_CLOEXEC|O_RDONLY);
427 [ # # ]: 0 : if (fd < 0)
428 [ # # ]: 0 : return log_error_errno(errno, "Failed to open $BOOT partition '%s': %m", *p);
429 : :
430 : 0 : r = rename_noreplace(fd, skip_slash(source1), fd, skip_slash(target));
431 [ # # ]: 0 : if (r == -EEXIST)
432 : 0 : goto exists;
433 [ # # ]: 0 : else if (r == -ENOENT) {
434 : :
435 : 0 : r = rename_noreplace(fd, skip_slash(source2), fd, skip_slash(target));
436 [ # # ]: 0 : if (r == -EEXIST)
437 : 0 : goto exists;
438 [ # # ]: 0 : else if (r == -ENOENT) {
439 : :
440 [ # # ]: 0 : 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 : 0 : goto exists;
442 : :
443 [ # # ]: 0 : if (errno != ENOENT)
444 [ # # ]: 0 : 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 : 0 : continue;
448 [ # # ]: 0 : } else if (r < 0)
449 [ # # ]: 0 : return log_error_errno(r, "Failed to rename '%s' to '%s': %m", source2, target);
450 : : else
451 [ # # ]: 0 : log_debug("Successfully renamed '%s' to '%s'.", source2, target);
452 : :
453 [ # # ]: 0 : } else if (r < 0)
454 [ # # ]: 0 : return log_error_errno(r, "Failed to rename '%s' to '%s': %m", source1, target);
455 : : else
456 [ # # ]: 0 : log_debug("Successfully renamed '%s' to '%s'.", source1, target);
457 : :
458 : : /* First, fsync() the directory these files are located in */
459 : 0 : parent = dirname_malloc(target);
460 [ # # ]: 0 : if (!parent)
461 : 0 : return log_oom();
462 : :
463 : 0 : r = fsync_path_at(fd, skip_slash(parent));
464 [ # # ]: 0 : if (r < 0)
465 [ # # ]: 0 : 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 [ # # ]: 0 : if (syncfs(fd) < 0)
469 [ # # ]: 0 : log_debug_errno(errno, "Failed to synchronize $BOOT partition, ignoring: %m");
470 : :
471 [ # # ]: 0 : log_info("Marked boot as '%s'. (Boot attempt counter is at %" PRIu64".)", argv[0], done);
472 : : }
473 : :
474 [ # # ]: 0 : log_error_errno(SYNTHETIC_ERRNO(EBUSY), "Can't find boot counter source file for '%s': %m", target);
475 : 0 : return 1;
476 : :
477 : 0 : exists:
478 [ # # ]: 0 : log_debug("Operation already executed before, not doing anything.");
479 : 0 : return 0;
480 : : }
481 : :
482 : 0 : 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 : 0 : log_parse_environment();
495 : 0 : log_open();
496 : :
497 : 0 : r = parse_argv(argc, argv);
498 [ # # ]: 0 : if (r <= 0)
499 : 0 : return r;
500 : :
501 [ # # ]: 0 : if (detect_container() > 0)
502 [ # # ]: 0 : return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP),
503 : : "Marking a boot is not supported in containers.");
504 : :
505 [ # # ]: 0 : if (!is_efi_boot())
506 [ # # ]: 0 : return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP),
507 : : "Marking a boot is only supported on EFI systems.");
508 : :
509 : 0 : return dispatch_verb(argc, argv, verbs, NULL);
510 : : }
511 : :
512 : 0 : DEFINE_MAIN_FUNCTION(run);
|