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
60 changes: 2 additions & 58 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,72 +13,16 @@ endif()
include("cmake/compat_find.cmake")

find_package(Threads REQUIRED)
find_package(CapnProto 0.9 NO_MODULE)
find_package(CapnProto 1.0 NO_MODULE)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In 527a099: build: require Cap'n Proto 1.0

nit: There is a text here that needs to be updated i think

index f2199cb..a6a8a0f 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -18,7 +18,7 @@ find_package(CapnProto 1.0 NO_MODULE)
    message(FATAL_ERROR
      "Cap'n Proto is required but was not found.\n"
      "To resolve, choose one of the following:\n"
-     "  - Install Cap'n Proto (version 1.0+ recommended)\n"
+     "  - Install Cap'n Proto (version 1.0+ required)\n"
      "  - For Bitcoin Core compilation build with -DENABLE_IPC=OFF to disable multiprocess support\n"
    )
  endif()

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes more sense, thanks. Done in 45514af.

if(NOT CapnProto_FOUND)
message(FATAL_ERROR
"Cap'n Proto is required but was not found.\n"
"To resolve, choose one of the following:\n"
" - Install Cap'n Proto (version 1.0+ recommended)\n"
" - Install Cap'n Proto (version 1.0+ required)\n"
" - For Bitcoin Core compilation build with -DENABLE_IPC=OFF to disable multiprocess support\n"
)
endif()

# Cap'n Proto compatibility checks
set(CAPNPROTO_ISSUES "")
set(CAPNPROTO_CVE_AFFECTED FALSE)
set(CAPNPROTO_CLANG_INCOMPATIBLE FALSE)

# Check for list-of-pointers memory access bug from Nov 2022
# https://nvd.nist.gov/vuln/detail/CVE-2022-46149
# https://github.com/advisories/GHSA-qqff-4vw4-f6hx
# https://github.com/capnproto/capnproto/security/advisories/GHSA-qqff-4vw4-f6hx
# https://github.com/capnproto/capnproto/blob/master/security-advisories/2022-11-30-0-pointer-list-bounds.md
# https://capnproto.org/news/2022-11-30-CVE-2022-46149-security-advisory.html
# https://dwrensha.github.io/capnproto-rust/2022/11/30/out_of_bounds_memory_access_bug.html
if(CapnProto_VERSION STREQUAL "0.9.0"
OR CapnProto_VERSION STREQUAL "0.9.1"
OR CapnProto_VERSION STREQUAL "0.10.0"
OR CapnProto_VERSION STREQUAL "0.10.1"
OR CapnProto_VERSION STREQUAL "0.10.2")
set(CAPNPROTO_CVE_AFFECTED TRUE)
string(APPEND CAPNPROTO_ISSUES "- CVE-2022-46149 security vulnerability (details: https://github.com/advisories/GHSA-qqff-4vw4-f6hx)\n")
endif()

# Check for Cap'n Proto / Clang / C++20 incompatibility
# Cap'n Proto 0.9.x and 0.10.x are incompatible with Clang 16+ when using C++20
# due to P2468R2 implementation. This was fixed in Cap'n Proto 1.0+.
# See: https://github.com/bitcoin-core/libmultiprocess/issues/199
if((CapnProto_VERSION VERSION_GREATER_EQUAL "0.9.0") AND
(CapnProto_VERSION VERSION_LESS "1.0.0") AND
(CMAKE_CXX_COMPILER_ID STREQUAL "Clang") AND
(CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL "16") AND
(CMAKE_CXX_STANDARD EQUAL 20))
set(CAPNPROTO_CLANG_INCOMPATIBLE TRUE)
string(APPEND CAPNPROTO_ISSUES "- Incompatible with Clang ${CMAKE_CXX_COMPILER_VERSION} when using C++20\n")
endif()

if(CAPNPROTO_CVE_AFFECTED OR CAPNPROTO_CLANG_INCOMPATIBLE)
set(RESOLUTION_OPTIONS "")

# Fixes both issues
string(APPEND RESOLUTION_OPTIONS " - Upgrade to Cap'n Proto version 1.0 or newer (recommended)\n")

if(CAPNPROTO_CVE_AFFECTED AND NOT CAPNPROTO_CLANG_INCOMPATIBLE)
string(APPEND RESOLUTION_OPTIONS " - Upgrade to a patched minor version (0.9.2, 0.10.3, or later)\n")
elseif(CAPNPROTO_CLANG_INCOMPATIBLE AND NOT CAPNPROTO_CVE_AFFECTED)
string(APPEND RESOLUTION_OPTIONS " - Use GCC instead of Clang\n")
endif()

