ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Linux内核KASAN从原理到实战:精准捕获内存越界与释放后使用

Linux内核KASAN从原理到实战:精准捕获内存越界与释放后使用 如果你在内核开发这条路上待过几年大概率经历过这样的场景新写的驱动在测试环境跑得好好的一旦上到生产负载不到半天系统就随机重启或是某个文件系统在极端压力下出现数据损坏但dmesg里干干净净什么异常都没有。这种问题最难的地方不是修而是找不到它在哪。用户态程序崩了会给你core dump内核如果内存被写坏往往要等它破坏到某个关键链表、某个锁、或者伙伴系统的元数据时系统才轰然倒下这时候离案发现场可能已经过去了数百万条指令甚至几个小时。linux KASANKernel Address Sanitizer就是为解决这类问题而生的。它是从用户态AddressSanitizer移植到内核的运行时内存错误检测工具能够在内存访问发生的瞬间抓住越界访问、释放后使用use-after-free、双重释放、栈溢出等典型内存错误并打印出完整的调用栈和对象生命周期信息。这篇内容不需要你有很深的内核背景只要你写过驱动、调试过内核模块、或者被内存问题折磨过就能跟着实操起来把KASAN变成你日常调试的标配工具。1. 为什么说内核内存错误是开发者的噩梦1.1 从一次半夜复现的内核崩溃说起我想先从一次真实经历说起。多年前我在调试一个网络驱动的并发问题现象是系统偶尔在深夜流量高峰时panic看门狗把日志dump出来寄存器现场指向完全无关的地址栈也被破坏得面目全非。我们用了一周时间加打印、开内核配置、关CPU调频都没能抓到第一现场。后来偶尔在邮件列表里看到有人推荐KASAN于是重新编译了内核同一台机器跑了一个晚上第二天早上dmesg里醒目的红字等着我BUG: KASAN: use-after-free in xxx_free_netdev0xXX/0xXX。分配栈、释放栈、当前访问点三份调用栈清清楚楚地摆在那里问题十分钟就定位了。这个经历让我对内核内存调试的看法彻底改变。内核崩溃和用户态程序崩溃本质上的不同在于用户态程序的内存错误通常只影响自己最多产生一个segfault内核的内存错误则可能破坏系统中的任何数据结构从页表到文件系统元数据从网络协议栈到调度器队列最终症状五花八门而且往往与根因毫无关联。更糟的是像越界写一个字节这种错误数据落点可能在另一个完全无关对象里等你发现的时候那个对象早就被用了一百遍原始污染源根本无从追查。用个生活化的类比用户态出错像楼上住户往楼下泼水楼下马上能感觉到内核内存出错像水管埋在墙里漏水等到墙面开裂、地板变形你才意识到出了问题这时候要找出是哪个接头漏的难度完全不在一个量级。1.2 KASAN到底在解决什么问题KASAN做的事情简单说就是给每一次内存访问加一道“安检”。它在编译阶段由编译器在每次内存读写的指令前插入检查代码在运行时判断这次访问是否越界、是否触碰了已经释放的内存、是否访问了不允许访问的红区redzone。一旦发现异常立即停止执行打印一份结构化报告而不是等着系统慢慢被破坏后崩溃。它能检测的问题类型非常明确包括但不限于堆内存越界读写slab-out-of-bounds释放后使用use-after-free双重释放double free栈变量的越界访问和栈溢出全局变量的越界访问部分内核对象的生命周期管理错误KASAN不是万能的它属于动态检测工具意味着只有在你的代码路径真正执行到错误访问时它才会触发但它一旦触发提供的信息量是其他手段无法比拟的。这也决定了它的典型用法在开发、测试、CI阶段持续跑KASAN内核让内存错误尽早暴露而不是等到生产环境炸了再追悔莫及。2. KASAN的底层原理影子内存与编译器插桩2.1 影子内存的本质给每个内存地址做记账KASAN的核心是影子内存shadow memory我习惯叫它“地址记账本”。原理不复杂操作系统把一块专门的内存区域留出来用来记录另一块内存区域的可访问状态。问题是怎么设计这套映射关系才能用尽量小的代价覆盖尽量多的地址空间。KASAN采用的方案是1/8映射。什么意思呢就是每8字节的真实内存用1个字节的影子内存来记录状态。在x86_64架构上如果某个内存地址是addr那么它的影子地址可以通过简单位运算得到shadow_addr (addr 3) KASAN_SHADOW_OFFSET。右移3位就是因为8字节对应1字节。举个例子一个地址0xffff888012345678右移三位后变成0x1fff11102468acf加上一个编译时的偏移常量就能定位到它对应的影子字节。这个影子字节的值含义很直接值为0表示对应的8个字节全部可正常访问。值为1到7表示前N个字节可访问剩下的不可访问。值为负数表示这8个字节整体不可访问可能是已经释放的内存也可能是红区或者未初始化的区域。有了这套机制编译器插入的检查代码逻辑就变得非常高效。访问一个地址时先算出影子地址读一个字节看看该字节描述的可访问范围和本次访问的字节数是否匹配。如果访问的偏移量超过了影子字节允许的范围就判定为非法访问立刻调用报告函数。2.2 红区Redzone让越界行为立刻暴露光有影子内存还不够。假如你分配了一个64字节的对象使用范围就是[0, 64)如果你越界写到了第70个字节而这个地址恰好落在另一个合法对象的起始区域那么影子内存会认为这次访问是合法的问题就被放过了。为了堵住这个漏洞KASAN引入了红区redzone机制。所谓红区就是在每个合法对象的前后故意添加一段特殊标记的内存区域。这段话值得仔细理解分配器在返回给你一个kmalloc对象时实际在真实数据区前后都留了冗余空间这些冗余空间在影子内存中被标记为不可访问。如果你写越界越过1字节就会立刻踩到后面的红区被影子内存抓住。同理如果你读越界读过了头也会撞上红区。在slub分配器里红区是KASAN启用时自动配置的不需要你手动处理。栈变量和全局变量一样也有红区编译器在函数栈帧里为每个局部数组变量周围填充redzone字节并标记影子状态全局变量同样会被编译器在两侧填充被标记为不可访问的区域。有了红区KASAN的检测精度就从“8字节粒度”提升到了真正的字节级能够精确报告“越界了12字节”还是“越界了0字节到右侧红区”。这一点在实际排查中的作用非常大后面讲报告解读时会看到。2.3 编译器如何插入检查KASAN的检查逻辑并不是手写在内核里的而是依赖于编译器的Sanitizer能力。内核编译时如果打开了CONFIG_KASAN编译器的-fsanitizekernel-address选项会被加到内核的所有C文件编译参数中。编译器会分析每个内存访问指令自动在合适的位置插入对__asan_load1、__asan_store8这类内部函数的调用或者把它们内联展开成一段紧凑的影子检查代码。这里有一个编译选项值得了解CONFIG_KASAN_INLINE和CONFIG_KASAN_OUTLINE。前者把检查逻辑内联到每次访问点代码膨胀更明显但性能相对好后者把检查收敛成函数调用代码体积更小但每个访问多一次调用开销。对于调试用途两者差别不大如果你的内核镜像体积有严格要求可以选OUTLINE。编译器插桩有一个天然盲区它只对编译器看到的C代码生效。那些用汇编手写的内存访问、内联汇编块里的读写、以及某些侵入性很强的指针操作可能在插桩范围之外。理解这一点很重要后面排查时会避免被误导。3. Generic、SW_TAGS、HW_TAGS三种模式的区别与选型3.1 三种模式横向对比KASAN并不是只有一种实现。Linux内核里根据架构和检测思路发展出三种不同模式很多人在menuconfig里看到Generic KASAN、SW_TAGS KASAN、HW_TAGS KASAN的时候会一头雾水这里把它们放在一起对比看就清楚了。对比项Generic KASANSW_TAGS KASANHW_TAGS KASAN支持架构x86_64、arm64等arm64arm64实现方式影子内存编译器插桩红区软件内存标签模拟ARM MTE硬件内存标签内存开销约1/8影子内存红区冗余较小不需要红区很小依赖硬件CPU开销很大通常2-5倍中等很低检测粒度字节级借助红区对象级标签匹配16字节粒度生效条件无特殊硬件要求arm64无需MTE需要ARMv8.5-A及MTE硬件Generic KASAN是最早也是覆盖最全的模式代码里的每一次内存访问都在监控之下红区机制让它能精确到字节。代价是内存和性能开销都很高所以通常只在调试专用内核里开启。SW_TAGS KASAN的思路则完全不同。它借鉴了ARM MTEMemory Tagging Extension的标签思想但不依赖硬件而是通过编译器给每个内存对象分配一个随机标签tag并把这个标签用影子内存记录下来。每次访问时编译器生成的代码会比较指针自带的标签和影子内存里记录的标签是否一致不一致就说明访问了已经释放的对象或者不属于当前对象的区域。这种方式不需要红区也不需要逐个字节做范围检查代价自然低不少。它的检测能力也有侧重点对释放后使用非常敏感但对“同一个对象内部的越界”无能为力——因为标签只在对象边界变化。HW_TAGS KASAN是三者里最“先进”的它把标签比较下沉到ARM MTE硬件指令里CPU在每次内存访问的硬件层面直接完成标签校验开销几乎可以忽略。这也让它成为唯一有可能在生产环境长期开启的KASAN模式。不过它需要特定硬件支持且内核版本和工具链要求都比较新。3.2 我应该用哪一种选型这个问题我自己经验可以总结成几句话。如果你在x86_64环境下做开发调试或者跑QEMU虚拟机验证直接选Generic KASAN不要犹豫。它是功能最完整、报告信息最丰富的模式兼容性也最好几乎所有GCC和Clang版本都能编译。唯一要记住的是它很慢不要在开KASAN的内核上跑性能基准测试。如果目标平台是arm64而且你想在内存受限或性能敏感的环境中长时间跑测试先考虑SW_TAGS KASAN它不需要MTE硬件支持面广性能损耗比Generic低一个档次代价是部分越界问题可能漏报。如果手里已经有支持MTE的ARMv8.5-A硬件那HW_TAGS KASAN是最优选择它可以跑在接近生产环境的负载下同时保持不错的内存错误捕获能力。有一点要特别注意同一种KASAN模式下CONFIG_KASAN_GENERIC、CONFIG_KASAN_SW_TAGS、CONFIG_KASAN_HW_TAGS三个Kconfig选项是互斥的按需选一个就行。内核编译完可以通过/sys/kernel/debug/kasan或者启动日志确认实际生效的模式。4. 内核编译全流程从配置到跑起来4.1 配置内核的完整步骤开启KASAN最重要的一步是正确配置内核我用x86_64和QEMU环境举例流程在其它架构上基本一样。第一步确认你的GCC版本足够新。GCC 4.9.3之后的版本才支持-fsanitizekernel-address选项现代发行版自带的GCC基本都满足。如果你用Clang同样支持但需要确认目标架构下KASAN的兼容性。第二步进入内核配置界面。以Linux 6.x内核为例路径是Kernel hacking - Memory Debugging - KASAN: runtime memory debugger或者在源码目录执行make menuconfig后用/搜索KASAN直接跳转到相关配置项。我习惯直接编辑.config这样更可控。默认的defconfig不会开启KASAN需要手动添加或修改以下关键配置# 核心开关 CONFIG_KASANy # 选一种模式互斥 CONFIG_KASAN_GENERICy # CONFIG_KASAN_SW_TAGSy # CONFIG_KASAN_HW_TAGSy # 建议开启栈变量越界检测 CONFIG_KASAN_STACKy # 检查粒度与代码膨胀的取舍调试用INLINE更常见 CONFIG_KASAN_INLINEy # CONFIG_KASAN_OUTLINEy如果你不是从头配内核可以考虑这种方式先make defconfig然后把希望开启的选项追加到.config里最后执行make olddefconfig让依赖关系自动处理。KASAN对slub分配器有硬性依赖CONFIG_SLUB是默认就有的不用担心。第三步编译内核。这一步比普通内核慢不少原因很简单KASAN插桩会让代码体积增长编译器工作量也更大。我在8核机器上编一个普通内核大概需要十几分钟开KASAN后可能接近半小时要有心理准备。make -j$(nproc)编译完成后确认arch/x86/boot/bzImage生成成功然后把它搬到一个方便启动的位置。4.2 启动参数与常见开关KASAN运行时的行为也可以通过内核启动参数控制这几个参数在调试时很常用。默认情况下KASAN在捕获到第一个内存错误之后就会自我禁用理由是害怕在异常状态中继续运行会产生大量级联报告干扰判断。如果你希望一次运行抓到尽可能多的错误加这个参数kasan_multi_shot另一个有用的参数是kasan.fault它可以控制KASAN发现错误后的行为kasan.faultreport只报告错误继续执行默认行为。kasan.faultpanic遇到错误立即panic适合需要第一时间停机抓现场的自动化测试环境。在CI环境里我通常同时加上panic_on_warn1和kasan.faultpanic保证任何KASAN报告都会立刻转化为一个可被监控系统捕获的panic而不是让测试继续跑下去污染结果。用来调试的资料CONFIG_KASAN开启后内核日志中会打印一行类似下面这样的信息看到它说明KASAN已经正常初始化Memory state around the buggy address: KernelAddressSanitizer initialized (syzkaller) or:如果是基于systemd的现代发行版可以在/proc/cmdline里确认参数是否生效。4.3 验证KASAN确实生效有时候配置了半天你不确定KASAN是不是真的在起作用。这里分享两个我常用的验证手段。第一个方法看启动日志。KASAN初始化成功时dmesg里会有类似KernelAddressSanitizer initialized的输出不同内核版本措辞略有差异搜索kasan关键字基本能找到。第二个方法是写一个故意越界的测试模块这是最稳妥的验证方式。比如写一个非常简单的模块在init函数里申请一块内存然后故意往地址末尾之后写几个字节。这个模块加载后如果KASAN工作正常立刻会在dmesg里抛出一份slab-out-of-bounds报告。测试完卸载模块即可代码就几行不复杂。验证用的驱动源码通常不需要纳入正式代码库我一般放在tools/testing/selftests类似的临时目录或者干脆在QEMU虚拟机里验证。下面是一段极简的触发代码思路static int __init kasandemo_init(void) { char *p kmalloc(16, GFP_KERNEL); if (!p) return -ENOMEM; p[16] 1; /* 越界写 */ kfree(p); return 0; }如果KASAN没有报告先回头检查内核配置是否真的编进去了。常见的原因是CONFIG_KASAN被某个架构依赖限制住了或者你改完.config后忘了执行make olddefconfig导致配置项被重置。5. 实例拆解一条KASAN报告的完整排查链路5.1 use-after-free报告逐行解读KASAN报告的信息密度很高很多新手看到一屏英文就头大其实每一行都有固定的含义。下面我用一份典型的use-after-free报告作为例子逐行拆解。 BUG: KASAN: use-after-free in mydriver_foo0x1c/0x40 [mydriver] Write of size 4 at addr ffff0000c0582c00 by task kworker/u8:2/85 CPU: 2 PID: 85 Comm: kworker/u8:2 Tainted: P Hardware name: QEMU Standard PC (i440FX PIIX, 1996) Call trace: dump_backtrace0x0/0x340 show_stack0x18/0x2c dump_stack_lvl0x60/0x90 print_address_description0x54/0x3a0 kasan_report0x1a0/0x2d0 __asan_store4_noabort0x28/0x38 mydriver_foo0x1c/0x40 [mydriver] mydriver_work_handler0x84/0x120 [mydriver] process_one_work0x1d8/0x700 worker_thread0x2c4/0x480 kthread0xf8/0x120 ret_from_fork0x10/0x20 Allocated by task 100: kmalloc_trace0x1f0/0x3b0 mydriver_init0x38/0x80 [mydriver] mydriver_probe0x1c4/0x240 [mydriver] ... Freed by task 120: kfree0x1d0/0x3c0 mydriver_cleanup0x50/0xa0 [mydriver] mydriver_remove0x84/0x180 [mydriver] ... The buggy address belongs to the object at ffff0000c0582c00 which belongs to the cache kmalloc-64 of size 64 The buggy address is located 0 bytes to the right of 16-byte region [ffff0000c0582bf0, ffff0000c0582c00) 第一行是报告的灵魂BUG: KASAN: use-after-free in mydriver_foo0x1c/0x40冒号前指明了错误类型冒号后是出错时正在执行的函数名可以理解为“凶手行凶时被拍到的画面”。后面紧跟Write of size 4 at addr ...告诉你这是一次4字节的写操作不是读操作写的是哪个地址哪个任务干的。紧接着的Call trace是当前访问点的完整调用栈它回答的问题是“谁、通过什么样的路径、访问了这块内存”。但注意这份调用栈只代表访问发生的那一刻并不代表问题根因就在这个函数里——真正的问题往往藏在对象分配和释放的调用栈里。5.2 顺着调用栈定位根因前面报告的精彩之处在Allocated by task和Freed by task两段。Allocated by task 100显示这个对象是在mydriver_init里通过kmalloc_trace分配的说明出生点很明确——这是驱动初始化时创建的一个结构体。Freed by task 120显示它被mydriver_cleanup里的kfree释放了而当前访问发生在mydriver_work_handler这个异步工作队列中。看到这里问题已经非常清楚了一个对象在mydriver_init里出生在mydriver_cleanup里被释放但还有一个mydriver_work_handler工作队列在异步引用它。驱动在移除时只做了kfree却没有确保所有对该对象持有引用的工作队列完成或取消于是出现了一个悬空指针。这就是典型的生命周期管理问题。这类问题的修复方向通常有三种用引用计数kref/refcount管理对象确保最后一个引用释放时才真正kfree。移除驱动时先cancel_work_sync或flush_work把异步任务同步完再释放对象。用RCU保护对象访问在release回调里延迟释放。哪种方案更合适取决于你的代码结构但KASAN报告已经帮你把故障路径完全勾勒出来了修起来目标非常明确。报告末尾还有一段非常重要的辅助信息The buggy address belongs to the object at ffff0000c0582c00 which belongs to the cache kmalloc-64 of size 64 The buggy address is located 0 bytes to the right of 16-byte region [ffff0000c0582bf0, ffff0000c0582c00)这段说的是出问题的地址在kmalloc-64这个slab缓存里对象总大小64字节而当前访问的地址正好位于一个16字节合法区域的右侧0字节处。这里的“0 bytes to the right of”含义很关键——你访问的地址是曾经对象的一部分但对象已经被释放红区标记让这次访问暴露了。排查到这一步你通常还缺一个东西出错指令对应的C代码行号。报告里给出的是函数偏移mydriver_foo0x1c/0x40如果你编译内核时保留了调试信息可以用内核源码自带的faddr2line脚本把偏移转换成源码行号./scripts/faddr2line vmlinux mydriver_foo0x1cvmlinux是编译内核时生成的带符号文件不是压缩后的bzImage。faddr2line脚本会自动解析地址输出对应的文件和行号直接对照源码看现场即可。这是我几乎每次查KASAN报告必用的工具。5.3 第二种常见报告slab-out-of-boundsuse-after-free之外另一类高频报告是slab-out-of-bounds也就是越界访问。它与UAF的关键区别在于对象并没有被释放你访问的地址超出了它应有的范围。一份典型的越界报告会这么写BUG: KASAN: slab-out-of-bounds in hci_uart_tx_wakeup0x4c/0x... Read of size 8 at addr ffff0000c0584f00 by task kworker/u9:0/12 ... The buggy address is located 0 bytes to the right of 8-byte region [ffff0000c0584ef8, ffff0000c0584f00)看到“0 bytes to the right of 8-byte region”就知道你申请了8字节却试图读第9个字节刚好踩到红区。这种越界常见原因包括循环边界没控制好多读了一个元素。结构体大小定义错误比如驱动里手动算偏移越过成员边界。把错误类型的指针强转后访问导致实际读写范围超出原对象。这类报告相对好修因为它已经把越界方向和距离都告诉你了。“0 bytes to the right”意味着你越过尾边界1字节“N bytes to the left”则意味着你在头部之前越界了。结合kmalloc-64之类的缓存信息你也可以确认对象实际分配大小是否符合预期。如果你看到Read of size 8而分配大小是16但报告说越界到了相邻对象那么你的代码可能遍历了数组却没有检查边界。经验之谈越界报告里地址离红区越远说明你的逻辑错误越离谱通常不是一个字节的偏差而是索引号完全算错了。5.4 修复后如何回归验证找到根因并修复之后KASAN的价值还没有结束——你需要用同样的压力路径重新验证修复是否有效。我的标准流程是修完代码后在开KASAN的内核上重跑之前触发问题的负载最好能连续跑几小时或一个晚上。如果KASAN不再报告同样的错误基本可以确认修复有效。但这还不够建议再多跑几种不同负载因为生命周期类bug往往会在多个异步路径上出现类似问题你修好了一个另一个可能还在。如果修复代码涉及引用计数或RCU改动建议同时打开KCSAN数据竞争检测器跑一轮。KASAN管内存合法性KCSAN管并发访问的竞争问题两者一起用覆盖面更全面。不过KCSAN的开销也不小别指望在生产内核里常开。6. 只有长时间跑过KASAN才会知道的坑6.1 性能开销不能只看spec很多人对KASAN的第一反应是就是编译时加个选项嘛慢点就慢点。但实际用过之后你要有心理准备Generic KASAN的运行时开销绝对不只是“慢一点”。在文件系统读写、网络收包这类热路径上性能下降可能达到2倍甚至更多。原因是每次普通内存访问前都多了一次影子内存检查和潜在的分支跳转缓存和流水线都会受影响。更隐蔽的是内存占用也明显上升影子内存本身占了约1/8的物理内存映射空间而每个分配对象周围的红区还会让slab的实际内存消耗膨胀。你在一个内存只有2GB的嵌入式板子上开Generic KASAN系统可能直接OOM。所以我的建议很明确不要把KASAN当作长期开启的默认配置。它应该属于专门的“调试内核”和CI测试环境。如果你需要在生产设备上做有限度的内存错误监控宁可考虑HW_TAGS KASAN如果能满足硬件条件也别把Generic模式直接丢到生产环境。6.2 误报与盲区KASAN的误报率相比其他动态检测工具要低一些但它绝对不是零误报。我自己遇到过几种容易误判的情况这里列出来帮你少走弯路。第一种是生命周期比较“野”的对象。有些驱动或框架代码会故意从slab缓存中释放对象后马上再复用它来保存临时数据这种“释放即复用”手法在没有KASAN的时代是安全的但在KASAN眼里释放后的写入会被当成use-after-free。遇到这种报告不要急着断定代码有bug先在邮件列表或者代码注释里找找有没有类似的“释放后复用”约定。如果确实是有意为之可以考虑用kmem_cache的SLAB_TYPESAFE_BY_RCU这类机制来声明合规的重用方式让KASAN理解这个模式。第二种是汇编代码绕过了检查。前面说过KASAN依赖编译器插桩但内核里部分热路径会手写汇编这些访问不在检查范围之内。如果KASAN长时间静默而崩溃现象仍然复现别认定KASAN没用——它很可能根本没有“看”到那次访问。第三种是关于vmalloc和DMA内存的盲区。早期KASAN对vmalloc区域覆盖不完整对ioremap映射的MMIO内存更是完全不检查。别指望KASAN能帮你抓到驱动访问外设寄存器越界的问题那是设备模型和ioremap层的问题。以内核的更新节奏来看新版对vmalloc的覆盖已经好了不少但如果你的驱动大量使用vmalloc建议先确认当前内核版本对vmalloc的KASAN支持是否到位。还有一个很反直觉的现象KASAN报告出来了但不代表那个对象的内存就真的坏在了那一行。有一种情况是报告中显示的“当前访问点”其实是一个合法访问只不过它读到的地址本身已经是一个悬空指针这个悬空指针是在更早的另一个模块里被写坏的。换句话说报告是“压垮骆驼的最后一根稻草”。这时候要多看Allocated和Freed栈配合Freed栈之前的调用路径一起分析才能找到最初弄脏指针的位置。6.3 与其它调试工具共存的注意事项KASAN不是唯一的内存调试利器开发环境里还有KMSAN、KCSAN、UBSAN等工具它们之间有些能共存有些会打架。最大的冲突是KASAN和KMSAN。KMSAN用于检测未初始化内存读取它同样依赖影子内存在x86_64上无法和KASAN同时开启。如果你需要排查“读到了未初始化的堆内存”这类问题只能二选一。好在这类问题场景相对特殊多数情况下KASAN优先级更高。KCSAN与KASAN则可以共存前者检测数据竞争后者检测内存合法性。功能上正好互补。我在CI环境里经常同时开这两个跑一轮压力测试既能抓越界又能抓竞争。副作用是性能开销相乘测试时间可能需要拉长。另一个经验是开KASAN时注意和CONFIG_DEBUG_PAGEALLOC、CONFIG_SLUB_DEBUG这类工具的联动。KASAN启用后slub层面也会启用一些 debug 机制红区检测和slub自身的校验可能同时触发打印信息会互相干扰。如果我明确要排查的是内存越界一般只开KASAN不开额外干扰项让报告干净利落。遇到编译问题也要留心。老版本GCC不支持-fsanitizekernel-address编译会直接报错。新版GCC和Clang支持得都很好但如果你的工具链太老要么升级GCC要么换Clang。确定了工具链之后make olddefconfig这一步千万别跳过KASAN牵扯的内核依赖不少漏掉依赖会让配置项莫名其妙消失。最后还想提醒一点KASAN报告里的CPU和PID对多核系统定位并发问题很有帮助。同一个对象在同一时刻被两个CPU访问KASAN会分别打报告通过CPU列你可以判断是锁没锁好还是任务迁移的问题。在做并发排查时把多个报告放在一起对比往往能找到访问模式的规律。如果你准备在自己的项目里引入KASAN我最想分享的一个建议是不要只在出问题时临时开一下而是把KASAN内核放进CI或者日常开发环境里长期跑。我在做驱动开发时很多bug就是通过跑一晚上压力测试才暴露的隔天起来看到报告修复成本低得多。内核里大部分内存类bug只要被KASAN抓到过一次其实就已经定位了剩下的事只是修生命周期管理的问题。与其在线上环境反复扑空不如把KASAN当作一道日常防线让内存错误在上线之前就现形。
返回列表