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:

PackageEcosystemAffected versionsFixed version
buffacrates.io< 0.8.00.8.0
connectrpccrates.io< 0.8.00.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:

  1. Can an external or lower-trust party supply protobuf to this service?
  2. Does that data reach buffa / connectrpc decode paths before authentication, strict rate limiting, or worker isolation?
  3. 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:

  1. Upgrade buffa to 0.8.0 or later.
  2. Upgrade connectrpc to 0.8.0 or later.
  3. 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:

  1. Patch the runtime and redeploy the affected services.
  2. Review crash-loop, OOM-kill, and restart telemetry for protobuf-facing workers.
  3. Lower overly generous message-size limits where possible.
  4. Add per-request rate limits or circuit breakers on protobuf decode paths that face low-trust senders.
  5. 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.

References