string(APPEND RESOLUTION_OPTIONS " - For Bitcoin Core compilation build with -DENABLE_IPC=OFF to disable multiprocess support\n")

message(FATAL_ERROR
"The version of Cap'n Proto detected: ${CapnProto_VERSION} has known compatibility issues:\n"
"${CAPNPROTO_ISSUES}"
"To resolve, choose one of the following:\n"
"${RESOLUTION_OPTIONS}"
)
endif()

set(MPGEN_EXECUTABLE "" CACHE FILEPATH "If specified, should be full path to an external mpgen binary to use rather than the one built internally.")

option(MP_ENABLE_CLANG_TIDY "Run clang-tidy with the compiler." OFF)
Expand Down
2 changes: 1 addition & 1 deletion ci/configs/olddeps.bash
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@ CI_DIR=build-olddeps
# Pin olddeps to an older Nixpkgs channel, to be able to test an older GCC.
CI_NIXPKGS_CHANNEL=nixos-25.05
export CXXFLAGS="-Werror -Wall -Wextra -Wpedantic -Wunused-const-variable -Wno-unused-parameter -Wno-error=array-bounds"
CAPNP_CHECKOUT=v0.9.2 # Use a checkout to compile it with the selected GCC
CAPNP_CHECKOUT=v1.0.0 # Use a checkout to compile it with the selected GCC
NIX_ARGS=(--argstr capnprotoVersion "none" --argstr cmakeVersion "3.12.4" --argstr gccVersion "10")
BUILD_ARGS=(-k)
2 changes: 1 addition & 1 deletion doc/install.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# libmultiprocess Installation

Installation currently requires Cap'n Proto 0.9 or higher:
Installation currently requires Cap'n Proto 1.0 or higher:

