]> git.ipfire.org Git - thirdparty/systemd.git/blob - src/shared/bootspec.c
test-network: add test for advertised hop limit
[thirdparty/systemd.git] / src / shared / bootspec.c
1 /* SPDX-License-Identifier: LGPL-2.1-or-later */
2
3 #include <unistd.h>
4
5 #include "bootspec-fundamental.h"
6 #include "bootspec.h"
7 #include "chase.h"
8 #include "conf-files.h"
9 #include "devnum-util.h"
10 #include "dirent-util.h"
11 #include "efi-loader.h"
12 #include "env-file.h"
13 #include "errno-util.h"
14 #include "fd-util.h"
15 #include "fileio.h"
16 #include "find-esp.h"
17 #include "path-util.h"
18 #include "pe-binary.h"
19 #include "pretty-print.h"
20 #include "recurse-dir.h"
21 #include "sort-util.h"
22 #include "stat-util.h"
23 #include "string-table.h"
24 #include "strv.h"
25 #include "terminal-util.h"
26 #include "unaligned.h"
27
28 static const char* const boot_entry_type_table[_BOOT_ENTRY_TYPE_MAX] = {
29 [BOOT_ENTRY_CONF] = "Boot Loader Specification Type #1 (.conf)",
30 [BOOT_ENTRY_UNIFIED] = "Boot Loader Specification Type #2 (.efi)",
31 [BOOT_ENTRY_LOADER] = "Reported by Boot Loader",
32 [BOOT_ENTRY_LOADER_AUTO] = "Automatic",
33 };
34
35 DEFINE_STRING_TABLE_LOOKUP_TO_STRING(boot_entry_type, BootEntryType);
36
37 static const char* const boot_entry_type_json_table[_BOOT_ENTRY_TYPE_MAX] = {
38 [BOOT_ENTRY_CONF] = "type1",
39 [BOOT_ENTRY_UNIFIED] = "type2",
40 [BOOT_ENTRY_LOADER] = "loader",
41 [BOOT_ENTRY_LOADER_AUTO] = "auto",
42 };
43
44 DEFINE_STRING_TABLE_LOOKUP_TO_STRING(boot_entry_type_json, BootEntryType);
45
46 BootEntryAddon* boot_entry_addon_free(BootEntryAddon *addon) {
47 if (!addon)
48 return NULL;
49
50 free(addon->location);
51 free(addon->cmdline);
52 return mfree(addon);
53 }
54
55 static void boot_entry_free(BootEntry *entry) {
56 assert(entry);
57
58 free(entry->id);
59 free(entry->id_old);
60 free(entry->path);
61 free(entry->root);
62 free(entry->title);
63 free(entry->show_title);
64 free(entry->sort_key);
65 free(entry->version);
66 free(entry->machine_id);
67 free(entry->architecture);
68 strv_free(entry->options);
69 free(entry->local_addons.items);
70 free(entry->kernel);
71 free(entry->efi);
72 strv_free(entry->initrd);
73 free(entry->device_tree);
74 strv_free(entry->device_tree_overlay);
75 }
76
77 static int mangle_path(
78 const char *fname,
79 unsigned line,
80 const char *field,
81 const char *p,
82 char **ret) {
83
84 _cleanup_free_ char *c = NULL;
85
86 assert(field);
87 assert(p);
88 assert(ret);
89
90 /* Spec leaves open if prefixed with "/" or not, let's normalize that */
91 if (path_is_absolute(p))
92 c = strdup(p);
93 else
94 c = strjoin("/", p);
95 if (!c)
96 return -ENOMEM;
97
98 /* We only reference files, never directories */
99 if (endswith(c, "/")) {
100 log_syntax(NULL, LOG_WARNING, fname, line, 0, "Path in field '%s' has trailing slash, ignoring: %s", field, c);
101 *ret = NULL;
102 return 0;
103 }
104
105 /* Remove duplicate "/" */
106 path_simplify(c);
107
108 /* No ".." or "." or so */
109 if (!path_is_normalized(c)) {
110 log_syntax(NULL, LOG_WARNING, fname, line, 0, "Path in field '%s' is not normalized, ignoring: %s", field, c);
111 *ret = NULL;
112 return 0;
113 }
114
115 *ret = TAKE_PTR(c);
116 return 1;
117 }
118
119 static int parse_path_one(
120 const char *fname,
121 unsigned line,
122 const char *field,
123 char **s,
124 const char *p) {
125
126 _cleanup_free_ char *c = NULL;
127 int r;
128
129 assert(field);
130 assert(s);
131 assert(p);
132
133 r = mangle_path(fname, line, field, p, &c);
134 if (r <= 0)
135 return r;
136
137 return free_and_replace(*s, c);
138 }
139
140 static int parse_path_strv(
141 const char *fname,
142 unsigned line,
143 const char *field,
144 char ***s,
145 const char *p) {
146
147 char *c;
148 int r;
149
150 assert(field);
151 assert(s);
152 assert(p);
153
154 r = mangle_path(fname, line, field, p, &c);
155 if (r <= 0)
156 return r;
157
158 return strv_consume(s, c);
159 }
160
161 static int parse_path_many(
162 const char *fname,
163 unsigned line,
164 const char *field,
165 char ***s,
166 const char *p) {
167
168 _cleanup_strv_free_ char **l = NULL, **f = NULL;
169 int r;
170
171 l = strv_split(p, NULL);
172 if (!l)
173 return -ENOMEM;
174
175 STRV_FOREACH(i, l) {
176 char *c;
177
178 r = mangle_path(fname, line, field, *i, &c);
179 if (r < 0)
180 return r;
181 if (r == 0)
182 continue;
183
184 r = strv_consume(&f, c);
185 if (r < 0)
186 return r;
187 }
188
189 return strv_extend_strv(s, f, /* filter_duplicates= */ false);
190 }
191
192 static int parse_tries(const char *fname, const char **p, unsigned *ret) {
193 _cleanup_free_ char *d = NULL;
194 unsigned tries;
195 size_t n;
196 int r;
197
198 assert(fname);
199 assert(p);
200 assert(*p);
201 assert(ret);
202
203 n = strspn(*p, DIGITS);
204 if (n == 0) {
205 *ret = UINT_MAX;
206 return 0;
207 }
208
209 d = strndup(*p, n);
210 if (!d)
211 return log_oom();
212
213 r = safe_atou_full(d, 10, &tries);
214 if (r >= 0 && tries > INT_MAX) /* sd-boot allows INT_MAX, let's use the same limit */
215 r = -ERANGE;
216 if (r < 0)
217 return log_error_errno(r, "Failed to parse tries counter of filename '%s': %m", fname);
218
219 *p = *p + n;
220 *ret = tries;
221 return 1;
222 }
223
224 int boot_filename_extract_tries(
225 const char *fname,
226 char **ret_stripped,
227 unsigned *ret_tries_left,
228 unsigned *ret_tries_done) {
229
230 unsigned tries_left = UINT_MAX, tries_done = UINT_MAX;
231 _cleanup_free_ char *stripped = NULL;
232 const char *p, *suffix, *m;
233 int r;
234
235 assert(fname);
236 assert(ret_stripped);
237 assert(ret_tries_left);
238 assert(ret_tries_done);
239
240 /* Be liberal with suffix, only insist on a dot. After all we want to cover any capitalization here
241 * (vfat is case insensitive after all), and at least .efi and .conf as suffix. */
242 suffix = strrchr(fname, '.');
243 if (!suffix)
244 goto nothing;
245
246 p = m = memrchr(fname, '+', suffix - fname);
247 if (!p)
248 goto nothing;
249 p++;
250
251 r = parse_tries(fname, &p, &tries_left);
252 if (r < 0)
253 return r;
254 if (r == 0)
255 goto nothing;
256
257 if (*p == '-') {
258 p++;
259
260 r = parse_tries(fname, &p, &tries_done);
261 if (r < 0)
262 return r;
263 if (r == 0)
264 goto nothing;
265 }
266
267 if (p != suffix)
268 goto nothing;
269
270 stripped = strndup(fname, m - fname);
271 if (!stripped)
272 return log_oom();
273
274 if (!strextend(&stripped, suffix))
275 return log_oom();
276
277 *ret_stripped = TAKE_PTR(stripped);
278 *ret_tries_left = tries_left;
279 *ret_tries_done = tries_done;
280
281 return 0;
282
283 nothing:
284 stripped = strdup(fname);
285 if (!stripped)
286 return log_oom();
287
288 *ret_stripped = TAKE_PTR(stripped);
289 *ret_tries_left = *ret_tries_done = UINT_MAX;
290 return 0;
291 }
292
293 static int boot_entry_load_type1(
294 FILE *f,
295 const char *root,
296 const char *dir,
297 const char *fname,
298 BootEntry *entry) {
299
300 _cleanup_(boot_entry_free) BootEntry tmp = BOOT_ENTRY_INIT(BOOT_ENTRY_CONF);
301 unsigned line = 1;
302 char *c;
303 int r;
304
305 assert(f);
306 assert(root);
307 assert(dir);
308 assert(fname);
309 assert(entry);
310
311 /* Loads a Type #1 boot menu entry from the specified FILE* object */
312
313 r = boot_filename_extract_tries(fname, &tmp.id, &tmp.tries_left, &tmp.tries_done);
314 if (r < 0)
315 return r;
316
317 if (!efi_loader_entry_name_valid(tmp.id))
318 return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid loader entry name: %s", fname);
319
320 c = endswith_no_case(tmp.id, ".conf");
321 if (!c)
322 return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid loader entry file suffix: %s", fname);
323
324 tmp.id_old = strndup(tmp.id, c - tmp.id); /* Without .conf suffix */
325 if (!tmp.id_old)
326 return log_oom();
327
328 tmp.path = path_join(dir, fname);
329 if (!tmp.path)
330 return log_oom();
331
332 tmp.root = strdup(root);
333 if (!tmp.root)
334 return log_oom();
335
336 for (;;) {
337 _cleanup_free_ char *buf = NULL, *field = NULL;
338
339 r = read_stripped_line(f, LONG_LINE_MAX, &buf);
340 if (r == 0)
341 break;
342 if (r == -ENOBUFS)
343 return log_syntax(NULL, LOG_ERR, tmp.path, line, r, "Line too long.");
344 if (r < 0)
345 return log_syntax(NULL, LOG_ERR, tmp.path, line, r, "Error while reading: %m");
346
347 line++;
348
349 if (IN_SET(buf[0], '#', '\0'))
350 continue;
351
352 const char *p = buf;
353 r = extract_first_word(&p, &field, NULL, 0);
354 if (r < 0) {
355 log_syntax(NULL, LOG_WARNING, tmp.path, line, r, "Failed to parse, ignoring line: %m");
356 continue;
357 }
358 if (r == 0) {
359 log_syntax(NULL, LOG_WARNING, tmp.path, line, 0, "Bad syntax, ignoring line.");
360 continue;
361 }
362
363 if (isempty(p)) {
364 /* Some fields can reasonably have an empty value. In other cases warn. */
365 if (!STR_IN_SET(field, "options", "devicetree-overlay"))
366 log_syntax(NULL, LOG_WARNING, tmp.path, line, 0, "Field '%s' without value, ignoring line.", field);
367
368 continue;
369 }
370
371 if (streq(field, "title"))
372 r = free_and_strdup(&tmp.title, p);
373 else if (streq(field, "sort-key"))
374 r = free_and_strdup(&tmp.sort_key, p);
375 else if (streq(field, "version"))
376 r = free_and_strdup(&tmp.version, p);
377 else if (streq(field, "machine-id"))
378 r = free_and_strdup(&tmp.machine_id, p);
379 else if (streq(field, "architecture"))
380 r = free_and_strdup(&tmp.architecture, p);
381 else if (streq(field, "options"))
382 r = strv_extend(&tmp.options, p);
383 else if (streq(field, "linux"))
384 r = parse_path_one(tmp.path, line, field, &tmp.kernel, p);
385 else if (streq(field, "efi"))
386 r = parse_path_one(tmp.path, line, field, &tmp.efi, p);
387 else if (streq(field, "initrd"))
388 r = parse_path_strv(tmp.path, line, field, &tmp.initrd, p);
389 else if (streq(field, "devicetree"))
390 r = parse_path_one(tmp.path, line, field, &tmp.device_tree, p);
391 else if (streq(field, "devicetree-overlay"))
392 r = parse_path_many(tmp.path, line, field, &tmp.device_tree_overlay, p);
393 else {
394 log_syntax(NULL, LOG_WARNING, tmp.path, line, 0, "Unknown line '%s', ignoring.", field);
395 continue;
396 }
397 if (r < 0)
398 return log_syntax(NULL, LOG_ERR, tmp.path, line, r, "Error while parsing: %m");
399 }
400
401 *entry = TAKE_STRUCT(tmp);
402 return 0;
403 }
404
405 int boot_config_load_type1(
406 BootConfig *config,
407 FILE *f,
408 const char *root,
409 const char *dir,
410 const char *fname) {
411 int r;
412
413 assert(config);
414 assert(f);
415 assert(root);
416 assert(dir);
417 assert(fname);
418
419 if (!GREEDY_REALLOC0(config->entries, config->n_entries + 1))
420 return log_oom();
421
422 r = boot_entry_load_type1(f, root, dir, fname, config->entries + config->n_entries);
423 if (r < 0)
424 return r;
425
426 config->n_entries++;
427 return 0;
428 }
429
430 void boot_config_free(BootConfig *config) {
431 assert(config);
432
433 free(config->default_pattern);
434 free(config->timeout);
435 free(config->editor);
436 free(config->auto_entries);
437 free(config->auto_firmware);
438 free(config->console_mode);
439 free(config->beep);
440
441 free(config->entry_oneshot);
442 free(config->entry_default);
443 free(config->entry_selected);
444
445 for (size_t i = 0; i < config->n_entries; i++)
446 boot_entry_free(config->entries + i);
447 free(config->entries);
448 free(config->global_addons.items);
449
450 set_free(config->inodes_seen);
451 }
452
453 int boot_loader_read_conf(BootConfig *config, FILE *file, const char *path) {
454 unsigned line = 1;
455 int r;
456
457 assert(config);
458 assert(file);
459 assert(path);
460
461 for (;;) {
462 _cleanup_free_ char *buf = NULL, *field = NULL;
463
464 r = read_stripped_line(file, LONG_LINE_MAX, &buf);
465 if (r == 0)
466 break;
467 if (r == -ENOBUFS)
468 return log_syntax(NULL, LOG_ERR, path, line, r, "Line too long.");
469 if (r < 0)
470 return log_syntax(NULL, LOG_ERR, path, line, r, "Error while reading: %m");
471
472 line++;
473
474 if (IN_SET(buf[0], '#', '\0'))
475 continue;
476
477 const char *p = buf;
478 r = extract_first_word(&p, &field, NULL, 0);
479 if (r < 0) {
480 log_syntax(NULL, LOG_WARNING, path, line, r, "Failed to parse, ignoring line: %m");
481 continue;
482 }
483 if (r == 0) {
484 log_syntax(NULL, LOG_WARNING, path, line, 0, "Bad syntax, ignoring line.");
485 continue;
486 }
487 if (isempty(p)) {
488 log_syntax(NULL, LOG_WARNING, path, line, 0, "Field '%s' without value, ignoring line.", field);
489 continue;
490 }
491
492 if (streq(field, "default"))
493 r = free_and_strdup(&config->default_pattern, p);
494 else if (streq(field, "timeout"))
495 r = free_and_strdup(&config->timeout, p);
496 else if (streq(field, "editor"))
497 r = free_and_strdup(&config->editor, p);
498 else if (streq(field, "auto-entries"))
499 r = free_and_strdup(&config->auto_entries, p);
500 else if (streq(field, "auto-firmware"))
501 r = free_and_strdup(&config->auto_firmware, p);
502 else if (streq(field, "console-mode"))
503 r = free_and_strdup(&config->console_mode, p);
504 else if (streq(field, "random-seed-mode"))
505 log_syntax(NULL, LOG_WARNING, path, line, 0, "'random-seed-mode' has been deprecated, ignoring.");
506 else if (streq(field, "beep"))
507 r = free_and_strdup(&config->beep, p);
508 else {
509 log_syntax(NULL, LOG_WARNING, path, line, 0, "Unknown line '%s', ignoring.", field);
510 continue;
511 }
512 if (r < 0)
513 return log_syntax(NULL, LOG_ERR, path, line, r, "Error while parsing: %m");
514 }
515
516 return 1;
517 }
518
519 static int boot_loader_read_conf_path(BootConfig *config, const char *root, const char *path) {
520 _cleanup_free_ char *full = NULL;
521 _cleanup_fclose_ FILE *f = NULL;
522 int r;
523
524 assert(config);
525 assert(path);
526
527 r = chase_and_fopen_unlocked(path, root, CHASE_PREFIX_ROOT|CHASE_PROHIBIT_SYMLINKS, "re", &full, &f);
528 if (r == -ENOENT)
529 return 0;
530 if (r < 0)
531 return log_error_errno(r, "Failed to open '%s/%s': %m", root, path);
532
533 return boot_loader_read_conf(config, f, full);
534 }
535
536 static int boot_entry_compare(const BootEntry *a, const BootEntry *b) {
537 int r;
538
539 assert(a);
540 assert(b);
541
542 r = CMP(!a->sort_key, !b->sort_key);
543 if (r != 0)
544 return r;
545
546 if (a->sort_key && b->sort_key) {
547 r = strcmp(a->sort_key, b->sort_key);
548 if (r != 0)
549 return r;
550
551 r = strcmp_ptr(a->machine_id, b->machine_id);
552 if (r != 0)
553 return r;
554
555 r = -strverscmp_improved(a->version, b->version);
556 if (r != 0)
557 return r;
558 }
559
560 return -strverscmp_improved(a->id, b->id);
561 }
562
563 static int config_check_inode_relevant_and_unseen(BootConfig *config, int fd, const char *fname) {
564 _cleanup_free_ char *d = NULL;
565 struct stat st;
566
567 assert(config);
568 assert(fd >= 0);
569 assert(fname);
570
571 /* So, here's the thing: because of the mess around /efi/ vs. /boot/ vs. /boot/efi/ it might be that
572 * people have these dirs, or subdirs of them symlinked or bind mounted, and we might end up
573 * iterating though some dirs multiple times. Let's thus rather be safe than sorry, and track the
574 * inodes we already processed: let's ignore inodes we have seen already. This should be robust
575 * against any form of symlinking or bind mounting, and effectively suppress any such duplicates. */
576
577 if (fstat(fd, &st) < 0)
578 return log_error_errno(errno, "Failed to stat('%s'): %m", fname);
579 if (!S_ISREG(st.st_mode)) {
580 log_debug("File '%s' is not a regular file, ignoring.", fname);
581 return false;
582 }
583
584 if (set_contains(config->inodes_seen, &st)) {
585 log_debug("Inode '%s' already seen before, ignoring.", fname);
586 return false;
587 }
588
589 d = memdup(&st, sizeof(st));
590 if (!d)
591 return log_oom();
592
593 if (set_ensure_consume(&config->inodes_seen, &inode_hash_ops, TAKE_PTR(d)) < 0)
594 return log_oom();
595
596 return true;
597 }
598
599 static int boot_entries_find_type1(
600 BootConfig *config,
601 const char *root,
602 const char *dir) {
603
604 _cleanup_free_ DirectoryEntries *dentries = NULL;
605 _cleanup_free_ char *full = NULL;
606 _cleanup_close_ int dir_fd = -EBADF;
607 int r;
608
609 assert(config);
610 assert(root);
611 assert(dir);
612
613 dir_fd = chase_and_open(dir, root, CHASE_PREFIX_ROOT|CHASE_PROHIBIT_SYMLINKS, O_DIRECTORY|O_CLOEXEC, &full);
614 if (dir_fd == -ENOENT)
615 return 0;
616 if (dir_fd < 0)
617 return log_error_errno(dir_fd, "Failed to open '%s/%s': %m", root, dir);
618
619 r = readdir_all(dir_fd, RECURSE_DIR_IGNORE_DOT, &dentries);
620 if (r < 0)
621 return log_error_errno(r, "Failed to read directory '%s': %m", full);
622
623 for (size_t i = 0; i < dentries->n_entries; i++) {
624 const struct dirent *de = dentries->entries[i];
625 _cleanup_fclose_ FILE *f = NULL;
626
627 if (!dirent_is_file(de))
628 continue;
629
630 if (!endswith_no_case(de->d_name, ".conf"))
631 continue;
632
633 r = xfopenat(dir_fd, de->d_name, "re", O_NOFOLLOW|O_NOCTTY, &f);
634 if (r < 0) {
635 log_warning_errno(r, "Failed to open %s/%s, ignoring: %m", full, de->d_name);
636 continue;
637 }
638
639 r = config_check_inode_relevant_and_unseen(config, fileno(f), de->d_name);
640 if (r < 0)
641 return r;
642 if (r == 0) /* inode already seen or otherwise not relevant */
643 continue;
644
645 r = boot_config_load_type1(config, f, root, full, de->d_name);
646 if (r == -ENOMEM) /* ignore all other errors */
647 return r;
648 }
649
650 return 0;
651 }
652
653 static int boot_entry_load_unified(
654 const char *root,
655 const char *path,
656 const char *osrelease,
657 const char *cmdline,
658 BootEntry *ret) {
659
660 _cleanup_free_ char *fname = NULL, *os_pretty_name = NULL, *os_image_id = NULL, *os_name = NULL, *os_id = NULL,
661 *os_image_version = NULL, *os_version = NULL, *os_version_id = NULL, *os_build_id = NULL;
662 _cleanup_(boot_entry_free) BootEntry tmp = BOOT_ENTRY_INIT(BOOT_ENTRY_UNIFIED);
663 const char *k, *good_name, *good_version, *good_sort_key;
664 _cleanup_fclose_ FILE *f = NULL;
665 int r;
666
667 assert(root);
668 assert(path);
669 assert(osrelease);
670
671 k = path_startswith(path, root);
672 if (!k)
673 return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Path is not below root: %s", path);
674
675 f = fmemopen_unlocked((void*) osrelease, strlen(osrelease), "r");
676 if (!f)
677 return log_error_errno(errno, "Failed to open os-release buffer: %m");
678
679 r = parse_env_file(f, "os-release",
680 "PRETTY_NAME", &os_pretty_name,
681 "IMAGE_ID", &os_image_id,
682 "NAME", &os_name,
683 "ID", &os_id,
684 "IMAGE_VERSION", &os_image_version,
685 "VERSION", &os_version,
686 "VERSION_ID", &os_version_id,
687 "BUILD_ID", &os_build_id);
688 if (r < 0)
689 return log_error_errno(r, "Failed to parse os-release data from unified kernel image %s: %m", path);
690
691 if (!bootspec_pick_name_version_sort_key(
692 os_pretty_name,
693 os_image_id,
694 os_name,
695 os_id,
696 os_image_version,
697 os_version,
698 os_version_id,
699 os_build_id,
700 &good_name,
701 &good_version,
702 &good_sort_key))
703 return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Missing fields in os-release data from unified kernel image %s, refusing.", path);
704
705 r = path_extract_filename(path, &fname);
706 if (r < 0)
707 return log_error_errno(r, "Failed to extract file name from '%s': %m", path);
708
709 r = boot_filename_extract_tries(fname, &tmp.id, &tmp.tries_left, &tmp.tries_done);
710 if (r < 0)
711 return r;
712
713 if (!efi_loader_entry_name_valid(tmp.id))
714 return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid loader entry name: %s", tmp.id);
715
716 if (os_id && os_version_id) {
717 tmp.id_old = strjoin(os_id, "-", os_version_id);
718 if (!tmp.id_old)
719 return log_oom();
720 }
721
722 tmp.path = strdup(path);
723 if (!tmp.path)
724 return log_oom();
725
726 tmp.root = strdup(root);
727 if (!tmp.root)
728 return log_oom();
729
730 tmp.kernel = path_make_absolute(k, "/");
731 if (!tmp.kernel)
732 return log_oom();
733
734 tmp.options = strv_new(skip_leading_chars(cmdline, WHITESPACE));
735 if (!tmp.options)
736 return log_oom();
737
738 delete_trailing_chars(tmp.options[0], WHITESPACE);
739
740 tmp.title = strdup(good_name);
741 if (!tmp.title)
742 return log_oom();
743
744 if (good_sort_key) {
745 tmp.sort_key = strdup(good_sort_key);
746 if (!tmp.sort_key)
747 return log_oom();
748 }
749
750 if (good_version) {
751 tmp.version = strdup(good_version);
752 if (!tmp.version)
753 return log_oom();
754 }
755
756 *ret = TAKE_STRUCT(tmp);
757 return 0;
758 }
759
760 /* Maximum PE section we are willing to load (Note that sections we are not interested in may be larger, but
761 * the ones we do care about and we are willing to load into memory have this size limit.) */
762 #define PE_SECTION_SIZE_MAX (4U*1024U*1024U)
763
764 static int find_sections(
765 int fd,
766 const char *path,
767 IMAGE_SECTION_HEADER **ret_sections,
768 PeHeader **ret_pe_header) {
769
770 _cleanup_free_ IMAGE_DOS_HEADER *dos_header = NULL;
771 IMAGE_SECTION_HEADER *sections;
772 PeHeader *pe_header;
773 int r;
774
775 assert(fd >= 0);
776 assert(path);
777
778 r = pe_load_headers(fd, &dos_header, &pe_header);
779 if (r < 0)
780 return log_warning_errno(r, "Failed to parse PE file '%s': %m", path);
781
782 r = pe_load_sections(fd, dos_header, pe_header, &sections);
783 if (r < 0)
784 return log_warning_errno(r, "Failed to parse PE sections of '%s': %m", path);
785
786 if (ret_pe_header)
787 *ret_pe_header = TAKE_PTR(pe_header);
788 if (ret_sections)
789 *ret_sections = TAKE_PTR(sections);
790
791 return 0;
792 }
793
794 static int find_cmdline_section(
795 int fd,
796 const char *path,
797 IMAGE_SECTION_HEADER *sections,
798 PeHeader *pe_header,
799 char **ret_cmdline) {
800
801 int r;
802 char *cmdline = NULL;
803
804 if (!ret_cmdline)
805 return 0;
806
807 r = pe_read_section_data(fd, pe_header, sections, ".cmdline", PE_SECTION_SIZE_MAX, (void**) &cmdline, NULL);
808 if (r == -ENXIO) /* cmdline is optional */
809 *ret_cmdline = NULL;
810 else if (r < 0)
811 return log_warning_errno(r, "Failed to read .cmdline section of '%s': %m", path);
812 else if (ret_cmdline)
813 *ret_cmdline = TAKE_PTR(cmdline);
814
815 return 0;
816 }
817
818 static int find_osrel_section(
819 int fd,
820 const char *path,
821 IMAGE_SECTION_HEADER *sections,
822 PeHeader *pe_header,
823 char **ret_osrelease) {
824
825 int r;
826
827 if (!ret_osrelease)
828 return 0;
829
830 r = pe_read_section_data(fd, pe_header, sections, ".osrel", PE_SECTION_SIZE_MAX, (void**) ret_osrelease, NULL);
831 if (r < 0)
832 return log_warning_errno(r, "Failed to read .osrel section of '%s': %m", path);
833
834 return 0;
835 }
836
837 static int find_uki_sections(
838 int fd,
839 const char *path,
840 char **ret_osrelease,
841 char **ret_cmdline) {
842
843 _cleanup_free_ IMAGE_SECTION_HEADER *sections = NULL;
844 _cleanup_free_ PeHeader *pe_header = NULL;
845 int r;
846
847 r = find_sections(fd, path, &sections, &pe_header);
848 if (r < 0)
849 return r;
850
851 if (!pe_is_uki(pe_header, sections))
852 return log_warning_errno(SYNTHETIC_ERRNO(EBADMSG), "Parsed PE file '%s' is not a UKI.", path);
853
854 r = find_osrel_section(fd, path, sections, pe_header, ret_osrelease);
855 if (r < 0)
856 return r;
857
858 r = find_cmdline_section(fd, path, sections, pe_header, ret_cmdline);
859 if (r < 0)
860 return r;
861
862 return 0;
863 }
864
865 static int find_addon_sections(
866 int fd,
867 const char *path,
868 char **ret_cmdline) {
869
870 _cleanup_free_ IMAGE_SECTION_HEADER *sections = NULL;
871 _cleanup_free_ PeHeader *pe_header = NULL;
872 int r;
873
874 r = find_sections(fd, path, &sections, &pe_header);
875 if (r < 0)
876 return r;
877
878 return find_cmdline_section(fd, path, sections, pe_header, ret_cmdline);
879 }
880
881 static int insert_boot_entry_addon(
882 BootEntryAddons *addons,
883 char *location,
884 char *cmdline) {
885
886 if (!GREEDY_REALLOC(addons->items, addons->count + 1))
887 return log_oom();
888
889 addons->items[addons->count] = (BootEntryAddon) {
890 .location = location,
891 .cmdline = cmdline,
892 };
893 addons->count++;
894
895 return 0;
896 }
897
898 static int boot_entries_find_unified_addons(
899 BootConfig *config,
900 int d_fd,
901 const char *addon_dir,
902 const char *root,
903 BootEntryAddons *addons) {
904
905 _cleanup_closedir_ DIR *d = NULL;
906 _cleanup_free_ char *full = NULL;
907 int r;
908
909 assert(addons);
910 assert(config);
911
912 r = chase_and_opendirat(d_fd, addon_dir, CHASE_AT_RESOLVE_IN_ROOT, &full, &d);
913 if (r == -ENOENT)
914 return 0;
915 if (r < 0)
916 return log_error_errno(r, "Failed to open '%s/%s': %m", root, addon_dir);
917
918 *addons = (BootEntryAddons) {};
919
920 FOREACH_DIRENT(de, d, return log_error_errno(errno, "Failed to read %s: %m", full)) {
921 _cleanup_free_ char *j = NULL, *cmdline = NULL, *location = NULL;
922 _cleanup_close_ int fd = -EBADF;
923
924 if (!dirent_is_file(de))
925 continue;
926
927 if (!endswith_no_case(de->d_name, ".addon.efi"))
928 continue;
929
930 fd = openat(dirfd(d), de->d_name, O_RDONLY|O_CLOEXEC|O_NONBLOCK|O_NOFOLLOW|O_NOCTTY);
931 if (fd < 0) {
932 log_warning_errno(errno, "Failed to open %s/%s, ignoring: %m", full, de->d_name);
933 continue;
934 }
935
936 r = config_check_inode_relevant_and_unseen(config, fd, de->d_name);
937 if (r < 0)
938 return r;
939 if (r == 0) /* inode already seen or otherwise not relevant */
940 continue;
941
942 j = path_join(full, de->d_name);
943 if (!j)
944 return log_oom();
945
946 if (find_addon_sections(fd, j, &cmdline) < 0)
947 continue;
948
949 location = strdup(j);
950 if (!location)
951 return log_oom();
952
953 r = insert_boot_entry_addon(addons, TAKE_PTR(location), TAKE_PTR(cmdline));
954 if (r < 0) {
955 free(addons);
956 return r;
957 }
958 }
959 return 0;
960 }
961
962 static int boot_entries_find_unified_global_addons(
963 BootConfig *config,
964 const char *root,
965 const char *d_name) {
966
967 int r;
968 _cleanup_closedir_ DIR *d = NULL;
969
970 r = chase_and_opendir(root, NULL, CHASE_PROHIBIT_SYMLINKS, NULL, &d);
971 if (r == -ENOENT)
972 return 0;
973 if (r < 0)
974 return log_error_errno(r, "Failed to open '%s/%s': %m", root, d_name);
975
976 return boot_entries_find_unified_addons(config, dirfd(d), d_name, root, &config->global_addons);
977 }
978
979 static int boot_entries_find_unified_local_addons(
980 BootConfig *config,
981 int d_fd,
982 const char *d_name,
983 const char *root,
984 BootEntry *ret) {
985
986 _cleanup_free_ char *addon_dir = NULL;
987
988 assert(ret);
989
990 addon_dir = strjoin(d_name, ".extra.d");
991 if (!addon_dir)
992 return log_oom();
993
994 return boot_entries_find_unified_addons(config, d_fd, addon_dir, root, &ret->local_addons);
995 }
996
997 static int boot_entries_find_unified(
998 BootConfig *config,
999 const char *root,
1000 const char *dir) {
1001
1002 _cleanup_closedir_ DIR *d = NULL;
1003 _cleanup_free_ char *full = NULL;
1004 int r;
1005
1006 assert(config);
1007 assert(dir);
1008
1009 r = chase_and_opendir(dir, root, CHASE_PREFIX_ROOT|CHASE_PROHIBIT_SYMLINKS, &full, &d);
1010 if (r == -ENOENT)
1011 return 0;
1012 if (r < 0)
1013 return log_error_errno(r, "Failed to open '%s/%s': %m", root, dir);
1014
1015 FOREACH_DIRENT(de, d, return log_error_errno(errno, "Failed to read %s: %m", full)) {
1016 _cleanup_free_ char *j = NULL, *osrelease = NULL, *cmdline = NULL;
1017 _cleanup_close_ int fd = -EBADF;
1018
1019 if (!dirent_is_file(de))
1020 continue;
1021
1022 if (!endswith_no_case(de->d_name, ".efi"))
1023 continue;
1024
1025 if (!GREEDY_REALLOC0(config->entries, config->n_entries + 1))
1026 return log_oom();
1027
1028 fd = openat(dirfd(d), de->d_name, O_RDONLY|O_CLOEXEC|O_NONBLOCK|O_NOFOLLOW|O_NOCTTY);
1029 if (fd < 0) {
1030 log_warning_errno(errno, "Failed to open %s/%s, ignoring: %m", full, de->d_name);
1031 continue;
1032 }
1033
1034 r = config_check_inode_relevant_and_unseen(config, fd, de->d_name);
1035 if (r < 0)
1036 return r;
1037 if (r == 0) /* inode already seen or otherwise not relevant */
1038 continue;
1039
1040 j = path_join(full, de->d_name);
1041 if (!j)
1042 return log_oom();
1043
1044 if (find_uki_sections(fd, j, &osrelease, &cmdline) < 0)
1045 continue;
1046
1047 r = boot_entry_load_unified(root, j, osrelease, cmdline, config->entries + config->n_entries);
1048 if (r < 0)
1049 continue;
1050
1051 /* look for .efi.extra.d */
1052 r = boot_entries_find_unified_local_addons(config, dirfd(d), de->d_name, full, config->entries + config->n_entries);
1053 if (r < 0)
1054 continue;
1055
1056 config->n_entries++;
1057 }
1058
1059 return 0;
1060 }
1061
1062 static bool find_nonunique(const BootEntry *entries, size_t n_entries, bool arr[]) {
1063 bool non_unique = false;
1064
1065 assert(entries || n_entries == 0);
1066 assert(arr || n_entries == 0);
1067
1068 for (size_t i = 0; i < n_entries; i++)
1069 arr[i] = false;
1070
1071 for (size_t i = 0; i < n_entries; i++)
1072 for (size_t j = 0; j < n_entries; j++)
1073 if (i != j && streq(boot_entry_title(entries + i),
1074 boot_entry_title(entries + j)))
1075 non_unique = arr[i] = arr[j] = true;
1076
1077 return non_unique;
1078 }
1079
1080 static int boot_entries_uniquify(BootEntry *entries, size_t n_entries) {
1081 _cleanup_free_ bool *arr = NULL;
1082 char *s;
1083
1084 assert(entries || n_entries == 0);
1085
1086 if (n_entries == 0)
1087 return 0;
1088
1089 arr = new(bool, n_entries);
1090 if (!arr)
1091 return -ENOMEM;
1092
1093 /* Find _all_ non-unique titles */
1094 if (!find_nonunique(entries, n_entries, arr))
1095 return 0;
1096
1097 /* Add version to non-unique titles */
1098 for (size_t i = 0; i < n_entries; i++)
1099 if (arr[i] && entries[i].version) {
1100 if (asprintf(&s, "%s (%s)", boot_entry_title(entries + i), entries[i].version) < 0)
1101 return -ENOMEM;
1102
1103 free_and_replace(entries[i].show_title, s);
1104 }
1105
1106 if (!find_nonunique(entries, n_entries, arr))
1107 return 0;
1108
1109 /* Add machine-id to non-unique titles */
1110 for (size_t i = 0; i < n_entries; i++)
1111 if (arr[i] && entries[i].machine_id) {
1112 if (asprintf(&s, "%s (%s)", boot_entry_title(entries + i), entries[i].machine_id) < 0)
1113 return -ENOMEM;
1114
1115 free_and_replace(entries[i].show_title, s);
1116 }
1117
1118 if (!find_nonunique(entries, n_entries, arr))
1119 return 0;
1120
1121 /* Add file name to non-unique titles */
1122 for (size_t i = 0; i < n_entries; i++)
1123 if (arr[i]) {
1124 if (asprintf(&s, "%s (%s)", boot_entry_title(entries + i), entries[i].id) < 0)
1125 return -ENOMEM;
1126
1127 free_and_replace(entries[i].show_title, s);
1128 }
1129
1130 return 0;
1131 }
1132
1133 static int boot_config_find(const BootConfig *config, const char *id) {
1134 assert(config);
1135
1136 if (!id)
1137 return -1;
1138
1139 if (id[0] == '@') {
1140 if (!strcaseeq(id, "@saved"))
1141 return -1;
1142 if (!config->entry_selected)
1143 return -1;
1144 id = config->entry_selected;
1145 }
1146
1147 for (size_t i = 0; i < config->n_entries; i++)
1148 if (fnmatch(id, config->entries[i].id, FNM_CASEFOLD) == 0)
1149 return i;
1150
1151 return -1;
1152 }
1153
1154 static int boot_entries_select_default(const BootConfig *config) {
1155 int i;
1156
1157 assert(config);
1158 assert(config->entries || config->n_entries == 0);
1159
1160 if (config->n_entries == 0) {
1161 log_debug("Found no default boot entry :(");
1162 return -1; /* -1 means "no default" */
1163 }
1164
1165 if (config->entry_oneshot) {
1166 i = boot_config_find(config, config->entry_oneshot);
1167 if (i >= 0) {
1168 log_debug("Found default: id \"%s\" is matched by LoaderEntryOneShot",
1169 config->entries[i].id);
1170 return i;
1171 }
1172 }
1173
1174 if (config->entry_default) {
1175 i = boot_config_find(config, config->entry_default);
1176 if (i >= 0) {
1177 log_debug("Found default: id \"%s\" is matched by LoaderEntryDefault",
1178 config->entries[i].id);
1179 return i;
1180 }
1181 }
1182
1183 if (config->default_pattern) {
1184 i = boot_config_find(config, config->default_pattern);
1185 if (i >= 0) {
1186 log_debug("Found default: id \"%s\" is matched by pattern \"%s\"",
1187 config->entries[i].id, config->default_pattern);
1188 return i;
1189 }
1190 }
1191
1192 log_debug("Found default: first entry \"%s\"", config->entries[0].id);
1193 return 0;
1194 }
1195
1196 static int boot_entries_select_selected(const BootConfig *config) {
1197 assert(config);
1198 assert(config->entries || config->n_entries == 0);
1199
1200 if (!config->entry_selected || config->n_entries == 0)
1201 return -1;
1202
1203 return boot_config_find(config, config->entry_selected);
1204 }
1205
1206 static int boot_load_efi_entry_pointers(BootConfig *config, bool skip_efivars) {
1207 int r;
1208
1209 assert(config);
1210
1211 if (skip_efivars || !is_efi_boot())
1212 return 0;
1213
1214 /* Loads the three "pointers" to boot loader entries from their EFI variables */
1215
1216 r = efi_get_variable_string(EFI_LOADER_VARIABLE(LoaderEntryOneShot), &config->entry_oneshot);
1217 if (r == -ENOMEM)
1218 return log_oom();
1219 if (r < 0 && !IN_SET(r, -ENOENT, -ENODATA))
1220 log_warning_errno(r, "Failed to read EFI variable \"LoaderEntryOneShot\", ignoring: %m");
1221
1222 r = efi_get_variable_string(EFI_LOADER_VARIABLE(LoaderEntryDefault), &config->entry_default);
1223 if (r == -ENOMEM)
1224 return log_oom();
1225 if (r < 0 && !IN_SET(r, -ENOENT, -ENODATA))
1226 log_warning_errno(r, "Failed to read EFI variable \"LoaderEntryDefault\", ignoring: %m");
1227
1228 r = efi_get_variable_string(EFI_LOADER_VARIABLE(LoaderEntrySelected), &config->entry_selected);
1229 if (r == -ENOMEM)
1230 return log_oom();
1231 if (r < 0 && !IN_SET(r, -ENOENT, -ENODATA))
1232 log_warning_errno(r, "Failed to read EFI variable \"LoaderEntrySelected\", ignoring: %m");
1233
1234 return 1;
1235 }
1236
1237 int boot_config_select_special_entries(BootConfig *config, bool skip_efivars) {
1238 int r;
1239
1240 assert(config);
1241
1242 r = boot_load_efi_entry_pointers(config, skip_efivars);
1243 if (r < 0)
1244 return r;
1245
1246 config->default_entry = boot_entries_select_default(config);
1247 config->selected_entry = boot_entries_select_selected(config);
1248
1249 return 0;
1250 }
1251
1252 int boot_config_finalize(BootConfig *config) {
1253 int r;
1254
1255 typesafe_qsort(config->entries, config->n_entries, boot_entry_compare);
1256
1257 r = boot_entries_uniquify(config->entries, config->n_entries);
1258 if (r < 0)
1259 return log_error_errno(r, "Failed to uniquify boot entries: %m");
1260
1261 return 0;
1262 }
1263
1264 int boot_config_load(
1265 BootConfig *config,
1266 const char *esp_path,
1267 const char *xbootldr_path) {
1268
1269 int r;
1270
1271 assert(config);
1272 config->global_addons = (BootEntryAddons) {};
1273
1274 if (esp_path) {
1275 r = boot_loader_read_conf_path(config, esp_path, "/loader/loader.conf");
1276 if (r < 0)
1277 return r;
1278
1279 r = boot_entries_find_type1(config, esp_path, "/loader/entries");
1280 if (r < 0)
1281 return r;
1282
1283 r = boot_entries_find_unified(config, esp_path, "/EFI/Linux/");
1284 if (r < 0)
1285 return r;
1286
1287 r = boot_entries_find_unified_global_addons(config, esp_path, "/loader/addons/");
1288 if (r < 0)
1289 return r;
1290 }
1291
1292 if (xbootldr_path) {
1293 r = boot_entries_find_type1(config, xbootldr_path, "/loader/entries");
1294 if (r < 0)
1295 return r;
1296
1297 r = boot_entries_find_unified(config, xbootldr_path, "/EFI/Linux/");
1298 if (r < 0)
1299 return r;
1300 }
1301
1302 return boot_config_finalize(config);
1303 }
1304
1305 int boot_config_load_auto(
1306 BootConfig *config,
1307 const char *override_esp_path,
1308 const char *override_xbootldr_path) {
1309
1310 _cleanup_free_ char *esp_where = NULL, *xbootldr_where = NULL;
1311 dev_t esp_devid = 0, xbootldr_devid = 0;
1312 int r;
1313
1314 assert(config);
1315
1316 /* This function is similar to boot_entries_load_config(), however we automatically search for the
1317 * ESP and the XBOOTLDR partition unless it is explicitly specified. Also, if the user did not pass
1318 * an ESP or XBOOTLDR path directly, let's see if /run/boot-loader-entries/ exists. If so, let's
1319 * read data from there, as if it was an ESP (i.e. loading both entries and loader.conf data from
1320 * it). This allows other boot loaders to pass boot loader entry information to our tools if they
1321 * want to. */
1322
1323 if (!override_esp_path && !override_xbootldr_path) {
1324 if (access("/run/boot-loader-entries/", F_OK) >= 0)
1325 return boot_config_load(config, "/run/boot-loader-entries/", NULL);
1326
1327 if (errno != ENOENT)
1328 return log_error_errno(errno,
1329 "Failed to determine whether /run/boot-loader-entries/ exists: %m");
1330 }
1331
1332 r = find_esp_and_warn(NULL, override_esp_path, /* unprivileged_mode= */ false, &esp_where, NULL, NULL, NULL, NULL, &esp_devid);
1333 if (r < 0) /* we don't log about ENOKEY here, but propagate it, leaving it to the caller to log */
1334 return r;
1335
1336 r = find_xbootldr_and_warn(NULL, override_xbootldr_path, /* unprivileged_mode= */ false, &xbootldr_where, NULL, &xbootldr_devid);
1337 if (r < 0 && r != -ENOKEY)
1338 return r; /* It's fine if the XBOOTLDR partition doesn't exist, hence we ignore ENOKEY here */
1339
1340 /* If both paths actually refer to the same inode, suppress the xbootldr path */
1341 if (esp_where && xbootldr_where && devnum_set_and_equal(esp_devid, xbootldr_devid))
1342 xbootldr_where = mfree(xbootldr_where);
1343
1344 return boot_config_load(config, esp_where, xbootldr_where);
1345 }
1346
1347 int boot_config_augment_from_loader(
1348 BootConfig *config,
1349 char **found_by_loader,
1350 bool only_auto) {
1351
1352 static const char *const title_table[] = {
1353 /* Pretty names for a few well-known automatically discovered entries. */
1354 "auto-osx", "macOS",
1355 "auto-windows", "Windows Boot Manager",
1356 "auto-efi-shell", "EFI Shell",
1357 "auto-efi-default", "EFI Default Loader",
1358 "auto-poweroff", "Power Off The System",
1359 "auto-reboot", "Reboot The System",
1360 "auto-reboot-to-firmware-setup", "Reboot Into Firmware Interface",
1361 NULL,
1362 };
1363
1364 assert(config);
1365
1366 /* Let's add the entries discovered by the boot loader to the end of our list, unless they are
1367 * already included there. */
1368
1369 STRV_FOREACH(i, found_by_loader) {
1370 BootEntry *existing;
1371 _cleanup_free_ char *c = NULL, *t = NULL, *p = NULL;
1372
1373 existing = boot_config_find_entry(config, *i);
1374 if (existing) {
1375 existing->reported_by_loader = true;
1376 continue;
1377 }
1378
1379 if (only_auto && !startswith(*i, "auto-"))
1380 continue;
1381
1382 c = strdup(*i);
1383 if (!c)
1384 return log_oom();
1385
1386 STRV_FOREACH_PAIR(a, b, title_table)
1387 if (streq(*a, *i)) {
1388 t = strdup(*b);
1389 if (!t)
1390 return log_oom();
1391 break;
1392 }
1393
1394 p = strdup(EFIVAR_PATH(EFI_LOADER_VARIABLE(LoaderEntries)));
1395 if (!p)
1396 return log_oom();
1397
1398 if (!GREEDY_REALLOC0(config->entries, config->n_entries + 1))
1399 return log_oom();
1400
1401 config->entries[config->n_entries++] = (BootEntry) {
1402 .type = startswith(*i, "auto-") ? BOOT_ENTRY_LOADER_AUTO : BOOT_ENTRY_LOADER,
1403 .id = TAKE_PTR(c),
1404 .title = TAKE_PTR(t),
1405 .path = TAKE_PTR(p),
1406 .reported_by_loader = true,
1407 .tries_left = UINT_MAX,
1408 .tries_done = UINT_MAX,
1409 };
1410 }
1411
1412 return 0;
1413 }
1414
1415 BootEntry* boot_config_find_entry(BootConfig *config, const char *id) {
1416 assert(config);
1417 assert(id);
1418
1419 for (size_t j = 0; j < config->n_entries; j++)
1420 if (strcaseeq_ptr(config->entries[j].id, id) ||
1421 strcaseeq_ptr(config->entries[j].id_old, id))
1422 return config->entries + j;
1423
1424 return NULL;
1425 }
1426
1427 static void boot_entry_file_list(
1428 const char *field,
1429 const char *root,
1430 const char *p,
1431 int *ret_status) {
1432
1433 assert(p);
1434 assert(ret_status);
1435
1436 int status = chase_and_access(p, root, CHASE_PREFIX_ROOT|CHASE_PROHIBIT_SYMLINKS, F_OK, NULL);
1437
1438 /* Note that this shows two '/' between the root and the file. This is intentional to highlight (in
1439 * the absence of color support) to the user that the boot loader is only interested in the second
1440 * part of the file. */
1441 printf("%13s%s %s%s/%s", strempty(field), field ? ":" : " ", ansi_grey(), root, ansi_normal());
1442
1443 if (status < 0) {
1444 errno = -status;
1445 printf("%s%s%s (%m)\n", ansi_highlight_red(), p, ansi_normal());
1446 } else
1447 printf("%s\n", p);
1448
1449 if (*ret_status == 0 && status < 0)
1450 *ret_status = status;
1451 }
1452
1453 static void print_addon(
1454 BootEntryAddon *addon,
1455 const char *addon_str) {
1456
1457 printf(" %s: %s\n", addon_str, addon->location);
1458 printf(" cmdline: %s%s\n", special_glyph(SPECIAL_GLYPH_TREE_RIGHT), addon->cmdline);
1459 }
1460
1461 static int print_cmdline(
1462 const BootEntry *e,
1463 const BootEntryAddons *global_arr) {
1464
1465 _cleanup_free_ char *final_cmdline = NULL;
1466
1467 assert(e);
1468
1469 if (!strv_isempty(e->options)) {
1470 _cleanup_free_ char *t = NULL, *t2 = NULL;
1471 _cleanup_strv_free_ char **ts = NULL;
1472
1473 t = strv_join(e->options, " ");
1474 if (!t)
1475 return log_oom();
1476
1477 ts = strv_split_newlines(t);
1478 if (!ts)
1479 return log_oom();
1480
1481 t2 = strv_join(ts, "\n ");
1482 if (!t2)
1483 return log_oom();
1484
1485 printf(" ukiCmdline: %s\n", t2);
1486 final_cmdline = TAKE_PTR(t2);
1487 }
1488
1489 FOREACH_ARRAY(addon, global_arr->items, global_arr->count) {
1490 print_addon(addon, "globalAddon");
1491 if (!strextend(&final_cmdline, " ", addon->cmdline))
1492 return log_oom();
1493 }
1494
1495 FOREACH_ARRAY(addon, e->local_addons.items, e->local_addons.count) {
1496 /* Add space at the beginning of addon_str to align it correctly */
1497 print_addon(addon, " localAddon");
1498 if (!strextend(&final_cmdline, " ", addon->cmdline))
1499 return log_oom();
1500 }
1501
1502 if (final_cmdline)
1503 printf(" finalCmdline: %s\n", final_cmdline);
1504
1505 return 0;
1506 }
1507
1508 static int json_addon(
1509 BootEntryAddon *addon,
1510 const char *addon_str,
1511 JsonVariant **array) {
1512
1513 int r;
1514
1515 r = json_variant_append_arrayb(array,
1516 JSON_BUILD_OBJECT(
1517 JSON_BUILD_PAIR(addon_str, JSON_BUILD_STRING(addon->location)),
1518 JSON_BUILD_PAIR("cmdline", JSON_BUILD_STRING(addon->cmdline))));
1519 if (r < 0)
1520 return log_oom();
1521
1522 return 0;
1523 }
1524
1525 static int json_cmdline(
1526 const BootEntry *e,
1527 const BootEntryAddons *global_arr,
1528 JsonVariant **v) {
1529
1530 _cleanup_free_ char *final_cmdline = NULL, *def_cmdline = NULL;
1531 _cleanup_(json_variant_unrefp) JsonVariant *addons_array = NULL;
1532 int r;
1533
1534 assert(e);
1535
1536 if (!strv_isempty(e->options)) {
1537 def_cmdline = strv_join(e->options, " ");
1538 if (!def_cmdline)
1539 return log_oom();
1540 final_cmdline = TAKE_PTR(def_cmdline);
1541 }
1542
1543 FOREACH_ARRAY(addon, global_arr->items, global_arr->count) {
1544 r = json_addon(addon, "globalAddon", &addons_array);
1545 if (r < 0)
1546 return r;
1547 if (!strextend(&final_cmdline, " ", addon->cmdline))
1548 return log_oom();
1549 }
1550
1551 FOREACH_ARRAY(addon, e->local_addons.items, e->local_addons.count) {
1552 r = json_addon(addon, "localAddon", &addons_array);
1553 if (r < 0)
1554 return r;
1555 if (!strextend(&final_cmdline, " ", addon->cmdline))
1556 return log_oom();
1557 }
1558
1559 r = json_variant_merge_objectb(
1560 v, JSON_BUILD_OBJECT(
1561 JSON_BUILD_PAIR_CONDITION(def_cmdline, "ukiCmdline", JSON_BUILD_STRING(def_cmdline)),
1562 JSON_BUILD_PAIR("addons", JSON_BUILD_VARIANT(addons_array)),
1563 JSON_BUILD_PAIR_CONDITION(final_cmdline, "finalCmdline", JSON_BUILD_STRING(final_cmdline))));
1564 if (r < 0)
1565 return log_oom();
1566 return 0;
1567 }
1568
1569 int show_boot_entry(
1570 const BootEntry *e,
1571 const BootEntryAddons *global_addons,
1572 bool show_as_default,
1573 bool show_as_selected,
1574 bool show_reported) {
1575
1576 int status = 0, r = 0;
1577
1578 /* Returns 0 on success, negative on processing error, and positive if something is wrong with the
1579 boot entry itself. */
1580
1581 assert(e);
1582
1583 printf(" type: %s\n",
1584 boot_entry_type_to_string(e->type));
1585
1586 printf(" title: %s%s%s",
1587 ansi_highlight(), boot_entry_title(e), ansi_normal());
1588
1589 if (show_as_default)
1590 printf(" %s(default)%s",
1591 ansi_highlight_green(), ansi_normal());
1592
1593 if (show_as_selected)
1594 printf(" %s(selected)%s",
1595 ansi_highlight_magenta(), ansi_normal());
1596
1597 if (show_reported) {
1598 if (e->type == BOOT_ENTRY_LOADER)
1599 printf(" %s(reported/absent)%s",
1600 ansi_highlight_red(), ansi_normal());
1601 else if (!e->reported_by_loader && e->type != BOOT_ENTRY_LOADER_AUTO)
1602 printf(" %s(not reported/new)%s",
1603 ansi_highlight_green(), ansi_normal());
1604 }
1605
1606 putchar('\n');
1607
1608 if (e->id)
1609 printf(" id: %s\n", e->id);
1610 if (e->path) {
1611 _cleanup_free_ char *text = NULL, *link = NULL;
1612
1613 const char *p = e->root ? path_startswith(e->path, e->root) : NULL;
1614 if (p) {
1615 text = strjoin(ansi_grey(), e->root, "/", ansi_normal(), "/", p);
1616 if (!text)
1617 return log_oom();
1618 }
1619
1620 /* Let's urlify the link to make it easy to view in an editor, but only if it is a text
1621 * file. Unified images are binary ELFs, and EFI variables are not pure text either. */
1622 if (e->type == BOOT_ENTRY_CONF)
1623 (void) terminal_urlify_path(e->path, text, &link);
1624
1625 printf(" source: %s\n", link ?: text ?: e->path);
1626 }
1627 if (e->tries_left != UINT_MAX) {
1628 printf(" tries: %u left", e->tries_left);
1629
1630 if (e->tries_done != UINT_MAX)
1631 printf("; %u done\n", e->tries_done);
1632 else
1633 printf("\n");
1634 }
1635
1636 if (e->sort_key)
1637 printf(" sort-key: %s\n", e->sort_key);
1638 if (e->version)
1639 printf(" version: %s\n", e->version);
1640 if (e->machine_id)
1641 printf(" machine-id: %s\n", e->machine_id);
1642 if (e->architecture)
1643 printf(" architecture: %s\n", e->architecture);
1644 if (e->kernel)
1645 boot_entry_file_list("linux", e->root, e->kernel, &status);
1646 if (e->efi)
1647 boot_entry_file_list("efi", e->root, e->efi, &status);
1648
1649 STRV_FOREACH(s, e->initrd)
1650 boot_entry_file_list(s == e->initrd ? "initrd" : NULL,
1651 e->root,
1652 *s,
1653 &status);
1654
1655 r = print_cmdline(e, global_addons);
1656 if (r < 0)
1657 return r;
1658
1659 if (e->device_tree)
1660 boot_entry_file_list("devicetree", e->root, e->device_tree, &status);
1661
1662 STRV_FOREACH(s, e->device_tree_overlay)
1663 boot_entry_file_list(s == e->device_tree_overlay ? "devicetree-overlay" : NULL,
1664 e->root,
1665 *s,
1666 &status);
1667
1668 return -status;
1669 }
1670
1671 int show_boot_entries(const BootConfig *config, JsonFormatFlags json_format) {
1672 int r;
1673
1674 assert(config);
1675
1676 if (!FLAGS_SET(json_format, JSON_FORMAT_OFF)) {
1677 _cleanup_(json_variant_unrefp) JsonVariant *array = NULL;
1678
1679 for (size_t i = 0; i < config->n_entries; i++) {
1680 const BootEntry *e = config->entries + i;
1681 _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
1682
1683 r = json_variant_merge_objectb(
1684 &v, JSON_BUILD_OBJECT(
1685 JSON_BUILD_PAIR("type", JSON_BUILD_STRING(boot_entry_type_json_to_string(e->type))),
1686 JSON_BUILD_PAIR_CONDITION(e->id, "id", JSON_BUILD_STRING(e->id)),
1687 JSON_BUILD_PAIR_CONDITION(e->path, "path", JSON_BUILD_STRING(e->path)),
1688 JSON_BUILD_PAIR_CONDITION(e->root, "root", JSON_BUILD_STRING(e->root)),
1689 JSON_BUILD_PAIR_CONDITION(e->title, "title", JSON_BUILD_STRING(e->title)),
1690 JSON_BUILD_PAIR_CONDITION(boot_entry_title(e), "showTitle", JSON_BUILD_STRING(boot_entry_title(e))),
1691 JSON_BUILD_PAIR_CONDITION(e->sort_key, "sortKey", JSON_BUILD_STRING(e->sort_key)),
1692 JSON_BUILD_PAIR_CONDITION(e->version, "version", JSON_BUILD_STRING(e->version)),
1693 JSON_BUILD_PAIR_CONDITION(e->machine_id, "machineId", JSON_BUILD_STRING(e->machine_id)),
1694 JSON_BUILD_PAIR_CONDITION(e->architecture, "architecture", JSON_BUILD_STRING(e->architecture)),
1695 JSON_BUILD_PAIR_CONDITION(e->kernel, "linux", JSON_BUILD_STRING(e->kernel)),
1696 JSON_BUILD_PAIR_CONDITION(e->efi, "efi", JSON_BUILD_STRING(e->efi)),
1697 JSON_BUILD_PAIR_CONDITION(!strv_isempty(e->initrd), "initrd", JSON_BUILD_STRV(e->initrd)),
1698 JSON_BUILD_PAIR_CONDITION(e->device_tree, "devicetree", JSON_BUILD_STRING(e->device_tree)),
1699 JSON_BUILD_PAIR_CONDITION(!strv_isempty(e->device_tree_overlay), "devicetreeOverlay", JSON_BUILD_STRV(e->device_tree_overlay))));
1700 if (r < 0)
1701 return log_oom();
1702
1703 r = json_cmdline(e, &config->global_addons, &v);
1704 if (r < 0)
1705 return log_oom();
1706
1707 /* Sanitizers (only memory sanitizer?) do not like function call with too many
1708 * arguments and trigger false positive warnings. Let's not add too many json objects
1709 * at once. */
1710 r = json_variant_merge_objectb(
1711 &v, JSON_BUILD_OBJECT(
1712 JSON_BUILD_PAIR("isReported", JSON_BUILD_BOOLEAN(e->reported_by_loader)),
1713 JSON_BUILD_PAIR_CONDITION(e->tries_left != UINT_MAX, "triesLeft", JSON_BUILD_UNSIGNED(e->tries_left)),
1714 JSON_BUILD_PAIR_CONDITION(e->tries_done != UINT_MAX, "triesDone", JSON_BUILD_UNSIGNED(e->tries_done)),
1715 JSON_BUILD_PAIR_CONDITION(config->default_entry >= 0, "isDefault", JSON_BUILD_BOOLEAN(i == (size_t) config->default_entry)),
1716 JSON_BUILD_PAIR_CONDITION(config->selected_entry >= 0, "isSelected", JSON_BUILD_BOOLEAN(i == (size_t) config->selected_entry))));
1717
1718 if (r < 0)
1719 return log_oom();
1720
1721 r = json_variant_append_array(&array, v);
1722 if (r < 0)
1723 return log_oom();
1724 }
1725
1726 json_variant_dump(array, json_format | JSON_FORMAT_EMPTY_ARRAY, NULL, NULL);
1727
1728 } else {
1729 for (size_t n = 0; n < config->n_entries; n++) {
1730 r = show_boot_entry(
1731 config->entries + n,
1732 &config->global_addons,
1733 /* show_as_default= */ n == (size_t) config->default_entry,
1734 /* show_as_selected= */ n == (size_t) config->selected_entry,
1735 /* show_discovered= */ true);
1736 if (r < 0)
1737 return r;
1738
1739 if (n+1 < config->n_entries)
1740 putchar('\n');
1741 }
1742 }
1743
1744 return 0;
1745 }