asio icon indicating copy to clipboard operation
asio copied to clipboard

Segfault when exception thrown from within stackful coroutine

Open ecorm opened this issue 2 years ago • 24 comments

As of Boost 1.80, throwing an exception within a stackful coroutine results in a segfault. In previous releases, the exception would be transported to asio::io_context::run and rethrown there.

Example program reproducing the problem:

#include <iostream>
#include <stdexcept>
#include <boost/asio/io_context.hpp>
#include <boost/asio/spawn.hpp>

//------------------------------------------------------------------------------
int main()
{
    boost::asio::io_context ioctx;

    boost::asio::spawn(
        ioctx,
        [](boost::asio::yield_context yield)
        {
            std::cout << "Before throw" << std::endl;
            throw std::runtime_error("bad");
            std::cout << "After throw" << std::endl;
        });

    try
    {
        ioctx.run();
    }
    catch (const std::exception& e)
    {
        std::cout << "Caught std::exception: " << e.what() << std::endl;
    }
    catch (...)
    {
        std::cout << "Caught unknown exception" << std::endl;
    }

    return 0;
}

With Boost 1.80, the output is:

Before throw
Segmentation fault (core dumped)

With GDB, it stops at some ldmxcrx instruction with no source-level debug information at all, including the stack trace. I'm using a release build of Boost.

With Boost 1.79, the output is:

Before throw
Caught std::exception: bad

ecorm avatar Aug 19 '22 00:08 ecorm

Hi. I maked some changed with the same example program, like as:

#include <iostream>
#include <boost/asio/io_context.hpp>
#include <boost/asio/spawn.hpp>
int main()
{
    boost::asio::io_context ioctx;
    boost::asio::spawn(
        ioctx,
        [](boost::asio::yield_context yield)
        {
            std::cout << "Hello World!" << std::endl;
        });
    ioctx.run();
    return 0;
}

When I build and run this program with boost_1_80_0, it would throw forced_unwind exception from: C:\local\boost_1_80_0\boost\coroutine\detail\push_coroutine_impl.hpp(line: 269) When I build and run this program with boost_1_79_0 or previous version, it would work correctly. I think that there may has some bugs in asio with boost_1_80_0 while the program leave out the coroutine .

AsonWon avatar Aug 22 '22 19:08 AsonWon

@AsonWon Please triple-backticks to properly render your code with indentation: https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/creating-and-highlighting-code-blocks

