diff options
| author | bitflash-maintainer <noreply@bitflash.invalid> | 2026-08-11 21:52:45 +0000 |
|---|---|---|
| committer | bitflash-maintainer <noreply@bitflash.invalid> | 2026-08-19 22:48:27 +0000 |
| commit | eb2e32e14b67d0eaad17cfa7086d8d7ebaf7f00f (patch) | |
| tree | 453af5925d5722cb7b91aefc45feff8491bacbf7 /src | |
| parent | e236d013e973fd00cbdbed4bea1e5f373c0d1236 (diff) | |
net: stop closing a descriptor number that now belongs to somebody else
THE WEDGE.
CNode::Disconnect() closed hSocket and left the member holding the stale
number. ~CNode's only guard is 'hSocket != INVALID_SOCKET' and nothing in
the tree ever assigned that sentinel, so the destructor closed the same
number a second time.
The two closes are five minutes apart by design: ThreadSocketHandler2
disconnects a node, sets nReleaseTime to GetTime() + 5*60, and parks it in
vNodesDisconnected; GetRefCount() reports it busy until that expires, so
'delete pnode' -- and the second close -- cannot run before then.
Linux hands out the lowest free descriptor. Measured on the pool: 202 open
descriptors with a high-water mark of 216, so a freed number is reissued
within seconds -- to a Nostr relay TLS socket, a rendezvous socket, a
tunnel end, or a miner connection. Five minutes later the destructor
closes it. The victim gets no error and no log line. It simply stops.
That is the whole signature, including the part that never made sense:
.btf block delivery and Nostr descriptor lookup share no lock, no queue
and no thread, so they should not fail together. They share the file
descriptor table, and a stray close is drawn from all of it.
It also explains why #78 did not help and may have hurt. #78 drops stuck
nodes -- it fired 41 times in one window -- and every drop is another
Disconnect(), which is another delayed close armed against an innocent
socket.
Second bug, same symptom, different subsystem: the pooled relay sockets.
ResolveDescriptor returned the instant a descriptor verified, leaving the
relay's EOSE unread on a socket that goes straight back into g_relayPool,
so the next caller read our leftovers as its own answer -- permanently one
message out of step, and no code path anywhere inspected the subscription
id that would have caught it. Worse, BtfResolve set lease.fOk = true on
every failure path including a dead RecvText, labelling it 'exchange
completed, just no match'. fOk = false is the only thing that ever closes
a pooled socket or clears fConnected, so a relay connection that died was
pinned in the pool, reported healthy, and reused for the life of the
process. That is 'no Nostr descriptor found' for a descriptor that nak can
see, cleared only by a restart.
Now: read to EOSE before returning, match the subscription id, and keep a
pooled socket only when the relay actually finished the conversation.
(cherry picked from commit ed4640c306cc46dbdbd92cb6dc55e7528651269c)
Diffstat (limited to 'src')
| -rw-r--r-- | src/nostr.cpp | 56 |
1 files changed, 45 insertions, 11 deletions
diff --git a/src/nostr.cpp b/src/nostr.cpp index b3e4a7c..3f43302 100644 --- a/src/nostr.cpp +++ b/src/nostr.cpp | |||
| @@ -1057,9 +1057,14 @@ static void HandlePoolAnnouncement(const json& ev) | |||
| 1057 | 1057 | ||
| 1058 | // Resolve a `.btf` address via a relay: fetch the descriptor published by that | 1058 | // Resolve a `.btf` address via a relay: fetch the descriptor published by that |
| 1059 | // address's key and verify it self-certifies under the decoded pubkey. | 1059 | // address's key and verify it self-certifies under the decoded pubkey. |
| 1060 | // fExchangeOk reports whether the relay actually finished the conversation | ||
| 1061 | // (an EOSE arrived) as opposed to the socket dying mid-read. The caller pools | ||
| 1062 | // this socket for reuse, and only a completed exchange leaves it in a state | ||
| 1063 | // the next caller can trust. | ||
| 1060 | static bool ResolveDescriptor(CWebSocket& ws, void* ctx, const string& btfAddr, | 1064 | static bool ResolveDescriptor(CWebSocket& ws, void* ctx, const string& btfAddr, |
| 1061 | btf::Descriptor& out) | 1065 | btf::Descriptor& out, bool& fExchangeOk) |
| 1062 | { | 1066 | { |
| 1067 | fExchangeOk = false; | ||
| 1063 | unsigned char pk[32]; | 1068 | unsigned char pk[32]; |
| 1064 | if (!btf::ParseAddress(btfAddr, pk)) | 1069 | if (!btf::ParseAddress(btfAddr, pk)) |
| 1065 | return false; | 1070 | return false; |
| @@ -1070,26 +1075,42 @@ static bool ResolveDescriptor(CWebSocket& ws, void* ctx, const string& btfAddr, | |||
| 1070 | json req = json::array({ "REQ", "btf-resolve", filter }); | 1075 | json req = json::array({ "REQ", "btf-resolve", filter }); |
| 1071 | if (!ws.SendText(req.dump())) | 1076 | if (!ws.SendText(req.dump())) |
| 1072 | return false; | 1077 | return false; |
| 1078 | bool fFound = false; | ||
| 1073 | for (;;) | 1079 | for (;;) |
| 1074 | { | 1080 | { |
| 1075 | string msg; | 1081 | string msg; |
| 1076 | if (!ws.RecvText(msg)) | 1082 | if (!ws.RecvText(msg)) |
| 1077 | break; | 1083 | return fFound; // socket died mid-exchange; fExchangeOk stays false |
| 1078 | json j; | 1084 | json j; |
| 1079 | try { j = json::parse(msg); } catch (...) { continue; } | 1085 | try { j = json::parse(msg); } catch (...) { continue; } |
| 1080 | if (!j.is_array() || j.empty() || !j[0].is_string()) continue; | 1086 | if (!j.is_array() || j.empty() || !j[0].is_string()) continue; |
| 1081 | string t = j[0].get<string>(); | 1087 | string t = j[0].get<string>(); |
| 1088 | // Every relay message names the subscription it belongs to. This socket | ||
| 1089 | // is pooled and reused, so a message left over from an earlier exchange | ||
| 1090 | // is the ordinary case here, not an exotic one -- and an unmatched EOSE | ||
| 1091 | // ends this query before it has begun, which is indistinguishable from | ||
| 1092 | // "that descriptor does not exist". Check the id. | ||
| 1093 | if (j.size() >= 2 && j[1].is_string() && j[1].get<string>() != "btf-resolve") | ||
| 1094 | continue; | ||
| 1082 | if (t == "EVENT" && j.size() >= 3) | 1095 | if (t == "EVENT" && j.size() >= 3) |
| 1083 | { | 1096 | { |
| 1084 | const json& ev = j[2]; | 1097 | const json& ev = j[2]; |
| 1085 | if (ev.contains("content") && | 1098 | if (!fFound && ev.contains("content") && |
| 1086 | btf::VerifyDescriptor(ctx, ev["content"].get<string>(), pk, out)) | 1099 | btf::VerifyDescriptor(ctx, ev["content"].get<string>(), pk, out)) |
| 1087 | return true; | 1100 | fFound = true; |
| 1101 | // Deliberately keep reading to EOSE rather than returning here. | ||
| 1102 | // Returning early leaves the rest of the relay's answer queued on a | ||
| 1103 | // socket that goes straight back into the pool, and the next caller | ||
| 1104 | // reads our leftovers as its own -- one message out of step, for | ||
| 1105 | // the life of the process. | ||
| 1088 | } | 1106 | } |
| 1089 | else if (t == "EOSE") | 1107 | else if (t == "EOSE") |
| 1108 | { | ||
| 1109 | fExchangeOk = true; // relay said "that is all": socket is clean | ||
| 1090 | break; | 1110 | break; |
| 1111 | } | ||
| 1091 | } | 1112 | } |
| 1092 | return false; | 1113 | return fFound; |
| 1093 | } | 1114 | } |
| 1094 | 1115 | ||
| 1095 | // Resolve a `.btf` address into its rendezvous coordinates by querying the | 1116 | // Resolve a `.btf` address into its rendezvous coordinates by querying the |
| @@ -1108,12 +1129,22 @@ bool BtfResolve(const std::string& btfAddr, std::string& meetingHostPort, | |||
| 1108 | if (!lease.Connected()) | 1129 | if (!lease.Connected()) |
| 1109 | continue; | 1130 | continue; |
| 1110 | btf::Descriptor d; | 1131 | btf::Descriptor d; |
| 1111 | if (!ResolveDescriptor(lease.Ws(), g_nostrKey.ctx, btfAddr, d)) | 1132 | bool fExchangeOk = false; |
| 1133 | if (!ResolveDescriptor(lease.Ws(), g_nostrKey.ctx, btfAddr, d, fExchangeOk)) | ||
| 1112 | { | 1134 | { |
| 1113 | lease.fOk = true; // exchange completed, just no match | 1135 | // Only keep the pooled socket if the relay actually finished |
| 1136 | // the conversation. This used to say "exchange completed, just | ||
| 1137 | // no match" on every failure -- including a dead socket -- so | ||
| 1138 | // ~CRelayLease never closed it and fConnected was never | ||
| 1139 | // cleared. A relay connection that died was then pinned in the | ||
| 1140 | // pool, reported healthy, and reused for every later lookup, | ||
| 1141 | // which is why "no Nostr descriptor found" persisted for a | ||
| 1142 | // descriptor that was demonstrably published, and why only a | ||
| 1143 | // restart cleared it. | ||
| 1144 | lease.fOk = fExchangeOk; | ||
| 1114 | continue; | 1145 | continue; |
| 1115 | } | 1146 | } |
| 1116 | lease.fOk = true; | 1147 | lease.fOk = fExchangeOk; |
| 1117 | if (d.meeting_node.empty() || d.meeting_node == "rendezvous-pending") | 1148 | if (d.meeting_node.empty() || d.meeting_node == "rendezvous-pending") |
| 1118 | continue; // descriptor predates the owner's relay config | 1149 | continue; // descriptor predates the owner's relay config |
| 1119 | if (!HexDecode(d.enc, enc_pub, 32)) | 1150 | if (!HexDecode(d.enc, enc_pub, 32)) |
| @@ -1761,14 +1792,17 @@ void ThreadNostrSeed(void* parg) | |||
| 1761 | PublishDescriptor(ws, key); | 1792 | PublishDescriptor(ws, key); |
| 1762 | Sleep(700); // let the relay store the replaceable event | 1793 | Sleep(700); // let the relay store the replaceable event |
| 1763 | btf::Descriptor d; | 1794 | btf::Descriptor d; |
| 1764 | if (ResolveDescriptor(ws, key.ctx, key.BtfAddress(), d)) | 1795 | bool fExchangeOk = false; |
| 1796 | if (ResolveDescriptor(ws, key.ctx, key.BtfAddress(), d, fExchangeOk)) | ||
| 1765 | { | 1797 | { |
| 1766 | LogPrint("nostr", "Nostr: .btf self-resolve OK via %s (meeting_node=%s, enc=%s...)\n", | 1798 | LogPrint("nostr", "Nostr: .btf self-resolve OK via %s (meeting_node=%s, enc=%s...)\n", |
| 1767 | pszNostrRelays[i], d.meeting_node.c_str(), d.enc.substr(0, 16).c_str()); | 1799 | pszNostrRelays[i], d.meeting_node.c_str(), d.enc.substr(0, 16).c_str()); |
| 1768 | lease.fOk = true; | 1800 | lease.fOk = fExchangeOk; |
| 1769 | break; | 1801 | break; |
| 1770 | } | 1802 | } |
| 1771 | lease.fOk = true; | 1803 | // Same rule as BtfResolve: a socket only goes back in the pool if |
| 1804 | // the relay finished the conversation on it. | ||
| 1805 | lease.fOk = fExchangeOk; | ||
| 1772 | } | 1806 | } |
| 1773 | CATCH_PRINT_EXCEPTION("btf self-test") | 1807 | CATCH_PRINT_EXCEPTION("btf self-test") |
| 1774 | } | 1808 | } |