@@ -67,6 +67,12 @@ async fn max_middleware_body_bytes() -> usize {
6767 chain[ 0 ] . max_payload_bytes ( )
6868}
6969const RELAY_BUF_SIZE : usize = 8192 ;
70+ /// Maximum aggregate chunk framing relayed for one body. Framing includes
71+ /// chunk-size lines, the CRLF after each chunk payload, and trailers. Unknown
72+ /// length framing lines must be read to their boundary one byte at a time so a
73+ /// pipelined request is not consumed; bounding their aggregate size also bounds
74+ /// the resulting read/write amplification for tiny chunks.
75+ const MAX_CHUNKED_FRAMING_BYTES : usize = 32 * 1024 ;
7076const RESPONSE_UNIT_COALESCE_TIMEOUT : std:: time:: Duration = std:: time:: Duration :: from_millis ( 2 ) ;
7177const HTTP_METHOD_PREFIXES : & [ & [ u8 ] ] = & [
7278 b"GET " ,
@@ -3131,6 +3137,7 @@ where
31313137 let mut pos = 0usize ;
31323138 let mut chunk_count = 0usize ;
31333139 let mut chunk_payload_bytes = 0usize ;
3140+ let mut framing_bytes = 0usize ;
31343141
31353142 // Parse chunk-size lines + chunk payloads until final 0-size chunk, then
31363143 // parse trailers until the terminating empty trailer line.
@@ -3140,6 +3147,10 @@ where
31403147 if let Some ( end) = find_crlf ( & parse_buf, pos) {
31413148 break end;
31423149 }
3150+ ensure_chunked_framing_capacity (
3151+ framing_bytes,
3152+ parse_buf. len ( ) . saturating_sub ( pos) . saturating_add ( 1 ) ,
3153+ ) ?;
31433154 let target_len = parse_buf
31443155 . len ( )
31453156 . checked_add ( 1 )
@@ -3155,6 +3166,7 @@ where
31553166 )
31563167 . await ?;
31573168 } ;
3169+ add_chunked_framing_bytes ( & mut framing_bytes, size_line_end + 2 - pos) ?;
31583170
31593171 let size_line = std:: str:: from_utf8 ( & parse_buf[ pos..size_line_end] )
31603172 . into_diagnostic ( )
@@ -3177,6 +3189,10 @@ where
31773189 if let Some ( end) = find_crlf ( & parse_buf, pos) {
31783190 break end;
31793191 }
3192+ ensure_chunked_framing_capacity (
3193+ framing_bytes,
3194+ parse_buf. len ( ) . saturating_sub ( pos) . saturating_add ( 1 ) ,
3195+ ) ?;
31803196 let target_len = parse_buf
31813197 . len ( )
31823198 . checked_add ( 1 )
@@ -3194,6 +3210,7 @@ where
31943210 } ;
31953211
31963212 let trailer_line = & parse_buf[ pos..trailer_end] ;
3213+ add_chunked_framing_bytes ( & mut framing_bytes, trailer_end + 2 - pos) ?;
31973214 pos = trailer_end + 2 ;
31983215 if trailer_line. is_empty ( ) {
31993216 debug ! (
@@ -3217,6 +3234,7 @@ where
32173234 . checked_add ( 2 )
32183235 . ok_or_else ( || miette ! ( "Chunk size overflow" ) ) ?;
32193236
3237+ ensure_chunked_framing_capacity ( framing_bytes, 2 ) ?;
32203238 relay_chunked_until_len (
32213239 reader,
32223240 writer,
@@ -3230,6 +3248,7 @@ where
32303248 if & parse_buf[ chunk_end..chunk_with_crlf_end] != b"\r \n " {
32313249 return Err ( miette ! ( "Chunk missing terminating CRLF" ) ) ;
32323250 }
3251+ add_chunked_framing_bytes ( & mut framing_bytes, 2 ) ?;
32333252 pos = chunk_with_crlf_end;
32343253 chunk_count += 1 ;
32353254 chunk_payload_bytes = chunk_payload_bytes. saturating_add ( chunk_size) ;
@@ -3242,6 +3261,21 @@ where
32423261 }
32433262}
32443263
3264+ fn ensure_chunked_framing_capacity ( current : usize , additional : usize ) -> Result < ( ) > {
3265+ if additional > MAX_CHUNKED_FRAMING_BYTES . saturating_sub ( current) {
3266+ return Err ( miette ! (
3267+ "Chunked body framing exceeds {MAX_CHUNKED_FRAMING_BYTES} bytes"
3268+ ) ) ;
3269+ }
3270+ Ok ( ( ) )
3271+ }
3272+
3273+ fn add_chunked_framing_bytes ( current : & mut usize , additional : usize ) -> Result < ( ) > {
3274+ ensure_chunked_framing_capacity ( * current, additional) ?;
3275+ * current += additional;
3276+ Ok ( ( ) )
3277+ }
3278+
32453279/// Read and forward only the bytes needed to reach `target_len`.
32463280///
32473281/// Limiting each socket read to the current chunked framing boundary prevents
@@ -3897,6 +3931,36 @@ mod tests {
38973931 }
38983932 }
38993933
3934+ #[ derive( Default ) ]
3935+ struct CountingWriter {
3936+ writes : usize ,
3937+ }
3938+
3939+ impl AsyncWrite for CountingWriter {
3940+ fn poll_write (
3941+ mut self : Pin < & mut Self > ,
3942+ _context : & mut Context < ' _ > ,
3943+ buffer : & [ u8 ] ,
3944+ ) -> Poll < std:: io:: Result < usize > > {
3945+ self . writes += 1 ;
3946+ Poll :: Ready ( Ok ( buffer. len ( ) ) )
3947+ }
3948+
3949+ fn poll_flush (
3950+ self : Pin < & mut Self > ,
3951+ _context : & mut Context < ' _ > ,
3952+ ) -> Poll < std:: io:: Result < ( ) > > {
3953+ Poll :: Ready ( Ok ( ( ) ) )
3954+ }
3955+
3956+ fn poll_shutdown (
3957+ self : Pin < & mut Self > ,
3958+ _context : & mut Context < ' _ > ,
3959+ ) -> Poll < std:: io:: Result < ( ) > > {
3960+ Poll :: Ready ( Ok ( ( ) ) )
3961+ }
3962+ }
3963+
39003964 fn write_header ( name : & str , value : & str , on_existing : ExistingHeaderAction ) -> HeaderMutation {
39013965 HeaderMutation {
39023966 operation : Some ( header_mutation:: Operation :: Write (
@@ -4660,6 +4724,37 @@ mod tests {
46604724 assert_eq ! ( forwarded, expected) ;
46614725 }
46624726
4727+ #[ tokio:: test]
4728+ async fn relay_chunked_bounds_tiny_chunk_io_amplification ( ) {
4729+ let mut wire = Vec :: new ( ) ;
4730+ for _ in 0 ..10_000 {
4731+ wire. extend_from_slice ( b"1\r \n a\r \n " ) ;
4732+ }
4733+ wire. extend_from_slice ( b"0\r \n \r \n " ) ;
4734+
4735+ let mut reader = CountingReader :: new ( wire) ;
4736+ let mut writer = CountingWriter :: default ( ) ;
4737+ let error = relay_chunked ( & mut reader, & mut writer, & [ ] , None )
4738+ . await
4739+ . expect_err ( "excessive aggregate chunk framing must be rejected" ) ;
4740+
4741+ assert ! (
4742+ error
4743+ . to_string( )
4744+ . contains( "Chunked body framing exceeds 32768 bytes" )
4745+ ) ;
4746+ assert ! (
4747+ reader. reads < 40_000 ,
4748+ "framing limit allowed {} socket reads" ,
4749+ reader. reads
4750+ ) ;
4751+ assert ! (
4752+ writer. writes < 40_000 ,
4753+ "framing limit allowed {} upstream writes" ,
4754+ writer. writes
4755+ ) ;
4756+ }
4757+
46634758 #[ test]
46644759 fn parse_no_body ( ) {
46654760 let headers = "GET /api HTTP/1.1\r \n Host: example.com\r \n \r \n " ;
0 commit comments