参考
Linker Script in Linux (3.1.1 Exception Table)
5. Kernel level exception handling
环境
ARM64
Linux-5.8
场景
用户通过系统调用给内核传递了一个参数,这个参数有一个该用户地址空间的地址,然后内核在访问时会发生什么情况呢?如果这个用户空间地址处于当前进程的有效vma中,那么正常的缺页异常可以处理。
但是如果这个参数是一个非法的用户地址,内核访问的话,会不会由于访问了非法地址而导致崩溃呢?
测试程序
驱动
static ssize_t fixup_read (struct file *filp, char __user *buf, size_t count, loff_t *fpos) { int err; err = put_user(10, buf); printk("%s: %d\n", __func__, err); return count; }
用户程序
int main(int argc, const char *argv[]) { int fd; char *buf; fd = open("/dev/fixup", O_RDWR); buf = 0x4000000; printf("read buf\n"); read(fd, buf, 5); return 0; }
内核log
[11131.867589] fixup_read: -14
可以看到put_user返回了错误码:-EFAULT
,并没有导致内核崩溃,测试程序也没有异常退出。在这背后发生了什么呢?其实内核在访问这个非法用户地址的时候,确实发生了缺页异常,不过在缺页异常中使用这里说的exception_table
进行了修复,返回了错误码。要完成这个功能,需要依赖put_user和缺页异常的支持。
原理
put_user
put_user
的实现在arch/arm64/include/asm/uaccess.h
中:
#define _ASM_EXTABLE(from, to) \ " .pushsection __ex_table, \"a\"\n" \ " .align 3\n" \ " .long (" #from " - .), (" #to " - .)\n" \ " .popsection\n" #define __put_mem_asm(store, reg, x, addr, err) \ asm volatile( \ "1: " store " " reg "1, [%2]\n" \ "2:\n" \ " .section .fixup,\"ax\"\n" \ " .align 2\n" \ "3: mov %w0, %3\n" \ " b 2b\n" \ " .previous\n" \ _ASM_EXTABLE(1b, 3b) \ : "+r" (err) \ : "r" (x), "r" (addr), "i" (-EFAULT)) #define __raw_put_mem(str, x, ptr, err) \ do { \ __typeof__(*(ptr)) __pu_val = (x); \ switch (sizeof(*(ptr))) { \ case 1: \ __put_mem_asm(str "b", "%w", __pu_val, (ptr), (err)); \ break; \ case 2: \ __put_mem_asm(str "h", "%w", __pu_val, (ptr), (err)); \ break; \ case 4: \ __put_mem_asm(str, "%w", __pu_val, (ptr), (err)); \ break; \ case 8: \ __put_mem_asm(str, "%x", __pu_val, (ptr), (err)); \ break; \ default: \ BUILD_BUG(); \ } \ } while (0) #define __raw_put_user(x, ptr, err) \ do { \ __chk_user_ptr(ptr); \ uaccess_ttbr0_enable(); \ __raw_put_mem("sttr", x, ptr, err); \ uaccess_ttbr0_disable(); \ } while (0) #define __put_user_error(x, ptr, err) \ do { \ __typeof__(*(ptr)) __user *__p = (ptr); \ might_fault(); \ if (access_ok(__p, sizeof(*__p))) { \ __p = uaccess_mask_ptr(__p); \ __raw_put_user((x), __p, (err)); \ } else { \ (err) = -EFAULT; \ } \ } while (0) #define __put_user(x, ptr) \ ({ \ int __pu_err = 0; \ __put_user_error((x), (ptr), __pu_err); \ __pu_err; \ }) #define put_user __put_user
重点关注下面的内容:
#define _ASM_EXTABLE(from, to) \ " .pushsection __ex_table, \"a\"\n" \ " .align 3\n" \ " .long (" #from " - .), (" #to " - .)\n" \ " .popsection\n" #define __put_mem_asm(store, reg, x, addr, err) \ asm volatile( \ "1: " store " " reg "1, [%2]\n" \ "2:\n" \ " .section .fixup,\"ax\"\n" \ " .align 2\n" \ "3: mov %w0, %3\n" \ " b 2b\n" \ " .previous\n" \ _ASM_EXTABLE(1b, 3b) \ : "+r" (err) \ : "r" (x), "r" (addr), "i" (-EFAULT))
上面用到了两个section,.fixup
和__ex_table
,其中前者是修复程序的入口,后者用于存放触发异常的指令所在的地址跟对应的修复程序的入口地址之间的映射关系。比如当1b
处的代码写addr
触发了异常后,陷入内核缺页异常,然后在内核缺页异常里搜索1b
对应的__ex_table
,这个表里记录了异常指令地址跟对应的修复程序的入口地址的映射关系,找到后在异常返回时会跳转到修复程序入口,也就是跳转到上面.fixup
中3b
处,然后将错误码-EFAULT
存入err
中,最后跳转到异常指令1b
的下一行2b
继续执行。
在每一个__ex_table
中存放了两个偏移量,分别是异常指令的地址1b
和修改指令的地址3b
跟当前地址的偏移,这样遍历__ex_table
数组的时候可以很容易获得1b
和3b
的地址。
在内核的链接脚本中有专门存放__ex_table
和.fixup
的段:
__ex_table
:
/* * Exception table */ #define EXCEPTION_TABLE(align) \ . = ALIGN(align); \ __ex_table : AT(ADDR(__ex_table) - LOAD_OFFSET) { \ __start___ex_table = .; \ KEEP(*(__ex_table)) \ __stop___ex_table = .; \ }
.fixup
:
.text : ALIGN(SEGMENT_ALIGN) { /* Real text segment */ _stext = .; /* Text and read-only data */ IRQENTRY_TEXT SOFTIRQENTRY_TEXT ENTRY_TEXT TEXT_TEXT SCHED_TEXT CPUIDLE_TEXT LOCK_TEXT KPROBES_TEXT HYPERVISOR_TEXT IDMAP_TEXT HIBERNATE_TEXT TRAMP_TEXT *(.fixup) *(.gnu.warning) . = ALIGN(16); *(.got) /* Global offset table */ }
内核缺页
do_translation_fault
----> do_page_fault
---->__do_kernel_fault
----> fixup_exception
int fixup_exception(struct pt_regs *regs) { const struct exception_table_entry *fixup; fixup = search_exception_tables(instruction_pointer(regs)); if (!fixup) return 0; if (in_bpf_jit(regs)) return arm64_bpf_fixup_exception(fixup, regs); regs->pc = (unsigned long)&fixup->fixup + fixup->fixup; return 1; }
instruction_pointer(regs)
返回异常指令的地址,也就是上面的1b
,然后调用search_exception_tables
搜索,搜索顺序是先从内核的__start___ex_table ~ __stop___ex_table
搜索,如果没有找到,那么会依次从内核module和bpf里搜索。
/* Given an address, look for it in the exception tables. */ const struct exception_table_entry *search_exception_tables(unsigned long addr) { const struct exception_table_entry *e; e = search_kernel_exception_table(addr); // 静态编译到内核中的 if (!e) e = search_module_extables(addr); // 从内核module里搜索 if (!e) e = search_bpf_extables(addr); // 从bpf里搜索 return e; }
search_kernel_exception_table
的实现如下:
/* Given an address, look for it in the kernel exception table */ const struct exception_table_entry *search_kernel_exception_table(unsigned long addr) { return search_extable(__start___ex_table, __stop___ex_table - __start___ex_table, addr); }
search_extable
的实现如下:
struct exception_table_entry { int insn, fixup; }; static inline unsigned long ex_to_insn(const struct exception_table_entry *x) { return (unsigned long)&x->insn + x->insn; } static int cmp_ex_search(const void *key, const void *elt) { const struct exception_table_entry *_elt = elt; unsigned long _key = *(unsigned long *)key; /* avoid overflow */ if (_key > ex_to_insn(_elt)) return 1; if (_key < ex_to_insn(_elt)) return -1; return 0; } /* * Search one exception table for an entry corresponding to the * given instruction address, and return the address of the entry, * or NULL if none is found. * We use a binary search, and thus we assume that the table is * already sorted. */ const struct exception_table_entry * search_extable(const struct exception_table_entry *base, const size_t num, unsigned long value) { return bsearch(&value, base, num, sizeof(struct exception_table_entry), cmp_ex_search); }
对于遍历到的每一个__ex_table
都会调用cmp_ex_search
,第一个参数key中存放的是&value
,第二个参数存放的是当前__ex_table
的地址。_key
中存放的是异常指令1b
的地址,ex_to_insn(_elt)
返回记录的异常指令1b
的地址(__ex_table
的地址+偏移),如果二者相等,表示当前__ex_table
就是我们想要的。
最后回到fixup_exception
中,search_exception_tables
返回找到的__ex_table
,然后计算修复指令的地址(unsigned long)&fixup->fixup + fixup->fixup
(地址+偏移),将结果赋值给regs->pc
,这样当异常返回后就会跳转到修复指令的地址开始执行,也就是上面提到的3b
位置。
上面分析了put_user
,对于get_user
、copy_to_user
以及copy_from_user
都采用了类似的方法来处理用户传递了非法的地址的情况,防止内核崩溃。