signals: Add a simplified boost-compatible implementation
What changed, and why it matters
This commit replaces Bitcoin Core's use of the Boost signals2 library with a new, simplified in-house implementation called btcsignals. It is a code refactoring/reimplementation intended to reduce dependency on Boost. The change itself does not fix a known bug or vulnerability, but any reimplementation of a threading/callback mechanism carries risk of introducing subtle concurrency or lifetime bugs.
Treat this as a high-risk refactoring: conduct thorough code review focused on thread safety, reentrancy, object lifetime, and exception safety; run stress tests with concurrent connect/disconnect/emit operations; verify behavior matches the replaced boost::signals2 semantics in all call sites.
Security signals we found
Reimplementation of a concurrency-sensitive callback/signal mechanism
Use of mutex, shared_ptr, and atomic_bool for slot lifetime and thread safety
Callback list copied under lock then invoked without lock, which can affect ordering and reentrancy behavior
Garbage collection of disconnected slots only happens on connect(), which could leave stale entries if no new connections occur
No explicit vendor security disclosure or CVE associated with this commit
Evidence from the diff
The commit adds src/btcsignals.h, a minimal API-compatible replacement for boost::signals2. It provides connection, scoped_connection, and signal classes using std::function, std::shared_ptr, std::atomic_bool, and a Mutex. The signal caches callbacks before invocation to avoid holding the lock during callbacks, garbage-collects disconnected slots in connect(), and supports optional_last_value semantics. It disables copy/move for signal and scoped_connection. The implementation is new code replacing a mature library, so correctness depends on review and testing.
Changed components
src/btcsignals.hBoost signals2 replacement used by Bitcoin Core event/callback systemInspect captured patch +246 / −4
diff --git a/src/btcsignals.h b/src/btcsignals.h
index 415d8b86..f4c2530b 100644
--- a/src/btcsignals.h
+++ b/src/btcsignals.h
@@ -5,10 +5,252 @@
#ifndef BITCOIN_BTCSIGNALS_H
#define BITCOIN_BTCSIGNALS_H
-#include <boost/signals2/connection.hpp>
-#include <boost/signals2/optional_last_value.hpp>
-#include <boost/signals2/signal.hpp>
+#include <sync.h>
-namespace btcsignals = boost::signals2;
+#include <algorithm>
+#include <atomic>
+#include <functional>
+#include <memory>
+#include <optional>
+#include <type_traits>
+#include <utility>
+#include <vector>
+
+/**
+ * btcsignals is a simple mechanism for signaling events to multiple subscribers.
+ * It is api-compatible with a minimal subset of boost::signals2.
+ *
+ * Rather than using a custom slot type, and the features/complexity that they
+ * imply, std::function is used to store the callbacks. Lifetime management of
+ * the callbacks is left up to the user.
+ *
+ * All usage is thread-safe except for interacting with a connection while
+ * copying/moving it on another thread.
+ */
+
+namespace btcsignals {
+
+/*
+ * optional_last_value is the default and only supported combiner.
+ * As such, its behavior is embedded into the signal functor.
+ *
+ * Because optional<void> is undefined, void must be special-cased.
+ */
+
+template <typename T>
+class optional_last_value
+{
+public:
+ using result_type = std::conditional_t<std::is_void_v<T>, void, std::optional<T>>;
+};
+
+template <typename Signature, typename Combiner = optional_last_value<typename std::function<Signature>::result_type>>
+class signal;
+
+/*
+ * State object representing the liveness of a registered callback.
+ * signal::connect() returns an enabled connection which can be held and
+ * disabled in the future.
+ */
+class connection
+{
+ template <typename Signature, typename Combiner>
+ friend class signal;
+
+ /*
+ * Tag for the constructor used by signal.
+ */
+ struct enabled_tag_type {
+ };
+ static constexpr enabled_tag_type enabled_tag{};
+
+ /**
+ * connections have shared_ptr-like copy and move semantics.
+ */
+ std::shared_ptr<std::atomic_bool> m_connected{};
+
+ /**
+ * Only a signal can create an enabled connection.
+ */
+ explicit connection(enabled_tag_type /*unused*/) : m_connected{std::make_shared<std::atomic_bool>(true)} {}
+
+public:
+ /**
+ * The default constructor creates a connection with no associated signal
+ */
+ constexpr connection() noexcept = default;
+
+ /**
+ * If a callback is associated with this connection, prevent it from being
+ * called in the future.
+ *
+ * If a connection is disabled as part of a signal's callback function, it
+ * will _not_ be executed in the current signal invocation.
+ *
+ * Note that disconnected callbacks are not removed from their owning
+ * signals here. They are garbage collected in signal::connect().
+ */
+ void disconnect()
+ {
+ if (m_connected) {
+ m_connected->store(false);
+ }
+ }
+
+ /**
+ * Returns true if this connection was created by a signal and has not been
+ * disabled.
+ */
+ bool connected() const
+ {
+ return m_connected && m_connected->load();
+ }
+};
+
+/*
+ * RAII-style connection management
+ */
+class scoped_connection
+{
+ connection m_conn;
+
+public:
+ scoped_connection(connection rhs) noexcept : m_conn{std::move(rhs)} {}
+
+ scoped_connection(scoped_connection&&) noexcept = default;
+ scoped_connection& operator=(scoped_connection&&) noexcept = default;
+
+ /**
+ * For simplicity, disable copy assignment and construction.
+ */
+ scoped_connection& operator=(const scoped_connection&) = delete;
+ scoped_connection(const scoped_connection&) = delete;
+
+ void disconnect()
+ {
+ m_conn.disconnect();
+ }
+
+ ~scoped_connection()
+ {
+ disconnect();
+ }
+};
+
+/*
+ * Functor for calling zero or more connected callbacks
+ */
+template <typename Signature, typename Combiner>
+class signal
+{
+ using function_type = std::function<Signature>;
+
+ static_assert(std::is_same_v<Combiner, optional_last_value<typename function_type::result_type>>, "only the optional_last_value combiner is supported");
+
+ /*
+ * Helper struct for maintaining a callback and its associated connection
+ */
+ struct connection_holder {
+ template <typename Callable>
+ connection_holder(Callable&& callback) : m_callback{std::forward<Callable>(callback)}
+ {
+ }
+
+ connection m_connection{connection::enabled_tag};
+ function_type m_callback;
+ };
+
+ mutable Mutex m_mutex;
+
+ /* Store connection_holders as shared_ptrs to avoid having to copy them by
+ * value in operator().
+ */
+ std::vector<std::shared_ptr<connection_holder>> m_connections GUARDED_BY(m_mutex){};
+
+public:
+ using result_type = Combiner::result_type;
+
+ constexpr signal() noexcept = default;
+ ~signal() = default;
+
+ /*
+ * For simplicity, disable all moving/copying/assigning.
+ */
+ signal(const signal&) = delete;
+ signal(signal&&) = delete;
+ signal& operator=(const signal&) = delete;
+ signal& operator=(signal&&) = delete;
+
+ /*
+ * Execute all enabled callbacks for the signal. Rather than allowing for
+ * custom combiners, the behavior of optional_last_value is hard-coded
+ * here. Return the value of the last executed callback, or nullopt if none
+ * were executed.
+ *
+ * Callbacks which return void require special handling.
+ *
+ * In order to avoid locking during the callbacks, the list of callbacks is
+ * cached before they are called. This allows a callback to call connect(),
+ * but the newly connected callback will not be run during the current
+ * signal invocation.
+ *
+ * Note that the parameters are accepted as universal references, though
+ * they are not perfectly forwarded as that could cause a use-after-move if
+ * more than one callback is enabled.
+ */
+ template <typename... Args>
+ result_type operator()(Args&&... args) const EXCLUSIVE_LOCKS_REQUIRED(!m_mutex)
+ {
+ std::vector<std::shared_ptr<connection_holder>> connections;
+ {
+ LOCK(m_mutex);
+ connections = m_connections;
+ }
+ if constexpr (std::is_void_v<result_type>) {
+ for (const auto& connection : connections) {
+ if (connection->m_connection.connected()) {
+ connection->m_callback(args...);
+ }
+ }
+ } else {
+ result_type ret{std::nullopt};
+ for (const auto& connection : connections) {
+ if (connection->m_connection.connected()) {
+ ret.emplace(connection->m_callback(args...));
+ }
+ }
+ return ret;
+ }
+ }
+
+ /*
+ * Connect a new callback to the signal. A forwarding callable accepts
+ * anything that can be stored in a std::function.
+ */
+ template <typename Callable>
+ connection connect(Callable&& func) EXCLUSIVE_LOCKS_REQUIRED(!m_mutex)
+ {
+ LOCK(m_mutex);
+
+ // Garbage-collect disconnected connections to prevent unbounded growth
+ std::erase_if(m_connections, [](const auto& holder) { return !holder->m_connection.connected(); });
+
+ const auto& connection = m_connections.emplace_back(std::make_shared<connection_holder>(std::forward<Callable>(func)));
+ return connection->m_connection;
+ }
+
+ /*
+ * Returns true if there are no enabled callbacks
+ */
+ [[nodiscard]] bool empty() const EXCLUSIVE_LOCKS_REQUIRED(!m_mutex)
+ {
+ LOCK(m_mutex);
+ return std::ranges::none_of(m_connections, [](const auto& holder) {
+ return holder->m_connection.connected();
+ });
+ }
+};
+
+} // namespace btcsignals
#endif // BITCOIN_BTCSIGNALS_H
Why this scored 27/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.