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
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
# 0.4.15 (June 15, 2026)

* Fix closing a connection when header size is "way too large" (currently x4 configured limit).
* Fix overflow calculating padding length if a DATA frame had 255 bytes of padding.
* Fix ignoring library-initiated resets in the connection state loop.
* Fix decoding panic with an absurd amount of headers and no limit to now use `try_append()`.
* Fix rejecting frames on streams whose HEADERS have not been sent.
* Fix `poll_capacity()` to not return `Some(Ok(0))`.
* Fix discarding of buffered DATA frames when a reset is scheduled.

# 0.4.14 (May 5, 2026)

* Add `header_table_size()` option to server builder.
Expand Down
14 changes: 14 additions & 0 deletions src/codec/framed_read.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,13 @@ fn decode_frame(
proto_err!(stream: "malformed header block; stream={:?}", id);
return Err(Error::library_reset(id, Reason::PROTOCOL_ERROR));
},
Err(frame::Error::HeaderListWayTooLarge) => {
proto_err!(conn: "decoded header list size over abuse limit");
return Err(Error::library_go_away_data(
Reason::ENHANCE_YOUR_CALM,
"header_list_way_too_large",
));
},
Err(_e) => {
proto_err!(conn: "failed HPACK decoding; err={:?}", _e);
return Err(Error::library_go_away(Reason::PROTOCOL_ERROR));
Expand Down Expand Up @@ -347,6 +354,13 @@ fn decode_frame(
proto_err!(stream: "malformed CONTINUATION frame; stream={:?}", id);
return Err(Error::library_reset(id, Reason::PROTOCOL_ERROR));
}
Err(frame::Error::HeaderListWayTooLarge) => {
proto_err!(conn: "decoded CONTINUATION header list size over abuse limit");
return Err(Error::library_go_away_data(
Reason::ENHANCE_YOUR_CALM,
"header_list_way_too_large",
));
}
Err(_e) => {
proto_err!(conn: "failed HPACK decoding; err={:?}", _e);
return Err(Error::library_go_away(Reason::PROTOCOL_ERROR));
Expand Down
43 changes: 40 additions & 3 deletions src/frame/data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -148,10 +148,47 @@ impl Data<Bytes> {
self.data.len()
}
}
}

#[cfg(test)]
mod tests {
use super::*;
use bytes::Bytes;

fn data_frame(data: &[u8], pad_len: Option<u8>) -> Data<Bytes> {
Data {
stream_id: StreamId::from(1),
data: Bytes::copy_from_slice(data),
flags: DataFlags::default(),
pad_len,
}
}

#[test]
fn padding_overhead_no_padding() {
let frame = data_frame(b"hello", None);
assert_eq!(frame.flow_controlled_len() - frame.payload().len(), 0);
}

#[test]
fn padding_overhead_small() {
let frame = data_frame(b"hello", Some(10));
assert_eq!(frame.flow_controlled_len() - frame.payload().len(), 11);
}

#[test]
fn padding_overhead_max_does_not_overflow() {
// Regression: the old padded_len() returned u8, so pad_len=255
// caused 255u8 + 1 = 0. The correct overhead is 256.
let frame = data_frame(b"", Some(255));
assert_eq!(frame.flow_controlled_len() - frame.payload().len(), 256);
}

/// If this frame is PADDED, it returns the pad len + 1 (length field).
pub(crate) fn padded_len(&self) -> Option<u8> {
self.pad_len.map(|n| n + 1)
#[test]
fn padding_overhead_max_with_data() {
let frame = data_frame(b"hello", Some(255));
assert_eq!(frame.flow_controlled_len() - frame.payload().len(), 256);
assert_eq!(frame.flow_controlled_len(), 261);
}
}

Expand Down
60 changes: 46 additions & 14 deletions src/frame/headers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,12 @@ use smallvec::SmallVec;

use std::fmt;
use std::io::Cursor;
use std::ops::ControlFlow;

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

const MAX_HEADER_LIST_ABUSE_MULTIPLIER: usize = 4;

/// Header frame
///
/// This could be either a request or a response.
Expand Down Expand Up @@ -985,7 +988,26 @@ impl HeaderBlock {
) -> Result<(), Error> {
let mut reg = !self.fields.is_empty();
let mut malformed = false;
let mut header_list_way_too_large = false;
let mut headers_size = self.calculate_header_list_size();
let max_header_list_abuse_size =
max_header_list_size.saturating_mul(MAX_HEADER_LIST_ABUSE_MULTIPLIER);

macro_rules! check_size {
() => {{
if headers_size > max_header_list_abuse_size {
tracing::trace!("load_hpack; header list size over abuse max");
header_list_way_too_large = true;
ControlFlow::Break(())
} else {
if headers_size >= max_header_list_size && !self.is_over_size {
tracing::trace!("load_hpack; header list size over max");
self.is_over_size = true;
}
ControlFlow::Continue(())
}
}};
}

macro_rules! set_pseudo {
($field:ident, $val:expr) => {{
Expand All @@ -999,11 +1021,11 @@ impl HeaderBlock {
let __val = $val;
headers_size +=
decoded_header_size(stringify!($field).len() + 1, __val.as_str().len());
if headers_size < max_header_list_size {
if check_size!().is_break() {
return ControlFlow::Break(());
}
if !self.is_over_size {
self.pseudo.$field = Some(__val);
} else if !self.is_over_size {
tracing::trace!("load_hpack; header list size over max");
self.is_over_size = true;
}
}
}};
Expand Down Expand Up @@ -1040,19 +1062,19 @@ impl HeaderBlock {
} else {
reg = true;

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());
let header_size = decoded_header_size(name.as_str().len(), value.len());
headers_size += header_size;
if check_size!().is_break() {
return ControlFlow::Break(());
}
if !self.is_over_size {
self.field_size += header_size;
if let Err(_) = self.fields.try_append(name, value) {
// HeaderMap capacity exceeded — treat as over-size
// so the stream is rejected downstream (RST_STREAM / 431)
// instead of panicking on the 24,577th unique header.
self.is_over_size = true;
}
} else if !self.is_over_size {
tracing::trace!("load_hpack; header list size over max");
self.is_over_size = true;
}
}
}
Expand All @@ -1063,11 +1085,21 @@ impl HeaderBlock {
Protocol(v) => set_pseudo!(protocol, v),
Status(v) => set_pseudo!(status, v),
}

ControlFlow::Continue(())
});

