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