Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/linux.yml
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,7 @@ jobs:
run: |
cd test
b2 ${{ matrix.config }} -l500 warnings-as-errors=on debug-iterators=on asserts=on deterministic-tests
b2 ${{ matrix.config }} -l500 testing.execute=off warnings-as-errors=on debug-iterators=on asserts=on test_lsd test_hasher test_hasher512 test_natpmp enum_if
b2 ${{ matrix.config }} -l500 testing.execute=off warnings-as-errors=on debug-iterators=on asserts=on test_lsd test_lsd_teardown test_hasher test_hasher512 test_natpmp enum_if
b2 ${{ matrix.config }} -l500 warnings-as-errors=on debug-iterators=on asserts=on simulate-slow=hash test_slow_hash
b2 ${{ matrix.config }} -l500 warnings-as-errors=on debug-iterators=on asserts=on simulate-slow=write test_disk_io

Expand Down
1 change: 1 addition & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
2.1.1 not released

* fix shutdown hang when uTP sockets are stalled on a closed UDP socket
* require webtorrent RTC offer IDs to be exactly 20 bytes
* fix point-to-point interfaces without a route to the internet being used for outgoing traffic
* apply IP filter to DHT node
Expand Down
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -951,6 +951,7 @@ TEST_SOURCES = \
test_ip_voter.cpp \
test_listen_socket.cpp \
test_lsd.cpp \
test_lsd_teardown.cpp \
test_magnet.cpp \
test_merkle.cpp \
test_merkle_tree.cpp \
Expand Down
10 changes: 10 additions & 0 deletions include/libtorrent/aux_/session_impl.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -768,6 +768,16 @@ namespace aux {

void stop_ip_notifier();
void stop_lsd();

// closes the listen socket's UDP socket and aborts the uTP
// connections bound to it, so their completion handlers (holding
// peer_connection references) are released rather than leaked.
// As a side effect, every stalled uTP socket in the matching
// socket manager is woken, not just the ones bound to this
// socket; sockets stalled on other (still open) UDP sockets
// simply re-subscribe
void close_udp_listen_socket(std::shared_ptr<listen_socket_t> const& s);

void stop_natpmp();
void stop_upnp();

Expand Down
3 changes: 3 additions & 0 deletions include/libtorrent/aux_/utp_socket_manager.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ namespace aux {
, error_code& ec, udp_send_flags_t flags = {});
void subscribe_writable(utp_socket_impl* s);

// aborts every uTP socket bound to this UDP socket, and wakes
// stalled sockets so they can be deleted. The UDP socket must have
// been closed before calling this
void remove_udp_socket(std::weak_ptr<utp_socket_interface> sock);

// internal, used by utp_stream
Expand Down
30 changes: 23 additions & 7 deletions src/session_impl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1109,12 +1109,12 @@ bool ssl_server_name_callback(ssl::stream_handle_type stream_handle, std::string
TORRENT_ASSERT(!ec);
}

// TODO: 3 closing the udp sockets here means that
// the uTP connections cannot be closed gracefully
if (l->udp_sock)
{
l->udp_sock->sock.close();
}
// this closes the UDP socket abruptly; the uTP connections on it
// are aborted rather than closed gracefully. Leaving them alive
// would let their completion handlers pin peers in
// m_undead_peers (and keep num_sockets() > 0) forever,
// preventing on_tick() from ever completing the shutdown
close_udp_listen_socket(l);
}

// we need to give all the sockets an opportunity to actually have their handlers
Expand Down Expand Up @@ -2204,7 +2204,7 @@ namespace {
}
#endif
if ((*remove_iter)->sock) (*remove_iter)->sock->close(ec);
if ((*remove_iter)->udp_sock) (*remove_iter)->udp_sock->sock.close();
close_udp_listen_socket(*remove_iter);
if ((*remove_iter)->natpmp_mapper) (*remove_iter)->natpmp_mapper->close();
if ((*remove_iter)->upnp_mapper) (*remove_iter)->upnp_mapper->close();
if ((*remove_iter)->lsd) (*remove_iter)->lsd->close();
Expand Down Expand Up @@ -7184,6 +7184,22 @@ namespace {
}
}

void session_impl::close_udp_listen_socket(std::shared_ptr<listen_socket_t> const& s)
{
if (!s->udp_sock) return;
s->udp_sock->sock.close();
// the uTP socket manager requires the UDP socket to be closed
// before being told it is going away. Only the manager matching
// this socket's transport can have uTP sockets bound to it, so
// don't wake the other manager's stalled sockets spuriously
#ifdef TORRENT_SSL_PEERS
if (s->ssl == transport::ssl)
m_ssl_utp_socket_manager.remove_udp_socket(s);
else
#endif
m_utp_socket_manager.remove_udp_socket(s);
}