if let Err(e) = res {
tracing::trace!("hpack decoding error; err={:?}", e);
return Err(e.into());
match res {
Ok(()) => {}
Err(e) => {
tracing::trace!("hpack decoding error; err={:?}", e);
return Err(e.into());
}
}

if header_list_way_too_large {
tracing::trace!("header list way too large; aborting connection");
return Err(Error::HeaderListWayTooLarge);
}

if malformed {
Expand Down
3 changes: 3 additions & 0 deletions src/frame/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,9 @@ pub enum Error {
/// A request or response is malformed.
MalformedMessage,

/// The decoded header list was too large to continue processing.
HeaderListWayTooLarge,

/// An invalid stream dependency ID was provided
///
/// This is returned if a HEADERS or PRIORITY frame is received with an
Expand Down
3 changes: 2 additions & 1 deletion src/fuzz_bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ pub mod fuzz_logic {
use bytes::BytesMut;
use http::header::HeaderName;
use std::io::Cursor;
use std::ops::ControlFlow;

pub fn fuzz_hpack(data_: &[u8]) {
let mut decoder_ = hpack::Decoder::new(0);
let mut buf = BytesMut::new();
buf.extend(data_);
let _dec_res = decoder_.decode(&mut Cursor::new(&mut buf), |_h| {});
let _dec_res = decoder_.decode(&mut Cursor::new(&mut buf), |_h| ControlFlow::Continue(()));

if let Ok(s) = std::str::from_utf8(data_) {
if let Ok(h) = http::Method::from_bytes(s.as_bytes()) {
Expand Down
25 changes: 19 additions & 6 deletions src/hpack/decoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use http::status::{self, StatusCode};
use std::cmp;
use std::collections::VecDeque;
use std::io::Cursor;
use std::ops::ControlFlow;
use std::str::Utf8Error;

/// Decodes headers using HPACK
Expand Down Expand Up @@ -179,7 +180,7 @@ impl Decoder {
mut f: F,
) -> Result<(), DecoderError>
where
F: FnMut(Header),
F: FnMut(Header) -> ControlFlow<()>,
{
use self::Representation::*;

Expand All @@ -203,7 +204,9 @@ impl Decoder {
can_resize = false;
let entry = self.decode_indexed(src)?;
consume(src);
f(entry);
if f(entry).is_break() {
break;
}
}
LiteralWithIndexing => {
tracing::trace!(rem = src.remaining(), kind = %"LiteralWithIndexing");
Expand All @@ -214,14 +217,18 @@ impl Decoder {
self.table.insert(entry.clone());
consume(src);

f(entry);
if f(entry).is_break() {
break;
}
}
LiteralWithoutIndexing => {
tracing::trace!(rem = src.remaining(), kind = %"LiteralWithoutIndexing");
can_resize = false;
let entry = self.decode_literal(src, false)?;
consume(src);
f(entry);
if f(entry).is_break() {
break;
}
}
LiteralNeverIndexed => {
tracing::trace!(rem = src.remaining(), kind = %"LiteralNeverIndexed");
Expand All @@ -231,7 +238,9 @@ impl Decoder {

// TODO: Track that this should never be indexed

f(entry);
if f(entry).is_break() {
break;
}
}
SizeUpdate => {
tracing::trace!(rem = src.remaining(), kind = %"SizeUpdate");
Expand Down Expand Up @@ -914,7 +923,8 @@ mod test {
fn test_decode_empty() {
let mut de = Decoder::new(0);
let mut buf = BytesMut::new();
let _: () = de.decode(&mut Cursor::new(&mut buf), |_| {}).unwrap();
de.decode(&mut Cursor::new(&mut buf), |_| ControlFlow::Continue(()))
.unwrap();
}

#[test]
Expand All @@ -930,6 +940,7 @@ mod test {
let mut res = vec![];
de.decode(&mut Cursor::new(&mut buf), |h| {
res.push(h);
ControlFlow::Continue(())
})
.unwrap();

Expand Down Expand Up @@ -970,6 +981,7 @@ mod test {
let e = de
.decode(&mut Cursor::new(&mut buf), |h| {
res.push(h);
ControlFlow::Continue(())
})
.unwrap_err();
// decode error because the header value is partial
Expand All @@ -979,6 +991,7 @@ mod test {
buf.extend(&value[1..]);
de.decode(&mut Cursor::new(&mut buf), |h| {
res.push(h);
ControlFlow::Continue(())
})
.unwrap();

Expand Down
3 changes: 3 additions & 0 deletions src/hpack/test/fixture.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use serde_json::Value;
use std::fs::File;
use std::io::prelude::*;
use std::io::Cursor;
use std::ops::ControlFlow;
use std::path::Path;
use std::str;

Expand Down Expand Up @@ -78,6 +79,7 @@ fn test_story(story: Value) {
let (name, value) = expect.remove(0);
assert_eq!(name, key_str(&e));
assert_eq!(value, value_str(&e));
ControlFlow::Continue(())
})
.unwrap();

Expand Down Expand Up @@ -112,6 +114,7 @@ fn test_story(story: Value) {
decoder
.decode(&mut Cursor::new(&mut buf), |e| {
assert_eq!(e, input.remove(0).reify().unwrap());
ControlFlow::Continue(())
})
.unwrap();

Expand Down
2 changes: 2 additions & 0 deletions src/hpack/test/fuzz.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use rand::rngs::StdRng;
use rand::{thread_rng, Rng, SeedableRng};

use std::io::Cursor;
use std::ops::ControlFlow;

const MAX_CHUNK: usize = 2 * 1024;

Expand Down Expand Up @@ -170,6 +171,7 @@ impl FuzzHpack {
.decode(&mut Cursor::new(&mut buf), |h| {
let e = expect.remove(0);
assert_eq!(h, e);
ControlFlow::Continue(())
})
.expect("full decode");
}
Expand Down
12 changes: 9 additions & 3 deletions src/proto/connection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -451,11 +451,17 @@ where
Ok(())
}
// Attempting to read a frame resulted in a stream level error.
// This is handled by resetting the frame then trying to read
// another frame.
// Locally detected stream errors are reported to the peer with
// RST_STREAM. Remotely initiated resets have already been applied
// by the streams state machine and must not be echoed back.
Err(Error::Reset(id, reason, initiator)) => {
if initiator == Initiator::Remote {
tracing::trace!(?id, ?reason, ?initiator, "stream reset");
return Ok(());
}

debug_assert_eq!(initiator, Initiator::Library);
tracing::trace!(?id, ?reason, "stream error");
tracing::trace!(?id, ?reason, ?initiator, "stream error");
match self.streams.send_reset(id, reason) {
Ok(()) => (),
Err(crate::proto::error::GoAway { debug_data, reason }) => {
Expand Down
Loading