介绍:通过增强Anolis5.10内核中kfence的功能,实现了一个在线、精准、可定制的内存调试解决方案。编者按:内核内存调试领域一直存在两大行业问题:“内存被修改”和“内存泄漏”?本文整理自龙蜥大讲堂第13期。需要哪些方案才能有效解决这两个问题?快来看看作者的详细介绍吧!一、背景长期以来,内核内存调试领域存在两大行业问题:“内存被修改”和“内存泄漏”。内存问题的行踪千奇百怪,飘忽不定。在Linux内核的调试问题中,它是最让开发者头疼的bug之一,因为内存问题往往发生在第N个站点,尤其是在生产环境中,到目前为止,还没有非常有效的精确在线调试的解决方案,导致排查困难,费时费力。接下来我们就来看看为什么“内存修改”和“内存泄漏”这两大问题很难解决。1.1内存改变Linux用户态的每个进程都有自己的虚拟内存空间,由TLB页表负责映射管理,从而实现进程间的隔离,互不干扰。但是,在内核态下,所有内核程序共享同一个内核地址空间,这就导致内核程序在分配和使用内存时要小心谨慎。出于性能的考虑,内核中的大部分内存分配行为都是直接在线性映射区分配一块内存供自己使用,分配后的具体使用行为没有任何监控和限制。线性映射区的地址只是相对于真实物理地址的线性偏移,几乎可以看作是对物理地址的直接操作,在内核态是完全公开和共享的。这意味着如果内核程序的行为不规范,可能会污染其他区域的内存。这样会造成很多问题,严重的时候会直接导致宕机。一个典型的场景例子:现在我们假设用户A已经向内存分配系统申请了地址0x00到0x0f,但这只是口头上的“君子协定”,A不必遵守。由于程序缺陷,A向隔壁的0x10写入了数据,而0x10是用户B的站点,当B试图读取他站点上的数据时,读取到了错误的数据。如果这里有值,就会出现计算错误,造成各种不可预知的后果。如果这里有一个指针,整个内核可能会直接崩溃。上面的例子称为越界,即用户A访问了一个不属于A的地址。其他内存被改变的情况还有use-after-free,invalid-free等,这些情况,可以认为是A释放了这块空间后,内核认为这块空间是空闲的,分配给B,然后A回到卡宾枪。例如,我们可以通过以下模块代码模拟各种内存修改示例://out-of-boundchar*s=kmalloc(8,GFP_KERNEL);s[8]='1';kfree(s);//使用-after-freechar*s=kmalloc(8,GFP_KERNEL);kfree(s);s[0]='1';//double-freechar*s=kmalloc(8,GFP_KERNEL);kfree(s);kfree(s);1.1.1为什么调试难在上面的例子中,宕机最终会是用户B造成的,各种日志记录和vmcore都会将矛头指向B,也就是说当宕机已经问题的第二个场景,和第一个改内存的场景有时间差。这时,A可能已经消失了。这时候内核开发人员查了半天,认为B应该不会出现这个错误,也不知道为什么B的内存会变成一个意想不到的值。他们会怀疑内存被别人篡改了,但是要找这个“别人的工作”是很辛苦的,运气好的话,可以在停机现场找到线索(比如犯人还留在附近,或者值囚犯写的很有特点),或者类似的宕机找联系的情况很多等等。不过也有运气不好的时候毫无头绪的情况(比如囚犯释放了记忆消失了),甚至很难主动复现(比如隔壁没人,修改了不相关的数据,或者修改后被所有者覆盖)等)1.1.2现有方案的局限性为了为了调试内存修改问题,Linux社区相继推出了SLUBDEBUG、KASAN、KFENCE等解决方案。但是,这些解决方案有很多局限性:SLUBDEBUG需要传递到bootcmdline并重启,这也会影响slab的性能,只能在slab场景下使用;KASAN功能强大,但也引入了较大的性能开销,因此不适合在线环境;后续基于标签的方案可以减轻开销,但依赖于Arm64的硬件特性,不具有普适性;KFENCE进步很大,可以在生产环境正常开启,但是它以抽样的方式发现问题的概率非常小,需要大规模的集群来提升概率。而且它只能检测slab相关的内存修改问题。1.2内存泄漏与内存修改相比,内存泄漏的影响更为“温和”,它会慢慢蚕食系统的内存。和大家熟知的内存泄漏一样,这是由于程序只分配了内存而忘记释放内存造成的。例如,下面的模块代码可以模拟内存泄漏:char*s;for(;;){s=kmalloc(8,GFP_KERNEL);ssleep(1);}1.2.1为什么调试难是因为用户态程序有自己独立的地址空间管理,问题可能比较容易定位(至少打开top可以看出哪个进程吃内存多);并且内核态的内存混在一起,很难定位问题的根源。开发者可能只是通过系统统计观察到某一类内存(slab/page)的占用在增加,而无法发现到底是谁在分配内存,没有释放。这是因为内核并没有记录线性映射区的分配情况,也就无从知道每一块内存的所有者是谁。1.2.2现有解决方案的局限性Linux社区在内核中引入了kmemleak机制,周期性地扫描检查内存中的值以及是否有指向已分配区域的指针。kmemleak方法不够严谨,不能部署到线上环境,误报问题多,所以定位不是很准确。此外,在用户态,阿里云自研的运维工具集sysAK也包含了内存泄漏检测。动态采集分配/释放行为,结合内存相似度检测,可以在生产环境部分场景下准确排查内存泄露问题。2.解决方案当出现内存问题时,如果vmcore没有捕捉到第一个场景,是不可能找到线索的。这个时候kernel同学的传统做法是切换到debugkernel,使用KASAN离线调试。但是线上环境复杂,有些非常隐蔽的问题,线下无法稳定复现,或者线上偶然出现。像这样棘手的问题往往只是搁置,等待下次出现,希望能提供更多线索。因此,我们看到了KFENCE本身的灵活性,对其进行了改进,使其成为在线/离线内存问题调试的灵活调整工具。最新的KFENCE技术的优点是可以灵活调整性能开销(以牺牲采样率为代价,也就是抓bug的概率),无需更改内核,重启即可开启;缺点是抓包概率太小,对于线上场景重启也比较麻烦。基于KFENCE技术的特点,我们对其功能进行了增强,并增加了一些新的设计,使其支持全监控和动态切换,适用于生产环境,已在龙蜥社区Linux5.10分支上发布。具体实现如下::可以在生产环境的内核中动态启用和禁用。关闭该功能时没有性能回退。可100%捕获slab/order-0page的越界、内存损坏、use-after-free、invasion-free故障。它可以准确捕捉问题出现的第一个场景(从这个意义上说,它可以显着加快问题的重现时间)。支持per-slabswitch以避免过多的内存和性能开销。支持slab/page内存泄漏的故障排除。对具体技术细节感兴趣的同学可以访问龙蜥社区的内核代码仓库阅读相关源码和文档(见文末链接)。2.1使用方法2.1.1启用功能(可选)配置过滤访问slab/sys/kernel/slab/
