lua 笔记:lua pack 库引起的 coredump
最近工作上,遇到一个 coredump,是一个对 lua 表进行序列化和反序列化的工具,也就是 github 上面这个:
https://github.com/connectFree/lev/blob/63e523dcaf329256aa5110ebab6f8de7ef8504c3/src/lev_mpack.c 。
序列化一个循环引用的表,导致 coredump 的,挖掘下来,实际上是可以分为两个问题的。
问题1:循环引用导致无限递归
序列化遇到 table 就递归处理其 key/value,没有循环检测,也没有递归深度上限。循环引用的时候就会出现死循环,最终 coredump。
mpwbuf_pack_table 并没有检测循环引用,里面直接调用 mpwbuf_pack_anytype ,而 mpwbuf_pack_anytype 遇到 table 类型又直接调用 mpwbuf_pack_table:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
static size_t mpwbuf_pack_table( mpwbuf_t *b, lua_State *L, int index ) {
...
/*try array first, and then map.*/
if(l>0){
unsigned char topbyte;
/* array!(ignore map part.) 0x90|n , 0xdc+2byte, 0xdd+4byte */
...
} else {
/* map! */
....
while( lua_next(L,index)){
wl += mpwbuf_pack_anytype(b,L,nstack+1); /* -2:key */
wl += mpwbuf_pack_anytype(b,L,nstack+2); /* -1:value */
lua_pop(L,1); /* remove value and keep key for next iteration */
}
}
return wl;
}
static size_t mpwbuf_pack_anytype( mpwbuf_t *b, lua_State *L, int index ) {
int t = lua_type(L,index);
switch(t){
...
case LUA_TTABLE:
return mpwbuf_pack_table(&g_mpwbuf, L, index );
....
default:
b->err |= ERRORBIT_TYPE_UNKNOWN;
break;
}
return 0;
}
那么像这样的循环引用就会无限循环的递归了:
1
2
3
local t = {}
t.self = t
xxx.pack(t)
解决办法
可以简单的增加 depth 变量,超过上限就退出循环并打印 error,这个库是固定缓冲区的,可以直接抛错,不怕内存泄漏:
1
2
3
4
5
if (depth >= MAX_P_DEPTH) {
lua_checkstack( L, 3 );
luaL_error( L, "pack error, reach MAX_P_DEPTH (%d)", MAX_P_DEPTH );
return 0;
}
也可以增加循环引用的判断,但会增加额外的运行负担。
问题2:Lua 栈空间不足导致越界
处理了问题1之后,比如将深度设为 500 发现还是可能会 coredump,原因是这个库没有调用 lua_checkstack 保证 lua 函数栈有足够的空间来 lua_push 数据。mpwbuf_pack_table 里面再递归调用 mpwbuf_pack_anytype 的时候,lua 的函数栈就不断的压数据,但没通过 lua_checkstack 去增大栈空间,结果内存写越界了。
lua 的函数调用栈,默认情况下 size 是 20,上限很高,但需要调用者自行调用 lua_checkstack 来保证足够的空间,虽然调用 lua_pushxxx 之类的函数往栈里压数据的时候,lua 是会调用 api_check 这个内部函数去检查栈大小,但默认情况下是不会产生 assert 的,除非编译的时候手动把 LUA_USE_APICHECK 这个 define 打开。
解决办法
在有需要 lua_pushxxx 的地方,按需要执行 lua_checkstack 确保栈空间足够,比如下面还有 3 个lua_pushxx 的时候,那么在 push 之前先执行:
1
lua_checkstack( L, 3 );
lua_checkstack(L, 3) 的意思是:检查并尝试确保当前 Lua 栈还能额外容纳至少 3 个值。
- 空间够时直接成功;不够时尝试扩容。
- 它不会压入任何值,
lua_gettop(L)不变。 官方说明
例如栈上已有 10 个值,调用成功后仍然是 10 个值,但有空间再压入至少 3 个。