OverviewFilesCommitsIssuesDocsReleasesPeers

hyperswarm-peer commits / 7a55292

udx: the loop-wake guard existed at ONE of twenty call sites
`_async_handle` is a uv_async_t that does not exist until `_thread_func` has run `uv_async_init` on it. Signalling it before that reads a null callback pointer and jumps to address zero. `run_on_loop` knew this and checked `_async_ready`, with a comment saying so. Nineteen other senders did not: swarm_join, swarm_leave, both peer variants, every dht_* command, send, disconnect_peer, start_dht, and stop. ⛔ IT WAS NOT THEORETICAL AND IT WAS NOT RARE. swarmd's new test_client connects and calls join() immediately after Daemon::start() returns — faster than anything had before — and segfaulted in **7 runs out of 20**. The core shows the UDX thread still inside udx_socket_bind while the caller is in uv_async_send, and a fourth thread parked at `0x0 in ?? ()`. Nothing caught it earlier because nothing was that quick: every existing e2e gate waits for `dht=up` before issuing a command, so the window closed before anyone reached it. A race this plain survived because the instruments were polite. One `_wake()` helper now owns the guard, and no call site gets to decide. Commands are queued regardless and _thread_func signals itself the instant the handle exists, so a wake skipped here is a wake deferred by microseconds, not a command lost. fixed 0/20 segfaults mutated 20/20 (guard removed from the helper — every sender then races, where before only the nineteen unguarded ones did) Full peer suite green after it: 17 offline gates, 4 against a local testnet, the JS vectors 65/65. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
7a552929938b99a9dbfcbb3ae0cc2ae70f36ae55
by plankatron <fleeky@gmail.com> · 2026-09-10T22:07:16.000Z

Changed files (2) +47 -21

