]> git.ipfire.org Git - thirdparty/systemd.git/blame - src/shared/bootspec.c
bootspec: pass around BootConfig struct instead of entries/n_entries fields only
[thirdparty/systemd.git] / src / shared / bootspec.c
CommitLineData
db9ecf05 1/* SPDX-License-Identifier: LGPL-2.1-or-later */
7e87c7d9 2
ca78ad1d 3#include <unistd.h>
7e87c7d9 4
7e87c7d9 5#include "bootspec.h"
e94830c0 6#include "bootspec-fundamental.h"
7e87c7d9 7#include "conf-files.h"
5e146a75 8#include "dirent-util.h"
0bb2f0f1 9#include "efi-loader.h"
5e146a75 10#include "env-file.h"
7e87c7d9
ZJS
11#include "fd-util.h"
12#include "fileio.h"
e94830c0 13#include "find-esp.h"
cc7a0bfa 14#include "path-util.h"
5e146a75 15#include "pe-header.h"
d04f0331 16#include "recurse-dir.h"
760877e9 17#include "sort-util.h"
af918182 18#include "stat-util.h"
7e87c7d9 19#include "strv.h"
5e146a75 20#include "unaligned.h"
7e87c7d9 21
0de2e1fd 22static void boot_entry_free(BootEntry *entry) {
4fe2ba0e 23 assert(entry);
7e87c7d9 24
12580bc3 25 free(entry->id);
15b82eec 26 free(entry->id_old);
2d3bfb69 27 free(entry->path);
43b736a8 28 free(entry->root);
7e87c7d9 29 free(entry->title);
64f05708 30 free(entry->show_title);
20ec8f53 31 free(entry->sort_key);
7e87c7d9
ZJS
32 free(entry->version);
33 free(entry->machine_id);
34 free(entry->architecture);
35 strv_free(entry->options);
36 free(entry->kernel);
37 free(entry->efi);
38 strv_free(entry->initrd);
39 free(entry->device_tree);
fdc5c042 40 strv_free(entry->device_tree_overlay);
7e87c7d9
ZJS
41}
42
43b736a8 43static int boot_entry_load(
d04f0331 44 FILE *f,
43b736a8 45 const char *root,
d04f0331
LP
46 const char *dir,
47 const char *id,
43b736a8
LP
48 BootEntry *entry) {
49
93f14ce2
LP
50 _cleanup_(boot_entry_free) BootEntry tmp = {
51 .type = BOOT_ENTRY_CONF,
52 };
53
7e87c7d9 54 unsigned line = 1;
736783d4 55 char *c;
7e87c7d9
ZJS
56 int r;
57
d04f0331 58 assert(f);
43b736a8 59 assert(root);
d04f0331
LP
60 assert(dir);
61 assert(id);
4fe2ba0e
LP
62 assert(entry);
63
d04f0331 64 /* Loads a Type #1 boot menu entry from the specified FILE* object */
7e87c7d9 65
d04f0331
LP
66 if (!efi_loader_entry_name_valid(id))
67 return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid loader entry name: %s", id);
68
69 c = endswith_no_case(id, ".conf");
736783d4 70 if (!c)
d04f0331 71 return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid loader entry file suffix: %s", id);
7e87c7d9 72
d04f0331
LP
73 tmp.id = strdup(id);
74 if (!tmp.id)
75 return log_oom();
eed7210a 76
d04f0331 77 tmp.id_old = strndup(id, c - id); /* Without .conf suffix */
736783d4
LP
78 if (!tmp.id_old)
79 return log_oom();
80
d04f0331 81 tmp.path = path_join(dir, id);
2d3bfb69
ZJS
82 if (!tmp.path)
83 return log_oom();
84
43b736a8
LP
85 tmp.root = strdup(root);
86 if (!tmp.root)
87 return log_oom();
88
7e87c7d9 89 for (;;) {
f99fdc3e
YW
90 _cleanup_free_ char *buf = NULL, *field = NULL;
91 const char *p;
7e87c7d9
ZJS
92
93 r = read_line(f, LONG_LINE_MAX, &buf);
94 if (r == 0)
95 break;
96 if (r == -ENOBUFS)
d04f0331 97 return log_error_errno(r, "%s:%u: Line too long", tmp.path, line);
7e87c7d9 98 if (r < 0)
d04f0331 99 return log_error_errno(r, "%s:%u: Error while reading: %m", tmp.path, line);
7e87c7d9
ZJS
100
101 line++;
102
103 if (IN_SET(*strstrip(buf), '#', '\0'))
104 continue;
105
f99fdc3e
YW
106 p = buf;
107 r = extract_first_word(&p, &field, " \t", 0);
108 if (r < 0) {
d04f0331 109 log_error_errno(r, "Failed to parse config file %s line %u: %m", tmp.path, line);
f99fdc3e
YW
110 continue;
111 }
112 if (r == 0) {
d04f0331 113 log_warning("%s:%u: Bad syntax", tmp.path, line);
7e87c7d9
ZJS
114 continue;
115 }
7e87c7d9 116
f99fdc3e 117 if (streq(field, "title"))
7e87c7d9 118 r = free_and_strdup(&tmp.title, p);
20ec8f53
LP
119 else if (streq(field, "sort-key"))
120 r = free_and_strdup(&tmp.sort_key, p);
f99fdc3e 121 else if (streq(field, "version"))
7e87c7d9 122 r = free_and_strdup(&tmp.version, p);
f99fdc3e 123 else if (streq(field, "machine-id"))
7e87c7d9 124 r = free_and_strdup(&tmp.machine_id, p);
f99fdc3e 125 else if (streq(field, "architecture"))
7e87c7d9 126 r = free_and_strdup(&tmp.architecture, p);
f99fdc3e 127 else if (streq(field, "options"))
7e87c7d9 128 r = strv_extend(&tmp.options, p);
f99fdc3e 129 else if (streq(field, "linux"))
7e87c7d9 130 r = free_and_strdup(&tmp.kernel, p);
f99fdc3e 131 else if (streq(field, "efi"))
7e87c7d9 132 r = free_and_strdup(&tmp.efi, p);
f99fdc3e 133 else if (streq(field, "initrd"))
7e87c7d9 134 r = strv_extend(&tmp.initrd, p);
f99fdc3e 135 else if (streq(field, "devicetree"))
7e87c7d9 136 r = free_and_strdup(&tmp.device_tree, p);
fdc5c042
LP
137 else if (streq(field, "devicetree-overlay")) {
138 _cleanup_strv_free_ char **l = NULL;
139
140 l = strv_split(p, NULL);
141 if (!l)
142 return log_oom();
143
144 r = strv_extend_strv(&tmp.device_tree_overlay, l, false);
145 } else {
d04f0331 146 log_notice("%s:%u: Unknown line \"%s\", ignoring.", tmp.path, line, field);
7e87c7d9
ZJS
147 continue;
148 }
149 if (r < 0)
d04f0331 150 return log_error_errno(r, "%s:%u: Error while reading: %m", tmp.path, line);
7e87c7d9
ZJS
151 }
152
153 *entry = tmp;
154 tmp = (BootEntry) {};
155 return 0;
156}
157
158void boot_config_free(BootConfig *config) {
4fe2ba0e
LP
159 assert(config);
160
7e87c7d9
ZJS
161 free(config->default_pattern);
162 free(config->timeout);
163 free(config->editor);
c1d4e298
JJ
164 free(config->auto_entries);
165 free(config->auto_firmware);
72263375 166 free(config->console_mode);
fe5a698f 167 free(config->random_seed_mode);
d403d8f0 168 free(config->beep);
7e87c7d9
ZJS
169
170 free(config->entry_oneshot);
171 free(config->entry_default);
a78e472d 172 free(config->entry_selected);
7e87c7d9 173
9817b7db 174 for (size_t i = 0; i < config->n_entries; i++)
7e87c7d9
ZJS
175 boot_entry_free(config->entries + i);
176 free(config->entries);
177}
178
0de2e1fd 179static int boot_loader_read_conf(const char *path, BootConfig *config) {
7e87c7d9
ZJS
180 _cleanup_fclose_ FILE *f = NULL;
181 unsigned line = 1;
182 int r;
183
4fe2ba0e
LP
184 assert(path);
185 assert(config);
186
7e87c7d9 187 f = fopen(path, "re");
f91ed3dc
LP
188 if (!f) {
189 if (errno == ENOENT)
190 return 0;
191
7e87c7d9 192 return log_error_errno(errno, "Failed to open \"%s\": %m", path);
f91ed3dc 193 }
7e87c7d9
ZJS
194
195 for (;;) {
f99fdc3e
YW
196 _cleanup_free_ char *buf = NULL, *field = NULL;
197 const char *p;
7e87c7d9
ZJS
198
199 r = read_line(f, LONG_LINE_MAX, &buf);
200 if (r == 0)
201 break;
202 if (r == -ENOBUFS)
203 return log_error_errno(r, "%s:%u: Line too long", path, line);
204 if (r < 0)
205 return log_error_errno(r, "%s:%u: Error while reading: %m", path, line);
206
207 line++;
208
209 if (IN_SET(*strstrip(buf), '#', '\0'))
210 continue;
211
f99fdc3e
YW
212 p = buf;
213 r = extract_first_word(&p, &field, " \t", 0);
214 if (r < 0) {
215 log_error_errno(r, "Failed to parse config file %s line %u: %m", path, line);
216 continue;
217 }
218 if (r == 0) {
7e87c7d9
ZJS
219 log_warning("%s:%u: Bad syntax", path, line);
220 continue;
221 }
7e87c7d9 222
f99fdc3e 223 if (streq(field, "default"))
7e87c7d9 224 r = free_and_strdup(&config->default_pattern, p);
f99fdc3e 225 else if (streq(field, "timeout"))
7e87c7d9 226 r = free_and_strdup(&config->timeout, p);
f99fdc3e 227 else if (streq(field, "editor"))
7e87c7d9 228 r = free_and_strdup(&config->editor, p);
790f84eb 229 else if (streq(field, "auto-entries"))
c1d4e298 230 r = free_and_strdup(&config->auto_entries, p);
790f84eb 231 else if (streq(field, "auto-firmware"))
c1d4e298 232 r = free_and_strdup(&config->auto_firmware, p);
790f84eb 233 else if (streq(field, "console-mode"))
d37b0737 234 r = free_and_strdup(&config->console_mode, p);
fe5a698f
YW
235 else if (streq(field, "random-seed-mode"))
236 r = free_and_strdup(&config->random_seed_mode, p);
d403d8f0
LP
237 else if (streq(field, "beep"))
238 r = free_and_strdup(&config->beep, p);
7e87c7d9 239 else {
feb41f1f 240 log_notice("%s:%u: Unknown line \"%s\", ignoring.", path, line, field);
7e87c7d9
ZJS
241 continue;
242 }
243 if (r < 0)
244 return log_error_errno(r, "%s:%u: Error while reading: %m", path, line);
245 }
246
f91ed3dc 247 return 1;
7e87c7d9
ZJS
248}
249
93bab288 250static int boot_entry_compare(const BootEntry *a, const BootEntry *b) {
20ec8f53
LP
251 int r;
252
253 assert(a);
254 assert(b);
255
256 r = CMP(!a->sort_key, !b->sort_key);
257 if (r != 0)
258 return r;
62a4b584 259
20ec8f53
LP
260 if (a->sort_key && b->sort_key) {
261 r = strcmp(a->sort_key, b->sort_key);
262 if (r != 0)
263 return r;
264
265 r = strcmp_ptr(a->machine_id, b->machine_id);
266 if (r != 0)
267 return r;
268
269 r = -strverscmp_improved(a->version, b->version);
270 if (r != 0)
271 return r;
272 }
273
62a4b584 274 return -strverscmp_improved(a->id, b->id);
7e87c7d9
ZJS
275}
276
43b736a8 277static int boot_entries_find(
85f4ae2f 278 BootConfig *config,
43b736a8 279 const char *root,
85f4ae2f 280 const char *dir) {
43b736a8 281
d04f0331
LP
282 _cleanup_free_ DirectoryEntries *dentries = NULL;
283 _cleanup_close_ int dir_fd = -1;
7e87c7d9 284 int r;
7e87c7d9 285
85f4ae2f 286 assert(config);
43b736a8 287 assert(root);
4fe2ba0e 288 assert(dir);
4fe2ba0e 289
d04f0331
LP
290 dir_fd = open(dir, O_DIRECTORY|O_CLOEXEC);
291 if (dir_fd < 0) {
292 if (errno == ENOENT)
293 return 0;
294
295 return log_error_errno(errno, "Failed to open '%s': %m", dir);
296 }
297
298 r = readdir_all(dir_fd, RECURSE_DIR_IGNORE_DOT, &dentries);
7e87c7d9 299 if (r < 0)
d04f0331
LP
300 return log_error_errno(r, "Failed to read directory '%s': %m", dir);
301
302 for (size_t i = 0; i < dentries->n_entries; i++) {
303 const struct dirent *de = dentries->entries[i];
304 _cleanup_fclose_ FILE *f = NULL;
305
306 if (!dirent_is_file(de))
307 continue;
308
309 if (!endswith_no_case(de->d_name, ".conf"))
310 continue;
7e87c7d9 311
85f4ae2f 312 if (!GREEDY_REALLOC0(config->entries, config->n_entries + 1))
7e87c7d9
ZJS
313 return log_oom();
314
d04f0331
LP
315 r = xfopenat(dir_fd, de->d_name, "re", 0, &f);
316 if (r < 0) {
317 log_warning_errno(r, "Failed to open %s/%s, ignoring: %m", dir, de->d_name);
318 continue;
319 }
320
321 r = fd_verify_regular(fileno(f));
322 if (r < 0) {
323 log_warning_errno(r, "File %s/%s is not regular, ignoring: %m", dir, de->d_name);
324 continue;
325 }
326
85f4ae2f 327 r = boot_entry_load(f, root, dir, de->d_name, config->entries + config->n_entries);
7e87c7d9
ZJS
328 if (r < 0)
329 continue;
330
85f4ae2f 331 config->n_entries++;
7e87c7d9
ZJS
332 }
333
7e87c7d9
ZJS
334 return 0;
335}
336
5e146a75
LP
337static int boot_entry_load_unified(
338 const char *root,
339 const char *path,
340 const char *osrelease,
341 const char *cmdline,
342 BootEntry *ret) {
343
c2caeb5d
LP
344 _cleanup_free_ char *os_pretty_name = NULL, *os_image_id = NULL, *os_name = NULL, *os_id = NULL,
345 *os_image_version = NULL, *os_version = NULL, *os_version_id = NULL, *os_build_id = NULL;
93f14ce2
LP
346 _cleanup_(boot_entry_free) BootEntry tmp = {
347 .type = BOOT_ENTRY_UNIFIED,
348 };
20ec8f53 349 const char *k, *good_name, *good_version, *good_sort_key;
5e146a75 350 _cleanup_fclose_ FILE *f = NULL;
5e146a75
LP
351 int r;
352
353 assert(root);
354 assert(path);
355 assert(osrelease);
356
357 k = path_startswith(path, root);
358 if (!k)
359 return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Path is not below root: %s", path);
360
673a1e6f 361 f = fmemopen_unlocked((void*) osrelease, strlen(osrelease), "r");
5e146a75
LP
362 if (!f)
363 return log_error_errno(errno, "Failed to open os-release buffer: %m");
364
15b82eec
SM
365 r = parse_env_file(f, "os-release",
366 "PRETTY_NAME", &os_pretty_name,
c2caeb5d
LP
367 "IMAGE_ID", &os_image_id,
368 "NAME", &os_name,
15b82eec 369 "ID", &os_id,
c2caeb5d
LP
370 "IMAGE_VERSION", &os_image_version,
371 "VERSION", &os_version,
372 "VERSION_ID", &os_version_id,
373 "BUILD_ID", &os_build_id);
5e146a75
LP
374 if (r < 0)
375 return log_error_errno(r, "Failed to parse os-release data from unified kernel image %s: %m", path);
376
20ec8f53 377 if (!bootspec_pick_name_version_sort_key(
c2caeb5d
LP
378 os_pretty_name,
379 os_image_id,
380 os_name,
381 os_id,
382 os_image_version,
383 os_version,
384 os_version_id,
385 os_build_id,
386 &good_name,
20ec8f53
LP
387 &good_version,
388 &good_sort_key))
5e146a75
LP
389 return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Missing fields in os-release data from unified kernel image %s, refusing.", path);
390
c2caeb5d
LP
391 r = path_extract_filename(path, &tmp.id);
392 if (r < 0)
393 return log_error_errno(r, "Failed to extract file name from '%s': %m", path);
5e146a75 394
eed7210a 395 if (!efi_loader_entry_name_valid(tmp.id))
dfc22cb4 396 return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid loader entry name: %s", tmp.id);
eed7210a 397
c2caeb5d
LP
398 if (os_id && os_version_id) {
399 tmp.id_old = strjoin(os_id, "-", os_version_id);
400 if (!tmp.id_old)
401 return log_oom();
402 }
403
5e146a75
LP
404 tmp.path = strdup(path);
405 if (!tmp.path)
406 return log_oom();
407
408 tmp.root = strdup(root);
409 if (!tmp.root)
410 return log_oom();
411
412 tmp.kernel = strdup(skip_leading_chars(k, "/"));
413 if (!tmp.kernel)
414 return log_oom();
415
416 tmp.options = strv_new(skip_leading_chars(cmdline, WHITESPACE));
417 if (!tmp.options)
418 return log_oom();
419
420 delete_trailing_chars(tmp.options[0], WHITESPACE);
421
c2caeb5d
LP
422 tmp.title = strdup(good_name);
423 if (!tmp.title)
424 return log_oom();
425
20ec8f53
LP
426 tmp.sort_key = strdup(good_sort_key);
427 if (!tmp.sort_key)
428 return log_oom();
429
c2caeb5d
LP
430 tmp.version = strdup(good_version);
431 if (!tmp.version)
432 return log_oom();
5e146a75
LP
433
434 *ret = tmp;
435 tmp = (BootEntry) {};
436 return 0;
437}
438
439/* Maximum PE section we are willing to load (Note that sections we are not interested in may be larger, but
440 * the ones we do care about and we are willing to load into memory have this size limit.) */
441#define PE_SECTION_SIZE_MAX (4U*1024U*1024U)
442
443static int find_sections(
444 int fd,
445 char **ret_osrelease,
446 char **ret_cmdline) {
447
448 _cleanup_free_ struct PeSectionHeader *sections = NULL;
449 _cleanup_free_ char *osrelease = NULL, *cmdline = NULL;
5e146a75
LP
450 ssize_t n;
451
9817b7db 452 struct DosFileHeader dos;
5e146a75
LP
453 n = pread(fd, &dos, sizeof(dos), 0);
454 if (n < 0)
455 return log_error_errno(errno, "Failed read DOS header: %m");
456 if (n != sizeof(dos))
457 return log_error_errno(SYNTHETIC_ERRNO(EIO), "Short read while reading DOS header, refusing.");
458
459 if (dos.Magic[0] != 'M' || dos.Magic[1] != 'Z')
460 return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "DOS executable magic missing, refusing.");
461
9817b7db
ZJS
462 uint64_t start = unaligned_read_le32(&dos.ExeHeader);
463
464 struct PeHeader pe;
5e146a75
LP
465 n = pread(fd, &pe, sizeof(pe), start);
466 if (n < 0)
467 return log_error_errno(errno, "Failed to read PE header: %m");
468 if (n != sizeof(pe))
469 return log_error_errno(SYNTHETIC_ERRNO(EIO), "Short read while reading PE header, refusing.");
470
471 if (pe.Magic[0] != 'P' || pe.Magic[1] != 'E' || pe.Magic[2] != 0 || pe.Magic[3] != 0)
472 return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "PE executable magic missing, refusing.");
473
9817b7db 474 size_t n_sections = unaligned_read_le16(&pe.FileHeader.NumberOfSections);
5e146a75
LP
475 if (n_sections > 96)
476 return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "PE header has too many sections, refusing.");
477
478 sections = new(struct PeSectionHeader, n_sections);
479 if (!sections)
480 return log_oom();
481
482 n = pread(fd, sections,
483 n_sections * sizeof(struct PeSectionHeader),
484 start + sizeof(pe) + unaligned_read_le16(&pe.FileHeader.SizeOfOptionalHeader));
485 if (n < 0)
486 return log_error_errno(errno, "Failed to read section data: %m");
487 if ((size_t) n != n_sections * sizeof(struct PeSectionHeader))
488 return log_error_errno(SYNTHETIC_ERRNO(EIO), "Short read while reading sections, refusing.");
489
9817b7db 490 for (size_t i = 0; i < n_sections; i++) {
5e146a75
LP
491 _cleanup_free_ char *k = NULL;
492 uint32_t offset, size;
493 char **b;
494
495 if (strneq((char*) sections[i].Name, ".osrel", sizeof(sections[i].Name)))
496 b = &osrelease;
497 else if (strneq((char*) sections[i].Name, ".cmdline", sizeof(sections[i].Name)))
498 b = &cmdline;
499 else
500 continue;
501
502 if (*b)
503 return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Duplicate section %s, refusing.", sections[i].Name);
504
505 offset = unaligned_read_le32(&sections[i].PointerToRawData);
506 size = unaligned_read_le32(&sections[i].VirtualSize);
507
508 if (size > PE_SECTION_SIZE_MAX)
509 return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Section %s too large, refusing.", sections[i].Name);
510
511 k = new(char, size+1);
512 if (!k)
513 return log_oom();
514
515 n = pread(fd, k, size, offset);
516 if (n < 0)
517 return log_error_errno(errno, "Failed to read section payload: %m");
a75fcef8 518 if ((size_t) n != size)
5e146a75
LP
519 return log_error_errno(SYNTHETIC_ERRNO(EIO), "Short read while reading section payload, refusing:");
520
521 /* Allow one trailing NUL byte, but nothing more. */
522 if (size > 0 && memchr(k, 0, size - 1))
523 return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Section contains embedded NUL byte: %m");
524
525 k[size] = 0;
526 *b = TAKE_PTR(k);
527 }
528
529 if (!osrelease)
530 return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Image lacks .osrel section, refusing.");
531
532 if (ret_osrelease)
533 *ret_osrelease = TAKE_PTR(osrelease);
534 if (ret_cmdline)
535 *ret_cmdline = TAKE_PTR(cmdline);
536
537 return 0;
538}
539
540static int boot_entries_find_unified(
85f4ae2f 541 BootConfig *config,
5e146a75 542 const char *root,
85f4ae2f 543 const char *dir) {
5e146a75
LP
544
545 _cleanup_(closedirp) DIR *d = NULL;
5e146a75
LP
546 int r;
547
85f4ae2f 548 assert(config);
5e146a75 549 assert(dir);
5e146a75
LP
550
551 d = opendir(dir);
552 if (!d) {
553 if (errno == ENOENT)
554 return 0;
555
556 return log_error_errno(errno, "Failed to open %s: %m", dir);
557 }
558
559 FOREACH_DIRENT(de, d, return log_error_errno(errno, "Failed to read %s: %m", dir)) {
560 _cleanup_free_ char *j = NULL, *osrelease = NULL, *cmdline = NULL;
561 _cleanup_close_ int fd = -1;
562
563 if (!dirent_is_file(de))
564 continue;
565
566 if (!endswith_no_case(de->d_name, ".efi"))
567 continue;
568
85f4ae2f 569 if (!GREEDY_REALLOC0(config->entries, config->n_entries + 1))
5e146a75
LP
570 return log_oom();
571
572 fd = openat(dirfd(d), de->d_name, O_RDONLY|O_CLOEXEC|O_NONBLOCK);
573 if (fd < 0) {
574 log_warning_errno(errno, "Failed to open %s/%s, ignoring: %m", dir, de->d_name);
575 continue;
576 }
577
578 r = fd_verify_regular(fd);
579 if (r < 0) {
dba33c4a 580 log_warning_errno(r, "File %s/%s is not regular, ignoring: %m", dir, de->d_name);
5e146a75
LP
581 continue;
582 }
583
584 r = find_sections(fd, &osrelease, &cmdline);
585 if (r < 0)
586 continue;
587
588 j = path_join(dir, de->d_name);
589 if (!j)
590 return log_oom();
591
85f4ae2f 592 r = boot_entry_load_unified(root, j, osrelease, cmdline, config->entries + config->n_entries);
5e146a75
LP
593 if (r < 0)
594 continue;
595
85f4ae2f 596 config->n_entries++;
5e146a75
LP
597 }
598
5e146a75
LP
599 return 0;
600}
601
bb682057 602static bool find_nonunique(const BootEntry *entries, size_t n_entries, bool arr[]) {
64f05708
ZJS
603 bool non_unique = false;
604
4fe2ba0e
LP
605 assert(entries || n_entries == 0);
606 assert(arr || n_entries == 0);
607
d5ac1d4e 608 for (size_t i = 0; i < n_entries; i++)
64f05708
ZJS
609 arr[i] = false;
610
d5ac1d4e
LP
611 for (size_t i = 0; i < n_entries; i++)
612 for (size_t j = 0; j < n_entries; j++)
64f05708
ZJS
613 if (i != j && streq(boot_entry_title(entries + i),
614 boot_entry_title(entries + j)))
615 non_unique = arr[i] = arr[j] = true;
616
617 return non_unique;
618}
619
620static int boot_entries_uniquify(BootEntry *entries, size_t n_entries) {
d5ac1d4e 621 _cleanup_free_ bool *arr = NULL;
64f05708 622 char *s;
64f05708 623
4fe2ba0e
LP
624 assert(entries || n_entries == 0);
625
d5ac1d4e
LP
626 if (n_entries == 0)
627 return 0;
628
629 arr = new(bool, n_entries);
630 if (!arr)
631 return -ENOMEM;
632
64f05708
ZJS
633 /* Find _all_ non-unique titles */
634 if (!find_nonunique(entries, n_entries, arr))
635 return 0;
636
637 /* Add version to non-unique titles */
d5ac1d4e 638 for (size_t i = 0; i < n_entries; i++)
64f05708 639 if (arr[i] && entries[i].version) {
d5ac1d4e 640 if (asprintf(&s, "%s (%s)", boot_entry_title(entries + i), entries[i].version) < 0)
64f05708
ZJS
641 return -ENOMEM;
642
643 free_and_replace(entries[i].show_title, s);
644 }
645
646 if (!find_nonunique(entries, n_entries, arr))
647 return 0;
648
649 /* Add machine-id to non-unique titles */
d5ac1d4e 650 for (size_t i = 0; i < n_entries; i++)
64f05708 651 if (arr[i] && entries[i].machine_id) {
d5ac1d4e 652 if (asprintf(&s, "%s (%s)", boot_entry_title(entries + i), entries[i].machine_id) < 0)
64f05708
ZJS
653 return -ENOMEM;
654
655 free_and_replace(entries[i].show_title, s);
656 }
657
658 if (!find_nonunique(entries, n_entries, arr))
659 return 0;
660
661 /* Add file name to non-unique titles */
d5ac1d4e 662 for (size_t i = 0; i < n_entries; i++)
64f05708 663 if (arr[i]) {
d5ac1d4e 664 if (asprintf(&s, "%s (%s)", boot_entry_title(entries + i), entries[i].id) < 0)
64f05708
ZJS
665 return -ENOMEM;
666
667 free_and_replace(entries[i].show_title, s);
668 }
669
670 return 0;
671}
672
20ec8f53
LP
673static int boot_config_find(const BootConfig *config, const char *id) {
674 assert(config);
675
676 if (!id)
677 return -1;
678
679 for (size_t i = 0; i < config->n_entries; i++)
680 if (fnmatch(id, config->entries[i].id, FNM_CASEFOLD) == 0)
681 return i;
682
683 return -1;
684}
685
ad1afd60 686static int boot_entries_select_default(const BootConfig *config) {
7e87c7d9
ZJS
687 int i;
688
4fe2ba0e 689 assert(config);
388d2993
ZJS
690 assert(config->entries || config->n_entries == 0);
691
692 if (config->n_entries == 0) {
693 log_debug("Found no default boot entry :(");
694 return -1; /* -1 means "no default" */
695 }
4fe2ba0e 696
20ec8f53
LP
697 if (config->entry_oneshot) {
698 i = boot_config_find(config, config->entry_oneshot);
699 if (i >= 0) {
700 log_debug("Found default: id \"%s\" is matched by LoaderEntryOneShot",
701 config->entries[i].id);
702 return i;
703 }
704 }
705
706 if (config->entry_default) {
707 i = boot_config_find(config, config->entry_default);
708 if (i >= 0) {
709 log_debug("Found default: id \"%s\" is matched by LoaderEntryDefault",
710 config->entries[i].id);
711 return i;
712 }
713 }
714
715 if (config->default_pattern) {
716 i = boot_config_find(config, config->default_pattern);
717 if (i >= 0) {
718 log_debug("Found default: id \"%s\" is matched by pattern \"%s\"",
719 config->entries[i].id, config->default_pattern);
720 return i;
721 }
722 }
723
724 log_debug("Found default: first entry \"%s\"", config->entries[0].id);
725 return 0;
7e87c7d9
ZJS
726}
727
a78e472d 728static int boot_entries_select_selected(const BootConfig *config) {
a78e472d
LP
729 assert(config);
730 assert(config->entries || config->n_entries == 0);
731
732 if (!config->entry_selected || config->n_entries == 0)
733 return -1;
734
20ec8f53 735 return boot_config_find(config, config->entry_selected);
a78e472d
LP
736}
737
738static int boot_load_efi_entry_pointers(BootConfig *config) {
739 int r;
740
741 assert(config);
742
743 if (!is_efi_boot())
744 return 0;
745
746 /* Loads the three "pointers" to boot loader entries from their EFI variables */
747
748 r = efi_get_variable_string(EFI_LOADER_VARIABLE(LoaderEntryOneShot), &config->entry_oneshot);
749 if (r < 0 && !IN_SET(r, -ENOENT, -ENODATA)) {
750 log_warning_errno(r, "Failed to read EFI variable \"LoaderEntryOneShot\": %m");
751 if (r == -ENOMEM)
752 return r;
753 }
754
755 r = efi_get_variable_string(EFI_LOADER_VARIABLE(LoaderEntryDefault), &config->entry_default);
756 if (r < 0 && !IN_SET(r, -ENOENT, -ENODATA)) {
757 log_warning_errno(r, "Failed to read EFI variable \"LoaderEntryDefault\": %m");
758 if (r == -ENOMEM)
759 return r;
760 }
761
762 r = efi_get_variable_string(EFI_LOADER_VARIABLE(LoaderEntrySelected), &config->entry_selected);
763 if (r < 0 && !IN_SET(r, -ENOENT, -ENODATA)) {
764 log_warning_errno(r, "Failed to read EFI variable \"LoaderEntrySelected\": %m");
765 if (r == -ENOMEM)
766 return r;
767 }
768
769 return 1;
770}
771
a2f8664e
LP
772int boot_entries_load_config(
773 const char *esp_path,
774 const char *xbootldr_path,
775 BootConfig *config) {
776
7e87c7d9
ZJS
777 const char *p;
778 int r;
779
4fe2ba0e
LP
780 assert(config);
781
a2f8664e
LP
782 if (esp_path) {
783 p = strjoina(esp_path, "/loader/loader.conf");
784 r = boot_loader_read_conf(p, config);
785 if (r < 0)
786 return r;
7e87c7d9 787
a2f8664e 788 p = strjoina(esp_path, "/loader/entries");
85f4ae2f 789 r = boot_entries_find(config, esp_path, p);
a2f8664e
LP
790 if (r < 0)
791 return r;
5e146a75
LP
792
793 p = strjoina(esp_path, "/EFI/Linux/");
85f4ae2f 794 r = boot_entries_find_unified(config, esp_path, p);
5e146a75
LP
795 if (r < 0)
796 return r;
a2f8664e
LP
797 }
798
799 if (xbootldr_path) {
800 p = strjoina(xbootldr_path, "/loader/entries");
85f4ae2f 801 r = boot_entries_find(config, xbootldr_path, p);
a2f8664e
LP
802 if (r < 0)
803 return r;
5e146a75
LP
804
805 p = strjoina(xbootldr_path, "/EFI/Linux/");
85f4ae2f 806 r = boot_entries_find_unified(config, xbootldr_path, p);
5e146a75
LP
807 if (r < 0)
808 return r;
a2f8664e 809 }
7e87c7d9 810
dd2bf34c
LP
811 typesafe_qsort(config->entries, config->n_entries, boot_entry_compare);
812
64f05708
ZJS
813 r = boot_entries_uniquify(config->entries, config->n_entries);
814 if (r < 0)
815 return log_error_errno(r, "Failed to uniquify boot entries: %m");
816
a78e472d
LP
817 r = boot_load_efi_entry_pointers(config);
818 if (r < 0)
819 return r;
7e87c7d9
ZJS
820
821 config->default_entry = boot_entries_select_default(config);
a78e472d
LP
822 config->selected_entry = boot_entries_select_selected(config);
823
7e87c7d9
ZJS
824 return 0;
825}
af918182 826
eea4ce1e
LP
827int boot_entries_load_config_auto(
828 const char *override_esp_path,
829 const char *override_xbootldr_path,
830 BootConfig *config) {
831
832 _cleanup_free_ char *esp_where = NULL, *xbootldr_where = NULL;
f63b5ad9 833 dev_t esp_devid = 0, xbootldr_devid = 0;
eea4ce1e
LP
834 int r;
835
836 assert(config);
837
838 /* This function is similar to boot_entries_load_config(), however we automatically search for the
839 * ESP and the XBOOTLDR partition unless it is explicitly specified. Also, if the user did not pass
840 * an ESP or XBOOTLDR path directly, let's see if /run/boot-loader-entries/ exists. If so, let's
841 * read data from there, as if it was an ESP (i.e. loading both entries and loader.conf data from
842 * it). This allows other boot loaders to pass boot loader entry information to our tools if they
843 * want to. */
844
845 if (!override_esp_path && !override_xbootldr_path) {
f40999f8
ZJS
846 if (access("/run/boot-loader-entries/", F_OK) >= 0)
847 return boot_entries_load_config("/run/boot-loader-entries/", NULL, config);
eea4ce1e 848
f40999f8
ZJS
849 if (errno != ENOENT)
850 return log_error_errno(errno,
851 "Failed to determine whether /run/boot-loader-entries/ exists: %m");
eea4ce1e
LP
852 }
853
f63b5ad9 854 r = find_esp_and_warn(override_esp_path, /* unprivileged_mode= */ false, &esp_where, NULL, NULL, NULL, NULL, &esp_devid);
cc5957dc 855 if (r < 0) /* we don't log about ENOKEY here, but propagate it, leaving it to the caller to log */
eea4ce1e
LP
856 return r;
857
f63b5ad9 858 r = find_xbootldr_and_warn(override_xbootldr_path, /* unprivileged_mode= */ false, &xbootldr_where, NULL, &xbootldr_devid);
eea4ce1e
LP
859 if (r < 0 && r != -ENOKEY)
860 return r; /* It's fine if the XBOOTLDR partition doesn't exist, hence we ignore ENOKEY here */
861
f63b5ad9
LP
862 /* If both paths actually refer to the same inode, suppress the xbootldr path */
863 if (esp_where && xbootldr_where && devid_set_and_equal(esp_devid, xbootldr_devid))
864 xbootldr_where = mfree(xbootldr_where);
865
f40999f8 866 return boot_entries_load_config(esp_where, xbootldr_where, config);
eea4ce1e
LP
867}
868
d4bd786d
LP
869int boot_entries_augment_from_loader(
870 BootConfig *config,
9951736b
LP
871 char **found_by_loader,
872 bool only_auto) {
d4bd786d
LP
873
874 static const char *const title_table[] = {
93f14ce2
LP
875 /* Pretty names for a few well-known automatically discovered entries. */
876 "auto-osx", "macOS",
877 "auto-windows", "Windows Boot Manager",
878 "auto-efi-shell", "EFI Shell",
879 "auto-efi-default", "EFI Default Loader",
880 "auto-reboot-to-firmware-setup", "Reboot Into Firmware Interface",
881 };
882
93f14ce2
LP
883 assert(config);
884
885 /* Let's add the entries discovered by the boot loader to the end of our list, unless they are
886 * already included there. */
887
93f14ce2 888 STRV_FOREACH(i, found_by_loader) {
bb682057 889 BootEntry *existing;
ce4c4f81 890 _cleanup_free_ char *c = NULL, *t = NULL, *p = NULL;
93f14ce2 891
bb682057
LP
892 existing = boot_config_find_entry(config, *i);
893 if (existing) {
894 existing->reported_by_loader = true;
93f14ce2 895 continue;
bb682057 896 }
93f14ce2 897
9951736b 898 if (only_auto && !startswith(*i, "auto-"))
93f14ce2
LP
899 continue;
900
901 c = strdup(*i);
902 if (!c)
903 return log_oom();
904
905 STRV_FOREACH_PAIR(a, b, (char**) title_table)
906 if (streq(*a, *i)) {
907 t = strdup(*b);
908 if (!t)
909 return log_oom();
910 break;
911 }
912
e6f055cb 913 p = strdup(EFIVAR_PATH(EFI_LOADER_VARIABLE(LoaderEntries)));
ce4c4f81
ZJS
914 if (!p)
915 return log_oom();
916
319a4f4b 917 if (!GREEDY_REALLOC0(config->entries, config->n_entries + 1))
93f14ce2
LP
918 return log_oom();
919
920 config->entries[config->n_entries++] = (BootEntry) {
bb682057 921 .type = startswith(*i, "auto-") ? BOOT_ENTRY_LOADER_AUTO : BOOT_ENTRY_LOADER,
93f14ce2
LP
922 .id = TAKE_PTR(c),
923 .title = TAKE_PTR(t),
ce4c4f81 924 .path = TAKE_PTR(p),
bb682057 925 .reported_by_loader = true,
93f14ce2
LP
926 };
927 }
928
929 return 0;
930}