Skip to content

Commit

Permalink
Add the ability to opt-out of ASan container annotations on a per-all…
Browse files Browse the repository at this point in the history
…ocator basis (microsoft#5241)

Co-authored-by: Casey Carter <[email protected]>
Co-authored-by: Stephan T. Lavavej <[email protected]>
  • Loading branch information
3 people authored and alexprabhatbara committed Feb 4, 2025
1 parent 2314e1a commit d2a2f27
Show file tree
Hide file tree
Showing 5 changed files with 227 additions and 102 deletions.
74 changes: 38 additions & 36 deletions stl/inc/vector
Original file line number Diff line number Diff line change
Expand Up @@ -483,48 +483,50 @@ private:
_STL_INTERNAL_CHECK(_Old_last_ != nullptr);
_STL_INTERNAL_CHECK(_New_last_ != nullptr);

if constexpr (!_Disable_ASan_container_annotations_for_allocator<allocator_type>) {
#if _HAS_CXX20
if (_STD is_constant_evaluated()) {
return;
}
if (_STD is_constant_evaluated()) {
return;
}
#endif // _HAS_CXX20

if (!_Asan_vector_should_annotate) {
return;
}

const void* const _First = _STD _Unfancy(_First_);
const void* const _End = _STD _Unfancy(_End_);
const void* const _Old_last = _STD _Unfancy(_Old_last_);
const void* const _New_last = _STD _Unfancy(_New_last_);
if constexpr ((_Container_allocation_minimum_asan_alignment<vector>) >= _Asan_granularity) {
// old state:
// [_First, _Old_last) valid
// [_Old_last, _End) poison
// new state:
// [_First, _New_last) valid
// [_New_last, asan_aligned_after(_End)) poison
_CSTD __sanitizer_annotate_contiguous_container(
_First, _STD _Get_asan_aligned_after(_End), _Old_last, _New_last);
} else {
const auto _Aligned = _STD _Get_asan_aligned_first_end(_First, _End);
if (_Aligned._First == _Aligned._End) {
// The buffer does not end at least one shadow memory section; nothing to do.
if (!_Asan_vector_should_annotate) {
return;
}

const void* const _Old_fixed = _Aligned._Clamp_to_end(_Old_last);
const void* const _New_fixed = _Aligned._Clamp_to_end(_New_last);

// old state:
// [_Aligned._First, _Old_fixed) valid
// [_Old_fixed, _Aligned._End) poison
// [_Aligned._End, _End) valid
// new state:
// [_Aligned._First, _New_fixed) valid
// [_New_fixed, _Aligned._End) poison
// [_Aligned._End, _End) valid
_CSTD __sanitizer_annotate_contiguous_container(_Aligned._First, _Aligned._End, _Old_fixed, _New_fixed);
const void* const _First = _STD _Unfancy(_First_);
const void* const _End = _STD _Unfancy(_End_);
const void* const _Old_last = _STD _Unfancy(_Old_last_);
const void* const _New_last = _STD _Unfancy(_New_last_);
if constexpr ((_Container_allocation_minimum_asan_alignment<vector>) >= _Asan_granularity) {
// old state:
// [_First, _Old_last) valid
// [_Old_last, _End) poison
// new state:
// [_First, _New_last) valid
// [_New_last, asan_aligned_after(_End)) poison
_CSTD __sanitizer_annotate_contiguous_container(
_First, _STD _Get_asan_aligned_after(_End), _Old_last, _New_last);
} else {
const auto _Aligned = _STD _Get_asan_aligned_first_end(_First, _End);
if (_Aligned._First == _Aligned._End) {
// The buffer does not end at least one shadow memory section; nothing to do.
return;
}

const void* const _Old_fixed = _Aligned._Clamp_to_end(_Old_last);
const void* const _New_fixed = _Aligned._Clamp_to_end(_New_last);

// old state:
// [_Aligned._First, _Old_fixed) valid
// [_Old_fixed, _Aligned._End) poison
// [_Aligned._End, _End) valid
// new state:
// [_Aligned._First, _New_fixed) valid
// [_New_fixed, _Aligned._End) poison
// [_Aligned._End, _End) valid
_CSTD __sanitizer_annotate_contiguous_container(_Aligned._First, _Aligned._End, _Old_fixed, _New_fixed);
}
}
}

Expand Down
4 changes: 4 additions & 0 deletions stl/inc/xmemory
Original file line number Diff line number Diff line change
Expand Up @@ -805,6 +805,10 @@ struct _Simple_types { // wraps types from allocators with simple addressing for
_INLINE_VAR constexpr size_t _Asan_granularity = 8;
_INLINE_VAR constexpr size_t _Asan_granularity_mask = _Asan_granularity - 1;

// Controls whether ASan `container-overflow` errors are reported for this allocator.
template <class>
constexpr bool _Disable_ASan_container_annotations_for_allocator = false;

struct _Asan_aligned_pointers {
const void* _First;
const void* _End;
Expand Down
87 changes: 45 additions & 42 deletions stl/inc/xstring
Original file line number Diff line number Diff line change
Expand Up @@ -622,54 +622,57 @@ private:

static _CONSTEXPR20 void _Apply_annotation(const value_type* const _First, const size_type _Capacity,
const size_type _Old_size, const size_type _New_size) noexcept {
if constexpr (!_Disable_ASan_container_annotations_for_allocator<allocator_type>) {
#if _HAS_CXX20
if (_STD is_constant_evaluated()) {
return;
}
if (_STD is_constant_evaluated()) {
return;
}
#endif // _HAS_CXX20
// Don't annotate small strings; only annotate on the heap.
if (_Capacity <= _Small_string_capacity || !_Asan_string_should_annotate) {
return;
}

// Note that `_Capacity`, `_Old_size`, and `_New_size` do not include the null terminator
const void* const _End = _First + _Capacity + 1;
const void* const _Old_last = _First + _Old_size + 1;
const void* const _New_last = _First + _New_size + 1;
// Don't annotate small strings; only annotate on the heap.
if (_Capacity <= _Small_string_capacity || !_Asan_string_should_annotate) {
return;
}

constexpr bool _Large_string_always_asan_aligned =
(_Container_allocation_minimum_asan_alignment<basic_string>) >= _Asan_granularity;
// Note that `_Capacity`, `_Old_size`, and `_New_size` do not include the null terminator
const void* const _End = _First + _Capacity + 1;
const void* const _Old_last = _First + _Old_size + 1;
const void* const _New_last = _First + _New_size + 1;

// for the non-aligned buffer options, the buffer must always have size >= 9 bytes,
// so it will always end at least one shadow memory section.
constexpr bool _Large_string_always_asan_aligned =
(_Container_allocation_minimum_asan_alignment<basic_string>) >= _Asan_granularity;

_Asan_aligned_pointers _Aligned;
if constexpr (_Large_string_always_asan_aligned) {
_Aligned = {_First, _STD _Get_asan_aligned_after(_End)};
} else {
_Aligned = _STD _Get_asan_aligned_first_end(_First, _End);
}
const void* const _Old_fixed = _Aligned._Clamp_to_end(_Old_last);
const void* const _New_fixed = _Aligned._Clamp_to_end(_New_last);

// --- always aligned case ---
// old state:
// [_First, _Old_last) valid
// [_Old_last, asan_aligned_after(_End)) poison
// new state:
// [_First, _New_last) valid
// [_New_last, asan_aligned_after(_End)) poison

// --- sometimes non-aligned case ---
// old state:
// [_Aligned._First, _Old_fixed) valid
// [_Old_fixed, _Aligned._End) poison
// [_Aligned._End, _End) valid
// new state:
// [_Aligned._First, _New_fixed) valid
// [_New_fixed, _Aligned._End) poison
// [_Aligned._End, _End) valid
_CSTD __sanitizer_annotate_contiguous_container(_Aligned._First, _Aligned._End, _Old_fixed, _New_fixed);
// for the non-aligned buffer options, the buffer must always have size >= 9 bytes,
// so it will always end at least one shadow memory section.

_Asan_aligned_pointers _Aligned;
if constexpr (_Large_string_always_asan_aligned) {
_Aligned = {_First, _STD _Get_asan_aligned_after(_End)};
} else {
_Aligned = _STD _Get_asan_aligned_first_end(_First, _End);
}
const void* const _Old_fixed = _Aligned._Clamp_to_end(_Old_last);
const void* const _New_fixed = _Aligned._Clamp_to_end(_New_last);

// --- always aligned case ---
// old state:
// [_First, _Old_last) valid
// [_Old_last, asan_aligned_after(_End)) poison
// new state:
// [_First, _New_last) valid
// [_New_last, asan_aligned_after(_End)) poison

// --- sometimes non-aligned case ---
// old state:
// [_Aligned._First, _Old_fixed) valid
// [_Old_fixed, _Aligned._End) poison
// [_Aligned._End, _End) valid
// new state:
// [_Aligned._First, _New_fixed) valid
// [_New_fixed, _Aligned._End) poison
// [_Aligned._End, _End) valid
_CSTD __sanitizer_annotate_contiguous_container(_Aligned._First, _Aligned._End, _Old_fixed, _New_fixed);
}
}

#define _ASAN_STRING_REMOVE(_Str) (_Str)._Remove_annotation()
Expand Down
69 changes: 59 additions & 10 deletions tests/std/tests/GH_002030_asan_annotate_string/test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,29 @@ STATIC_ASSERT(_Container_allocation_minimum_asan_alignment<
basic_string<wchar_t, char_traits<wchar_t>, implicit_allocator<wchar_t>>>
== 2);

// Simple allocator that opts out of ASan annotations (via `_Disable_ASan_container_annotations_for_allocator`)
template <class T, class Pocma = true_type, class Stateless = true_type>
struct implicit_allocator_no_asan_annotations : implicit_allocator<T, Pocma, Stateless> {
implicit_allocator_no_asan_annotations() = default;
template <class U>
constexpr implicit_allocator_no_asan_annotations(
const implicit_allocator_no_asan_annotations<U, Pocma, Stateless>&) noexcept {}

T* allocate(size_t n) {
T* mem = new T[n + 1];
return mem + 1;
}

void deallocate(T* p, size_t) noexcept {
delete[] (p - 1);
}
};

template <class T, class Pocma, class Stateless>
constexpr bool
_Disable_ASan_container_annotations_for_allocator<implicit_allocator_no_asan_annotations<T, Pocma, Stateless>> =
true;

template <class Alloc>
void test_construction() {
using CharType = typename Alloc::value_type;
Expand Down Expand Up @@ -1855,6 +1878,28 @@ void run_tests() {
#endif // ^^^ no workaround ^^^
}

// Test that writing to uninitialized memory in a string triggers an ASan container-overflow error. (See GH-5251.)
template <class CharType, class Alloc = allocator<CharType>>
void run_asan_container_overflow_death_test() {

// We'll give the string capacity 100 (all uninitialized memory, except for the null terminator).
basic_string<CharType, char_traits<CharType>, Alloc> myString;
myString.reserve(100);

// Write to the element at index 50 to trigger an ASan container-overflow check.
CharType* myData = &myString[0];
myData[50] = CharType{'A'};
}

// Test that ASan `container-overflow` checks can be disabled for a custom allocator.
template <class CharType>
void run_asan_annotations_disablement_test() {

// ASan annotations are disabled for the `implicit_allocator_no_asan_annotations` allocator,
// which should make the container-overflow 'death test' pass.
run_asan_container_overflow_death_test<CharType, implicit_allocator_no_asan_annotations<CharType>>();
}

template <class CharType, template <class, class, class> class Alloc>
void run_custom_allocator_matrix() {
run_tests<Alloc<CharType, true_type, true_type>>();
Expand All @@ -1869,6 +1914,11 @@ void run_allocator_matrix() {
run_custom_allocator_matrix<CharType, aligned_allocator>();
run_custom_allocator_matrix<CharType, explicit_allocator>();
run_custom_allocator_matrix<CharType, implicit_allocator>();

// To test ASan annotation disablement, we use an ad-hoc allocator type to avoid disrupting other
// tests that depend on annotations being enabled. Therefore, unlike the prior tests,
// this test is not parameterized by the allocator type.
run_asan_annotations_disablement_test<CharType>();
}

void test_DevCom_10116361() {
Expand Down Expand Up @@ -1919,15 +1969,6 @@ void test_gh_3955() {
assert(s == t);
}

void test_gh_5251() {
// GH-5251 <string>: ASan annotations do not prevent writing to allocated
// but uninitialized basic_string memory
string myString;
myString.reserve(100);
char* myData = &myString[0];
myData[50] = 'A'; // ASan should fire!
}

int main(int argc, char* argv[]) {
std_testing::death_test_executive exec([] {
run_allocator_matrix<char>();
Expand All @@ -1944,7 +1985,15 @@ int main(int argc, char* argv[]) {
test_gh_3955();
});
#ifdef __SANITIZE_ADDRESS__
exec.add_death_tests({test_gh_5251});
exec.add_death_tests({
run_asan_container_overflow_death_test<char>,
#ifdef __cpp_char8_t
run_asan_container_overflow_death_test<char8_t>,
#endif // __cpp_char8_t
run_asan_container_overflow_death_test<char16_t>,
run_asan_container_overflow_death_test<char32_t>,
run_asan_container_overflow_death_test<wchar_t>,
});
#endif // ASan instrumentation enabled
return exec.run(argc, argv);
}
Loading

0 comments on commit d2a2f27

Please sign in to comment.