Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ASan should detect writing to a basic_string's reserved but uninitialized memory #5252

Conversation

davidmrdavid
Copy link
Member

@davidmrdavid davidmrdavid commented Jan 24, 2025

Fixes #5251

Problem:

As described in the linked bug, the following program should throw under /fsanitize=address:

#include <vector>
#include <string>

int main()
{
    // This crashes (expectedly)
    //std::vector<int> vec;
    //vec.reserve(100);
    //vec.data()[50] = 1;

    // This does not crash (it should crash, like `vector`)
    std::basic_string<char> myString;
    myString.reserve(100);
    char* data = &myString[0];
    data[50] = 'A';
}

But it does not. This PR aims to fix that.

Approach:

The issue resides in xstring's reserve implementation, inlined below:

    constexpr void reserve(_CRT_GUARDOVERFLOW const size_type _Newcap) {
        // [... removed stuff]
        const size_type _Old_size = _Mypair._Myval2._Mysize;
        _Reallocate_grow_by(_Newcap - _Old_size,
            [](_Elem* const _New_ptr, const _Elem* const _Old_ptr, const size_type _Old_size)
                _STATIC_CALL_OPERATOR { _Traits::copy(_New_ptr, _Old_ptr, _Old_size + 1); });

        _Mypair._Myval2._Mysize = _Old_size;
        // [... removed stuff]

The call to _Reallocate_grow_by calls the ASan annotation machinery under the assumption that the size (i.e the initialized memory) of the string equals it's capacity (i.e the allocated memory). You can see that in the following selected snippets of _Reallocate_grow_by:

    template <class _Fty, class... _ArgTys>
    _CONSTEXPR20 basic_string& _Reallocate_grow_by(const size_type _Size_increase, _Fty _Fn, _ArgTys... _Args) {
        // reallocate to increase size by _Size_increase elements, new buffer prepared by
        // _Fn(_New_ptr, _Old_ptr, _Old_size, _Args...)
        
        // [... removed stuff]
        const size_type _New_size     = _Old_size + _Size_increase; // << [added: assumes new size is equal to capacity]
        const size_type _Old_capacity = _My_data._Myres;
        size_type _New_capacity       = _Calculate_growth(_New_size);
        auto& _Al                     = _Getal();
        const pointer _New_ptr        = _Allocate_for_capacity(_Al, _New_capacity); // throws

        // [... removed stuff]
        _ASAN_STRING_REMOVE(*this);
        _My_data._Mysize      = _New_size;
        _My_data._Myres       = _New_capacity;
        // [... removed stuff]

        _ASAN_STRING_CREATE(*this); // [added: adjusts ASan with wrong assumption]
        return *this;
    }

So the tactical fix is simple: To modify the ASan annotations on reserve after the call to _Reallocate_grow_by. This PR does just that.

Investigation notes on _Reallocate_grow_by:

This bug made me suspicious that maybe there were other latent ASan annotations bugs, so I did a quick search over the utilization of _Reallocate_grow_by in case there were other cases that seemed wrong. Spoilers: I found no such other cases.

Here's the callers of _Reallocate_grow_by, which should make it clear that it's implementation is correct in most cases.:

  • append: the aforementioned assumption makes sense here, under reallocation.
  • insert: the size = capacity assumption makes sense here, under reallocation, as well.

The following two cases I'm less certain of, but seem ok too:

  • resize_and_overwrite: seems right as I don't see any post-operation size adjustements.
  • replace: would appreciate a second eye on this one.

And of course, the last one is the known buggy reserve case.

@davidmrdavid davidmrdavid self-assigned this Jan 24, 2025
@davidmrdavid davidmrdavid added bug Something isn't working ASan Address Sanitizer labels Jan 24, 2025
@davidmrdavid davidmrdavid marked this pull request as ready for review January 24, 2025 23:32
@davidmrdavid davidmrdavid requested a review from a team as a code owner January 24, 2025 23:32
@davidmrdavid

This comment was marked as resolved.

This comment was marked as resolved.

@davidmrdavid

This comment was marked as resolved.

stl/inc/xstring Outdated Show resolved Hide resolved
stl/inc/xstring Show resolved Hide resolved
tests/std/tests/GH_002030_asan_annotate_string/test.cpp Outdated Show resolved Hide resolved
tests/std/tests/GH_002030_asan_annotate_string/test.cpp Outdated Show resolved Hide resolved
tests/std/tests/GH_002030_asan_annotate_string/test.cpp Outdated Show resolved Hide resolved
@StephanTLavavej StephanTLavavej changed the title Throw under ASan when writing to a basic_string's reserved but uninitialized memory ASan should detect writing to a basic_string's reserved but uninitialized memory Jan 28, 2025
@StephanTLavavej
Copy link
Member

Thanks, this is great! I pushed minor nitpicks. 😻

I also updated the PR title (which will become the commit title) because "throw" implies throwing an exception, which isn't what ASan does.

@StephanTLavavej StephanTLavavej removed their assignment Jan 28, 2025
@StephanTLavavej StephanTLavavej self-assigned this Jan 28, 2025
@StephanTLavavej
Copy link
Member

I'm mirroring this to the MSVC-internal repo - please notify me if any further changes are pushed.

@StephanTLavavej StephanTLavavej merged commit d655c17 into microsoft:main Jan 29, 2025
39 checks passed
@StephanTLavavej
Copy link
Member

Thanks for this significant correctness fix! All shall love Address Sanitizer and despair! 🧝‍♀️ 💍 😻

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
Archived in project
Development

Successfully merging this pull request may close these issues.

<string>: ASan annotations do not prevent writing to allocated but uninitialized basic_string memory
3 participants