地址消毒器有时会错过释放后的堆使用
address sanitizer sometimes misses heap-use-after-free
将指针保留在已调整大小并随后取消引用的矢量元素上
是未定义的行为。
在使用 std::vector<int>
(使用 #if 0
)的以下程序上测试这种不良做法时,
地址清理器正确报告了释放后堆使用错误。
$ ./prog
capa: 8
v[0]: 0x603000000010 <1000>
p: 0x603000000010 <1000>
capa: 16
v[0]: 0x6060000000e0 <1000>
=================================================================
==23068==ERROR: AddressSanitizer: heap-use-after-free on address 0x603000000010
但是当尝试使用 std::vector<std::string>
(使用 #if 1
)进行相同的实验时,
地址清理器不报告任何内容,这会导致使用已销毁的字符串
(可能在调整大小期间移动)通过指针!
$ ./prog
capa: 8
v[0]: 0x611000000040 <1000>
p: 0x611000000040 <1000>
capa: 16
v[0]: 0x615000000080 <1000>
p: 0x611000000040 <>
我的问题:为什么地址清理程序在第二种情况下不报告错误?
编辑:valgrind 报告错误。
我在 GNU/Linux x86_64 (Archlinux) 上使用 g++ 9.2.0 和 clang++ 9.0.0 测试了以下程序。
/**
g++ -std=c++17 -o prog prog.cpp \
-pedantic -Wall -Wextra -Wconversion -Wno-sign-conversion \
-g -O0 -UNDEBUG -fsanitize=address,undefined
**/
#include <iostream>
#include <vector>
#if 1
# include <string>
inline auto make_elem(int n) { return std::to_string(n); }
#else
inline auto make_elem(int n) { return n; }
#endif
using elem_t = decltype(make_elem(0));
inline
void
fill(std::vector<elem_t> &v,
int sz)
{
v.resize(std::size_t(sz));
for(auto i=0; i<sz; ++i)
{
v[i]=make_elem(1000+i);
}
}
inline
void
show(const std::vector<elem_t> &v,
const elem_t *p)
{
std::cout << "capa: " << v.capacity() << '\n';
std::cout << "v[0]: " << &v[0] << " <" << v[0] << ">\n";
std::cout << "p: " << p << " <" << *p << ">\n"; // <-- possible invalid pointer here
}
int
main()
{
constexpr auto sz=8;
auto v=std::vector<elem_t>{};
fill(v, sz);
const auto *p=data(v);
show(v, p);
fill(v, 2*sz);
show(v, p);
return 0;
}
我也已就此提交 upstream bug。
我已经评论了 github 问题,但简短的回答是,由于 libstdc++.so.6
拆分某些常见模板实例的方式,例如
basic_ostream<...>::operator<<(basic_ostream<...>&, const std::string &);
并仅在 libstdc++.so.6
中实例化它们一次,并且因为 libstdc++.so.6
本身不是作为一个检测代码,所有检测代码可以看到的是您将一个悬空指针传递给外部函数。它不知道外部函数会用这个指针做什么,所以不能报错。
问题不会用clang++ ... -stdlib=libc++
重现(已正确报告悬空访问)。
将指针保留在已调整大小并随后取消引用的矢量元素上 是未定义的行为。
在使用 std::vector<int>
(使用 #if 0
)的以下程序上测试这种不良做法时,
地址清理器正确报告了释放后堆使用错误。
$ ./prog
capa: 8
v[0]: 0x603000000010 <1000>
p: 0x603000000010 <1000>
capa: 16
v[0]: 0x6060000000e0 <1000>
=================================================================
==23068==ERROR: AddressSanitizer: heap-use-after-free on address 0x603000000010
但是当尝试使用 std::vector<std::string>
(使用 #if 1
)进行相同的实验时,
地址清理器不报告任何内容,这会导致使用已销毁的字符串
(可能在调整大小期间移动)通过指针!
$ ./prog
capa: 8
v[0]: 0x611000000040 <1000>
p: 0x611000000040 <1000>
capa: 16
v[0]: 0x615000000080 <1000>
p: 0x611000000040 <>
我的问题:为什么地址清理程序在第二种情况下不报告错误?
编辑:valgrind 报告错误。
我在 GNU/Linux x86_64 (Archlinux) 上使用 g++ 9.2.0 和 clang++ 9.0.0 测试了以下程序。
/**
g++ -std=c++17 -o prog prog.cpp \
-pedantic -Wall -Wextra -Wconversion -Wno-sign-conversion \
-g -O0 -UNDEBUG -fsanitize=address,undefined
**/
#include <iostream>
#include <vector>
#if 1
# include <string>
inline auto make_elem(int n) { return std::to_string(n); }
#else
inline auto make_elem(int n) { return n; }
#endif
using elem_t = decltype(make_elem(0));
inline
void
fill(std::vector<elem_t> &v,
int sz)
{
v.resize(std::size_t(sz));
for(auto i=0; i<sz; ++i)
{
v[i]=make_elem(1000+i);
}
}
inline
void
show(const std::vector<elem_t> &v,
const elem_t *p)
{
std::cout << "capa: " << v.capacity() << '\n';
std::cout << "v[0]: " << &v[0] << " <" << v[0] << ">\n";
std::cout << "p: " << p << " <" << *p << ">\n"; // <-- possible invalid pointer here
}
int
main()
{
constexpr auto sz=8;
auto v=std::vector<elem_t>{};
fill(v, sz);
const auto *p=data(v);
show(v, p);
fill(v, 2*sz);
show(v, p);
return 0;
}
我也已就此提交 upstream bug。
我已经评论了 github 问题,但简短的回答是,由于 libstdc++.so.6
拆分某些常见模板实例的方式,例如
basic_ostream<...>::operator<<(basic_ostream<...>&, const std::string &);
并仅在 libstdc++.so.6
中实例化它们一次,并且因为 libstdc++.so.6
本身不是作为一个检测代码,所有检测代码可以看到的是您将一个悬空指针传递给外部函数。它不知道外部函数会用这个指针做什么,所以不能报错。
问题不会用clang++ ... -stdlib=libc++
重现(已正确报告悬空访问)。