Loading...
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 | /* * alternative runtime patching * inspired by the x86 version * * Copyright (C) 2014 ARM Ltd. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ #define pr_fmt(fmt) "alternatives: " fmt #include <linux/init.h> #include <linux/cpu.h> #include <asm/cacheflush.h> #include <asm/alternative.h> #include <asm/cpufeature.h> #include <asm/insn.h> #include <linux/stop_machine.h> #define __ALT_PTR(a,f) (u32 *)((void *)&(a)->f + (a)->f) #define ALT_ORIG_PTR(a) __ALT_PTR(a, orig_offset) #define ALT_REPL_PTR(a) __ALT_PTR(a, alt_offset) extern struct alt_instr __alt_instructions[], __alt_instructions_end[]; struct alt_region { struct alt_instr *begin; struct alt_instr *end; }; /* * Check if the target PC is within an alternative block. */ static bool branch_insn_requires_update(struct alt_instr *alt, unsigned long pc) { unsigned long replptr; if (kernel_text_address(pc)) return 1; replptr = (unsigned long)ALT_REPL_PTR(alt); if (pc >= replptr && pc <= (replptr + alt->alt_len)) return 0; /* * Branching into *another* alternate sequence is doomed, and * we're not even trying to fix it up. */ BUG(); } static u32 get_alt_insn(struct alt_instr *alt, u32 *insnptr, u32 *altinsnptr) { u32 insn; insn = le32_to_cpu(*altinsnptr); if (aarch64_insn_is_branch_imm(insn)) { s32 offset = aarch64_get_branch_offset(insn); unsigned long target; target = (unsigned long)altinsnptr + offset; /* * If we're branching inside the alternate sequence, * do not rewrite the instruction, as it is already * correct. Otherwise, generate the new instruction. */ if (branch_insn_requires_update(alt, target)) { offset = target - (unsigned long)insnptr; insn = aarch64_set_branch_offset(insn, offset); } } return insn; } static void __apply_alternatives(void *alt_region) { struct alt_instr *alt; struct alt_region *region = alt_region; u32 *origptr, *replptr; for (alt = region->begin; alt < region->end; alt++) { u32 insn; int i, nr_inst; if (!cpus_have_cap(alt->cpufeature)) continue; BUG_ON(alt->alt_len != alt->orig_len); pr_info_once("patching kernel code\n"); origptr = ALT_ORIG_PTR(alt); replptr = ALT_REPL_PTR(alt); nr_inst = alt->alt_len / sizeof(insn); for (i = 0; i < nr_inst; i++) { insn = get_alt_insn(alt, origptr + i, replptr + i); *(origptr + i) = cpu_to_le32(insn); } flush_icache_range((uintptr_t)origptr, (uintptr_t)(origptr + nr_inst)); } } /* * We might be patching the stop_machine state machine, so implement a * really simple polling protocol here. */ static int __apply_alternatives_multi_stop(void *unused) { static int patched = 0; struct alt_region region = { .begin = __alt_instructions, .end = __alt_instructions_end, }; /* We always have a CPU 0 at this point (__init) */ if (smp_processor_id()) { while (!READ_ONCE(patched)) cpu_relax(); isb(); } else { BUG_ON(patched); __apply_alternatives(®ion); /* Barriers provided by the cache flushing */ WRITE_ONCE(patched, 1); } return 0; } void __init apply_alternatives_all(void) { /* better not try code patching on a live SMP system */ stop_machine(__apply_alternatives_multi_stop, NULL, cpu_online_mask); } void apply_alternatives(void *start, size_t length) { struct alt_region region = { .begin = start, .end = start + length, }; __apply_alternatives(®ion); } |