proto: reject transport parameters with a mismatched length - #2832
Merged
Merged
Conversation
MaxFreedomPollard
requested review from
Ralith,
djc and
gretchenfrage
as code owners
September 6, 2026 20:40
djc
approved these changes
Sep 9, 2026
djc
left a comment
Member
There was a problem hiding this comment.
Thanks!
Please squash these commits.
And would be great if you're able to backport this to the 0.11.x branch, as well.
The decoding loop in TransportParameters::read reads each parameter's length field, but three of its arms never check that the value really occupies that many bytes. MaxDatagramFrameSize only rejects a length above 8, MinAckDelayDraft07 checks nothing, and PreferredAddress decodes a fixed number of bytes for a given connection ID length. Surplus bytes stay in the buffer and the next turn of the loop decodes them as if they were the next parameter. A peer can use that to smuggle in a parameter it never encoded. Sending max_datagram_frame_size with a length of 3 and the value bytes 00 0c 00 leaves 0c 00 behind, which is a complete disable_active_migration. Compare the declared length against the number of bytes consumed once, after the match, so the check covers every arm. Cover each of the three arms with its own test: read_length_mismatch, read_min_ack_delay_length_mismatch and read_preferred_address_length_mismatch each build a byte string for their case and expect Err(Error::Malformed).
MaxFreedomPollard
force-pushed
the
proto-tp-length-check
branch
from
September 10, 2026 13:10
4894d0c to
0ac469f
Compare
djc
approved these changes
Sep 10, 2026
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.
TransportParameters::readreads the length field of every transport parameter, but three arms of its decoding loop never check that the value actually takes up that many bytes. Inquinn-proto/src/transport_parameters.rson main,PreferredAddress(line 457) decodes a fixed number of bytes for a given connection ID length,MaxDatagramFrameSize(line 469) only rejects a length above 8, andMinAckDelayDraft07(line 479) checks nothing at all. Anything left over stays in the buffer, and the next turn of the loop decodes it as though it were the start of the next parameter. Every other arm does check, either directly or throughdecode_cid, and the macro arm rejectslen != value.size().The effect is that a peer can hide a parameter it never encoded. Send
max_datagram_frame_sizewith a declared length of 3 and the value bytes00 0c 00: the value decodes as the one-byte varint 0, and the trailing0c 00is a completedisable_active_migration. On main that byte string parses without error and comes back withmax_datagram_frame_size: Some(0)anddisable_active_migration: true. So the parameters quinn ends up acting on are not the ones the peer encoded.RFC 9000 section 18 says the Transport Parameter Length field "contains the length of the Transport Parameter Value field in bytes", and section 7.4 asks for TRANSPORT_PARAMETER_ERROR on a parameter with an invalid value, which is what
Error::Malformedmaps to here.Rather than patch the three arms one at a time, I record
r.remaining()before the match and compare it once afterwards. That covers all three, and any arm added later gets the check for free. The newread_length_mismatchtest builds a byte string for each of the three cases and expectsErr(Error::Malformed).Verification on macOS with rustc 1.95.0:
cargo test --locked -p quinn-protogives 303 passed, 0 failed, plus 3 doc-tests passing.cargo fmt --all -- --checkandcargo clippy --locked -p quinn-proto --all-targets -- -D warningsare both clean. To confirm the test is really testing the fix I backed the length check out and left the test in:read_length_mismatchthen fails on the first case, reporting the parsed parameters withdisable_active_migration: truewhere it expectedErr(Malformed).