```sh
apt install libcapnp-dev capnproto
Expand Down
25 changes: 23 additions & 2 deletions include/mp/proxy-io.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
#include <condition_variable>
#include <cstdlib>
#include <functional>
#include <kj/async.h>
#include <kj/exception.h>
#include <kj/function.h>
#include <kj/io.h>
#include <map>
Expand All @@ -39,6 +41,17 @@ struct InvokeContext
struct ClientInvokeContext : InvokeContext
{
ThreadContext& thread_context;
//! Callback that overloads can set while building a parameter to make a
//! function call cancelable. If set, clientInvoke calls it on the event loop
//! thread with the kj::Canceler object wrapping the request when the request is
//! sent, and with null when the request finishes and the canceler is destroyed.
std::function<void(kj::Canceler*)> set_canceler;
//! Callback that lets clients set a custom error handler for kj::Exception.
//! For example, a canceled request fails the same way a disconnect does,
//! and this is where the cancellation parameter can throw its own
//! exception instead. If set, clientInvoke calls it and rethrows anything
//! it throws.
std::function<void(const kj::Exception&)> handle_error;
ClientInvokeContext(Connection& conn, ThreadContext& thread_context)
: InvokeContext{conn}, thread_context{thread_context}
{
Expand All @@ -58,14 +71,22 @@ struct ServerInvokeContext : InvokeContext
//! results structs if the request is canceled while the worker thread is
//! reading params (`call_context.getParams()`) or writing results
//! (`call_context.getResults()`).
Lock* cancel_lock{nullptr};
Lock* request_lock{nullptr};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In commit 7f31996: proxy: rename cancel_lock and cancel_mutex to request_lock and request_mutex

This is a good change to use the naming request_lock, the previous name would have been confusing, as it could suggest that it controls whether cancellation happens. The new change better describes what it protects, which is the request data.

nit: there is a comment that I think you missed;

index 63b3fdb..c1ca04f 100644
--- a/include/mp/type-context.h
+++ b/include/mp/type-context.h
@@ -154,7 +154,7 @@ auto PassField(Priority<1>, TypeList<>, ServerContext& server_context, const Fn&
         // makes another IPC call), so avoid modifying the map.
         const bool erase_thread{inserted};
         KJ_DEFER(
-            // Release the cancel lock before calling loop->sync and
+            // Release the request lock before calling loop->sync and
             // waiting for the event loop thread, because if a
             // cancellation happened, it needs to run the on_cancel
             // callback above. It's safe to release request_lock at

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Done in 2e5e66d

//! For IPC methods that execute asynchronously, not on the event-loop
//! thread: mutex request_lock refers to, set together with it. Null for
//! methods executing on the event-loop thread.
Mutex* request_mutex{nullptr};
//! For IPC methods that execute asynchronously, not on the event-loop
//! thread, this is set to true if the IPC call was canceled by the client
//! or canceled by a disconnection. If the call runs on the event-loop
//! thread, it can't be canceled. This should be accessed with cancel_lock
//! thread, it can't be canceled. This should be accessed with request_lock
//! held if it is not null, since in the asynchronous case it is accessed
//! from multiple threads.
bool request_canceled{false};
//! For IPC methods that execute asynchronously, not on the event-loop
//! thread: callback registered by a wrapped method. Runs when request
//! cancellation is detected.
std::function<void()> cancel_fn;

ServerInvokeContext(ProxyServer& proxy_server, CallContext& call_context, int req)
: InvokeContext{*proxy_server.m_context.connection}, proxy_server{proxy_server}, call_context{call_context}, req{req}
Expand Down
118 changes: 101 additions & 17 deletions include/mp/proxy-types.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
#include <mp/proxy-io.h>

#include <exception>
#include <kj/async.h>
#include <kj/common.h>
#include <kj/memory.h>
#include <optional>
#include <set>
#include <typeindex>
Expand Down Expand Up @@ -540,28 +543,84 @@ ClientParam<Accessor, Types...> MakeClientParam(Types&&... values)
return {std::forward<Types>(values)...};
}

//! Client parameter with no capnp field, generated for method parameters
//! declared with `$Proxy.extraParam`. Its value is handed to a
//! CustomBuildExtraParam overload instead of being built into the request.
template <typename... Types>
struct ClientParam<void, Types...>
{
ClientParam(Types&&... values) : m_values{std::forward<Types>(values)...} {}

struct BuildParams : IterateFieldsHelper<BuildParams, sizeof...(Types)>
{
template <typename Params, typename ParamList>
void handleField(ClientInvokeContext& invoke_context, Params&, ParamList)
{
static_assert((requires(ClientInvokeContext& context, Types&& value) {
CustomBuildExtraParam(TypeList<RemoveCvRef<Types>>(), context, std::forward<Types>(value));
} && ...),
"Wrapped C++ method has more parameters than its corresponding Cap'n Proto method has fields. "
"Declare extra parameters with $Proxy.extraParam in the Cap'n Proto schema and add a matching "
"`CustomBuildExtraParam` overload.");
auto const fun = [&](Types&&... values) {
(CustomBuildExtraParam(TypeList<RemoveCvRef<Types>>(), invoke_context, std::forward<Types>(values)), ...);
};
std::apply(fun, std::move(m_client_param->m_values));
}
BuildParams(ClientParam* client_param) : m_client_param(client_param) {}
ClientParam* m_client_param;
};

struct ReadResults : IterateFieldsHelper<ReadResults, sizeof...(Types)>
{
template <typename Results, typename ParamList>
void handleField(ClientInvokeContext&, Results&, ParamList)
{
}
ReadResults(ClientParam*) {}
};

std::tuple<Types&&...> m_values;
};

struct ServerCall
{
// FIXME: maybe call call_context.releaseParams()
template <typename ServerContext, typename... Args>
decltype(auto) invoke(ServerContext& server_context, TypeList<>, Args&&... args) const
template <typename ServerContext, typename... Extra, typename... Args>
decltype(auto) invoke(ServerContext& server_context, TypeList<Extra...>, Args&&... args) const
{
// If cancel_lock is set, release it while executing the method, and
// Construct the extra parameters before request_lock is released below.
// CustomReadExtraParam overloads build values from the request being
// executed, so they need the same protection as normal capnp fields
// from the event loop deleting request state on cancellation.
static_assert((requires { CustomReadExtraParam(TypeList<RemoveCvRef<Extra>>(), server_context); } && ...),
"Wrapped C++ method has more parameters than its corresponding Cap'n Proto method has fields. "
"Declare extra parameters with $Proxy.extraParam in the Cap'n Proto schema and add a matching "
"`CustomReadExtraParam` overload.");
std::tuple<RemoveCvRef<Extra>...> extra{CustomReadExtraParam(TypeList<RemoveCvRef<Extra>>(), server_context)...};
// If request_lock is set, release it while executing the method, and
// reacquire it afterwards. The lock is needed to prevent params and
// response structs from being deleted by the event loop thread if the
// request is canceled, so it is only needed before and after method
// execution. It is important to release the lock during execution
// because the method can take arbitrarily long to return and the event
// loop will need the lock itself in on_cancel if the call is canceled.
if (server_context.cancel_lock) server_context.cancel_lock->m_lock.unlock();
if (server_context.request_lock) server_context.request_lock->m_lock.unlock();
return TryFinally(
[&]() -> decltype(auto) {
return ProxyServerMethodTraits<
typename decltype(server_context.call_context.getParams())::Reads
>::invoke(server_context, std::forward<Args>(args)...);
return std::apply(
[&](RemoveCvRef<Extra>&... extra_args) -> decltype(auto) {
return ProxyServerMethodTraits<
typename decltype(server_context.call_context.getParams())::Reads
>::invoke(server_context, std::forward<Args>(args)..., std::move(extra_args)...);
},
extra);
},
[&] {
if (server_context.cancel_lock) server_context.cancel_lock->m_lock.lock();
if (server_context.request_lock) server_context.request_lock->m_lock.lock();
// The method returned, so destroy the callback it registered
// through its cancellation argument, if any.
server_context.cancel_fn = nullptr;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In cfdcb45: proxy: support cancellation extra parameters

Could cancellation run the server callback after the local variables it references have been destroyed?

The callback is cleared only after the wrapped method returns, but the method’s local variables are destroyed before that. If cancellation arrives in between, the callback could access destroyed state—for example, a local mutex or condition variable.

I reproduced this with a test that pauses during method cleanup and triggers cancellation after the local state is destroyed. The callback still runs.

index 619930e..5ad02a0 100644
--- a/test/mp/test/test.cpp
+++ b/test/mp/test/test.cpp
@@ -816,6 +816,85 @@ KJ_TEST("Cancel an in-flight IPC call")
     KJ_EXPECT(foo->add(1, 2) == 3);
 }

+KJ_TEST("Cancellation callback does not outlive server method locals")
+{
+    TestSetup setup;
+    auto* foo = setup.client.get();
+    foo->initThreadMap();
+    constexpr std::chrono::seconds timeout{30};
+    std::promise<void> locals_destroyed;
+    auto locals_destroyed_future = locals_destroyed.get_future();
+    std::promise<void> cancellation_attempted;
+    auto cancellation_attempted_future = cancellation_attempted.get_future();
+    std::atomic<bool> callback_after_destruction{false};
+
+    setup.server->m_impl->m_cancel_fn = [&](CancelArg cancel) {
+        // This guard runs after local_state is destroyed, but before the
+        // method returns to ServerCall's callback cleanup. Hold that window
+        // open so cancellation reliably arrives during normal scope exit.
+        KJ_DEFER(
+            locals_destroyed.set_value();
+            KJ_EXPECT(cancellation_attempted_future.wait_for(timeout) == std::future_status::ready);
+        );
+        );
+            KJ_EXPECT(cancellation_attempted_future.wait_for(timeout) == std::future_status::ready);
+        );
+        auto local_state = std::make_shared<int>(42);
+        cancel([state = std::weak_ptr<int>{local_state}, &callback_after_destruction] {
+            // Observe the lifetime safely, instead of dereferencing a dangling
+            // reference to a local mutex/condition variable as real code might.
+            callback_after_destruction = state.expired();
+        });
+        // Return normally; do not wait for cancellation in the method body.
+    };
+
+    std::promise<CancelFn> cancel_fn;
+    auto cancel_future = cancel_fn.get_future();
+    std::thread canceler([&] {
+        CancelFn fire = cancel_future.get();
+        KJ_EXPECT(locals_destroyed_future.wait_for(timeout) == std::future_status::ready);
+        fire();
+        cancellation_attempted.set_value();
+    });
+    bool interrupted{false};
+    try {
+        foo->callCancelFnAsync([&](CancelFn fn) { cancel_fn.set_value(std::move(fn)); });
+    } catch (const InterruptException&) {
+        interrupted = true;
+    }
+    canceler.join();
+    KJ_EXPECT(interrupted);
+    KJ_EXPECT(!callback_after_destruction.load());
+    KJ_EXPECT(foo->add(1, 2) == 3);
+}
+

Should we provide a way to unregister the callback before the captured local variables go out of scope?

@xyzconstant xyzconstant Sep 9, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ViniciusCestarii reported the same issue and suggested an RAII guard that unregisters the cancel callback before the wrapped method's locals go out of scope.

Following @ryanofsky's suggestion in #342 (comment), it's a bit of a redesign. In the latest pushes, type-cancel.h was moved to test/, and now the source code changes are smaller than in your last review.

// If the IPC request was canceled, throw InterruptException
// because there is no point continuing and trying to fill the
// call_context.getResults() struct. It's also important to stop
Expand Down Expand Up @@ -593,10 +652,10 @@ struct ServerRet : Parent
{
ServerRet(Parent parent) : Parent(parent) {}

template <typename ServerContext, typename... Args>
void invoke(ServerContext& server_context, TypeList<>, Args&&... args) const
template <typename ServerContext, typename ArgTypes, typename... Args>
void invoke(ServerContext& server_context, ArgTypes arg_types, Args&&... args) const
{
auto&& result = Parent::invoke(server_context, TypeList<>(), std::forward<Args>(args)...);
auto&& result = Parent::invoke(server_context, arg_types, std::forward<Args>(args)...);
auto&& results = server_context.call_context.getResults();
InvokeContext& invoke_context = server_context;
BuildField(TypeList<decltype(result)>(), invoke_context, Make<StructField, Accessor>(results),
Expand All @@ -609,11 +668,11 @@ struct ServerExcept : Parent
{
ServerExcept(Parent parent) : Parent(parent) {}

template <typename ServerContext, typename... Args>
void invoke(ServerContext& server_context, TypeList<>, Args&&... args) const
template <typename ServerContext, typename ArgTypes, typename... Args>
void invoke(ServerContext& server_context, ArgTypes arg_types, Args&&... args) const
{
try {
return Parent::invoke(server_context, TypeList<>(), std::forward<Args>(args)...);
return Parent::invoke(server_context, arg_types, std::forward<Args>(args)...);
} catch (const Exception& exception) {
auto&& results = server_context.call_context.getResults();
BuildField(TypeList<Exception>(), server_context, Make<StructField, Accessor>(results), exception);
Expand Down Expand Up @@ -759,7 +818,18 @@ void clientInvoke(ProxyClient& proxy_client, const GetRequest& get_request, Fiel
MP_LOGPLAIN(*proxy_client.m_context.loop, Log::Trace)
<< "send data: " << LogEscape(request.toString(), proxy_client.m_context.loop->m_log_opts.max_chars);

proxy_client.m_context.loop->m_task_set->add(request.send().then(
kj::Promise<::capnp::Response<typename Request::Results>> promise{request.send()};

// If `set_canceler` was set, construct a kj::Canceler object and wrap
// the request promise with it.
kj::Own<kj::Canceler> canceler;
if (invoke_context->set_canceler) {
canceler = kj::heap<kj::Canceler>();
promise = canceler->wrap(kj::mv(promise));
invoke_context->set_canceler(canceler.get());
}

proxy_client.m_context.loop->m_task_set->add(promise.then(
[&](::capnp::Response<typename Request::Results>&& response) {
MP_LOGPLAIN(*proxy_client.m_context.loop, Log::Debug)
<< "{" << thread_context.thread_name << "} IPC client recv "
Expand All @@ -777,7 +847,16 @@ void clientInvoke(ProxyClient& proxy_client, const GetRequest& get_request, Fiel
thread_context.waiter->m_cv.notify_all();
},
[&](const ::kj::Exception& e) {
if (e.getType() == ::kj::Exception::Type::DISCONNECTED) {
if (invoke_context->handle_error) {
try {
invoke_context->handle_error(e);
} catch (...) {
exception = std::current_exception();
}
}
if (exception) {
// Rethrown below.
} else if (e.getType() == ::kj::Exception::Type::DISCONNECTED) {
disconnected = "IPC client method call interrupted by disconnect.";
} else {
kj_exception = kj::str("kj::Exception: ", e).cStr();
Expand All @@ -787,7 +866,12 @@ void clientInvoke(ProxyClient& proxy_client, const GetRequest& get_request, Fiel
const Lock lock(thread_context.waiter->m_mutex);
done = true;
thread_context.waiter->m_cv.notify_all();
}));
}).attach(kj::defer([canceler = kj::mv(canceler), set_canceler = invoke_context->set_canceler] {
// Runs on the event loop thread when the request promise is
// destroyed. Tell the cancellation parameter the canceler is
// about to go away.
if (set_canceler) set_canceler(nullptr);
})));
});

Lock lock(thread_context.waiter->m_mutex);
Expand Down
6 changes: 6 additions & 0 deletions include/mp/proxy.capnp
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ annotation name(field, method): Text;
annotation skip(field): Void;
# Synonym for count(0).

annotation extraParam(method): Text;
# Adds an extra C++-only parameter to the generated method signature.
#
# This parameter has no corresponding capnp parameter and is not serialized or
# sent over RPC. The annotation value names the parameter in generated C++ code.

interface ThreadMap $count(0) {
# Interface letting clients control which thread a method call should
# execute on. Clients create and name threads and pass the thread handle as
Expand Down
Loading
Loading