Bug 66
Sending an empty byte string over IPC is undefined behavior
- Status
- Fixed
- Symptom
- Undefined behavior; worst effect seen: sanitizer report only
- Where seen
- A fuzz target, libmultiprocess tests (mptest)
- Trigger
- Unusual input from a peer or caller
- First reported
- 2026-07-01 (first report)
- Fixed
- 2026-07-13
- Fix
- mp#305
- Shipped to Bitcoin Core
- bitcoin#35720
- In released binaries
- Yes (v30.0-v31.1)
- Defect ever on master
- Yes
Summary
Serializing an empty byte vector into a Cap'n Proto Data field (the schema type for byte strings such as scripts) called memcpy with a null source pointer, which is undefined behaviour even though nothing is copied, and UBSan reports it. It was found by the new IPC fuzz target in Bitcoin Core's fuzzer,address,undefined,integer CI job. There is no known observable effect on any real platform. Released binaries v30.0 through v31.1 contain it (and v29 source builds with IPC enabled). Fixed in mp#305 and shipped to Bitcoin Core master by the subtree update bitcoin#35720.
What happened
- The new
ipcfuzz target in bitcoin#35118 (@enirox001) skipped empty vectors with a guard. @marcofleon asked why on 2026-07-01; removing the guard failed thefuzzer,address,undefined,integerCI job withinclude/mp/type-data.h:36:11: runtime error: null pointer passed as argument 2, which is declared to never be null, and enirox001 named the memcpy the same day. - @maflcko reproduced it in
mptestwith a newpassDatamethod and fixed it in libmultiprocess. @Sjors (output) and @ViniciusCestarii reproduced the UBSan report before the fix and its absence after.
Root cause
CustomBuildField for byte spans in include/mp/type-data.h did memcpy(result.begin(), data.data(), data.size()). For an empty vector, data() may be null and size() is 0. memcpy declares both pointers non-null regardless of the count, so the call is undefined behaviour and UBSan's nonnull-attribute check reports it. Any empty byte vector in a Data field reaches it, for example an empty script sent by a client.
It took a fuzzer to produce an empty vector on a Data field under UBSan; the unit tests and Bitcoin Core's functional tests never sent empty byte strings.
The code
The generic Data field builder in include/mp/type-data.h, as of fac7b9b7f688^, used for any byte-span type such as std::vector<unsigned char>:
template <typename LocalType, typename Value, typename Output>
void CustomBuildField(TypeList<LocalType>, Priority<2>, InvokeContext& invoke_context, Value&& value, Output&& output)
requires (std::is_same_v<decltype(output.get()), ::capnp::Data::Builder> && IsByteSpan<LocalType>)
{
auto data = std::span{value};
auto result = output.init(data.size());
memcpy(result.begin(), data.data(), data.size());
}
For an empty vector data.data() can be null. memcpy requires non-null pointers even when the count is 0, so the call is undefined behaviour, and UBSan reports a null pointer passed as an argument declared never null. Nothing is copied, so no real platform misbehaves; the cost was a failing fuzz job.
The fix uses std::ranges::copy, which is defined for empty ranges (fac7b9b7f688; the <ranges> include and the new passData test are omitted):
@@ -33,7 +34,7 @@ requires (std::is_same_v<decltype(output.get()), ::capnp::Data::Builder> && IsBy
auto data = std::span{value};
auto result = output.init(data.size());
- memcpy(result.begin(), data.data(), data.size());
+ std::ranges::copy(data, result.begin());
}
std::ranges::copy copies nothing for an empty span and never touches the pointers, so there is no call with a null argument left for UBSan to flag.
Fix
fac7b9b7f688 (maflcko) replaces the call with std::ranges::copy(data, result.begin()), which is defined for empty ranges (+2/-1), and adds the passData test method and a test that round-trips an empty vector. maflcko described it as "UB in theory (not sure if a compiler exists in practise to exploit this)". In his ACK ryanofsky asked whether optimizers could drop nearby null checks, and accepted the change as an improvement either way.
Timeline
- 2024-09-25: the same
memcpyreaches Bitcoin Core insrc/ipc/capnp/common-types.hwith bitcoin#30510, which added the IPC wrapper for the mining interface. - 2025-01-27: moved into libmultiprocess by 7d59b8d14c86 ("move: add mp/type-data.h").
- 2026-07-01: found by the fuzz target in bitcoin#35118.
- 2026-07-13: fix merged in mp#305.
- 2026-07-14: shipped to Bitcoin Core master by the subtree update bitcoin#35720.
- Releases: v30.0 through v31.1 contain the
memcpy; v29 has it in source builds with IPC enabled.
Lessons
What would have prevented this bug, found it sooner, or made it easier to debug. The patterns page groups these lessons across all bugs.
Pattern An in-process interface trusting what now comes from another process: Empty input
- Prevented by a different API shape, a review checklist item
- Earliest detector UBSan (did not exist then)
- Diagnosis easy
- Fix design easy
- Debugging aids sanitizer output, a clear assertion message
- Rust yes
Would Rust have prevented it? Yes. Slice::copy_from_slice is defined for empty slices and a Rust slice never carries a null data pointer, so there is no non-null contract to violate; the C++ memcpy was UB only on paper. More on Rust
Could it have been prevented?
Yes, by the fix's own choice of API. std::ranges::copy (or std::copy, or a span
assign) is defined for an empty range and never dereferences or passes the pointers,
while memcpy declares both arguments non-null whatever the count. A house rule to use
the standard algorithms rather than raw memcpy on containers, or a review question
"what does this do for an empty input?", would have caught it when the byte-span
builder was written for Bitcoin Core's common-types.h in 2017 and again when
7d59b8d14c86
moved it into the library. The memcpy non-null rule is obscure enough that
neither the author nor the reviewers of either version thought of it, and
maflcko's fix commit still calls it a workaround for a UBSan warning rather than
a bug, so "prevented" here means a lint-level habit, not a design insight.
Could it have been found sooner?
Yes, by any UBSan run that sends an empty byte vector through a Data field. Bitcoin
Core has had UBSan CI jobs for years, and the library's mptest round-trips many
types, but no test in either project sent an empty byte string; the fuzz target in
bitcoin#35118 was the first thing to
feed arbitrary values into the generated serialization code, and it hit the empty
vector at once. The only reason it was not reported earlier from that PR is that the
author had guarded the case away; marcofleon's
question
about the guard is what turned a workaround into a report. The fix adds passData
and an empty-vector round trip to mptest, which is the unit-level detector that was
missing.
What made it hard to debug?
Nothing. UBSan's nonnull-attribute message named the file, line and argument
(type-data.h:36:11: null pointer passed as argument 2), enirox001
identified the memcpy
the same day, and maflcko reproduced it in mptest and fixed it in three lines
(fac7b9b7f688).
The only discussion was whether it mattered: maflcko called it "UB in theory", and
ryanofsky's
ACK
asked whether an optimizer could drop nearby null checks on the strength of the
non-null attribute, which is the practical risk a formal-UB finding carries.
Lesson
Every serializer and copier has an empty-input case; write a test for it, and prefer standard algorithms whose empty-range behaviour is defined over C calls whose pointer preconditions hold even when the count is zero.
Classification
The survey's classification of this bug. The explorer charts these fields across all bugs; the method page explains them.
| Field | Value |
|---|---|
| Symptom | ub |
| Worst effect observed | sanitizer-report-only |
| Seen in | fuzz-target, mptest |
| Scope | production |
| Trigger | Unusual input (unusual-input) |
| Phase | steady-state |
| Side | both |
| Component | type-data |
| Root cause kind | api-misuse |
| Fix kind | one-line-tweak |
| Detection | fuzz |
| Needs a peer that is not libmultiprocess C++ | no |
| Needed a sanitizer to see | yes |
| Defect ever on master | yes |
| Released | binary |
| Confidence | medium |