upleb.uk

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

summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-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) {