chriskohlhoff / asio

Asio C++ Library
http://think-async.com/Asio
4.88k stars 1.21k forks source link

experimental::coro memory leak #1377

Open 0x0badc0de opened 11 months ago

0x0badc0de commented 11 months ago

Boost-1.83, gcc-13.2

If io_context executing experimental::coro is stopped, coro stack is not unwound, memory is not freed. While awaitable does it as expected.

Following code:

#include <boost/asio.hpp>
#include <boost/asio/experimental/coro.hpp>
#include <boost/asio/experimental/use_coro.hpp>
#include <boost/asio/experimental/co_spawn.hpp>

#include <iostream>

using namespace std::literals::chrono_literals;

struct Foo {
    int n = 0;

    Foo(int n_) : n(n_) { std::cout << "Foo " << n << std::endl; }
    ~Foo() { std::cout << "~Foo " << n << std::endl; }
};

int main()
{
    auto coro1 = [](auto&& ioc) -> boost::asio::experimental::coro<> {
        Foo foo(1);
        boost::asio::steady_timer timer(ioc.get_executor());
        timer.expires_after(10s);
        co_await timer.async_wait(boost::asio::experimental::use_coro);
    };

    auto coro2 = [](auto&& ioc) -> boost::asio::awaitable<void> {
        Foo foo(2);
        boost::asio::steady_timer timer(ioc.get_executor());
        timer.expires_after(10s);
        co_await timer.async_wait(boost::asio::use_awaitable);
    };

    auto stop_coro = [](auto&& ioc) -> boost::asio::experimental::coro<> {
        Foo foo(3);
        boost::asio::steady_timer timer(ioc.get_executor());
        timer.expires_after(5s);
        co_await timer.async_wait(boost::asio::experimental::use_coro);
        ioc.stop();
    };

    boost::asio::io_context ioc;
    boost::asio::experimental::co_spawn(coro1(ioc), boost::asio::detached);
    boost::asio::co_spawn(ioc, coro2(ioc), boost::asio::detached);
    boost::asio::experimental::co_spawn(stop_coro(ioc), boost::asio::detached);
    ioc.run();
}

Outputs:

ASM generation compiler returned: 0
Execution build compiler returned: 0
Program returned: 1

=================================================================
==1==ERROR: LeakSanitizer: detected memory leaks
Indirect leak of 320 byte(s) in 1 object(s) allocated from:
    #0 0x7f6e64ef3518 in operator new(unsigned long) (/opt/compiler-explorer/gcc-13.2.0/lib64/libasan.so.8+0xdb518) (BuildId: 5ce9c09d3612315d01d50bcaafaea176e7ddab77)
    #1 0x407d3b in operator()<boost::asio::io_context&> /app/example.cpp:24
    #2 0x407d3b in main /app/example.cpp:42

Indirect leak of 240 byte(s) in 1 object(s) allocated from:
    #0 0x7f6e64ef3518 in operator new(unsigned long) (/opt/compiler-explorer/gcc-13.2.0/lib64/libasan.so.8+0xdb518) (BuildId: 5ce9c09d3612315d01d50bcaafaea176e7ddab77)
    #1 0x415404 in boost::asio::experimental::coro<void () noexcept, void, boost::asio::associated_executor<boost::asio::basic_waitable_timer<std::chrono::_V2::steady_clock, boost::asio::wait_traits<std::chrono::_V2::steady_clock>, boost::asio::any_io_executor>::initiate_async_wait, boost::asio::basic_system_executor<boost::asio::execution::detail::blocking::possibly_t<0>, boost::asio::execution::detail::relationship::fork_t<0>, std::allocator<void> > >::type, std::allocator<void> > boost::asio::async_result<boost::asio::experimental::use_coro_t<std::allocator<void> >, void (boost::system::error_code)>::initiate_impl<boost::asio::basic_waitable_timer<std::chrono::_V2::steady_clock, boost::asio::wait_traits<std::chrono::_V2::steady_clock>, boost::asio::any_io_executor>::initiate_async_wait>(boost::asio::basic_waitable_timer<std::chrono::_V2::steady_clock, boost::asio::wait_traits<std::chrono::_V2::steady_clock>, boost::asio::any_io_executor>::initiate_async_wait, std::allocator_arg_t, std::allocator<void>) /opt/compiler-explorer/libs/boost_1_83_0/boost/asio/experimental/impl/use_coro.hpp:70

