upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src/btftunnel.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/btftunnel.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/btftunnel.cpp')
-rw-r--r--src/btftunnel.cpp52
1 files changed, 48 insertions, 4 deletions
diff --git a/src/btftunnel.cpp b/src/btftunnel.cpp
index 63d178b..2b3f818 100644
--- a/src/btftunnel.cpp
+++ b/src/btftunnel.cpp
@@ -25,8 +25,10 @@ typedef int SOCKET;
25#endif 25#endif
26#include "sockcount.h" 26#include "sockcount.h"
27#include <cstring> 27#include <cstring>
28#include <cstdio>
28#include <vector> 29#include <vector>
29#include <thread> 30#include <thread>
31#include <system_error>
30 32
31namespace btf 33namespace btf
32{ 34{
@@ -121,18 +123,60 @@ static void PumpIn(SOCKET pumpEnd, RvSocket rv, std::vector<unsigned char> key)
121 123
122// Start the two pumps and hand back the app-facing socket. When both pumps 124// Start the two pumps and hand back the app-facing socket. When both pumps
123// finish, close the pump end and the rendezvous socket. 125// finish, close the pump end and the rendezvous socket.
126//
127// Every tunnel is two threads, and a tunnel is one peer connection (a .btf
128// dial or accept, a pool miner, a participant's pool session) -- so the
129// thread count scales with peer count, which is attacker-influenced. When
130// pthread_create is refused std::thread throws std::system_error; uncaught it
131// reached std::terminate and took the whole process down over one connection
132// it could not serve. Both spawns are guarded, and the failure paths return
133// INVALID_SOCKET, which every caller already handles by dropping just that
134// connection.
124static btf_socket_t StartTunnel(RvSocket rv, const unsigned char shared[32]) 135static btf_socket_t StartTunnel(RvSocket rv, const unsigned char shared[32])
125{ 136{
126 btf_socket_t appEnd, pumpEnd; 137 btf_socket_t appEnd, pumpEnd;
127 if (!MakeLocalPair(appEnd, pumpEnd)) { RvClose(rv); return INVALID_SOCKET; } 138 if (!MakeLocalPair(appEnd, pumpEnd)) { RvClose(rv); return INVALID_SOCKET; }
128 139
129 std::vector<unsigned char> key(shared, shared + 32); 140 std::vector<unsigned char> key(shared, shared + 32);
130 std::thread(PumpOut, pumpEnd, rv, key).detach(); 141
131 std::thread([pumpEnd, rv, key]() { 142 // The inbound pump starts first because it owns the cleanup: it closes
132 PumpIn(pumpEnd, rv, key); 143 // pumpEnd and rv when it exits. That ownership is what makes the second
144 // spawn's failure path below race-free.
145 try
146 {
147 std::thread([pumpEnd, rv, key]() {
148 PumpIn(pumpEnd, rv, key);
149 BtfCloseSocket(pumpEnd);
150 RvClose(rv);
151 }).detach();
152 }
153 catch (const std::system_error& e)
154 {
155 // Nothing is running yet, so closing everything here is safe.
156 fprintf(stderr, "btf tunnel: cannot spawn inbound pump (%s), tunnel dropped\n",
157 e.what());
133 BtfCloseSocket(pumpEnd); 158 BtfCloseSocket(pumpEnd);
159 BtfCloseSocket(appEnd);
134 RvClose(rv); 160 RvClose(rv);
135 }).detach(); 161 return INVALID_SOCKET;
162 }
163
164 try
165 {
166 std::thread(PumpOut, pumpEnd, rv, key).detach();
167 }
168 catch (const std::system_error& e)
169 {
170 // The inbound pump is parked in RvReadN(rv). shutdown() wakes it, and
171 // it closes pumpEnd and rv itself -- closing them here instead would
172 // race exactly that, and a reused descriptor is how an unrelated
173 // socket gets closed.
174 fprintf(stderr, "btf tunnel: cannot spawn outbound pump (%s), tunnel dropped\n",
175 e.what());
176 shutdown((SOCKET)rv, SD_BOTH);
177 BtfCloseSocket(appEnd);
178 return INVALID_SOCKET;
179 }
136 return appEnd; 180 return appEnd;
137} 181}
138 182