Skip to content

Commit 08129b2

Browse files
authored
fix: Flow control capacity leak with padded frames (#894)
1 parent 1e68f99 commit 08129b2

2 files changed

Lines changed: 144 additions & 2 deletions

File tree

src/proto/streams/streams.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -561,7 +561,7 @@ impl Inner {
561561
if self.actions.may_have_forgotten_stream(peer, id) {
562562
tracing::debug!("recv_data for old stream={:?}, sending STREAM_CLOSED", id,);
563563

564-
let sz = frame.payload().len();
564+
let sz = frame.flow_controlled_len();
565565
// This should have been enforced at the codec::FramedRead layer, so
566566
// this is just a sanity check.
567567
assert!(sz <= super::MAX_WINDOW_SIZE as usize);
@@ -581,7 +581,7 @@ impl Inner {
581581
let send_buffer = &mut *send_buffer;
582582

583583
self.counts.transition(stream, |counts, stream| {
584-
let sz = frame.payload().len();
584+
let sz = frame.flow_controlled_len();
585585
let res = actions.recv.recv_data(frame, stream);
586586

587587
// Any stream error after receiving a DATA frame means

tests/h2-tests/tests/flow_control.rs

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -446,6 +446,148 @@ async fn stream_error_release_connection_capacity() {
446446
join(srv, client).await;
447447
}
448448

449+
// Regression test for TODO
450+
#[tokio::test]
451+
async fn padded_data_stream_error_releases_connection_capacity() {
452+
h2_support::trace_init!();
453+
let (io, mut srv) = mock::new();
454+
455+
// Padded EOS frame: 1 byte pad_len + 8 bytes data + 1 byte padding.
456+
// flow_controlled_len = 10, payload (data only) = 8.
457+
let mut padded_eos = vec![0u8; 10];
458+
padded_eos[0] = 1;
459+
460+
let srv = async move {
461+
let settings = srv.assert_client_handshake().await;
462+
assert_default_settings!(settings);
463+
srv.recv_frame(
464+
frames::headers(1)
465+
.request("GET", "https://http2.akamai.com/")
466+
.eos(),
467+
)
468+
.await;
469+
// Wrong content-length triggers a stream error on the padded EOS frame.
470+
srv.send_frame(
471+
frames::headers(1)
472+
.response(200)
473+
.field("content-length", &*(16_384 * 3).to_string()),
474+
)
475+
.await;
476+
srv.send_frame(frames::data(1, vec![0; 16_384])).await;
477+
srv.send_frame(frames::data(1, vec![0; 16_384])).await;
478+
srv.send_frame(frames::data(1, &padded_eos[..]).padded().eos())
479+
.await;
480+
srv.recv_frame(frames::reset(1).protocol_error()).await;
481+
// Released capacity must include the padded frame's full
482+
// flow_controlled_len (10), not just its payload (8).
483+
srv.recv_frame(frames::window_update(0, 16_384 * 2 + 10))
484+
.await;
485+
};
486+
487+
let client = async move {
488+
let (mut client, mut conn) = client::handshake(io).await.unwrap();
489+
let request = Request::builder()
490+
.uri("https://http2.akamai.com/")
491+
.body(())
492+
.unwrap();
493+
494+
let req = async {
495+
let resp = client
496+
.send_request(request, true)
497+
.unwrap()
498+
.0
499+
.await
500+
.expect("response");
501+
assert_eq!(resp.status(), StatusCode::OK);
502+
let mut body = resp.into_parts().1;
503+
let mut cap = body.flow_control().clone();
504+
let to_release = 16_384 * 2;
505+
let mut should_recv_bytes = to_release;
506+
let mut should_recv_frames = 2usize;
507+
508+
let err = body
509+
.try_for_each(|bytes| async move {
510+
should_recv_bytes -= bytes.len();
511+
should_recv_frames -= 1;
512+
if should_recv_bytes == 0 {
513+
assert_eq!(should_recv_frames, 0);
514+
}
515+
Ok(())
516+
})
517+
.await
518+
.expect_err("body");
519+
assert_eq!(
520+
err.to_string(),
521+
"stream error detected: unspecific protocol error detected"
522+
);
523+
cap.release_capacity(to_release).expect("release_capacity");
524+
};
525+
conn.drive(req).await;
526+
conn.await.expect("client");
527+
};
528+
529+
join(srv, client).await;
530+
}
531+
532+
// Regression test for TODO
533+
#[tokio::test]
534+
async fn padded_data_on_forgotten_stream_releases_connection_capacity() {
535+
h2_support::trace_init!();
536+
let (io, mut srv) = mock::new();
537+
538+
// Padded frame: 1 byte pad_len + 16378 bytes data + 5 bytes padding.
539+
// flow_controlled_len = 16384, payload (data only) = 16378.
540+
let mut padded = vec![0u8; 16_384];
541+
padded[0] = 5;
542+
543+
let srv = async move {
544+
let settings = srv.assert_client_handshake().await;
545+
assert_default_settings!(settings);
546+
srv.recv_frame(
547+
frames::headers(1)
548+
.request("GET", "https://example.com/")
549+
.eos(),
550+
)
551+
.await;
552+
srv.send_frame(frames::headers(1).response(200)).await;
553+
srv.send_frame(frames::data(1, vec![0; 16_384])).await;
554+
srv.recv_frame(frames::reset(1).cancel()).await;
555+
// Wait for the reset to expire so the stream is forgotten.
556+
idle_ms(50).await;
557+
srv.ping_pong([1; 8]).await;
558+
// Stream 1 has been evicted. Send a padded DATA frame for it.
559+
srv.send_frame(frames::data(1, &padded[..]).padded().eos())
560+
.await;
561+
// Released capacity must cover both frames using their full
562+
// flow_controlled_len. There used to be a bug where the padded
563+
// frame would release only 16378 (payload) instead of 16384.
564+
srv.recv_frame(frames::window_update(0, 16_384 * 2)).await;
565+
srv.recv_frame(frames::reset(1).stream_closed()).await;
566+
};
567+
568+
let client = async move {
569+
let (mut client, conn) = client::Builder::new()
570+
.reset_stream_duration(Duration::from_millis(10))
571+
.handshake::<_, Bytes>(io)
572+
.await
573+
.expect("handshake");
574+
575+
let req = async {
576+
let resp = client.get("https://example.com/").await.expect("response");
577+
assert_eq!(resp.status(), StatusCode::OK);
578+
// This drop sends RST_STREAM(CANCEL)
579+
drop(resp);
580+
};
581+
582+
let mut conn = Box::pin(async move { conn.await.expect("client") });
583+
conn.drive(req).await;
584+
conn.await;
585+
drop(client);
586+
};
587+
588+
join(srv, client).await;
589+
}
590+
449591
#[tokio::test]
450592
async fn stream_close_by_data_frame_releases_capacity() {
451593
h2_support::trace_init!();

0 commit comments

Comments
 (0)