upleb.uk

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

summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBitflash <btf01@proton.me>2026-07-28 15:25:57 -0300
committerGitHub <noreply@github.com>2026-07-28 15:25:57 -0300
commit63764708903cf93476b0fa43580e23ff75ffceba (patch)
tree7825001b9037531ee625edd38b9cf177bc0300d0
parent8a613d756c25983588c6ccabf3752aa31a4e7cb2 (diff)
rpc: bound and guard the Stratum surface (#19)
Everything an unauthenticated socket can reach in the pool server, in one pass. A line that never ends. RecvLine appended to the per-miner buffer and looked for a newline; a peer that sends bytes and no newline grew that buffer for as long as it kept writing, before authorizing or subscribing. Capped at 64 KB, which drops the miner. No limit on connections. Every accepted socket became a Miner with its own buffer and nothing bounded the list. Capped at 1000. Malformed requests crashed the node, not just the pool. ThreadRPCServer has no exception handler, so anything thrown from line handling reached the top of the thread and terminated the process. Three ways in, all valid JSON that parses cleanly: [1,2,3] -- value() on a non-object throws {"method": 1} -- value() with the wrong type throws submit with params [1,2,3] -- get<std::string>() on a number throws mining.authorize had the subtler shape of the same thing: it tested p.size() without testing p.is_array(), and indexing a json object with a number throws while size() happily returns its key count. Each is now rejected on its own terms, and the call is wrapped so a throw drops that miner instead of the node.
-rw-r--r--src/rpc.cpp80
1 files changed, 72 insertions, 8 deletions
diff --git a/src/rpc.cpp b/src/rpc.cpp
index 6f81153..9cd0ed6 100644
--- a/src/rpc.cpp
+++ b/src/rpc.cpp
@@ -553,6 +553,12 @@ static bool SendLine(btf_socket_t fd, const json& j)
553 return true; 553 return true;
554} 554}
555 555
556// Ceilings on what an unauthenticated peer can make the pool operator hold.
557// Stratum lines are a few hundred bytes and a real pool this size will not see
558// anything near a thousand miners.
559static const size_t MAX_STRATUM_LINE = 64 * 1024;
560static const size_t MAX_POOL_MINERS = 1000;
561
556// Non-blocking recv: pull whatever bytes are available now into buf, then 562// Non-blocking recv: pull whatever bytes are available now into buf, then
557// return one complete line if one is ready. Three outcomes: 563// return one complete line if one is ready. Three outcomes:
558// true -- line is filled; buf may still contain more lines 564// true -- line is filled; buf may still contain more lines
@@ -585,6 +591,16 @@ static bool RecvLine(btf_socket_t fd, std::string& buf,
585 } 591 }
586#endif 592#endif
587 593
594 // A peer that never sends a newline would otherwise grow this buffer for
595 // as long as it keeps writing, and it does not have to authorize first --
596 // the read happens before any of that. Stratum lines are a few hundred
597 // bytes; anything past the cap is not a slow line, it is a flood.
598 if (buf.size() > MAX_STRATUM_LINE) {
599 disconnected = true;
600 buf.clear();
601 return false;
602 }
603
588 size_t pos = buf.find('\n'); 604 size_t pos = buf.find('\n');
589 if (pos == std::string::npos) return false; // no complete line yet 605 if (pos == std::string::npos) return false; // no complete line yet
590 606
@@ -668,6 +684,16 @@ static bool HandleLine(Miner* m, const std::string& rawLine,
668 return true; // don't disconnect on bad JSON, just ignore the line 684 return true; // don't disconnect on bad JSON, just ignore the line
669 } 685 }
670 686
687 // value() throws when the document is not an object, and again when a key
688 // is present with the wrong type -- "[1,2,3]" and {"method":1} are both
689 // valid JSON that parse cleanly and then blow up on the next two lines.
690 if (!req.is_object() || (req.contains("method") && !req["method"].is_string())) {
691 LogPrint("worker", "[worker] malformed request from %s: %.80s\n",
692 m->address.empty() ? "(unauth)" : m->address.c_str(),
693 rawLine.c_str());
694 return true;
695 }
696
671 json id = req.value("id", json(nullptr)); 697 json id = req.value("id", json(nullptr));
672 std::string method = req.value("method", ""); 698 std::string method = req.value("method", "");
673 699
@@ -691,9 +717,13 @@ static bool HandleLine(Miner* m, const std::string& rawLine,
691 717
692 // ------------------------------------------------------------------ 718 // ------------------------------------------------------------------
693 if (method == "mining.authorize") { 719 if (method == "mining.authorize") {
720 // is_array() matters as much as the element checks: indexing a json
721 // object with a number throws, and size() on an object returns its key
722 // count, so "params":{"a":1} would pass a bare size() test.
694 auto& p = req["params"]; 723 auto& p = req["params"];
695 m->address = (p.size() > 0 && p[0].is_string()) ? p[0].get<std::string>() : ""; 724 bool fArr = p.is_array();
696 m->worker = (p.size() > 1 && p[1].is_string()) ? p[1].get<std::string>() : "worker"; 725 m->address = (fArr && p.size() > 0 && p[0].is_string()) ? p[0].get<std::string>() : "";
726 m->worker = (fArr && p.size() > 1 && p[1].is_string()) ? p[1].get<std::string>() : "worker";
697 m->lastSeen = GetTime(); 727 m->lastSeen = GetTime();
698 728
699 // Validate address before authorising 729 // Validate address before authorising
@@ -748,9 +778,13 @@ static bool HandleLine(Miner* m, const std::string& rawLine,
748 } 778 }
749 779
750 auto& p = req["params"]; 780 auto& p = req["params"];
751 if (p.size() < 3) { 781 // Check the shape as well as the length: a submit carrying
752 LogPrint("worker", "[worker->pool] submit bad params from %s (size=%zu)\n", 782 // "params":[1,2,3] would otherwise reach get<std::string>() and throw
753 m->address.c_str(), p.size()); 783 // a json::type_error that nothing above catches, taking the pool down
784 // with it. Authorising costs an attacker nothing but an address.
785 if (!p.is_array() || p.size() < 3 || !p[1].is_string() || !p[2].is_string()) {
786 LogPrint("worker", "[worker->pool] submit bad params from %s\n",
787 m->address.c_str());
754 reply(false, "bad params"); 788 reply(false, "bad params");
755 return true; 789 return true;
756 } 790 }
@@ -1010,6 +1044,16 @@ static void PoolEventLoop()
1010 { 1044 {
1011 std::lock_guard<std::mutex> lk(gReadyMutex); 1045 std::lock_guard<std::mutex> lk(gReadyMutex);
1012 for (btf_socket_t fd : gReadyQueue) { 1046 for (btf_socket_t fd : gReadyQueue) {
1047 // Connecting costs nothing and every accepted socket carries a
1048 // read buffer, so an unbounded miner list is an unbounded
1049 // allocation for whoever runs the pool.
1050 if (gMiners.size() >= MAX_POOL_MINERS) {
1051 LogPrint("worker", "[pool] refusing miner fd=%d --"
1052 " already at %zu connections\n",
1053 (int)fd, gMiners.size());
1054 sock_close(fd);
1055 continue;
1056 }
1013 gMiners.push_back(new Miner(fd)); 1057 gMiners.push_back(new Miner(fd));
1014 LogPrint("worker", "[pool] miner fd=%d entered event loop" 1058 LogPrint("worker", "[pool] miner fd=%d entered event loop"
1015 " -- awaiting subscribe/authorize\n", (int)fd); 1059 " -- awaiting subscribe/authorize\n", (int)fd);
@@ -1154,9 +1198,29 @@ static void PoolEventLoop()
1154 bool disc = false; 1198 bool disc = false;
1155 // Drain all complete lines buffered for this miner. 1199 // Drain all complete lines buffered for this miner.
1156 // HandleLine always returns true; disc signals clean disconnect. 1200 // HandleLine always returns true; disc signals clean disconnect.
1157 while (RecvLine(m->fd, m->readBuf, line, disc)) 1201 // ThreadRPCServer has no handler of its own, so anything
1158 HandleLine(m, line, roundShareCount, 1202 // thrown here would reach the top of the thread and
1159 roundShareTotal, blocksFoundThisSession); 1203 // terminate the whole node -- not just the pool. Every
1204 // line arrives from an unauthenticated socket.
1205 while (RecvLine(m->fd, m->readBuf, line, disc)) {
1206 try {
1207 HandleLine(m, line, roundShareCount,
1208 roundShareTotal, blocksFoundThisSession);
1209 } catch (const std::exception& e) {
1210 LogPrint("worker", "[pool] dropping miner %s --"
1211 " exception handling line: %s\n",
1212 m->address.empty() ? "(unauth)" : m->address.c_str(),
1213 e.what());
1214 disc = true;
1215 break;
1216 } catch (...) {
1217 LogPrint("worker", "[pool] dropping miner %s --"
1218 " unknown exception handling line\n",
1219 m->address.empty() ? "(unauth)" : m->address.c_str());
1220 disc = true;
1221 break;
1222 }
1223 }
1160 if (disc) dead.push_back(m); 1224 if (disc) dead.push_back(m);
1161 } 1225 }
1162 for (Miner* m : dead) { 1226 for (Miner* m : dead) {