Skip to content

Encoding API is non-fallible: arbitrary input data can trigger panics via expect/unwrap and silently corrupt wire framing via truncating casts #313

Description

@digizeph

Problem

All encode() methods across the codebase return Bytes (infallible). This means any condition that should be an encoding error — a value too large for its wire field, too many elements for a count field, etc. — is currently handled either by:

  1. Panicking (.expect() / .unwrap() / panic!)
  2. Silently truncating (.len() as u8 / .len() as u16) — producing corrupt wire output with no signal to the caller
  3. Swallowing the error and emitting empty bytes (.unwrap_or_else(|e| { warn!(...); Bytes::new() }))

PR #312 (BGP OPEN encoder fix) added one such panic: u8::try_from(encoded_value.len()).expect("BGP capability value length exceeds 255 octets") in encode_bgp_open_param_value. The investigation below shows this is not an isolated case.

A parser library that processes untrusted/arbitrary input must never let that input crash the process during encoding (e.g., a round-trip parse → encode of a crafted MRT file). The same input data already survived parsing without error — the encoding layer should be at least as robust.

Catalog of crash-prone and silently-corrupting encoding sites

A. Hard panics (.expect() / panic!)

File Line Code Trigger
src/parser/bgp/messages.rs 402 u8::try_from(encoded_value.len()).expect("BGP capability value length exceeds 255 octets") A parsed CapabilityValue::Raw with >255 bytes, or a capability struct with >255 bytes of encoded data. Reachable via round-trip of any OPEN with an oversized raw capability.
src/parser/bgp/messages.rs 222–223 RawBgpOpenHeader::ref_from_bytes(&header_bytes).expect("header_bytes is exactly 10 bytes…") PARSER code, not encoder — but same class of infallible assumption. Safe today because has_n_remaining(10) guards it, but the expect is a latent trap.

B. Silent truncation — as u8 casts (corrupt wire output, no error)

These casts silently discard high bits. A value >255 wraps around, producing a wrong count/length on the wire with no error to the caller.

File Line Code What overflows Wire corruption
src/parser/bgp/messages.rs 474 buf.put_u8(value.len() as u8) OPEN opt-param value >255 bytes (non-extended) Truncated length field → malformed OPEN
src/parser/bgp/messages.rs 670 bytes.put_u16(msg_bytes.len() as u16 + 16 + 2 + 1) BGP message body >65519 bytes Truncated total length → truncated message
src/parser/bgp/messages.rs 597 bytes.put_u16(withdrawn_bytes.len() as u16) Withdrawn prefixes >65535 bytes Truncated withdrawn length → corrupt UPDATE
src/parser/bgp/messages.rs 603 bytes.put_u16(attr_bytes.len() as u16) Attributes >65535 bytes Truncated attr length → corrupt UPDATE
src/parser/bgp/attributes/mod.rs 576 bytes.put_u8(value_bytes.len() as u8) Non-extended attribute value >255 bytes Truncated attr length
src/parser/bgp/attributes/attr_02_17_as_path.rs 63,68,73,78 output.put_u8(asns.len() as u8) AS_PATH/AS_SET segment with >255 ASNs Truncated segment count → corrupt AS_PATH
src/parser/bgp/attributes/attr_14_15_nlri.rs 190 bytes.put_u8(next_hop_bytes.len() as u8) Next-hop >255 bytes Truncated next-hop length
src/parser/bgp/attributes/attr_23_tunnel_encap.rs 99,101,102,110 Multiple as u8/as u16 Sub-TLV or tunnel >255/65535 bytes Truncated lengths
src/parser/bgp/attributes/attr_38_bfd_discriminator.rs 50 buf.put_u8(tlv.value.len() as u8) BFD TLV value >255 bytes Truncated length
src/parser/bgp/attributes/attr_40_bgp_prefix_sid.rs 34 buf.put_u16(tlv.value.len() as u16) Prefix-SID TLV >65535 bytes Truncated length
src/parser/bgp/attributes/attr_41_bier.rs 34 buf.put_u16(tlv.value.len() as u16) BIER TLV >65535 bytes Truncated length
src/parser/bgp/attributes/attr_37_sfp.rs 34 buf.put_u16(tlv.value.len() as u16) SFP TLV >65535 bytes Truncated length
src/parser/bgp/attributes/attr_29_linkstate.rs 442,450,458 Multiple as u16 Link-state TLV >65535 bytes Truncated lengths
src/models/bgp/flowspec/nlri.rs 93 encode_length(data.len() as u16, &mut result) Flowspec NLRI component >65535 bytes Truncated length
src/parser/mrt/messages/table_dump_v2/rib_afi_entries.rs 200 bytes.put_u16(attr_bytes.len() as u16) RIB entry attributes >65535 bytes Truncated length
src/parser/mrt/messages/table_dump.rs 156 bytes.put_u16(attr_bytes.len() as u16) Table dump v1 attributes >65535 bytes Truncated length
src/parser/mrt/messages/table_dump_v2/peer_index_table.rs 149,155 view_name.len() as u16, peers.len() as u16 Peer table >65535 bytes Truncated lengths
src/parser/mrt/messages/table_dump_v2/geo_peer_table.rs 147,155 Similar as u16 Geo peer table >65535 bytes Truncated lengths
src/parser/mrt/mrt_header.rs (encode) length: data_bytes.len() as u32 MRT record >4GB Truncated length

