发现net-snmp的snmp_set_var_typed_value()函数的"缺陷"

      较早前跟同事测试的时候偶然发现了net-snmp SDK中的snmp_set_var_typed_value()函数的一个"BUG"。当时的过程非常有趣(误打误撞)且具有一定的典型性(跨平台、SDK),故在此与大家分享:

      我有一位亦师亦友的同事在工作之余独立开发了一套可管理我们设备的SNMP网管软件(本公司已设有另外的团队做该类产品),让我帮忙给他测试通信服务器部分,该部分基于VC6.0开发。测试了一段时间,虽有一些小瑕疵但是功能方面大的BUG一直未出现。一切都那么正常。

      正当我们为其正确性感到高兴的时候,突然发现某些时候访问某设备会出现超时异常。起初我们都认为是服务器代码有问题,单步调试了许久,但是却没有浮现该错误,反倒是出现了另外的错误。在排除干扰后,我们怀疑还是设备本身出了问题(因为该设备是已通过评审鉴定、开始量产的设备,而且此前做过多次类似的测试都没有发现那样的问题,所以我们一开始对它抱有很强的信心,并没有怀疑它,通过这件事也告诉大家在分析问题时千万不要抱有太强的主观判断,不放过每种可能性)。于是我们立马用第三方测试软件MIB browser测试了一下,果真是设备出了问题。可问题出在哪里呢?

    错误难以重现,设备现在又处于“宕机”状态,一筹莫展,找不到什么好的对策来定位错误,只好乖乖的重启设备,一编一遍的读取设备的参数(有点多,上百个)啦,最后终于找出了引起访问异常的那个参数,同时查看设备状态,发现设备内存占用急剧上升,不多久就内存耗尽。咦!难道是内存泄露?马上查看设备代码,目测了几分钟,大家四目相对,”这么简短的几行代码哪会出现内存泄露呢?“(当时那个时候我们仍然犯了先入为主的错误,因为snmpd代理是用net-snmp的开发包开发的,net-snmp又是业界最著名的SDK,也推出了若干年,版本都更新好多次了,所以认为它的代码都是十分可靠的),没有怀疑net-snmp的代码,就只好怀疑自己的代码了,把函数接口返回的数据全部打印一遍,果然有错!---接口返回的某数组数据长度为-1。好吧,自家接口有错误,这也情有可原。

     可这数据长度为-1是如何而来的呢?调用的这个接口是去共享内存取数据,数据是从NVRAM初始化到共享内存中的,作为出厂配置存在的,而且这份配置数据是多次测试过的,Flash也没有坏块的,但是数据确确实实就是不正确。此事过了一两天,想到了原因:该设备上的flash是后来硬件工程师更换上去的开发初期使用过的一块flash,不过大家都忘记了也没有特殊标记无法区分,后面我们的配置数据有增多,而出现异常的参数就属于新增的数据,后来在升级程序时并没有擦除flash导致老数据还存在,flash上未初始化的数据区读出来就都是-1。

     不过最后我们追踪net-snmp的源码,发现源码中的snmp_set_var_typed_value()函数确实有设计缺陷,snmp_set_var_typed_value()调用了另外一个函数snmp_set_var_value(),在snmp_set_var_value()中,net-snmp只对该len做了是否等于0的检查而没有做小于等于0的检查:

if (len <= (sizeof(vars->buf) - 1)) {
        vars->val.string = (u_char *) vars->buf;
        largeval = 0;
    }

虽然使用者也有责任,但这样的设计在接口中确实是不应该出现的(net-snmp非常优秀,我不是要吐槽它,本人水平很低,只是就事论事)。

      笔者对此事颇感兴趣,就更进一步的追查,看看net-snmp源码中的哪部分导致了内存耗尽呢?通过查看源码不难发现在以下地方出现了问题:

 /** FALL THROUGH */
    case ASN_PRIV_IMPLIED_OCTET_STR:
    case ASN_OCTET_STR:
    case ASN_BIT_STR:
    case ASN_OPAQUE:
    case ASN_NSAP:
        if (largeval) {
            vars->val.string = (u_char *) malloc(vars->val_len + 1);
        }
        if (vars->val.string == NULL) {
            snmp_log(LOG_ERR,"no storage for string\n");
            return 1;
        }
        memmove(vars->val.string, value, vars->val_len);
        /*
         * Make sure the string is zero-terminated; some bits of code make
         * this assumption.  Easier to do this here than fix all these wrong
         * assumptions.  
         */
        vars->val.string[vars->val_len] = '\0';
        break;

跟踪函数执行过程,在case里,malloc和memmove都有执行,嗯,,那么有没有可能是他俩导致的内存问题吧?感觉有点像,可他们都是标准的C函数,应该不可能犯这种错误吧。malloc(0)应该不会有问题,那多半就是memmove() 在len < 0会有错咯,只好测试一下了。测试发现在主机上memmove() len < 0时会有段错误,但不会造成内存上升,在嵌入式系统中竟然可以正确执行,但是没有造成内存上升,在snmp_set_var_value中的memmove中硬编码却会出现。那是net-snmp其他地方的函数造成的么?看了一下源码,目前暂未发现这种可能性。如果有朋友知道原因,欢迎交流!

    ( 我们用的net-snmp.5.4.2.1版本,我检查了新发布的版本5.7.2,源码中有部分修改,但是该BUG还在。)


/********************************************2014-1-13 华丽的分割线**********************************************/

虽然碰巧发现了这个现象,却一直没有想通原因,后来笔者便用我那蹩脚的chenglish,去net-snmp官网上提交了这个“BUG”,期待能够得到一个解答,但是过了很久都没有回复。今天突然想起这事,就再去官网看了一下,发现我的问题得到了net-snmp首席开发者Niels Baggesen先森的回答。他很无奈的说道:

Unfortunately we cannot check the length for being negative, as the type of the length argument is size_t which is unsigned.
It is a bit difficult to find a meaningful upper limit to check against.
   “unfortunately,a bit difficult”.哈哈,说不定“老先生”对我这种基础不扎实的“程序猿”有点失望吧~

    至此我终于明白了这个错误产生的原因,我忽略了参数的数据类型,snmp_set_var_value()的函数原型是:

int
snmp_set_var_value(netsnmp_variable_list * vars,
                   const u_char * value, size_t len)

    在32位系统里size_t几乎都是unsigned int型,是无符号的,无符号数是无法比较正负的,对于snmp而言,也难以找到一个通用的大数来做上限判断是否溢出,而为了保证程序的可移植性,又不得不使用size_t类型(还真是不幸啊…)。当函数调用时给size_t形参传入一个int型实参,会自动将实参转换成形参类型。另外当程序中将len赋值给vars->val_len时编译器也做了隐式类型转换:

vars->val_len = len;
    在这个赋值语句中编译器先把等号右边的值转换成等号左边变量的类型再赋值的。无论如何,最终vars->val_len的值就成了-1的二进制码对应的无符号整数(0xffffffff)。于是后来悲剧就发生了(从硬编码测试结果来看,是vars->val_len的值太大导致程序其他地方出现了内存问题,而不是该函数中的malloc和memmove引起的,具体问题出现在哪我也没有再深究了)。


你可能感兴趣的:(SNMP)