init refactor: Only initialize node.notifications one time
What changed, and why it matters
This is a code cleanup in Bitcoin Core's startup sequence. It changes how an internal notification object is created so it is only created once instead of being deleted and recreated during startup. The commit message says this is needed so that other parts of the program (like mining code) can safely read this object's state while the node is starting up or shutting down, because the object's mutex would otherwise be destroyed and recreated, which could cause crashes or race conditions. There is no direct evidence in the diff of an exploitable security bug.
Treat as a defensive hardening/refactoring commit. Reviewers should verify that all paths that previously relied on a freshly constructed KernelNotifications object still behave correctly with the new explicit state reset, and that no code path can access node.notifications before it is constructed in AppInitMain. No immediate security response is indicated by the supplied materials.
Security signals we found
Use-after-free / race-condition class lifetime fix: mutex and condition variable no longer destroyed and recreated during chainstate reload
Null/dangling pointer prevention: KernelNotifications object created once early in AppInitMain before IPC exposure
State reset via setChainstateLoaded(false) instead of object replacement
Commit message explicitly frames change as needed for safe external access during startup/shutdown
No explicit CVE, advisory, or security disclosure referenced in commit or supplied materials
Evidence from the diff
The commit refactors InitAndLoadChainstate() so that node.notifications is no longer reset() and re-created on each invocation (which can happen twice during reindex). Instead, KernelNotifications is constructed once in AppInitMain() before IPC address listening, and a new setChainstateLoaded() method resets internal KernelState (currently just tip_block and a chainstate_loaded flag) while preserving the mutex and condition variable. This avoids lifetime issues where external callers (mining code, IPC methods) could access a KernelNotifications pointer whose underlying mutex was being destroyed and recreated, or access a null pointer before re-creation.
Changed components
src/init.cpp - AppInitMain and InitAndLoadChainstatesrc/node/kernel_notifications.cpp - blockTip and TipBlock state accesssrc/node/kernel_notifications.h - KernelState struct, setChainstateLoaded methodInspect captured patch +31 / −9
diff --git a/src/init.cpp b/src/init.cpp
index bdff6863..9d7c76a8 100644
--- a/src/init.cpp
+++ b/src/init.cpp
@@ -1304,16 +1304,12 @@ static ChainstateLoadResult InitAndLoadChainstate(
const ArgsManager& args)
{
// This function may be called twice, so any dirty state must be reset.
- node.notifications.reset(); // Drop state, such as a cached tip block
+ node.notifications->setChainstateLoaded(false); // Drop state, such as a cached tip block
node.mempool.reset();
node.chainman.reset(); // Drop state, such as an initialized m_block_tree_db
const CChainParams& chainparams = Params();
- Assert(!node.notifications); // Was reset above
- node.notifications = std::make_unique<KernelNotifications>(Assert(node.shutdown_request), node.exit_status, *Assert(node.warnings));
- ReadNotificationArgs(args, *node.notifications);
-
CTxMemPool::Options mempool_opts{
.check_ratio = chainparams.DefaultConsistencyChecks() ? 1 : 0,
.signals = node.validation_signals.get(),
@@ -1414,6 +1410,7 @@ static ChainstateLoadResult InitAndLoadChainstate(
std::tie(status, error) = catch_exceptions([&] { return VerifyLoadedChainstate(chainman, options); });
if (status == node::ChainstateLoadStatus::SUCCESS) {
LogInfo("Block index and chainstate loaded");
+ node.notifications->setChainstateLoaded(true);
}
}
return {status, error};
@@ -1486,6 +1483,13 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info)
node.validation_signals = std::make_unique<ValidationSignals>(std::make_unique<SerialTaskRunner>(scheduler));
auto& validation_signals = *node.validation_signals;
+ // Create KernelNotifications object. Important to do this early before
+ // calling ipc->listenAddress() below so makeMining and other IPC methods
+ // can use this.
+ assert(!node.notifications);
+ node.notifications = std::make_unique<KernelNotifications>(Assert(node.shutdown_request), node.exit_status, *Assert(node.warnings));
+ ReadNotificationArgs(args, *node.notifications);
+
// Create client interfaces for wallets that are supposed to be loaded
// according to -wallet and -disablewallet options. This only constructs
// the interfaces, it doesn't load wallet data. Wallets actually get loaded
diff --git a/src/node/kernel_notifications.cpp b/src/node/kernel_notifications.cpp
index f207b49a..ab0e5ccb 100644
--- a/src/node/kernel_notifications.cpp
+++ b/src/node/kernel_notifications.cpp
@@ -53,7 +53,7 @@ kernel::InterruptResult KernelNotifications::blockTip(SynchronizationState state
{
LOCK(m_tip_block_mutex);
Assume(index.GetBlockHash() != uint256::ZERO);
- m_tip_block = index.GetBlockHash();
+ m_state.tip_block = index.GetBlockHash();
m_tip_block_cv.notify_all();
}
@@ -103,7 +103,7 @@ void KernelNotifications::fatalError(const bilingual_str& message)
std::optional<uint256> KernelNotifications::TipBlock()
{
AssertLockHeld(m_tip_block_mutex);
- return m_tip_block;
+ return m_state.tip_block;
};
diff --git a/src/node/kernel_notifications.h b/src/node/kernel_notifications.h
index e9f1e8f1..b90248bf 100644
--- a/src/node/kernel_notifications.h
+++ b/src/node/kernel_notifications.h
@@ -29,6 +29,18 @@ namespace node {
class Warnings;
static constexpr int DEFAULT_STOPATHEIGHT{0};
+//! State tracked by the KernelNotifications interface meant to be used by
+//! mining code, index code, RPCs, and other code sitting above the validation
+//! layer.
+//!
+//! Currently just tracks the chain tip, but could be used to hold other
+//! information in the future, like the last flushed block, pruning
+//! information, etc.
+struct KernelState {
+ bool chainstate_loaded{false};
+ std::optional<uint256> tip_block;
+};
+
class KernelNotifications : public kernel::Notifications
{
public:
@@ -49,6 +61,13 @@ public:
void fatalError(const bilingual_str& message) override;
+ void setChainstateLoaded(bool chainstate_loaded) EXCLUSIVE_LOCKS_REQUIRED(!m_tip_block_mutex) {
+ LOCK(m_tip_block_mutex);
+ if (!chainstate_loaded) m_state = {};
+ m_state.chainstate_loaded = chainstate_loaded;
+ m_tip_block_cv.notify_all();
+ }
+
//! Block height after which blockTip notification will return Interrupted{}, if >0.
int m_stop_at_height{DEFAULT_STOPATHEIGHT};
//! Useful for tests, can be set to false to avoid shutdown on fatal error.
@@ -56,6 +75,7 @@ public:
Mutex m_tip_block_mutex;
std::condition_variable m_tip_block_cv GUARDED_BY(m_tip_block_mutex);
+ KernelState m_state GUARDED_BY(m_tip_block_mutex);
//! The block for which the last blockTip notification was received.
//! It's first set when the tip is connected during node initialization.
//! Might be unset during an early shutdown.
@@ -65,8 +85,6 @@ private:
const std::function<bool()>& m_shutdown_request;
std::atomic<int>& m_exit_status;
node::Warnings& m_warnings;
-
- std::optional<uint256> m_tip_block GUARDED_BY(m_tip_block_mutex);
};
void ReadNotificationArgs(const ArgsManager& args, KernelNotifications& notifications);
Why this scored 24/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.