最近遇到了一个问题,一个平时运行没有毛病的模块,被ASAN报heap use after free,问题出在堆内存,但是看上去全局变量没有哪里会被释放,唯一有风险的地方是在栈内存,这就比较有意思了。
局部变量的生命周期
局部变量一般分配在栈内存上,随着函数执行结束而释放:
int foo()
{
int a = 1; //入栈
return a;
}
int main()
{
foo(); //出栈
return 0;
}
如果在main中有变量接收了返回值,那么它就会在堆内存中开辟一块空间:
int foo()
{
int a = 1; //入栈
cout << &a << endl;
return a;
}
int main()
{
int a = foo(); //出栈,main的a复制foo的返回值
cout << &a << endl;
return 0;
}
可以看到两次地址是不一样的。
如果局部变量是个指针,就会导致危险行为:
const char* foo()
{
string s = "hello";
const char * s2 = s.c_str();
cout << s2 << endl;
cout << (void*)s2 << endl;
return s2;
}
int main()
{
const char* s = foo();
cout << s << endl;
cout << (void*)s << endl;
return 0;
}
main的s只是复制了foo返回的地址,而地址指向的内存是已经被释放的,也就是s变成了野指针,这在运行时会造成严重的错误,很容易被发现。
如果是STL容器呢?
typedef map<int,string> SMap;
const char* foo()
{
SMap testMap;
SMap::iterator iter;
string s = "hello";
testMap.insert(make_pair(0,s));
iter = testMap.find(0);
return iter->second.c_str();
}
int main()
{
const char* s = foo();
cout << s << endl;
return 0;
}
这里的main函数可以正常输出"hello",即使那一块内存已经被释放了,但是对系统来说,那一块内存还是在内存池里泡着,所以并没有被系统释放,通过以下代码可以验证:
typedef map<int,string> SMap;
const char* foo()
{
SMap testMap;
SMap::iterator iter;
string s = "hello";
testMap.insert(make_pair(0,s));
iter = testMap.find(0);
return iter->second.c_str();
}
int main()
{
const char* s = foo();
cout << s << endl;
SMap testMap;
testMap.insert(make_pair(1,"world"));
cout << s << endl;
return 0;
}
它的输出是:
hello
world
可以看到,在main中新建的map覆盖了foo函数中临时map的值,这是由于内存池的机制导致的,对STL来说,这一块内存是已经被释放的,它被标记为空闲,只是内容还暂时保留,所以虽然一开始的s可以输出正确的"hello",但一旦在调用它之前又创建了新的map并insert了差不多长度的内容的话(长度不等内存池可能会调用别的内存块,可以自行验证),这块内存就会被覆盖,这是非常危险的行为,但是又极其隐蔽,因为只要你调用及时,s的值是完全正确的。
总结
STL容器实现了内存池,内存是预先开辟好,而不是临时申请的,所以即使是一个局部的map,调用insert时,它创建的内容也是在堆上而非栈上,只有容器本身是在栈上,这就导致了局部变量引起heap UAF。