Retired connection ID token remains valid until ACK
Summary
After s2n-quic sends RETIRE_CONNECTION_ID, it marks the retired peer CID as PendingAcknowledgement, but the corresponding stateless reset token remains in stateless_reset_map until the retire frame is ACKed. RFC 9000 requires the token to become invalid when the CID is retired via RETIRE_CONNECTION_ID, and it forbids checking tokens for retired CIDs. This is a real inconsistency.
Standard Requirement
RFC 9000 says stateless reset tokens are invalidated when their associated connection ID is retired via a RETIRE_CONNECTION_ID frame. It also says an endpoint must not check stateless reset tokens associated with connection IDs it has not used or connection IDs that have been retired.
The key point is that the token becomes invalid when the CID is retired via RETIRE_CONNECTION_ID, not when that frame is later ACKed.
Relevant Source Code
implementions/s2n-quic/quic/s2n-quic-transport/src/connection/peer_id_registry.rs:455-466
if let Some(packet_number) = context.write_frame(&frame::RetireConnectionId {
sequence_number: id_info.sequence_number.into(),
}) {
id_info.status = PendingAcknowledgement(packet_number);
self.transmission_interest.clear();
self.ack_interest.clear();
}
After sending RETIRE_CONNECTION_ID, the implementation enters PendingAcknowledgement but does not remove the token.
implementions/s2n-quic/quic/s2n-quic-transport/src/connection/peer_id_registry.rs:483-505
if let PendingAcknowledgement(packet_number) = id_info.status {
if ack_set.contains(packet_number) {
if let Some(token) = id_info.stateless_reset_token {
mapper_state.stateless_reset_map.remove(&token);
}
self.ack_interest.clear();
return false;
}
}
Token removal happens only during ACK handling.
implementions/s2n-quic/quic/s2n-quic-transport/src/endpoint/mod.rs:897-916
fn close_on_matching_stateless_reset(
&mut self,
payload: &[u8],
timestamp: Timestamp,
) -> Option<InternalConnectionId> {
let token_index = payload.len().checked_sub(StatelessResetTokenLen)?;
let buffer = buffer.skip(token_index).ok()?;
let (token, _) = buffer.decode().ok()?;
let internal_id = self
.connection_id_mapper
.remove_internal_connection_id_by_stateless_reset_token(&token)?;
As long as the retired token remains in the mapper, an endpoint datagram ending in that token can still enter Stateless Reset handling.
Implementation Behavior
After receiving NEW_CONNECTION_ID with a larger Retire Prior To, the old peer CID is marked PendingRetirement. After sending RETIRE_CONNECTION_ID, it becomes PendingAcknowledgement; the old token still matches in stateless_reset_map. Only after ACK arrives does on_packet_ack remove the token and delete the CID.
Inconsistency Reason
RFC 9000 requires a retired CID's token to be invalid and no longer checked. The implementation delays invalidation until ACK, so during the interval after RETIRE_CONNECTION_ID is sent and before it is ACKed, the retired CID's token is still treated as a valid stateless reset token.
Runtime Evidence
I ran the existing test:
cargo test -p s2n-quic-transport connection::peer_id_registry::tests::retire_connection_id_when_retire_prior_to_increases -- --exact --nocapture
It passed with exit code 0. The test asserts that before ACK, the old token still maps to the internal connection ID, and after ACK, the token no longer maps:
reg.on_transmit(&mut write_context);
assert_eq!(
Some(reg.internal_id),
mapper.remove_internal_connection_id_by_stateless_reset_token(&TEST_TOKEN_1)
);
reg.on_packet_ack(&PacketNumberRange::new(packet_number, packet_number));
assert_eq!(
None,
mapper.remove_internal_connection_id_by_stateless_reset_token(&TEST_TOKEN_1)
);
I also ran a temporary negative reproducer that expected the old token to be invalid immediately after reg.on_transmit(&mut write_context). That reproducer failed with exit code 101 and observed:
assertion `left == right` failed
left: None
right: Some(InternalConnectionId(0))
The temporary test was removed after the run.
Impact
While ACK is delayed or lost, a stateless reset token for a retired CID can still trigger Stateless Reset detection. That violates RFC 9000's token invalidation and no-checking rules for retired CIDs.
Fix Direction
Remove the corresponding token from stateless_reset_map immediately when RETIRE_CONNECTION_ID is sent and the CID is retired. Keep enough retirement state to track ACK and retransmit the retire frame if needed.
Merged Redundant Reports
The following reports described the same root cause and fix direction, so they were merged into this report and removed:
0233-static unresolved requirement.md: retired CID stateless reset token remains active while RETIRE_CONNECTION_ID is pending ACK.
0238-static unresolved requirement.md: same pending-ACK retired-CID token lifetime issue.
Retired connection ID token remains valid until ACK
Summary
After s2n-quic sends
RETIRE_CONNECTION_ID, it marks the retired peer CID asPendingAcknowledgement, but the corresponding stateless reset token remains instateless_reset_mapuntil the retire frame is ACKed. RFC 9000 requires the token to become invalid when the CID is retired viaRETIRE_CONNECTION_ID, and it forbids checking tokens for retired CIDs. This is a real inconsistency.Standard Requirement
RFC 9000 says stateless reset tokens are invalidated when their associated connection ID is retired via a
RETIRE_CONNECTION_IDframe. It also says an endpoint must not check stateless reset tokens associated with connection IDs it has not used or connection IDs that have been retired.The key point is that the token becomes invalid when the CID is retired via
RETIRE_CONNECTION_ID, not when that frame is later ACKed.Relevant Source Code
implementions/s2n-quic/quic/s2n-quic-transport/src/connection/peer_id_registry.rs:455-466After sending
RETIRE_CONNECTION_ID, the implementation entersPendingAcknowledgementbut does not remove the token.implementions/s2n-quic/quic/s2n-quic-transport/src/connection/peer_id_registry.rs:483-505Token removal happens only during ACK handling.
implementions/s2n-quic/quic/s2n-quic-transport/src/endpoint/mod.rs:897-916As long as the retired token remains in the mapper, an endpoint datagram ending in that token can still enter Stateless Reset handling.
Implementation Behavior
After receiving
NEW_CONNECTION_IDwith a largerRetire Prior To, the old peer CID is markedPendingRetirement. After sendingRETIRE_CONNECTION_ID, it becomesPendingAcknowledgement; the old token still matches instateless_reset_map. Only after ACK arrives doeson_packet_ackremove the token and delete the CID.Inconsistency Reason
RFC 9000 requires a retired CID's token to be invalid and no longer checked. The implementation delays invalidation until ACK, so during the interval after
RETIRE_CONNECTION_IDis sent and before it is ACKed, the retired CID's token is still treated as a valid stateless reset token.Runtime Evidence
I ran the existing test:
It passed with exit code
0. The test asserts that before ACK, the old token still maps to the internal connection ID, and after ACK, the token no longer maps:I also ran a temporary negative reproducer that expected the old token to be invalid immediately after
reg.on_transmit(&mut write_context). That reproducer failed with exit code101and observed:The temporary test was removed after the run.
Impact
While ACK is delayed or lost, a stateless reset token for a retired CID can still trigger Stateless Reset detection. That violates RFC 9000's token invalidation and no-checking rules for retired CIDs.
Fix Direction
Remove the corresponding token from
stateless_reset_mapimmediately whenRETIRE_CONNECTION_IDis sent and the CID is retired. Keep enough retirement state to track ACK and retransmit the retire frame if needed.Merged Redundant Reports
The following reports described the same root cause and fix direction, so they were merged into this report and removed:
0233-static unresolved requirement.md: retired CID stateless reset token remains active whileRETIRE_CONNECTION_IDis pending ACK.0238-static unresolved requirement.md: same pending-ACK retired-CID token lifetime issue.