net: Prevent node from binding to the same CService
What changed, and why it matters
This change is a user-experience and reliability fix, not a security patch in the traditional sense. It stops Bitcoin Core from starting when the user accidentally gives it the same network binding address twice (for example, '-bind=0.0.0.0 -bind=0.0.0.0'). Previously the node would crash later with a confusing 'already running' message. Now it exits early with a clear message about the duplicate configuration. There is no evidence it fixes an exploitable vulnerability.
Treat as a normal reliability/usability improvement. No urgent security action required. Reviewers may want to confirm the duplicate detection correctly handles default ephemeral ports and any future binding types.
Security signals we found
Prevents misleading fatal error that could delay incident response
Adds input validation for binding configuration
No memory safety, cryptographic, or authorization changes
No remote or local privilege escalation path evident from diff
Evidence from the diff
The commit adds a new static helper CheckBindingConflicts() in src/init.cpp that collects all configured CService bindings from vWhiteBinds, vBinds, and onion_binds into a std::set and returns the first duplicate. AppInitMain() calls this before node.connman->Start() and returns an InitError with a specific message if a duplicate is found. A functional test is added to verify duplicate -bind, -bind=…=onion, and -whitebind combinations are rejected.
Changed components
src/init.cpptest/functional/feature_bind_extra.pyInspect captured patch +55 / −3
diff --git a/src/init.cpp b/src/init.cpp
index b6b52e2c..2856b819 100644
--- a/src/init.cpp
+++ b/src/init.cpp
@@ -1233,6 +1233,40 @@ bool CheckHostPortOptions(const ArgsManager& args) {
return true;
}
+/**
+ * @brief Checks for duplicate bindings across all binding configurations.
+ *
+ * @param[in] conn_options Connection options containing the binding vectors to check
+ * @return std::optional<CService> containing the first duplicate found, or std::nullopt if no duplicates
+ */
+static std::optional<CService> CheckBindingConflicts(const CConnman::Options& conn_options)
+{
+ std::set<CService> seen;
+
+ // Check all whitelisted bindings
+ for (const auto& wb : conn_options.vWhiteBinds) {
+ if (!seen.insert(wb.m_service).second) {
+ return wb.m_service;
+ }
+ }
+
+ // Check regular bindings
+ for (const auto& bind : conn_options.vBinds) {
+ if (!seen.insert(bind).second) {
+ return bind;
+ }
+ }
+
+ // Check onion bindings
+ for (const auto& onion_bind : conn_options.onion_binds) {
+ if (!seen.insert(onion_bind).second) {
+ return onion_bind;
+ }
+ }
+
+ return std::nullopt;
+}
+
// A GUI user may opt to retry once with do_reindex set if there is a failure during chainstate initialization.
// The function therefore has to support re-entry.
static ChainstateLoadResult InitAndLoadChainstate(
@@ -2142,6 +2176,13 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info)
connOptions.m_i2p_accept_incoming = args.GetBoolArg("-i2pacceptincoming", DEFAULT_I2P_ACCEPT_INCOMING);
+ if (auto conflict = CheckBindingConflicts(connOptions)) {
+ return InitError(strprintf(
+ _("Duplicate binding configuration for address %s. "
+ "Please check your -bind, -bind=...=onion and -whitebind settings."),
+ conflict->ToStringAddrPort()));
+ }
+
if (!node.connman->Start(scheduler, connOptions)) {
return false;
}
diff --git a/test/functional/feature_bind_extra.py b/test/functional/feature_bind_extra.py
index 6b53de18..d2f3daf9 100755
--- a/test/functional/feature_bind_extra.py
+++ b/test/functional/feature_bind_extra.py
@@ -3,10 +3,12 @@
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
"""
-Test starting bitcoind with -bind and/or -bind=...=onion and confirm
-that bind happens on the expected ports.
+Test starting bitcoind with -bind and/or -bind=...=onion, confirm that
+it binds to the expected ports, and verify that duplicate or conflicting
+-bind/-whitebind configurations are rejected with a descriptive error.
"""
+from itertools import combinations_with_replacement
from test_framework.netutil import (
addr_to_hex,
get_bind_addrs,
@@ -14,13 +16,13 @@ from test_framework.netutil import (
from test_framework.test_framework import (
BitcoinTestFramework,
)
+from test_framework.test_node import ErrorMatch
from test_framework.util import (
assert_equal,
p2p_port,
rpc_port,
)
-
class BindExtraTest(BitcoinTestFramework):
def set_test_params(self):
self.setup_clean_chain = True
@@ -87,5 +89,14 @@ class BindExtraTest(BitcoinTestFramework):
binds = set(filter(lambda e: e[1] != rpc_port(i), binds))
assert_equal(binds, set(expected_services))
+ self.stop_node(0)
+
+ addr = "127.0.0.1:11012"
+ for opt1, opt2 in combinations_with_replacement([f"-bind={addr}", f"-bind={addr}=onion", f"-whitebind=noban@{addr}"], 2):
+ self.nodes[0].assert_start_raises_init_error(
+ [opt1, opt2],
+ "Error: Duplicate binding configuration",
+ match=ErrorMatch.PARTIAL_REGEX)
+
if __name__ == '__main__':
BindExtraTest(__file__).main()
Why this scored 25/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.