Loading src/quic.cpp +14 −3 Original line number Diff line number Diff line Loading @@ -7327,6 +7327,12 @@ size_t quic::sendStreamDataEarly(uint64_t stream_id, const uint8_t* data, size_t if (stream.send_offset + send_len > stream.max_data_remote) { send_len = (stream.max_data_remote > stream.send_offset) ? (stream.max_data_remote - stream.send_offset) : 0; } // FIN declares the stream's final size. If flow control only admits a // prefix of the caller's buffer, that prefix is not the final write: the // caller will retry the remainder after receiving more credit. Attaching // FIN here would declare a too-small final size and make that retry a // FINAL_SIZE_ERROR at the peer. const bool fin_eligible = fin && send_len == len; if (send_len == 0 && !fin) return 0; // Conservative fixed chunk size — 0-RTT packets are padded to the same Loading @@ -7340,7 +7346,7 @@ size_t quic::sendStreamDataEarly(uint64_t stream_id, const uint8_t* data, size_t do { size_t chunk = std::min(send_len - sent_total, max_chunk); bool is_last = (sent_total + chunk >= send_len); bool set_fin = fin && is_last; bool set_fin = fin_eligible && is_last; std::vector<uint8_t> chunk_data(data + sent_total, data + sent_total + chunk); std::vector<uint8_t> stream_frame = buildStreamFrame(stream_id, chunk_data, stream.send_offset, set_fin); Loading Loading @@ -7441,6 +7447,11 @@ size_t quic::sendStreamData(uint64_t stream_id, const uint8_t* data, size_t len, (stream.max_data_remote - stream.send_offset) : 0; } // A STREAM FIN fixes the final stream size permanently. Completion of a // flow-control-clamped prefix is therefore not completion of the caller's // write. Only the call that can accept all `len` bytes may carry FIN. const bool fin_eligible = fin && send_len == len; if (send_len == 0 && !fin) { return 0; } Loading Loading @@ -7506,7 +7517,7 @@ size_t quic::sendStreamData(uint64_t stream_id, const uint8_t* data, size_t len, while (sent_total < send_len) { size_t chunk = std::min(send_len - sent_total, max_chunk); bool is_last = (sent_total + chunk >= send_len); bool set_fin = fin && is_last; bool set_fin = fin_eligible && is_last; // Congestion control: use estimated actual packet size, not max_datagram size_t est_pkt_size = header_overhead + frame_hdr_max + chunk; Loading Loading @@ -7731,7 +7742,7 @@ size_t quic::sendStreamData(uint64_t stream_id, const uint8_t* data, size_t len, // Only mark FIN if we actually sent all requested data (FIN is on the last chunk). // If CC broke the loop early, the FIN was never put on the wire. if (fin && sent_total >= send_len) { if (fin_eligible && sent_total >= send_len) { // FIN-only: when send_len == 0, the main loop never ran, so we must // send an empty STREAM frame with the FIN bit on the wire. if (send_len == 0 && !stream.send_fin) { Loading test/quic_rfc9000_test.cpp +44 −0 Original line number Diff line number Diff line Loading @@ -497,6 +497,49 @@ static void test_empty_fin() { } } // ============================================================================ // RFC 9000 §4.5: Flow-control truncation must not declare a final size // ============================================================================ static void test_flow_control_truncated_fin() { std::cout << "\n=== RFC 9000 §4.5: FIN after flow-control truncation ===" << std::endl; quic client; client.setTrustPolicy(netplus::TlsTrustPolicy{false}); connectClient(client); uint64_t sid = client.openStream(true); std::vector<uint8_t> payload(512); fillPattern(payload, 0x5A); // Artificially restrict the sender's view of the peer's connection // credit so the pointer overload accepts only a prefix of this write. const uint64_t original_max_data = client._max_data_remote; const uint64_t prefix = 128; client._max_data_remote = client._data_sent + prefix; size_t first = client.sendStreamData(sid, payload.data(), payload.size(), true); check(first == prefix, "Flow control accepts only the available prefix"); check(!client._streams[sid].send_fin, "Truncated prefix does not set the stream FIN state"); // Restore credit and send the remainder. This call, and only this call, // may declare the final size. client._max_data_remote = original_max_data; size_t second = client.sendStreamData(sid, payload.data() + first, payload.size() - first, true); check(first + second == payload.size(), "Remainder is accepted after credit returns"); check(client._streams[sid].send_fin, "Final write sets the stream FIN state"); std::vector<uint8_t> recv_buf(payload.size() + 4096); size_t total = drainStream(client, sid, recv_buf, 5); check(total == payload.size(), "Peer receives the complete final-size payload"); check(total == payload.size() && std::memcmp(payload.data(), recv_buf.data(), payload.size()) == 0, "Payload remains intact across the flow-control boundary"); } // ============================================================================ // RFC 9000 §4.6: MAX_STREAMS — Rapid stream creation // ============================================================================ Loading Loading @@ -680,6 +723,7 @@ int main() { test_uni_streams(); test_fin_handling(); test_empty_fin(); test_flow_control_truncated_fin(); test_data_integrity_small(); test_data_integrity_large(); test_connection_flow_control(); Loading Loading
src/quic.cpp +14 −3 Original line number Diff line number Diff line Loading @@ -7327,6 +7327,12 @@ size_t quic::sendStreamDataEarly(uint64_t stream_id, const uint8_t* data, size_t if (stream.send_offset + send_len > stream.max_data_remote) { send_len = (stream.max_data_remote > stream.send_offset) ? (stream.max_data_remote - stream.send_offset) : 0; } // FIN declares the stream's final size. If flow control only admits a // prefix of the caller's buffer, that prefix is not the final write: the // caller will retry the remainder after receiving more credit. Attaching // FIN here would declare a too-small final size and make that retry a // FINAL_SIZE_ERROR at the peer. const bool fin_eligible = fin && send_len == len; if (send_len == 0 && !fin) return 0; // Conservative fixed chunk size — 0-RTT packets are padded to the same Loading @@ -7340,7 +7346,7 @@ size_t quic::sendStreamDataEarly(uint64_t stream_id, const uint8_t* data, size_t do { size_t chunk = std::min(send_len - sent_total, max_chunk); bool is_last = (sent_total + chunk >= send_len); bool set_fin = fin && is_last; bool set_fin = fin_eligible && is_last; std::vector<uint8_t> chunk_data(data + sent_total, data + sent_total + chunk); std::vector<uint8_t> stream_frame = buildStreamFrame(stream_id, chunk_data, stream.send_offset, set_fin); Loading Loading @@ -7441,6 +7447,11 @@ size_t quic::sendStreamData(uint64_t stream_id, const uint8_t* data, size_t len, (stream.max_data_remote - stream.send_offset) : 0; } // A STREAM FIN fixes the final stream size permanently. Completion of a // flow-control-clamped prefix is therefore not completion of the caller's // write. Only the call that can accept all `len` bytes may carry FIN. const bool fin_eligible = fin && send_len == len; if (send_len == 0 && !fin) { return 0; } Loading Loading @@ -7506,7 +7517,7 @@ size_t quic::sendStreamData(uint64_t stream_id, const uint8_t* data, size_t len, while (sent_total < send_len) { size_t chunk = std::min(send_len - sent_total, max_chunk); bool is_last = (sent_total + chunk >= send_len); bool set_fin = fin && is_last; bool set_fin = fin_eligible && is_last; // Congestion control: use estimated actual packet size, not max_datagram size_t est_pkt_size = header_overhead + frame_hdr_max + chunk; Loading Loading @@ -7731,7 +7742,7 @@ size_t quic::sendStreamData(uint64_t stream_id, const uint8_t* data, size_t len, // Only mark FIN if we actually sent all requested data (FIN is on the last chunk). // If CC broke the loop early, the FIN was never put on the wire. if (fin && sent_total >= send_len) { if (fin_eligible && sent_total >= send_len) { // FIN-only: when send_len == 0, the main loop never ran, so we must // send an empty STREAM frame with the FIN bit on the wire. if (send_len == 0 && !stream.send_fin) { Loading
test/quic_rfc9000_test.cpp +44 −0 Original line number Diff line number Diff line Loading @@ -497,6 +497,49 @@ static void test_empty_fin() { } } // ============================================================================ // RFC 9000 §4.5: Flow-control truncation must not declare a final size // ============================================================================ static void test_flow_control_truncated_fin() { std::cout << "\n=== RFC 9000 §4.5: FIN after flow-control truncation ===" << std::endl; quic client; client.setTrustPolicy(netplus::TlsTrustPolicy{false}); connectClient(client); uint64_t sid = client.openStream(true); std::vector<uint8_t> payload(512); fillPattern(payload, 0x5A); // Artificially restrict the sender's view of the peer's connection // credit so the pointer overload accepts only a prefix of this write. const uint64_t original_max_data = client._max_data_remote; const uint64_t prefix = 128; client._max_data_remote = client._data_sent + prefix; size_t first = client.sendStreamData(sid, payload.data(), payload.size(), true); check(first == prefix, "Flow control accepts only the available prefix"); check(!client._streams[sid].send_fin, "Truncated prefix does not set the stream FIN state"); // Restore credit and send the remainder. This call, and only this call, // may declare the final size. client._max_data_remote = original_max_data; size_t second = client.sendStreamData(sid, payload.data() + first, payload.size() - first, true); check(first + second == payload.size(), "Remainder is accepted after credit returns"); check(client._streams[sid].send_fin, "Final write sets the stream FIN state"); std::vector<uint8_t> recv_buf(payload.size() + 4096); size_t total = drainStream(client, sid, recv_buf, 5); check(total == payload.size(), "Peer receives the complete final-size payload"); check(total == payload.size() && std::memcmp(payload.data(), recv_buf.data(), payload.size()) == 0, "Payload remains intact across the flow-control boundary"); } // ============================================================================ // RFC 9000 §4.6: MAX_STREAMS — Rapid stream creation // ============================================================================ Loading Loading @@ -680,6 +723,7 @@ int main() { test_uni_streams(); test_fin_handling(); test_empty_fin(); test_flow_control_truncated_fin(); test_data_integrity_small(); test_data_integrity_large(); test_connection_flow_control(); Loading