clinux-kernelkernelkernel-modulearm64

Handling an undefined instruction in the kernel


So I'm playing around with reading system registers in the kernel and I've recently run into a bit of a roadblock.

In ARM64, certain system registers (e.g. OSECCR_EL1) are not always implemented. If they are implemented, then trying an mrs instruction is fine - nothing bad happens. But if they AREN'T implemented, then the kernel throws an Oops due to an undefined instruction.

This isn't unreasonable, however, as I'm inside a kernel module while running this mrs instruction, I don't see an easy way to recover from this oops, or even recognize that a particular system register read was going to fail in the first place.

Is there any easy way to identify beforehand whether a system register is valid, or at the very least, handle a kernel oops in a way that doesn't immediately stop my kernel module function execution?


Solution

  • Note: I'm going to use undef hooks, which were reworked in v6.2, so for Linux >= 6.2 things are a bit different: check this other answer.

    Since you say you are just "playing around", I'm going to suggest a kinda dirty, but pretty straightforward solution.

    The Linux kernel for ARM has its own way of handling undefined instructions to emulate them, this is done through simple "undefined instruction hooks", defined in arch/arm64/include/asm/traps.h:

    struct undef_hook {
        struct list_head node;
        u32 instr_mask;
        u32 instr_val;
        u64 pstate_mask;
        u64 pstate_val;
        int (*fn)(struct pt_regs *regs, u32 instr);
    };
    

    These hooks are added through the (unfortunately not exported) function register_undef_hook(), and removed through unregister_undef_hook().

    To solve your problem, you have two options:

    1. Export both functions by modifying arch/arm64/kernel/traps.c adding the following two lines of code:

       // after register_undef_hook
       EXPORT_SYMBOL(register_undef_hook);
      
       // after unregister_undef_hook
       EXPORT_SYMBOL(unregister_undef_hook);
      

      Now recompile the kernel and the functions will be exported and available to be used in modules. You now have a way of easily handling undefined instructions how you want.

    2. Use kallsyms_lookup_name() to lookup the symbols at runtime directly from your module, without the need to re-compile the kernel. A bit messier, but probably easier and surely overall a faster solution.

    For option #1, here's an example module that does exactly what you want:

    // SPDX-License-Identifier: GPL-3.0
    #include <linux/init.h>   // module_{init,exit}()
    #include <linux/module.h> // THIS_MODULE, MODULE_VERSION, ...
    #include <asm/traps.h>    // struct undef_hook, register_undef_hook()
    #include <asm/ptrace.h>   // struct pt_regs
    
    #ifdef pr_fmt
    #undef pr_fmt
    #endif
    #define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
    
    static void whoops(void)
    {
        // Execute a known invalid instruction.
        asm volatile (".word 0xf7f0a000");
    }
    
    static int undef_instr_handler(struct pt_regs *regs, u32 instr)
    {
        pr_info("*gotcha*\n");
    
        // Just skip over to the next instruction.
        regs->pc += 4;
    
        return 0; // All fine!
    }
    
    static struct undef_hook uh = {
        .instr_mask  = 0x0, // any instruction
        .instr_val   = 0x0, // any instruction
        .pstate_mask = 0x0, // any pstate
        .pstate_val  = 0x0, // any pstate
        .fn          = undef_instr_handler
    };
    
    static int __init modinit(void)
    {
        register_undef_hook(&uh);
    
        pr_info("Jumping off a cliff...\n");
        whoops();
        pr_info("Woah, I survived!\n");
    
        return 0;
    }
    
    static void __exit modexit(void)
    {
        unregister_undef_hook(&uf);
    }
    
    module_init(modinit);
    module_exit(modexit);
    MODULE_VERSION("0.1");
    MODULE_DESCRIPTION("Test undefined instruction handling on arm64.");
    MODULE_AUTHOR("Marco Bonelli");
    MODULE_LICENSE("GPL");
    

    For option #2, you can just modify the above code adding the following:

    #include <linux/kallsyms.h> // kallsyms_lookup_name()
    
    // Define two global pointers.
    static void (*register_undef_hook_ptr)(struct undef_hook *);
    static void (*unregister_undef_hook_ptr)(struct undef_hook *);
    
    static int __init modinit(void)
    {
        // Lookup wanted symbols.
        register_undef_hook_ptr   = (void *)kallsyms_lookup_name("register_undef_hook");
        unregister_undef_hook_ptr = (void *)kallsyms_lookup_name("unregister_undef_hook");
    
        if (!register_undef_hook_ptr)
            return -EFAULT;
    
        // ...
    
        return 0;
    }
    
    static void __exit modexit(void)
    {
        if (unregister_undef_hook_ptr)
            unregister_undef_hook_ptr(&uh);
    }
    

    Here's the dmesg output:

    [    1.508253] testmod: Jumping off a cliff...
    [    1.508781] testmod: *gotcha*
    [    1.509207] testmod: Woah, I survived!
    

    Some notes