]>
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" |
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 | 22 | static 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 | 43 | static 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 | ||
158 | void 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 | 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; | |
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 | 277 | static 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 |
337 | static 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 | ||
443 | static 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(§ions[i].PointerToRawData); | |
506 | size = unaligned_read_le32(§ions[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 | ||
540 | static 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 | 602 | static 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 | ||
620 | static 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 |
673 | static 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 | 686 | static 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 | 728 | static 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 | ||
738 | static 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 |
772 | int 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 |
827 | int 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 |
869 | int 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 | } |