Skip to content

Commit 1a357aa

Browse files
committed
fix: limit number of CONTINUATION frames allowed
Calculate the amount of allowed CONTINUATION frames based on other settings. max_header_list_size / max_frame_size That is about how many CONTINUATION frames would be needed to send headers up to the max allowed size. We then multiply by that by a small amount, to allow for implementations that don't perfectly pack into the minimum frames *needed*. In practice, *much* more than that would be a very inefficient peer, or a peer trying to waste resources. See https://seanmonstar.com/blog/hyper-http2-continuation-flood/ for more info.
1 parent 5b6c9e0 commit 1a357aa

File tree

2 files changed

+98
-4
lines changed

2 files changed

+98
-4
lines changed

src/codec/framed_read.rs

+49-4
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ pub struct FramedRead<T> {
3030

3131
max_header_list_size: usize,
3232

33+
max_continuation_frames: usize,
34+
3335
partial: Option<Partial>,
3436
}
3537

@@ -41,6 +43,8 @@ struct Partial {
4143

4244
/// Partial header payload
4345
buf: BytesMut,
46+
47+
continuation_frames_count: usize,
4448
}
4549

4650
#[derive(Debug)]
@@ -51,10 +55,14 @@ enum Continuable {
5155

5256
impl<T> FramedRead<T> {
5357
pub fn new(inner: InnerFramedRead<T, LengthDelimitedCodec>) -> FramedRead<T> {
58+
let max_header_list_size = DEFAULT_SETTINGS_MAX_HEADER_LIST_SIZE;
59+
let max_continuation_frames =
60+
calc_max_continuation_frames(max_header_list_size, inner.decoder().max_frame_length());
5461
FramedRead {
5562
inner,
5663
hpack: hpack::Decoder::new(DEFAULT_SETTINGS_HEADER_TABLE_SIZE),
57-
max_header_list_size: DEFAULT_SETTINGS_MAX_HEADER_LIST_SIZE,
64+
max_header_list_size,
65+
max_continuation_frames,
5866
partial: None,
5967
}
6068
}
@@ -68,7 +76,6 @@ impl<T> FramedRead<T> {
6876
}
6977

7078
/// Returns the current max frame size setting
71-
#[cfg(feature = "unstable")]
7279
#[inline]
7380
pub fn max_frame_size(&self) -> usize {
7481
self.inner.decoder().max_frame_length()
@@ -80,13 +87,17 @@ impl<T> FramedRead<T> {
8087
#[inline]
8188
pub fn set_max_frame_size(&mut self, val: usize) {
8289
assert!(DEFAULT_MAX_FRAME_SIZE as usize <= val && val <= MAX_MAX_FRAME_SIZE as usize);
83-
self.inner.decoder_mut().set_max_frame_length(val)
90+
self.inner.decoder_mut().set_max_frame_length(val);
91+
// Update max CONTINUATION frames too, since its based on this
92+
self.max_continuation_frames = calc_max_continuation_frames(self.max_header_list_size, val);
8493
}
8594

8695
/// Update the max header list size setting.
8796
#[inline]
8897
pub fn set_max_header_list_size(&mut self, val: usize) {
8998
self.max_header_list_size = val;
99+
// Update max CONTINUATION frames too, since its based on this
100+
self.max_continuation_frames = calc_max_continuation_frames(val, self.max_frame_size());
90101
}
91102

92103
/// Update the header table size setting.
@@ -96,12 +107,22 @@ impl<T> FramedRead<T> {
96107
}
97108
}
98109

110+
fn calc_max_continuation_frames(header_max: usize, frame_max: usize) -> usize {
111+
// At least this many frames needed to use max header list size
112+
let min_frames_for_list = (header_max / frame_max).max(1);
113+
// Some padding for imperfectly packed frames
114+
// 25% without floats
115+
let padding = min_frames_for_list >> 2;
116+
min_frames_for_list.saturating_add(padding).max(5)
117+
}
118+
99119
/// Decodes a frame.
100120
///
101121
/// This method is intentionally de-generified and outlined because it is very large.
102122
fn decode_frame(
103123
hpack: &mut hpack::Decoder,
104124
max_header_list_size: usize,
125+
max_continuation_frames: usize,
105126
partial_inout: &mut Option<Partial>,
106127
mut bytes: BytesMut,
107128
) -> Result<Option<Frame>, Error> {
@@ -169,6 +190,7 @@ fn decode_frame(
169190
*partial_inout = Some(Partial {
170191
frame: Continuable::$frame(frame),
171192
buf: payload,
193+
continuation_frames_count: 0,
172194
});
173195

174196
return Ok(None);
@@ -273,6 +295,22 @@ fn decode_frame(
273295
return Err(Error::library_go_away(Reason::PROTOCOL_ERROR));
274296
}
275297

298+
// Check for CONTINUATION flood
299+
if is_end_headers {
300+
partial.continuation_frames_count = 0;
301+
} else {
302+
let cnt = partial.continuation_frames_count + 1;
303+
if cnt > max_continuation_frames {
304+
tracing::debug!("too_many_continuations, max = {}", max_continuation_frames);
305+
return Err(Error::library_go_away_data(
306+
Reason::ENHANCE_YOUR_CALM,
307+
"too_many_continuations",
308+
));
309+
} else {
310+
partial.continuation_frames_count = cnt;
311+
}
312+
}
313+
276314
// Extend the buf
277315
if partial.buf.is_empty() {
278316
partial.buf = bytes.split_off(frame::HEADER_LEN);
@@ -354,9 +392,16 @@ where
354392
ref mut hpack,
355393
max_header_list_size,
356394
ref mut partial,
395+
max_continuation_frames,
357396
..
358397
} = *self;
359-
if let Some(frame) = decode_frame(hpack, max_header_list_size, partial, bytes)? {
398+
if let Some(frame) = decode_frame(
399+
hpack,
400+
max_header_list_size,
401+
max_continuation_frames,
402+
partial,
403+
bytes,
404+
)? {
360405
tracing::debug!(?frame, "received");
361406
return Poll::Ready(Some(Ok(frame)));
362407
}

tests/h2-tests/tests/server.rs

+49
Original file line numberDiff line numberDiff line change
@@ -883,6 +883,55 @@ async fn too_big_headers_sends_reset_after_431_if_not_eos() {
883883
join(client, srv).await;
884884
}
885885

886+
#[tokio::test]
887+
async fn too_many_continuation_frames_sends_goaway() {
888+
h2_support::trace_init!();
889+
let (io, mut client) = mock::new();
890+
891+
let client = async move {
892+
let settings = client.assert_server_handshake().await;
893+
assert_frame_eq(settings, frames::settings().max_header_list_size(1024 * 32));
894+
895+
// the mock impl automatically splits into CONTINUATION frames if the
896+
// headers are too big for one frame. So without a max header list size
897+
// set, we'll send a bunch of headers that will eventually get nuked.
898+
client
899+
.send_frame(
900+
frames::headers(1)
901+
.request("GET", "https://example.com/")
902+
.field("a".repeat(10_000), "b".repeat(10_000))
903+
.field("c".repeat(10_000), "d".repeat(10_000))
904+
.field("e".repeat(10_000), "f".repeat(10_000))
905+
.field("g".repeat(10_000), "h".repeat(10_000))
906+
.field("i".repeat(10_000), "j".repeat(10_000))
907+
.field("k".repeat(10_000), "l".repeat(10_000))
908+
.field("m".repeat(10_000), "n".repeat(10_000))
909+
.field("o".repeat(10_000), "p".repeat(10_000))
910+
.field("y".repeat(10_000), "z".repeat(10_000)),
911+
)
912+
.await;
913+
client
914+
.recv_frame(frames::go_away(0).calm().data("too_many_continuations"))
915+
.await;
916+
};
917+
918+
let srv = async move {
919+
let mut srv = server::Builder::new()
920+
// should mean ~3 continuation
921+
.max_header_list_size(1024 * 32)
922+
.handshake::<_, Bytes>(io)
923+
.await
924+
.expect("handshake");
925+
926+
let err = srv.next().await.unwrap().expect_err("server");
927+
assert!(err.is_go_away());
928+
assert!(err.is_library());
929+
assert_eq!(err.reason(), Some(Reason::ENHANCE_YOUR_CALM));
930+
};
931+
932+
join(client, srv).await;
933+
}
934+
886935
#[tokio::test]
887936
async fn pending_accept_recv_illegal_content_length_data() {
888937
h2_support::trace_init!();

0 commit comments

Comments
 (0)