OverviewFilesCommitsIssuesDocsReleasesPeers

hyperswarm-peer commits / 01e8400

swarmd: content teardown freed the index out from under the loop
Every content command — serve, fetch, cancel, the progress sample — is a lambda queued onto the UDX loop that captures `this` and reads index_ and proto_. Content::close() posted its channel teardown and then reset both immediately, on the main thread, without waiting: a serve posted a moment earlier ran afterwards, against an index that had been destroyed. Measured before the fix as a SEGV on the UDX thread inside Content::serve's lambda, at an unordered_map lookup in the chunk-map index, in two runs out of three of swarmd's test_client. run_on_loop is FIFO, so a teardown posted last runs last — but only if we wait for it before freeing anything. It now does, bounded at two seconds because hanging a shutdown on a transport that has already stopped would be worse than skipping work that transport can no longer do. That is what HyperOverlay::detach does for the same hazard, and this is the same hazard. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
01e84007aeff66345c7da6be146ec385bcb44d06
by plankatron <fleeky@gmail.com> · 2026-09-10T22:07:35.000Z

Changed files (1) +27 -4

swarmd/src/content.cpp +27 -4
……@@ -2,7 +2,9 @@
22
33#include <algorithm>
44#include <chrono>
5#include <condition_variable>
56#include <cstdio>
7#include <mutex>
68
79namespace swarmd {
810
……@@ -68,18 +70,39 @@ bool Content::open(const std::string &storage_dir, UdxTransport *transport) {
6870void Content::close() {
6971 if (!open_) return;
7072 open_ = false;
71 // The channels belong to the UDX loop and must be torn down there. If the
72 // transport is already stopped there is no loop to marshal to, and nothing
73 // else is touching them either.
73
74 // ⛔ WAIT FOR THE LOOP, DO NOT JUST POST TO IT.
75 //
76 // Every command — serve, fetch, cancel, the progress sample — is a lambda
77 // queued onto the UDX loop that captures `this` and reads index_ and proto_.
78 // Posting the teardown and then resetting them here races those: a serve
79 // posted a moment earlier runs afterwards, against an index that has been
80 // destroyed. Measured, as an intermittent SEGV on the UDX thread inside
81 // Content::serve's lambda, in two runs out of three.
82 //
83 // run_on_loop is FIFO, so a teardown posted last runs last — but only if we
84 // wait for it before freeing anything. That is what HyperOverlay::detach
85 // does (src/overlay_node.cpp) and this is the same hazard.
7486 if (t_) {
75 t_->run_on_loop([this]() {
87 std::mutex m;
88 std::condition_variable cv;
89 bool done = false;
90 t_->run_on_loop([this, &m, &cv, &done]() {
7691 for (auto &kv : peers_) {
7792 if (proto_) proto_->remove_peer(kv.second);
7893 delete kv.second;
7994 }
8095 peers_.clear();
96 { std::lock_guard<std::mutex> lk(m); done = true; }
97 cv.notify_one();
8198 });
99 std::unique_lock<std::mutex> lk(m);
100 // Bounded: a transport already stopped will never run this, and hanging
101 // a shutdown on it would be worse than the leak of not running it.
102 cv.wait_for(lk, std::chrono::seconds(2), [&done]() { return done; });
82103 }
104
105 // Only now is nothing on the loop pointing at either of these.
83106 proto_.reset();
84107 if (index_) index_->close();
85108 index_.reset();