地址消毒器有时会错过释放后的堆使用

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++重现(已正确报告悬空访问)。