Hi,
while reviewing picoquic's QUIC implementation against RFC 9000, we noticed several
places where the current behavior appears to differ from the specification. Each item below
cites the relevant RFC text alongside the corresponding source code for your reference.
We hope this is helpful for improving RFC conformance.
1. 0-RTT Transport Parameter Limits Not Currently Guarded Against Reduction
RFC Reference: RFC 9000 Section 7.4.1
"If 0-RTT data is accepted by the server, the server MUST NOT reduce any limits or alter any values that might be violated by the client with its 0-RTT data. In particular, a server that accepts 0-RTT data MUST NOT set values for the following parameters (Section 18.2) that are smaller than the remembered values of the parameters."
-
active_connection_id_limit
-
initial_max_data
-
initial_max_stream_data_bidi_local
-
initial_max_stream_data_bidi_remote
-
initial_max_stream_data_uni
-
initial_max_streams_bidi
-
initial_max_streams_uni
Analysis:
When preparing transport extensions for a resumed connection, picoquic_prepare_transport_extensions (transport.c:306–364) encodes cnx->local_parameters directly without comparing them against the 0-RTT values stored in the session ticket. The ticket store does record the relevant parameters (ticket_store.c:84–91), but no comparison is performed during the encoding path. If a server's configuration changes between connections (e.g., lowering initial_max_data), the new, lower values would be sent without any guard, whereas RFC 9000 expects them to be at least as large as the remembered values.
Source Code Evidence (transport.c):
// transport.c:306-358
int picoquic_prepare_transport_extensions(picoquic_cnx_t* cnx, ...) {
...
bytes = picoquic_transport_param_type_varint_encode(bytes, bytes_max,
picoquic_tp_initial_max_data, cnx->local_parameters.initial_max_data);
// Encodes local_parameters directly — no comparison with stored 0-RTT values
...
}
2. Stream Direction Validation Differs from RFC for Several Frame Types
RFC References: RFC 9000 Section 19.4 (RESET_STREAM), Section 19.5 (STOP_SENDING), Section 19.8 (STREAM), Section 19.10 (MAX_STREAM_DATA), Section 19.13 (STREAM_DATA_BLOCKED)
"An endpoint that receives a RESET_STREAM frame for a send-only stream MUST terminate the connection with error STREAM_STATE_ERROR." (Section 19.4)
"An endpoint that receives a MAX_STREAM_DATA frame for a receive-only stream MUST terminate the connection with error STREAM_STATE_ERROR." (Section 19.10)
"An endpoint that receives a STREAM_DATA_BLOCKED frame for a send-only stream MUST terminate the connection with error STREAM_STATE_ERROR." (Section 19.13)
Analysis:
Several frame handlers do not appear to perform stream direction validation. In the STREAM_DATA_BLOCKED handler (picoquic_decode_stream_blocked_frame, frames.c:5105–5115), a TODO comment at line 5107 acknowledges the missing check. The RESET_STREAM handler at frames.c:367 processes resets on send-only streams without a direction check. In MAX_STREAM_DATA (picoquic_decode_max_stream_data_frame, frames.c:4563–4589), no direction validation is performed — data from receive-only streams is silently accepted. Of these frame types, only the STOP_SENDING handler (frames.c:1124) currently checks for receive-only stream direction. RFC 9000 expects STREAM_STATE_ERROR for all of these cases.
Source Code Evidence (frames.c):
// frames.c:5105-5115 — STREAM_DATA_BLOCKED handler
const uint8_t* picoquic_decode_stream_blocked_frame(picoquic_cnx_t* cnx,
const uint8_t* bytes, const uint8_t* bytes_max)
{
/* TODO: check that the stream number is valid */
if ((bytes = picoquic_frames_varint_skip(bytes+1, bytes_max)) == NULL ||
(bytes = picoquic_frames_varint_skip(bytes, bytes_max)) == NULL)
{
picoquic_connection_error(cnx, PICOQUIC_TRANSPORT_FRAME_FORMAT_ERROR,
picoquic_frame_type_stream_data_blocked);
}
return bytes;
}
// frames.c:4563-4588 — MAX_STREAM_DATA handler (no direction check)
const uint8_t* picoquic_decode_max_stream_data_frame(picoquic_cnx_t* cnx,
const uint8_t* bytes, const uint8_t* bytes_max)
{
uint64_t stream_id;
uint64_t maxdata = 0;
picoquic_stream_head_t* stream = NULL;
if ((bytes = picoquic_frames_varint_decode(bytes + 1, bytes_max, &stream_id)) == NULL ||
(bytes = picoquic_frames_varint_decode(bytes, bytes_max, &maxdata)) == NULL)
{
picoquic_connection_error(cnx, PICOQUIC_TRANSPORT_FRAME_FORMAT_ERROR,
picoquic_frame_type_max_stream_data);
}
else if ((stream = picoquic_find_stream(cnx, stream_id)) == NULL) {
stream = picoquic_create_missing_streams(cnx, stream_id, 1);
}
if (stream != NULL && maxdata > stream->maxdata_remote) {
stream->maxdata_remote = maxdata;
// No direction check — accepts MAX_STREAM_DATA on receive-only streams
}
return bytes;
}
3. MAX_STREAMS Threshold and Error Code Differ from RFC
RFC References: RFC 9000 Section 4.6, Section 19.14
"If a max_streams transport parameter or a MAX_STREAMS frame is received with a value greater than 2^60, this would allow a maximum stream ID that cannot be expressed as a variable-length integer [...] the connection MUST be closed immediately with a connection error of type [...] FRAME_ENCODING_ERROR if it was received in a frame." (Section 4.6)
Analysis:
In picoquic_decode_max_streams_frame (frames.c:4743–4746), the threshold is checked, but the error code used is STREAM_LIMIT_ERROR (0x04) rather than FRAME_ENCODING_ERROR (0x07).
Source Code Evidence (frames.c):
// frames.c:4743-4746 — MAX_STREAMS
if (max_stream_id >= (1ull << 62)) {
(void)picoquic_connection_error(cnx,
PICOQUIC_TRANSPORT_STREAM_LIMIT_ERROR, max_streams_frame_type);
// sends 0x04 rather than 0x07 (FRAME_ENCODING_ERROR)
bytes = NULL;
}
4. Oversized STREAM Frame Offset Uses FINAL_OFFSET_ERROR Rather Than FRAME_ENCODING_ERROR
RFC Reference: RFC 9000 Section 19.8
"The largest offset delivered on a stream -- the sum of the offset and data length -- cannot exceed 2^62-1, as it is not possible to provide flow control credit for that data. Receipt of a frame that exceeds this limit MUST be treated as a connection error of type FRAME_ENCODING_ERROR or FLOW_CONTROL_ERROR."
Analysis:
In picoquic_decode_stream_frame (frames.c:1531–1533), the overflow detection logic is correct — offset + data_length >= (1ull<<62) properly catches values exceeding 2^62-1. However, the error code used is PICOQUIC_TRANSPORT_FINAL_OFFSET_ERROR (0x06), whereas RFC 9000 specifies either FRAME_ENCODING_ERROR (0x07) or FLOW_CONTROL_ERROR (0x03). The connection is terminated with an error, but the error code does not match either of the two codes permitted by the specification.
Source Code Evidence (frames.c):
// frames.c:1529-1534
if (picoquic_parse_stream_header(bytes, bytes_max - bytes,
&stream_id, &offset, &data_length, &fin, &consumed) != 0) {
bytes = NULL;
} else if (offset + data_length >= (1ull<<62)){
picoquic_connection_error(cnx, PICOQUIC_TRANSPORT_FINAL_OFFSET_ERROR, 0);
// sends 0x06 (FINAL_OFFSET_ERROR) rather than 0x07 or 0x03
bytes = NULL;
}
5. NEW_CONNECTION_ID Length Exceeding 20 Bytes Sends PROTOCOL_VIOLATION Rather Than FRAME_ENCODING_ERROR
RFC Reference: RFC 9000 Section 19.15
"Length: An 8-bit unsigned integer containing the length of the connection ID. Values less than 1 and greater than 20 are invalid and MUST be treated as a connection error of type FRAME_ENCODING_ERROR."
Analysis:
In picoquic_decode_new_connection_id_frame (frames.c:680–684), when the CID length exceeds PICOQUIC_CONNECTION_ID_MAX_SIZE (20), the code sends PICOQUIC_TRANSPORT_PROTOCOL_VIOLATION (0x0A) rather than FRAME_ENCODING_ERROR (0x07). The zero-length CID case is handled separately at quicctx.c:2962 and does use the correct FRAME_FORMAT_ERROR (0x07) code.
Source Code Evidence (frames.c):
// frames.c:680-685
else if (cid_length > PICOQUIC_CONNECTION_ID_MAX_SIZE) {
/* TODO: should be PICOQUIC_TRANSPORT_PROTOCOL_VIOLATION if retire_before > sequence */
picoquic_connection_error(cnx, PICOQUIC_TRANSPORT_PROTOCOL_VIOLATION,
picoquic_frame_type_new_connection_id);
// sends 0x0A (PROTOCOL_VIOLATION) rather than 0x07 (FRAME_ENCODING_ERROR)
bytes = NULL;
}
6. active_connection_id_limit Values Below 2 Not Currently Validated
RFC Reference: RFC 9000 Section 18.2
"The value of the active_connection_id_limit parameter MUST be at least 2. An endpoint that receives a value less than 2 MUST close the connection with an error of type TRANSPORT_PARAMETER_ERROR."
Analysis:
In the transport parameter decoder (transport.c:710–714), active_connection_id_limit is decoded and stored without any range validation. A TODO comment at line 713 acknowledges this: /* TODO: may need to check the value, but conditions are unclear */. Values of 0 or 1 are silently accepted, whereas RFC 9000 requires TRANSPORT_PARAMETER_ERROR for values below 2.
Source Code Evidence (transport.c):
// transport.c:710-714
case picoquic_tp_active_connection_id_limit:
cnx->remote_parameters.active_connection_id_limit = (uint32_t)
picoquic_transport_param_varint_decode(cnx, bytes + byte_index,
extension_length, &ret);
/* TODO: may need to check the value, but conditions are unclear */
break;
7. Transport Parameter Range Validation Not Enforced for ack_delay_exponent and max_ack_delay
RFC References: RFC 9000 Section 18.2
"ack_delay_exponent (0x0a): [...] Values above 20 are invalid."
"max_ack_delay (0x0b): [...] Values of 2^14 or greater are invalid."
Analysis:
For ack_delay_exponent (transport.c:654–657), the value is decoded and cast to uint8_t with no upper-bound check. Values above 20 are silently accepted, whereas the RFC declares them invalid. For max_ack_delay (transport.c:686–691), a range check does exist, but it uses strict greater-than (>) rather than greater-than-or-equal (>=). Since the constant PICOQUIC_MAX_ACK_DELAY_MAX_MS is defined as 0x4000 (16384), the comparison max_ack_delay > 16384000 permits a value of exactly 16384 ms to pass. RFC 9000 states "values of 2^14 or greater are invalid", meaning >= 16384 should be the threshold.
Source Code Evidence (transport.c):
// transport.c:654-657 — ack_delay_exponent (no range check)
case picoquic_tp_ack_delay_exponent:
cnx->remote_parameters.ack_delay_exponent = (uint8_t)
picoquic_transport_param_varint_decode(cnx, bytes + byte_index,
extension_length, &ret);
break;
// transport.c:686-692 — max_ack_delay (off-by-one boundary)
case picoquic_tp_max_ack_delay:
cnx->remote_parameters.max_ack_delay = (uint32_t)
picoquic_transport_param_varint_decode(cnx, bytes + byte_index,
extension_length, &ret) * 1000;
if (cnx->remote_parameters.max_ack_delay > PICOQUIC_MAX_ACK_DELAY_MAX_MS * 1000) {
// Uses '>' instead of '>='; value exactly 16384 passes through
ret = picoquic_connection_error_ex(cnx, PICOQUIC_TRANSPORT_PARAMETER_ERROR, ...);
}
break;
Hi,
while reviewing picoquic's QUIC implementation against RFC 9000, we noticed several
places where the current behavior appears to differ from the specification. Each item below
cites the relevant RFC text alongside the corresponding source code for your reference.
We hope this is helpful for improving RFC conformance.
1. 0-RTT Transport Parameter Limits Not Currently Guarded Against Reduction
RFC Reference: RFC 9000 Section 7.4.1
active_connection_id_limit
initial_max_data
initial_max_stream_data_bidi_local
initial_max_stream_data_bidi_remote
initial_max_stream_data_uni
initial_max_streams_bidi
initial_max_streams_uni
Analysis:
When preparing transport extensions for a resumed connection,
picoquic_prepare_transport_extensions(transport.c:306–364) encodescnx->local_parametersdirectly without comparing them against the 0-RTT values stored in the session ticket. The ticket store does record the relevant parameters (ticket_store.c:84–91), but no comparison is performed during the encoding path. If a server's configuration changes between connections (e.g., loweringinitial_max_data), the new, lower values would be sent without any guard, whereas RFC 9000 expects them to be at least as large as the remembered values.Source Code Evidence (
transport.c):2. Stream Direction Validation Differs from RFC for Several Frame Types
RFC References: RFC 9000 Section 19.4 (RESET_STREAM), Section 19.5 (STOP_SENDING), Section 19.8 (STREAM), Section 19.10 (MAX_STREAM_DATA), Section 19.13 (STREAM_DATA_BLOCKED)
Analysis:
Several frame handlers do not appear to perform stream direction validation. In the
STREAM_DATA_BLOCKEDhandler (picoquic_decode_stream_blocked_frame, frames.c:5105–5115), a TODO comment at line 5107 acknowledges the missing check. TheRESET_STREAMhandler at frames.c:367 processes resets on send-only streams without a direction check. InMAX_STREAM_DATA(picoquic_decode_max_stream_data_frame, frames.c:4563–4589), no direction validation is performed — data from receive-only streams is silently accepted. Of these frame types, only theSTOP_SENDINGhandler (frames.c:1124) currently checks for receive-only stream direction. RFC 9000 expectsSTREAM_STATE_ERRORfor all of these cases.Source Code Evidence (
frames.c):3. MAX_STREAMS Threshold and Error Code Differ from RFC
RFC References: RFC 9000 Section 4.6, Section 19.14
Analysis:
In
picoquic_decode_max_streams_frame(frames.c:4743–4746), the threshold is checked, but the error code used isSTREAM_LIMIT_ERROR(0x04) rather thanFRAME_ENCODING_ERROR(0x07).Source Code Evidence (
frames.c):4. Oversized STREAM Frame Offset Uses FINAL_OFFSET_ERROR Rather Than FRAME_ENCODING_ERROR
RFC Reference: RFC 9000 Section 19.8
Analysis:
In
picoquic_decode_stream_frame(frames.c:1531–1533), the overflow detection logic is correct —offset + data_length >= (1ull<<62)properly catches values exceeding2^62-1. However, the error code used isPICOQUIC_TRANSPORT_FINAL_OFFSET_ERROR(0x06), whereas RFC 9000 specifies eitherFRAME_ENCODING_ERROR(0x07) orFLOW_CONTROL_ERROR(0x03). The connection is terminated with an error, but the error code does not match either of the two codes permitted by the specification.Source Code Evidence (
frames.c):5. NEW_CONNECTION_ID Length Exceeding 20 Bytes Sends PROTOCOL_VIOLATION Rather Than FRAME_ENCODING_ERROR
RFC Reference: RFC 9000 Section 19.15
Analysis:
In
picoquic_decode_new_connection_id_frame(frames.c:680–684), when the CID length exceedsPICOQUIC_CONNECTION_ID_MAX_SIZE(20), the code sendsPICOQUIC_TRANSPORT_PROTOCOL_VIOLATION(0x0A) rather thanFRAME_ENCODING_ERROR(0x07). The zero-length CID case is handled separately at quicctx.c:2962 and does use the correctFRAME_FORMAT_ERROR(0x07) code.Source Code Evidence (
frames.c):6. active_connection_id_limit Values Below 2 Not Currently Validated
RFC Reference: RFC 9000 Section 18.2
Analysis:
In the transport parameter decoder (transport.c:710–714),
active_connection_id_limitis decoded and stored without any range validation. A TODO comment at line 713 acknowledges this:/* TODO: may need to check the value, but conditions are unclear */. Values of 0 or 1 are silently accepted, whereas RFC 9000 requiresTRANSPORT_PARAMETER_ERRORfor values below 2.Source Code Evidence (
transport.c):7. Transport Parameter Range Validation Not Enforced for ack_delay_exponent and max_ack_delay
RFC References: RFC 9000 Section 18.2
Analysis:
For
ack_delay_exponent(transport.c:654–657), the value is decoded and cast touint8_twith no upper-bound check. Values above 20 are silently accepted, whereas the RFC declares them invalid. Formax_ack_delay(transport.c:686–691), a range check does exist, but it uses strict greater-than (>) rather than greater-than-or-equal (>=). Since the constantPICOQUIC_MAX_ACK_DELAY_MAX_MSis defined as0x4000(16384), the comparisonmax_ack_delay > 16384000permits a value of exactly 16384 ms to pass. RFC 9000 states "values of 2^14 or greater are invalid", meaning>= 16384should be the threshold.Source Code Evidence (
transport.c):