Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 6 additions & 7 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ jobs:
strategy:
matrix:
rust:
- nightly
- beta
- stable
steps:
Expand Down Expand Up @@ -61,12 +60,12 @@ jobs:
run: ./ci/h2spec.sh
if: matrix.rust == 'stable'

clippy_check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Clippy
run: cargo clippy --all-targets --all-features
#clippy_check:
# runs-on: ubuntu-latest
# steps:
# - uses: actions/checkout@v4
# - name: Run Clippy
# run: cargo clippy --all-targets --all-features

msrv:
name: Check MSRV
Expand Down
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
# 0.3.26 (April 3, 2024)

* Limit number of CONTINUATION frames for misbehaving connections.

# 0.3.25 (March 15, 2024)

* Improve performance decoding many headers.

# 0.3.24 (January 17, 2024)

* Limit error resets for misbehaving connections.

# 0.3.23 (January 10, 2024)

* Backport fix from 0.4.1 for stream capacity assignment.

# 0.3.22 (November 15, 2023)

* Add `header_table_size(usize)` option to client and server builders.
Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ name = "h2"
# - html_root_url.
# - Update CHANGELOG.md.
# - Create git tag
version = "0.3.22"
version = "0.3.26"
license = "MIT"
authors = [
"Carl Lerche <[email protected]>",
Expand Down
27 changes: 27 additions & 0 deletions src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,12 @@ pub struct Builder {
/// The stream ID of the first (lowest) stream. Subsequent streams will use
/// monotonically increasing stream IDs.
stream_id: StreamId,

/// Maximum number of locally reset streams due to protocol error across
/// the lifetime of the connection.
///
/// When this gets exceeded, we issue GOAWAYs.
local_max_error_reset_streams: Option<usize>,
}

#[derive(Debug)]
Expand Down Expand Up @@ -645,6 +651,7 @@ impl Builder {
initial_max_send_streams: usize::MAX,
settings: Default::default(),
stream_id: 1.into(),
local_max_error_reset_streams: Some(proto::DEFAULT_LOCAL_RESET_COUNT_MAX),
}
}

Expand Down Expand Up @@ -973,6 +980,23 @@ impl Builder {
self
}

/// Sets the maximum number of local resets due to protocol errors made by the remote end.
///
/// Invalid frames and many other protocol errors will lead to resets being generated for those streams.
/// Too many of these often indicate a malicious client, and there are attacks which can abuse this to DOS servers.
/// This limit protects against these DOS attacks by limiting the amount of resets we can be forced to generate.
///
/// When the number of local resets exceeds this threshold, the client will close the connection.
///
/// If you really want to disable this, supply [`Option::None`] here.
/// Disabling this is not recommended and may expose you to DOS attacks.
///
/// The default value is currently 1024, but could change.
pub fn max_local_error_reset_streams(&mut self, max: Option<usize>) -> &mut Self {
self.local_max_error_reset_streams = max;
self
}

/// Sets the maximum number of pending-accept remotely-reset streams.
///
/// Streams that have been received by the peer, but not accepted by the
Expand Down Expand Up @@ -1293,6 +1317,7 @@ where
reset_stream_duration: builder.reset_stream_duration,
reset_stream_max: builder.reset_stream_max,
remote_reset_stream_max: builder.pending_accept_reset_stream_max,
local_error_reset_streams_max: builder.local_max_error_reset_streams,
settings: builder.settings.clone(),
},
);
Expand Down Expand Up @@ -1606,9 +1631,11 @@ impl proto::Peer for Peer {
proto::DynPeer::Client
}

/*
fn is_server() -> bool {
false
}
*/

