Summary
Bbr3::inflight_at_loss performs an unchecked u64 subtraction that underflows when the loss already exceeds the loss threshold at the previous packet. In a debug build (overflow checks on) this panics and takes down the connection's runtime worker; in release it silently wraps to a huge u64, producing a garbage inflight_at_loss and, downstream, a corrupted inflight/cwnd estimate.
Panic
thread 'tokio-rt-worker' panicked at noq-proto-1.1.0/src/congestion/bbr3/mod.rs:626:33:
attempt to subtract with overflow
Location and cause
noq-proto/src/congestion/bbr3/mod.rs (line 626 on main today, identical in the published 1.1.0):
fn inflight_at_loss(&mut self, packet_size: u64) -> u64 {
if let Some(rate_sample) = self.rs {
let inflight_prev = rate_sample.tx_in_flight.saturating_sub(packet_size);
let inflight_prev_threshold = LOSS_THRESH * inflight_prev as f64;
let lost_prev = rate_sample.lost.saturating_sub(packet_size);
let compared_loss = (inflight_prev_threshold.round() as u64) - lost_prev; // <-- underflows
let lost_prefix = compared_loss as f64 / (1.0 - LOSS_THRESH);
let inflight_at_loss = inflight_prev + lost_prefix as u64;
return inflight_at_loss;
}
0
}
Every neighboring term uses saturating_sub, but compared_loss uses a plain -. When lost_prev > LOSS_THRESH * inflight_prev (i.e. the bytes lost already exceed the loss-threshold fraction of the prior inflight), (inflight_prev_threshold.round() as u64) - lost_prev underflows.
This mirrors BBRInflightAtLoss() in draft-ietf-ccwg-bbr-05 §5.5.10.2, where the expression BBRLossThresh * inflight_prev - lost_prev is computed in real arithmetic and is legitimately allowed to go negative (yielding a lost_prefix < 0 and inflight < inflight_prev). Evaluating it in u64 loses that sign and underflows instead.
When it triggers
inflight_at_loss runs during loss handling, so any connection that declares loss can hit it. It reproduces readily under a bufferbloated / lossy bottleneck (a droptail queue overflowing counts as loss even with no configured random loss). A short, loss-free transfer will not hit it, which is why it only shows up under sustained load.
Reproduction context
noq-proto 1.1.0, debug build (overflow-checks = true).
- macOS aarch64.
- Hit via a QUIC server (a MoQ relay using the noq backend) with BBRv3 selected, serving a ~2 Mbit media flow across a
dummynet-shaped 3 Mbit link with a deep (~600 ms) droptail queue and ~30 ms RTT. The relay's tokio-rt-worker panicked and aborted a few seconds into sustained delivery.
Suggested fix
Minimal: clamp the subtraction.
let compared_loss = (inflight_prev_threshold.round() as u64).saturating_sub(lost_prev);
That floors inflight_at_loss at inflight_prev, which is a reasonable "loss already past threshold" result. If you want to match the draft's real-valued formula faithfully (allowing inflight_at_loss < inflight_prev), compute the numerator in f64/i64 and clamp the final result to >= 0 rather than clamping the intermediate.
Happy to send a PR for either approach.
(written by Opus 4.8)
Summary
Bbr3::inflight_at_lossperforms an uncheckedu64subtraction that underflows when the loss already exceeds the loss threshold at the previous packet. In a debug build (overflow checks on) this panics and takes down the connection's runtime worker; in release it silently wraps to a hugeu64, producing a garbageinflight_at_lossand, downstream, a corrupted inflight/cwnd estimate.Panic
Location and cause
noq-proto/src/congestion/bbr3/mod.rs(line 626 onmaintoday, identical in the published 1.1.0):Every neighboring term uses
saturating_sub, butcompared_lossuses a plain-. Whenlost_prev > LOSS_THRESH * inflight_prev(i.e. the bytes lost already exceed the loss-threshold fraction of the prior inflight),(inflight_prev_threshold.round() as u64) - lost_prevunderflows.This mirrors
BBRInflightAtLoss()in draft-ietf-ccwg-bbr-05 §5.5.10.2, where the expressionBBRLossThresh * inflight_prev - lost_previs computed in real arithmetic and is legitimately allowed to go negative (yielding alost_prefix < 0andinflight < inflight_prev). Evaluating it inu64loses that sign and underflows instead.When it triggers
inflight_at_lossruns during loss handling, so any connection that declares loss can hit it. It reproduces readily under a bufferbloated / lossy bottleneck (a droptail queue overflowing counts as loss even with no configured random loss). A short, loss-free transfer will not hit it, which is why it only shows up under sustained load.Reproduction context
noq-proto1.1.0, debug build (overflow-checks = true).dummynet-shaped 3 Mbit link with a deep (~600 ms) droptail queue and ~30 ms RTT. The relay'stokio-rt-workerpanicked and aborted a few seconds into sustained delivery.Suggested fix
Minimal: clamp the subtraction.
That floors
inflight_at_lossatinflight_prev, which is a reasonable "loss already past threshold" result. If you want to match the draft's real-valued formula faithfully (allowinginflight_at_loss < inflight_prev), compute the numerator inf64/i64and clamp the final result to>= 0rather than clamping the intermediate.Happy to send a PR for either approach.
(written by Opus 4.8)