地址消毒剂(ASan)是 在Visual Studio 2019版本16.9中正式发布 . 我们最近使用此功能 在MSVC编译器中查找并修复一个bug . 为了进一步验证ASan实现的有用性,我们还将其用于一系列广泛使用的开放源代码项目,在这些项目中发现了Boost、azureiotcsdk和OpenSSL中的bug。在本文中,我们通过描述我们发现的bug的类型以及它们在这些项目中的表现来展示我们的发现。我们提供了到GitHub提交的链接,这些bug是在那里修复的,这样您就可以很有帮助地了解所涉及的代码更改。如果你不熟悉ASan是什么以及如何使用它,你可能想看看 地址消毒剂文档 在深入研究本文之前。
Boost和渴望迭代器
安 急切迭代器 指向容器边界外的元素,然后取消引用的元素。以下代码示例显示了此错误内存访问模式的示例:
template <typename Iter> int ComputeSum(Iter b, Iter e) { int sum = 0; for (; b <= e; ++b) { // ERROR: will dereference the 'end' iterator // due to the use of the '<=' operator above. sum += *b; } return sum; }
有时,急切的迭代器可能会错误地出现在更复杂的循环中,例如 do_length
Boost的UTF-8转换方面实现的函数,如下所示:
int utf8_codecvt_facet::do_length( std::mbstate_t &, const char * from, const char * from_end, std::size_t max_limit ) const #if BOOST_WORKAROUND(__IBMCPP__, BOOST_TESTED_AT(600)) throw() #endif { int last_octet_count=0; std::size_t char_count = 0; const char* from_next = from; while (from_next+last_octet_count <= from_end && char_count <= max_limit) { from_next += last_octet_count; last_octet_count = (get_octet_count(*from_next)); ++char_count; } return static_cast<int>(from_next-from); }
这里,less或equal运算符用于正确设置 from_next
到 from_end
当后者指向UTF-8字符边界时。但是,这也会导致终止迭代器被取消引用的错误。使用ASan生成此代码并在Visual Studio中进行调试将导致ASan在预期位置中断:
我们让推进小组知道了这个问题,他们很快就解决了 提交了对GitHub的修复 .
azureiotcsdk:一个数组及其长度常量
当一个常量用于跟踪数组的长度但长度不正确时,数组与其长度常量之间就会发生不一致。当在内存复制操作中使用长度常量时,这可能会导致内存访问错误。下面的简单示例说明了问题:
#include <cstring> unsigned char GLOBAL_BUFFER[] = { 1,2,3,4,5 }; constexpr size_t BUF_SIZE = 6; void CopyGlobalBuffer(unsigned char* dst) { // ERROR: AddressSanitizer: global-buffer-overflow std::memcpy(dst, GLOBAL_BUFFER, BUF_SIZE); }
我们在Azure IoT C SDK中发现了此错误的一个实例,其中字符串的长度常量与实际长度不匹配:
static const unsigned char* TWIN_REPORTED_PROPERTIES = (const unsigned char*) "{ "reportedStateProperty0": "reportedStateProperty0", " ""reportedStateProperty1": "reportedStateProperty1" }"; static int TWIN_REPORTED_PROPERTIES_LENGTH = 117;
价值 TWIN_REPORTED_PROPERTIES_LENGTH
常量是117,而 TWIN_REPORTED_PROPERTIES
字符串为107,在使用复制字符串时导致全局缓冲区溢出 memcpy
. 使用ASan构建此代码并使用visualstudio进行调试在调用 memcpy
,在一个名为 CONSTBUFFER_Create_Internal
:
这并没有立即告诉我们错误的来源,但由于Visual Studio中的ASan集成,可以使用“调用堆栈”窗口遍历堆栈并找到传递错误大小值的函数:
这个案子的罪魁祸首是 send_one_report_patch
函数,已通过 TWIN_REPORTED_PROPERTIES
和 TWIN_REPORTED_PROPERTIES_LENGTH
一个间接调用 CONSTBUFFER_Create_Internal
:
static void send_one_report_patch(TWIN_MESSENGER_HANDLE handle, time_t current_time) { const unsigned char* buffer = (unsigned char*)TWIN_REPORTED_PROPERTIES; size_t size = TWIN_REPORTED_PROPERTIES_LENGTH; CONSTBUFFER_HANDLE report = real_CONSTBUFFER_Create(buffer, size); umock_c_reset_all_calls(); set_twin_messenger_report_state_async_expected_calls(report, current_time); (void)twin_messenger_report_state_async(handle, report, TEST_on_report_state_complete_callback, NULL); real_CONSTBUFFER_DecRef(report); }
我们通过使用 sizeof
运算符将长度常量设置为始终反映字符串实际大小的值。你可以找到我们的 GitHub上的bug修复提交 .
OpenSSL和Shapeshifting类型
当类型的大小因预处理器定义的不同而变化时,就产生了变形类型。如果假定该类型具有特定的大小,则可能会发生内存访问错误。下面是一个简单的例子:
#include <cstdint> #include <cstring> #include <array> #ifdef BIGGER_INT typedef int64_t MyInt; #else typedef int32_t MyInt; #endif MyInt GLOBAL_BUFFER[] = { 1,2,3,4,5 }; void SizeTypeExample() { int localBuffer[std::size(GLOBAL_BUFFER)]; // ERROR: AddressSanitizer: stack-buffer-overflow std::memcpy(localBuffer, GLOBAL_BUFFER, sizeof(GLOBAL_BUFFER)); }
如果 BIGGER_INT
定义了 memcpy
操作可能会触发堆栈缓冲区溢出,因为 localBuffer
变量假设 MyInt
大小与 int
. 在 test_param_time_t
OpenSSL测试:
static int test_param_time_t(int n) { time_t in, out; unsigned char buf[MAX_LEN], cmp[sizeof(size_t)]; const size_t len = raw_values[n].len >= sizeof(size_t) ? sizeof(time_t) : raw_values[n].len; OSSL_PARAM param = OSSL_PARAM_time_t("a", NULL); memset(buf, 0, sizeof(buf)); le_copy(buf, raw_values[n].value, sizeof(in)); memcpy(&in, buf, sizeof(in)); param.data = &out; if (!TEST_true(OSSL_PARAM_set_time_t(¶m, in))) return 0; le_copy(cmp, &out, sizeof(out)); if (!TEST_mem_eq(cmp, len, raw_values[n].value, len)) return 0; in = 0; if (!TEST_true(OSSL_PARAM_get_time_t(¶m, &in))) return 0; le_copy(cmp, &in, sizeof(in)); if (!TEST_mem_eq(cmp, sizeof(in), raw_values[n].value, sizeof(in))) return 0; param.data = &out; return test_param_type_extra(¶m, raw_values[n].value, sizeof(size_t)); }
在这里, size_t
假定与 time_t
,但根据编译的体系结构,情况并非总是如此。复制时 out
到 cmp
使用 le_copy
函数,则复制操作的大小为 sizeof(time_t)
但是 cmp
缓冲区已用大小初始化 size_t
. 在使用ASan构建OpenSSL测试并使用visualstudio进行调试时,调试器在内部出现ASan错误 le_copy
:
再次感谢VS中的ASan集成,我们能够使用调用堆栈窗口找到bug的实际来源:调用堆栈 test_param_time_t
功能:
我们让OpenSSL团队知道了这个错误,并进行了修复 在GitHub上提交 .
今天试试消毒剂!
在本文中,我们分享了如何使用AddressSanitizer在各种开源项目中发现bug。我们希望这将激励您在自己的代码库中尝试这个特性。您是否在项目中发现了急切的迭代器、变形类型或数组/长度常量不一致?让我们知道在下面的评论,在Twitter上 (@VisualC) ),或通过电子邮件 visualcpp@microsoft.com .
本文包含以下来源的代码段:
utf8u编码vtu facet.ipp 文件, Boost C++库 ,版权所有(c)2001 Ronald Garcia和Andrew Lumsdaine,根据 Boost软件许可证1.0版 .
Azure IoT C SDK和库 ,版权所有(c)Microsoft Corporation,根据 麻省理工许可证 .
Azure C共享实用程序 ,版权所有(c)Microsoft Corporation,根据 麻省理工许可证 .
参数api测试.c 文件, OpenSSL ,版权所有2019-2021 The OpenSSL Project Authors,版权所有(c)2019 Oracle和/或其附属公司,根据 Apache许可证2.0 .