Skip to content

Commit 4cda475

Browse files
authored
Merge pull request #784 from frstrtr/ltc/send-socket-deadline
ltc: Send() socket deadline + desync-safe teardown (parity with dash #781)
2 parents 119346d + 8cc269c commit 4cda475

2 files changed

Lines changed: 90 additions & 0 deletions

File tree

src/impl/ltc/coin/rpc.cpp

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@
66

77
#include <core/log.hpp>
88
#include <core/hash.hpp>
9+
10+
#ifndef _WIN32
11+
#include <sys/socket.h> // setsockopt SO_SND/RCVTIMEO (Send() deadline)
12+
#include <sys/time.h> // struct timeval
13+
#endif
14+
915
namespace ltc
1016
{
1117

@@ -67,6 +73,10 @@ void NodeRPC::connect(NetService address, std::string userpass)
6773
{
6874
try
6975
{
76+
// Bound the synchronous Send() I/O BEFORE check()
77+
// runs its first RPC, so a wedged litecoind cannot
78+
// hang the caller (io thread or thread_pool worker).
79+
apply_socket_timeouts();
7080
if (check())
7181
{
7282
m_connected = true;
@@ -128,9 +138,51 @@ void NodeRPC::sync_reconnect()
128138
LOG_WARNING << "CoindRPC sync_reconnect connect failed: " << ec.message();
129139
return;
130140
}
141+
apply_socket_timeouts(); // re-arm the Send() deadline on the fresh socket
131142
LOG_INFO << "CoindRPC reconnected (sync)";
132143
}
133144

145+
void NodeRPC::close_stream()
146+
{
147+
// Same teardown idiom as sync_reconnect()/the destructor. m_connected is
148+
// cleared so the next Send() write-fails into a clean sync_reconnect().
149+
beast::error_code ec;
150+
m_stream.socket().shutdown(io::ip::tcp::socket::shutdown_both, ec);
151+
m_stream.close();
152+
m_connected = false;
153+
}
154+
155+
void NodeRPC::apply_socket_timeouts()
156+
{
157+
// Force the socket back to BLOCKING mode, then set kernel send/receive
158+
// timeouts. async_connect (connect()) leaves asio's non_blocking flag set;
159+
// under it a synchronous recv returns EAGAIN immediately and SO_RCVTIMEO is
160+
// a no-op. In blocking mode the sync http::write/http::read inside Send() do
161+
// true blocking send()/recv() which the kernel bounds by SO_SND/RCVTIMEO,
162+
// returning an error after RPC_IO_TIMEOUT_SECONDS instead of hanging on a
163+
// wedged litecoind/dogecoind. (beast tcp_stream::expires_after cannot be
164+
// used: it only governs ASYNC ops -- the sync path calls socket.read_some()
165+
// directly.) Per-operation inactivity timeout: a large-but-steadily-arriving
166+
// GBT does NOT trip it; only a genuine stall does. POSIX (SO_*TIMEO takes a
167+
// timeval); guarded off Windows (no <sys/time.h>/timeval, and Winsock
168+
// SO_RCVTIMEO takes a DWORD) -- the deploy target is Linux, and macOS keeps
169+
// the real deadline. On Windows this is a no-op (pre-parity behaviour).
170+
#ifndef _WIN32
171+
if (!m_stream.socket().is_open())
172+
return;
173+
boost::system::error_code ec;
174+
m_stream.socket().non_blocking(false, ec); // guarantee SO_*TIMEO applies
175+
if (ec)
176+
LOG_WARNING << "CoindRPC: could not set blocking mode: " << ec.message();
177+
struct timeval tv;
178+
tv.tv_sec = RPC_IO_TIMEOUT_SECONDS;
179+
tv.tv_usec = 0;
180+
const int fd = m_stream.socket().native_handle();
181+
::setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
182+
::setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
183+
#endif
184+
}
185+
134186
std::string NodeRPC::Send(const std::string &request)
135187
{
136188
std::lock_guard<std::mutex> _rpc_lock(m_rpc_mutex);
@@ -151,6 +203,10 @@ std::string NodeRPC::Send(const std::string &request)
151203
sync_reconnect();
152204
continue;
153205
}
206+
// Deadline-desync guard (see the read-fail path below): tear the
207+
// socket down before giving up so a partially-written request can
208+
// never be answered into a LATER Send() on a reused connection.
209+
close_stream();
154210
return {};
155211
}
156212

@@ -169,6 +225,17 @@ std::string NodeRPC::Send(const std::string &request)
169225
sync_reconnect();
170226
continue;
171227
}
228+
// Deadline-desync guard (SO_RCVTIMEO deadline hazard): this final
229+
// attempt has already WRITTEN the request into litecoind but its
230+
// response is unread (read timed out). jsonrpccxx reuses the constant
231+
// id "curltest" and never validates response ids, so reusing this
232+
// open connection would make the NEXT Send() read THIS request's late
233+
// response -> permanent off-by-one desync (a GBT would parse a
234+
// getbestblockhash string -> zeroed work -> a stuck "honest set-gap"
235+
// outage until the connection churns). Tear the socket down so the
236+
// next Send() write-fails into a clean sync_reconnect(). Already
237+
// under m_rpc_mutex.
238+
close_stream();
172239
return {};
173240
}
174241

src/impl/ltc/coin/rpc.hpp

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,29 @@ class NodeRPC : public jsonrpccxx::IClientConnector
5858
std::string Send(const std::string &request) override;
5959
nlohmann::json CallAPIMethod(const std::string& method, const jsonrpccxx::positional_parameter& params = {});
6060

61+
// Socket deadline (parity with dash #781, impl/dash/coin/rpc.{hpp,cpp}): a
62+
// REAL bound on the synchronous Send() I/O so a wedged-but-connected
63+
// litecoind/dogecoind (socket open, no bytes) cannot hang the caller forever
64+
// -- today that permanently wedges the RPC path (refresh-wedge) AND blocks
65+
// join() at shutdown (SIGINT-hang). beast's SYNCHRONOUS read_some/write_some
66+
// call socket.read_some() directly and do NOT honour tcp_stream::
67+
// expires_after (that timer only fires for ASYNC ops), so the robust bound is
68+
// a kernel SO_RCV/SNDTIMEO on the socket forced back to BLOCKING mode
69+
// (async_connect leaves asio's non_blocking flag set, under which SO_*TIMEO
70+
// is a no-op). apply_socket_timeouts() does both; called after every
71+
// successful (re)connect. Linux release target; no-op on Windows (pre-parity
72+
// behaviour: no deadline).
73+
static constexpr int RPC_IO_TIMEOUT_SECONDS = 12;
74+
void apply_socket_timeouts();
75+
// Tear down m_stream (sync_reconnect idiom). Called on a FINAL-attempt Send()
76+
// failure so a half-written / unread request can never be answered into a
77+
// later Send() on a reused connection -- the deadline-desync guard. The
78+
// constant "curltest" id (ID above) + jsonrpccxx not validating response ids
79+
// make any reused-connection off-by-one a permanent set-gap outage (a GBT
80+
// would parse a getbestblockhash string -> zeroed work). Caller holds
81+
// m_rpc_mutex.
82+
void close_stream();
83+
6184
public:
6285
NodeRPC(io::io_context* context, ltc::interfaces::Node* coin, bool testnet);
6386
~NodeRPC();

0 commit comments

Comments
 (0)