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:
- Panicking (
.expect() / .unwrap() / panic!)
- Silently truncating (
.len() as u8 / .len() as u16) — producing corrupt wire output with no signal to the caller
- 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
Problem
All
encode()methods across the codebase returnBytes(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:.expect()/.unwrap()/panic!).len() as u8/.len() as u16) — producing corrupt wire output with no signal to the caller.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")inencode_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!)src/parser/bgp/messages.rsu8::try_from(encoded_value.len()).expect("BGP capability value length exceeds 255 octets")CapabilityValue::Rawwith >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.rsRawBgpOpenHeader::ref_from_bytes(&header_bytes).expect("header_bytes is exactly 10 bytes…")has_n_remaining(10)guards it, but theexpectis a latent trap.B. Silent truncation —
as u8casts (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.
src/parser/bgp/messages.rsbuf.put_u8(value.len() as u8)src/parser/bgp/messages.rsbytes.put_u16(msg_bytes.len() as u16 + 16 + 2 + 1)src/parser/bgp/messages.rsbytes.put_u16(withdrawn_bytes.len() as u16)src/parser/bgp/messages.rsbytes.put_u16(attr_bytes.len() as u16)src/parser/bgp/attributes/mod.rsbytes.put_u8(value_bytes.len() as u8)src/parser/bgp/attributes/attr_02_17_as_path.rsoutput.put_u8(asns.len() as u8)src/parser/bgp/attributes/attr_14_15_nlri.rsbytes.put_u8(next_hop_bytes.len() as u8)src/parser/bgp/attributes/attr_23_tunnel_encap.rsas u8/as u16src/parser/bgp/attributes/attr_38_bfd_discriminator.rsbuf.put_u8(tlv.value.len() as u8)src/parser/bgp/attributes/attr_40_bgp_prefix_sid.rsbuf.put_u16(tlv.value.len() as u16)src/parser/bgp/attributes/attr_41_bier.rsbuf.put_u16(tlv.value.len() as u16)src/parser/bgp/attributes/attr_37_sfp.rsbuf.put_u16(tlv.value.len() as u16)src/parser/bgp/attributes/attr_29_linkstate.rsas u16src/models/bgp/flowspec/nlri.rsencode_length(data.len() as u16, &mut result)src/parser/mrt/messages/table_dump_v2/rib_afi_entries.rsbytes.put_u16(attr_bytes.len() as u16)src/parser/mrt/messages/table_dump.rsbytes.put_u16(attr_bytes.len() as u16)src/parser/mrt/messages/table_dump_v2/peer_index_table.rsview_name.len() as u16,peers.len() as u16src/parser/mrt/messages/table_dump_v2/geo_peer_table.rsas u16src/parser/mrt/mrt_header.rslength: data_bytes.len() as u32C. Silent truncation —
as u16castsSame class as B but for 2-octet fields. All the
as u16entries in the table above silently produce corrupt output for oversized inputs.D. Error swallowed, empty bytes emitted
src/parser/bgp/attributes/mod.rsencode_nlri(v, true, add_path).unwrap_or_else(|e| { warn!(...); Bytes::new() })src/parser/bgp/attributes/mod.rssrc/parser/bgp/attributes/mod.rsAttributeValue::AttrSet(_v) => { Bytes::new() }Proposed solution
1. Add an
EncodingErrortype2. Add fallible
encode_to/try_encodemethodsKeep the existing infallible
encode() -> Bytesmethods as convenience wrappers (for backwards compatibility) that calltry_encodeand panic on error — useful in tests and contexts where the input is known to be valid. But add: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 u16truncation casts with checked conversionsEvery
put_u8(x.len() as u8)becomes:4. Replace the
.expect()inencode_bgp_open_param_valuewith?5. Change MP_REACH/MP_UNREACH NLRI encoding from silent-swallow to returning errors via
try_encodePriority
The
.expect()panic inencode_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
.expect()panic; also fixed the OPEN param_len encoding bugResult<_, ParserError>). The encoding side should match.