编辑
2025-10-01
记录知识
0

目录

什么是INLINE模式
测试堆栈
反汇编
总结

在《KASAN(1)-简单实践》中有一个配置CONFIG_KASAN_INLINE我们没有详细解释,只是简单的根据kernel docs的说明进行了解释,这里根据实际分析来详细介绍一下kasan的inline的检测流程。

什么是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

其逻辑如下

  • 40:保存x0的值
  • 44: 将x0+123,123是kmalloc的size
  • 48: 设置x1为0xffd000000000
  • 4c: 计算x1为0xdfffffd000000000,这个值就是CONFIG_KASAN_SHADOW_OFFSET
  • 50: 将32位的w0+0x7,给到w2
  • 54: 将x0右移3位
  • 58: x3+x1就是影子区域内存地址,加载值给到w1
  • 5c: 比较值是否为0,为0代码可访问
  • 60: 如果不为0,再比较w2和w1,w2是访问的影子内存值,也就是x0+123,w1是影子的边界值,判断w2是否大于w1
  • 64: 如果小于等于,那么处于可访问位置,则正常跳转到6c处
  • 68: 如果大于,那么意味着oob发生了,跳转到函数__asan_report_store1_noabort
  • 6c: 设置w1为120
  • 70: 恢复x0

总结上面的逻辑就是

  • 判断访问的内存地址和影子区域的下毒的值,如果是0或者小于可访问值(1-7),那么代表没有发生oob,如果大于可访问值那么调用__asan_report_store1_noabort上报oob错误

可以看到__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错误的,其主要流程概要如下

  1. gcc内部对load,store进行了gcc的插桩
  2. 这个插桩会在load,store之前通过指令插入
  3. 这段指令主要判断操作的内存的影子对应区域是否可访问
  4. 如不可访问,则跳转到标准函数__asan_report_load
  5. 标准函数由内核实现,其主要跳转到kasan_report上

inline的检测逻辑清楚了,接下来我们探究一下outline的工作模式