src/udx_transport.cpp +44 -21
……@@ -146,7 +146,7 @@ void UdxTransport::stop() {
146146 _running.store(false);
147147
148148 // Wake up the event loop so it can exit.
149 uv_async_send(&_async_handle);
149 _wake();
150150
151151 if (_thread.joinable()) {
152152 _thread.join();
……@@ -187,7 +187,7 @@ void UdxTransport::send(int32_t target_peer, const uint8_t *data, int32_t size,
187187 cmd.mode = mode;
188188 cmd.channel = channel;
189189 _queue_command(std::move(cmd));
190 uv_async_send(&_async_handle);
190 _wake();
191191}
192192
193193void UdxTransport::disconnect_peer(int32_t peer_id, bool force) {
……@@ -196,7 +196,7 @@ void UdxTransport::disconnect_peer(int32_t peer_id, bool force) {
196196 cmd.peer_id = peer_id;
197197 cmd.force = force;
198198 _queue_command(std::move(cmd));
199 uv_async_send(&_async_handle);
199 _wake();
200200}
201201
202202bool UdxTransport::pop_peer_event(PeerEvent &out) {
……@@ -270,7 +270,7 @@ void UdxTransport::start_dht(const Ed25519KeyPair &kp,
270270 _running.store(true);
271271 _thread = std::thread(&UdxTransport::_thread_func, this);
272272 } else {
273 uv_async_send(&_async_handle);
273 _wake();
274274 }
275275}
276276
……@@ -279,14 +279,14 @@ void UdxTransport::dht_connect(const uint8_t *remote_public_key) {
279279 cmd.type = CMD_DHT_CONNECT;
280280 memcpy(cmd.remote_public_key, remote_public_key, 32);
281281 _queue_command(std::move(cmd));
282 uv_async_send(&_async_handle);
282 _wake();
283283}
284284
285285void UdxTransport::dht_listen() {
286286 Command cmd;
287287 cmd.type = CMD_DHT_LISTEN;
288288 _queue_command(std::move(cmd));
289 uv_async_send(&_async_handle);
289 _wake();
290290}
291291
292292void UdxTransport::dht_announce(const uint8_t *topic) {
……@@ -294,7 +294,7 @@ void UdxTransport::dht_announce(const uint8_t *topic) {
294294 cmd.type = CMD_DHT_ANNOUNCE;
295295 cmd.data.assign(topic, topic + 32);
296296 _queue_command(std::move(cmd));
297 uv_async_send(&_async_handle);
297 _wake();
298298}
299299
300300void UdxTransport::dht_lookup(const uint8_t *topic) {
……@@ -302,7 +302,7 @@ void UdxTransport::dht_lookup(const uint8_t *topic) {
302302 cmd.type = CMD_DHT_LOOKUP;
303303 cmd.data.assign(topic, topic + 32);
304304 _queue_command(std::move(cmd));
305 uv_async_send(&_async_handle);
305 _wake();
306306}
307307
308308void UdxTransport::dht_join_topic(const uint8_t *topic, bool server, bool client) {
……@@ -312,7 +312,7 @@ void UdxTransport::dht_join_topic(const uint8_t *topic, bool server, bool client
312312 cmd.server = server;
313313 cmd.client = client;
314314 _queue_command(std::move(cmd));
315 uv_async_send(&_async_handle);
315 _wake();
316316}
317317
318318void UdxTransport::dht_leave_topic(const uint8_t *topic) {
……@@ -320,7 +320,7 @@ void UdxTransport::dht_leave_topic(const uint8_t *topic) {
320320 cmd.type = CMD_DHT_LEAVE_TOPIC;
321321 cmd.data.assign(topic, topic + 32);
322322 _queue_command(std::move(cmd));
323 uv_async_send(&_async_handle);
323 _wake();
324324}
325325
326326void UdxTransport::dht_immutable_put(const uint8_t *value, size_t value_len) {
……@@ -328,7 +328,7 @@ void UdxTransport::dht_immutable_put(const uint8_t *value, size_t value_len) {
328328 cmd.type = CMD_DHT_IMMUTABLE_PUT;
329329 cmd.data.assign(value, value + value_len);
330330 _queue_command(std::move(cmd));
331 uv_async_send(&_async_handle);
331 _wake();
332332}
333333
334334void UdxTransport::dht_immutable_get(const uint8_t *key) {
……@@ -336,7 +336,7 @@ void UdxTransport::dht_immutable_get(const uint8_t *key) {
336336 cmd.type = CMD_DHT_IMMUTABLE_GET;
337337 cmd.data.assign(key, key + 32);
338338 _queue_command(std::move(cmd));
339 uv_async_send(&_async_handle);
339 _wake();
340340}
341341
342342void UdxTransport::dht_mutable_put(const uint8_t *value, size_t value_len, uint64_t seq) {
……@@ -345,7 +345,7 @@ void UdxTransport::dht_mutable_put(const uint8_t *value, size_t value_len, uint6
345345 cmd.data.assign(value, value + value_len);
346346 cmd.seq = seq;
347347 _queue_command(std::move(cmd));
348 uv_async_send(&_async_handle);
348 _wake();
349349}
350350
351351void UdxTransport::dht_mutable_get(const uint8_t *public_key, uint64_t min_seq) {
……@@ -354,7 +354,7 @@ void UdxTransport::dht_mutable_get(const uint8_t *public_key, uint64_t min_seq)
354354 cmd.data.assign(public_key, public_key + 32);
355355 cmd.seq = min_seq;
356356 _queue_command(std::move(cmd));
357 uv_async_send(&_async_handle);
357 _wake();
358358}
359359
360360// ---- Background Thread ----
……@@ -369,6 +369,29 @@ static void _tick_cb(uv_timer_t *handle) {
369369 transport->_process_commands();
370370}
371371
372// ⛔ THE ONLY WAY TO WAKE THE LOOP, because the guard was previously written at
373// exactly ONE of twenty call sites.
374//
375// `_async_handle` is a uv_async_t that does not exist until `_thread_func` has
376// run `uv_async_init` on it. Signalling it before that reads a null callback
377// pointer and jumps to address zero. run_on_loop checked `_async_ready`;
378// swarm_join, swarm_leave, the peer variants, every dht_* command, send,
379// disconnect_peer, start_dht and stop did not — nineteen unguarded senders and
380// one guarded one.
381//
382// It was reachable the moment a client issued a command promptly after start:
383// measured as a SEGV in 7 runs of 20 of swarmd's test_client, with the UDX
384// thread still inside udx_socket_bind and thread 4 at `0x0 in ?? ()`. Nothing
385// before it called anything that fast — the e2e gates all wait for dht=up
386// first, which is why a race this plain survived.
387//
388// Commands are queued regardless, and `_thread_func` sends to itself the
389// instant the handle exists, so a wake dropped here is a wake deferred by
390// microseconds and not a command lost.
391void UdxTransport::_wake() {
392 if (_async_ready) uv_async_send(&_async_handle);
393}
394
372395void UdxTransport::_thread_func() {
373396 uv_loop_init(&_loop);
374397 udx_init(&_loop, &_udx, nullptr);
……@@ -378,7 +401,7 @@ void UdxTransport::_thread_func() {
378401 // Only now may anyone signal it. Publish AFTER init, and flush whatever was
379402 // queued while the thread was starting.
380403 _async_ready = true;
381 uv_async_send(&_async_handle);
404 _wake();
382405
383406 // Periodic timer for processing commands.
384407 uv_timer_init(&_loop, &_tick_timer);
……@@ -450,8 +473,8 @@ void UdxTransport::run_on_loop(std::function<void()> task) {
450473 std::lock_guard<std::mutex> lock(_tasks_mutex);
451474 _tasks.push_back(std::move(task));
452475 }
453 // Queue always; wake only once the handle exists. See _async_ready.
454 if (_async_ready) uv_async_send(&_async_handle);
476 // Queue always; wake only once the handle exists. See _wake().
477 _wake();
455478}
456479
457480void UdxTransport::_process_commands() {
……@@ -1222,7 +1245,7 @@ void UdxTransport::swarm_join(const uint8_t *topic, bool server, bool client) {
12221245 cmd.server = server;
12231246 cmd.client = client;
12241247 _queue_command(std::move(cmd));
1225 uv_async_send(&_async_handle);
1248 _wake();
12261249}
12271250
12281251void UdxTransport::swarm_leave(const uint8_t *topic) {
……@@ -1230,7 +1253,7 @@ void UdxTransport::swarm_leave(const uint8_t *topic) {
12301253 cmd.type = CMD_SWARM_LEAVE;
12311254 cmd.data.assign(topic, topic + 32);
12321255 _queue_command(std::move(cmd));
1233 uv_async_send(&_async_handle);
1256 _wake();
12341257}
12351258
12361259void UdxTransport::swarm_join_peer(const uint8_t *pk) {
……@@ -1238,7 +1261,7 @@ void UdxTransport::swarm_join_peer(const uint8_t *pk) {
12381261 cmd.type = CMD_SWARM_JOIN_PEER;
12391262 memcpy(cmd.remote_public_key, pk, 32);
12401263 _queue_command(std::move(cmd));
1241 uv_async_send(&_async_handle);
1264 _wake();
12421265}
12431266
12441267void UdxTransport::swarm_leave_peer(const uint8_t *pk) {
……@@ -1246,7 +1269,7 @@ void UdxTransport::swarm_leave_peer(const uint8_t *pk) {
12461269 cmd.type = CMD_SWARM_LEAVE_PEER;
12471270 memcpy(cmd.remote_public_key, pk, 32);
12481271 _queue_command(std::move(cmd));
1249 uv_async_send(&_async_handle);
1272 _wake();
12501273}
12511274
12521275bool UdxTransport::pop_swarm_event(SwarmEvent &out) {
src/udx_transport.h +3 -0
……@@ -221,6 +221,9 @@ public:
221221 // once the loop is up.
222222 std::atomic<bool> _async_ready{ false };
223223
224 // Signal the loop. The ONLY way — see the comment on the definition.
225 void _wake();
226
224227 // Called from Godot _poll() to fire peer signals on main thread.
225228 bool pop_peer_event(PeerEvent &out);
226229