ldns老版本隐藏得比较深的bug

问题:

        我在nginx中fork出一个进程来专门做DNS解析的工作,在本地开发环境中一切正常,而在线上服务器环境中这个进程就不定期的死掉重启,而且还不太容易复现。

排查:

        直接在线上服务器开启coredump,让linux帮忙把进程挂掉时的core文件保存下来,再用gdb分析,后来如愿得到core文件了,用gdb调试,从调用堆栈来看,是死在glibc的_int_malloc函数中,再细看是一个指针值等于0x4,而对这个指针进行解引用将引起段错误,从而进程挂掉重启。这种问题肯定是glibc的内存管理器被整乱了,导致这个问题也只有两个原因:一是内存越界写入,二是重复释放内存。这个问题是与glibc有关,所以第二种可能比较大。关于内存问题有挺多调试工具的,比如valgrind、mudflap、efence,可惜我的这个问题用这几个工具都没找出来,后来只能用SystemTap来调试了,调试脚本就是上一篇SystemTap使用技巧【四】讲的技巧,如下:

probe begin {
    printf("=============begin============\n")
}

//记录内存分配和释放的计数关联数组
global g_mem_ref_tbl
//记录内存分配和释放的调用堆栈关联数组
global g_mem_bt_tbl

probe process("/lib/x86_64-linux-gnu/libc.so.6").function("__libc_malloc").return, process("/lib/x86_64-linux-gnu/libc.so.6").function("__libc_calloc").return {
    if (target() == pid()) {
        if (g_mem_ref_tbl[$return] == 0) {
            g_mem_ref_tbl[$return]++
            g_mem_bt_tbl[$return] = sprint_ubacktrace()
        }
    }
}

probe process("/lib/x86_64-linux-gnu/libc.so.6").function("__libc_free").call {
    if (target() == pid()) {
        g_mem_ref_tbl[$mem]--

        if (g_mem_ref_tbl[$mem] == 0) {
            if ($mem != 0) {
                //记录上次释放的调用堆栈
                g_mem_bt_tbl[$mem] = sprint_ubacktrace()
            }
        } else if (g_mem_ref_tbl[$mem] < 0 && $mem != 0) {
            //如果调用free已经失衡,那就出现了重复释放内存的问题,这里输出当前调用堆栈,以及这个地址上次释放的调用堆栈
            printf("MMMMMMMMMMMMMMMMMMMMMMMMMMMM\n")
            printf("g_mem_ref_tbl[%p]: %d\n", $mem, g_mem_ref_tbl[$mem])
            print_ubacktrace()
            printf("last free backtrace:\n%s\n", g_mem_bt_tbl[$mem])
            printf("WWWWWWWWWWWWWWWWWWWWWWWWWWWW\n")
        }
    }
}

probe end {
    //最后输出产生泄漏的内存是在哪里分配的
    printf("=============end============\n")
    foreach(mem in g_mem_ref_tbl) {
        if (g_mem_ref_tbl[mem] > 0) {
            printf("%s\n", g_mem_bt_tbl[mem])
        }
    }
}
调试得到的结果类似下图(这个图是自己写一个简单的ldns例子的调试结果):


从上面的截图中可以看出,在libldns.so.1.6.11库里面有两处重复释放了内存,还从上图标出的第一次释放内存的堆栈中看出调用路径是ldns_resolver_pop_nameserver->__libc_realloc->free,而第二次调用路径是ldns_resolver_deep_free->free,下面先看看ldns_resolver_pop_nameserver和ldns_resolver_deep_free这两个函数的实现代码:




ldns_resolver_pop_nameserver函数中第256、257行都用到了LDNS_XREALLOC,这是一个宏,就是realloc,在上面SystemTap结果的调用路径中两处从ldns_resolver_pop_nameserver调用__libc_realloc再到free的地方应该就是这两代码了,那为什么会调用到free呢,realloc代码暂且不看了,先看看这两行代码的第三个参数都是(ns_count - 1),而ns_count就是之前ldns_resolver_push_nameserver进去的nameserver个数,如果为1,那ns_count-1就为0,相当于调用realloc调整一个内存大小为0,realloc将直接把这块内存给释放掉,然后返回NULL,这些从realloc的实现代码可以得到证实。所以ldns_resolver_pop_nameserver函数的第260、262行将不会得到执行,从而保存在r中的nameservers和rtt这两个指针就变为野指针,然后在ldns_resolver_deep_free函数的第916、936行又把这两个野指针再释放一次,所以glibc的内存管理器链表就乱了,下次再申请内存时就很容易出段错误了。

问题的根本原因已经找到了,那解决办法就好办了->

修复:

因为ldns这个库是用apt-get安装的,版本为1.6.11,比较老,官网的版本已经为1.6.17了,下载最新代码对比一下,这个bug的确已经被官网修复了:


再多个版本之间diff几次发现,在1.6.14这个版本就已经修复了这个问题。所以解决办法就是更新ldns库到最新版本就可以了,如果更新不了ldns库的话,就不要调用ldns_resolver_pop_nameserver函数了,反正在ldns_resolver_deep_free函数里也会再次把内存释放掉,只是后面这种方法不能在一个ldns_resolver句柄中切换DNS nameserver了,建议还是更新ldns到最新版吧,老版本的一些其他bug可能在新版本中也解决了呢。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值