c++boostboost-asioboost-beast

call ssl::stream::async_shutdown after read operation cancelled


I have one active async operation, it is http::async_read. At the moment when I want to close the ssl connection, I cancel this async operation on the socket by calling ip::tcp::socket::cancel.

Then I terminate the ssl connection like this

socket_wrapper.cancel(); // cancel http::async_read
boost::asio::post(socket_wrapper.get_stream().get_executor(), [this, self]
{
    socket_wrapper.get_stream().async_shutdown([this, self](const boost::system::error_code& ec)
    {
        socket_wrapper.close();
    });
});

I call boost::asio::post to make sure the completion handler for the cancelled http::async_read was called. After that I call async_shutdown. The async_shutdown operation completes successfully in most cases.

However, sometimes I get an exception: "Exception thrown: read access violation. s was nullptr." and the debugger points to the following code from the boost::asio sources:

int engine::do_shutdown(void*, std::size_t)
{
  int result = ::SSL_shutdown(ssl_);
  if (result == 0)
    result = ::SSL_shutdown(ssl_);
  return result;
}

What could be the reason?


Solution

  • I call boost::asio::post to make sure the completion handler for the cancelled http::async_read was called

    Sadly, that doesn't work. Post doesn't ensure completion happens in a specific order. It may or may not (if you very diligently control threads and poll() on the context you might be able to get it right, but I wouldn't risk it and your code doesn't mention it, so I'm assuming you didn't do anything to orchestrate order of handlers).

    You need specific synchronization before you even post the shutdown.

    Here's completely deconstructed example, knowing that you do not want to rely on thread synchronization primitives (good idea, see below):

    Live On Coliru

    #define BOOST_ASIO_ENABLE_HANDLER_TRACKING
    #include <boost/asio.hpp>
    #include <boost/asio/ssl.hpp>
    #include <iostream>
    
    using namespace std::literals;
    namespace asio = boost::asio;
    namespace ssl  = asio::ssl;
    using asio::ip::tcp;
    using boost::system::error_code;
    
    static std::atomic_bool s_write_completed{false};
    static std::atomic_bool s_read_completed{false};
    
    static void read_handler(error_code ec, size_t) {
        s_read_completed = true;
        std::cout << "Read completion: " << ec.message() << std::endl;
    }
    
    static void write_handler(error_code ec, size_t) {
        s_write_completed = true;
        std::cout << "Write completion: " << ec.message() << std::endl;
    }
    
    static void shutdown_handler(error_code ec) { //
        std::cout << "Shutdown completion: " << ec.message() << std::endl;
    }
    
    int main() {
        asio::io_context         ioc(1); // single thread
        ssl::context             ctx(ssl::context::sslv23);
        ssl::stream<tcp::socket> s(ioc, ctx);
        s.lowest_layer().connect({{}, 8989});
        s.handshake(ssl::stream_base::client); // synchronous for simplicity
    
        // deferred ops
        asio::cancellation_signal signal;
    
        s.async_write_some(asio::buffer("Hello, world!\n"sv),
                           bind_cancellation_slot(signal.slot(), write_handler));
    
        std::array<char, 1024> buffer;
        s.async_read_some(asio::buffer(buffer),
                          bind_cancellation_slot(signal.slot(), read_handler));
    
        // std::thread io(io_worker, std::ref(ioc));
    
        asio::steady_timer tim2(ioc, 50ms);
        tim2.async_wait([&](error_code) { //
            signal.emit(asio::cancellation_type::all);
        });
    
        // process all pending operations
        while (ioc.run_one())
            std::cout << "Progress: " << s_read_completed << ", " << s_write_completed << std::endl;
    
        assert(s_read_completed);  // read operation should have completed
        assert(s_write_completed); // write operation should have completed
    
        s.async_shutdown(shutdown_handler);
    
        ioc.restart();
        ioc.run(); // run to completion
    }
    

    Which will print as standard output:

    Write completion: Success
    Progress: 0, 1
    Progress: 0, 1
    Progress: 0, 1
    Progress: 0, 1
    Read completion: Operation canceled
    Progress: 1, 1
    Shutdown completion: Success
    

    The handler vizualization:

    enter image description here

    Legend: some receives succeed as part of SSL negotiation. The ec125 indicates cancellation. The "right most column" represents the shutdown which is initiated AFTER the read was completed with asio::error::operation_aborted, and consists of a read & receive, as per SSL protocol.

    BONUS

    Normally, you don't bother with all the detail. You would have async operations manage the lifetime of your session object, and implicitly know when all your operations completed because the destructor is invoked. There you safely deal with shutdown, keeping in mind not to throw exceptions:

    Live On Coliru

    #define BOOST_ASIO_ENABLE_HANDLER_TRACKING
    #include <boost/asio.hpp>
    #include <boost/asio/ssl.hpp>
    #include <deque>
    #include <iostream>
    
    using namespace std::literals;
    namespace asio = boost::asio;
    namespace ssl  = asio::ssl;
    using asio::ip::tcp;
    using boost::system::error_code;
    
    struct ClientSession : std::enable_shared_from_this<ClientSession> {
        ClientSession(asio::any_io_executor ex) : s{std::move(ex), ctx_} {
            s.lowest_layer().connect({{}, 8989});
            s.handshake(ssl::stream_base::client); // synchronous for simplicity
        }
    
        void start() { do_read_loop(); }
    
        void send(std::string msg) {
            post(s.get_executor(), [this, self = shared_from_this(), m = std::move(msg)]() mutable {
                outbox_.push_back(std::move(m));
                if (outbox_.size() == 1)
                    do_write_loop();
            });
        }
    
        void shutdown() {
            post(s.get_executor(), [this, self = shared_from_this()]() {
                s.lowest_layer().cancel();
            });
        }
    
        ~ClientSession() {
            error_code ec;
            s.shutdown(ec); // synchronous shutdown
            std::cout << "Shutdown: " << ec.message() << std::endl;
        }
    
      private:
        ssl::context             ctx_{ssl::context::sslv23};
        ssl::stream<tcp::socket> s;
    
        std::array<char, 1024> buffer_;
        std::deque<std::string> outbox_;
    
        void do_read_loop() {
            s.async_read_some(asio::buffer(buffer_), [this, self = shared_from_this()](error_code ec, size_t n) {
                if (ec)
                    std::cout << "Read error: " << ec.message() << std::endl;
                else {
                    std::cout << "Read " << n << " bytes: " << std::string(buffer_.data(), n) << std::endl;
                    do_read_loop(); // continue reading
                }
            });
        }
    
        void do_write_loop() {
            if (outbox_.empty())
                return;
    
            asio::async_write( //
                s, asio::buffer(outbox_.front()), [this, self = shared_from_this()](error_code ec, size_t n) {
                    if (ec) {
                        std::cout << "Write error: " << ec.message() << std::endl;
                    } else {
                        outbox_.pop_front();
                        std::cout << "Wrote " << n << " bytes." << std::endl;
                    }
                    do_write_loop(); // continue writing
                });
        }
    };
    
    int main() {
        asio::io_context ioc(1); // single thread
    
        {
            auto sess = make_shared<ClientSession>(ioc.get_executor()); //
            sess->start();
    
            ioc.run_for(1s);
            sess->send("Hello, world!\n"); // send a message
    
            ioc.run_for(3s);
            sess->send("Bye, world!\n"); // send a message
            sess->shutdown(); // cancellation
        } // sess goes out of scope here, so destructor will run once cancellation(s) complete
    
        // run to completion
        ioc.run();
    }
    

    A local demo against openssl s_server -port 8989 ...:

    In text, e.g.:

    Read 18 bytes: Hello from server
    
    Wrote 14 bytes.
    Read 18 bytes: Hello from server
    
    Read 18 bytes: Hello from server
    
    Read 18 bytes: Hello from server
    
    Wrote 12 bytes.
    Read error: Operation canceled
    Shutdown: Success