Skip to content

Commit 107ce34

Browse files
committed
fix(network): bound chunk framing amplification
Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com>
1 parent 5cc8e2f commit 107ce34

1 file changed

Lines changed: 95 additions & 0 deletions

File tree

  • crates/openshell-supervisor-network/src/l7

‎crates/openshell-supervisor-network/src/l7/rest.rs‎

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,12 @@ async fn max_middleware_body_bytes() -> usize {
6767
chain[0].max_payload_bytes()
6868
}
6969
const 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;
7076
const RESPONSE_UNIT_COALESCE_TIMEOUT: std::time::Duration = std::time::Duration::from_millis(2);
7177
const 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\na\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\nHost: example.com\r\n\r\n";

0 commit comments

Comments
 (0)