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

find_package(Threads REQUIRED)
find_package(CapnProto 0.9 NO_MODULE)
find_package(CapnProto 1.0 NO_MODULE)
if(NOT CapnProto_FOUND)
message(FATAL_ERROR
"Cap'n Proto is required but was not found.\n"
Expand All @@ -23,62 +23,6 @@ find_package(CapnProto 0.9 NO_MODULE)
)
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 Expand Up @@ -157,6 +101,7 @@ set(MP_PUBLIC_HEADERS
include/mp/proxy.h
include/mp/type-char.h
include/mp/type-chrono.h
include/mp/type-cancel.h
include/mp/type-context.h
include/mp/type-data.h
include/mp/type-decay.h
Expand Down
2 changes: 1 addition & 1 deletion ci/configs/olddeps.bash
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ CI_DIR=build-olddeps
# requires an older GCC.
NIXPKGS_CHANNEL=nixos-25.05
export CXXFLAGS="-Werror -Wall -Wextra -Wpedantic -Wno-unused-parameter -Wno-error=array-bounds"
NIX_ARGS=(--argstr capnprotoVersion "0.9.2" --argstr cmakeVersion "3.12.4" --argstr gccVersion "11")
NIX_ARGS=(--argstr capnprotoVersion "1.0.0" --argstr cmakeVersion "3.12.4" --argstr gccVersion "11")
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
73 changes: 71 additions & 2 deletions include/mp/proxy-io.h
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ struct InvokeContext
struct ClientInvokeContext : InvokeContext
{
ThreadContext& thread_context;
std::function<void(std::function<void()>)> cancel_receiver;
ClientInvokeContext(Connection& conn, ThreadContext& thread_context)
: InvokeContext{conn}, thread_context{thread_context}
{
Expand All @@ -58,14 +59,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};
//! 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 Expand Up @@ -383,6 +392,66 @@ class EventLoop
std::function<void(std::any)> testing_hook_misc;
};

//! Cancellation state of one IPC call, created by clientInvoke.
class ClientCancelState
{
public:
explicit ClientCancelState(EventLoop& loop) : m_loop(loop) {}

//! Cancel the in-flight request, waking the blocked client thread.
//! Callable from any thread.
inline void cancel();

//! Whether cancel was called.
bool canceled()
{
const Lock lock{m_mutex};
return m_canceled;
}

//! Keeps the event loop alive while the caller holds the cancel
//! function.
EventLoopRef m_loop;
Mutex m_mutex;
bool m_canceled MP_GUARDED_BY(m_mutex){false};
//! Canceler of the request promise, owned by the RequestCanceler attached
//! to it. Null before the request is sent and after it completes.
kj::Canceler* m_canceler MP_GUARDED_BY(m_mutex){nullptr};
};

//! kj::Canceler wrapping a request promise. Attached to the promise so it
//! is created and destroyed on the event loop thread, keeping
//! ClientCancelState::m_canceler valid while the request is in flight.
struct RequestCanceler : kj::Canceler
{
explicit RequestCanceler(std::shared_ptr<ClientCancelState> state) : m_state(std::move(state))
{
const Lock lock{m_state->m_mutex};
m_state->m_canceler = this;
}
~RequestCanceler()
{
const Lock lock{m_state->m_mutex};
m_state->m_canceler = nullptr;
}
std::shared_ptr<ClientCancelState> m_state;
};

void ClientCancelState::cancel()
{
{
const Lock lock{m_mutex};
if (m_canceled) return;
m_canceled = true;
// Null canceler means the call already completed, so there is nothing to cancel.
if (!m_canceler) return;
}
m_loop->sync([&] {
const Lock lock{m_mutex};
if (m_canceler) m_canceler->cancel("canceled by client");
});
}

//! Single element task queue used to handle recursive capnp calls. (If the
//! server makes a callback into the client in the middle of a request, while the client
//! thread is blocked waiting for server response, this is what allows the
Expand Down
109 changes: 92 additions & 17 deletions include/mp/proxy-types.h
Original file line number Diff line number Diff line change
Expand Up @@ -534,28 +534,93 @@ 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
//! MaybeBuildExtraParam 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)
{
auto const fun = [&]<typename... Values>(Values&&... values) {
(MaybeBuildExtraParam(TypeList<RemoveCvRef<Values>>(), invoke_context, std::forward<Values>(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;
};

template <typename LocalType, typename Value>
void MaybeBuildExtraParam(TypeList<LocalType> param, ClientInvokeContext& invoke_context, Value&& value)
{
if constexpr (requires { CustomBuildExtraParam(param, invoke_context, std::forward<Value>(value)); }) {
CustomBuildExtraParam(param, invoke_context, std::forward<Value>(value));
}
}

template <typename LocalType, typename ServerContext>
LocalType MaybeReadExtraParam(TypeList<LocalType> param, ServerContext& server_context)
{
static_assert(requires { CustomReadExtraParam(param, 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.");
return CustomReadExtraParam(param, server_context);
}

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.
std::tuple<RemoveCvRef<Extra>...> extra{MaybeReadExtraParam(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;
// 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 @@ -587,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 @@ -603,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 @@ -732,7 +797,9 @@ void clientInvoke(ProxyClient& proxy_client, const GetRequest& get_request, Fiel
std::exception_ptr exception;
std::string kj_exception;
bool done = false;
bool canceled = false;
const char* disconnected = nullptr;
const auto cancel_state = std::make_shared<ClientCancelState>(*proxy_client.m_context.loop);
proxy_client.m_context.loop->sync([&]() {
if (!proxy_client.m_context.connection) {
const Lock lock(thread_context.waiter->m_mutex);
Expand All @@ -753,7 +820,8 @@ 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(
auto request_canceler = kj::heap<RequestCanceler>(cancel_state);
proxy_client.m_context.loop->m_task_set->add(request_canceler->wrap(request.send()).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 @@ -771,7 +839,9 @@ 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 (cancel_state->canceled()) {
canceled = true;
} 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 @@ -781,12 +851,17 @@ 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::mv(request_canceler)));
});

if (invoke_context && invoke_context->cancel_receiver) {
invoke_context->cancel_receiver([cancel_state] { cancel_state->cancel(); });
}

Lock lock(thread_context.waiter->m_mutex);
thread_context.waiter->wait(lock, [&done]() { return done; });
if (exception) std::rethrow_exception(exception);
if (canceled) throw InterruptException{"canceled"};
if (!kj_exception.empty()) MP_LOGPLAIN(*proxy_client.m_context.loop, Log::Raise) << kj_exception;
if (disconnected) MP_LOGPLAIN(*proxy_client.m_context.loop, Log::Raise) << disconnected;
}
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