protocol: Limit allocations when decoding frames

`VarInt` can get large enough to exhaust available memory on many
machines. Instead of allocating directly, check the size of the
allocation against some limit.

The limit chosen for frames is 128 KiB, which is roughly twice as much
as actual gossip and Git protocol frames are expected to be. However,
128 KiB is arguably short enough to not burden a standard consumer
machine too much.
This commit is contained in:
Lorenz Leutgeb
2026-09-10 10:22:22 +02:00
parent d8298a1d4c
commit 0e5a56f25f
4 changed files with 35 additions and 7 deletions
+12 -1
View File
@@ -58,7 +58,18 @@ impl<const B: usize, D: wire::Decode> Deserializer<B, D> {
Ok(Some(msg))
}
Err(wire::Error::UnexpectedEnd { .. }) => Ok(None),
Err(wire::Error::UnexpectedEnd { .. }) => {
log::debug!("Dropping incomplete frame: Expected more bytes.");
Ok(None)
}
Err(wire::Error::FrameTooLong { length, limit }) => {
log::debug!(
"Dropping frame that is too large: Expected at most {} bytes, got {} bytes.",
limit,
length
);
Ok(None)
}
Err(wire::Error::Invalid(err)) => Err(err),
}
}
+4 -1
View File
@@ -93,6 +93,9 @@ pub enum Error {
#[error(transparent)]
Invalid(#[from] Invalid),
#[error("frame too long: expected at most {limit}, got {length}")]
FrameTooLong { length: usize, limit: usize },
#[error(
"unexpected end of buffer, requested {requested} more bytes but only {available} are available"
)]
@@ -147,7 +150,7 @@ pub trait Decode: Sized {
}
Ok(value)
}
Err(err @ Error::UnexpectedEnd { .. }) => {
Err(err @ (Error::UnexpectedEnd { .. } | Error::FrameTooLong { .. })) => {
panic!("{}", err);
}
Err(Error::Invalid(e)) => Err(e),
+5 -2
View File
@@ -210,6 +210,9 @@ pub struct Frame<M = Message> {
}
impl<M> Frame<M> {
/// The maximum length of a frame, in bytes (128 KiB).
pub const LENGTH_LIMIT: usize = 1 << 17;
/// Create a 'git' protocol frame.
pub fn git(stream: StreamId, data: Vec<u8>) -> Self {
Self {
@@ -360,7 +363,7 @@ impl<M: wire::Decode> wire::Decode for Frame<M> {
Ok(frame)
}
Ok(StreamType::Gossip) => {
let data = varint::payload::decode(buf)?;
let data = varint::payload::decode(buf, Frame::<M>::LENGTH_LIMIT)?;
let mut cursor = io::Cursor::new(data);
let msg = M::decode(&mut cursor)?;
let frame = Frame {
@@ -375,7 +378,7 @@ impl<M: wire::Decode> wire::Decode for Frame<M> {
Ok(frame)
}
Ok(StreamType::Git) => {
let data = varint::payload::decode(buf)?;
let data = varint::payload::decode(buf, Frame::<M>::LENGTH_LIMIT)?;
Ok(Frame::git(stream, data))
}
Err(n) => Err(wire::Invalid::StreamType { actual: n }.into()),
+14 -3
View File
@@ -163,10 +163,21 @@ pub mod payload {
buf.put_slice(payload);
}
/// Decode varint-prefixed data payload.
pub fn decode(buf: &mut impl Buf) -> Result<Vec<u8>, wire::Error> {
/// Decode varint-prefixed data payload, if the size of the payload
/// is at most `limit` bytes.
pub fn decode(buf: &mut impl Buf, limit: usize) -> Result<Vec<u8>, wire::Error> {
let size = VarInt::decode(buf)?;
let mut data = vec![0; *size as usize];
let size = *size as usize;
if size > limit {
return Err(wire::Error::FrameTooLong {
length: size,
limit,
});
}
let mut data = vec![0; size];
buf.try_copy_to_slice(&mut data[..])?;
Ok(data)