diff options
| author | bitflash-maintainer <noreply@bitflash.invalid> | 2026-09-02 17:39:50 +0000 |
|---|---|---|
| committer | bitflash-maintainer <noreply@bitflash.invalid> | 2026-09-02 17:39:50 +0000 |
| commit | 0a8c445742ce871c6b9352bf406d2232a6fd3134 (patch) | |
| tree | 058ed75eee5a1559d8c19f3411bd2ec729e1439c /src/btftunnel.cpp | |
| parent | 0b566b4869b9e86c90162befd87f9882b6dd143c (diff) | |
tunnel: give both pumps shared ownership of the sockets they close
PumpIn's wrapper closed pumpEnd and the rendezvous socket the moment it
exited, without waiting for PumpOut, which then called shutdown() on both
at its own exit. Which of the two got there first decided whether that
shutdown addressed a live descriptor or a number the kernel had already
handed back out -- in which case it tears down an unrelated connection.
The inbound pump wins that race whenever the rendezvous socket dies before
the app socket, which is the ordinary shape of a relay restart or a far
side hanging up.
The failure path added in 0b566b4 has the same hole. Its catch shuts down
rv to wake an inbound pump it assumes is parked in RvReadN. PumpIn does
not park when the rendezvous connection is already dead: it returns on its
first length read and its wrapper closes both descriptors. A refused
pthread_create and a dead rv are not independent -- both are symptoms of a
loaded process -- so that is the likely case, not the exotic one.
Both descriptors now belong to a shared_ptr the two pumps hold for the
whole of their bodies, so the close happens after the last pump has
stopped touching them and no shutdown can precede it. appEnd is unchanged:
the caller gets it on success, StartTunnel closes it on every failure
path, no pump ever touches it.
0b566b4's message says of that catch that "closing those descriptors from
the failure path instead would race it". True as far as it goes, but it
reads as though what the code did instead was safe, and it was not -- the
shutdown raced the same close. There was no ordering there to prefer, only
a missing owner.
Measured against the real code with the relay in a separate process, every
close() and shutdown() traced and pthread_create refused on demand:
normal teardown, 25 tunnels 29 shutdowns on a closed fd -> 0
refused outbound spawn, 25 25 shutdowns on a closed fd -> 0
Close counts are identical before and after (8 and 5 per tunnel, every one
accounted for) and live descriptors return to their starting count, so the
defect was only ever the ordering, never a leak or a double close.
Diffstat (limited to 'src/btftunnel.cpp')
| -rw-r--r-- | src/btftunnel.cpp | 85 |
1 files changed, 68 insertions, 17 deletions
diff --git a/src/btftunnel.cpp b/src/btftunnel.cpp index 2b3f818..4acca1a 100644 --- a/src/btftunnel.cpp +++ b/src/btftunnel.cpp | |||
| @@ -26,6 +26,8 @@ typedef int SOCKET; | |||
| 26 | #include "sockcount.h" | 26 | #include "sockcount.h" |
| 27 | #include <cstring> | 27 | #include <cstring> |
| 28 | #include <cstdio> | 28 | #include <cstdio> |
| 29 | #include <memory> | ||
| 30 | #include <new> | ||
| 29 | #include <vector> | 31 | #include <vector> |
| 30 | #include <thread> | 32 | #include <thread> |
| 31 | #include <system_error> | 33 | #include <system_error> |
| @@ -121,9 +123,44 @@ static void PumpIn(SOCKET pumpEnd, RvSocket rv, std::vector<unsigned char> key) | |||
| 121 | shutdown((SOCKET)rv, SD_BOTH); | 123 | shutdown((SOCKET)rv, SD_BOTH); |
| 122 | } | 124 | } |
| 123 | 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. | ||
| 138 | struct TunnelEnds | ||
| 139 | { | ||
| 140 | SOCKET pumpEnd; | ||
| 141 | RvSocket rv; | ||
| 142 | TunnelEnds(SOCKET p, RvSocket r) : pumpEnd(p), rv(r) {} | ||
| 143 | ~TunnelEnds() { BtfCloseSocket(pumpEnd); RvClose(rv); } | ||
| 144 | private: | ||
| 145 | TunnelEnds(const TunnelEnds&); | ||
| 146 | TunnelEnds& operator=(const TunnelEnds&); | ||
| 147 | }; | ||
| 148 | |||
| 124 | // 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 |
| 125 | // finish, close the pump end and the rendezvous socket. | 150 | // finish, close the pump end and the rendezvous socket. |
| 126 | // | 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 | // | ||
| 127 | // Every tunnel is two threads, and a tunnel is one peer connection (a .btf | 164 | // Every tunnel is two threads, and a tunnel is one peer connection (a .btf |
| 128 | // dial or accept, a pool miner, a participant's pool session) -- so the | 165 | // dial or accept, a pool miner, a participant's pool session) -- so the |
| 129 | // thread count scales with peer count, which is attacker-influenced. When | 166 | // thread count scales with peer count, which is attacker-influenced. When |
| @@ -139,41 +176,55 @@ static btf_socket_t StartTunnel(RvSocket rv, const unsigned char shared[32]) | |||
| 139 | 176 | ||
| 140 | std::vector<unsigned char> key(shared, shared + 32); | 177 | std::vector<unsigned char> key(shared, shared + 32); |
| 141 | 178 | ||
| 142 | // The inbound pump starts first because it owns the cleanup: it closes | 179 | // make_shared, not shared_ptr(new ...): it allocates before it constructs, |
| 143 | // pumpEnd and rv when it exits. That ownership is what makes the second | 180 | // so it either comes back owning both descriptors or throws having taken |
| 144 | // spawn's failure path below race-free. | 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"); | ||
| 191 | BtfCloseSocket(pumpEnd); | ||
| 192 | BtfCloseSocket(appEnd); | ||
| 193 | RvClose(rv); | ||
| 194 | return INVALID_SOCKET; | ||
| 195 | } | ||
| 196 | |||
| 145 | try | 197 | try |
| 146 | { | 198 | { |
| 147 | std::thread([pumpEnd, rv, key]() { | 199 | std::thread([ends, key]() { PumpIn(ends->pumpEnd, ends->rv, key); }).detach(); |
| 148 | PumpIn(pumpEnd, rv, key); | ||
| 149 | BtfCloseSocket(pumpEnd); | ||
| 150 | RvClose(rv); | ||
| 151 | }).detach(); | ||
| 152 | } | 200 | } |
| 153 | catch (const std::system_error& e) | 201 | catch (const std::system_error& e) |
| 154 | { | 202 | { |
| 155 | // Nothing is running yet, so closing everything here is safe. | 203 | // Nothing is running yet: `ends` is the only reference, and returning |
| 204 | // drops it, which is what closes pumpEnd and rv. | ||
| 156 | fprintf(stderr, "btf tunnel: cannot spawn inbound pump (%s), tunnel dropped\n", | 205 | fprintf(stderr, "btf tunnel: cannot spawn inbound pump (%s), tunnel dropped\n", |
| 157 | e.what()); | 206 | e.what()); |
| 158 | BtfCloseSocket(pumpEnd); | ||
| 159 | BtfCloseSocket(appEnd); | 207 | BtfCloseSocket(appEnd); |
| 160 | RvClose(rv); | ||
| 161 | return INVALID_SOCKET; | 208 | return INVALID_SOCKET; |
| 162 | } | 209 | } |
| 163 | 210 | ||
| 164 | try | 211 | try |
| 165 | { | 212 | { |
| 166 | std::thread(PumpOut, pumpEnd, rv, key).detach(); | 213 | std::thread([ends, key]() { PumpOut(ends->pumpEnd, ends->rv, key); }).detach(); |
| 167 | } | 214 | } |
| 168 | catch (const std::system_error& e) | 215 | catch (const std::system_error& e) |
| 169 | { | 216 | { |
| 170 | // The inbound pump is parked in RvReadN(rv). shutdown() wakes it, and | 217 | // The inbound pump may be parked in RvReadN(rv) or may already have |
| 171 | // it closes pumpEnd and rv itself -- closing them here instead would | 218 | // exited -- when the spawn is refused because the process is loaded, |
| 172 | // race exactly that, and a reused descriptor is how an unrelated | 219 | // the rendezvous socket is often dead for the same reason, and then |
| 173 | // socket gets closed. | 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. | ||
| 174 | fprintf(stderr, "btf tunnel: cannot spawn outbound pump (%s), tunnel dropped\n", | 224 | fprintf(stderr, "btf tunnel: cannot spawn outbound pump (%s), tunnel dropped\n", |
| 175 | e.what()); | 225 | e.what()); |
| 176 | shutdown((SOCKET)rv, SD_BOTH); | 226 | shutdown((SOCKET)ends->rv, SD_BOTH); |
| 227 | shutdown(ends->pumpEnd, SD_BOTH); | ||
| 177 | BtfCloseSocket(appEnd); | 228 | BtfCloseSocket(appEnd); |
| 178 | return INVALID_SOCKET; | 229 | return INVALID_SOCKET; |
| 179 | } | 230 | } |