C. Silent truncation — as u16 casts

Same class as B but for 2-octet fields. All the as u16 entries in the table above silently produce corrupt output for oversized inputs.

D. Error swallowed, empty bytes emitted

File Line Code Effect
src/parser/bgp/attributes/mod.rs 545–548 encode_nlri(v, true, add_path).unwrap_or_else(|e| { warn!(...); Bytes::new() }) MP_REACH_NLRI encoding failure → silently emits a 0-byte attribute value (looks like empty NLRI on the wire)
src/parser/bgp/attributes/mod.rs 550–555 Same for MP_UNREACH_NLRI Same problem
src/parser/bgp/attributes/mod.rs 568–571 AttributeValue::AttrSet(_v) => { Bytes::new() } ATTR_SET not implemented → silently emits empty bytes (incomplete attribute)

Proposed solution

1. Add an EncodingError type

// src/error.rs
#[derive(Debug)]
pub enum EncodingError {
    /// A value exceeded the maximum size for its wire field.
    /// E.g. an AS_PATH segment with >255 ASNs, or an attribute value >65535 bytes.
    ValueTooLarge {
        field: &'static str,
        actual: usize,
        max: usize,
    },
}

2. Add fallible encode_to / try_encode methods

Keep the existing infallible encode() -> Bytes methods as convenience wrappers (for backwards compatibility) that call try_encode and panic on error — useful in tests and contexts where the input is known to be valid. But add:

impl BgpOpenMessage {
    pub fn try_encode(&self) -> Result<Bytes, EncodingError> { ... }
}

Apply the same pattern to: BgpUpdateMessage, BgpMessage, Attribute, Attributes, encode_as_path, encode_nlri, BgpNotificationMessage, MRT message encoders, etc.

3. Replace all as u8 / as u16 truncation casts with checked conversions

Every put_u8(x.len() as u8) becomes:

let len = u8::try_from(value.len()).map_err(|_| EncodingError::ValueTooLarge { ... })?;
buf.put_u8(len);

4. Replace the .expect() in encode_bgp_open_param_value with ?

5. Change MP_REACH/MP_UNREACH NLRI encoding from silent-swallow to returning errors via try_encode

Priority

The .expect() panic in encode_bgp_open_param_value (category A) is the most urgent — it's a direct crash from arbitrary input. The silent truncation casts (category B/C) are correctness bugs that produce corrupt output, which is arguably worse than crashing because it's invisible.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingenhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions