memcached里面一段神奇,危险,暂且无bug的code: add_iov
翻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的代码····哭笑不得啊
近期评论