diff options
Diffstat (limited to '')
-rw-r--r-- | arch/mips/kernel/relocate.c | 466 |
1 files changed, 466 insertions, 0 deletions
diff --git a/arch/mips/kernel/relocate.c b/arch/mips/kernel/relocate.c new file mode 100644 index 000000000..58fc8d089 --- /dev/null +++ b/arch/mips/kernel/relocate.c @@ -0,0 +1,466 @@ +/* + * This file is subject to the terms and conditions of the GNU General Public + * License. See the file "COPYING" in the main directory of this archive + * for more details. + * + * Support for Kernel relocation at boot time + * + * Copyright (C) 2015, Imagination Technologies Ltd. + * Authors: Matt Redfearn (matt.redfearn@mips.com) + */ +#include <asm/bootinfo.h> +#include <asm/cacheflush.h> +#include <asm/fw/fw.h> +#include <asm/sections.h> +#include <asm/setup.h> +#include <asm/timex.h> +#include <linux/elf.h> +#include <linux/kernel.h> +#include <linux/libfdt.h> +#include <linux/of_fdt.h> +#include <linux/panic_notifier.h> +#include <linux/sched/task.h> +#include <linux/start_kernel.h> +#include <linux/string.h> +#include <linux/printk.h> + +#define RELOCATED(x) ((void *)((long)x + offset)) + +extern u32 _relocation_start[]; /* End kernel image / start relocation table */ +extern u32 _relocation_end[]; /* End relocation table */ + +extern long __start___ex_table; /* Start exception table */ +extern long __stop___ex_table; /* End exception table */ + +extern void __weak plat_fdt_relocated(void *new_location); + +/* + * This function may be defined for a platform to perform any post-relocation + * fixup necessary. + * Return non-zero to abort relocation + */ +int __weak plat_post_relocation(long offset) +{ + return 0; +} + +static inline u32 __init get_synci_step(void) +{ + u32 res; + + __asm__("rdhwr %0, $1" : "=r" (res)); + + return res; +} + +static void __init sync_icache(void *kbase, unsigned long kernel_length) +{ + void *kend = kbase + kernel_length; + u32 step = get_synci_step(); + + do { + __asm__ __volatile__( + "synci 0(%0)" + : /* no output */ + : "r" (kbase)); + + kbase += step; + } while (step && kbase < kend); + + /* Completion barrier */ + __sync(); +} + +static void __init apply_r_mips_64_rel(u32 *loc_new, long offset) +{ + *(u64 *)loc_new += offset; +} + +static void __init apply_r_mips_32_rel(u32 *loc_new, long offset) +{ + *loc_new += offset; +} + +static int __init apply_r_mips_26_rel(u32 *loc_orig, u32 *loc_new, long offset) +{ + unsigned long target_addr = (*loc_orig) & 0x03ffffff; + + if (offset % 4) { + pr_err("Dangerous R_MIPS_26 REL relocation\n"); + return -ENOEXEC; + } + + /* Original target address */ + target_addr <<= 2; + target_addr += (unsigned long)loc_orig & 0xf0000000; + + /* Get the new target address */ + target_addr += offset; + + if ((target_addr & 0xf0000000) != ((unsigned long)loc_new & 0xf0000000)) { + pr_err("R_MIPS_26 REL relocation overflow\n"); + return -ENOEXEC; + } + + target_addr -= (unsigned long)loc_new & 0xf0000000; + target_addr >>= 2; + + *loc_new = (*loc_new & ~0x03ffffff) | (target_addr & 0x03ffffff); + + return 0; +} + + +static void __init apply_r_mips_hi16_rel(u32 *loc_orig, u32 *loc_new, + long offset) +{ + unsigned long insn = *loc_orig; + unsigned long target = (insn & 0xffff) << 16; /* high 16bits of target */ + + target += offset; + + *loc_new = (insn & ~0xffff) | ((target >> 16) & 0xffff); +} + +static int __init reloc_handler(u32 type, u32 *loc_orig, u32 *loc_new, + long offset) +{ + switch (type) { + case R_MIPS_64: + apply_r_mips_64_rel(loc_new, offset); + break; + case R_MIPS_32: + apply_r_mips_32_rel(loc_new, offset); + break; + case R_MIPS_26: + return apply_r_mips_26_rel(loc_orig, loc_new, offset); + case R_MIPS_HI16: + apply_r_mips_hi16_rel(loc_orig, loc_new, offset); + break; + default: + pr_err("Unhandled relocation type %d at 0x%pK\n", type, + loc_orig); + return -ENOEXEC; + } + + return 0; +} + +static int __init do_relocations(void *kbase_old, void *kbase_new, long offset) +{ + u32 *r; + u32 *loc_orig; + u32 *loc_new; + int type; + int res; + + for (r = _relocation_start; r < _relocation_end; r++) { + /* Sentinel for last relocation */ + if (*r == 0) + break; + + type = (*r >> 24) & 0xff; + loc_orig = kbase_old + ((*r & 0x00ffffff) << 2); + loc_new = RELOCATED(loc_orig); + + res = reloc_handler(type, loc_orig, loc_new, offset); + if (res) + return res; + } + + return 0; +} + +/* + * The exception table is filled in by the relocs tool after vmlinux is linked. + * It must be relocated separately since there will not be any relocation + * information for it filled in by the linker. + */ +static int __init relocate_exception_table(long offset) +{ + unsigned long *etable_start, *etable_end, *e; + + etable_start = RELOCATED(&__start___ex_table); + etable_end = RELOCATED(&__stop___ex_table); + + for (e = etable_start; e < etable_end; e++) + *e += offset; + + return 0; +} + +#ifdef CONFIG_RANDOMIZE_BASE + +static inline __init unsigned long rotate_xor(unsigned long hash, + const void *area, size_t size) +{ + const typeof(hash) *ptr = PTR_ALIGN(area, sizeof(hash)); + size_t diff, i; + + diff = (void *)ptr - area; + if (unlikely(size < diff + sizeof(hash))) + return hash; + + size = ALIGN_DOWN(size - diff, sizeof(hash)); + + for (i = 0; i < size / sizeof(hash); i++) { + /* Rotate by odd number of bits and XOR. */ + hash = (hash << ((sizeof(hash) * 8) - 7)) | (hash >> 7); + hash ^= ptr[i]; + } + + return hash; +} + +static inline __init unsigned long get_random_boot(void) +{ + unsigned long entropy = random_get_entropy(); + unsigned long hash = 0; + + /* Attempt to create a simple but unpredictable starting entropy. */ + hash = rotate_xor(hash, linux_banner, strlen(linux_banner)); + + /* Add in any runtime entropy we can get */ + hash = rotate_xor(hash, &entropy, sizeof(entropy)); + +#if defined(CONFIG_USE_OF) + /* Get any additional entropy passed in device tree */ + if (initial_boot_params) { + int node, len; + u64 *prop; + + node = fdt_path_offset(initial_boot_params, "/chosen"); + if (node >= 0) { + prop = fdt_getprop_w(initial_boot_params, node, + "kaslr-seed", &len); + if (prop && (len == sizeof(u64))) + hash = rotate_xor(hash, prop, sizeof(*prop)); + } + } +#endif /* CONFIG_USE_OF */ + + return hash; +} + +static inline __init bool kaslr_disabled(void) +{ + char *str; + +#if defined(CONFIG_CMDLINE_BOOL) + const char *builtin_cmdline = CONFIG_CMDLINE; + + str = strstr(builtin_cmdline, "nokaslr"); + if (str == builtin_cmdline || + (str > builtin_cmdline && *(str - 1) == ' ')) + return true; +#endif + str = strstr(arcs_cmdline, "nokaslr"); + if (str == arcs_cmdline || (str > arcs_cmdline && *(str - 1) == ' ')) + return true; + + return false; +} + +static inline void __init *determine_relocation_address(void) +{ + /* Choose a new address for the kernel */ + unsigned long kernel_length; + void *dest = &_text; + unsigned long offset; + + if (kaslr_disabled()) + return dest; + + kernel_length = (long)_end - (long)(&_text); + + offset = get_random_boot() << 16; + offset &= (CONFIG_RANDOMIZE_BASE_MAX_OFFSET - 1); + if (offset < kernel_length) + offset += ALIGN(kernel_length, 0xffff); + + return RELOCATED(dest); +} + +#else + +static inline void __init *determine_relocation_address(void) +{ + /* + * Choose a new address for the kernel + * For now we'll hard code the destination + */ + return (void *)0xffffffff81000000; +} + +#endif + +static inline int __init relocation_addr_valid(void *loc_new) +{ + if ((unsigned long)loc_new & 0x0000ffff) { + /* Inappropriately aligned new location */ + return 0; + } + if ((unsigned long)loc_new < (unsigned long)&_end) { + /* New location overlaps original kernel */ + return 0; + } + return 1; +} + +static inline void __init update_kaslr_offset(unsigned long *addr, long offset) +{ + unsigned long *new_addr = (unsigned long *)RELOCATED(addr); + + *new_addr = (unsigned long)offset; +} + +#if defined(CONFIG_USE_OF) +void __weak *plat_get_fdt(void) +{ + return NULL; +} +#endif + +void *__init relocate_kernel(void) +{ + void *loc_new; + unsigned long kernel_length; + unsigned long bss_length; + long offset = 0; + int res = 1; + /* Default to original kernel entry point */ + void *kernel_entry = start_kernel; + void *fdt = NULL; + + /* Get the command line */ + fw_init_cmdline(); +#if defined(CONFIG_USE_OF) + /* Deal with the device tree */ + fdt = plat_get_fdt(); + early_init_dt_scan(fdt); + if (boot_command_line[0]) { + /* Boot command line was passed in device tree */ + strscpy(arcs_cmdline, boot_command_line, COMMAND_LINE_SIZE); + } +#endif /* CONFIG_USE_OF */ + + kernel_length = (long)(&_relocation_start) - (long)(&_text); + bss_length = (long)&__bss_stop - (long)&__bss_start; + + loc_new = determine_relocation_address(); + + /* Sanity check relocation address */ + if (relocation_addr_valid(loc_new)) + offset = (unsigned long)loc_new - (unsigned long)(&_text); + + /* Reset the command line now so we don't end up with a duplicate */ + arcs_cmdline[0] = '\0'; + + if (offset) { + void (*fdt_relocated_)(void *) = NULL; +#if defined(CONFIG_USE_OF) + unsigned long fdt_phys = virt_to_phys(fdt); + + /* + * If built-in dtb is used then it will have been relocated + * during kernel _text relocation. If appended DTB is used + * then it will not be relocated, but it should remain + * intact in the original location. If dtb is loaded by + * the bootloader then it may need to be moved if it crosses + * the target memory area + */ + + if (fdt_phys >= virt_to_phys(RELOCATED(&_text)) && + fdt_phys <= virt_to_phys(RELOCATED(&_end))) { + void *fdt_relocated = + RELOCATED(ALIGN((long)&_end, PAGE_SIZE)); + memcpy(fdt_relocated, fdt, fdt_totalsize(fdt)); + fdt = fdt_relocated; + fdt_relocated_ = RELOCATED(&plat_fdt_relocated); + } +#endif /* CONFIG_USE_OF */ + + /* Copy the kernel to it's new location */ + memcpy(loc_new, &_text, kernel_length); + + /* Perform relocations on the new kernel */ + res = do_relocations(&_text, loc_new, offset); + if (res < 0) + goto out; + + /* Sync the caches ready for execution of new kernel */ + sync_icache(loc_new, kernel_length); + + res = relocate_exception_table(offset); + if (res < 0) + goto out; + + /* + * The original .bss has already been cleared, and + * some variables such as command line parameters + * stored to it so make a copy in the new location. + */ + memcpy(RELOCATED(&__bss_start), &__bss_start, bss_length); + + /* + * If fdt was stored outside of the kernel image and + * had to be moved then update platform's state data + * with the new fdt location + */ + if (fdt_relocated_) + fdt_relocated_(fdt); + + /* + * Last chance for the platform to abort relocation. + * This may also be used by the platform to perform any + * initialisation required now that the new kernel is + * resident in memory and ready to be executed. + */ + if (plat_post_relocation(offset)) + goto out; + + /* The current thread is now within the relocated image */ + __current_thread_info = RELOCATED(&init_thread_union); + + /* Return the new kernel's entry point */ + kernel_entry = RELOCATED(start_kernel); + + /* Error may occur before, so keep it at last */ + update_kaslr_offset(&__kaslr_offset, offset); + } +out: + return kernel_entry; +} + +/* + * Show relocation information on panic. + */ +static void show_kernel_relocation(const char *level) +{ + if (__kaslr_offset > 0) { + printk(level); + pr_cont("Kernel relocated by 0x%pK\n", (void *)__kaslr_offset); + pr_cont(" .text @ 0x%pK\n", _text); + pr_cont(" .data @ 0x%pK\n", _sdata); + pr_cont(" .bss @ 0x%pK\n", __bss_start); + } +} + +static int kernel_location_notifier_fn(struct notifier_block *self, + unsigned long v, void *p) +{ + show_kernel_relocation(KERN_EMERG); + return NOTIFY_DONE; +} + +static struct notifier_block kernel_location_notifier = { + .notifier_call = kernel_location_notifier_fn +}; + +static int __init register_kernel_offset_dumper(void) +{ + atomic_notifier_chain_register(&panic_notifier_list, + &kernel_location_notifier); + return 0; +} +__initcall(register_kernel_offset_dumper); |