4

I want to send unsolicited messages over an SSL connection. Meaning that the server sends a message not based on a request from a client, but because some event happened that the client needs to know about.

I just use the SSL server example from the boost site, added a timer that sends 'hello' after 10 seconds, everything works fine before the timer expires (the server echo's everything), the 'hello' is also received, but after that the application crashes on the next time a text is sent to the server.

For me even more strange is the fact that when I disable the SSL code, so use a normal socket and do the same using telnet, it works FINE and keeps on working fine!!!

I ran into this problem for the second time now, and I really do not have an idea why this is happening the way it happens.

Below is the total source that I altered to demonstrate the problem. Compile it without the SSL define and use telnet and everything works OK, define SSL and use openssl, or the client SSL example from the boost website and the thing crashes.

#include <cstdlib>
#include <iostream>
#include <boost/bind.hpp>
#include <boost/asio.hpp>
#include <boost/asio/ssl.hpp>

//#define SSL

typedef boost::asio::ssl::stream<boost::asio::ip::tcp::socket> ssl_socket;

class session
{
public:
  session(boost::asio::io_service& io_service,
      boost::asio::ssl::context& context)
#ifdef SSL  
    : socket_(io_service, context)
#else
    : socket_(io_service)

#endif    
  {
  }

  ssl_socket::lowest_layer_type& socket()
  {
    return socket_.lowest_layer();
  }

  void start()
  {
#ifdef SSL      
    socket_.async_handshake(boost::asio::ssl::stream_base::server,
        boost::bind(&session::handle_handshake, this,
          boost::asio::placeholders::error));
#else
      socket_.async_read_some(boost::asio::buffer(data_, max_length),
          boost::bind(&session::handle_read, this,
            boost::asio::placeholders::error,
            boost::asio::placeholders::bytes_transferred));

      boost::shared_ptr< boost::asio::deadline_timer > timer(new     boost::asio::deadline_timer( socket_.get_io_service() ));
      timer->expires_from_now( boost::posix_time::seconds( 10 ) );
      timer->async_wait( boost::bind( &session::SayHello, this, _1, timer ) );

#endif    
  }

  void handle_handshake(const boost::system::error_code& error)
  {
    if (!error)
    {
      socket_.async_read_some(boost::asio::buffer(data_, max_length),
          boost::bind(&session::handle_read, this,
            boost::asio::placeholders::error,
            boost::asio::placeholders::bytes_transferred));

      boost::shared_ptr< boost::asio::deadline_timer > timer(new     boost::asio::deadline_timer( socket_.get_io_service() ));
      timer->expires_from_now( boost::posix_time::seconds( 10 ) );
      timer->async_wait( boost::bind( &session::SayHello, this, _1, timer ) );

    }
    else
    {
      delete this;
    }
  }

  void SayHello(const boost::system::error_code& error, boost::shared_ptr<     boost::asio::deadline_timer > timer) {
      std::string hello = "hello";

        boost::asio::async_write(socket_,
          boost::asio::buffer(hello, hello.length()),
          boost::bind(&session::handle_write, this,
            boost::asio::placeholders::error));

      timer->expires_from_now( boost::posix_time::seconds( 10 ) );
      timer->async_wait( boost::bind( &session::SayHello, this, _1, timer ) );

  }


  void handle_read(const boost::system::error_code& error,
      size_t bytes_transferred)
  {
    if (!error)
    {
      boost::asio::async_write(socket_,
          boost::asio::buffer(data_, bytes_transferred),
          boost::bind(&session::handle_write, this,
            boost::asio::placeholders::error));
    }
    else
    {
      std::cout << "session::handle_read() -> Delete, ErrorCode: "<< error.value() <<     std::endl;
      delete this;
    }
  }

  void handle_write(const boost::system::error_code& error)
  {
    if (!error)
    {
      socket_.async_read_some(boost::asio::buffer(data_, max_length),
          boost::bind(&session::handle_read, this,
            boost::asio::placeholders::error,
            boost::asio::placeholders::bytes_transferred));
    }
    else
    {
      std::cout << "session::handle_write() -> Delete, ErrorCode: "<< error.value() <<     std::endl;
      delete this;
    }
  }

private:
#ifdef SSL    
  ssl_socket socket_;
#else
  boost::asio::ip::tcp::socket socket_;
#endif  
  enum { max_length = 1024 };
  char data_[max_length];
};

class server
{
public:
  server(boost::asio::io_service& io_service, unsigned short port)
    : io_service_(io_service),
      acceptor_(io_service,
          boost::asio::ip::tcp::endpoint(boost::asio::ip::tcp::v4(), port)),
      context_(boost::asio::ssl::context::sslv23)
  {
#ifdef SSL        
    context_.set_options(
        boost::asio::ssl::context::default_workarounds
        | boost::asio::ssl::context::no_sslv2
        | boost::asio::ssl::context::single_dh_use);
    context_.set_password_callback(boost::bind(&server::get_password, this));
    context_.use_certificate_chain_file("server.crt");
    context_.use_private_key_file("server.key", boost::asio::ssl::context::pem);
    context_.use_tmp_dh_file("dh512.pem");
#endif
    start_accept();
  }

  std::string get_password() const
  {
    return "test";
  }

  void start_accept()
  {
    session* new_session = new session(io_service_, context_);
    acceptor_.async_accept(new_session->socket(),
        boost::bind(&server::handle_accept, this, new_session,
          boost::asio::placeholders::error));
  }

  void handle_accept(session* new_session,
      const boost::system::error_code& error)
  {
    if (!error)
    {
      new_session->start();
    }
    else
    {
      delete new_session;
    }

    start_accept();
  }

private:
  boost::asio::io_service& io_service_;
  boost::asio::ip::tcp::acceptor acceptor_;
  boost::asio::ssl::context context_;
};

int main(int argc, char* argv[])
{
  try
  {
    boost::asio::io_service io_service;

    using namespace std; // For atoi.
    server s(io_service, 7777 /*atoi(argv[1])*/);

    io_service.run();
  }
  catch (std::exception& e)
  {
    std::cerr << "Exception: " << e.what() << "\n";
  }

  return 0;
}

I use boost 1.49 and OpenSSL 1.0.0i-fips 19 Apr 2012. I tried investigating this problem as much as possible, the last time I had this problem (a couple of months ago), I received an error number that I could trace to this error message: error: decryption failed or bad record mac.

But I have no idea what is going wrong and how to fix this, any suggestions are welcome.

ikku
  • 405
  • 3
  • 14

1 Answers1

9

The problem is multiple concurrent async read and writes. I were able to crash this program even with raw sockets (glibc detected double free or corruption). Let's see what happens after session starts (in braces I put number of concurrent scheduled async reads and writes):

  1. schedule async read (1, 0)

  2. (assume that data comes) handle_read is executed, it schedules async write (0, 1)

  3. (data are written) handle_write is executed, it schedules async read (1, 0)

    Now, it could loop over 1. - 3. without any problem indefinitely. But then timer expires...

  4. (assume, that no new data come from client, so there is still one async read scheduled) timer expires, so SayHello is executed, it schedules async write, still no problem (1, 1)

  5. (data from SayHello are written, but still no new data comes from client) handle_write is executed, it schedules async read (2, 0)

    Now, we are done. If any new data from client will come, part of them could be read by one async read and part by another. For raw sockets, it might even seem to work (despite possibility, that there might be 2 concurrent writes scheduled, so echo on client side might look mixed). For SSL this might corrupt incoming data stream, and this is probably what happens.

How to fix it:

  1. strand will not help in this case (it is not concurrent handler executions, but scheduled async reads and writes).
  2. It is not enough, if async write handler in SayHello does nothing (there will be no concurrent reads then, but still concurrent writes might occur).
  3. If you really want to have two diffident kind of writes (echo and timer), you have to implement some kind of queue of messages to write, to avoid mixing writes from echo and timer.
  4. General remark: it was simple example, but using shared_ptr instead of delete this is much better way of handling memory allocation with boost::asio. It will prevent from missing errors resulting in memory leak.
Community
  • 1
  • 1
Greg
  • 1,660
  • 13
  • 12
  • Thank you very much for your very clear analysis, I understand the problem now. But instead of adding a queue, the mixing writes could also be prevented by using a mutex, right? – ikku Aug 12 '12 at 12:01
  • To use mutex, you would need to use sync writes also, which is probably not what you want. Instead of queue, one extra buffer and bool flag could do the trick. It is even better, because it limits memory usage. – Greg Aug 12 '12 at 12:07
  • Doesn't boost::asio already does a form of queuing? Multiple calls to `boost::asio::async_write` will mix up the sent data? – ikku Aug 12 '12 at 12:12
  • That a queue will do the job, I understand, but please explain your second option, the extra buffer and bool flag. I haven't got a picture clear of that solution. – ikku Aug 12 '12 at 12:16
  • 2
    Unfortunately, it is up to programmer to not schedule multiple async_writes. Please see http://stackoverflow.com/questions/7754695/boost-asio-async-write-how-to-not-interleaving-async-write-calls. For the bool flag and extra buffer, you could have flag "there is write in progress", in this case you copy data to extra buffer and set another flag "data in buffer" (sorry, you need two flags), when write handler finishes and sees "data in buffer", it writes buffer again instead of scheduling read. This assumes, that no source (timer, echo) sends two messages at a time. Tricky, but possible. – Greg Aug 12 '12 at 12:23
  • 1
    In the end I implemented a queue based on the link you give, and all works fine now. Thank you again! – ikku Aug 12 '12 at 14:00