Re: [PATCH 2/2] arm: use fixmap for text patching when text is RO

From: Kees Cook
Date: Wed Apr 23 2014 - 17:09:31 EST


On Fri, Apr 4, 2014 at 2:27 PM, Rabin Vincent <rabin@xxxxxx> wrote:
> Use fixmaps for text patching when the kernel text is read-only,
> inspired by x86. This makes jump labels and kprobes work with the
> currently available CONFIG_DEBUG_SET_MODULE_RONX and the upcoming
> CONFIG_DEBUG_RODATA options.
>
> Signed-off-by: Rabin Vincent <rabin@xxxxxx>
> ---
> arch/arm/include/asm/fixmap.h | 3 ++
> arch/arm/kernel/jump_label.c | 2 +-
> arch/arm/kernel/patch.c | 66 ++++++++++++++++++++++++++++++++++++++-----
> arch/arm/kernel/patch.h | 12 +++++++-
> 4 files changed, 74 insertions(+), 9 deletions(-)
>
> diff --git a/arch/arm/include/asm/fixmap.h b/arch/arm/include/asm/fixmap.h
> index 55ed076..79c1719 100644
> --- a/arch/arm/include/asm/fixmap.h
> +++ b/arch/arm/include/asm/fixmap.h
> @@ -18,6 +18,9 @@
> #define FIXADDR_TOP (FIXADDR_END - PAGE_SIZE)
>
> enum fixed_addresses {
> + FIX_TEXT_POKE0,
> + FIX_TEXT_POKE1,
> +
> FIX_KMAP_BEGIN,
> FIX_KMAP_END = (FIXADDR_TOP - FIXADDR_START) >> PAGE_SHIFT,
> __end_of_fixed_addresses
> diff --git a/arch/arm/kernel/jump_label.c b/arch/arm/kernel/jump_label.c
> index 4ce4f78..afeeb9e 100644
> --- a/arch/arm/kernel/jump_label.c
> +++ b/arch/arm/kernel/jump_label.c
> @@ -19,7 +19,7 @@ static void __arch_jump_label_transform(struct jump_entry *entry,
> insn = arm_gen_nop();
>
> if (is_static)
> - __patch_text(addr, insn);
> + __patch_text_early(addr, insn);
> else
> patch_text(addr, insn);
> }
> diff --git a/arch/arm/kernel/patch.c b/arch/arm/kernel/patch.c
> index 07314af..761c5b6 100644
> --- a/arch/arm/kernel/patch.c
> +++ b/arch/arm/kernel/patch.c
> @@ -1,8 +1,11 @@
> #include <linux/kernel.h>
> +#include <linux/spinlock.h>
> #include <linux/kprobes.h>
> +#include <linux/mm.h>
> #include <linux/stop_machine.h>
>
> #include <asm/cacheflush.h>
> +#include <asm/fixmap.h>
> #include <asm/smp_plat.h>
> #include <asm/opcodes.h>
>
> @@ -13,21 +16,67 @@ struct patch {
> unsigned int insn;
> };
>
> -void __kprobes __patch_text(void *addr, unsigned int insn)
> +static DEFINE_SPINLOCK(patch_lock);
> +
> +static void __kprobes *patch_map(void *addr, int fixmap, unsigned long *flags)
> +{
> + unsigned int uintaddr = (uintptr_t) addr;
> + bool module = !core_kernel_text(uintaddr);
> + struct page *page;
> +
> + if (module && IS_ENABLED(CONFIG_DEBUG_SET_MODULE_RONX)) {
> + page = vmalloc_to_page(addr);
> + } else if (!module && IS_ENABLED(CONFIG_ARM_KERNMEM_PERMS)) {
> + page = virt_to_page(addr);
> + } else {
> + return addr;
> + }
> +
> + if (flags)
> + spin_lock_irqsave(&patch_lock, *flags);
> +
> + set_fixmap(fixmap, page_to_phys(page));
> +
> + return (void *) (__fix_to_virt(fixmap) + (uintaddr & ~PAGE_MASK));
> +}
> +
> +static void __kprobes patch_unmap(int fixmap, unsigned long *flags)
> +{
> + clear_fixmap(fixmap);
> +
> + if (flags)
> + spin_unlock_irqrestore(&patch_lock, *flags);
> +}
> +
> +void __kprobes __patch_text_real(void *addr, unsigned int insn, bool remap)
> {
> bool thumb2 = IS_ENABLED(CONFIG_THUMB2_KERNEL);
> + unsigned int uintaddr = (uintptr_t) addr;
> + unsigned long flags;
> + void *waddr = addr;
> int size;
>
> + if (remap)
> + waddr = patch_map(addr, FIX_TEXT_POKE0, &flags);
> +
> if (thumb2 && __opcode_is_thumb16(insn)) {
> - *(u16 *)addr = __opcode_to_mem_thumb16(insn);
> + *(u16 *)waddr = __opcode_to_mem_thumb16(insn);
> size = sizeof(u16);
> - } else if (thumb2 && ((uintptr_t)addr & 2)) {
> + } else if (thumb2 && (uintaddr & 2)) {
> + bool twopage = (uintaddr & ~PAGE_MASK) == PAGE_SIZE - 2;
> u16 first = __opcode_thumb32_first(insn);
> u16 second = __opcode_thumb32_second(insn);
> - u16 *addrh = addr;
> + u16 *addrh0 = waddr;
> + u16 *addrh1 = waddr + 2;
> +
> + if (remap && twopage)
> + addrh1 = patch_map(addr + 2, FIX_TEXT_POKE1, NULL);
>
> - addrh[0] = __opcode_to_mem_thumb16(first);
> - addrh[1] = __opcode_to_mem_thumb16(second);
> + *addrh0 = __opcode_to_mem_thumb16(first);
> + *addrh1 = __opcode_to_mem_thumb16(second);
> +
> + if (twopage && addrh1 != addr + 2)
> + patch_unmap(FIX_TEXT_POKE1, NULL);
>
> size = sizeof(u32);
> } else {
> @@ -36,10 +85,13 @@ void __kprobes __patch_text(void *addr, unsigned int insn)
> else
> insn = __opcode_to_mem_arm(insn);
>
> - *(u32 *)addr = insn;
> + *(u32 *)waddr = insn;
> size = sizeof(u32);
> }
>
> + if (waddr != addr)
> + patch_unmap(FIX_TEXT_POKE0, &flags);
> +
> flush_icache_range((uintptr_t)(addr),
> (uintptr_t)(addr) + size);
> }
> diff --git a/arch/arm/kernel/patch.h b/arch/arm/kernel/patch.h
> index b4731f2..77e054c 100644
> --- a/arch/arm/kernel/patch.h
> +++ b/arch/arm/kernel/patch.h
> @@ -2,6 +2,16 @@
> #define _ARM_KERNEL_PATCH_H
>
> void patch_text(void *addr, unsigned int insn);
> -void __patch_text(void *addr, unsigned int insn);
> +void __patch_text_real(void *addr, unsigned int insn, bool remap);
> +
> +static inline void __patch_text(void *addr, unsigned int insn)
> +{
> + __patch_text_real(addr, insn, true);
> +}
> +
> +static inline void __patch_text_early(void *addr, unsigned int insn)
> +{
> + __patch_text_real(addr, insn, false);
> +}
>
> #endif
> --
> 1.9.1
>

I think we can use this for the kgdb case too. Has anyone tried that
yet? Perhaps implementing text_poke() in terms of patch_map/unmap
calls would be best? Then the arm-specific kgdb hooks can use that,
since it does the icache flushing separately.

-Kees

--
Kees Cook
Chrome OS Security
--
To unsubscribe from this list: send the line "unsubscribe linux-kernel" in
the body of a message to majordomo@xxxxxxxxxxxxxxx
More majordomo info at http://vger.kernel.org/majordomo-info.html
Please read the FAQ at http://www.tux.org/lkml/