void session_impl::stop_natpmp()
{
for (auto& s : m_listen_sockets)
Expand Down
15 changes: 15 additions & 0 deletions src/utp_socket_manager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -266,9 +266,24 @@ namespace libtorrent::aux {
m_drained_event.push_back(s);
}

// precondition: the UDP socket must have been closed before this is
// called. Stalled sockets are woken below on the assumption that their
// send attempts fail against the closed socket, tearing the connection
// down; on a still-open (backpressured) socket they would just stall
// again
void utp_socket_manager::remove_udp_socket(std::weak_ptr<utp_socket_interface> sock)
{
auto iface = sock.lock();

// sockets that stalled on this UDP socket will never receive their
// writable notification (the UDP socket is going away). Deliver it
// now: writable() clears the stalled state, which would otherwise
// prevent the socket from ever being deleted (should_delete()
// requires !m_stalled). Sockets stalled on other (still live)
// interfaces simply re-subscribe, just as they do when
// on_udp_writeable() flushes all stalled sockets
writable();

for (auto& s : m_utp_sockets)
{
if (s.second->m_sock.lock() != iface)
Expand Down
2 changes: 2 additions & 0 deletions test/Jamfile
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,8 @@ run test_priority.cpp ;
run test_upnp.cpp ;
run test_lsd.cpp ;
explicit test_lsd ;
run test_lsd_teardown.cpp ;
explicit test_lsd_teardown ;
run test_hasher.cpp ;
explicit test_hasher ;
run test_hasher512.cpp ;
Expand Down
125 changes: 125 additions & 0 deletions test/test_lsd_teardown.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/*

Copyright (c) 2026, KSEGIT
All rights reserved.

You may use, distribute and modify this code under the terms of the BSD license,
see LICENSE file.
*/

#include "libtorrent/session.hpp"
#include "libtorrent/settings_pack.hpp"
#include "libtorrent/add_torrent_params.hpp"
#include "libtorrent/magnet_uri.hpp"
#include "libtorrent/error_code.hpp"

#include "libtorrent/aux_/scope_end.hpp"

#include "test.hpp"
#include "test_utils.hpp" // for test_listen_interface()

#include <chrono>
#include <condition_variable>
#include <cstdio>
#include <cstdlib>
#include <mutex>
#include <thread>

// Regression guard for the macOS shutdown freeze (qbittorrent/qBittorrent#24353)
// and the libtorrent session-destructor hang (#4510).
//
// With Local Service Discovery enabled and an active torrent, the single
// io_context thread periodically runs on_lsd_announce -> announce_lsd ->
// lsd::announce_impl. Historically that performed a *synchronous*, uncancellable
// multicast send_to(). When the outbound interface stalls (e.g. a VPN tunnel
// that vanishes across sleep/wake) the send blocks in-kernel forever, the io
// thread never returns to its run loop, abort() (dispatched onto that same
// thread) never runs, and session_proxy::~session_proxy()'s std::thread::join()
// deadlocks. Downstream this is the macOS "must force quit" freeze.
//
// This test pins the portable invariant the fix protects: with LSD active,
// aborting the session and destroying the session_proxy completes within a
// bounded time. The bound is deliberately generous (30s vs the few
// milliseconds a healthy teardown takes) so the test is not timing-flaky; a
// true deadlock blows well past it. Note: on a healthy CI interface a
// blocking send also returns quickly, so this test cannot simulate the
// stalled-interface trigger itself (nor does it exercise the uTP teardown
// path - no peer connections exist). On hosts where no interface can join
// the LSD multicast group the announce is skipped entirely and only the
// bounded-teardown invariant is exercised. The test guards that invariant
// and, via the watchdog below, makes any future teardown deadlock fail fast
// with attribution instead of hanging the test binary.
TORRENT_TEST(lsd_teardown_is_bounded)
{
using namespace lt;
using namespace std::chrono;

// if the deadlock this test guards against regresses, the session_proxy
// destructor below never returns and the TEST_CHECK at the end is never
// reached. This watchdog converts that indefinite hang into a bounded,
// clearly-attributed failure instead of an opaque CI job timeout.
std::mutex done_mutex;
std::condition_variable done_cv;
bool done = false;
std::thread watchdog([&] {
std::unique_lock<std::mutex> l(done_mutex);
if (done_cv.wait_for(l, seconds(90), [&] { return done; })) return;
std::fprintf(stderr, "test_lsd_teardown: TIMEOUT - session teardown "
"appears deadlocked (io thread not joinable)\n");
std::abort();
});
// join via scope guard: if anything below throws, unwinding through a
// joinable std::thread would call std::terminate
auto join_watchdog = aux::scope_end([&] {
{
std::lock_guard<std::mutex> l(done_mutex);
done = true;
}
done_cv.notify_one();
watchdog.join();
});

steady_clock::time_point start;
{
// declared first so it is destroyed last (joins the io thread)
session_proxy proxy;

settings_pack pack;
pack.set_int(settings_pack::alert_mask
, alert_category::error | alert_category::status);
pack.set_bool(settings_pack::enable_dht, false);
pack.set_bool(settings_pack::enable_lsd, true);
pack.set_bool(settings_pack::enable_upnp, false);
pack.set_bool(settings_pack::enable_natpmp, false);
pack.set_str(settings_pack::listen_interfaces, test_listen_interface());

lt::session ses(pack);

// add an (active) torrent so the LSD announce path actually fires
error_code ec;
add_torrent_params atp = parse_magnet_uri(
"magnet:?xt=urn:btih:0123456789abcdef0123456789abcdef01234567", ec);
TEST_CHECK(!ec);
atp.save_path = ".";
atp.flags &= ~torrent_flags::paused;
torrent_handle th = ses.add_torrent(atp, ec);
TEST_CHECK(!ec);

// deterministically drive the LSD announce path rather than waiting
// for the periodic announce timer
th.force_lsd_announce();
// give the io thread time to process the announce
std::this_thread::sleep_for(milliseconds(500));

start = steady_clock::now();
proxy = ses.abort();
// end of scope: ses is destroyed, then proxy's dtor joins the io thread
}
auto const elapsed = duration_cast<milliseconds>(steady_clock::now() - start);

std::printf("LSD session teardown took %d ms\n", int(elapsed.count()));

// If the io thread had wedged in a synchronous LSD send, the join above
// would never return and we would never reach this line within the bound.
TEST_CHECK(elapsed < milliseconds(30000));
}