首页 > C/C++, Memcached, 软件工程 > memcached里面一段神奇,危险,暂且无bug的code: add_iov

memcached里面一段神奇,危险,暂且无bug的code: add_iov

2014年10月7日 发表评论 阅读评论 12783次阅读    

翻memcached代码,看到一个函数:add_iov , 在里面着实纳闷了许久,多次认为这个代码会有问题,于是打日志,gdb上去调试,最后不得不承认: 这代码能work!(不过很危险)

下面来瞅瞅这块code, add_iov 用来拼接返回给客户端的数据结构,比如get aaa 指令,服务器需要拼接这样的返回结果:

VALUE aaaa 0 5
12345
END

调用方式可能是这样的:

                  if (add_iov(c, "VALUE ", 6) != 0 ||
                      add_iov(c, ITEM_key(it), it->nkey) != 0 ||
                      add_iov(c, ITEM_suffix(it), it->nsuffix + it->nbytes) != 0)
                      {//第一行: VALUE aaaa 0 5
                          item_remove(it);
                          break;
                      }

啊也就是可以一段一段的进行拼接。下面来看一下其代码。

/*
 * Adds data to the list of pending data that will be written out to a
 * connection.
 *
 * Returns 0 on success, -1 on out-of-memory.
 */

static int add_iov(conn *c, const void *buf, int len) {
    struct msghdr *m;
    int leftover;
    bool limit_to_mtu;

    assert(c != NULL);

    do {//这个循环是因为一个UDP数据包最大只能是UDP_MAX_PAYLOAD_SIZE, 1400, 所以得拆包。TCP也顺带做了
        m = &c->msglist[c->msgused - 1];

        /*
         * Limit UDP packets, and the first payloads of TCP replies, to
         * UDP_MAX_PAYLOAD_SIZE bytes.
         */
        limit_to_mtu = IS_UDP(c->transport) || (1 == c->msgused);

        /* We may need to start a new msghdr if this one is full. */
        //这里是说,一块消息头所指向的iov数组项太多了,或者本块
        if (m->msg_iovlen == IOV_MAX ||
            (limit_to_mtu && c->msgbytes >= UDP_MAX_PAYLOAD_SIZE)) {
            add_msghdr(c);//里面会增加c->msgused++;不用担心iov数目是否足够,至少保证有1个的
            //是吗?这个函数一定有个前提就是iov数组足够??至少有一个空的?不信。 ps: http://chenzhenianqing.cn/articles/1251.html
            m = &c->msglist[c->msgused - 1];//msgused - 1 就是刚才新增加的那个头
        }

        if (ensure_iov_space(c) != 0)//这里安全么,add_msghdr依赖这里来确保空间
            return -1;

        //一个msghdr最多能容纳1400个字节的数据,多了就只能拆了
        /* If the fragment is too big to fit in the datagram, split it up */
        if (limit_to_mtu && len + c->msgbytes > UDP_MAX_PAYLOAD_SIZE) {//如果数据包太大,就需要拆包,分多次发送
            leftover = len + c->msgbytes - UDP_MAX_PAYLOAD_SIZE;
            len -= leftover;
        } else {
            leftover = 0;
        }

        m = &c->msglist[c->msgused - 1];
        m->msg_iov[m->msg_iovlen].iov_base = (void *)buf;//指向数据部分,这个数据不需要释放,因为上层可能传递常量
        m->msg_iov[m->msg_iovlen].iov_len = len;

        c->msgbytes += len;
        c->iovused++;//iov数组增加
        m->msg_iovlen++;//这个头所代表的iov数组的数目,也增加,这样内核可以一块整个使用

        buf = ((char *)buf) + len;//拆分数据
        len = leftover;
    } while (leftover > 0);

    return 0;
}

啊然后里面关于:add_msghdr这个函数,是用来在必要的时候,比如一个msghdr结构存储了太多数据,struct iovec 结构的话(默认大于400个,IOV_MAX宏), 或者总数据量大于UDP_MAX_PAYLOAD_SIZE(默认1400字节)的时候,会调用add_msghdr新增加一个头部结构:

