high
CVE
CVE-2026-55407
CWE
CWE-400, CWE-770
Affected Surface
Rust crates `buffa` and `connectrpc` before 0.8.0, Rust services, proxies, queue consumers, and file-ingestion jobs that decode untrusted protobuf messages with generated code that leaves `preserve_unknown_fields=true`, Connect RPC, gRPC, or custom protobuf handlers that call `Message::decode`, `Message::decode_from_slice`, `MessageView::decode_view`, or equivalent generated decode paths on attacker-controlled input
CVE-2026-55407 matters because it hits a trust boundary many teams still model as “just protobuf parsing.” In reality, pre-0.8.0 versions of buffa and connectrpc could turn compact attacker-controlled protobuf input into disproportionately large heap growth while preserving unknown fields. That makes the bug relevant to internet-facing RPC handlers, queue consumers, binary file importers, and any Rust service that decodes protobuf from less-trusted senders.
The important nuance is that this is not a memory-corruption story. It is a resource-amplification story. Rust’s type safety stays intact while the decoder still allocates enough heap to crash a worker or push a service into a restart loop.
Affected packages and versions
Multiple public sources line up on the same version boundary:
| Package | Ecosystem | Affected versions | Fixed version |
|---|---|---|---|
buffa | crates.io | < 0.8.0 | 0.8.0 |
connectrpc | crates.io | < 0.8.0 | 0.8.0 |
The practical exposure condition is narrower than “any project that depends on these crates,” but still common in real services:
- the application decodes protobuf from an untrusted network peer, queue, or file
- generated code preserves unknown fields, which is the default path called out in the disclosure
- the process has enough memory headroom that a single request can force large allocations before ordinary payload-size checks intervene
If your Rust service accepts protobuf from customers, partner systems, webhooks, agents, or multi-tenant internal callers, this deserves a real review rather than a routine low-priority dependency bump.
The vulnerable decode path is short and direct
The pre-0.8.0 decode_unknown_field implementation contains two important branches. The first is the obvious one: a length-delimited field uses an attacker-supplied length directly in a heap allocation.
WireType::LengthDelimited => {
let len = decode_varint(buf)?;
let len = usize::try_from(len).map_err(|_| DecodeError::MessageTooLarge)?;
if buf.remaining() < len {
return Err(DecodeError::UnexpectedEof);
}
let mut data = alloc::vec![0u8; len];
buf.copy_to_slice(&mut data);
UnknownFieldData::LengthDelimited(data)
}
That path is already enough to tell you the decoder trusts wire-length metadata enough to allocate from it. But the more interesting branch is the group decoder:
WireType::StartGroup => {
let depth = depth
.checked_sub(1)
.ok_or(DecodeError::RecursionLimitExceeded)?;
let group_field_number = tag.field_number();
let mut nested = UnknownFields::new();
loop {
let nested_tag = Tag::decode(buf)?;
if nested_tag.wire_type() == WireType::EndGroup {
if nested_tag.field_number() != group_field_number {
return Err(DecodeError::InvalidEndGroup(nested_tag.field_number()));
}
break;
}
nested.push(decode_unknown_field(nested_tag, buf, depth)?);
}
UnknownFieldData::Group(nested)
}
The pre-fix code had a recursion limit, but not a meaningful budget on the number of unknown fields the decoder was willing to materialize. That distinction matters: depth-limiting stops a stack blow-up, but it does not stop a flat or shallow payload from forcing the decoder to push a very large number of UnknownField records into heap-backed vectors.
Why the default size guard is not enough
One tempting but incorrect defense is “there is already a message-size cap.” In the vulnerable line, the crate’s default decode options were:
pub struct DecodeOptions {
recursion_limit: u32,
max_message_size: usize,
}
const DEFAULT_MAX_MESSAGE_SIZE: usize = 0x7FFF_FFFF;
That 0x7FFF_FFFF default is roughly 2 GiB - 1. More importantly, it only constrains the encoded message size, not the amount of heap the decoder may create while preserving unknown fields. A payload can therefore stay below the transport or decoder byte limit and still expand much further in memory after parsing begins.
This is the AppSec lesson to keep: input size and allocation size after parsing are not the same security boundary.
What changed in 0.8.0
The 0.8.0 release added an explicit unknown-field budget instead of relying on recursion depth and input-size caps alone.
At the API layer, the crate now exposes a per-decode allowance:
pub const DEFAULT_UNKNOWN_FIELD_LIMIT: usize = 1_000_000;
pub struct DecodeContext<'a> {
depth: u32,
unknown_fields_remaining: &'a core::cell::Cell<usize>,
}
pub fn register_unknown_field(&self) -> Result<(), DecodeError> {
let remaining = self.unknown_fields_remaining.get();
if remaining == 0 {
return Err(DecodeError::UnknownFieldLimitExceeded);
}
self.unknown_fields_remaining.set(remaining - 1);
Ok(())
}
And the decode path now consumes one allowance slot before materializing each preserved unknown field:
// Every decoded field occupies one `UnknownField` slot in its parent's
// vector — consume an allowance slot up front so runs of tiny fields
// cannot amplify into unbounded heap growth.
ctx.register_unknown_field()?;
That is the architectural difference that matters. Before the fix, the decoder only tracked depth. After the fix, it tracks the number of unknown-field objects it is willing to materialize during a single top-level decode.
Where this shows up in real applications
The vulnerable pattern is broader than a single RPC framework. Look for:
- Connect RPC or gRPC handlers that decode protobuf bodies from untrusted clients
- internal queues that carry protobuf events from lower-trust producers
- ingestion jobs that read
.binpb, message dumps, or replay artifacts from object storage - services that proxy protobuf between versions and preserve unknown fields for forward compatibility
- multi-tenant agent or automation backends where one tenant can submit structured binary messages that another long-lived worker decodes
The highest-risk design is a long-lived process that decodes arbitrary protobuf from external callers and shares memory with other tenants, secrets, or high-value workloads. Even if the bug “only” causes denial of service, repeated request-driven worker death can translate into a practical availability incident very quickly.
Scoping and detection
Start with dependency presence:
cargo tree -i buffa
cargo tree -i connectrpc
rg -n 'buffa|connectrpc' Cargo.lock Cargo.toml
Then find code paths that actually decode untrusted input:
rg -n 'decode_from_slice|decode_view|Message::decode|DecodeOptions::new' src crates apps
rg -n 'preserve_unknown_fields' .
For application review, ask three questions:
- Can an external or lower-trust party supply protobuf to this service?
- Does that data reach
buffa/connectrpcdecode paths before authentication, strict rate limiting, or worker isolation? - Would repeated worker OOMs create a user-visible outage, crash loop, or queue backlog?
If the answer to all three is yes, treat this as a meaningful exposure even if your deployment is “only” internet-adjacent or internally reachable.
Remediation
The clean fix is straightforward:
- Upgrade
buffato0.8.0or later. - Upgrade
connectrpcto0.8.0or later. - Rebuild and redeploy all protobuf-decoding services that vendor or statically link the older runtime.
If you cannot upgrade immediately, use the mitigation called out in the disclosure and regenerate code with:
preserve_unknown_fields = false
That mitigation reduces forward-compatibility behavior, so it needs application-level review, but it is much safer than leaving the old materialization path exposed on hostile inputs.
Response guidance
Because this is a denial-of-service issue rather than a code-execution bug, response should focus on availability and exposure windows:
- Patch the runtime and redeploy the affected services.
- Review crash-loop, OOM-kill, and restart telemetry for protobuf-facing workers.
- Lower overly generous message-size limits where possible.
- Add per-request rate limits or circuit breakers on protobuf decode paths that face low-trust senders.
- Separate externally reachable decoding workers from high-value long-lived processes when practical.
The key takeaway is not just “upgrade these crates.” It is that protobuf unknown-field preservation is part of your memory-attack surface. In buffa and connectrpc, the last few days’ disclosure shows that preserving forward compatibility without a separate allocation budget can turn tiny wire fields into large heap pressure. For AppSec teams reviewing Rust services, that is a real parser-boundary lesson, not just a dependency-management footnote.