asio
asio copied to clipboard
Segfault when exception thrown from within stackful coroutine
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
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 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 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)?
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:
The test result for boost_1_80_0:
The setting for 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?
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
@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 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.
Yep: https://github.com/chriskohlhoff/asio/issues/1111#issuecomment-1223584534
Instead of detached
pass [](std::exception_ptr e){ if (e) std::rethrow_exception(e); }
Well. If I cancel the select option [不在此列表中的所有C++ Exception], I will find an exception in the output window:
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?
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
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 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 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:
The exception output window:
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.
@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 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?
@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.
@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: @.***>
@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.
I have closed the exception report in Visual Studio's output window by this:
@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.
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'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.
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:
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.