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
1 change: 1 addition & 0 deletions src/Makefile.test.include
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ DIGIBYTE_TESTS =\
test/digidollar_oracle_feeds_wave11_tests.cpp \
test/digidollar_hot_path_logging_tests.cpp \
test/musig2_net_processing_tests.cpp \
test/oracle_shutdown_tests.cpp \
test/rh50_oracle_keyset_alignment_tests.cpp \
test/rh51_checkphase3_v1_split_tests.cpp \
test/rh52_bip34_scriptnum_escape_tests.cpp \
Expand Down
7 changes: 7 additions & 0 deletions src/oracle/signing_orchestrator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -843,6 +843,13 @@ void OracleSigningOrchestrator::Shutdown()
{
if (g_signing_orchestrator) {
g_signing_orchestrator->Stop();
// Stop() only unregisters, which is non-blocking and can return while a
// notification is still in flight on the scheduler thread. Drain the
// queue before destroying the object that callback is about to touch,
// otherwise it locks m_sessions_mutex on freed memory: the scheduler
// thread then blocks forever (and Shutdown() blocks behind it in
// CScheduler::stop) or aborts, depending on the C++ runtime.
SyncWithValidationInterfaceQueue();
g_signing_orchestrator.reset();
}
}
Expand Down
56 changes: 56 additions & 0 deletions src/test/oracle_shutdown_tests.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Copyright (c) 2026 The DigiByte Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
//
// Shutdown-ordering test for the MuSig2 signing orchestrator.
//
// OracleSigningOrchestrator is a validation interface subscriber, and its
// Shutdown() destroys the object. UnregisterValidationInterface() is
// non-blocking and can return while a notification is still in flight, so
// destroying without draining the queue lets a queued BlockConnected callback
// run on freed memory (OnBlockConnected -> CleanupOldSessions locks
// m_sessions_mutex), which deadlocks the scheduler thread on glibc and aborts
// on libc++.
//
// The use-after-free itself is a microsecond-wide race and cannot be asserted
// on directly, so this pins the contract that prevents it: Shutdown() must not
// return while a queued notification is still pending.

#include <oracle/signing_orchestrator.h>
#include <util/time.h>
#include <validationinterface.h>

#include <test/util/setup_common.h>

#include <boost/test/unit_test.hpp>

#include <atomic>
#include <chrono>

BOOST_FIXTURE_TEST_SUITE(oracle_shutdown_tests, TestingSetup)

BOOST_AUTO_TEST_CASE(shutdown_waits_for_in_flight_validation_callbacks)
{
if (!g_signing_orchestrator) {
OracleSigningOrchestrator::Initialize();
}
BOOST_REQUIRE(g_signing_orchestrator);

// Occupy the validation interface queue with work that outlives a
// non-draining Shutdown(). A queued orchestrator notification behaves the
// same way; this stands in for one without depending on chain activity.
std::atomic<bool> callback_finished{false};
CallFunctionInValidationInterfaceQueue([&callback_finished] {
UninterruptibleSleep(std::chrono::milliseconds{250});
callback_finished = true;
});

OracleSigningOrchestrator::Shutdown();

// Without the drain, Shutdown() returns while the callback is still
// pending -- and the next queued notification would touch an orchestrator
// that no longer exists.
BOOST_CHECK(callback_finished.load());
}

BOOST_AUTO_TEST_SUITE_END()
Loading