在《KASAN(1)-简单实践》中有一个配置CONFIG_KASAN_INLINE我们没有详细解释,只是简单的根据kernel docs的说明进行了解释,这里根据实际分析来详细介绍一下kasan的inline的检测流程。
kasan的inline模式是通过指令插桩的,了解ftrace的可以知道,gcc有办法在编译的时候对函数进行插桩操作,从而使得程序具备可调试特性,同样的kasan也是如此,这里的插桩也是通过gcc来插桩。想了解ftrace如何插桩可以查看文章《ftrace调试内核》
为了了解INLINE的实现原理,这里以一个简单的代码作为测试
static noinline void kmalloc_oob_right(void) { char *ptr; size_t size = 123; ptr = kmalloc(size, GFP_KERNEL); ptr[size] = 'x'; kfree(ptr); return ; }
其检测堆栈如下
[ 10.118159] dump_backtrace+0x0/0x3bc [ 10.118182] show_stack+0x1c/0x24 [ 10.118204] dump_stack_lvl+0x130/0x168 [ 10.118228] print_address_description.constprop.0+0x74/0x2b8 [ 10.118251] kasan_report+0x1e8/0x200 [ 10.118273] __asan_report_store1_noabort+0x30/0x5c [ 10.118294] kmalloc_oob_right+0x8c/0x90 [ 10.118315] test_kasan_module_init+0x18/0x40 [ 10.118335] do_one_initcall+0xb0/0x4e0 [ 10.118360] kernel_init_freeable+0x47c/0x4e4 [ 10.118380] kernel_init+0x18/0x13c [ 10.118400] ret_from_fork+0x10/0x18
可以看到,配置为inline模式下,代码在kmalloc_oob_right
之后直接调用了__asan_report_store1_noabort
,这个是怎么实现的呢。本文主要探究这个。
首先,为了排除函数的宏展开的代码,可以尝试编译时加上-E
来展开。
对于内核,每个c都提供了.cmd
文件方便调试,文章《vDSO--示例之实现系统调用》也通过了此方法来了解syscall的展开问题。
此时我们关注文件lib/.test_kasan_kylin.mod.o.cmd
我们看到此测试模块的编译代码如下
cmd_lib/test_kasan_kylin.mod.o := /root/kernel/roc-rk3588s-pc/kernel/scripts/gcc-wrapper.py gcc -Wp,-MMD,lib/.test_kasan_kylin.mod.o.d -nostdinc -isystem /usr/lib/gcc/aarch64-linux-gnu/10/include -I./arch/arm64/include -I./arch/arm64/include/generated -I./include -I./arch/arm64/include/uapi -I./arch/arm64/include/generated/uapi -I./include/uapi -I./include/generated/uapi -include ./include/linux/kconfig.h -include ./include/linux/compiler_types.h -D__KERNEL__ -mlittle-endian -DCC_USING_PATCHABLE_FUNCTION_ENTRY -DKASAN_SHADOW_SCALE_SHIFT=3 -fmacro-prefix-map=./= -Wall -Wundef -Werror=strict-prototypes -Wno-trigraphs -fno-strict-aliasing -fno-common -fshort-wchar -fno-PIE -Werror=implicit-function-declaration -Werror=implicit-int -Werror=return-type -Wno-format-security -std=gnu89 -mgeneral-regs-only -DCONFIG_CC_HAS_K_CONSTRAINT=1 -Wno-psabi -mabi=lp64 -fno-asynchronous-unwind-tables -fno-unwind-tables -mbranch-protection=none -Wa,-march=armv8.5-a -DARM64_ASM_ARCH='"armv8.5-a"' -DKASAN_SHADOW_SCALE_SHIFT=3 -fno-delete-null-pointer-checks -Wno-frame-address -Wno-format-truncation -Wno-format-overflow -Wno-address-of-packed-member -O2 -fno-allow-store-data-races -Wframe-larger-than=2048 -fstack-protector-strong -Werror -Wno-unused-but-set-variable -Wno-unused-const-variable -fno-omit-frame-pointer -fno-optimize-sibling-calls -g -Wdeclaration-after-statement -Wno-pointer-sign -Wno-stringop-truncation -Wno-zero-length-bounds -Wno-array-bounds -Wno-stringop-overflow -Wno-restrict -Wno-maybe-uninitialized -fno-strict-overflow -fno-stack-check -fconserve-stack -Werror=date-time -Werror=incompatible-pointer-types -Werror=designated-init -Wno-packed-not-aligned -mstack-protector-guard=sysreg -mstack-protector-guard-reg=sp_el0 -mstack-protector-guard-offset=1344 -fsanitize=kernel-address -fasan-shadow-offset=0xdfffffd000000000 --param asan-globals=1 --param asan-instrumentation-with-call-threshold=10000 --param asan-stack=1 --param asan-instrument-allocas=1 -DMODULE -DKBUILD_BASENAME='"test_kasan_kylin.mod"' -DKBUILD_MODNAME='"test_kasan_kylin"' -D__KBUILD_MODNAME=kmod_test_kasan_kylin -c -o lib/test_kasan_kylin.mod.o lib/test_kasan_kylin.mod.c
知道这个就比较简单了,如果我们只需要宏展开,可以如下
/root/kernel/roc-rk3588s-pc/kernel/scripts/gcc-wrapper.py gcc -Wp,-MMD,lib/.test_kasan_kylin.mod.o.d -nostdinc -isystem /usr/lib/gcc/aarch64-linux-gnu/10/include -I./arch/arm64/include -I./arch/arm64/include/generated -I./include -I./arch/arm64/include/uapi -I./arch/arm64/include/generated/uapi -I./include/uapi -I./include/generated/uapi -include ./include/linux/kconfig.h -include ./include/linux/compiler_types.h -D__KERNEL__ -mlittle-endian -DCC_USING_PATCHABLE_FUNCTION_ENTRY -DKASAN_SHADOW_SCALE_SHIFT=3 -fmacro-prefix-map=./= -Wall -Wundef -Werror=strict-prototypes -Wno-trigraphs -fno-strict-aliasing -fno-common -fshort-wchar -fno-PIE -Werror=implicit-function-declaration -Werror=implicit-int -Werror=return-type -Wno-format-security -std=gnu89 -mgeneral-regs-only -DCONFIG_CC_HAS_K_CONSTRAINT=1 -Wno-psabi -mabi=lp64 -fno-asynchronous-unwind-tables -fno-unwind-tables -mbranch-protection=none -Wa,-march=armv8.5-a -DARM64_ASM_ARCH='"armv8.5-a"' -DKASAN_SHADOW_SCALE_SHIFT=3 -fno-delete-null-pointer-checks -Wno-frame-address -Wno-format-truncation -Wno-format-overflow -Wno-address-of-packed-member -O2 -fno-allow-store-data-races -Wframe-larger-than=2048 -fstack-protector-strong -Werror -Wno-unused-but-set-variable -Wno-unused-const-variable -fno-omit-frame-pointer -fno-optimize-sibling-calls -g -Wdeclaration-after-statement -Wno-pointer-sign -Wno-stringop-truncation -Wno-zero-length-bounds -Wno-array-bounds -Wno-stringop-overflow -Wno-restrict -Wno-maybe-uninitialized -fno-strict-overflow -fno-stack-check -fconserve-stack -Werror=date-time -Werror=incompatible-pointer-types -Werror=designated-init -Wno-packed-not-aligned -mstack-protector-guard=sysreg -mstack-protector-guard-reg=sp_el0 -mstack-protector-guard-offset=1344 -fsanitize=kernel-address -fasan-shadow-offset=0xdfffffd000000000 --param asan-globals=1 --param asan-instrumentation-with-call-threshold=10000 --param asan-stack=1 --param asan-instrument-allocas=1 -DMODULE -DKBUILD_BASENAME='"test_kasan_kylin.mod"' -DKBUILD_MODNAME='"test_kasan_kylin"' -D__KBUILD_MODNAME=kmod_test_kasan_kylin -c -E -o lib/test_kasan_kylin.mod.o.E lib/test_kasan_kylin.c
此时我们打开lib/test_kasan_kylin.mod.o.E
即可,找到函数kmalloc_oob_right
,如下
static __attribute__((__noinline__)) void kmalloc_oob_right(void) { char *ptr; size_t size = 123; ptr = kmalloc(size, ((( gfp_t)(0x400u|0x800u)) | (( gfp_t)0x40u) | (( gfp_t)0x80u))); ptr[size] = 'x'; kfree(ptr); return ; }
可以看到,函数并没有被宏展开,那么可以进一步确认是否通过inline/指令插桩实现
只要不是宏展开的问题,我们可以使用objdump反汇编,如下
# objdump -d lib/test_kasan_kylin.ko 0000000000000000 <kmalloc_oob_right>: 0: a9be7bfd stp x29, x30, [sp, #-32]! 4: 90000000 adrp x0, 0 <kmalloc_caches> 8: 91000000 add x0, x0, #0x0 c: 910003fd mov x29, sp 10: d2dffa01 mov x1, #0xffd000000000 // #281268818280448 14: d343fc02 lsr x2, x0, #3 18: f2fbffe1 movk x1, #0xdfff, lsl #48 1c: f9000bf3 str x19, [sp, #16] 20: 38e16841 ldrsb w1, [x2, x1] 24: 34000041 cbz w1, 2c <kmalloc_oob_right+0x2c> 28: 94000000 bl 0 <__asan_report_load8_noabort> 2c: 90000000 adrp x0, 0 <kmalloc_caches> 30: d2800f62 mov x2, #0x7b // #123 34: 52819801 mov w1, #0xcc0 // #3264 38: f9400000 ldr x0, [x0] 3c: 94000000 bl 0 <kmem_cache_alloc_trace> 40: aa0003f3 mov x19, x0 44: 9101ec00 add x0, x0, #0x7b 48: d2dffa01 mov x1, #0xffd000000000 // #281268818280448 4c: f2fbffe1 movk x1, #0xdfff, lsl #48 50: 12000802 and w2, w0, #0x7 54: d343fc03 lsr x3, x0, #3 58: 38e16861 ldrsb w1, [x3, x1] 5c: 7100003f cmp w1, #0x0 60: 7a411041 ccmp w2, w1, #0x1, ne // ne = any 64: 5400004b b.lt 6c <kmalloc_oob_right+0x6c> // b.tstop 68: 94000000 bl 0 <__asan_report_store1_noabort> 6c: 52800f01 mov w1, #0x78 // #120 70: aa1303e0 mov x0, x19 74: 3901ee61 strb w1, [x19, #123] 78: 94000000 bl 0 <kfree> 7c: f9400bf3 ldr x19, [sp, #16] 80: a8c27bfd ldp x29, x30, [sp], #32 84: d65f03c0 ret
可以看到代码在ptr[size] = 'x';
之前被插入了一端汇编。被插入的汇编内容如下
40: aa0003f3 mov x19, x0 44: 9101ec00 add x0, x0, #0x7b 48: d2dffa01 mov x1, #0xffd000000000 // #281268818280448 4c: f2fbffe1 movk x1, #0xdfff, lsl #48 50: 12000802 and w2, w0, #0x7 54: d343fc03 lsr x3, x0, #3 58: 38e16861 ldrsb w1, [x3, x1] 5c: 7100003f cmp w1, #0x0 60: 7a411041 ccmp w2, w1, #0x1, ne // ne = any 64: 5400004b b.lt 6c <kmalloc_oob_right+0x6c> // b.tstop 68: 94000000 bl 0 <__asan_report_store1_noabort> 6c: 52800f01 mov w1, #0x78 // #120 70: aa1303e0 mov x0, x19
其逻辑如下
总结上面的逻辑就是
可以看到__asan_report_store1_noabort
是一个函数调用,根据堆栈其下一步调用了kasan_report
我们反汇编__asan_report_store1_noabort
看看,如下
crash> dis __asan_report_load8_noabort 0xffffffd008656aa4 <__asan_report_load8_noabort>: stp x29, x30, [sp,#-16]! 0xffffffd008656aa8 <__asan_report_load8_noabort+4>: adrp x1, 0xffffffd00e994000 0xffffffd008656aac <__asan_report_load8_noabort+8>: mov x3, #0xffffffffffffffff // #-1 0xffffffd008656ab0 <__asan_report_load8_noabort+12>: hint #0x7 0xffffffd008656ab4 <__asan_report_load8_noabort+16>: mov x29, sp 0xffffffd008656ab8 <__asan_report_load8_noabort+20>: ldr x1, [x1,#2064] 0xffffffd008656abc <__asan_report_load8_noabort+24>: lsl x3, x3, x1 0xffffffd008656ac0 <__asan_report_load8_noabort+28>: tbz x30, #55, 0xffffffd008656adc <__asan_report_load8_noabort+56> 0xffffffd008656ac4 <__asan_report_load8_noabort+32>: orr x3, x30, x3 0xffffffd008656ac8 <__asan_report_load8_noabort+36>: mov w2, #0x0 // #0 0xffffffd008656acc <__asan_report_load8_noabort+40>: mov x1, #0x8 // #8 0xffffffd008656ad0 <__asan_report_load8_noabort+44>: bl 0xffffffd0086554e0 <kasan_report> 0xffffffd008656ad4 <__asan_report_load8_noabort+48>: ldp x29, x30, [sp],#16 0xffffffd008656ad8 <__asan_report_load8_noabort+52>: ret 0xffffffd008656adc <__asan_report_load8_noabort+56>: and x3, x3, #0x7fffffffffffff 0xffffffd008656ae0 <__asan_report_load8_noabort+60>: mov w2, #0x0 // #0 0xffffffd008656ae4 <__asan_report_load8_noabort+64>: bic x3, x30, x3 0xffffffd008656ae8 <__asan_report_load8_noabort+68>: mov x1, #0x8 // #8 0xffffffd008656aec <__asan_report_load8_noabort+72>: bl 0xffffffd0086554e0 <kasan_report> 0xffffffd008656af0 <__asan_report_load8_noabort+76>: ldp x29, x30, [sp],#16 0xffffffd008656af4 <__asan_report_load8_noabort+80>: ret
可以看到,这里符合调用约定,所以这是一个标准函数,我们可以翻阅代码查看更加轻松,代码如下
#define DEFINE_ASAN_REPORT_LOAD(size) \ void __asan_report_load##size##_noabort(unsigned long addr) \ { \ kasan_report(addr, size, false, _RET_IP_); \ } \
可以看到其直接调用了kasan_report
符合正常堆栈逻辑了。
本文通过反汇编的方式理解了kasan是如何通过inline的方式检测oob错误的,其主要流程概要如下
inline的检测逻辑清楚了,接下来我们探究一下outline的工作模式