]>
Commit | Line | Data |
---|---|---|
b30be4dc RH |
1 | // SPDX-License-Identifier: GPL-2.0-only |
2 | /* | |
3 | * Copyright (C) 2020 Arm Limited | |
4 | * | |
5 | * Based on arch/arm64/kernel/machine_kexec_file.c: | |
6 | * Copyright (C) 2018 Linaro Limited | |
7 | * | |
8 | * And arch/powerpc/kexec/file_load.c: | |
9 | * Copyright (C) 2016 IBM Corporation | |
10 | */ | |
11 | ||
12 | #include <linux/kernel.h> | |
13 | #include <linux/kexec.h> | |
fee3ff99 | 14 | #include <linux/memblock.h> |
b30be4dc RH |
15 | #include <linux/libfdt.h> |
16 | #include <linux/of.h> | |
17 | #include <linux/of_fdt.h> | |
18 | #include <linux/random.h> | |
8587ca6f | 19 | #include <linux/slab.h> |
b30be4dc RH |
20 | #include <linux/types.h> |
21 | ||
b30be4dc RH |
22 | #define RNG_SEED_SIZE 128 |
23 | ||
24 | /* | |
25 | * Additional space needed for the FDT buffer so that we can add initrd, | |
26 | * bootargs, kaslr-seed, rng-seed, useable-memory-range and elfcorehdr. | |
27 | */ | |
28 | #define FDT_EXTRA_SPACE 0x1000 | |
29 | ||
30 | /** | |
31 | * fdt_find_and_del_mem_rsv - delete memory reservation with given address and size | |
32 | * | |
33 | * @fdt: Flattened device tree for the current kernel. | |
34 | * @start: Starting address of the reserved memory. | |
35 | * @size: Size of the reserved memory. | |
36 | * | |
37 | * Return: 0 on success, or negative errno on error. | |
38 | */ | |
39 | static int fdt_find_and_del_mem_rsv(void *fdt, unsigned long start, unsigned long size) | |
40 | { | |
41 | int i, ret, num_rsvs = fdt_num_mem_rsv(fdt); | |
42 | ||
43 | for (i = 0; i < num_rsvs; i++) { | |
44 | u64 rsv_start, rsv_size; | |
45 | ||
46 | ret = fdt_get_mem_rsv(fdt, i, &rsv_start, &rsv_size); | |
47 | if (ret) { | |
48 | pr_err("Malformed device tree.\n"); | |
49 | return -EINVAL; | |
50 | } | |
51 | ||
52 | if (rsv_start == start && rsv_size == size) { | |
53 | ret = fdt_del_mem_rsv(fdt, i); | |
54 | if (ret) { | |
55 | pr_err("Error deleting device tree reservation.\n"); | |
56 | return -EINVAL; | |
57 | } | |
58 | ||
59 | return 0; | |
60 | } | |
61 | } | |
62 | ||
63 | return -ENOENT; | |
64 | } | |
65 | ||
fee3ff99 LR |
66 | /** |
67 | * get_addr_size_cells - Get address and size of root node | |
68 | * | |
69 | * @addr_cells: Return address of the root node | |
70 | * @size_cells: Return size of the root node | |
71 | * | |
72 | * Return: 0 on success, or negative errno on error. | |
73 | */ | |
74 | static int get_addr_size_cells(int *addr_cells, int *size_cells) | |
75 | { | |
76 | struct device_node *root; | |
77 | ||
78 | root = of_find_node_by_path("/"); | |
79 | if (!root) | |
80 | return -EINVAL; | |
81 | ||
82 | *addr_cells = of_n_addr_cells(root); | |
83 | *size_cells = of_n_size_cells(root); | |
84 | ||
85 | of_node_put(root); | |
86 | ||
87 | return 0; | |
88 | } | |
89 | ||
90 | /** | |
91 | * do_get_kexec_buffer - Get address and size of device tree property | |
92 | * | |
93 | * @prop: Device tree property | |
94 | * @len: Size of @prop | |
95 | * @addr: Return address of the node | |
96 | * @size: Return size of the node | |
97 | * | |
98 | * Return: 0 on success, or negative errno on error. | |
99 | */ | |
100 | static int do_get_kexec_buffer(const void *prop, int len, unsigned long *addr, | |
101 | size_t *size) | |
102 | { | |
103 | int ret, addr_cells, size_cells; | |
104 | ||
105 | ret = get_addr_size_cells(&addr_cells, &size_cells); | |
106 | if (ret) | |
107 | return ret; | |
108 | ||
109 | if (len < 4 * (addr_cells + size_cells)) | |
110 | return -ENOENT; | |
111 | ||
112 | *addr = of_read_number(prop, addr_cells); | |
113 | *size = of_read_number(prop + 4 * addr_cells, size_cells); | |
114 | ||
115 | return 0; | |
116 | } | |
117 | ||
118 | /** | |
119 | * ima_get_kexec_buffer - get IMA buffer from the previous kernel | |
120 | * @addr: On successful return, set to point to the buffer contents. | |
121 | * @size: On successful return, set to the buffer size. | |
122 | * | |
123 | * Return: 0 on success, negative errno on error. | |
124 | */ | |
125 | int ima_get_kexec_buffer(void **addr, size_t *size) | |
126 | { | |
127 | int ret, len; | |
128 | unsigned long tmp_addr; | |
129 | size_t tmp_size; | |
130 | const void *prop; | |
131 | ||
132 | if (!IS_ENABLED(CONFIG_HAVE_IMA_KEXEC)) | |
133 | return -ENOTSUPP; | |
134 | ||
135 | prop = of_get_property(of_chosen, "linux,ima-kexec-buffer", &len); | |
136 | if (!prop) | |
137 | return -ENOENT; | |
138 | ||
139 | ret = do_get_kexec_buffer(prop, len, &tmp_addr, &tmp_size); | |
140 | if (ret) | |
141 | return ret; | |
142 | ||
143 | *addr = __va(tmp_addr); | |
144 | *size = tmp_size; | |
145 | ||
146 | return 0; | |
147 | } | |
148 | ||
149 | /** | |
150 | * ima_free_kexec_buffer - free memory used by the IMA buffer | |
151 | */ | |
152 | int ima_free_kexec_buffer(void) | |
153 | { | |
154 | int ret; | |
155 | unsigned long addr; | |
156 | size_t size; | |
157 | struct property *prop; | |
158 | ||
159 | if (!IS_ENABLED(CONFIG_HAVE_IMA_KEXEC)) | |
160 | return -ENOTSUPP; | |
161 | ||
162 | prop = of_find_property(of_chosen, "linux,ima-kexec-buffer", NULL); | |
163 | if (!prop) | |
164 | return -ENOENT; | |
165 | ||
166 | ret = do_get_kexec_buffer(prop->value, prop->length, &addr, &size); | |
167 | if (ret) | |
168 | return ret; | |
169 | ||
170 | ret = of_remove_property(of_chosen, prop); | |
171 | if (ret) | |
172 | return ret; | |
173 | ||
3ecc6834 | 174 | return memblock_phys_free(addr, size); |
fee3ff99 LR |
175 | } |
176 | ||
177 | /** | |
178 | * remove_ima_buffer - remove the IMA buffer property and reservation from @fdt | |
179 | * | |
180 | * @fdt: Flattened Device Tree to update | |
181 | * @chosen_node: Offset to the chosen node in the device tree | |
182 | * | |
183 | * The IMA measurement buffer is of no use to a subsequent kernel, so we always | |
184 | * remove it from the device tree. | |
185 | */ | |
186 | static void remove_ima_buffer(void *fdt, int chosen_node) | |
187 | { | |
188 | int ret, len; | |
189 | unsigned long addr; | |
190 | size_t size; | |
191 | const void *prop; | |
192 | ||
193 | if (!IS_ENABLED(CONFIG_HAVE_IMA_KEXEC)) | |
194 | return; | |
195 | ||
196 | prop = fdt_getprop(fdt, chosen_node, "linux,ima-kexec-buffer", &len); | |
197 | if (!prop) | |
198 | return; | |
199 | ||
200 | ret = do_get_kexec_buffer(prop, len, &addr, &size); | |
201 | fdt_delprop(fdt, chosen_node, "linux,ima-kexec-buffer"); | |
202 | if (ret) | |
203 | return; | |
204 | ||
205 | ret = fdt_find_and_del_mem_rsv(fdt, addr, size); | |
206 | if (!ret) | |
207 | pr_debug("Removed old IMA buffer reservation.\n"); | |
208 | } | |
209 | ||
210 | #ifdef CONFIG_IMA_KEXEC | |
fee3ff99 LR |
211 | /** |
212 | * setup_ima_buffer - add IMA buffer information to the fdt | |
213 | * @image: kexec image being loaded. | |
214 | * @fdt: Flattened device tree for the next kernel. | |
215 | * @chosen_node: Offset to the chosen node. | |
216 | * | |
217 | * Return: 0 on success, or negative errno on error. | |
218 | */ | |
219 | static int setup_ima_buffer(const struct kimage *image, void *fdt, | |
220 | int chosen_node) | |
221 | { | |
28db15d4 | 222 | int ret; |
fee3ff99 LR |
223 | |
224 | if (!image->ima_buffer_size) | |
225 | return 0; | |
226 | ||
28db15d4 LR |
227 | ret = fdt_appendprop_addrrange(fdt, 0, chosen_node, |
228 | "linux,ima-kexec-buffer", | |
229 | image->ima_buffer_addr, | |
230 | image->ima_buffer_size); | |
fee3ff99 LR |
231 | if (ret < 0) |
232 | return -EINVAL; | |
233 | ||
234 | ret = fdt_add_mem_rsv(fdt, image->ima_buffer_addr, | |
235 | image->ima_buffer_size); | |
236 | if (ret) | |
237 | return -EINVAL; | |
238 | ||
239 | pr_debug("IMA buffer at 0x%llx, size = 0x%zx\n", | |
240 | image->ima_buffer_addr, image->ima_buffer_size); | |
241 | ||
242 | return 0; | |
243 | } | |
244 | #else /* CONFIG_IMA_KEXEC */ | |
245 | static inline int setup_ima_buffer(const struct kimage *image, void *fdt, | |
246 | int chosen_node) | |
247 | { | |
248 | return 0; | |
249 | } | |
250 | #endif /* CONFIG_IMA_KEXEC */ | |
251 | ||
b30be4dc RH |
252 | /* |
253 | * of_kexec_alloc_and_setup_fdt - Alloc and setup a new Flattened Device Tree | |
254 | * | |
255 | * @image: kexec image being loaded. | |
256 | * @initrd_load_addr: Address where the next initrd will be loaded. | |
257 | * @initrd_len: Size of the next initrd, or 0 if there will be none. | |
258 | * @cmdline: Command line for the next kernel, or NULL if there will | |
259 | * be none. | |
260 | * @extra_fdt_size: Additional size for the new FDT buffer. | |
261 | * | |
262 | * Return: fdt on success, or NULL errno on error. | |
263 | */ | |
264 | void *of_kexec_alloc_and_setup_fdt(const struct kimage *image, | |
265 | unsigned long initrd_load_addr, | |
266 | unsigned long initrd_len, | |
267 | const char *cmdline, size_t extra_fdt_size) | |
268 | { | |
269 | void *fdt; | |
270 | int ret, chosen_node; | |
271 | const void *prop; | |
272 | size_t fdt_size; | |
273 | ||
274 | fdt_size = fdt_totalsize(initial_boot_params) + | |
275 | (cmdline ? strlen(cmdline) : 0) + | |
276 | FDT_EXTRA_SPACE + | |
277 | extra_fdt_size; | |
278 | fdt = kvmalloc(fdt_size, GFP_KERNEL); | |
279 | if (!fdt) | |
280 | return NULL; | |
281 | ||
282 | ret = fdt_open_into(initial_boot_params, fdt, fdt_size); | |
283 | if (ret < 0) { | |
284 | pr_err("Error %d setting up the new device tree.\n", ret); | |
285 | goto out; | |
286 | } | |
287 | ||
288 | /* Remove memory reservation for the current device tree. */ | |
289 | ret = fdt_find_and_del_mem_rsv(fdt, __pa(initial_boot_params), | |
290 | fdt_totalsize(initial_boot_params)); | |
291 | if (ret == -EINVAL) { | |
292 | pr_err("Error removing memory reservation.\n"); | |
293 | goto out; | |
294 | } | |
295 | ||
296 | chosen_node = fdt_path_offset(fdt, "/chosen"); | |
297 | if (chosen_node == -FDT_ERR_NOTFOUND) | |
298 | chosen_node = fdt_add_subnode(fdt, fdt_path_offset(fdt, "/"), | |
299 | "chosen"); | |
300 | if (chosen_node < 0) { | |
301 | ret = chosen_node; | |
302 | goto out; | |
303 | } | |
304 | ||
cc6ef3d1 | 305 | ret = fdt_delprop(fdt, chosen_node, "linux,elfcorehdr"); |
b30be4dc RH |
306 | if (ret && ret != -FDT_ERR_NOTFOUND) |
307 | goto out; | |
cc6ef3d1 | 308 | ret = fdt_delprop(fdt, chosen_node, "linux,usable-memory-range"); |
b30be4dc RH |
309 | if (ret && ret != -FDT_ERR_NOTFOUND) |
310 | goto out; | |
311 | ||
312 | /* Did we boot using an initrd? */ | |
313 | prop = fdt_getprop(fdt, chosen_node, "linux,initrd-start", NULL); | |
314 | if (prop) { | |
315 | u64 tmp_start, tmp_end, tmp_size; | |
316 | ||
317 | tmp_start = fdt64_to_cpu(*((const fdt64_t *) prop)); | |
318 | ||
319 | prop = fdt_getprop(fdt, chosen_node, "linux,initrd-end", NULL); | |
320 | if (!prop) { | |
321 | ret = -EINVAL; | |
322 | goto out; | |
323 | } | |
324 | ||
325 | tmp_end = fdt64_to_cpu(*((const fdt64_t *) prop)); | |
326 | ||
327 | /* | |
328 | * kexec reserves exact initrd size, while firmware may | |
329 | * reserve a multiple of PAGE_SIZE, so check for both. | |
330 | */ | |
331 | tmp_size = tmp_end - tmp_start; | |
332 | ret = fdt_find_and_del_mem_rsv(fdt, tmp_start, tmp_size); | |
333 | if (ret == -ENOENT) | |
334 | ret = fdt_find_and_del_mem_rsv(fdt, tmp_start, | |
335 | round_up(tmp_size, PAGE_SIZE)); | |
336 | if (ret == -EINVAL) | |
337 | goto out; | |
338 | } | |
339 | ||
340 | /* add initrd-* */ | |
341 | if (initrd_load_addr) { | |
cc6ef3d1 | 342 | ret = fdt_setprop_u64(fdt, chosen_node, "linux,initrd-start", |
b30be4dc RH |
343 | initrd_load_addr); |
344 | if (ret) | |
345 | goto out; | |
346 | ||
cc6ef3d1 | 347 | ret = fdt_setprop_u64(fdt, chosen_node, "linux,initrd-end", |
b30be4dc RH |
348 | initrd_load_addr + initrd_len); |
349 | if (ret) | |
350 | goto out; | |
351 | ||
352 | ret = fdt_add_mem_rsv(fdt, initrd_load_addr, initrd_len); | |
353 | if (ret) | |
354 | goto out; | |
355 | ||
356 | } else { | |
cc6ef3d1 | 357 | ret = fdt_delprop(fdt, chosen_node, "linux,initrd-start"); |
b30be4dc RH |
358 | if (ret && (ret != -FDT_ERR_NOTFOUND)) |
359 | goto out; | |
360 | ||
cc6ef3d1 | 361 | ret = fdt_delprop(fdt, chosen_node, "linux,initrd-end"); |
b30be4dc RH |
362 | if (ret && (ret != -FDT_ERR_NOTFOUND)) |
363 | goto out; | |
364 | } | |
365 | ||
366 | if (image->type == KEXEC_TYPE_CRASH) { | |
367 | /* add linux,elfcorehdr */ | |
368 | ret = fdt_appendprop_addrrange(fdt, 0, chosen_node, | |
cc6ef3d1 | 369 | "linux,elfcorehdr", image->elf_load_addr, |
b30be4dc RH |
370 | image->elf_headers_sz); |
371 | if (ret) | |
372 | goto out; | |
373 | ||
374 | /* | |
375 | * Avoid elfcorehdr from being stomped on in kdump kernel by | |
376 | * setting up memory reserve map. | |
377 | */ | |
378 | ret = fdt_add_mem_rsv(fdt, image->elf_load_addr, | |
379 | image->elf_headers_sz); | |
380 | if (ret) | |
381 | goto out; | |
382 | ||
383 | /* add linux,usable-memory-range */ | |
384 | ret = fdt_appendprop_addrrange(fdt, 0, chosen_node, | |
cc6ef3d1 | 385 | "linux,usable-memory-range", crashk_res.start, |
b30be4dc RH |
386 | crashk_res.end - crashk_res.start + 1); |
387 | if (ret) | |
388 | goto out; | |
389 | } | |
390 | ||
391 | /* add bootargs */ | |
392 | if (cmdline) { | |
cc6ef3d1 | 393 | ret = fdt_setprop_string(fdt, chosen_node, "bootargs", cmdline); |
b30be4dc RH |
394 | if (ret) |
395 | goto out; | |
396 | } else { | |
cc6ef3d1 | 397 | ret = fdt_delprop(fdt, chosen_node, "bootargs"); |
b30be4dc RH |
398 | if (ret && (ret != -FDT_ERR_NOTFOUND)) |
399 | goto out; | |
400 | } | |
401 | ||
402 | /* add kaslr-seed */ | |
cc6ef3d1 | 403 | ret = fdt_delprop(fdt, chosen_node, "kaslr-seed"); |
b30be4dc RH |
404 | if (ret == -FDT_ERR_NOTFOUND) |
405 | ret = 0; | |
406 | else if (ret) | |
407 | goto out; | |
408 | ||
409 | if (rng_is_initialized()) { | |
410 | u64 seed = get_random_u64(); | |
411 | ||
cc6ef3d1 | 412 | ret = fdt_setprop_u64(fdt, chosen_node, "kaslr-seed", seed); |
b30be4dc RH |
413 | if (ret) |
414 | goto out; | |
415 | } else { | |
416 | pr_notice("RNG is not initialised: omitting \"%s\" property\n", | |
cc6ef3d1 | 417 | "kaslr-seed"); |
b30be4dc RH |
418 | } |
419 | ||
420 | /* add rng-seed */ | |
421 | if (rng_is_initialized()) { | |
422 | void *rng_seed; | |
423 | ||
cc6ef3d1 | 424 | ret = fdt_setprop_placeholder(fdt, chosen_node, "rng-seed", |
b30be4dc RH |
425 | RNG_SEED_SIZE, &rng_seed); |
426 | if (ret) | |
427 | goto out; | |
428 | get_random_bytes(rng_seed, RNG_SEED_SIZE); | |
429 | } else { | |
430 | pr_notice("RNG is not initialised: omitting \"%s\" property\n", | |
cc6ef3d1 | 431 | "rng-seed"); |
b30be4dc RH |
432 | } |
433 | ||
434 | ret = fdt_setprop(fdt, chosen_node, "linux,booted-from-kexec", NULL, 0); | |
fee3ff99 LR |
435 | if (ret) |
436 | goto out; | |
437 | ||
438 | remove_ima_buffer(fdt, chosen_node); | |
439 | ret = setup_ima_buffer(image, fdt, fdt_path_offset(fdt, "/chosen")); | |
b30be4dc RH |
440 | |
441 | out: | |
442 | if (ret) { | |
443 | kvfree(fdt); | |
444 | fdt = NULL; | |
445 | } | |
446 | ||
447 | return fdt; | |
448 | } |