static int add_msghdr(conn *c)
{//发送的数据结构, msghdr里面指向iov结构,后者就是data部分
    struct msghdr *msg;
    assert(c != NULL);
    if (c->msgsize == c->msgused) {//描述头不够了,扩容一倍
        msg = realloc(c->msglist, c->msgsize * 2 * sizeof(struct msghdr));
        if (! msg) {
            STATS_LOCK();
            stats.malloc_fails++;
            STATS_UNLOCK();
            return -1;
        }
        c->msglist = msg;
        c->msgsize *= 2;
    }

    msg = c->msglist + c->msgused;//msgused位置,就是可以使用的空闲位置

    /* this wipes msg_iovlen, msg_control, msg_controllen, and
       msg_flags, the last 3 of which aren't defined on solaris: */
    memset(msg, 0, sizeof(struct msghdr));

    msg->msg_iov = &c->iov[c->iovused];//不用担心数目是否足够,因为上层保证至少iov有一个空闲的? 后续详细看看,是吗?
    c->msgbytes = 0;//这个的意思是,本连接,当前最新的msghdr所包含的数据多少
    c->msgused++;//指向下一个结构
    return 0;
}

后面的ensure_iov_space函数是用来给c->iov数组扩容的,这个数组是给c->msglist来使用的。看看其代码:

static int ensure_iov_space(conn *c) {
    assert(c != NULL);

    if (c->iovused >= c->iovsize) {//全部用完了,只能重新申请一个了。指数上升
        int i, iovnum;
        //默认iovsize 为IOV_LIST_INITIAL, 也就是400
        struct iovec *new_iov = (struct iovec *)realloc(c->iov,
                                (c->iovsize * 2) * sizeof(struct iovec));
        if (! new_iov) {
            STATS_LOCK();
            stats.malloc_fails++;
            STATS_UNLOCK();
            return -1;
        }
        c->iov = new_iov;
        c->iovsize *= 2;
        printf("ensure_iov_space, newsize:%d\n", c->iovsize);

        /* Point all the msghdr structures at the new list. */
        for (i = 0, iovnum = 0; i < c->msgused; i++) {
            c->msglist[i].msg_iov = &c->iov[iovnum];
            iovnum += c->msglist[i].msg_iovlen;//msg_iovlen为这个header的msg_iov所指向的iov的数目,所以不是同步跳动的,因此这里不可回跳
        }
    }

    return 0;
}

代码都在上面了。然而, 看的时候感觉, add_msghdr函数在赋值msg->msg_iov字段的时候,msg->msg_iov = &c->iov[c->iovused]; 语句没有检查c->iov数组是否足够,就直接访问c->iovused 索引位置上的数据,没有检查c->iovused 是否小于c->iovsize. 这样会不会导致core?

初步想,肯定不会了,估计上层做了判断保证的。好吧,这是初步,那就确认一下。add_iov函数在调用add_msghdr之前,也没有看检查iovsize呀,并且其里面还不断增加了c->iovused++;只有ensure_iov_space 函数会检查c->iovused >= c->iovsize 是否为true,如果已经越过了,就重新realloc新数组。但这是在调用add_msghdr之后的事情,add_msghdr里面就去取数组项了。

看起来似乎还有是有问题。于是手动修改了UDP_MAX_PAYLOAD_SIZE为6个字符,这样好手动命令行调试。

set aaaaaaaaaaaaa 0 0 10
1234567890
STORED
get aaaaaaaaaaaaa
VALUE aaaaaaaaaaaaa 0 10
1234567890
END

gdb调试了看,没有如期的在add_msghdr里面访问不该访问的数组成员。原来是因为add_iov里面调用add_msghdr的if语句没有进去,之所以这样,是这坨代码对于TCP协议,只有在第一个msghdr结构才去检查c->msgbytes >= UDP_MAX_PAYLOAD_SIZE, 也就是说limit_to_mtu后来为false了,所以不会再调用add_msghdr, 逃过一劫。

