125 lines
3.2 KiB
C
125 lines
3.2 KiB
C
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
/*
|
|
* livepatch.c - powerpc-specific Kernel Live Patching Core
|
|
*
|
|
* Copyright (C) 2018 Huawei Technologies Co., Ltd.
|
|
* Copyright (C) 2023 Huawei Technologies Co., Ltd.
|
|
*
|
|
* This program is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU General Public License
|
|
* as published by the Free Software Foundation; either version 2
|
|
* of the License, or (at your option) any later version.
|
|
*
|
|
* 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) KBUILD_MODNAME ": " fmt
|
|
|
|
#include <linux/module.h>
|
|
#include <linux/uaccess.h>
|
|
#include <linux/livepatch.h>
|
|
#include <asm/livepatch.h>
|
|
#include <asm/cacheflush.h>
|
|
#include <linux/slab.h>
|
|
#include <asm/code-patching.h>
|
|
|
|
static inline bool offset_in_range(unsigned long pc, unsigned long addr,
|
|
long range)
|
|
{
|
|
long offset = addr - pc;
|
|
|
|
return (offset >= -range && offset < range);
|
|
}
|
|
|
|
long arch_klp_save_old_code(struct arch_klp_data *arch_data, void *old_func)
|
|
{
|
|
long ret;
|
|
int i;
|
|
|
|
for (i = 0; i < LJMP_INSN_SIZE; i++) {
|
|
ret = copy_from_kernel_nofault(&arch_data->old_insns[i],
|
|
((u32 *)old_func) + i, PPC32_INSN_SIZE);
|
|
if (ret)
|
|
break;
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
static int do_patch(unsigned long pc, unsigned long new_addr)
|
|
{
|
|
int ret;
|
|
u32 insns[LJMP_INSN_SIZE];
|
|
|
|
if (offset_in_range(pc, new_addr, SZ_32M)) {
|
|
ppc_inst_t instr;
|
|
|
|
create_branch(&instr, (ppc_inst_t *)pc, new_addr, 0);
|
|
ret = patch_instruction((ppc_inst_t *)pc, instr);
|
|
if (ret) {
|
|
pr_err("patch instruction small range failed, ret=%d\n", ret);
|
|
return -EPERM;
|
|
}
|
|
} else {
|
|
/*
|
|
* lis r12,sym@ha
|
|
* addi r12,r12,sym@l
|
|
* mtctr r12
|
|
* bctr
|
|
*/
|
|
insns[0] = 0x3d800000 + ((new_addr + 0x8000) >> 16);
|
|
insns[1] = 0x398c0000 + (new_addr & 0xffff);
|
|
insns[2] = 0x7d8903a6;
|
|
insns[3] = 0x4e800420;
|
|
|
|
ret = klp_patch_text((u32 *)pc, insns, LJMP_INSN_SIZE);
|
|
if (ret) {
|
|
pr_err("patch instruction large range failed, ret=%d\n", ret);
|
|
return -EPERM;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
int arch_klp_patch_func(struct klp_func *func)
|
|
{
|
|
struct klp_func_node *func_node;
|
|
int ret;
|
|
|
|
func_node = func->func_node;
|
|
list_add_rcu(&func->stack_node, &func_node->func_stack);
|
|
ret = do_patch((unsigned long)func->old_func, (unsigned long)func->new_func);
|
|
if (ret)
|
|
list_del_rcu(&func->stack_node);
|
|
return ret;
|
|
}
|
|
|
|
void arch_klp_unpatch_func(struct klp_func *func)
|
|
{
|
|
struct klp_func_node *func_node;
|
|
struct klp_func *next_func;
|
|
unsigned long pc;
|
|
int ret;
|
|
|
|
func_node = func->func_node;
|
|
pc = (unsigned long)func_node->old_func;
|
|
list_del_rcu(&func->stack_node);
|
|
if (list_empty(&func_node->func_stack)) {
|
|
ret = klp_patch_text((u32 *)pc, func_node->arch_data.old_insns,
|
|
LJMP_INSN_SIZE);
|
|
if (ret) {
|
|
pr_err("restore instruction failed, ret=%d\n", ret);
|
|
return;
|
|
}
|
|
} else {
|
|
next_func = list_first_or_null_rcu(&func_node->func_stack,
|
|
struct klp_func, stack_node);
|
|
do_patch(pc, (unsigned long)next_func->new_func);
|
|
}
|
|
}
|