]>
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); | |
d486a0ea LP |
177 | |
178 | set_free(config->inodes_seen); | |
7e87c7d9 ZJS |
179 | } |
180 | ||
0de2e1fd | 181 | static 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 | 252 | static 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 |
279 | static 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 | ||
284 | static 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 | ||
294 | DEFINE_HASH_OPS_WITH_KEY_DESTRUCTOR(inode_hash_ops, struct stat, inode_hash_func, inode_compare_func, free); | |
295 | ||
296 | static 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 | 331 | static 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 |
391 | static 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 | ||
497 | static 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(§ions[i].PointerToRawData); | |
560 | size = unaligned_read_le32(§ions[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 | ||
594 | static 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 | 655 | static 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 | ||
673 | static 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 |
726 | static 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 | 739 | static 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 | 781 | static 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 | ||
791 | static 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 |
825 | int 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 |
880 | int 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 |
922 | int 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 | } |