c++visual-studiocrash

Why is this simple program which uses std::async crashing?


As part of looking into Crash in __cxa_end_catch on AIX using XLClang++, I tried refactoring my reproducer to avoid using std::exception_ptr, to see if that would affect anything:

#include <cstdio>
#include <exception>
#include <future>
#include <thread>
#include <vector>

std::future<std::invalid_argument> GetExceptionFromDeadThread()
{
    return std::async(std::launch::async, []() {
        try
        {
            throw std::invalid_argument("Some string long enough to allocate on the heap?                                                                                                             ");
        }
        catch (std::invalid_argument e)
        {
            return e;
        }
        catch (...)
        {
            std::fprintf(stderr, "'std::rethrow_exception(future.get())' threw unexpected exception");
            abort();
        }
    });
}

int main()
{
    try
    {
        constexpr size_t numThreads(100);
        std::vector<std::future<std::invalid_argument>> futures;

        while (true)
        {
            for (size_t i = 0; i < numThreads; ++i)
            {
                futures.push_back(GetExceptionFromDeadThread());
            }

            while (!futures.empty())
            {
                auto& future(futures.back());
                future.wait();

                try
                {
                    throw future.get();
                }
                catch (std::invalid_argument&)
                {
                    std::fputs(".", stdout);
                }
                catch (...)
                {
                    std::fprintf(stderr, "'std::rethrow_exception(future.get())' threw unexpected exception");
                    abort();
                }

                futures.pop_back();
            }
        }
    }
    catch (...)
    {
        std::fprintf(stderr, "Caught unexpected exception");
        abort();
    }
}

But I noticed, while testing on my Windows development machine (w/ Visual Studio 2022, default-created C++ console project in Debug|x64 configuration), that it was crashing! This disturbs me as I thought I was looking into an issue w/ the AIX compiler/toolchain, but maybe I'm doing something wrong?

HEAP[exception_ptr_test.exe]: Invalid address specified to RtlValidateHeap( 0000021A37BB0000, 00007FFFDA43A2F0 )

