Loading debian/changelog +24 −0 Original line number Diff line number Diff line libnetplus (20260505+9) unstable; urgency=medium * QUIC: fix connection-level flow control double-counting — _data_recv was incremented by the full STREAM frame length on every receive, including retransmissions and overlapping data. Over many streams (e.g. during long cluster imports) this inflated the counter and could trigger a spurious FLOW_CONTROL_ERROR that killed the connection mid-transfer. Fix: compute overlap with existing recv_ranges to determine new unique bytes; only count those for _data_recv and the connection-level limit check. * QUIC: fix sendStreamData(vector) silent data loss on congestion control stall — after 50 stalls the function silently broke out of the send loop, losing remaining data with no error to the caller. Fix: throw NetException so callers (e.g. paritypp store_stripe) know the stream write failed and can retry. * QUIC: fix unidirectional stream dispatch before data complete — uni-stream callbacks fired on recv_fin (FIN frame arrival) instead of recv_complete (all data contiguous from offset 0). If packets arrived out of order or needed retransmission, the callback received a buffer with zero-filled gaps. Fix: check recv_complete (matching the bidi path) and std::move the buffer to avoid copy + memory waste. -- Jan Koester <jan.koester@tuxist.de> Mon, 05 May 2026 12:00:00 +0200 libnetplus (20260504+8) unstable; urgency=medium * QUIC: fix PTO re-queue spin loop — when sendPacket() failed (EAGAIN), Loading src/quic.cpp +36 −9 Original line number Diff line number Diff line Loading @@ -3357,6 +3357,20 @@ void quic::processStreamFrame(const uint8_t* data, size_t len, size_t& offset) { } } // Compute new unique bytes (exclude retransmitted/overlapping data) // so that connection-level flow control only counts each byte once. uint64_t new_unique_bytes = 0; if (stream_len > 0) { uint64_t end_offset = stream_offset + stream_len; uint64_t overlap = 0; for (const auto& r : stream.recv_ranges) { if (r.first >= end_offset) break; if (r.second <= stream_offset) continue; overlap += std::min(r.second, end_offset) - std::max(r.first, stream_offset); } new_unique_bytes = stream_len - overlap; } // Enforce receive-side flow control: peer must not exceed our advertised limits if (stream_len > 0) { uint64_t end_offset = stream_offset + stream_len; Loading @@ -3374,8 +3388,8 @@ void quic::processStreamFrame(const uint8_t* data, size_t len, size_t& offset) { return; } // Connection-level check if (_data_recv + stream_len > _max_data_local_committed) { // Connection-level check (only count new unique bytes) if (_data_recv + new_unique_bytes > _max_data_local_committed) { // FLOW_CONTROL_ERROR (0x03): peer exceeded connection flow control limit std::vector<uint8_t> close_frame = buildConnectionCloseFrame(0x03, "connection flow control exceeded"); std::vector<uint8_t> packet = buildShortHeaderPacket(close_frame); Loading Loading @@ -3463,8 +3477,8 @@ void quic::processStreamFrame(const uint8_t* data, size_t len, size_t& offset) { offset += stream_len; // Update connection-level receive counter _data_recv += stream_len; // Update connection-level receive counter (only new unique bytes) _data_recv += new_unique_bytes; // Flow control: defer MAX_STREAM_DATA when >50% of window consumed uint64_t consumed = stream.recv_buffer.size(); Loading Loading @@ -3532,13 +3546,19 @@ void quic::processStreamFrame(const uint8_t* data, size_t len, size_t& offset) { stream.recv_buffer.clear(); stream.recv_buffer.shrink_to_fit(); stream.recv_ranges.clear(); } else if (!is_bidi_request && stream.recv_fin && !stream.recv_dispatched) { // Unidirectional stream with FIN — defer via _pending_dispatches // to avoid deadlock (callback may call sendStreamData which locks _quic_mutex) } else if (!is_bidi_request && stream.recv_complete && !stream.recv_dispatched) { // Unidirectional stream fully received — defer via _pending_dispatches // to avoid deadlock (callback may call sendStreamData which locks _quic_mutex). // Must check recv_complete (not just recv_fin) to ensure all data is // contiguous before dispatching — otherwise gaps from lost/reordered // packets would deliver a buffer with zero-filled holes. stream.recv_dispatched = true; _pending_dispatches.push_back({stream_id, std::vector<uint8_t>(stream.recv_buffer), std::move(stream.recv_buffer), true}); stream.recv_buffer.clear(); stream.recv_buffer.shrink_to_fit(); stream.recv_ranges.clear(); } } } Loading Loading @@ -3852,7 +3872,14 @@ void quic::sendStreamData(uint64_t stream_id, const std::vector<uint8_t>& data, while (remaining > 0) { size_t sent = sendStreamData(stream_id, data.data() + offset, remaining, fin); if (sent == 0) { if (++stall_count > 50) break; // give up after ~5s of total CC stall if (++stall_count > 50) { // Congestion control stall exceeded — data was not fully sent. // Throw so the caller knows the stream is incomplete. NetException exception; exception[NetException::Error] << "sendStreamData: congestion stall, " << remaining << " of " << data.size() << " bytes unsent on stream " << stream_id; throw exception; } // Pump incoming to process ACKs and open congestion window pumpIncoming(); continue; Loading Loading
debian/changelog +24 −0 Original line number Diff line number Diff line libnetplus (20260505+9) unstable; urgency=medium * QUIC: fix connection-level flow control double-counting — _data_recv was incremented by the full STREAM frame length on every receive, including retransmissions and overlapping data. Over many streams (e.g. during long cluster imports) this inflated the counter and could trigger a spurious FLOW_CONTROL_ERROR that killed the connection mid-transfer. Fix: compute overlap with existing recv_ranges to determine new unique bytes; only count those for _data_recv and the connection-level limit check. * QUIC: fix sendStreamData(vector) silent data loss on congestion control stall — after 50 stalls the function silently broke out of the send loop, losing remaining data with no error to the caller. Fix: throw NetException so callers (e.g. paritypp store_stripe) know the stream write failed and can retry. * QUIC: fix unidirectional stream dispatch before data complete — uni-stream callbacks fired on recv_fin (FIN frame arrival) instead of recv_complete (all data contiguous from offset 0). If packets arrived out of order or needed retransmission, the callback received a buffer with zero-filled gaps. Fix: check recv_complete (matching the bidi path) and std::move the buffer to avoid copy + memory waste. -- Jan Koester <jan.koester@tuxist.de> Mon, 05 May 2026 12:00:00 +0200 libnetplus (20260504+8) unstable; urgency=medium * QUIC: fix PTO re-queue spin loop — when sendPacket() failed (EAGAIN), Loading
src/quic.cpp +36 −9 Original line number Diff line number Diff line Loading @@ -3357,6 +3357,20 @@ void quic::processStreamFrame(const uint8_t* data, size_t len, size_t& offset) { } } // Compute new unique bytes (exclude retransmitted/overlapping data) // so that connection-level flow control only counts each byte once. uint64_t new_unique_bytes = 0; if (stream_len > 0) { uint64_t end_offset = stream_offset + stream_len; uint64_t overlap = 0; for (const auto& r : stream.recv_ranges) { if (r.first >= end_offset) break; if (r.second <= stream_offset) continue; overlap += std::min(r.second, end_offset) - std::max(r.first, stream_offset); } new_unique_bytes = stream_len - overlap; } // Enforce receive-side flow control: peer must not exceed our advertised limits if (stream_len > 0) { uint64_t end_offset = stream_offset + stream_len; Loading @@ -3374,8 +3388,8 @@ void quic::processStreamFrame(const uint8_t* data, size_t len, size_t& offset) { return; } // Connection-level check if (_data_recv + stream_len > _max_data_local_committed) { // Connection-level check (only count new unique bytes) if (_data_recv + new_unique_bytes > _max_data_local_committed) { // FLOW_CONTROL_ERROR (0x03): peer exceeded connection flow control limit std::vector<uint8_t> close_frame = buildConnectionCloseFrame(0x03, "connection flow control exceeded"); std::vector<uint8_t> packet = buildShortHeaderPacket(close_frame); Loading Loading @@ -3463,8 +3477,8 @@ void quic::processStreamFrame(const uint8_t* data, size_t len, size_t& offset) { offset += stream_len; // Update connection-level receive counter _data_recv += stream_len; // Update connection-level receive counter (only new unique bytes) _data_recv += new_unique_bytes; // Flow control: defer MAX_STREAM_DATA when >50% of window consumed uint64_t consumed = stream.recv_buffer.size(); Loading Loading @@ -3532,13 +3546,19 @@ void quic::processStreamFrame(const uint8_t* data, size_t len, size_t& offset) { stream.recv_buffer.clear(); stream.recv_buffer.shrink_to_fit(); stream.recv_ranges.clear(); } else if (!is_bidi_request && stream.recv_fin && !stream.recv_dispatched) { // Unidirectional stream with FIN — defer via _pending_dispatches // to avoid deadlock (callback may call sendStreamData which locks _quic_mutex) } else if (!is_bidi_request && stream.recv_complete && !stream.recv_dispatched) { // Unidirectional stream fully received — defer via _pending_dispatches // to avoid deadlock (callback may call sendStreamData which locks _quic_mutex). // Must check recv_complete (not just recv_fin) to ensure all data is // contiguous before dispatching — otherwise gaps from lost/reordered // packets would deliver a buffer with zero-filled holes. stream.recv_dispatched = true; _pending_dispatches.push_back({stream_id, std::vector<uint8_t>(stream.recv_buffer), std::move(stream.recv_buffer), true}); stream.recv_buffer.clear(); stream.recv_buffer.shrink_to_fit(); stream.recv_ranges.clear(); } } } Loading Loading @@ -3852,7 +3872,14 @@ void quic::sendStreamData(uint64_t stream_id, const std::vector<uint8_t>& data, while (remaining > 0) { size_t sent = sendStreamData(stream_id, data.data() + offset, remaining, fin); if (sent == 0) { if (++stall_count > 50) break; // give up after ~5s of total CC stall if (++stall_count > 50) { // Congestion control stall exceeded — data was not fully sent. // Throw so the caller knows the stream is incomplete. NetException exception; exception[NetException::Error] << "sendStreamData: congestion stall, " << remaining << " of " << data.size() << " bytes unsent on stream " << stream_id; throw exception; } // Pump incoming to process ACKs and open congestion window pumpIncoming(); continue; Loading