今天我们继续分析一个因野指针访问导致的内存异常、出现coredump问题。在上一篇案例中,我们分享了一个在内存被释放后,业务模块仍然在使用导致业务模块自身出现coredump的现象。其实,在使用野指针访问内存时还有一种可能,就是业务模块在使用野指针继续访问内存没有导致业务模块自身出现异常,但由于该块内存已经属于一块空闲内存,它被glibc的ptmalloc虚拟内存分配器管理所管理。ptmalloc分配器会在用户内存块的开头位置的前8个字节(在32位系统上)依次存放两个结构体指针fd和bk,它们分别指向空闲双向链表的前后节点,从而维护空闲链表上空闲内存块的连接(如unsorted bins)。内存块相关结构如下:
可以想象,当业务模块继续使用野指针,当改写了内存块的头两个指针位置,即fd和bk,此时fd和bk将不再指向空闲双线链表的前后内存块节点。若此时刚好该进程的其他业务模块需要分配内存,ptmalloc刚好选择了该野指针指向的内存块。ptmalloc选择该空闲内存块时会进行相关的一系列校验,包括校验该空闲内存块的fd、bk指针是否指向其前后空闲内存块(如通过fd找到后一个空闲内存块,检查后一个内存块的前向指针bk是否指向自身)。若校验不合格,则说明内存异常,ptmalloc会发送abort信号结束当前进程并产生coredump。
本次分享的案例就是基于上述原因产生的问题。该类问题的难点在于产生coredump时记录的栈回溯只是单纯地利用malloc分配内存,它并不是内存被异常改写的源头,某种意义上它也只是个“受害者”,这给我们找到内存异常被改写源头带来的较大的麻烦。下面基于实例进行讲解,其中具体代码流程进行了简化。
问题现象
测试环境在进行了将近40个小时的压力测试发现产生了一个coredump。通过初步分析,发现是利用malloc申请一个内存时,在glibc中产生了断言abort错误,从而产生了coredump。
问题分析
像这类glibc中分配内存时产生abort断言错误,通常是由于内存被改写导致。针对内存被改写,具体又有2种场景,分别如下:
- 该内存块被前一个内存块越界改写。
- 该内存块已经被释放,但仍然被业务模块改写访问,破坏了管理数据结构,从而出现异常。
针对上述两种可能,我们分别进行分析并排查。
内存越界改写
当内存块被其前一个内存块越界改写时,异常内存块的管理头结构malloc_chunk数据必将被破坏(暂不考虑跳跃式改写)。因此,为了验证这种可能,我们利用x命令打印出异常内存块的前面数据,发现内存块的管理头结构malloc_chunk数据正常(如表示内存块大小字段正常),并未发现明显改写,因此排除了这种可能。
野指针访问改写
对于野指针访问改写场景,通常是由于将申请的内存块地址保存在了全局变量指针中,而在某些业务模块通过局部变量指针接受了该全局变量指针,在一些流程中通过局部变量指针释放了该内存块,但并未将全局变量指针置为NULL。而在其他业务模块中发现该全局变量指针不为空,则继续使用甚至改写了该内存,从而出现问题。
为了验证这种可能,我们查找异常内存块指针是在哪个地方申请使用的。此时我们既可以通过打印相关内存块的特征字符串来查找,也可以通过内存块指针来查找。对于通过内存块指针来查找,我们有2种方法:
- 一种是通过打印当前进程的相关全局指针变量,查看哪个全局指针变量指向了该异常内存块即为使用内存块的位置。
- 另一种是通过find命令在当前进程的全局变量区域(即.data段)暴力查找。若找到,通过p /a形式打印该部分内存块即可显示该部分内存属于哪个全局变量。
通过上面两种方法我们可以找到异常内存块具体被哪个全局变量指针引用,这样我们跟踪该全局变量指针具体申请、释放的位置,特别重点排查通过局部变量释放了该内存、但未将全局变量指针置为NULL的情形。根据该思路我们确认了可疑处,确认存在通过局部指针释放了内存、但未将全局指针变量置为NULL,导致后续访问改写导致内存块异常。
相关示例代码如下: