Skip to content

Fix poisoning leftover for vector and string ASan annotations#6285

Open
amyw-msft wants to merge 8 commits into
microsoft:mainfrom
amyw-msft:asan_leftover_poisoning
Open

Fix poisoning leftover for vector and string ASan annotations#6285
amyw-msft wants to merge 8 commits into
microsoft:mainfrom
amyw-msft:asan_leftover_poisoning

Conversation

@amyw-msft
Copy link
Copy Markdown
Member

Fixes #6276

AddressSanitizer poisons memory in 8 byte chunks. It can mark a memory region as 'partially addressable', but only by marking the first x bytes as valid to access and remaining as poisoned. There is no way to say "only the first x bytes are poisoned". The result is that the ends of buffers that are not 8-byte aligned either need to over-poison past the end of the buffer, or under-poison and allow potential buffer overflow to go unnoticed under ASan.

For ASan container annotations, we choose which strategy to use based on the allocator. If we can be sure that the allocator only allocations in 8-byte aligned chunks, then we can safely over-poison past the end of the buffer. However, when we do this (increase the pointer that points to the 'end'), we also need to adjust the actual 'last valid' pointer as well if they are equal, because the way we clean up the container poisoning is by calling that annotations API, saying that the last valid and the end pointer are the same.

This change checks for this condition and advances the 'last valid' pointer if it is equal to the 'end' pointer. Without this fix, shadow bytes will remain poisoned after an annotated container leaves scope.

…'end' point in situations where the end has been extended to the end of the allocation block.
Copilot AI review requested due to automatic review settings May 13, 2026 23:35
@amyw-msft amyw-msft requested a review from a team as a code owner May 13, 2026 23:35
@github-project-automation github-project-automation Bot moved this to Initial Review in STL Code Reviews May 13, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes ASan container annotation cleanup for std::vector and std::basic_string when the allocator is at least 8-byte aligned. In that path, the implementation over-poisons up to the next 8-byte shadow boundary past _End. When removing or shrinking the annotation, the "old/new last valid" pointers passed to __sanitizer_annotate_contiguous_container must also be bumped to that same aligned boundary whenever they equal _End; otherwise the trailing shadow bytes past _End remain poisoned after the container goes out of scope.

Changes:

  • In stl/inc/vector's aligned-allocator branch of _Apply_annotation, advance _Old_last/_New_last to the aligned-after-end pointer when they equal _End.
  • Apply the analogous fix to stl/inc/xstring's _Apply_annotation, and restructure the function so the aligned branch returns early.
  • Add a new regression test (GH_006276_annotation_poison_cleanup) exercising both the under-poison and over-poison paths for string and vector with an ASan-unaware arena allocator.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
stl/inc/vector Compute aligned old/new last pointers and pass them to the sanitizer call in the aligned-allocator branch.
stl/inc/xstring Same fix for basic_string; restructure so the aligned branch returns early before the non-aligned code.
tests/std/tests/GH_006276_annotation_poison_cleanup/test.cpp New regression test verifying shadow bytes are fully cleared after container destruction.
tests/std/tests/GH_006276_annotation_poison_cleanup/env.lst ASan test matrix for the new test.
tests/std/test.lst Register the new test directory.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread stl/inc/xstring Outdated
Copilot AI review requested due to automatic review settings May 14, 2026 17:13
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.

Copy link
Copy Markdown
Member

@davidmrdavid davidmrdavid left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, but will defer to STL. Just a small observation

Comment thread tests/std/tests/GH_006276_annotation_poison_cleanup/test.cpp Outdated
@StephanTLavavej
Copy link
Copy Markdown
Member

ARM64 crash in the new test; is this something concerning, or pre-existing rare flakiness?

Test step failed unexpectedly.
Command: "C:\stlBuild\tests\std\tests\GH_006276_annotation_poison_cleanup\Output\15\GH_006276_annotation_poison_cleanup.exe"
Exit Code: 3221225477 (0xC0000005)

@StephanTLavavej StephanTLavavej added bug Something isn't working ASan Address Sanitizer labels May 14, 2026
@StephanTLavavej StephanTLavavej moved this from Initial Review to Work In Progress in STL Code Reviews May 14, 2026
Copy link
Copy Markdown
Member

@zacklj89 zacklj89 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't immediately know what could be causing the ARM64 failure, but let me know if I can help investigate.

Comment thread stl/inc/vector Outdated
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) {
// If we realign the _End forward to maximize coverage, we need to keep other significant points
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By "significant points" here, we mean _Old_last and _New_last right? Maybe we could clarify if there's another push:

// If we realign _End forward to maximize coverage, also realign _Old_last and _New_last when they
// equal _End. Otherwise, the ASan annotation may leave stale shadow bytes behind when cleaning up.

Comment thread stl/inc/vector Outdated

// Allocation is potentially unaligned, so we cannot annotate whole buffer since we might be
// entering memory owned by someone else. Therefore, pull back where the annotations end on the buffer,
// but may miss some coverage near end of buffer.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another comment comment, but feel free to not integrate it. I haven't worked as closely here as you, so maybe this is redundant, but:

// The allocation may not end on an ASan granularity boundary. In that case, annotating up to _End
// could affect shadow bytes for memory beyond this allocation, so restrict annotations to the largest
// aligned subrange inside the buffer. This may leave the tail of the buffer unannotated.

Comment thread stl/inc/xstring Outdated

const void* const _New_end_aligned = _STD _Get_asan_aligned_after(_End);
const void* const _Old_last_aligned = (_Old_last == _End) ? _New_end_aligned : _Old_last;
const void* const _New_last_aligned = (_New_last == _End) ? _New_end_aligned : _New_last;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work 🥲

test_arena.print_shadow();
}

