Skip to content
Open
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
42 changes: 38 additions & 4 deletions h3/src/qpack/decoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ impl Decoder {
return Err(DecoderError::MissingRefs(required_ref));
}

let decoder_table = self.table.decoder(base);
let decoder_table = self.table.decoder(base, required_ref);

let mut mem_size = 0;
let mut fields = Vec::new();
Expand Down Expand Up @@ -375,7 +375,7 @@ mod tests {
assert!(decoder.on_encoder_recv(&mut enc, &mut dec).is_ok());

assert_eq!(
decoder.table.decoder(1).get_relative(0),
decoder.table.decoder(1, 1).get_relative(0),
Ok(&StaticTable::get(1).unwrap().with_value("serial value"))
);

Expand Down Expand Up @@ -443,7 +443,7 @@ mod tests {
assert!(decoder.on_encoder_recv(&mut enc, &mut dec).is_ok());

assert_eq!(
decoder.table.decoder(1).get_relative(0),
decoder.table.decoder(1, 1).get_relative(0),
Ok(&HeaderField::new("key", "value"))
);

Expand Down Expand Up @@ -636,6 +636,35 @@ mod tests {
assert_eq!(fields, &[field(2), field(3), field(4)])
}

#[test]
fn repro_post_base_overflow() {
// Peer-controlled: Required Insert Count = 1, Base = RIC + Delta Base ~ 2^63,
// post-base index ~ 2^63 -> base + index overflows usize in VirtualAddressSpace::post_base.
let base = 1 + (1usize << 63) + 126;
let mut buf = vec![];
HeaderPrefix::new(1, base, 4, TABLE_SIZE).encode(&mut buf);
IndexedWithPostBase((1usize << 63) + 14).encode(&mut buf);

let mut read = Cursor::new(&buf);
let decoder = Decoder::from(build_table_with_size(4));
let res = decoder.decode_header(&mut read);
assert!(res.is_err(), "expected an error, got {res:?}");
}

#[test]
fn repro_post_base_above_required_insert_count() {
// RFC 9204 4.5.1: a reference with absolute index >= Required Insert Count
// MUST be treated as QPACK_DECOMPRESSION_FAILED. RIC = 2, Base = 2, post-base 0 -> abs 3.
let mut buf = vec![];
HeaderPrefix::new(2, 2, 4, TABLE_SIZE).encode(&mut buf);
IndexedWithPostBase(0).encode(&mut buf);

let mut read = Cursor::new(&buf);
let decoder = Decoder::from(build_table_with_size(4));
let res = decoder.decode_header(&mut read);
assert!(res.is_err(), "expected an error, got {res:?}");
}

#[test]
fn decode_name_ref_header_field() {
let mut buf = vec![];
Expand Down Expand Up @@ -665,7 +694,12 @@ mod tests {
#[test]
fn decode_post_base_name_ref_header_field() {
let mut buf = vec![];
HeaderPrefix::new(2, 2, 4, TABLE_SIZE).encode(&mut buf);
// Required Insert Count must cover the post-base reference's absolute index
// (base 2, post-base 0 -> entry foo3). The previous value (2) declared fewer
// entries than the block referenced, which RFC 9204 4.5.1 forbids; it only
// decoded because the post-base bounds check used the table's total inserted
// count instead of the Required Insert Count.
HeaderPrefix::new(4, 2, 4, TABLE_SIZE).encode(&mut buf);
LiteralWithPostBaseNameRef::new(0, "new bar3")
.encode(&mut buf)
.unwrap();
Expand Down
19 changes: 15 additions & 4 deletions h3/src/qpack/dynamic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,19 +28,26 @@ pub enum Error {
pub struct DynamicTableDecoder<'a> {
table: &'a DynamicTable,
base: usize,
required_ref: usize,
}

impl<'a> DynamicTableDecoder<'a> {
pub(super) fn get_relative(&self, index: usize) -> Result<&HeaderField, Error> {
let real_index = self.table.vas.relative_base(self.base, index)?;
let real_index = self
.table
.vas
.relative_base(self.base, index, self.required_ref)?;
self.table
.fields
.get(real_index)
.ok_or(Error::BadIndex(real_index))
}

pub(super) fn get_postbase(&self, index: usize) -> Result<&HeaderField, Error> {
let real_index = self.table.vas.post_base(self.base, index)?;
let real_index = self
.table
.vas
.post_base(self.base, index, self.required_ref)?;
self.table
.fields
.get(real_index)
Expand Down Expand Up @@ -258,8 +265,12 @@ impl DynamicTable {
DynamicTable::default()
}

pub fn decoder(&self, base: usize) -> DynamicTableDecoder<'_> {
DynamicTableDecoder { table: self, base }
pub fn decoder(&self, base: usize, required_ref: usize) -> DynamicTableDecoder<'_> {
DynamicTableDecoder {
table: self,
base,
required_ref,
}
}

pub fn encoder(&mut self, stream_id: u64) -> DynamicTableEncoder<'_> {
Expand Down
48 changes: 35 additions & 13 deletions h3/src/qpack/vas.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,19 +109,41 @@ impl VirtualAddressSpace {
index != 0 && index <= self.dropped
}

pub fn relative_base(&self, base: usize, index: RelativeIndex) -> Result<usize, Error> {
if self.delta == 0 || index > base || base - index <= self.dropped {
// `required` is the Required Insert Count of the header block. Per RFC 9204 §4.5.1,
// a reference to a dynamic entry whose absolute index is beyond the entries covered by
// the Required Insert Count must be rejected (QPACK_DECOMPRESSION_FAILED), so the
// absolute index (`base - index` here, 1-based) is bounded by `required`, not by the
// table's current `inserted` count.
pub fn relative_base(
&self,
base: usize,
index: RelativeIndex,
required: usize,
) -> Result<usize, Error> {
if self.delta == 0
|| index > base
|| base - index <= self.dropped
|| base - index > required
{
Err(Error::RelativeIndex(index))
} else {
Ok(base - self.dropped - index - 1)
}
}

pub fn post_base(&self, base: usize, index: RelativeIndex) -> Result<usize, Error> {
if self.delta == 0 || base + index >= self.inserted || base + index < self.dropped {
Err(Error::PostbaseIndex(index))
} else {
Ok(base + index - self.dropped)
pub fn post_base(
&self,
base: usize,
index: RelativeIndex,
required: usize,
) -> Result<usize, Error> {
// `base + index` is attacker-controlled and can overflow `usize`, hence `checked_add`.
// The absolute index must be below the Required Insert Count (RFC 9204 §4.5.1).
match base.checked_add(index) {
Some(abs) if self.delta != 0 && abs >= self.dropped && abs < required => {
Ok(abs - self.dropped)
}
_ => Err(Error::PostbaseIndex(index)),
}
}

Expand Down Expand Up @@ -150,7 +172,7 @@ mod tests {
#[test]
fn test_no_relative_index_when_empty() {
let vas = VirtualAddressSpace::default();
let res = vas.relative_base(0, 0);
let res = vas.relative_base(0, 0, 0);
assert_eq!(res, Err(Error::RelativeIndex(0)));
}

Expand All @@ -170,7 +192,7 @@ mod tests {
vas.add();
(1..*count).for_each(|_| { vas.add(); });

assert_eq!(vas.relative_base(*count, count - 1), Ok(0), "{:?}", vas);
assert_eq!(vas.relative_base(*count, count - 1, *count), Ok(0), "{:?}", vas);
}

#[test]
Expand All @@ -182,7 +204,7 @@ mod tests {
(1..*count).for_each(|_| { vas.add(); });
(0..*count - 1).for_each(|_| vas.drop());

assert_eq!(vas.relative_base(*count, count - 1), Err(Error::RelativeIndex(count - 1)), "{:?}", vas);
assert_eq!(vas.relative_base(*count, count - 1, *count), Err(Error::RelativeIndex(count - 1)), "{:?}", vas);
}

#[test]
Expand All @@ -193,7 +215,7 @@ mod tests {
(1..*count).for_each(|_| { vas.add(); });
vas.add();

assert_eq!(vas.relative_base(*count, 0), Ok(count -1),
assert_eq!(vas.relative_base(*count, 0, *count), Ok(count -1),
"{:?}", vas);
}

Expand All @@ -206,7 +228,7 @@ mod tests {
vas.add();
(0..*count - 1).for_each(|_| { vas.drop(); });

assert_eq!(vas.relative_base(*count, 0), Ok(0),
assert_eq!(vas.relative_base(*count, 0, *count), Ok(0),
"{:?}", vas);
}
}
Expand All @@ -228,7 +250,7 @@ mod tests {
vas.add();
});

assert_eq!(vas.post_base(4, 1), Ok(5));
assert_eq!(vas.post_base(4, 1, 7), Ok(5));
}

#[test]
Expand Down