upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/src/btfrv.cpp
diff options
context:
space:
mode:
authorbitflash-maintainer <noreply@bitflash.invalid>2026-09-02 07:09:45 +0000
committerbitflash-maintainer <noreply@bitflash.invalid>2026-09-02 07:09:45 +0000
commit0b566b4869b9e86c90162befd87f9882b6dd143c (patch)
tree978c6a3d0ecf729fa72ab38468280d195843996a /src/btfrv.cpp
parenta44c9402c20493f0964a1296dd38d313216fbd4d (diff)
net: a refused thread spawn drops one peer, not the node
std::thread throws std::system_error when pthread_create is refused (RLIMIT_NPROC, a container's pids cgroup, memory pressure), and nothing caught it, so the node died by std::terminate. Threads are spawned per peer connection, so the pressure that triggers this scales with peer count, which is attacker-influenced. Observed killing a healthy participant worker mid-mining. Every spawn site in the node build, and what a failure now does: - btftunnel.cpp: both tunnel pumps are guarded. The inbound pump starts first because it owns cleanup of the pump end and rendezvous socket; if the second spawn is refused, shutdown() wakes the first out of its read and it cleans up itself -- closing those descriptors from the failure path instead would race it onto reused descriptors. Both paths return INVALID_SOCKET, which every caller already treats as "this connection failed". - btfrv.cpp: the relay's accept loop drops the connection it cannot serve and stops accepting for a moment -- longer after consecutive failures, capped at 5s -- instead of dying at an arbitrary one. The per-pair second pump now runs on the handling thread itself, leaving exactly one spawn to guard and nothing running yet to race. - main.cpp: a participant that cannot spawn its hash workers stops the ones that did start, drops the pool connection, and returns to the reconnect loop, the same way an ordinary disconnect does. - randomx_pow.cpp: dataset init joins the workers it got, releases the dataset, and stays in light mode -- the same trade the allocation failure path above it already makes. - rpc.cpp, net.cpp: the remaining unchecked _beginthread calls (which report failure by value, they cannot throw) now say precisely what did not start. The pool's accept and payout threads failing silently left the pool running deaf -- nobody accepts, nobody gets paid, and nothing in the log says why. Left alone deliberately: the per-relay accept spawns (rpc.cpp) already check, log and retry on the next poll; startup-critical spawns in net.cpp keep failing hard; the selftest-only std::thread uses may abort a selftest run, which is acceptable where dying over one peer is not. Verified: full node rebuilds clean and links; pool-stratum selftest passes; a scratch harness under RLIMIT_NPROC=1 confirms _beginthread reports -1 and std::thread throws system_error, the premises of every path above.
Diffstat (limited to 'src/btfrv.cpp')
-rw-r--r--src/btfrv.cpp49
1 files changed, 46 insertions, 3 deletions
diff --git a/src/btfrv.cpp b/src/btfrv.cpp
index 133eb89..8ab735c 100644
--- a/src/btfrv.cpp
+++ b/src/btfrv.cpp
@@ -40,6 +40,8 @@ typedef int SOCKET;
40#include <thread> 40#include <thread>
41#include <mutex> 41#include <mutex>
42#include <atomic> 42#include <atomic>
43#include <chrono>
44#include <system_error>
43 45
44namespace btf 46namespace btf
45{ 47{
@@ -209,8 +211,28 @@ static void HandleIncoming(SOCKET s)
209 bool a = WriteN(svc, &ok, 1); 211 bool a = WriteN(svc, &ok, 1);
210 bool b = WriteN(s, &ok, 1); 212 bool b = WriteN(s, &ok, 1);
211 if (!a || !b) { CLOSESOCK(svc); CLOSESOCK(s); return; } 213 if (!a || !b) { CLOSESOCK(svc); CLOSESOCK(s); return; }
212 std::thread(Forward, s, svc).detach(); 214 // One thread pumps s -> svc; this thread pumps svc -> s. Spawning a
213 std::thread(Forward, svc, s).detach(); 215 // thread per direction left no clean way to fail the second spawn:
216 // std::thread throws std::system_error when pthread_create is refused
217 // (NPROC limit, container pids cgroup, memory pressure), the first
218 // pump was already detached, and closing its sockets from here would
219 // race it -- so nothing caught the throw and it reached
220 // std::terminate, killing the relay over one pair it could not serve.
221 // Running one direction on this thread leaves exactly one spawn to
222 // guard, and a failure here has nothing running yet to race.
223 try
224 {
225 std::thread(Forward, s, svc).detach();
226 }
227 catch (const std::system_error& e)
228 {
229 fprintf(stderr, "rendezvous relay: cannot spawn forward thread (%s), pair dropped\n",
230 e.what());
231 CLOSESOCK(svc);
232 CLOSESOCK(s);
233 return;
234 }
235 Forward(svc, s);
214 } 236 }
215 else 237 else
216 { 238 {
@@ -236,6 +258,7 @@ bool RvRelayRun(unsigned short port)
236 if (bind(g_listen, (struct sockaddr*)&addr, sizeof(addr)) != 0) { CLOSESOCK(g_listen); return false; } 258 if (bind(g_listen, (struct sockaddr*)&addr, sizeof(addr)) != 0) { CLOSESOCK(g_listen); return false; }
237 if (listen(g_listen, 16) != 0) { CLOSESOCK(g_listen); return false; } 259 if (listen(g_listen, 16) != 0) { CLOSESOCK(g_listen); return false; }
238 260
261 int nSpawnFails = 0;
239 while (!g_stop) 262 while (!g_stop)
240 { 263 {
241 struct sockaddr_in cli; 264 struct sockaddr_in cli;
@@ -246,7 +269,27 @@ bool RvRelayRun(unsigned short port)
246 if (g_stop) break; 269 if (g_stop) break;
247 continue; 270 continue;
248 } 271 }
249 std::thread(HandleIncoming, s).detach(); 272 try
273 {
274 std::thread(HandleIncoming, s).detach();
275 nSpawnFails = 0;
276 }
277 catch (const std::system_error& e)
278 {
279 // A refused pthread_create is backpressure, not a reason to die:
280 // the condition behind it (NPROC, pids cgroup, memory) does not
281 // clear in milliseconds, and the connection count feeding it is
282 // attacker-influenced. Drop this one peer, stop accepting for a
283 // moment -- longer each time, capped -- and keep serving the
284 // connections already established.
285 fprintf(stderr, "rendezvous relay: cannot spawn connection thread (%s), connection dropped\n",
286 e.what());
287 CLOSESOCK(s);
288 nSpawnFails++;
289 int nWaitMs = nSpawnFails < 5 ? nSpawnFails * 1000 : 5000;
290 for (int i = 0; i < nWaitMs && !g_stop; i += 100)
291 std::this_thread::sleep_for(std::chrono::milliseconds(100));
292 }
250 } 293 }
251 CLOSESOCK(g_listen); 294 CLOSESOCK(g_listen);
252 g_listen = INVALID_SOCKET; 295 g_listen = INVALID_SOCKET;