QPACK: use || in the varint decode range guards - #13655
Open
brbzull0 wants to merge 1 commit into
Open
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.
Contributor
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.