Even better, add the c++ tag next to the triple backticks as so: ```c++

ecorm avatar Aug 22 '22 20:08 ecorm

@ecorm Can you please try with the changes in this commit https://github.com/chriskohlhoff/asio/commit/13045b6017e13b5884b4a95b6a80f485b42f1edc

@AsonWon I cannot reproduce any problem with your test program. Are you getting an unhandled exception resulting in a crash? Or are you simply observing that there is a forced_unwind exception being thrown and handled within the spawn implementation (which is expected behaviour)?

chriskohlhoff avatar Aug 23 '22 00:08 chriskohlhoff

Hi. I maked some changed with the same example program, like as:

#include <iostream>
#include <boost/asio/io_context.hpp>
#include <boost/asio/spawn.hpp>
int main()
{
    boost::asio::io_context ioctx;
    boost::asio::spawn(
        ioctx,
        [](boost::asio::yield_context yield)
        {
            std::cout << "Hello World!" << std::endl;
        });
    ioctx.run();
    return 0;
}

When I build and run this program with boost_1_80_0, it would throw forced_unwind exception from: C:\local\boost_1_80_0\boost\coroutine\detail\push_coroutine_impl.hpp(line: 269) When I build and run this program with boost_1_79_0 or previous version, it would work correctly. I think that there may has some bugs in asio with boost_1_80_0 while the program leave out the coroutine .

The test result for boost_1_79_0: boost_1_79_0

The test result for boost_1_80_0: boost_1_80_0

The setting for exception: exception

I don't know the difference between boost_1_80_0 and boost_1_79_0, and this bug always appeared in all my coroutines when these coroutines were completed and exit. How can i solve it when it was happpend?

AsonWon avatar Aug 23 '22 02:08 AsonWon

Can you please try with the changes in this commit https://github.com/chriskohlhoff/asio/commit/13045b6017e13b5884b4a95b6a80f485b42f1edc

@chriskohlhoff I tried the same example program above modified to use standalone Asio, and it works with the commit you linked, using Boost 1.80 for the coroutine stuff:

Before throw
Caught std::exception: bad

Thanks! I hope Boost publishes a hotfix version before the next "major" version.

Environment info: GCC 10.3.0 OS: Linux Mint 20 Ulyana Linux Kernel: 5.4.0-124-generic

ecorm avatar Aug 23 '22 04:08 ecorm

@AsonWon That's the debugger telling you an exception is being thrown. Is it being caught inside the library? It should be. If so, that's not a bug.

chriskohlhoff avatar Aug 23 '22 05:08 chriskohlhoff

@chriskohlhoff Your fix doesn't seem to work when defining ASIO_DISABLE_BOOST_COROUTINE.

I've tried this example:

#define ASIO_DISABLE_BOOST_COROUTINE

#include <iostream>
#include <stdexcept>
#include <asio/detached.hpp>
#include <asio/io_context.hpp>
#include <asio/spawn.hpp>

//------------------------------------------------------------------------------
int main()
{
    asio::io_context ioctx;
    // auto work = asio::make_work_guard(ioctx);

    asio::spawn(
        ioctx,
        [](asio::yield_context yield)
        {
            std::cout << "Before throw" << std::endl;
            throw std::runtime_error("bad");
            std::cout << "After throw" << std::endl;
        },
        asio::detached
    );

    try
    {
        std::cout << "Before ioctx.run()" << std::endl;
        ioctx.run();
        std::cout << "After ioctx.run()" << std::endl;
    }
    catch (const std::exception& e)
    {
        std::cout << "Caught std::exception: " << e.what() << std::endl;
    }
    catch (...)
    {
        std::cout << "Caught unknown exception" << std::endl;
    }

    return 0;
}

The output is:

Before ioctx.run()
Before throw
After ioctx.run()

In other words, ioctx.run returns when the exception is thrown, instead of propagating it.

If I enable the make_work_guard line, it deadlocks after printing:

Before ioctx.run()
Before throw

It seems as if the coroutine is simply discarded when an exception is thrown, and the io_context just proceeds with other work.

ecorm avatar Aug 23 '22 06:08 ecorm

Yep: https://github.com/chriskohlhoff/asio/issues/1111#issuecomment-1223584534

Instead of detached pass [](std::exception_ptr e){ if (e) std::rethrow_exception(e); }

chriskohlhoff avatar Aug 23 '22 06:08 chriskohlhoff

Well. If I cancel the select option [不在此列表中的所有C++ Exception], I will find an exception in the output window: boost_1_80_0_exception I don't understand it, why this exception can be always caught while the program leave out the lambda coroutine or similar coroutinue? It has been working correctly in this way before boost_1_80_0. How could i do if i want to avoid this exception in my program?

AsonWon avatar Aug 23 '22 06:08 AsonWon

Instead of detached pass [](std::exception_ptr e){ if (e) std::rethrow_exception(e); }

Ah, yes, I see now that spawn takes a completion token with a handler having the signature void handler(std::exception_ptr);. Sorry about that, my understanding of spawn was about a decade old.

This now works for me (with your commit https://github.com/chriskohlhoff/asio/commit/13045b6017e13b5884b4a95b6a80f485b42f1edc) and matches the old behavior

#define ASIO_DISABLE_BOOST_COROUTINE

#include <exception>
#include <iostream>
#include <stdexcept>
#include <asio/io_context.hpp>
#include <asio/spawn.hpp>

//------------------------------------------------------------------------------
struct propagating_t
{
    void operator()(std::exception_ptr e) const
    {
        if (e) std::rethrow_exception(e);
    }
};

const propagating_t propagating;

//------------------------------------------------------------------------------
int main()
{
    asio::io_context ioctx;

    asio::spawn(
        ioctx,
        [](asio::yield_context yield)
        {
            std::cout << "Before throw" << std::endl;
            throw std::runtime_error("bad");
            std::cout << "After throw" << std::endl;
        },
        propagating
    );

    try
    {
        ioctx.run();
    }
    catch (const std::exception& e)
    {
        std::cout << "Caught std::exception: " << e.what() << std::endl;
    }
    catch (...)
    {
        std::cout << "Caught unknown exception" << std::endl;
    }

    return 0;
}

Output:

Before throw
Caught std::exception: bad

ecorm avatar Aug 23 '22 06:08 ecorm

This is the new program that i want to avoid or catch this exception:

#include <iostream>
#include <boost/asio/io_context.hpp>
#include <boost/asio/spawn.hpp>
int main()
{
    boost::asio::io_context ioctx;
    boost::asio::spawn(
        ioctx,
        [](boost::asio::yield_context yield)
        {
            std::cout << "Hello World!" << std::endl;
        });
    try
    {
        ioctx.run();
    }
    catch (const std::logic_error &e)
    {
        std::cout << "LogicError: " << e.what() << std::endl;
    }
    catch (const std::runtime_error &e)
    {
        std::cout << "RunError: " << e.what() << std::endl;
    }
    catch (const boost::coroutines::detail::forced_unwind &e)
    {
        //std::cout << "UnWindError: " << e.what() << std::endl;
        std::cout << "UnWindError: " << std::endl;
    }
    catch (...)
    {
        std::cout << "OtherError: " << std::endl;
    }
    return 0;
}

When I run it with boost_1_80_0, it doesn't seem to work with any catch conditions.

My development environment: OS: Windows 10 VS: Visual Studio 2019 BOOST: boost_1_80_0-msvc-14.2-64

AsonWon avatar Aug 23 '22 13:08 AsonWon

@AsonWon The forced_unwind exception is already caught internally by the the Boost library, so it will never make its way to your main. The Boost library throws and catches the forced_unwind exception internally by design.

ecorm avatar Aug 23 '22 17:08 ecorm

@ecorm I understand what you said. My question is that, how should i do if i want to disable or avoid this forced_unwind exception? or I ignore it and do nothing? My system has implemented with many coroutine such as this, when i debug it, I will receive many output message about this forced_unwind exception. It looks so strange that there may be some bugs near this forced_unwind exception. It seems that this exception is always caught during the program leave out these coroutines that spawned by boost::asio::spawn, whenever i select the option [C++ Exceptions] in VisualStudio 2019 or not. This question appears only in boost_1_80_0, not in boost_1_79_0 or before.

The exception setting window: exception_setting The exception output window: exception_output

AsonWon avatar Aug 23 '22 19:08 AsonWon

My question is that, how should i do if i want to disable or avoid this forced_unwind exception? or I ignore it and do nothing?

You can't disable the internal throwing of forced_unwind. Boost.Context and boost::asio::spawn depend on it to do the proper cleanup of the coroutine's stack. I don't know if there's a better way to implement this cleanup other than throwing an exception. The way stackful coroutines are implemented in those libraries is all magic to me.

But yes, you can ignore forced_unwind and do nothing. I haven't used Visual Studio in a long time, but I think there might be IntelliTrace settings where you can turn off logging of thrown and caught exceptions: https://docs.microsoft.com/en-us/visualstudio/debugger/intellitrace-features?view=vs-2022

I agree with you that the frequent internal throwing and catching of forced_unwind can make debugging difficult when you're trying to troubleshoot exceptions thrown by your own code. If you make your application's exceptions derive from std::exception, then you can use the VS exception filter to ignore forced_unwind, which does not derive from std::exception.

Stackful coroutines are expensive to spawn, because they each need their own stack. If your code is spawning them frequently, consider trying to reuse the same long-lived coroutine. Or use the traditional callback technique and save the state of your logic in class member variables. Or consider using C++20 coroutines which are stackless.

The problem you describe is separate from the segfault problem I reported in this issue. Please create a new issue for it.

ecorm avatar Aug 23 '22 20:08 ecorm

@chriskohlhoff Am I correct to assume that the debugging ergonomics problem described by AsonWon is purely due to Boost.Context? If so, the issue should be reported in the Boost.Context repository.

ecorm avatar Aug 23 '22 20:08 ecorm

@ecorm Yes. It's a better way to clean the coroutine's stack by throwing an exception compare with other methods. The asio module is too complex for me to troubleshoot which place caused this question, may be the boost::asio module didn't handle this exception, may be the boost::coroutine module didn't handle this exception, I don't know.

Under normal conditions, the user's program will not generate this exception report in the output window, unless the user's program doesn't catch this exception, or the boost library doesn't catch this exception. It's not an question caught by Visual Studio, I made some attempt on Visual Studio, I cann't ignore this exception to do some settings by the VS exception filter. I‘m sorry to insert my question in this issue only because I went through all the issues under boost::asio, and found that the the issue you created is similar to my question, they are caused by boost::asio::spawn, appear only in boost_1_80_0, and I can reproduce it by modifying your sample program.

@chriskohlhoff How do you think about this? Should I reported this issue in boost::context?

AsonWon avatar Aug 24 '22 02:08 AsonWon

@chriskohlhoff According to my test results, the forced_unwind exception being thrown by boost::asio is not being caught inside the library, the caught code may not be executed inside the library. I use boost_1_80_0-msvc-14.2-64. I compared the code changes about boost::asio/boost::context/boost::coroutine/boost::coroutine2 between boost_1_80_0 and boost_1_79_0, only found that boost::asio and boost::context have changed recently.

changes

AsonWon avatar Aug 24 '22 11:08 AsonWon

@AsonWon can you please paste the output of you test program above when you run it outside of visual studio, at the command prompt.

On Wed, Aug 24, 2022, at 9:45 PM, AsonWon wrote:

@chriskohlhoff https://github.com/chriskohlhoff According to my test results, the forced_unwind exception being thrown by boost::asio is not being caught inside the library, the caught code may not be executed inside the library. I use boost_1_80_0-msvc-14.2-64.

— Reply to this email directly, view it on GitHub https://github.com/chriskohlhoff/asio/issues/1110#issuecomment-1225606862, or unsubscribe https://github.com/notifications/unsubscribe-auth/AADQ5STBJ5S7QFNAYTBLN33V2YDL5ANCNFSM5667OL7Q. You are receiving this because you were mentioned.Message ID: @.***>

chriskohlhoff avatar Aug 24 '22 13:08 chriskohlhoff

@ecorm @chriskohlhoff I'm real sorry to bring trouble to both of you because of my poor understanding of c++ exception. You are right. The c++ programs I wrote before rarely raises any exceptions in Visual Studio's output window, so when I first see the exception record in the output window, I throught there may has a bug near the code with lambda coroutine. I just tried to write a simple C++ exception program and test some results about catch the exception or not. Yes, when the exception throw is being caught, there has one record about exception in Visual Studio's output window and the program exit normally. when the exception throw is not being caught, there has two recordes about exception and the program exit abnormally with an error prompt dialog. The following code is an uncaught exception that I throw myself:

#include <iostream>
#include <stdexcept>
int main(int argc, char *argv[])
{
    throw std::runtime_error("bad");
    return 0;
}

The following screenshot is an uncaught exception that I throw myself. png1 png2 I have closed the exception report in Visual Studio's output window by this: png3

AsonWon avatar Aug 24 '22 14:08 AsonWon

@AsonWon I'm glad you figured it out.

Your findings has still brought up the problem of Boost.Context throwing (and catching) exceptions as part of its normal operations. This is a nuisance for those of us who use the GDB debugger to catch any thrown exception while troubleshooting problems in our own code.

ecorm avatar Aug 24 '22 15:08 ecorm

I've made a tentative change in https://github.com/chriskohlhoff/asio/commit/96e1a95449664d426afeacd010454b9750a34de4 to allow the coroutine to run to completion if the user-supplied function has itself completed. This should eliminate the forced_unwind in that case.

chriskohlhoff avatar Aug 25 '22 00:08 chriskohlhoff

I've made a tentative change in https://github.com/chriskohlhoff/asio/commit/96e1a95449664d426afeacd010454b9750a34de4 to allow the coroutine to run to completion if the user-supplied function has itself completed. This should eliminate the forced_unwind in that case.

I confirm that GDB no longer breaks when set up to break on on any C++ exception thrown, using the following example:

#include <iostream>
#include <memory>
#include <asio/detached.hpp>
#include <asio/io_context.hpp>
#include <asio/spawn.hpp>

//------------------------------------------------------------------------------
int main()
{
    asio::io_context ioctx;

    asio::spawn(
        ioctx,
        [](asio::yield_context yield)
        {
            std::cout << "Coroutine enter" << std::endl;
            auto ptr = std::make_shared<int>(42);
            std::cout << "Coroutine leave" << std::endl;
        }
        // , asio::detached
        );

    ioctx.run();
    std::cout << "After ioctx.run()" << std::endl;

    return 0;
}

I've tried it with ASIO_DISABLE_BOOST_COROUTINE defined and undefined. Valgind doesn't complain about any memory leaks.

When using my original example that throws a std::runtime_error, it only breaks once where std::runtime_error is thrown. Valgrind doesn't complain either when I add auto ptr = std::make_shared<int>(42); to the coroutine.

I'll whip up another test program where the coroutine doesn't run to completion.

ecorm avatar Aug 25 '22 17:08 ecorm

Here's a test where the coroutine does not run to completion:

#include <iostream>
#include <memory>
#include <asio/detached.hpp>
#include <asio/io_context.hpp>
#include <asio/spawn.hpp>
#include <asio/steady_timer.hpp>

//------------------------------------------------------------------------------
int main()
{
    {
        asio::io_context ioctx;
        asio::steady_timer timer(ioctx);
        int count = 0;

        asio::spawn(
            ioctx,
            [&timer, &count](asio::yield_context yield)
            {
                std::cout << "Coroutine enter" << std::endl;
                std::shared_ptr<int> ptr{
                    new int(42),
                    [](int* i)
                    {
                        std::cout << "ptr deleter" << std::endl;
                        delete i;
                    }};
                timer.expires_from_now(std::chrono::milliseconds(1000));
                timer.async_wait(yield);
                std::cout << "Tick" << std::endl;
                ++count;
                timer.expires_from_now(std::chrono::milliseconds(1000));
                timer.async_wait(yield);
                ++count;
                std::cout << "Coroutine leave" << std::endl;
            }
#ifdef ASIO_DISABLE_BOOST_COROUTINE
             , asio::detached
#endif
            );

        while (count == 0)
            ioctx.poll();

        std::cout << "Leaving scope" << std::endl;
    }

    std::cout << "Exiting main" << std::endl;
    return 0;
}

Output:

Coroutine enter
Tick
Leaving scope
ptr deleter
Exiting main

The forced coroutine stack unwinding occurs, as evidenced by ptr deleter being printed and Coroutine leave not being printed.

With "break on C++ exceptions thrown" enabled in GDB, it breaks with the forced_unwind exception thrown right after it prints "Leaving scope".

I get the same behavior with ASIO_DISABLE_BOOST_COROUTINE defined.

Everything seems to be working with https://github.com/chriskohlhoff/asio/commit/96e1a95449664d426afeacd010454b9750a34de4, as far as I can tell. :+1:

ecorm avatar Aug 25 '22 17:08 ecorm

Hi! I faced with the same issue on Windows after upgrading to Boost v1.80.0, https://github.com/chriskohlhoff/asio/commit/13045b6017e13b5884b4a95b6a80f485b42f1edc fixes the crash in my case so I hope it gets merged soon.

k15tfu avatar Aug 26 '22 19:08 k15tfu