upleb.uk

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

summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorbitflash-maintainer <noreply@bitflash.invalid>2026-09-03 21:49:26 +0000
committerbitflash-maintainer <noreply@bitflash.invalid>2026-09-03 21:49:26 +0000
commit5210bd66bb3837c7f8c6e551abc7088e9eb0b36f (patch)
treeee098e4a9e6cdbd2451481bc9ae169fef29d32ea
parentad62cfc9e7301a5ac456b92fa31523a516688d30 (diff)
parentb8a976f4870ba5dfc7a6af1ea09c9277b71d686c (diff)
merge: !4 fix/robustness into deploy/all-2026-08-24
-rw-r--r--src/btfrv.cpp49
-rw-r--r--src/btftunnel.cpp103
-rw-r--r--src/gui.cpp34
-rw-r--r--src/main.cpp30
-rw-r--r--src/net.cpp5
-rw-r--r--src/randomx_pow.cpp28
-rw-r--r--src/rpc.cpp15
-rw-r--r--src/util.h2
8 files changed, 239 insertions, 27 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;
diff --git a/src/btftunnel.cpp b/src/btftunnel.cpp
index 63d178b..4acca1a 100644
--- a/src/btftunnel.cpp
+++ b/src/btftunnel.cpp
@@ -25,8 +25,12 @@ typedef int SOCKET;
25#endif 25#endif
26#include "sockcount.h" 26#include "sockcount.h"
27#include <cstring> 27#include <cstring>
28#include <cstdio>
29#include <memory>
30#include <new>
28#include <vector> 31#include <vector>
29#include <thread> 32#include <thread>
33#include <system_error>
30 34
31namespace btf 35namespace btf
32{ 36{
@@ -119,20 +123,111 @@ static void PumpIn(SOCKET pumpEnd, RvSocket rv, std::vector<unsigned char> key)
119 shutdown((SOCKET)rv, SD_BOTH); 123 shutdown((SOCKET)rv, SD_BOTH);
120} 124}
121 125
126// The pump end and the rendezvous socket, owned jointly by whoever is still
127// using them. Closed once, by the last reference to go.
128//
129// Joint ownership rather than "the inbound pump owns the cleanup", because
130// either pump can reach its exit first and BOTH touch BOTH descriptors on the
131// way out -- PumpOut ends in shutdown(rv), shutdown(pumpEnd) and PumpIn ends in
132// the mirror image. A single owner therefore has no exit at which the other is
133// guaranteed to be finished. Holding a reference for the whole body of each
134// pump gives it one: the close cannot happen until the last pump has stopped
135// touching the descriptors, so no shutdown() can land on a number that has
136// already been closed and handed back out by the kernel to an unrelated
137// connection.
138struct TunnelEnds
139{
140 SOCKET pumpEnd;
141 RvSocket rv;
142 TunnelEnds(SOCKET p, RvSocket r) : pumpEnd(p), rv(r) {}
143 ~TunnelEnds() { BtfCloseSocket(pumpEnd); RvClose(rv); }
144private:
145 TunnelEnds(const TunnelEnds&);
146 TunnelEnds& operator=(const TunnelEnds&);
147};
148
122// Start the two pumps and hand back the app-facing socket. When both pumps 149// Start the two pumps and hand back the app-facing socket. When both pumps
123// finish, close the pump end and the rendezvous socket. 150// finish, close the pump end and the rendezvous socket.
151//
152// That sentence used to be a comment describing something the code did not do.
153// The inbound pump closed both descriptors as soon as it exited, without
154// waiting for the outbound pump, which then called shutdown() on both at its
155// own exit. Whether that shutdown hit a live descriptor or a closed one came
156// down to which pump got there first, and the rendezvous socket dying before
157// the app socket -- a relay restart, the far side hanging up -- is precisely
158// the order that puts the inbound pump ahead. Measured over 25 such teardowns
159// against the real code: 29 shutdowns landed on an already-closed descriptor.
160//
161// appEnd is not part of this. It is handed to the caller on success and closed
162// here on every failure path; no pump ever touches it.
163//
164// Every tunnel is two threads, and a tunnel is one peer connection (a .btf
165// dial or accept, a pool miner, a participant's pool session) -- so the
166// thread count scales with peer count, which is attacker-influenced. When
167// pthread_create is refused std::thread throws std::system_error; uncaught it
168// reached std::terminate and took the whole process down over one connection
169// it could not serve. Both spawns are guarded, and the failure paths return
170// INVALID_SOCKET, which every caller already handles by dropping just that
171// connection.
124static btf_socket_t StartTunnel(RvSocket rv, const unsigned char shared[32]) 172static btf_socket_t StartTunnel(RvSocket rv, const unsigned char shared[32])
125{ 173{
126 btf_socket_t appEnd, pumpEnd; 174 btf_socket_t appEnd, pumpEnd;
127 if (!MakeLocalPair(appEnd, pumpEnd)) { RvClose(rv); return INVALID_SOCKET; } 175 if (!MakeLocalPair(appEnd, pumpEnd)) { RvClose(rv); return INVALID_SOCKET; }
128 176
129 std::vector<unsigned char> key(shared, shared + 32); 177 std::vector<unsigned char> key(shared, shared + 32);
130 std::thread(PumpOut, pumpEnd, rv, key).detach(); 178
131 std::thread([pumpEnd, rv, key]() { 179 // make_shared, not shared_ptr(new ...): it allocates before it constructs,
132 PumpIn(pumpEnd, rv, key); 180 // so it either comes back owning both descriptors or throws having taken
181 // ownership of neither. The two-step form can run the deleter and then
182 // throw, which would close both here and again below.
183 std::shared_ptr<TunnelEnds> ends;
184 try
185 {
186 ends = std::make_shared<TunnelEnds>(pumpEnd, rv);
187 }
188 catch (const std::bad_alloc&)
189 {
190 fprintf(stderr, "btf tunnel: out of memory starting tunnel, tunnel dropped\n");
133 BtfCloseSocket(pumpEnd); 191 BtfCloseSocket(pumpEnd);
192 BtfCloseSocket(appEnd);
134 RvClose(rv); 193 RvClose(rv);
135 }).detach(); 194 return INVALID_SOCKET;
195 }
196
197 try
198 {
199 std::thread([ends, key]() { PumpIn(ends->pumpEnd, ends->rv, key); }).detach();
200 }
201 catch (const std::system_error& e)
202 {
203 // Nothing is running yet: `ends` is the only reference, and returning
204 // drops it, which is what closes pumpEnd and rv.
205 fprintf(stderr, "btf tunnel: cannot spawn inbound pump (%s), tunnel dropped\n",
206 e.what());
207 BtfCloseSocket(appEnd);
208 return INVALID_SOCKET;
209 }
210
211 try
212 {
213 std::thread([ends, key]() { PumpOut(ends->pumpEnd, ends->rv, key); }).detach();
214 }
215 catch (const std::system_error& e)
216 {
217 // The inbound pump may be parked in RvReadN(rv) or may already have
218 // exited -- when the spawn is refused because the process is loaded,
219 // the rendezvous socket is often dead for the same reason, and then
220 // PumpIn returns on its first length read. Either way this thread
221 // still holds a reference, so both descriptors are still open: the
222 // shutdown wakes the pump if it is parked and is harmless if it is
223 // not. The close belongs to whichever reference goes last.
224 fprintf(stderr, "btf tunnel: cannot spawn outbound pump (%s), tunnel dropped\n",
225 e.what());
226 shutdown((SOCKET)ends->rv, SD_BOTH);
227 shutdown(ends->pumpEnd, SD_BOTH);
228 BtfCloseSocket(appEnd);
229 return INVALID_SOCKET;
230 }
136 return appEnd; 231 return appEnd;
137} 232}
138 233
diff --git a/src/gui.cpp b/src/gui.cpp
index 8dcb854..9b3802b 100644
--- a/src/gui.cpp
+++ b/src/gui.cpp
@@ -15,6 +15,7 @@
15#include <atomic> 15#include <atomic>
16#include <mutex> 16#include <mutex>
17#include <thread> 17#include <thread>
18#include <system_error>
18#include "bip32.h" 19#include "bip32.h"
19#include "walletcmd.h" 20#include "walletcmd.h"
20#include "font_roboto.h" 21#include "font_roboto.h"
@@ -910,8 +911,16 @@ static void DrawOptionsDialog()
910 911
911 if (nMineMode == MINE_OPERATOR) { 912 if (nMineMode == MINE_OPERATOR) {
912 if (!gPoolServerRunning) { 913 if (!gPoolServerRunning) {
913 gPoolServerRunning = true; 914 // Claim the server is up only once it is. The flag used to
914 _beginthread(ThreadRPCServer, 0, NULL); 915 // be set first and the result never looked at, so a refused
916 // spawn left an operator with a pool that reports itself
917 // running, no RPC server for miners to reach, and nothing
918 // anywhere saying why. The flag also latches: with it set,
919 // this branch is never retried.
920 if (_beginthread(ThreadRPCServer, 0, NULL) == (uintptr_t)-1)
921 printf("Error: _beginthread(ThreadRPCServer) failed -- pool server not started\n");
922 else
923 gPoolServerRunning = true;
915 } else if (!gPoolRunning) { 924 } else if (!gPoolRunning) {
916 gPoolRunning = true; 925 gPoolRunning = true;
917 } 926 }
@@ -1124,11 +1133,28 @@ static void DrawRestorePhraseDialog()
1124 } 1133 }
1125 g_restoreDerived.store(0); 1134 g_restoreDerived.store(0);
1126 g_restoreRecovered.store(0); 1135 g_restoreRecovered.store(0);
1136 // Set before the spawn, not after: RestoreThread clears this
1137 // flag itself when it finishes, and a short restore can beat
1138 // us back here and have its `false` overwritten, leaving the
1139 // dialog disabled forever.
1127 g_restoreRunning.store(true); 1140 g_restoreRunning.store(true);
1128 // On its own thread: a restore walks the whole chain once per 1141 // On its own thread: a restore walks the whole chain once per
1129 // batch and would otherwise freeze the window for as long as it 1142 // batch and would otherwise freeze the window for as long as it
1130 // takes. 1143 // takes. A refused spawn is one restore that does not happen,
1131 std::thread(RestoreThread, std::string(g_restoreInput)).detach(); 1144 // not a reason to take the window down with an uncaught
1145 // std::system_error -- and the flag has to come back off, or
1146 // the dialog sits greyed out reporting a search nobody is
1147 // running.
1148 try
1149 {
1150 std::thread(RestoreThread, std::string(g_restoreInput)).detach();
1151 }
1152 catch (const std::system_error& e)
1153 {
1154 g_restoreRunning.store(false);
1155 std::lock_guard<std::mutex> lock(g_restoreResultMutex);
1156 g_restoreResult = std::string("Could not start the search: ") + e.what();
1157 }
1132 } 1158 }
1133 } 1159 }
1134 if (fBusy) 1160 if (fBusy)
diff --git a/src/main.cpp b/src/main.cpp
index 7b3e555..d154523 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -14,6 +14,7 @@
14#include <atomic> 14#include <atomic>
15#include <mutex> 15#include <mutex>
16#include <thread> 16#include <thread>
17#include <system_error>
17#include "bip32.h" 18#include "bip32.h"
18#include "btfaddr.h" 19#include "btfaddr.h"
19#include "btftunnel.h" 20#include "btftunnel.h"
@@ -3899,9 +3900,32 @@ connected:
3899 std::vector<ParticipantShareHit> shareHits; 3900 std::vector<ParticipantShareHit> shareHits;
3900 std::mutex shareHitsMtx; 3901 std::mutex shareHitsMtx;
3901 std::vector<std::thread> hashThreads; 3902 std::vector<std::thread> hashThreads;
3902 for (int i = 0; i < nWorkers; i++) 3903 try
3903 hashThreads.emplace_back(ParticipantHashWorker, &jobShared, i, nWorkers, 3904 {
3904 &shareHits, &shareHitsMtx); 3905 for (int i = 0; i < nWorkers; i++)
3906 hashThreads.emplace_back(ParticipantHashWorker, &jobShared, i, nWorkers,
3907 &shareHits, &shareHitsMtx);
3908 }
3909 catch (const std::system_error& e)
3910 {
3911 // A refused pthread_create used to reach std::terminate here and
3912 // kill the worker mid-session -- observed twice on an otherwise
3913 // healthy participant. Stop the workers that did start the same way
3914 // the disconnect path below does, drop this pool connection, and let
3915 // the caller's reconnect loop try again once the pressure clears.
3916 LogPrint("worker", "[worker] cannot spawn hash threads (%s)"
3917 " -- dropping pool connection\n", e.what());
3918 SetParticipantMiningStatus("hash thread spawn failed");
3919 {
3920 std::lock_guard<std::mutex> lk(jobShared.mtx);
3921 jobShared.stop = true;
3922 jobShared.haveJob = false;
3923 }
3924 for (std::thread& t : hashThreads)
3925 t.join();
3926 closesocket(s);
3927 return false;
3928 }
3905 LogPrint("worker", "[worker] spawned %d hash threads on shared RandomX dataset\n", 3929 LogPrint("worker", "[worker] spawned %d hash threads on shared RandomX dataset\n",
3906 nWorkers); 3930 nWorkers);
3907 const uint64 sessionStartHashes = gParticipantHashes.load(); 3931 const uint64 sessionStartHashes = gParticipantHashes.load();
diff --git a/src/net.cpp b/src/net.cpp
index fed6123..16038c8 100644
--- a/src/net.cpp
+++ b/src/net.cpp
@@ -2090,7 +2090,7 @@ bool StartNode(string& strError)
2090 if (addrIncoming.ip) 2090 if (addrIncoming.ip)
2091 addrLocalHost.ip = addrIncoming.ip; 2091 addrLocalHost.ip = addrIncoming.ip;
2092 2092
2093 _beginthread([](void*) { 2093 if (_beginthread([](void*) {
2094 unsigned int ip = addrLocalHost.ip; 2094 unsigned int ip = addrLocalHost.ip;
2095 if (GetMyExternalIP(ip)) { 2095 if (GetMyExternalIP(ip)) {
2096 addrLocalHost.ip = ip; 2096 addrLocalHost.ip = ip;
@@ -2098,7 +2098,8 @@ bool StartNode(string& strError)
2098 CWalletDB().WriteSetting("addrIncoming", addrIncoming); 2098 CWalletDB().WriteSetting("addrIncoming", addrIncoming);
2099 printf("External IP updated: %s\n", addrLocalHost.ToStringIP().c_str()); 2099 printf("External IP updated: %s\n", addrLocalHost.ToStringIP().c_str());
2100 } 2100 }
2101 }, 0, NULL); 2101 }, 0, NULL) == (uintptr_t)-1)
2102 printf("Error: _beginthread(GetMyExternalIP) failed\n");
2102 2103
2103 // Peers that answered last time, dialled straight away. The relays are the 2104 // Peers that answered last time, dialled straight away. The relays are the
2104 // only way to find anyone otherwise, and reaching them plus walking their 2105 // only way to find anyone otherwise, and reaching them plus walking their
diff --git a/src/randomx_pow.cpp b/src/randomx_pow.cpp
index 1d058d4..9c59364 100644
--- a/src/randomx_pow.cpp
+++ b/src/randomx_pow.cpp
@@ -8,6 +8,7 @@
8#include <thread> 8#include <thread>
9#include <vector> 9#include <vector>
10#include <mutex> 10#include <mutex>
11#include <system_error>
11 12
12// Fixed key (seed) that determines the RandomX cache/dataset. Keeping it 13// Fixed key (seed) that determines the RandomX cache/dataset. Keeping it
13// constant avoids per-epoch "reseed" logic; for the CPU+RAM fairness goal 14// constant avoids per-epoch "reseed" logic; for the CPU+RAM fairness goal
@@ -101,13 +102,28 @@ bool RandomXInitDataset(int nThreads)
101 102
102 std::vector<std::thread> workers; 103 std::vector<std::thread> workers;
103 unsigned long per = total / nThreads; 104 unsigned long per = total / nThreads;
104 for (int i = 0; i < nThreads; i++) 105 try
105 { 106 {
106 unsigned long start = i * per; 107 for (int i = 0; i < nThreads; i++)
107 unsigned long count = (i == nThreads - 1) ? (total - start) : per; 108 {
108 workers.emplace_back([start, count]() { 109 unsigned long start = i * per;
109 randomx_init_dataset(g_dataset, g_cache, start, count); 110 unsigned long count = (i == nThreads - 1) ? (total - start) : per;
110 }); 111 workers.emplace_back([start, count]() {
112 randomx_init_dataset(g_dataset, g_cache, start, count);
113 });
114 }
115 }
116 catch (const std::system_error& e)
117 {
118 // Same refusal as the allocation failure above: light mode is slow
119 // but alive, std::terminate is neither. Join the workers that did
120 // start before releasing the dataset out from under them.
121 for (auto& w : workers) w.join();
122 randomx_release_dataset(g_dataset);
123 g_dataset = NULL;
124 printf("RandomX: cannot spawn dataset init threads (%s), staying in light mode\n",
125 e.what());
126 return false;
111 } 127 }
112 for (auto& w : workers) w.join(); 128 for (auto& w : workers) w.join();
113 129
diff --git a/src/rpc.cpp b/src/rpc.cpp
index a4b53ca..13cb6c9 100644
--- a/src/rpc.cpp
+++ b/src/rpc.cpp
@@ -5077,10 +5077,17 @@ void ThreadRPCServer(void*)
5077 // armed for its one wallet probe before it is allowed to send. 5077 // armed for its one wallet probe before it is allowed to send.
5078 ReconcilePayouts(); 5078 ReconcilePayouts();
5079 5079
5080 _beginthread(AcceptLoopFn, 0, NULL); 5080 // _beginthread cannot throw (it reports -1), but an unchecked failure
5081 _beginthread(LocalAcceptFn, 0, NULL); 5081 // leaves the pool running deaf: nobody accepts, announces, or pays, and
5082 _beginthread(ThreadBtfPoolAnnouncer, 0, NULL); 5082 // nothing in the log says why.
5083 _beginthread(PayoutThreadFn, 0, NULL); 5083 if (_beginthread(AcceptLoopFn, 0, NULL) == (uintptr_t)-1)
5084 LogPrint("pool", "[pool] FATAL: accept loop thread failed to spawn -- miners cannot connect\n");
5085 if (_beginthread(LocalAcceptFn, 0, NULL) == (uintptr_t)-1)
5086 LogPrint("pool", "[pool] local accept thread failed to spawn\n");
5087 if (_beginthread(ThreadBtfPoolAnnouncer, 0, NULL) == (uintptr_t)-1)
5088 LogPrint("pool", "[pool] announcer thread failed to spawn -- pool will not be advertised\n");
5089 if (_beginthread(PayoutThreadFn, 0, NULL) == (uintptr_t)-1)
5090 LogPrint("pool", "[pool] FATAL: payout thread failed to spawn -- no one gets paid\n");
5084 5091
5085 PoolEventLoop(); // blocks until gPoolRunning = false or fShutdown 5092 PoolEventLoop(); // blocks until gPoolRunning = false or fShutdown
5086 5093
diff --git a/src/util.h b/src/util.h
index d72518b..58d1638 100644
--- a/src/util.h
+++ b/src/util.h
@@ -199,7 +199,7 @@ protected:
199public: 199public:
200 char* pszFile; 200 char* pszFile;
201 int nLine; 201 int nLine;
202 explicit CCriticalSection() { InitializeCriticalSection(&cs); } 202 explicit CCriticalSection() : pszFile(NULL), nLine(0) { InitializeCriticalSection(&cs); }
203 ~CCriticalSection() { DeleteCriticalSection(&cs); } 203 ~CCriticalSection() { DeleteCriticalSection(&cs); }
204 void Enter() { EnterCriticalSection(&cs); } 204 void Enter() { EnterCriticalSection(&cs); }
205 void Leave() { LeaveCriticalSection(&cs); } 205 void Leave() { LeaveCriticalSection(&cs); }