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