-
Notifications
You must be signed in to change notification settings - Fork 997
fix: do not send zero-length non-FIN stream frames #2492
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -499,7 +499,7 @@ const MAX_PROBING_TIMEOUTS: usize = 3; | |
| const DEFAULT_INITIAL_CONGESTION_WINDOW_PACKETS: usize = 10; | ||
|
|
||
| // The maximum data offset that can be stored in a crypto stream. | ||
| const MAX_CRYPTO_STREAM_OFFSET: u64 = 1 << 16; | ||
| pub(crate) const MAX_CRYPTO_STREAM_OFFSET: u64 = 1 << 16; | ||
|
|
||
| // The send capacity factor. | ||
| const TX_CAP_FACTOR: f64 = 1.0; | ||
|
|
@@ -5199,7 +5199,6 @@ impl<F: BufFactory> Connection<F> { | |
|
|
||
| // Create a single STREAM frame for the first stream that is flushable. | ||
| if (pkt_type == Type::Short || pkt_type == Type::ZeroRTT) && | ||
| left > frame::MAX_STREAM_OVERHEAD && | ||
| !is_closing && | ||
| path.active() && | ||
| !dgram_emitted | ||
|
|
@@ -5239,13 +5238,16 @@ impl<F: BufFactory> Connection<F> { | |
| octets::varint_len(stream_off) + // offset | ||
| 2; // length, always encode as 2-byte varint | ||
|
|
||
| let max_len = match left.checked_sub(hdr_len) { | ||
| Some(v) => v, | ||
| // Require at least 1 byte of payload beyond the header. | ||
| // If even the minimum payload (hdr_len + 1 byte) doesn't | ||
| // fit, stop trying. We could continue to the next stream | ||
| // (a lower stream_id or offset might encode in fewer varint | ||
| // bytes), but we'd need a different iterator, since | ||
| // peek_flushable() returns the same stream on every iteration. | ||
| let max_len = match left.checked_sub(hdr_len + 1) { | ||
| Some(v) => v + 1, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Technically the +1 is only needed in cases where the stream has at least 1 byte to send. The case of stream FIN with no data could be transmitted with v == 0.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. +1 I think we need a little more nuance here |
||
| None => { | ||
| let priority_key = Arc::clone(&stream.priority_key); | ||
| self.streams.remove_flushable(&priority_key); | ||
|
|
||
| continue; | ||
| break; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. An implication of break instead of continue is that other streams with lower stream id or offset could have written a tiny frame but now they won't. I need to look into the implications of the The check for I think with the lower MIN this branch will be easier to test correct. This is a very good thing.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. FYI, I was actually wondering if should remove the MIN_STREAM_OVERHEAD guard altogether and just rely on this check here. As soon as a stream has offset>64 we're past the guard anyways, so I don't think it really does much and just adds clutter / complexity.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have no objections to removal of the MIN_STREAM_OVERHEAD guard.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we want to continue instead of break, we'd just need to add a different iterator for flushable. I think many of the other
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm fine with the break.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am confused by the
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh I see, you removed the call to
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this change in behaviour needs some more consideration, the break risks a high-priority stream blocking forward progress on others and introducing tail latency. Worst case, a certain combo could completely stall out all other streams. I think I'd be more comfortable with having a min guard (to prevent looping through the whole set when we really tand no hope of sending anything) and trying to walk the entire flushable queue. We could avoid needing a new fluhable iterator type if we store the removed flushable values and reinsert them after we're done sending. But if its easy enough to add a new iterator that would be ok too.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this is still a net improvement. The vast majority of cases / streams would have never hit this The impact on lower priority streams should be minimal, I think. In the best case, another stream could have fit That said, adding the iterator is pretty straightforward and it's certainly a further improvement.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes the new approach does benefit from supporting smaller stream frames. I think the old bug had an upside. If you have a huge STREAM frame to send and Adding an iterator lets us have the best of both worlds |
||
| }, | ||
| }; | ||
|
|
||
|
|
@@ -5256,6 +5258,14 @@ impl<F: BufFactory> Connection<F> { | |
| let (len, fin) = | ||
| stream.send.emit(&mut stream_payload.as_mut()[..max_len])?; | ||
|
|
||
| // The stream was flushable and max_len ≥ 1, so emit() must | ||
| // have produced at least one byte, or set FIN. While a zero | ||
| // length non-fin STREAM frame is legal, it's silly. | ||
| debug_assert!( | ||
| len > 0 || fin, | ||
| "emit() from flushable stream produced 0 bytes without FIN" | ||
| ); | ||
|
|
||
| // Encode the frame's header. | ||
| // | ||
| // Due to how `OctetsMut::split_at()` works, `stream_hdr` starts | ||
|
|
@@ -5301,10 +5311,9 @@ impl<F: BufFactory> Connection<F> { | |
|
|
||
| #[cfg(feature = "fuzzing")] | ||
| // Coalesce STREAM frames when fuzzing. | ||
| if left > frame::MAX_STREAM_OVERHEAD { | ||
| continue; | ||
| } | ||
| continue; | ||
|
|
||
| #[cfg(not(feature = "fuzzing"))] | ||
| break; | ||
| } | ||
| } | ||
|
|
@@ -5335,14 +5344,15 @@ impl<F: BufFactory> Connection<F> { | |
| path.recovery.ping_sent(epoch); | ||
| } | ||
|
|
||
| if !has_data && | ||
| !dgram_emitted && | ||
|
gregor-cf marked this conversation as resolved.
|
||
| cwnd_available > frame::MAX_STREAM_OVERHEAD | ||
| { | ||
| // TODO: 12 is the old MAX_STREAM_OVERHEAD value. See | ||
| // https://github.com/cloudflare/quiche/pull/2453 for more | ||
| // comprehensive app-limited changes/fixes. | ||
| if !has_data && !dgram_emitted && cwnd_available > 12 { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we discovered that a more correct number is 20. But I'm fine keeping it as it for consistency. |
||
| path.recovery.on_app_limited(); | ||
| } | ||
|
|
||
| if frames.is_empty() { | ||
| // Used by legacy recovery. | ||
| // When we reach this point we are not able to write more, so set | ||
| // app_limited to false. | ||
| path.recovery.update_app_limited(false); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -297,7 +297,9 @@ impl<F: BufFactory> SendBuf<F> { | |
| // report final_size | ||
| self.emit_off = cmp::max(self.emit_off, next_off); | ||
|
|
||
| Ok((out.len() - out_len, fin)) | ||
| let written = out.len() - out_len; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what does this change do?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks like a functional no-op |
||
|
|
||
| Ok((written, fin)) | ||
| } | ||
|
|
||
| /// Updates the max_data limit to the given value. | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.