Backport #102894 to 26.4: Fix short writes in Poco BufferedStreamBuf::flushBuffer

This commit is contained in:
robot-clickhouse
2026-04-24 08:58:07 +00:00
parent 0ba8641d9f
commit 3bda96ae3d
8 changed files with 536 additions and 21 deletions
@@ -136,12 +136,16 @@ private:
int flushBuffer()
{
int n = int(this->pptr() - this->pbase());
if (writeToDevice(this->pbase(), n) == n)
int offset = 0;
while (offset < n)
{
this->pbump(-n);
return n;
int written = writeToDevice(this->pbase() + offset, n - offset);
if (written <= 0)
return -1;
offset += written;
}
return -1;
this->pbump(-n);
return n;
}
std::streamsize _bufsize;
@@ -134,12 +134,16 @@ private:
int flushBuffer()
{
int n = int(this->pptr() - this->pbase());
if (writeToDevice(this->pbase(), n) == n)
int offset = 0;
while (offset < n)
{
this->pbump(-n);
return n;
int written = writeToDevice(this->pbase() + offset, n - offset);
if (written <= 0)
return -1;
offset += written;
}
return -1;
this->pbump(-n);
return n;
}
std::streamsize _bufsize;
+12 -3
View File
@@ -52,8 +52,8 @@ void HTTPChunkedStreamBuf::close()
if (_mode & std::ios::out && _chunk != std::char_traits<char>::eof())
{
sync();
_session.write("0\r\n\r\n", 5);
if (writeToDevice("", 0) < 0)
throw MessageException("Failed to write terminating chunk");
_chunk = std::char_traits<char>::eof();
}
}
@@ -167,7 +167,16 @@ int HTTPChunkedStreamBuf::writeToDevice(const char* buffer, std::streamsize leng
_chunkBuffer.append("\r\n", 2);
_chunkBuffer.append(buffer, static_cast<std::string::size_type>(length));
_chunkBuffer.append("\r\n", 2);
_session.write(_chunkBuffer.data(), static_cast<std::streamsize>(_chunkBuffer.size()));
std::streamsize chunkSize = static_cast<std::streamsize>(_chunkBuffer.size());
std::streamsize offset = 0;
while (offset < chunkSize)
{
int written = _session.write(_chunkBuffer.data() + offset, chunkSize - offset);
if (written <= 0)
return -1;
offset += written;
}
return static_cast<int>(length);
}
+5 -8
View File
@@ -68,14 +68,11 @@ int HTTPFixedLengthStreamBuf::readFromDevice(char* buffer, std::streamsize lengt
int HTTPFixedLengthStreamBuf::writeToDevice(const char* buffer, std::streamsize length)
{
int n = 0;
if (_count < _length)
{
if (_count + length > _length)
length = static_cast<std::streamsize>(_length - _count);
n = _session.write(buffer, length);
if (n > 0) _count += n;
}
if (_count + length > _length)
throw MessageException("Write past Content-Length");
int n = _session.write(buffer, length);
if (n > 0) _count += n;
return n;
}
@@ -322,6 +322,8 @@ int SecureSocketImpl::sendBytes(const void* buffer, int length, int flags)
rc = handleError(rc);
if (rc == 0) throw SSLConnectionUnexpectedlyClosedException();
if (rc < 0 && _pSocket->getBlocking())
throw Poco::TimeoutException("SSL_write timed out");
}
_pSocket->useSendThrottlerBudget(rc);
@@ -364,7 +366,10 @@ int SecureSocketImpl::receiveBytes(void* buffer, int length, int flags)
while (mustRetry(rc, remaining_time));
if (rc <= 0)
{
return handleError(rc);
rc = handleError(rc);
if (rc < 0 && _pSocket->getBlocking())
throw Poco::TimeoutException("SSL_read timed out");
return rc;
}
_pSocket->useRecvThrottlerBudget(rc);
@@ -398,7 +403,10 @@ int SecureSocketImpl::completeHandshake()
while (mustRetry(rc, remaining_time));
if (rc <= 0)
{
return handleError(rc);
rc = handleError(rc);
if (rc < 0 && _pSocket->getBlocking())
throw Poco::TimeoutException("SSL handshake timed out");
return rc;
}
_needHandshake = false;
return rc;
@@ -0,0 +1,161 @@
#include <gtest/gtest.h>
#include <Poco/BufferedStreamBuf.h>
#include <ostream>
#include <sstream>
#include <vector>
/// Mock BufferedStreamBuf that simulates short writes from the device.
/// Each call to writeToDevice writes at most `max_write_size` bytes.
class ShortWriteStreamBuf : public Poco::BufferedStreamBuf
{
public:
ShortWriteStreamBuf(int buffer_size, int max_write_size_)
: Poco::BufferedStreamBuf(buffer_size, std::ios::out)
, max_write_size(max_write_size_)
{
}
const std::string & written() const { return output; }
int writeCount() const { return write_calls; }
private:
int writeToDevice(const char * buffer, std::streamsize length) override
{
++write_calls;
int to_write = std::min(static_cast<int>(length), max_write_size);
output.append(buffer, to_write);
return to_write;
}
std::string output;
int max_write_size;
int write_calls = 0;
};
/// Mock that fails after writing a certain number of bytes total.
class FailAfterNStreamBuf : public Poco::BufferedStreamBuf
{
public:
FailAfterNStreamBuf(int buffer_size, int fail_after_)
: Poco::BufferedStreamBuf(buffer_size, std::ios::out)
, fail_after(fail_after_)
{
}
const std::string & written() const { return output; }
private:
int writeToDevice(const char * buffer, std::streamsize length) override
{
if (total_written >= fail_after)
return 0;
int to_write = std::min(static_cast<int>(length), fail_after - total_written);
output.append(buffer, to_write);
total_written += to_write;
return to_write;
}
std::string output;
int fail_after;
int total_written = 0;
};
TEST(BufferedStreamBuf, FlushHandlesShortWrites)
{
/// Buffer size 16, device writes at most 3 bytes per call.
ShortWriteStreamBuf buf(16, 3);
std::ostream os(&buf);
std::string data = "Hello, World!"; /// 13 bytes
os.write(data.data(), data.size());
os.flush();
ASSERT_TRUE(os.good()) << "Stream should be in good state after flush";
ASSERT_EQ(buf.written(), data);
/// 13 bytes with max 3 per call = at least 5 writeToDevice calls
ASSERT_GE(buf.writeCount(), 5);
}
TEST(BufferedStreamBuf, FlushHandlesShortWritesSingleByte)
{
/// Worst case: device writes 1 byte at a time.
ShortWriteStreamBuf buf(32, 1);
std::ostream os(&buf);
std::string data = "Short write test with single byte device";
os.write(data.data(), data.size());
os.flush();
ASSERT_TRUE(os.good());
ASSERT_EQ(buf.written(), data);
ASSERT_EQ(buf.writeCount(), static_cast<int>(data.size()));
}
TEST(BufferedStreamBuf, FlushHandlesFullWriteInOneCall)
{
/// Device writes everything in one call -- loop should iterate once.
ShortWriteStreamBuf buf(64, 1000);
std::ostream os(&buf);
std::string data = "All at once";
os.write(data.data(), data.size());
os.flush();
ASSERT_TRUE(os.good());
ASSERT_EQ(buf.written(), data);
ASSERT_EQ(buf.writeCount(), 1);
}
TEST(BufferedStreamBuf, FlushSetsErrorOnWriteFailure)
{
/// Device fails immediately (returns 0).
FailAfterNStreamBuf buf(16, 0);
std::ostream os(&buf);
os.exceptions(std::ios::badbit);
std::string data = "Will fail";
os.write(data.data(), data.size());
ASSERT_THROW(os.flush(), std::ios_base::failure);
}
TEST(BufferedStreamBuf, FlushWritesPartialThenFails)
{
/// Device writes 5 bytes then fails.
FailAfterNStreamBuf buf(32, 5);
std::ostream os(&buf);
os.exceptions(std::ios::badbit);
std::string data = "1234567890"; /// 10 bytes, device fails after 5
os.write(data.data(), data.size());
ASSERT_THROW(os.flush(), std::ios_base::failure);
/// The first 5 bytes should have been written before failure.
ASSERT_EQ(buf.written(), "12345");
}
TEST(BufferedStreamBuf, OverflowTriggersShortWriteLoop)
{
/// Buffer size 8, device writes 3 bytes at a time.
/// Writing 20 bytes should trigger overflow (flushing the 8-byte buffer
/// via the short-write loop) then continue filling the buffer.
ShortWriteStreamBuf buf(8, 3);
std::ostream os(&buf);
std::string data = "01234567ABCDEFGHIJKL"; /// 20 bytes
os.write(data.data(), data.size());
os.flush();
ASSERT_TRUE(os.good());
ASSERT_EQ(buf.written(), data);
}
@@ -0,0 +1,103 @@
#include <gtest/gtest.h>
#include <Poco/Net/HTTPFixedLengthStream.h>
#include <Poco/Net/HTTPClientSession.h>
#include <Poco/Net/ServerSocket.h>
#include <Poco/Net/StreamSocket.h>
#include <Poco/Net/SocketAddress.h>
#include <Poco/Net/NetException.h>
namespace
{
/// A listening socket that never accepts. The kernel's listen backlog
/// completes the TCP handshake, so a client can connect and write small
/// amounts of data (absorbed by the kernel send buffer) without a server
/// thread.
class PassiveListener
{
public:
PassiveListener() : server_socket(Poco::Net::SocketAddress("127.0.0.1", 0), 1) {}
Poco::Net::StreamSocket connect()
{
Poco::Net::StreamSocket sock;
sock.connect(Poco::Net::SocketAddress("127.0.0.1", server_socket.address().port()));
return sock;
}
private:
Poco::Net::ServerSocket server_socket;
};
}
/// Writing exactly Content-Length bytes should succeed.
TEST(HTTPFixedLengthStreamBuf, WriteExactLength)
{
PassiveListener listener;
Poco::Net::HTTPClientSession session(listener.connect());
Poco::Net::HTTPFixedLengthOutputStream stream(session, 10);
stream.write("0123456789", 10);
stream.flush();
ASSERT_TRUE(stream.good()) << "Stream should be good after writing exactly Content-Length bytes";
}
/// Writing more than Content-Length should throw MessageException.
TEST(HTTPFixedLengthStreamBuf, WriteOverLengthThrows)
{
PassiveListener listener;
Poco::Net::HTTPClientSession session(listener.connect());
/// Content-Length is 5, but we will try to write 10 bytes.
Poco::Net::HTTPFixedLengthOutputStream stream(session, 5);
/// The data goes into the 8KB buffer first. On flush, flushBuffer calls
/// writeToDevice which clamps to Content-Length (writes 5 bytes), then the
/// loop calls writeToDevice again with the remaining 5 bytes, which throws
/// MessageException because _count >= _length.
stream.write("0123456789", 10);
bool got_exception = false;
try
{
stream.flush();
}
catch (const Poco::Net::MessageException &)
{
got_exception = true;
}
ASSERT_TRUE(got_exception) << "Expected MessageException when writing past Content-Length";
}
/// Writing exactly Content-Length and then one more byte should throw.
TEST(HTTPFixedLengthStreamBuf, WriteBoundaryPlusOneThrows)
{
PassiveListener listener;
Poco::Net::HTTPClientSession session(listener.connect());
Poco::Net::HTTPFixedLengthOutputStream stream(session, 5);
/// Write exactly 5 + 1 bytes.
stream.write("012345", 6);
bool got_exception = false;
try
{
stream.flush();
}
catch (const Poco::Net::MessageException &)
{
got_exception = true;
}
ASSERT_TRUE(got_exception) << "Expected MessageException when writing Content-Length + 1";
}
+229
View File
@@ -0,0 +1,229 @@
#include "config.h"
#if USE_SSL
#include <gtest/gtest.h>
#include <Poco/Net/SecureServerSocket.h>
#include <Poco/Net/SecureStreamSocket.h>
#include <Poco/Net/Context.h>
#include <Poco/Net/SSLException.h>
#include <Poco/Net/SSLManager.h>
#include <Poco/SharedPtr.h>
#include <Poco/Timespan.h>
#include <Poco/Exception.h>
#include <openssl/evp.h>
#include <openssl/pem.h>
#include <openssl/x509.h>
#include <thread>
#include <atomic>
#include <vector>
namespace
{
/// Generate a self-signed certificate and private key in memory,
/// write them to temporary files for Poco::Net::Context.
struct EphemeralCert
{
std::string cert_path;
std::string key_path;
EphemeralCert()
{
EVP_PKEY * pkey = EVP_RSA_gen(2048);
if (!pkey)
throw std::runtime_error("EVP_RSA_gen failed");
X509 * x509 = X509_new();
if (!x509)
{
EVP_PKEY_free(pkey);
throw std::runtime_error("X509_new failed");
}
ASN1_INTEGER_set(X509_get_serialNumber(x509), 1);
X509_gmtime_adj(X509_getm_notBefore(x509), 0);
X509_gmtime_adj(X509_getm_notAfter(x509), 3600);
X509_set_pubkey(x509, pkey);
X509_NAME * name = X509_get_subject_name(x509);
X509_NAME_add_entry_by_txt(name, "CN", MBSTRING_ASC, reinterpret_cast<const unsigned char *>("localhost"), -1, -1, 0);
X509_set_issuer_name(x509, name);
X509_sign(x509, pkey, EVP_sha256());
cert_path = writeToTempFile(
[&](BIO * bio) { PEM_write_bio_X509(bio, x509); }, "cert");
key_path = writeToTempFile(
[&](BIO * bio) { PEM_write_bio_PrivateKey(bio, pkey, nullptr, nullptr, 0, nullptr, nullptr); }, "key");
X509_free(x509);
EVP_PKEY_free(pkey);
}
~EphemeralCert()
{
(void)unlink(cert_path.c_str());
(void)unlink(key_path.c_str());
}
private:
template <typename Fn>
static std::string writeToTempFile(Fn writer, const char * suffix)
{
char path[256];
(void)snprintf(path, sizeof(path), "/tmp/gtest_ssl_%s_XXXXXX", suffix);
int fd = mkstemp(path);
if (fd < 0)
throw std::runtime_error("mkstemp failed");
BIO * bio = BIO_new_fd(fd, BIO_CLOSE);
writer(bio);
BIO_free(bio);
return path;
}
};
Poco::Net::Context::Ptr makeContext(const EphemeralCert & cert, Poco::Net::Context::Usage usage)
{
Poco::Net::Context::Params params;
params.privateKeyFile = cert.key_path;
params.certificateFile = cert.cert_path;
params.verificationMode = Poco::Net::Context::VERIFY_NONE;
return new Poco::Net::Context(usage, params);
}
}
/// Test that a blocking SSL socket write throws TimeoutException
/// when the peer stops reading and the send timeout expires.
TEST(SSLSocketTimeout, SendBytesThrowsTimeoutOnBlockingSocket)
{
EphemeralCert cert;
auto server_ctx = makeContext(cert, Poco::Net::Context::SERVER_USE);
auto client_ctx = makeContext(cert, Poco::Net::Context::CLIENT_USE);
Poco::Net::SecureServerSocket server_socket(
Poco::Net::SocketAddress("127.0.0.1", 0), 1, server_ctx);
auto port = server_socket.address().port();
std::atomic<bool> server_done{false};
/// Server thread: accept and handshake, then sit idle (never read).
std::thread server_thread([&]
{
try
{
auto accepted = server_socket.acceptConnection();
/// Handshake happens on first I/O. Do a small read to trigger it.
char buf[1];
try { accepted.receiveBytes(buf, 1); } catch (...) {} /// Ok: handshake may fail. NOLINT(bugprone-empty-catch)
/// Keep the connection open until the test completes.
while (!server_done.load())
std::this_thread::sleep_for(std::chrono::milliseconds(50));
}
catch (...) {} /// Ok: server thread cleanup, test checks client-side behavior. NOLINT(bugprone-empty-catch)
});
try
{
Poco::Net::SecureStreamSocket client(
Poco::Net::SocketAddress("127.0.0.1", port), client_ctx);
/// Very short send timeout so the test doesn't wait long.
client.setSendTimeout(Poco::Timespan(0, 200'000)); /// 200ms
/// Write enough data to fill the TCP send buffer and SSL buffer.
/// Typical TCP buffer is 128KB-256KB. Write 4MB to be sure.
std::vector<char> data(4 * 1024 * 1024, 'X');
bool got_timeout = false;
try
{
size_t offset = 0;
while (offset < data.size())
{
int sent = client.sendBytes(data.data() + offset, static_cast<int>(data.size() - offset));
if (sent > 0)
offset += sent;
else
break;
}
}
catch (const Poco::TimeoutException &)
{
got_timeout = true;
}
ASSERT_TRUE(got_timeout) << "Expected Poco::TimeoutException when writing to a non-reading SSL peer";
}
catch (const Poco::Exception & e)
{
/// Connection setup can fail on some systems; skip gracefully.
/// Clean up the server thread before GTEST_SKIP returns from the function.
server_done.store(true);
server_socket.close();
server_thread.join();
GTEST_SKIP() << "SSL setup failed: " << e.displayText();
}
server_done.store(true);
/// Close the listening socket to unblock acceptConnection if the client
/// failed before connecting (e.g. SSL context error).
server_socket.close();
server_thread.join();
}
/// Test that SSL handshake throws TimeoutException when the peer
/// is a plain TCP listener that never speaks SSL.
/// No server thread needed -- the kernel's listen backlog completes the
/// TCP handshake, but nobody reads the accepted socket so the SSL
/// ClientHello gets no response.
TEST(SSLSocketTimeout, HandshakeThrowsTimeoutOnNonSSLPeer)
{
EphemeralCert cert;
auto client_ctx = makeContext(cert, Poco::Net::Context::CLIENT_USE);
/// Listen but never accept -- kernel backlog handles TCP handshake.
Poco::Net::ServerSocket listener(Poco::Net::SocketAddress("127.0.0.1", 0), 1);
auto port = listener.address().port();
bool got_timeout = false;
try
{
Poco::Net::StreamSocket raw_sock;
raw_sock.connect(Poco::Net::SocketAddress("127.0.0.1", port));
raw_sock.setSendTimeout(Poco::Timespan(0, 200'000));
raw_sock.setReceiveTimeout(Poco::Timespan(0, 200'000));
Poco::Net::SecureStreamSocket ssl_sock(
Poco::Net::SecureStreamSocket::attach(raw_sock, client_ctx));
/// completeHandshake is triggered by the first I/O.
/// The peer won't respond with ServerHello, so it will time out.
char buf[1] = {'X'};
ssl_sock.sendBytes(buf, 1);
}
catch (const Poco::TimeoutException &)
{
got_timeout = true;
}
catch (const Poco::Exception &)
{
/// Some SSL implementations may throw a different SSL error
/// before the timeout fires. That's acceptable -- the key thing
/// is we don't silently return -1.
got_timeout = true;
}
ASSERT_TRUE(got_timeout) << "Expected exception when SSL handshake times out against a non-SSL peer";
}
#endif /// USE_SSL