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