-
Notifications
You must be signed in to change notification settings - Fork 2.3k
Description
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
- Connect a
Poco::Redis::Clientto a Redis server - Send a command (e.g., pipelined XADD)
- Kill the Redis server process while the client is reading the reply
- 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 ofException): 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()treatsn <= 0as 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::Clientused for pipelined XADD commands to a Redis server inside a container