>   ntdll.dll!RtlpBreakPointHeap()  Unknown
    ntdll.dll!RtlpValidateHeapEntry()   Unknown
    ntdll.dll!RtlValidateHeap() Unknown
    KernelBase.dll!HeapValidate()   Unknown
    ucrtbased.dll!_CrtIsValidHeapPointer(const void * block) Line 1407  C++
    ucrtbased.dll!free_dbg_nolock(void * const block, const int block_use) Line 904 C++
    ucrtbased.dll!_free_dbg(void * block, int block_use) Line 1030  C++
    ucrtbased.dll!free(void * block) Line 32    C++
    vcruntime140d.dll!__std_exception_destroy(__std_exception_data * data) Line 46  C++
    exception_ptr_test.exe!std::exception::~exception() Line 91 C++
    exception_ptr_test.exe!std::logic_error::~logic_error() C++
    exception_ptr_test.exe!std::invalid_argument::~invalid_argument()   C++
    exception_ptr_test.exe!std::_Packaged_state<std::invalid_argument __cdecl(void)>::_Call_immediate() Line 494    C++
    exception_ptr_test.exe!std::_Task_async_state<std::invalid_argument>::{ctor}::__l2::<lambda_1>::operator()() Line 664   C++
    exception_ptr_test.exe!std::invoke<`std::_Task_async_state<std::invalid_argument>::_Task_async_state<std::invalid_argument><std::_Fake_no_copy_callable_adapter<`GetExceptionFromDeadThread'::`2'::<lambda_1>>>'::`2'::<lambda_1> &>(std::_Task_async_state<std::invalid_argument>::{ctor}::__l2::<lambda_1> & _Obj) Line 1695  C++
    exception_ptr_test.exe!std::_Func_impl_no_alloc<`std::_Task_async_state<std::invalid_argument>::_Task_async_state<std::invalid_argument><std::_Fake_no_copy_callable_adapter<`GetExceptionFromDeadThread'::`2'::<lambda_1>>>'::`2'::<lambda_1>,void>::_Do_call() Line 874   C++
    exception_ptr_test.exe!std::_Func_class<void>::operator()() Line 920    C++
    exception_ptr_test.exe!Concurrency::details::_MakeVoidToUnitFunc::__l2::<lambda_1>::operator()() Line 2363  C++
    exception_ptr_test.exe!std::invoke<`Concurrency::details::_MakeVoidToUnitFunc'::`2'::<lambda_1> &>(Concurrency::details::_MakeVoidToUnitFunc::__l2::<lambda_1> & _Obj) Line 1696    C++
    exception_ptr_test.exe!std::_Func_impl_no_alloc<`Concurrency::details::_MakeVoidToUnitFunc'::`2'::<lambda_1>,unsigned char>::_Do_call() Line 878    C++
    exception_ptr_test.exe!std::_Func_class<unsigned char>::operator()() Line 921   C++
    exception_ptr_test.exe!Concurrency::task<unsigned char>::_InitialTaskHandle<void,`std::_Task_async_state<std::invalid_argument>::_Task_async_state<std::invalid_argument><std::_Fake_no_copy_callable_adapter<`GetExceptionFromDeadThread'::`2'::<lambda_1>>>'::`2'::<lambda_1>,Concurrency::details::_TypeSelectorNoAsync>::_LogWorkItemAndInvokeUserLambda<std::function<unsigned char __cdecl(void)>>(std::function<unsigned char __cdecl(void)> _func) Line 3528    C++
    exception_ptr_test.exe!Concurrency::task<unsigned char>::_InitialTaskHandle<void,`std::_Task_async_state<std::invalid_argument>::_Task_async_state<std::invalid_argument><std::_Fake_no_copy_callable_adapter<`GetExceptionFromDeadThread'::`2'::<lambda_1>>>'::`2'::<lambda_1>,Concurrency::details::_TypeSelectorNoAsync>::_Init(Concurrency::details::_TypeSelectorNoAsync __formal) Line 3548   C++
    exception_ptr_test.exe!Concurrency::task<unsigned char>::_InitialTaskHandle<void,`std::_Task_async_state<std::invalid_argument>::_Task_async_state<std::invalid_argument><std::_Fake_no_copy_callable_adapter<`GetExceptionFromDeadThread'::`2'::<lambda_1>>>'::`2'::<lambda_1>,Concurrency::details::_TypeSelectorNoAsync>::_Perform() Line 3533   C++
    exception_ptr_test.exe!Concurrency::details::_PPLTaskHandle<unsigned char,Concurrency::task<unsigned char>::_InitialTaskHandle<void,`std::_Task_async_state<std::invalid_argument>::_Task_async_state<std::invalid_argument><std::_Fake_no_copy_callable_adapter<`GetExceptionFromDeadThread'::`2'::<lambda_1>>>'::`2'::<lambda_1>,Concurrency::details::_TypeSelectorNoAsync>,Concurrency::details::_TaskProcHandle>::invoke() Line 1475   C++
    exception_ptr_test.exe!Concurrency::details::_TaskProcHandle::_RunChoreBridge(void * _Parameter) Line 171   C++
    exception_ptr_test.exe!Concurrency::details::_DefaultPPLTaskScheduler::_PPLTaskChore::_Callback(void * _Args) Line 57   C++
    msvcp140d.dll!Concurrency::details::`anonymous namespace'::_Task_scheduler_callback(_TP_CALLBACK_INSTANCE * _Pci, void * _Args, _TP_WORK * __formal) Line 134   C++
    ntdll.dll!TppWorkpExecuteCallback() Unknown
    ntdll.dll!TppWorkerThread() Unknown
    kernel32.dll!BaseThreadInitThunk()  Unknown
    ntdll.dll!RtlUserThreadStart()  Unknown

State of other threads

Also, changing return e to return std::invalid_argument(e) makes the crash go away.


Solution

  • This is a compiler bug in msvc.

    I've reported the issue here: Wrong codegen for returning exceptions caught by value with NVRO enabled

    It only happens with NRVO (named return value optimization) enabled.
    NRVO is disabled by default unless you enable optimizations or compile for C++20 (or later):

    /Zc:nrvo (Control optional NRVO)

    This option is off by default, but is set automatically when you compile using the /O2 option, the /permissive- option, or /std:c++20 or later.

    So it will occur in default Release builds and (if you're targeting C++20 or later) also in Debug builds.


    Here's a minimal example that still exhibits the observed problem:

    godbolt

    #include <stdexcept>
    
    int main() {
        auto that = []() {
            try {
                throw std::invalid_argument("foo");
            } catch (std::invalid_argument e) {
                return e;
            }
        };
    
        auto a = that();
        auto b = that();
    }
    

    If nrvo is explicitly disabled (/Zc:nrvo-) then it works as expected, otherwise it crashes in the destructors of a / b.


    The underlying problem seems to be that msvc initializes the return object at the wrong stack address: (probably due to it trying to do nrvo for the caught exception object)

    godbolt

    #include <stdexcept>
    #include <cstdio>
    
    struct my_obj {
        my_obj() {
            std::printf("my_obj()                 %p\n", this);
        }
    
        ~my_obj() {
            std::printf("~my_obj()                %p\n", this);
        }
    
        my_obj(my_obj&& o): orig(o.orig) {
            std::printf("my_obj(my_obj&&)         %p      other: %p\n", this, &o);
        }
    
        my_obj(my_obj const& o) : orig(o.orig) {
            std::printf("my_obj(my_obj const&)    %p      other: %p\n", this, &o);
        }
    
        my_obj& operator=(my_obj const& o) {
            orig = o.orig;
            std::printf("operator=(my_obj const&) %p      other: %p\n", this, &o);
            return *this;
        }
    
         my_obj& operator=(my_obj&& o) {
            orig = o.orig;
            std::printf("operator=(my_obj&&)      %p      other: %p\n", this, &o);
            return *this;
        }
    
        void what() {
            std::printf("what()                   %p   orig obj: %p\n", this, orig);
        }
    
        void* orig = this;
    };
    
    int main() {
        auto that = []() {
            try {
                throw my_obj{};
            } catch (my_obj e) {
                return e;
            }
        };
    
        auto a = that();
        a.what();
        std::printf("-----------------------------------\n");
        auto b = that();
        b.what();
        std::printf("-----------------------------------\n");
    }
    

    Example output:

    my_obj()                 000000244FCFFA28
    my_obj(my_obj const&)    000000244FCFFA30      other: 000000244FCFFA28
    ~my_obj()                000000244FCFFA28
    what()                   000000244FCFFA90   orig obj: 0000000000000000
    -----------------------------------
    my_obj()                 000000244FCFFA28
    my_obj(my_obj const&)    000000244FCFFA30      other: 000000244FCFFA28
    ~my_obj()                000000244FCFFA28
    what()                   000000244FCFFA88   orig obj: 00007FF677181439
    -----------------------------------
    ~my_obj()                000000244FCFFA88
    ~my_obj()                000000244FCFFA90
    

    Note that the original exception was constructed at 000000244FCFFA28 and then copied to 000000244FCFFA30 (this is most likely e in the catch block).

    But after that the exception is not copied / moved into the return value (a is 000000244FCFFA90 and b is 000000244FCFFA88)

    => Both a and b are not initialized and contain random garbage from the stack.

    Additionally the exception object is leaked, because the copy to 000000244FCFFA30 never got destructed.


    For the problem with XLclang on AIX:
    I'm not sure since i've never worked with those before.

    But it has quite a few issues related to exceptions that have already been fixed in more recent versions - so maybe updating your xlclang version fixes the problem.

    [and more]

    see here for the full list of bugfixes in each version