QPACK: use || in the varint decode range guards - #13655
Conversation
Ten guards in QPACK.cc read `xpack_decode_integer(...) < 0 && value > 0xFFFF`, so neither condition rejects anything: a decode failure falls through, and an oversized varint is silently narrowed into the surrounding uint16_t. The delta_base_index guard also had its comparison inverted. The unchecked failure matters more than the truncation. On failure xpack_decode_integer returns -1, and callers then do `read_len += ret`, giving SIZE_MAX. IOBufferReader::consume() takes that as -1, its release-assert passes because is_read_avail_more_than(-2) is true, and start_offset moves backwards. The helper returns 0 rather than a negative, so _on_encoder_stream_read_ready() does not abort and its `while (is_read_avail_more_than(0))` loop re-reads the same byte. Flip the ten operators and correct the inverted comparison. The value guard in _read_insert_with_name_ref also has its bound raised from 0xFF to 0xFFFF, matching the other nine sites. This is required rather than cosmetic: value_len is a size_t, so 0xFF bounds nothing, and under || a 0xFF bound would reject every header value longer than 255 bytes. At 0xFFFF the check is unreachable, since xpack_decode_string is already capped by _header_field_max_size.
There was a problem hiding this comment.
Warning
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
Pull request overview
Fixes QPACK varint range-guard logic to correctly reject decode errors and oversized values (preventing silent truncation into uint16_t), and adds a regression test for an oversized Required Insert Count.
Changes:
- Replace
&&with||in multiple QPACK decode range guards so either a decode failure or an out-of-range value is rejected. - Fix an inverted comparison in
_decode_header()fordelta_base_index. - Add a unit test that drives
QPACK::decode()with an oversized varint and asserts decode failure.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| src/proxy/http3/QPACK.cc | Corrects guard conditions to reject decode failures and out-of-range varints (and fixes one inverted bound check). |
| src/proxy/http3/test/test_QPACK.cc | Adds a regression test ensuring oversized Required Insert Count is rejected rather than truncated. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
bneradt
left a comment
There was a problem hiding this comment.
No actionable findings in this diff. Reviewed all ten guards against the XPACK decoder return conventions and the surrounding destination types, including the corrected delta-base comparison and the value-length bound. The new regression test exercises the synchronous oversized Required Insert Count rejection. This was a source review; I did not run a local build or the QPACK tests. The current PR checks are green.
Ten guards in
src/proxy/http3/QPACK.ccare writtenWith
&&, neither condition rejects on its own: a decode failure falls throughbecause the value check does not hold, and an in-range failure-free decode of an
oversized varint falls through because the error check does not hold. The value
is then narrowed into the surrounding
uint16_t. Thedelta_base_indexguard in_decode_header()additionally had its comparison inverted(
delta_base_index < 0xFFFFwhere>was meant).Lines: 287, 925, 934, 1517, 1524, 1548, 1555, 1578, 1600, 1622.
One bound change, and why it is required
The value guard in
_read_insert_with_name_ref()(line 1525 on master) is theonly one of the ten bounded at
0xFF; the other nine use0xFFFF. This raisesit to
0xFFFFas well, and that is load bearing rather than tidying:value_lenis asize_t &, so0xFFis not protecting against any narrowing --it is a bare sanity limit on a decoded header value length. Under
&&it neverfired, so it did not matter. Under
||it fires, and a0xFFbound would abortthe decode for any header value longer than 255 bytes -- ordinary cookies and
tokens included. At
0xFFFFthe check is unreachable, becausexpack_decode_string()is already capped by_header_field_max_size(32768 bydefault), which matches the behaviour of the other nine sites.
Flipping that operator without raising the bound would be a regression, so the
two changes belong together.
The unchecked failure is the more interesting half
On failure
xpack_decode_integer()returns -1. Callers then doread_len += ret, andread_lenissize_t, so it becomesSIZE_MAX.IOBufferReader::consume()receives that asint64_t-1:is_read_avail_more_than(-2)is true, so the release-assert passes andstart_offsetmoves backwards by one. The helper returns 0 rather than anegative, so
_on_encoder_stream_read_ready()does not call_abort_decode(),and its
while (reader.is_read_avail_more_than(0))loop reads the same byteagain on the next iteration.
_read_duplicate()(line 1578) has exactly thisshape.
Widening the destination types would address the truncation but not this half --
the
< 0check is what is missing.Relationship to #13621
#13621 also touches
_read_insert_with_name_ref()(line 1517), widening itsindexparameter touint64_tso the narrowing disappears rather than beingrejected. That is a better fix for that one site.
This change flips the operator there too, so the ten sites are corrected
consistently and none is left with a known-broken guard while #13621 is in
review. If #13621 lands afterwards its approach supersedes the operator flip at
that line -- a one-line resolution. Line 1524, the value guard in the same
function, is not covered by #13621 at all.
Test
Adds
decode() rejects oversized Required Insert Count at entrytotest_QPACK.cc, tagged[qpack-decode-entry-bounds]. It drivesQPACK::decode()with a 4-byte 8-bit-prefix varint encoding0x10000andasserts the decoder reports failure rather than accepting it.
Confirmed to be a regression test: with the guards restored to
&&it fails onboth assertions with
sync_ret := 0andhandler->last_event() := 2700(QPACK_EVENT_DECODE_COMPLETE) -- the oversizedcount is accepted and truncated to 0. With the change it passes, 3 assertions.
Also run, 5/5 pass:
h3_proxy_verifier,h3_python_client,h3_stream_lifetime,h3_flow_control,h3_sni_check.The remaining eight guards have no direct test. Reaching
_decode_header()requires getting past the
QPACK::decode()check this test exercises, and theencoder-stream helpers need a QPACK encoder stream driven with crafted
instructions, which the current test harness does not set up.