Skip to content

RedisStreamBuf: SIGSEGV when socket dies during std::getline() #5217

@aleks-f

Description

@aleks-f

Describe the bug

RedisStreamBuf::readFromDevice() and writeToDevice() do not catch exceptions from receiveBytes()/sendBytes(). When the Redis server dies (e.g., process killed, container destroyed), the socket operations throw Poco::IOException or Poco::Net::ConnectionResetException. These exceptions propagate through BufferedStreamBuf::underflow() into std::getline(), which is not exception-safe — causing undefined behavior that manifests as a SIGSEGV.

Root cause

RedisStreamBuf::readFromDevice() directly calls _redis.receiveBytes():

int RedisStreamBuf::readFromDevice(char* buffer, std::streamsize len)
{
    return _redis.receiveBytes(buffer, static_cast<int>(len));
}

When the remote end dies, receiveBytes() throws. The call chain is:

Client::readReply()
  → _pInput->get()                    // std::istream::get()
    → RedisStreamBuf::underflow()     // via std::streambuf::sbumpc()
      → readFromDevice()
        → receiveBytes()              // throws ConnectionResetException

Or via result->read(*_pInput):

Client::readReply()
  → result->read(*_pInput)
    → RedisInputStream::getline()
      → std::getline(*this, line)     // not exception-safe
        → RedisStreamBuf::underflow()
          → readFromDevice()
            → receiveBytes()          // throws

BufferedStreamBuf::underflow() handles readFromDevice() returning n <= 0 correctly (returns char_traits::eof()), but does not catch exceptions. The exception propagates through std::getline(), which in many standard library implementations is not exception-safe when underflow() throws — resulting in undefined behavior (SIGSEGV).

The same issue exists in writeToDevice() with sendBytes().

To Reproduce

  1. Connect a Poco::Redis::Client to a Redis server
  2. Send a command (e.g., pipelined XADD)
  3. Kill the Redis server process while the client is reading the reply
  4. The client crashes with SIGSEGV in RedisInputStream::getline()

Crash backtrace:

#0  Poco::Redis::RedisInputStream::getline()
#1  Poco::Redis::Type<std::string>::read()
#2  Poco::Redis::Client::readReply()
#3  Poco::Redis::Client::execute<std::string>()

This is reliably triggered by shutting down Redis while a client application is actively writing/reading.

Fix

Wrap receiveBytes() and sendBytes() in try-catch blocks so exceptions are converted to error return values that BufferedStreamBuf::underflow()/overflow() already handle:

-int RedisStreamBuf::readFromDevice(char* buffer, std::streamsize len)
-{
-    return _redis.receiveBytes(buffer, static_cast<int>(len));
-}
+int RedisStreamBuf::readFromDevice(char* buffer, std::streamsize len)
+{
+    try
+    {
+        return _redis.receiveBytes(buffer, static_cast<int>(len));
+    }
+    catch (Poco::TimeoutException&)
+    {
+        return -1;
+    }
+    catch (Poco::Exception&)
+    {
+        return _redis.getError();
+    }
+    catch (...)
+    {
+        return -1;
+    }
+}

Same pattern for writeToDevice().

The three catch clauses handle:

  • TimeoutException (caught first, subclass of Exception): returns -1. A timeout has no meaningful socket error code — it's a transient condition, not an orderly close (which would be 0).
  • Poco::Exception (connection reset, broken pipe, etc.): returns _redis.getError() (SO_ERROR), which is typically a non-zero errno value. underflow() treats n <= 0 as EOF.
  • ... (unknown exceptions): returns -1 as a safe fallback.

In all cases, underflow() returns char_traits::eof(), Client::readReply() sees EOF from _pInput->get(), calls disconnect(), and throws RedisException("Lost connection to Redis server") — allowing the caller to handle reconnection cleanly.

Environment

  • OS: Linux
  • POCO Version: 1.15.0
  • Crash observed with Poco::Redis::Client used for pipelined XADD commands to a Redis server inside a container

Metadata

Metadata

Assignees

Labels

Type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions