diff options
| author | bitflash-maintainer <noreply@bitflash.invalid> | 2026-09-02 07:09:45 +0000 |
|---|---|---|
| committer | bitflash-maintainer <noreply@bitflash.invalid> | 2026-09-02 07:09:45 +0000 |
| commit | 0b566b4869b9e86c90162befd87f9882b6dd143c (patch) | |
| tree | 978c6a3d0ecf729fa72ab38468280d195843996a /src/randomx_pow.cpp | |
| parent | a44c9402c20493f0964a1296dd38d313216fbd4d (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/randomx_pow.cpp')
| -rw-r--r-- | src/randomx_pow.cpp | 28 |
1 files changed, 22 insertions, 6 deletions
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 | ||