fn convert_poll_message(
pseudo: Pseudo,
Expand Down
53 changes: 49 additions & 4 deletions src/codec/framed_read.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ pub struct FramedRead<T> {

max_header_list_size: usize,

max_continuation_frames: usize,

partial: Option<Partial>,
}

Expand All @@ -41,6 +43,8 @@ struct Partial {

/// Partial header payload
buf: BytesMut,

continuation_frames_count: usize,
}

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

impl<T> FramedRead<T> {
pub fn new(inner: InnerFramedRead<T, LengthDelimitedCodec>) -> FramedRead<T> {
let max_header_list_size = DEFAULT_SETTINGS_MAX_HEADER_LIST_SIZE;
let max_continuation_frames =
calc_max_continuation_frames(max_header_list_size, inner.decoder().max_frame_length());
FramedRead {
inner,
hpack: hpack::Decoder::new(DEFAULT_SETTINGS_HEADER_TABLE_SIZE),
max_header_list_size: DEFAULT_SETTINGS_MAX_HEADER_LIST_SIZE,
max_header_list_size,
max_continuation_frames,
partial: None,
}
}
Expand All @@ -68,7 +76,6 @@ impl<T> FramedRead<T> {
}

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

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

/// Update the header table size setting.
Expand All @@ -96,12 +107,22 @@ impl<T> FramedRead<T> {
}
}

fn calc_max_continuation_frames(header_max: usize, frame_max: usize) -> usize {
// At least this many frames needed to use max header list size
let min_frames_for_list = (header_max / frame_max).max(1);
// Some padding for imperfectly packed frames
// 25% without floats
let padding = min_frames_for_list >> 2;
min_frames_for_list.saturating_add(padding).max(5)
}

/// Decodes a frame.
///
/// This method is intentionally de-generified and outlined because it is very large.
fn decode_frame(
hpack: &mut hpack::Decoder,
max_header_list_size: usize,
max_continuation_frames: usize,
partial_inout: &mut Option<Partial>,
mut bytes: BytesMut,
) -> Result<Option<Frame>, Error> {
Expand Down Expand Up @@ -169,6 +190,7 @@ fn decode_frame(
*partial_inout = Some(Partial {
frame: Continuable::$frame(frame),
buf: payload,
continuation_frames_count: 0,
});

return Ok(None);
Expand Down Expand Up @@ -273,6 +295,22 @@ fn decode_frame(
return Err(Error::library_go_away(Reason::PROTOCOL_ERROR));
}

// Check for CONTINUATION flood
if is_end_headers {
partial.continuation_frames_count = 0;
} else {
let cnt = partial.continuation_frames_count + 1;
if cnt > max_continuation_frames {
tracing::debug!("too_many_continuations, max = {}", max_continuation_frames);
return Err(Error::library_go_away_data(
Reason::ENHANCE_YOUR_CALM,
"too_many_continuations",
));
} else {
partial.continuation_frames_count = cnt;
}
}

// Extend the buf
if partial.buf.is_empty() {
partial.buf = bytes.split_off(frame::HEADER_LEN);
Expand Down Expand Up @@ -354,9 +392,16 @@ where
ref mut hpack,
max_header_list_size,
ref mut partial,
max_continuation_frames,
..
} = *self;
if let Some(frame) = decode_frame(hpack, max_header_list_size, partial, bytes)? {
if let Some(frame) = decode_frame(
hpack,
max_header_list_size,
max_continuation_frames,
partial,
bytes,
)? {
tracing::debug!(?frame, "received");
return Poll::Ready(Some(Ok(frame)));
}
Expand Down
1 change: 1 addition & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ pub struct Error {
#[derive(Debug)]
enum Kind {
/// A RST_STREAM frame was received or sent.
#[allow(dead_code)]
Reset(StreamId, Reason, Initiator),

/// A GO_AWAY frame was received or sent.
Expand Down
25 changes: 18 additions & 7 deletions src/frame/headers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use std::fmt;
use std::io::Cursor;

type EncodeBuf<'a> = bytes::buf::Limit<&'a mut BytesMut>;

/// Header frame
///
/// This could be either a request or a response.
Expand Down Expand Up @@ -87,6 +88,9 @@ struct HeaderBlock {
/// The decoded header fields
fields: HeaderMap,

/// Precomputed size of all of our header fields, for perf reasons
field_size: usize,

/// Set to true if decoding went over the max header list size.
is_over_size: bool,

Expand Down Expand Up @@ -115,6 +119,7 @@ impl Headers {
stream_id,
stream_dep: None,
header_block: HeaderBlock {
field_size: calculate_headermap_size(&fields),
fields,
is_over_size: false,
pseudo,
Expand All @@ -131,6 +136,7 @@ impl Headers {
stream_id,
stream_dep: None,
header_block: HeaderBlock {
field_size: calculate_headermap_size(&fields),
fields,
is_over_size: false,
pseudo: Pseudo::default(),
Expand Down Expand Up @@ -196,6 +202,7 @@ impl Headers {
stream_dep,
header_block: HeaderBlock {
fields: HeaderMap::new(),
field_size: 0,
is_over_size: false,
pseudo: Pseudo::default(),
},
Expand Down Expand Up @@ -350,6 +357,7 @@ impl PushPromise {
PushPromise {
flags: PushPromiseFlag::default(),
header_block: HeaderBlock {
field_size: calculate_headermap_size(&fields),
fields,
is_over_size: false,
pseudo,
Expand Down Expand Up @@ -441,6 +449,7 @@ impl PushPromise {
flags,
header_block: HeaderBlock {
fields: HeaderMap::new(),
field_size: 0,
is_over_size: false,
pseudo: Pseudo::default(),
},
Expand Down Expand Up @@ -892,6 +901,8 @@ impl HeaderBlock {

headers_size += decoded_header_size(name.as_str().len(), value.len());
if headers_size < max_header_list_size {
self.field_size +=
decoded_header_size(name.as_str().len(), value.len());
self.fields.append(name, value);
} else if !self.is_over_size {
tracing::trace!("load_hpack; header list size over max");
Expand Down Expand Up @@ -958,14 +969,16 @@ impl HeaderBlock {
+ pseudo_size!(status)
+ pseudo_size!(authority)
+ pseudo_size!(path)
+ self
.fields
.iter()
.map(|(name, value)| decoded_header_size(name.as_str().len(), value.len()))
.sum::<usize>()
+ self.field_size
}
}

fn calculate_headermap_size(map: &HeaderMap) -> usize {
map.iter()
.map(|(name, value)| decoded_header_size(name.as_str().len(), value.len()))
.sum::<usize>()
}

fn decoded_header_size(name: usize, value: usize) -> usize {
name + value + 32
}
Expand All @@ -974,8 +987,6 @@ fn decoded_header_size(name: usize, value: usize) -> usize {
mod test {
use std::iter::FromIterator;

use http::HeaderValue;

use super::*;
use crate::frame;
use crate::hpack::{huffman, Encoder};
Expand Down
1 change: 0 additions & 1 deletion src/hpack/decoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -829,7 +829,6 @@ pub fn get_static(idx: usize) -> Header {
#[cfg(test)]
mod test {
use super::*;
use crate::hpack::Header;

#[test]
fn test_peek_u8() {
Expand Down
1 change: 0 additions & 1 deletion src/hpack/encoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,6 @@ fn position(buf: &BytesMut) -> usize {
#[cfg(test)]
mod test {
use super::*;
use crate::hpack::Header;
use http::*;

#[test]
Expand Down
1 change: 0 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,6 @@
//! [`server::handshake`]: server/fn.handshake.html
//! [`client::handshake`]: client/fn.handshake.html

#![doc(html_root_url = "https://docs.rs/h2/0.3.22")]
#![deny(
missing_debug_implementations,
missing_docs,
Expand Down
Loading