int main() {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think it would be worthwhile to add a test here that does like shrink from full and tests the shadow, like testing _Old_last == _End and doing pop_back() on a full vector/string?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found more issues testing this. Latest change addresses the issues. Also decided to integrate the kind of testing I'm doing here into the larger string and vector annotation tests to ensure full coverage.

amyw-msft added 4 commits June 3, 2026 15:48
… imbue a sense of intent at the call site (i.e. 'end' pointers should be realigned when overpoisoning, but all other pointers should not - other attempts to capture this were convoluted). Moved testing into pre-existing vector/string annotation tests, with an added allocator that will be able to detect 'left-poison-bytes-behind' issues as well as a microsoftgh-6276 specific test case and extra testing for finding similar cases (insert/remove triggering a realloc).
Copilot AI review requested due to automatic review settings June 4, 2026 20:43
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 11 comments.

Comment thread stl/inc/vector
Comment on lines +496 to +504
_CONSTEXPR20 const void* _Annotation_begin() const noexcept {
_STL_INTERNAL_CHECK(_Mypair._Myval2._Myfirst != nullptr);
return _STD _Unfancy(_Mypair._Myval2._Myfirst);
}

_CONSTEXPR20 void _Modify_annotation(const difference_type _Count) const noexcept {
// Extends/shrinks the annotated range by _Count
if (_Count == 0) { // nothing to do
// This also avoids calling _Apply_annotation() with null pointers
// when the vector has zero capacity, see GH-2464.
return;
_CONSTEXPR20 const void* _Annotation_at(const size_type _Index) const noexcept {
_STL_INTERNAL_CHECK(_Mypair._Myval2._Myfirst != nullptr);
return _STD _Unfancy(_Mypair._Myval2._Myfirst + _Index);
}
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll edit the comments in xmemory and remove any unused code.

The pointers passed in to __sanitizer_annotate_contiguous_container are permitted to be unaligned w.r.t. shadow granularity.

If the beginning is not aligned, ASan still handles the first shadow byte correctly and doesn't need extra over-poisoning logic like the end, because unlike at the end of the buffer (which would require "first X bytes are poisoned" semantics), we do have the semantics to say "first X bytes are valid".

If the end is not aligned, ASan rounds down to the nearest shadow granularity. For this reason, if the STL knows no one will poison the rest of the final shadow byte due to the allocator alignment, the STL will round up before passing the pointer (our 'over-poisoning' technique) to ensure no coverage is lost at the end of the buffer.

Comment thread stl/inc/vector
Comment on lines +520 to +529
if constexpr ((_Container_allocation_minimum_asan_alignment<vector>) >= _Asan_granularity) {
// If the allocator alignment is less than or equal to the ASan granularity, then we can
// realign the end forward to maximize the annotation coverage.
return _STD _Get_asan_aligned_after(_End);
} else {
// The allocation may not end on an ASan granularity boundary. In that case, annotating up to _End
// could affect shadow bytes for memory beyond this allocation, so restrict annotations to the largest
// aligned subrange inside the buffer. This may leave the tail of the buffer unannotated.

return _End; // Using unaligned end will snap it to previous ASan granularity boundary.
Comment thread stl/inc/xstring
Comment on lines +683 to 685
_CONSTEXPR20 const void* _Annotation_begin() const noexcept {
return _Mypair._Myval2._Myptr();
}
Comment thread stl/inc/xstring
Comment on lines +699 to +712
constexpr bool _Large_string_always_asan_aligned =
(_Container_allocation_minimum_asan_alignment<basic_string>) >= _Asan_granularity;

if constexpr (_Large_string_always_asan_aligned) {
// If the allocator alignment is less than or equal to the ASan granularity, then we can
// realign the end forward to maximize the annotation coverage.
return _STD _Get_asan_aligned_after(_End);
} else {
// The allocation may not end on an ASan granularity boundary. In that case, annotating up to _End
// could affect shadow bytes for memory beyond this allocation, so restrict annotations to the largest
// aligned subrange inside the buffer. This may leave the tail of the buffer unannotated.

return _End; // Using unaligned end will snap it to previous ASan granularity boundary.
}
Comment on lines +13 to +15
#ifdef __SANITIZE_ADDRESS__
#include <cassert>
#include <cstdio>
Comment on lines +35 to +42
NO_SANITIZE_ADDRESS unsigned char* shadow_addr_of(const void* const addr) {
return reinterpret_cast<unsigned char*>(
(reinterpret_cast<uintptr_t>(addr) >> 3) + __asan_shadow_memory_dynamic_address);
}

NO_SANITIZE_ADDRESS unsigned char shadow_byte_of(const void* addr) {
return *shadow_addr_of(addr);
}
Comment on lines +44 to +48
void print_shadow_bytes(const void* addr, size_t num_bytes, const void* error_addr = nullptr,
unsigned char expected_shadow_byte = 0xff /*unused shadow byte*/) {
constexpr size_t shadow_bytes_per_line = 16;
constexpr uintptr_t bytes_per_line_mask = (shadow_bytes_per_line * shadow_granularity) - 1;

Comment on lines +346 to +358
// Try push/pop at various sizes to cover resize code path (gh-6276)
for (size_t i = 1; i < Size; ++i) {
vector<T, Alloc> v(Size, T());
v.push_back(T());
assert(verify_vector(v));
}

for (size_t i = 1; i < Size; ++i) {
vector<T, Alloc> v(Size, T());
v.pop_back();
assert(verify_vector(v));
}
}
assert(verify_string(copy_assigned_sso_to_large));

str copy_assigned_large_to_large(get_large_input<CharType>());
str copy_assigned_large_to_large(get_large_input<CharType>()); // creating allocator 28 with arena 8
assert(verify_string(move_assigned_sso_to_large));

str move_assigned_large_to_large(get_large_input<CharType>());
str move_assigned_large_to_large(get_large_input<CharType>()); // creating allocator 42 with arena 12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ASan Address Sanitizer bug Something isn't working

Projects

Status: Work In Progress

Development

Successfully merging this pull request may close these issues.

ASan STL string and vector annotations leave poisoning behind

5 participants