Audit report AR-03
Reported privately in an independent security review, 2026-08-07.
- Report date
- 2026-08-07
- Kind
- API
- Status on master
- Present
- Proposed issue state
- Open
- Linked bugs
- Bug 23: Scripts passed to the node over IPC are decoded in the wrong format
Summary
If an interface method returned a std::span of bytes, or had one as an output parameter, the generated client would hand back a view into the Cap'n Proto response buffer, which is freed before the call returns. The mechanism is real, but no interface in Bitcoin Core or libmultiprocess returns or outputs a span, so nothing is affected today. It is an API trap that a compile-time check would close.
The report
The generic Data codec in include/mp/type-data.h accepts any local type that satisfies:
std::convertible_to<T, std::span<const U>>
std::constructible_from<T, const U*, const U*>
for a byte type U. That includes owning containers such as std::vector<unsigned char>, but also std::span<const unsigned char>. Its CustomReadField builds the local value directly from pointers into the Data::Reader:
const kj::byte* begin = input.get().begin();
const kj::byte* end = input.get().end();
return read_dest.construct(
reinterpret_cast<const ByteType*>(begin),
reinterpret_cast<const ByteType*>(end));
For a return value, clientInvoke reads the result while the response object is alive and stores it in a local variable. The response and its message memory are destroyed when the response callback returns, before clientInvoke returns to its caller. A span result therefore points into freed memory on arrival. Reading it could crash or expose stale allocator contents. The report states that it found no current Bitcoin Core interface returning a span, so it presents this as a generic library problem rather than a confirmed Bitcoin Core call path.
The reproduction has two standalone parts, neither linked against the library. A compile-time check copies the IsSpanOf/IsByteSpan concepts and asserts that std::span<const unsigned char> satisfies them. A model then builds a span over a malloc buffer, frees the buffer (standing in for the response), and reads the first byte; AddressSanitizer reports heap-use-after-free.
Proposed change
Reject non-owning view types as results and output parameters at compile time, deserialize Data results into owning containers, and allow spans only as inputs, where the bytes are copied into the request before the call returns. The patch sketch adds an assertion to the read overload:
decltype(auto) CustomReadField(...)
requires (... && IsByteSpan<LocalType>)
{
+ static_assert(!IsNonOwningSpan<LocalType>,
+ "std::span may be used only as an input; RPC results must own bytes");
The IsNonOwningSpan trait does not exist yet and would need to be written. The report also suggests type-trait tests proving that a span cannot be a result or output type.
Assessment
The quoted code is unchanged on libmultiprocess master, and the lifetime reasoning is right for a hypothetical method: in clientInvoke (include/mp/proxy-types.h) results are read inside the .then() callback that receives the capnp::Response, and the response is destroyed when that callback returns.
Nothing uses it. No interface on Bitcoin Core master, on the unmerged multiprocess branch, or in libmultiprocess's tests and examples returns or outputs a span; the only span uses are local temporaries inside field helpers. Span inputs are safe, because the request parameters stay alive for the whole server call. The concept was written for owning byte containers and blob types such as uint256 (the comment above CustomBuildField says so), and the read overload was added by mp#135 to fix a real crash with CScript arguments.
The reproduction shows that the concept accepts a span and that reading freed memory is detected. It does not compile a generated method that returns a span. Declaring such a method would be a programming error that review would likely catch, and the fix is a static_assert that turns it into a compile error. No peer behaviour is involved.
Related bug page for the same code: bitcoin-node crashes deserializing a CScript passed over IPC because the span CustomBuildField had no matching CustomReadField.
Reproduction. Not yet attempted against Bitcoin Core master.
Status
Present on master; no affected interface. Next step: a small libmultiprocess change adding a static_assert that rejects non-owning types in the Data read overload. Idea; no issue open.
Reported privately in an independent security review on 2026-08-07.