]> git.ipfire.org Git - thirdparty/systemd.git/blame - src/shared/bootspec.c
bootspec: try harder to suppress duplicate enumerated entries
[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);
d486a0ea
LP
177
178 set_free(config->inodes_seen);
7e87c7d9
ZJS
179}
180
0de2e1fd 181static int boot_loader_read_conf(const char *path, BootConfig *config) {
7e87c7d9
ZJS
182 _cleanup_fclose_ FILE *f = NULL;
183 unsigned line = 1;
184 int r;
185
4fe2ba0e
LP
186 assert(path);
187 assert(config);
188
7e87c7d9 189 f = fopen(path, "re");
f91ed3dc
LP
190 if (!f) {
191 if (errno == ENOENT)
192 return 0;
193
7e87c7d9 194 return log_error_errno(errno, "Failed to open \"%s\": %m", path);
f91ed3dc 195 }
7e87c7d9
ZJS
196
197 for (;;) {
f99fdc3e
YW
198 _cleanup_free_ char *buf = NULL, *field = NULL;
199 const char *p;
7e87c7d9
ZJS
200
201 r = read_line(f, LONG_LINE_MAX, &buf);
202 if (r == 0)
203 break;
204 if (r == -ENOBUFS)
205 return log_error_errno(r, "%s:%u: Line too long", path, line);
206 if (r < 0)
207 return log_error_errno(r, "%s:%u: Error while reading: %m", path, line);
208
209 line++;
210
211 if (IN_SET(*strstrip(buf), '#', '\0'))
212 continue;
213
f99fdc3e
YW
214 p = buf;
215 r = extract_first_word(&p, &field, " \t", 0);
216 if (r < 0) {
217 log_error_errno(r, "Failed to parse config file %s line %u: %m", path, line);
218 continue;
219 }
220 if (r == 0) {
7e87c7d9
ZJS
221 log_warning("%s:%u: Bad syntax", path, line);
222 continue;
223 }
7e87c7d9 224
f99fdc3e 225 if (streq(field, "default"))
7e87c7d9 226 r = free_and_strdup(&config->default_pattern, p);
f99fdc3e 227 else if (streq(field, "timeout"))
7e87c7d9 228 r = free_and_strdup(&config->timeout, p);
f99fdc3e 229 else if (streq(field, "editor"))
7e87c7d9 230 r = free_and_strdup(&config->editor, p);
790f84eb 231 else if (streq(field, "auto-entries"))
c1d4e298 232 r = free_and_strdup(&config->auto_entries, p);
790f84eb 233 else if (streq(field, "auto-firmware"))
c1d4e298 234 r = free_and_strdup(&config->auto_firmware, p);
790f84eb 235 else if (streq(field, "console-mode"))
d37b0737 236 r = free_and_strdup(&config->console_mode, p);
fe5a698f
YW
237 else if (streq(field, "random-seed-mode"))
238 r = free_and_strdup(&config->random_seed_mode, p);
d403d8f0
LP
239 else if (streq(field, "beep"))
240 r = free_and_strdup(&config->beep, p);
7e87c7d9 241 else {
feb41f1f 242 log_notice("%s:%u: Unknown line \"%s\", ignoring.", path, line, field);
7e87c7d9
ZJS
243 continue;
244 }
245 if (r < 0)
246 return log_error_errno(r, "%s:%u: Error while reading: %m", path, line);
247 }
248
f91ed3dc 249 return 1;
7e87c7d9
ZJS
250}
251
93bab288 252static int boot_entry_compare(const BootEntry *a, const BootEntry *b) {
20ec8f53
LP
253 int r;
254
255 assert(a);
256 assert(b);
257
258 r = CMP(!a->sort_key, !b->sort_key);
259 if (r != 0)
260 return r;
62a4b584 261
20ec8f53
LP
262 if (a->sort_key && b->sort_key) {
263 r = strcmp(a->sort_key, b->sort_key);
264 if (r != 0)
265 return r;
266
267 r = strcmp_ptr(a->machine_id, b->machine_id);
268 if (r != 0)
269 return r;
270
271 r = -strverscmp_improved(a->version, b->version);
272 if (r != 0)
273 return r;
274 }
275
62a4b584 276 return -strverscmp_improved(a->id, b->id);
7e87c7d9
ZJS
277}
278
d486a0ea
LP
279static void inode_hash_func(const struct stat *q, struct siphash *state) {
280 siphash24_compress(&q->st_dev, sizeof(q->st_dev), state);
281 siphash24_compress(&q->st_ino, sizeof(q->st_ino), state);
282}
283
284static int inode_compare_func(const struct stat *a, const struct stat *b) {
285 int r;
286
287 r = CMP(a->st_dev, b->st_dev);
288 if (r != 0)
289 return r;
290
291 return CMP(a->st_ino, b->st_ino);
292}
293
294DEFINE_HASH_OPS_WITH_KEY_DESTRUCTOR(inode_hash_ops, struct stat, inode_hash_func, inode_compare_func, free);
295
296static int config_check_inode_relevant_and_unseen(BootConfig *config, int fd, const char *fname) {
297 _cleanup_free_ char *d = NULL;
298 struct stat st;
299
300 assert(config);
301 assert(fd >= 0);
302 assert(fname);
303
304 /* So, here's the thing: because of the mess around /efi/ vs. /boot/ vs. /boot/efi/ it might be that
305 * people have these dirs, or subdirs of them symlinked or bind mounted, and we might end up
306 * iterating though some dirs multiple times. Let's thus rather be safe than sorry, and track the
307 * inodes we already processed: let's ignore inodes we have seen already. This should be robust
308 * against any form of symlinking or bind mounting, and effectively suppress any such duplicates. */
309
310 if (fstat(fd, &st) < 0)
311 return log_error_errno(errno, "Failed to stat('%s'): %m", fname);
312 if (!S_ISREG(st.st_mode)) {
313 log_debug("File '%s' is not a reguar file, ignoring.", fname);
314 return false;
315 }
316
317 if (set_contains(config->inodes_seen, &st)) {
318 log_debug("Inode '%s' already seen before, ignoring.", fname);
319 return false;
320 }
321 d = memdup(&st, sizeof(st));
322 if (!d)
323 return log_oom();
324 if (set_ensure_put(&config->inodes_seen, &inode_hash_ops, d) < 0)
325 return log_oom();
326
327 TAKE_PTR(d);
328 return true;
329}
330
43b736a8 331static int boot_entries_find(
85f4ae2f 332 BootConfig *config,
43b736a8 333 const char *root,
85f4ae2f 334 const char *dir) {
43b736a8 335
d04f0331
LP
336 _cleanup_free_ DirectoryEntries *dentries = NULL;
337 _cleanup_close_ int dir_fd = -1;
7e87c7d9 338 int r;
7e87c7d9 339
85f4ae2f 340 assert(config);
43b736a8 341 assert(root);
4fe2ba0e 342 assert(dir);
4fe2ba0e 343
d04f0331
LP
344 dir_fd = open(dir, O_DIRECTORY|O_CLOEXEC);
345 if (dir_fd < 0) {
346 if (errno == ENOENT)
347 return 0;
348
349 return log_error_errno(errno, "Failed to open '%s': %m", dir);
350 }
351
352 r = readdir_all(dir_fd, RECURSE_DIR_IGNORE_DOT, &dentries);
7e87c7d9 353 if (r < 0)
d04f0331
LP
354 return log_error_errno(r, "Failed to read directory '%s': %m", dir);
355
356 for (size_t i = 0; i < dentries->n_entries; i++) {
357 const struct dirent *de = dentries->entries[i];
358 _cleanup_fclose_ FILE *f = NULL;
359
360 if (!dirent_is_file(de))
361 continue;
362
363 if (!endswith_no_case(de->d_name, ".conf"))
364 continue;
7e87c7d9 365
d04f0331
LP
366 r = xfopenat(dir_fd, de->d_name, "re", 0, &f);
367 if (r < 0) {
368 log_warning_errno(r, "Failed to open %s/%s, ignoring: %m", dir, de->d_name);
369 continue;
370 }
371
d486a0ea
LP
372 r = config_check_inode_relevant_and_unseen(config, fileno(f), de->d_name);
373 if (r < 0)
374 return r;
375 if (r == 0) /* inode already seen or otherwise not relevant */
d04f0331 376 continue;
d486a0ea
LP
377
378 if (!GREEDY_REALLOC0(config->entries, config->n_entries + 1))
379 return log_oom();
d04f0331 380
85f4ae2f 381 r = boot_entry_load(f, root, dir, de->d_name, config->entries + config->n_entries);
7e87c7d9
ZJS
382 if (r < 0)
383 continue;
384
85f4ae2f 385 config->n_entries++;
7e87c7d9
ZJS
386 }
387
7e87c7d9
ZJS
388 return 0;
389}
390
5e146a75
LP
391static int boot_entry_load_unified(
392 const char *root,
393 const char *path,
394 const char *osrelease,
395 const char *cmdline,
396 BootEntry *ret) {
397
c2caeb5d
LP
398 _cleanup_free_ char *os_pretty_name = NULL, *os_image_id = NULL, *os_name = NULL, *os_id = NULL,
399 *os_image_version = NULL, *os_version = NULL, *os_version_id = NULL, *os_build_id = NULL;
93f14ce2
LP
400 _cleanup_(boot_entry_free) BootEntry tmp = {
401 .type = BOOT_ENTRY_UNIFIED,
402 };
20ec8f53 403 const char *k, *good_name, *good_version, *good_sort_key;
5e146a75 404 _cleanup_fclose_ FILE *f = NULL;
5e146a75
LP
405 int r;
406
407 assert(root);
408 assert(path);
409 assert(osrelease);
410
411 k = path_startswith(path, root);
412 if (!k)
413 return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Path is not below root: %s", path);
414
673a1e6f 415 f = fmemopen_unlocked((void*) osrelease, strlen(osrelease), "r");
5e146a75
LP
416 if (!f)
417 return log_error_errno(errno, "Failed to open os-release buffer: %m");
418
15b82eec
SM
419 r = parse_env_file(f, "os-release",
420 "PRETTY_NAME", &os_pretty_name,
c2caeb5d
LP
421 "IMAGE_ID", &os_image_id,
422 "NAME", &os_name,
15b82eec 423 "ID", &os_id,
c2caeb5d
LP
424 "IMAGE_VERSION", &os_image_version,
425 "VERSION", &os_version,
426 "VERSION_ID", &os_version_id,
427 "BUILD_ID", &os_build_id);
5e146a75
LP
428 if (r < 0)
429 return log_error_errno(r, "Failed to parse os-release data from unified kernel image %s: %m", path);
430
20ec8f53 431 if (!bootspec_pick_name_version_sort_key(
c2caeb5d
LP
432 os_pretty_name,
433 os_image_id,
434 os_name,
435 os_id,
436 os_image_version,
437 os_version,
438 os_version_id,
439 os_build_id,
440 &good_name,
20ec8f53
LP
441 &good_version,
442 &good_sort_key))
5e146a75
LP
443 return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Missing fields in os-release data from unified kernel image %s, refusing.", path);
444
c2caeb5d
LP
445 r = path_extract_filename(path, &tmp.id);
446 if (r < 0)
447 return log_error_errno(r, "Failed to extract file name from '%s': %m", path);
5e146a75 448
eed7210a 449 if (!efi_loader_entry_name_valid(tmp.id))
dfc22cb4 450 return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid loader entry name: %s", tmp.id);
eed7210a 451
c2caeb5d
LP
452 if (os_id && os_version_id) {
453 tmp.id_old = strjoin(os_id, "-", os_version_id);
454 if (!tmp.id_old)
455 return log_oom();
456 }
457
5e146a75
LP
458 tmp.path = strdup(path);
459 if (!tmp.path)
460 return log_oom();
461
462 tmp.root = strdup(root);
463 if (!tmp.root)
464 return log_oom();
465
466 tmp.kernel = strdup(skip_leading_chars(k, "/"));
467 if (!tmp.kernel)
468 return log_oom();
469
470 tmp.options = strv_new(skip_leading_chars(cmdline, WHITESPACE));
471 if (!tmp.options)
472 return log_oom();
473
474 delete_trailing_chars(tmp.options[0], WHITESPACE);
475
c2caeb5d
LP
476 tmp.title = strdup(good_name);
477 if (!tmp.title)
478 return log_oom();
479
20ec8f53
LP
480 tmp.sort_key = strdup(good_sort_key);
481 if (!tmp.sort_key)
482 return log_oom();
483
c2caeb5d
LP
484 tmp.version = strdup(good_version);
485 if (!tmp.version)
486 return log_oom();
5e146a75
LP
487
488 *ret = tmp;
489 tmp = (BootEntry) {};
490 return 0;
491}
492
493/* Maximum PE section we are willing to load (Note that sections we are not interested in may be larger, but
494 * the ones we do care about and we are willing to load into memory have this size limit.) */
495#define PE_SECTION_SIZE_MAX (4U*1024U*1024U)
496
497static int find_sections(
498 int fd,
499 char **ret_osrelease,
500 char **ret_cmdline) {
501
502 _cleanup_free_ struct PeSectionHeader *sections = NULL;
503 _cleanup_free_ char *osrelease = NULL, *cmdline = NULL;
5e146a75
LP
504 ssize_t n;
505
9817b7db 506 struct DosFileHeader dos;
5e146a75
LP
507 n = pread(fd, &dos, sizeof(dos), 0);
508 if (n < 0)
509 return log_error_errno(errno, "Failed read DOS header: %m");
510 if (n != sizeof(dos))
511 return log_error_errno(SYNTHETIC_ERRNO(EIO), "Short read while reading DOS header, refusing.");
512
513 if (dos.Magic[0] != 'M' || dos.Magic[1] != 'Z')
514 return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "DOS executable magic missing, refusing.");
515
9817b7db
ZJS
516 uint64_t start = unaligned_read_le32(&dos.ExeHeader);
517
518 struct PeHeader pe;
5e146a75
LP
519 n = pread(fd, &pe, sizeof(pe), start);
520 if (n < 0)
521 return log_error_errno(errno, "Failed to read PE header: %m");
522 if (n != sizeof(pe))
523 return log_error_errno(SYNTHETIC_ERRNO(EIO), "Short read while reading PE header, refusing.");
524
525 if (pe.Magic[0] != 'P' || pe.Magic[1] != 'E' || pe.Magic[2] != 0 || pe.Magic[3] != 0)
526 return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "PE executable magic missing, refusing.");
527
9817b7db 528 size_t n_sections = unaligned_read_le16(&pe.FileHeader.NumberOfSections);
5e146a75
LP
529 if (n_sections > 96)
530 return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "PE header has too many sections, refusing.");
531
532 sections = new(struct PeSectionHeader, n_sections);
533 if (!sections)
534 return log_oom();
535
536 n = pread(fd, sections,
537 n_sections * sizeof(struct PeSectionHeader),
538 start + sizeof(pe) + unaligned_read_le16(&pe.FileHeader.SizeOfOptionalHeader));
539 if (n < 0)
540 return log_error_errno(errno, "Failed to read section data: %m");
541 if ((size_t) n != n_sections * sizeof(struct PeSectionHeader))
542 return log_error_errno(SYNTHETIC_ERRNO(EIO), "Short read while reading sections, refusing.");
543
9817b7db 544 for (size_t i = 0; i < n_sections; i++) {
5e146a75
LP
545 _cleanup_free_ char *k = NULL;
546 uint32_t offset, size;
547 char **b;
548
549 if (strneq((char*) sections[i].Name, ".osrel", sizeof(sections[i].Name)))
550 b = &osrelease;
551 else if (strneq((char*) sections[i].Name, ".cmdline", sizeof(sections[i].Name)))
552 b = &cmdline;
553 else
554 continue;
555
556 if (*b)
557 return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Duplicate section %s, refusing.", sections[i].Name);
558
559 offset = unaligned_read_le32(&sections[i].PointerToRawData);
560 size = unaligned_read_le32(&sections[i].VirtualSize);
561
562 if (size > PE_SECTION_SIZE_MAX)
563 return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Section %s too large, refusing.", sections[i].Name);
564
565 k = new(char, size+1);
566 if (!k)
567 return log_oom();
568
569 n = pread(fd, k, size, offset);
570 if (n < 0)
571 return log_error_errno(errno, "Failed to read section payload: %m");
a75fcef8 572 if ((size_t) n != size)
5e146a75
LP
573 return log_error_errno(SYNTHETIC_ERRNO(EIO), "Short read while reading section payload, refusing:");
574
575 /* Allow one trailing NUL byte, but nothing more. */
576 if (size > 0 && memchr(k, 0, size - 1))
577 return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Section contains embedded NUL byte: %m");
578
579 k[size] = 0;
580 *b = TAKE_PTR(k);
581 }
582
583 if (!osrelease)
584 return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Image lacks .osrel section, refusing.");
585
586 if (ret_osrelease)
587 *ret_osrelease = TAKE_PTR(osrelease);
588 if (ret_cmdline)
589 *ret_cmdline = TAKE_PTR(cmdline);
590
591 return 0;
592}
593
594static int boot_entries_find_unified(
85f4ae2f 595 BootConfig *config,
5e146a75 596 const char *root,
85f4ae2f 597 const char *dir) {
5e146a75
LP
598
599 _cleanup_(closedirp) DIR *d = NULL;
5e146a75
LP
600 int r;
601
85f4ae2f 602 assert(config);
5e146a75 603 assert(dir);
5e146a75
LP
604
605 d = opendir(dir);
606 if (!d) {
607 if (errno == ENOENT)
608 return 0;
609
610 return log_error_errno(errno, "Failed to open %s: %m", dir);
611 }
612
613 FOREACH_DIRENT(de, d, return log_error_errno(errno, "Failed to read %s: %m", dir)) {
614 _cleanup_free_ char *j = NULL, *osrelease = NULL, *cmdline = NULL;
615 _cleanup_close_ int fd = -1;
616
617 if (!dirent_is_file(de))
618 continue;
619
620 if (!endswith_no_case(de->d_name, ".efi"))
621 continue;
622
85f4ae2f 623 if (!GREEDY_REALLOC0(config->entries, config->n_entries + 1))
5e146a75
LP
624 return log_oom();
625
626 fd = openat(dirfd(d), de->d_name, O_RDONLY|O_CLOEXEC|O_NONBLOCK);
627 if (fd < 0) {
628 log_warning_errno(errno, "Failed to open %s/%s, ignoring: %m", dir, de->d_name);
629 continue;
630 }
631
d486a0ea
LP
632 r = config_check_inode_relevant_and_unseen(config, fd, de->d_name);
633 if (r < 0)
634 return r;
635 if (r == 0) /* inode already seen or otherwise not relevant */
5e146a75 636 continue;
5e146a75 637
d486a0ea 638 if (find_sections(fd, &osrelease, &cmdline) < 0)
5e146a75
LP
639 continue;
640
641 j = path_join(dir, de->d_name);
642 if (!j)
643 return log_oom();
644
85f4ae2f 645 r = boot_entry_load_unified(root, j, osrelease, cmdline, config->entries + config->n_entries);
5e146a75
LP
646 if (r < 0)
647 continue;
648
85f4ae2f 649 config->n_entries++;
5e146a75
LP
650 }
651
5e146a75
LP
652 return 0;
653}
654
bb682057 655static bool find_nonunique(const BootEntry *entries, size_t n_entries, bool arr[]) {
64f05708
ZJS
656 bool non_unique = false;
657
4fe2ba0e
LP
658 assert(entries || n_entries == 0);
659 assert(arr || n_entries == 0);
660
d5ac1d4e 661 for (size_t i = 0; i < n_entries; i++)
64f05708
ZJS
662 arr[i] = false;
663
d5ac1d4e
LP
664 for (size_t i = 0; i < n_entries; i++)
665 for (size_t j = 0; j < n_entries; j++)
64f05708
ZJS
666 if (i != j && streq(boot_entry_title(entries + i),
667 boot_entry_title(entries + j)))
668 non_unique = arr[i] = arr[j] = true;
669
670 return non_unique;
671}
672
673static int boot_entries_uniquify(BootEntry *entries, size_t n_entries) {
d5ac1d4e 674 _cleanup_free_ bool *arr = NULL;
64f05708 675 char *s;
64f05708 676
4fe2ba0e
LP
677 assert(entries || n_entries == 0);
678
d5ac1d4e
LP
679 if (n_entries == 0)
680 return 0;
681
682 arr = new(bool, n_entries);
683 if (!arr)
684 return -ENOMEM;
685
64f05708
ZJS
686 /* Find _all_ non-unique titles */
687 if (!find_nonunique(entries, n_entries, arr))
688 return 0;
689
690 /* Add version to non-unique titles */
d5ac1d4e 691 for (size_t i = 0; i < n_entries; i++)
64f05708 692 if (arr[i] && entries[i].version) {
d5ac1d4e 693 if (asprintf(&s, "%s (%s)", boot_entry_title(entries + i), entries[i].version) < 0)
64f05708
ZJS
694 return -ENOMEM;
695
696 free_and_replace(entries[i].show_title, s);
697 }
698
699 if (!find_nonunique(entries, n_entries, arr))
700 return 0;
701
702 /* Add machine-id to non-unique titles */
d5ac1d4e 703 for (size_t i = 0; i < n_entries; i++)
64f05708 704 if (arr[i] && entries[i].machine_id) {
d5ac1d4e 705 if (asprintf(&s, "%s (%s)", boot_entry_title(entries + i), entries[i].machine_id) < 0)
64f05708
ZJS
706 return -ENOMEM;
707
708 free_and_replace(entries[i].show_title, s);
709 }
710
711 if (!find_nonunique(entries, n_entries, arr))
712 return 0;
713
714 /* Add file name to non-unique titles */
d5ac1d4e 715 for (size_t i = 0; i < n_entries; i++)
64f05708 716 if (arr[i]) {
d5ac1d4e 717 if (asprintf(&s, "%s (%s)", boot_entry_title(entries + i), entries[i].id) < 0)
64f05708
ZJS
718 return -ENOMEM;
719
720 free_and_replace(entries[i].show_title, s);
721 }
722
723 return 0;
724}
725
20ec8f53
LP
726static int boot_config_find(const BootConfig *config, const char *id) {
727 assert(config);
728
729 if (!id)
730 return -1;
731
732 for (size_t i = 0; i < config->n_entries; i++)
733 if (fnmatch(id, config->entries[i].id, FNM_CASEFOLD) == 0)
734 return i;
735
736 return -1;
737}
738
ad1afd60 739static int boot_entries_select_default(const BootConfig *config) {
7e87c7d9
ZJS
740 int i;
741
4fe2ba0e 742 assert(config);
388d2993
ZJS
743 assert(config->entries || config->n_entries == 0);
744
745 if (config->n_entries == 0) {
746 log_debug("Found no default boot entry :(");
747 return -1; /* -1 means "no default" */
748 }
4fe2ba0e 749
20ec8f53
LP
750 if (config->entry_oneshot) {
751 i = boot_config_find(config, config->entry_oneshot);
752 if (i >= 0) {
753 log_debug("Found default: id \"%s\" is matched by LoaderEntryOneShot",
754 config->entries[i].id);
755 return i;
756 }
757 }
758
759 if (config->entry_default) {
760 i = boot_config_find(config, config->entry_default);
761 if (i >= 0) {
762 log_debug("Found default: id \"%s\" is matched by LoaderEntryDefault",
763 config->entries[i].id);
764 return i;
765 }
766 }
767
768 if (config->default_pattern) {
769 i = boot_config_find(config, config->default_pattern);
770 if (i >= 0) {
771 log_debug("Found default: id \"%s\" is matched by pattern \"%s\"",
772 config->entries[i].id, config->default_pattern);
773 return i;
774 }
775 }
776
777 log_debug("Found default: first entry \"%s\"", config->entries[0].id);
778 return 0;
7e87c7d9
ZJS
779}
780
a78e472d 781static int boot_entries_select_selected(const BootConfig *config) {
a78e472d
LP
782 assert(config);
783 assert(config->entries || config->n_entries == 0);
784
785 if (!config->entry_selected || config->n_entries == 0)
786 return -1;
787
20ec8f53 788 return boot_config_find(config, config->entry_selected);
a78e472d
LP
789}
790
791static int boot_load_efi_entry_pointers(BootConfig *config) {
792 int r;
793
794 assert(config);
795
796 if (!is_efi_boot())
797 return 0;
798
799 /* Loads the three "pointers" to boot loader entries from their EFI variables */
800
801 r = efi_get_variable_string(EFI_LOADER_VARIABLE(LoaderEntryOneShot), &config->entry_oneshot);
802 if (r < 0 && !IN_SET(r, -ENOENT, -ENODATA)) {
803 log_warning_errno(r, "Failed to read EFI variable \"LoaderEntryOneShot\": %m");
804 if (r == -ENOMEM)
805 return r;
806 }
807
808 r = efi_get_variable_string(EFI_LOADER_VARIABLE(LoaderEntryDefault), &config->entry_default);
809 if (r < 0 && !IN_SET(r, -ENOENT, -ENODATA)) {
810 log_warning_errno(r, "Failed to read EFI variable \"LoaderEntryDefault\": %m");
811 if (r == -ENOMEM)
812 return r;
813 }
814
815 r = efi_get_variable_string(EFI_LOADER_VARIABLE(LoaderEntrySelected), &config->entry_selected);
816 if (r < 0 && !IN_SET(r, -ENOENT, -ENODATA)) {
817 log_warning_errno(r, "Failed to read EFI variable \"LoaderEntrySelected\": %m");
818 if (r == -ENOMEM)
819 return r;
820 }
821
822 return 1;
823}
824
a2f8664e
LP
825int boot_entries_load_config(
826 const char *esp_path,
827 const char *xbootldr_path,
828 BootConfig *config) {
829
7e87c7d9
ZJS
830 const char *p;
831 int r;
832
4fe2ba0e
LP
833 assert(config);
834
a2f8664e
LP
835 if (esp_path) {
836 p = strjoina(esp_path, "/loader/loader.conf");
837 r = boot_loader_read_conf(p, config);
838 if (r < 0)
839 return r;
7e87c7d9 840
a2f8664e 841 p = strjoina(esp_path, "/loader/entries");
85f4ae2f 842 r = boot_entries_find(config, esp_path, p);
a2f8664e
LP
843 if (r < 0)
844 return r;
5e146a75
LP
845
846 p = strjoina(esp_path, "/EFI/Linux/");
85f4ae2f 847 r = boot_entries_find_unified(config, esp_path, p);
5e146a75
LP
848 if (r < 0)
849 return r;
a2f8664e
LP
850 }
851
852 if (xbootldr_path) {
853 p = strjoina(xbootldr_path, "/loader/entries");
85f4ae2f 854 r = boot_entries_find(config, xbootldr_path, p);
a2f8664e
LP
855 if (r < 0)
856 return r;
5e146a75
LP
857
858 p = strjoina(xbootldr_path, "/EFI/Linux/");
85f4ae2f 859 r = boot_entries_find_unified(config, xbootldr_path, p);
5e146a75
LP
860 if (r < 0)
861 return r;
a2f8664e 862 }
7e87c7d9 863
dd2bf34c
LP
864 typesafe_qsort(config->entries, config->n_entries, boot_entry_compare);
865
64f05708
ZJS
866 r = boot_entries_uniquify(config->entries, config->n_entries);
867 if (r < 0)
868 return log_error_errno(r, "Failed to uniquify boot entries: %m");
869
a78e472d
LP
870 r = boot_load_efi_entry_pointers(config);
871 if (r < 0)
872 return r;
7e87c7d9
ZJS
873
874 config->default_entry = boot_entries_select_default(config);
a78e472d
LP
875 config->selected_entry = boot_entries_select_selected(config);
876
7e87c7d9
ZJS
877 return 0;
878}
af918182 879
eea4ce1e
LP
880int boot_entries_load_config_auto(
881 const char *override_esp_path,
882 const char *override_xbootldr_path,
883 BootConfig *config) {
884
885 _cleanup_free_ char *esp_where = NULL, *xbootldr_where = NULL;
f63b5ad9 886 dev_t esp_devid = 0, xbootldr_devid = 0;
eea4ce1e
LP
887 int r;
888
889 assert(config);
890
891 /* This function is similar to boot_entries_load_config(), however we automatically search for the
892 * ESP and the XBOOTLDR partition unless it is explicitly specified. Also, if the user did not pass
893 * an ESP or XBOOTLDR path directly, let's see if /run/boot-loader-entries/ exists. If so, let's
894 * read data from there, as if it was an ESP (i.e. loading both entries and loader.conf data from
895 * it). This allows other boot loaders to pass boot loader entry information to our tools if they
896 * want to. */
897
898 if (!override_esp_path && !override_xbootldr_path) {
f40999f8
ZJS
899 if (access("/run/boot-loader-entries/", F_OK) >= 0)
900 return boot_entries_load_config("/run/boot-loader-entries/", NULL, config);
eea4ce1e 901
f40999f8
ZJS
902 if (errno != ENOENT)
903 return log_error_errno(errno,
904 "Failed to determine whether /run/boot-loader-entries/ exists: %m");
eea4ce1e
LP
905 }
906
f63b5ad9 907 r = find_esp_and_warn(override_esp_path, /* unprivileged_mode= */ false, &esp_where, NULL, NULL, NULL, NULL, &esp_devid);
cc5957dc 908 if (r < 0) /* we don't log about ENOKEY here, but propagate it, leaving it to the caller to log */
eea4ce1e
LP
909 return r;
910
f63b5ad9 911 r = find_xbootldr_and_warn(override_xbootldr_path, /* unprivileged_mode= */ false, &xbootldr_where, NULL, &xbootldr_devid);
eea4ce1e
LP
912 if (r < 0 && r != -ENOKEY)
913 return r; /* It's fine if the XBOOTLDR partition doesn't exist, hence we ignore ENOKEY here */
914
f63b5ad9
LP
915 /* If both paths actually refer to the same inode, suppress the xbootldr path */
916 if (esp_where && xbootldr_where && devid_set_and_equal(esp_devid, xbootldr_devid))
917 xbootldr_where = mfree(xbootldr_where);
918
f40999f8 919 return boot_entries_load_config(esp_where, xbootldr_where, config);
eea4ce1e
LP
920}
921
d4bd786d
LP
922int boot_entries_augment_from_loader(
923 BootConfig *config,
9951736b
LP
924 char **found_by_loader,
925 bool only_auto) {
d4bd786d
LP
926
927 static const char *const title_table[] = {
93f14ce2
LP
928 /* Pretty names for a few well-known automatically discovered entries. */
929 "auto-osx", "macOS",
930 "auto-windows", "Windows Boot Manager",
931 "auto-efi-shell", "EFI Shell",
932 "auto-efi-default", "EFI Default Loader",
933 "auto-reboot-to-firmware-setup", "Reboot Into Firmware Interface",
934 };
935
93f14ce2
LP
936 assert(config);
937
938 /* Let's add the entries discovered by the boot loader to the end of our list, unless they are
939 * already included there. */
940
93f14ce2 941 STRV_FOREACH(i, found_by_loader) {
bb682057 942 BootEntry *existing;
ce4c4f81 943 _cleanup_free_ char *c = NULL, *t = NULL, *p = NULL;
93f14ce2 944
bb682057
LP
945 existing = boot_config_find_entry(config, *i);
946 if (existing) {
947 existing->reported_by_loader = true;
93f14ce2 948 continue;
bb682057 949 }
93f14ce2 950
9951736b 951 if (only_auto && !startswith(*i, "auto-"))
93f14ce2
LP
952 continue;
953
954 c = strdup(*i);
955 if (!c)
956 return log_oom();
957
958 STRV_FOREACH_PAIR(a, b, (char**) title_table)
959 if (streq(*a, *i)) {
960 t = strdup(*b);
961 if (!t)
962 return log_oom();
963 break;
964 }
965
e6f055cb 966 p = strdup(EFIVAR_PATH(EFI_LOADER_VARIABLE(LoaderEntries)));
ce4c4f81
ZJS
967 if (!p)
968 return log_oom();
969
319a4f4b 970 if (!GREEDY_REALLOC0(config->entries, config->n_entries + 1))
93f14ce2
LP
971 return log_oom();
972
973 config->entries[config->n_entries++] = (BootEntry) {
bb682057 974 .type = startswith(*i, "auto-") ? BOOT_ENTRY_LOADER_AUTO : BOOT_ENTRY_LOADER,
93f14ce2
LP
975 .id = TAKE_PTR(c),
976 .title = TAKE_PTR(t),
ce4c4f81 977 .path = TAKE_PTR(p),
bb682057 978 .reported_by_loader = true,
93f14ce2
LP
979 };
980 }
981
982 return 0;
983}