diff --git a/CHANGELOG.md b/CHANGELOG.md index 0da4c53..c83ed8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -193,6 +193,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 accounting change. Operators with mixed outbound + inbound deployments no longer see legitimate inbound peers rejected once outbound connections fill the pool past the configured cap. +- `PeerRecvDrain::drop` no longer calls `std::thread::join` on the + worker thread. The drain worker uses `packet_tx.blocking_send(...)` + on a tokio mpsc Sender, which internally parks the worker in + `tokio::block_on` on the same `current_thread` runtime that drives + `rx_loop`. Joining synchronously from inside `remove_active_peer` + (which runs on the runtime thread, the runtime's sole driver) + produced a circular wait: rx_loop blocked in libc futex via + `Thread::join`, the worker unable to observe the stop flag because + the runtime that polls it is the very thread now blocked joining + it, and all other peer-drain workers parked on the same runtime + via `block_on`. Full daemon wedge, fipsctl unresponsive, SIGTERM + ignored. Trigger was peer-removal via the 30-s link-dead-timeout + cleanup path with any in-flight worker, with statistical likelihood + amplified by aggressive multi-npub-from-one-NAT reconnect patterns + but not bounded to them. Fix: detach the std::thread (drop the + `JoinHandle` without joining); the stop flag + self-pipe write + already signal the worker to exit; the kernel-level `libc::poll()` + inside the drain loop sees the wake, checks the flag, exits, and + the OS reclaims the thread state independently. - Outbound connection initiation now honors the `node.limits.max_peers` cap that was previously only checked on inbound msg1 admission. Four paths gated: auto-reconnect retries (`process_pending_retries`), diff --git a/src/transport/udp/peer_drain.rs b/src/transport/udp/peer_drain.rs index fe8df5b..5e12442 100644 --- a/src/transport/udp/peer_drain.rs +++ b/src/transport/udp/peer_drain.rs @@ -118,11 +118,20 @@ impl Drop for PeerRecvDrain { // POLLIN, observe the stop flag, and exit. let byte = 1u8; let _ = unsafe { libc::write(self.stop_pipe_tx, &byte as *const _ as *const _, 1) }; - // 3. Join — bounded wait, the thread exits within one - // poll-iteration of seeing the stop flag. - if let Some(j) = self.join.take() { - let _ = j.join(); - } + // 3. Detach the std::thread (drop the JoinHandle without joining). + // The drain loop sends inbound packets via + // `packet_tx.blocking_send(...)` on a tokio mpsc Sender, which + // internally parks the worker thread in `tokio::block_on` on + // the *same* current_thread runtime that drives `rx_loop`. + // `rx_loop` is the sole runtime driver. Calling `join()` here + // blocks the runtime thread in libc futex — and the worker + // being joined can only make progress (to observe the stop + // flag + exit its loop) by being polled by that same runtime. + // Circular wait; full daemon wedge. Dropping the JoinHandle + // detaches the thread; the kernel-level libc::poll() sees the + // self-pipe wake, the drain loop checks the stop flag, exits, + // and the OS reclaims the thread state independently. + drop(self.join.take()); // 4. Close the write end of the pipe. unsafe { libc::close(self.stop_pipe_tx) }; }