Indirect leak of 192 byte(s) in 1 object(s) allocated from:
    #0 0x7f6e64ef3518 in operator new(unsigned long) (/opt/compiler-explorer/gcc-13.2.0/lib64/libasan.so.8+0xdb518) (BuildId: 5ce9c09d3612315d01d50bcaafaea176e7ddab77)
    #1 0x46b34e in boost::asio::experimental::detail::partial_coro boost::asio::experimental::detail::post_coroutine<boost::asio::any_io_executor, boost::asio::detail::prepend_handler<boost::asio::detail::composed_op<boost::asio::experimental::detail::coro_spawn_op<void, void, boost::asio::any_io_executor>, boost::asio::detail::composed_work<void (boost::asio::any_io_executor)>, boost::asio::detail::detached_handler, void (std::__exception_ptr::exception_ptr)>, int>, std::__exception_ptr::exception_ptr&>(boost::asio::any_io_executor, boost::asio::detail::prepend_handler<boost::asio::detail::composed_op<boost::asio::experimental::detail::coro_spawn_op<void, void, boost::asio::any_io_executor>, boost::asio::detail::composed_work<void (boost::asio::any_io_executor)>, boost::asio::detail::detached_handler, void (std::__exception_ptr::exception_ptr)>, int>, std::__exception_ptr::exception_ptr&) /opt/compiler-explorer/libs/boost_1_83_0/boost/asio/experimental/detail/partial_promise.hpp:167

SUMMARY: AddressSanitizer: 752 byte(s) leaked in 3 allocation(s).
Foo 1
Foo 2
Foo 3
~Foo 3
~Foo 2

While I'd expect it to output

Foo 1
Foo 2
Foo 3
~Foo 3
~Foo 2
~Foo 1

https://godbolt.org/z/PxKo87vW7

klemens-morgenstern commented 11 months ago

Aren't you spawning the thing onto the system_executor?

0x0badc0de commented 11 months ago

If I enable handle tracking and print ioc address on the begging, following is printed:

ioc=0x7ffc7cd95490
@asio|1698406999.105721|0*1|io_context@0x7ffc7cd95490.execute
@asio|1698406999.105760|0^2|in 'co_spawn_entry_point' (/usr/include/boost/asio/impl/co_spawn.hpp:188)
@asio|1698406999.105760|0*2|io_context@0x7ffc7cd95490.execute
@asio|1698406999.105770|0*3|io_context@0x7ffc7cd95490.execute
@asio|1698406999.105775|>1|
Foo 1
@asio|1698406999.105801|1*4|deadline_timer@0x55d37d75c910.async_wait
@asio|1698406999.105809|<1|
@asio|1698406999.105812|>2|
Foo 2
@asio|1698406999.105820|2*5|deadline_timer@0x55d37d75ce48.async_wait
@asio|1698406999.105824|<2|
@asio|1698406999.105827|>3|
Foo 3
@asio|1698406999.105834|3*6|deadline_timer@0x55d37d75d020.async_wait
@asio|1698406999.105840|<3|
@asio|1698407004.106107|>6|ec=system:0
@asio|1698407004.106147|6|deadline_timer@0x55d37d75d020.cancel
~Foo 3
@asio|1698407004.106163|6*7|io_context@0x7ffc7cd95490.execute
@asio|1698407004.106168|<6|
@asio|1698407004.106176|5*8|io_context@0x7ffc7cd95490.execute
@asio|1698407004.106179|~5|
@asio|1698407004.106182|~4|
@asio|1698407004.106187|7*9|io_context@0x7ffc7cd95490.execute
@asio|1698407004.106190|~7|
@asio|1698407004.106221|8|deadline_timer@0x55d37d75ce48.cancel
~Foo 2
@asio|1698407004.106227|~8|
@asio|1698407004.106231|~9|

Local io_context only and deadline_times, no mentions of system executor.

klemens-morgenstern commented 11 months ago

Oh right, it doesn't register the spawned coro into a service, so in that case it may leak. I'll take a look, that's a legit bug.