那就试着改一下IOV_MAX,改小一点为1,这样复现更容易。还是上面的测试命令。发过去后,发现确实在set aaaaaaaaaaaaa后,add_msghdr里面果然在c->iovsize == 1 的时候,访问了c->iov[1] 的地址, 不过只是访问&c->iov[c->iovused]有么有, 这样不会有事的。但是这不是把它赋值到了msg->msg_iov嘛,待会就一定会访问的。回头看add_iov 函数,果然后面就去设置数据缓冲区了:

        m = &c->msglist[c->msgused - 1];
        m->msg_iov[m->msg_iovlen].iov_base = (void *)buf;//指向数据部分,这个数据不需要释放,因为上层可能传递常量
        m->msg_iov[m->msg_iovlen].iov_len = len;

看起来还是有问题,会挂的。只是出现的概率非常小而已。再gdb设置特殊测试数据看看。发现还是没有在写数据的时候挂。原来是因为虽然在add_msghdr里面取了不应该取的c->iov[1] 号地址,赋值给了msg->msg_iov这样似乎埋下了很大的安全隐患,但是,add_msghdr函数调用后面,接着调用了ensure_iov_space, 而后者干了一件非常英明,但却很危险的事情: 在检测到c->iovused >= c->iovsize后,重新分配c->iov数组,并且调整c->msglist数组的指向,指向到新数组。也就是说,会重新调整所有的c->msglist[i].msg_iov , 包括上面指向非法地址的msg->msg_iov 这个指针,也是当前的最后一个指针,指向新扩容的c->iov[c->iovused]位置。

这样在外层使用的时候,之前错误的m = &c->msglist[c->msgused - 1]->msg_iov 指针,在ensure_iov_space里面又修改正确了!!!所以就不会出现core了。

多么神奇的代码!所以如下代码2个if中间,是决不允许访问m->msg_iov 的,否则按说一定会挂掉。测试一下,在中间加了一次访问,看看会不会挂掉:

        if (m->msg_iovlen == IOV_MAX ||
            (limit_to_mtu && c->msgbytes >= UDP_MAX_PAYLOAD_SIZE)) {
            add_msghdr(c);//里面会增加c->msgused++;不用担心iov数目是否足够,至少保证有1个的
            //是吗?这个函数一定有个前提就是iov数组足够??至少有一个空的?不信 ps: http://chenzhenianqing.cn/articles/1251.html
            m = &c->msglist[c->msgused - 1];//msgused - 1 就是刚才新增加的那个头
        }
        //test
        m->msg_iov[m->msg_iovlen].iov_len = len;
        if (ensure_iov_space(c) != 0)//这里安全么,add_msghdr依赖这里来确保空间
            return -1;

用之前的测试命令测试,果然挂掉了:

20180 已放弃               (吐核)./bin/memcached -p 11111 -l 192.168.1.130

到这里算是彻底清晰了,这里在add_msghdr没有检查数组下标,而去取值了,但是却没有写入数据,所以没错;然后在接下来,又对数组扩容,并且修改了之前错误的指向,因此避免了问题。只是在这块代码直接,后续千万不能增加写语句,否则会莫名其妙的挂掉。总结一句就是:我没有检查数组下标,但在访问数据之前,我正好修复了它,所以没有出错。

多么神奇,但能work的代码····哭笑不得啊

Share
  1. 本文目前尚无任何评论.
  1. 本文目前尚无任何 trackbacks 和 pingbacks.

注意: 评论者允许使用'@user空格'的方式将自己的评论通知另外评论者。例如, ABC是本文的评论者之一,则使用'@ABC '(不包括单引号)将会自动将您的评论发送给ABC。使用'@all ',将会将评论发送给之前所有其它评论者。请务必注意user必须和评论者名相匹配(大小写一致)。