Audit report AR-33

Reported privately in an independent security review, 2026-09-16.

Report date
2026-09-16
Kind
Bug
Status on master
Present
Proposed issue state
Open

Summary

libmultiprocess's deprecated EventLoop constructor, which accepts the old-style log callback, always wraps that callback in a new LogFn, even when it is empty. The first log message then calls the empty std::function and throws std::bad_function_call, so passing an empty callback to turn logging off makes logging throw instead. The report is correct. Bitcoin Core passes a real callback and is not affected; this is a small bug for other library users.

The report

For compatibility, EventLoop has a constructor that takes the previous callback type, std::function<void(bool, std::string)>. It captures that callback in a lambda and passes the lambda on as the new LogFn, so the LogFn always looks set, even when the old callback was empty. The first enabled MP_LOG call then invokes the empty function and throws std::bad_function_call.

The report limits the impact to direct libmultiprocess users that call the deprecated constructor with an empty callback: a normal log message throws, synchronously and visibly. It found no peer, RPC or memory-safety impact and no effect on Bitcoin Core's callback.

The reproduction is a KJ test against the real library, "Empty legacy log callback", which creates an event loop this way on its own thread and records the exception from the first log message.

Proposed change

Leave the translated LogFn empty when the old callback is empty, and wrap non-empty callbacks as before:

     EventLoop(const char* exe_name, std::function<void(bool, std::string)> old_callback, void* context = nullptr)
         : EventLoop(exe_name,
-                LogFn{[old_callback = std::move(old_callback)](LogMessage log_data) {old_callback(log_data.level == Log::Raise, std::move(log_data.message));}},
+                old_callback ? LogFn{[old_callback = std::move(old_callback)](LogMessage log_data) {old_callback(log_data.level == Log::Raise, std::move(log_data.message));}} : LogFn{},
                 context){}

The test's expectation changes from an exception to no exception. The report prefers landing the change in libmultiprocess first, followed by a normal subtree update.

Assessment

Present on libmultiprocess master: the deprecated constructor wraps old_callback unconditionally. Bitcoin Core uses the current constructor with a real log function, so it cannot hit this. No peer is involved, and there is no stability effect for Bitcoin Core; outside the survey's scope.

Related logging report: AR-35 (log callbacks that throw during cleanup).

Status

Present on master. Next step: a one-line libmultiprocess PR with the test. No public PR yet.

Reported privately in an independent security review on 2026-09-16.