错误:container-overflow

地址擦除器错误:容器溢出

在 Visual Studio 2022 版本 17.2 及更高版本中,Microsoft Visual C++ 标准库 (STL) 可在一定程度上帮助使用 AddressSanitizer。 以下容器类型具有用于检测内存访问问题的注释:

标准容器类型 禁用注释宏 支持的版本
std::vector _DISABLE_VECTOR_ANNOTATION Visual Studio 2022 17.2
std::string _DISABLE_STRING_ANNOTATION Visual Studio 2022 17.6

有检查可确保无单一定义规则 (ODR) 冲突。 当一个翻译单元使用 ASan 注释批注标准类型(例如 vector)但另一个翻译单元未进行批注时,会发生 ODR 冲突。 在此示例中,链接器可能会同时看到一个vector<int>::push_back声明包含地址清理器注释,而另一个vector<int>::push_back声明不包含。 为了避免此问题,用于链接二进制文件的每个静态库和对象还必须启用 ASan 注释。 实际上,必须在启用 AddressSanitizer 的情况下生成这些静态库和对象。 将代码与不同的注释设置混合会导致错误:

my_static.lib(my_code.obj) : error LNK2038: mismatch detected for 'annotate_vector': value '0' doesn't match value '1' in main.obj

要解决此错误,请在使用相应宏的所有项目中禁用注释,或者使用/fsanitize=address和启用的注释生成每个项目。 (默认情况下启用注释。)

示例:在 std::vector 中访问保留内存

// Compile with: cl /EHsc /fsanitize=address /Zi
#include <vector>

int main() {   
    // Create a vector of size 10, but with a capacity of 20.    
    std::vector<int> v(10);
    v.reserve(20);

    // In versions prior to 17.2, MSVC ASan does NOT raise an exception here.
    // While this is an out-of-bounds write to 'v', MSVC ASan
    // ensures the write is within the heap allocation size (20).
    // With 17.2 and later, MSVC ASan will raise a 'container-overflow' exception:
    // ==18364==ERROR: AddressSanitizer: container-overflow on address 0x1263cb8a0048 at pc 0x7ff6466411ab bp 0x005cf81ef7b0 sp 0x005cf81ef7b8
    v[10] = 1;

    // Regardless of version, MSVC ASan DOES raise an exception here, as this write
    // is out of bounds from the heap allocation.
    v[20] = 1;
}

要生成并测试此示例,请在 Visual Studio 2022 版本 17.2 或更高版本的开发人员命令提示符窗口中运行以下命令:

cl /EHsc example1.cpp /fsanitize=address /Zi
devenv /debugexe example1.exe

std::vector 中的保留内存访问的错误结果

Screenshot of debugger displaying container-overflow error in example 1.

自定义分配器和容器溢出

地址清理器容器溢出检查支持非std::allocator分配器。 但是,由于 AddressSanitizer 不知道自定义分配器是否符合 AddressSanitizer 要求,例如在 8 字节边界上对齐分配,或者未在分配末尾与下一个 8 字节边界之间放置数据,因此可能并不总是能够检查在分配后端的访问是否正确。

AddressSanitizer 在 8 字节区块中标记内存块。 它不能将不可访问字节放在单个区块中的可访问字节之前。 区块中有 8 个可访问字节,或 4 个可访问字节后跟 4 个不可访问字节是有效的。 四个不可访问字节不能后跟 4 个可访问字节。

如果来自自定义分配器的分配末尾与 8 字节区块的末尾不完全一致,AddressSanitizer 必须假定分配器使分配末尾和区块末尾之间的字节可供要写入的分配器或用户使用。 因此,它不能将最后 8 字节区块中的字节标记为不可访问。 在以下vector示例中,使用自定义分配器分配内存时,“?”指的是未初始化的数据,“-”指的是不可访问的内存。

std::vector<uint8_t, MyCustomAlloc<uint8_t>> v;
v.reserve(20);
v.assign({0, 1, 2, 3});
// the buffer of `v` is as follows:
//    | v.data()
//    |       | v.data() + v.size()
//    |       |                                     | v.data() + v.capacity()
//  [ 0 1 2 3 ? ? ? ? ][ ? ? ? ? ? ? ? ? ][ ? ? ? ? - - - - ]
//        chunk 1            chunk 2            chunk 3

在上一示例中,区块 3 具有 4 个字节的内存,这些内存假定不可访问,因为它们介于保留(v.reserve(20))的 20 个字节的分配末尾和第三个 8 字节逻辑分组的的末尾之间(请记住,AddressSanitizer 在 8 字节区块中标记内存块)。

理想情况下,我们会标记影子内存,地址清理器为每个 8 字节内存块预留该内存,以跟踪该 8 字节块中的哪些字节有效、哪些无效(以及原因),以便v.data() + [0, v.size())可访问而v.data() + [v.size(), v.capacity())不可访问。 请注意此处的间隔表示法的使用:“[”表示包含,“)”表示排除。 如果用户正在使用自定义分配器,则我们不知道v.data() + v.capacity()之后的内存是否可访问。 我们必须假定可访问。 我们更愿意在影子内存中将这些字节标记为不可访问,但必须将这些字节标记为可访问,以便在分配后仍可访问这些字节。

std::allocator使用_Minimum_asan_allocation_alignment静态成员变量以告知vectorstring,他们可以信任分配器不会在分配后直接放置数据。 这可确保分配器不会在分配末尾和区块末尾之间使用内存。 因此,地址清理器可以将区块的一部分标记为无法访问以捕获溢出。

如果希望实现信任自定义分配器正在处理分配末尾和区块末尾之间的内存,以便它可以将内存标记为不可访问并捕获溢出,请将_Minimum_asan_allocation_alignment设置为实际的最小对齐。 要使 AddressSanitizer 正常工作,对齐必须至少为 8。

另请参阅

AddressSanitizer 概述
AddressSanitizer 已知问题
AddressSanitizer 生成和语言参考
AddressSanitizer 运行时参考
AddressSanitizer 阴影字节
AddressSanitizer 云或分布式测试
AddressSanitizer 调试程序集成
AddressSanitizer 错误示例