fix(proto): do not coalesce into a too small datagram tail - #792
fix(proto): do not coalesce into a too small datagram tail#792HeikoBornholdt wants to merge 6 commits into
Conversation
92e80dc to
310c02d
Compare
## Description Fixes n0-computer#791. The CONNECTION_CLOSE branch passes its datagram to the next packet number space without checking that there is room for another packet. With `initial_mtu` above 1200 the Initial CONNECTION_CLOSE packet, padded to 1200, leaves a tail behind, and when that tail is under `MIN_PACKET_SPACE` the Handshake space writes into it anyway. In a release build, where the assert is gone, it goes ahead and does that. At `min_mtu` and `initial_mtu` of 1250, `poll_transmit` hands back `size = 1282, segment_size = 1250`. The Handshake packet starts at byte 1200 and ends at 1282, so it runs 32 bytes past the end of the datagram it went into. GSO then sends two datagrams, 1250 and 32 bytes, and the Handshake CONNECTION_CLOSE packet is cut in half between them. Neither half can be decrypted, so the other side drops both, and those last 32 bytes go out as their own useless datagram. My connection still closed, but only because the Initial copy of the CONNECTION_CLOSE is intact in the first datagram. For the fix I did not add a second size check to the CONNECTION_CLOSE branch. Instead I made the loop keep the promise the assert makes: if what is left of the datagram cannot hold a packet, finish the datagram, so the code below starts a new one for the next packet. `TransmitBuf::finish_datagram` does that. For the first datagram of a batch it is `clip_segment_size`; a later one ends up shorter than the segment size, so the batch has to end there and `max_datagrams` is capped. It only does anything in the cases the assert rejects today, so anything that passes the assert works exactly as before. ## Breaking Changes None 🤞. ## Change checklist <!-- Remove any that are not relevant. --> - [x] Self-review. - [x] Tests if relevant. - [x] This PR was created by a human that thought critically about the proposed change and wrote an as clear and concise description as they could. - [x] This PR isn't slop, and is carefully crafted to do have the intented effect. - [x] `cargo make` passes locally.
310c02d to
ca6c16b
Compare
flub
left a comment
There was a problem hiding this comment.
Thanks for the great bug report! Interesting to see you hit this edge case, and apologies for leaving it unresolved with just a TODO. There's a bunch more improvments we need to make to this code!
I think your bug report points to the padding being wrong after the CONNECTION_CLOSE was written in the fist space.
When PacketBuilder::finish_and_track is called with PadDatagram::ToMinMtu it should not use MIN_INITIAL_SIZE, but should rather use the configured minimum MTU. Possibly all uses of MIN_INITIAL_SIZE are wrong? Anyway, in this case maybe the TransmitBuf can gain a field to know what the min_mtu is so that it can do the padding correctly?
Secondly as you say the check for whether enough space is remaining to coalesce is missing in the connection-close branch. I'd expect to check this before the builder.finish_and_track call on line 1704 so that it passes in the correct amount of padding to builder.finish_and_track (i.e. 0 or min_mtu).
I think those two changes together would also fix the issue you experience? Does this make sense?
The test raised `min_mtu` together with `initial_mtu`. Those are the only sizes where the tail after the Initial CONNECTION_CLOSE depends on which of the two the padding follows, so a fix that pads to `min_mtu` rather than `MIN_INITIAL_SIZE` would fill the datagram and the test would pass without the coalescing check. Pin `min_mtu` to the QUIC minimum instead. The tail is then there in either case: it is the difference between the datagram size and whatever the packet was padded to, and the datagram size is the path MTU. It is set explicitly rather than left at the default so that a change to that default cannot silently change what the test covers.
The comment above the assertion and the TODO above the CONNECTION_CLOSE branch both still describe the state before the check existed. Point them at the loop, and leave the parts of the TODO that are still open.
|
Thank you! Also for taking a look. I tried the padding change and I don't think it closes the hole. With It did show up a weakness in my test though, which had both values at 1250. I've changed it to The other uses of On the space check, agreed. I put it at the top of the space loop rather than before I also shortened your TODO above the branch to the parts that are still open, easy to put back. |
flub
left a comment
There was a problem hiding this comment.
Hum yes, my comment about padding doesn't make much sense. Also good catch on min_mtu vs initial_mtu.
IIUC when this condition hits there will be a CONNECTION_CLOSE frame missing in one of the higher spaces? That is still a remaining bug, but a lot less harmful. It's fine to not address in this PR, small steps are great, I only want to understand the implications.
73bf30b to
2cd3cff
Compare
2cd3cff to
630df68
Compare
|
You're right, |
Description
Fixes #791.
The CONNECTION_CLOSE branch passes its datagram to the next packet number space without checking that there is room for another packet. With
initial_mtuabove 1200 the Initial CONNECTION_CLOSE packet, padded to 1200, leaves a tail behind, and when that tail is underMIN_PACKET_SPACEthe Handshake space writes into it anyway.In a release build, where the assert is gone, it goes ahead and does that. At
min_mtuandinitial_mtuof 1250,poll_transmithands backsize = 1282, segment_size = 1250. The Handshake packet starts at byte 1200 and ends at 1282, so it runs 32 bytes past the end of the datagram it went into. GSO then sends two datagrams, 1250 and 32 bytes, and the Handshake CONNECTION_CLOSE packet is cut in half between them. Neither half can be decrypted, so the other side drops both, and those last 32 bytes go out as their own useless datagram. My connection still closed, but only because the Initial copy of the CONNECTION_CLOSE is intact in the first datagram.For the fix I did not add a second size check to the CONNECTION_CLOSE branch. Instead I made the loop keep the promise the assert makes: if what is left of the datagram cannot hold a packet, finish the datagram, so the code below starts a new one for the next packet.
TransmitBuf::finish_datagramdoes that. For the first datagram of a batch it isclip_segment_size; a later one ends up shorter than the segment size, so the batch has to end there andmax_datagramsis capped.It only does anything in the cases the assert rejects today, so anything that passes the assert works exactly as before.
Breaking Changes
None 🤞.
Change checklist
proposed change and wrote an as clear and concise description as
they could.
intented effect.
cargo makepasses locally.