@@ -14,6 +14,15 @@ use alloy_rpc_types_engine::{
1414
1515use crate :: { BaseExecutionPayload , BaseExecutionPayloadSidecar , BaseExecutionPayloadV4 } ;
1616
17+ /// Maximum allowed decoded size for a snappy-compressed [`NetworkPayloadEnvelope`].
18+ ///
19+ /// Mirrors `MAX_GOSSIP_SIZE` in `base-consensus-gossip` and bounds the heap
20+ /// allocation performed by [`NetworkPayloadEnvelope::decode_v1`] and friends.
21+ /// Without this cap, a wire-valid 9 `MiB` snappy frame can declare a 200 `MiB`
22+ /// decoded length and force the decoder to allocate that buffer before any
23+ /// downstream length, SSZ, or signature check runs.
24+ pub const MAX_DECOMPRESSED_ENVELOPE_BYTES : usize = 10 * ( 1 << 20 ) ;
25+
1726/// A thin wrapper around [`BaseExecutionPayload`] that includes the parent beacon block root.
1827#[ derive( Debug , Clone , PartialEq , Eq ) ]
1928#[ cfg_attr( feature = "serde" , derive( serde:: Serialize , serde:: Deserialize ) ) ]
@@ -249,13 +258,31 @@ pub struct NetworkPayloadEnvelope {
249258}
250259
251260impl NetworkPayloadEnvelope {
261+ /// Snappy-decompresses `data` after checking that the declared decoded size
262+ /// does not exceed [`MAX_DECOMPRESSED_ENVELOPE_BYTES`].
263+ ///
264+ /// `snap::raw::decompress_len` parses only the varu32 preamble and never
265+ /// allocates, so this guard runs in O(1) and prevents an unauthenticated
266+ /// peer from forcing arbitrarily large heap allocations on the consensus
267+ /// node before any signature or SSZ check has run.
268+ #[ cfg( feature = "std" ) ]
269+ fn decompress_bounded ( data : & [ u8 ] ) -> Result < Vec < u8 > , PayloadEnvelopeError > {
270+ let declared = snap:: raw:: decompress_len ( data) ?;
271+ if declared > MAX_DECOMPRESSED_ENVELOPE_BYTES {
272+ return Err ( PayloadEnvelopeError :: DecodedTooLarge {
273+ given : declared,
274+ max : MAX_DECOMPRESSED_ENVELOPE_BYTES ,
275+ } ) ;
276+ }
277+ Ok ( snap:: raw:: Decoder :: new ( ) . decompress_vec ( data) ?)
278+ }
279+
252280 /// Decode a payload envelope from a snappy-compressed byte array.
253281 /// The payload version decoded is `ExecutionPayloadV1` from SSZ bytes.
254282 #[ cfg( feature = "std" ) ]
255283 pub fn decode_v1 ( data : & [ u8 ] ) -> Result < Self , PayloadEnvelopeError > {
256284 use ssz:: Decode ;
257- let mut decoder = snap:: raw:: Decoder :: new ( ) ;
258- let decompressed = decoder. decompress_vec ( data) ?;
285+ let decompressed = Self :: decompress_bounded ( data) ?;
259286
260287 if decompressed. len ( ) < 66 {
261288 return Err ( PayloadEnvelopeError :: InvalidLength ) ;
@@ -298,8 +325,7 @@ impl NetworkPayloadEnvelope {
298325 #[ cfg( feature = "std" ) ]
299326 pub fn decode_v2 ( data : & [ u8 ] ) -> Result < Self , PayloadEnvelopeError > {
300327 use ssz:: Decode ;
301- let mut decoder = snap:: raw:: Decoder :: new ( ) ;
302- let decompressed = decoder. decompress_vec ( data) ?;
328+ let decompressed = Self :: decompress_bounded ( data) ?;
303329
304330 if decompressed. len ( ) < 66 {
305331 return Err ( PayloadEnvelopeError :: InvalidLength ) ;
@@ -342,8 +368,7 @@ impl NetworkPayloadEnvelope {
342368 #[ cfg( feature = "std" ) ]
343369 pub fn decode_v3 ( data : & [ u8 ] ) -> Result < Self , PayloadEnvelopeError > {
344370 use ssz:: Decode ;
345- let mut decoder = snap:: raw:: Decoder :: new ( ) ;
346- let decompressed = decoder. decompress_vec ( data) ?;
371+ let decompressed = Self :: decompress_bounded ( data) ?;
347372
348373 if decompressed. len ( ) < 98 {
349374 return Err ( PayloadEnvelopeError :: InvalidLength ) ;
@@ -398,8 +423,7 @@ impl NetworkPayloadEnvelope {
398423 #[ cfg( feature = "std" ) ]
399424 pub fn decode_v4 ( data : & [ u8 ] ) -> Result < Self , PayloadEnvelopeError > {
400425 use ssz:: Decode ;
401- let mut decoder = snap:: raw:: Decoder :: new ( ) ;
402- let decompressed = decoder. decompress_vec ( data) ?;
426+ let decompressed = Self :: decompress_bounded ( data) ?;
403427
404428 if decompressed. len ( ) < 98 {
405429 return Err ( PayloadEnvelopeError :: InvalidLength ) ;
@@ -478,6 +502,14 @@ pub enum PayloadEnvelopeError {
478502 /// The payload envelope is of invalid length.
479503 #[ error( "Invalid length" ) ]
480504 InvalidLength ,
505+ /// The declared decompressed size exceeds [`MAX_DECOMPRESSED_ENVELOPE_BYTES`].
506+ #[ error( "Decoded payload too large: {given} > {max}" ) ]
507+ DecodedTooLarge {
508+ /// Declared decoded size in bytes.
509+ given : usize ,
510+ /// Maximum allowed decoded size in bytes.
511+ max : usize ,
512+ } ,
481513}
482514
483515impl From < alloy_primitives:: SignatureError > for PayloadEnvelopeError {
@@ -634,4 +666,28 @@ mod tests {
634666 let encoded = payload_envelop. encode_v4 ( ) . unwrap ( ) ;
635667 assert_eq ! ( data, encoded) ;
636668 }
669+
670+ #[ test]
671+ #[ cfg( feature = "std" ) ]
672+ fn test_decode_rejects_oversized_decompressed_payload ( ) {
673+ let huge = vec ! [ 0u8 ; MAX_DECOMPRESSED_ENVELOPE_BYTES + 1 ] ;
674+ let bomb = snap:: raw:: Encoder :: new ( ) . compress_vec ( & huge) . unwrap ( ) ;
675+
676+ let declared = snap:: raw:: decompress_len ( & bomb) . unwrap ( ) ;
677+ assert ! ( declared > MAX_DECOMPRESSED_ENVELOPE_BYTES ) ;
678+ assert ! ( bomb. len( ) < MAX_DECOMPRESSED_ENVELOPE_BYTES ) ;
679+
680+ for result in [
681+ NetworkPayloadEnvelope :: decode_v1 ( & bomb) ,
682+ NetworkPayloadEnvelope :: decode_v2 ( & bomb) ,
683+ NetworkPayloadEnvelope :: decode_v3 ( & bomb) ,
684+ NetworkPayloadEnvelope :: decode_v4 ( & bomb) ,
685+ ] {
686+ assert ! ( matches!(
687+ result,
688+ Err ( PayloadEnvelopeError :: DecodedTooLarge { given, max } )
689+ if given == declared && max == MAX_DECOMPRESSED_ENVELOPE_BYTES
690+ ) ) ;
691+ }
692+ }
637693}
0 commit comments