Audit report AR-63

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

Report date
2026-09-27
Kind
Hardening
Status on master
Present
Proposed issue state
Open

Summary

When a libmultiprocess server object is released without its client calling destroy, the object's cleanup callbacks run on the asynchronous cleanup thread with no exception handling, so a callback that throws terminates the process and the callbacks after it never run. The report is right about the code, but neither libmultiprocess nor Bitcoin Core registers a server-side cleanup callback, so Bitcoin Core is not affected. The Bitcoin Core callback the report cites runs on a different path that this change does not touch. Robustness for other library users, in the same family as AR-35 (throwing log callback) and AR-75 (throwing synchronous cleanup callback).

The report

Applications can attach cleanup callbacks to a proxy object's context, and nothing requires them not to throw. When a server object is released without destroy(), the cleanup thread destroys the application object and then calls the callbacks in order, without catching exceptions. A test that registers a throwing callback followed by a second one, then disconnects the client, saw the process abort with SIGABRT and the second callback skipped. The report notes that Bitcoin Core registers waitSpawned (which waits for a spawned child process to exit) as a cleanup callback, and that waiting for a process can fail with a system error.

The report rates it low. The trigger is a cleanup callback that fails after a server object is released or its connection drops. The effect is a local process abort and a skipped cleanup.

Proposed change

Destroy the application object as before, then run each callback separately, catch and log a failure, and continue with the rest:

+void AsyncCleanup::operator()()
+{
+    impl.reset();
+    while (!fns.empty()) {
+        auto fn = std::move(fns.front());
+        fns.pop_front();
+        KJ_IF_MAYBE(exception, kj::runCatchingExceptions([&] { fn(); })) {
+            KJ_LOG(ERROR, "Asynchronous IPC cleanup callback failed.", *exception);
+        }
+    }
+}

The change builds on the cleanup-queue rework in AR-62, which introduced AsyncCleanup. The report considered documenting that callbacks must not throw, rejected because existing registrations and other applications would stay exposed, and notes that AR-35 covers log callbacks while this covers cleanup callbacks.

Assessment

The mechanism is accurate. On libmultiprocess master and in Bitcoin Core's copy, the cleanup thread calls the queued cleanup without a try, and the callback list runner stops at the first exception, so a throw ends the thread and C++ terminates the process.

Nothing in Bitcoin Core puts a callback on that list. libmultiprocess's own callbacks are registered on ProxyClient objects, not server objects, and Bitcoin Core registers none on servers. That includes waitSpawned: Bitcoin Core attaches it to the client object for a spawned process, so it runs from the client's destructor in the parent process, not on the cleanup thread. A failure there would also terminate the process, because that destructor cannot throw either, but this change does not cover it. That path is part of AR-66, which covers interrupted waits for spawned children. In Bitcoin Core today only a test-only RPC spawns processes.

The realistic code on the cleanup thread is the destruction of the application object itself, and the change leaves that outside the new error handling. That is fine, because C++ destructors terminate on a throw anyway.

So this matters only to applications that attach their own cleanup callbacks to server objects. The patch is small. Like AR-75 and AR-35, it raises the question of whether application callbacks may throw during teardown. Three local catch blocks are a weaker answer than one policy: either document that cleanup callbacks must not throw, or catch in the shared helper that runs every cleanup list, which would also cover the client-side path above. A log message through the application's log callback, with the exception text, would also be more useful than KJ's own logging.

Status

Present on master. Hardening; best decided together with AR-35 and AR-75. No public issue or PR yet.

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