Skip to content

Pre-PTO CRYPTO retransmission limits are not fully established #3290

Description

@LiD0209

Pre-PTO CRYPTO retransmission limits are not fully established

Problem Description

The test cases exercise the same RFC 9002 Section 6.2.4 requirement for a finite per-connection limit on sending unacknowledged CRYPTO data before PTO expiry. Static review left unresolved preconditions, and no implementation-specific executable reproducer was available in the current run.

Standard Requirement

an endpoint
   MAY, for a limited number of times per connection, send a packet
   containing unacknowledged CRYPTO data earlier than the PTO expiry

Interpretation: when an endpoint uses the pre-PTO transmission optimization for unacknowledged CRYPTO data, it must impose a finite per-connection limit. It must not apply that optimization an unlimited number of times on one connection.

Relevant Source Code

The requirement was mapped to s2n-quic PTO and congestion-control source categories. The cited source shows the relevant probe transmission mechanism, but static review alone did not close every precondition, cross-state behavior, or runtime observable needed for a conclusion.

quic/s2n-quic-core/src/recovery/pto.rs:17-31

impl Pto {
    /// Called when a timeout has occurred. Returns `Ready` if the PTO timer had expired.
    #[inline]
    pub fn on_timeout(&mut self, packets_in_flight: bool, timestamp: Timestamp) -> Poll<()> {
        ensure!(
            self.timer.poll_expiration(timestamp).is_ready(),
            Poll::Pending
        );

        //= https://www.rfc-editor.org/rfc/rfc9002#section-6.2.4
        //# When a PTO timer expires, a sender MUST send at least one ack-
        //# eliciting packet in the packet number space as a probe.

        //= https://www.rfc-editor.org/rfc/rfc9002#section-6.2.2.1
        //# Since the server could be blocked until more datagrams are received

PTO timeout handling is the trigger point for sending at least one ack-eliciting probe packet.

quic/s2n-quic-core/src/recovery/pto.rs:95-152

impl transmission::Provider for Pto {
    #[inline]
    fn on_transmit<W: transmission::Writer>(&mut self, context: &mut W) {
        ensure!(context.transmission_mode().is_loss_recovery_probing());
        ensure!(self.has_transmission_interest());

        //= https://www.rfc-editor.org/rfc/rfc9002#section-6.2.4
        //# All probe packets sent on a PTO MUST be ack-eliciting.
        if !context.ack_elicitation().is_ack_eliciting() {
            let frame = frame::Ping;

            //= https://www.rfc-editor.org/rfc/rfc9002#section-7.5
            //# Probe packets MUST NOT be blocked by the congestion controller.
            ensure!(context.write_frame_forced(&frame).is_some());
        }

        self.on_transmit_once();
    }
}

impl transmission::interest::Provider for Pto {
    #[inline]
    fn transmission_interest<Q: transmission::interest::Query>(
        &self,
        query: &mut Q,
    ) -> transmission::interest::Result {
        if self.transmissions() > 0 {
            query.on_forced()?;
        }

        Ok(())
    }
}

PTO probe transmission forces an ack-eliciting PING when no other ack-eliciting frame is present and uses forced transmission interest.

quic/s2n-quic-transport/src/connection/transmission.rs:119-141

//= https://www.rfc-editor.org/rfc/rfc9002#section-7
//# An endpoint MUST NOT send a packet if it would cause bytes_in_flight
//# (see Appendix B.2) to be larger than the congestion window, unless
//# the packet is sent on a PTO timer expiration (see Section 6.2) or
//# when entering recovery (see Section 7.3.2).
let transmission_constraint =
    if space_manager.requires_probe() && self.context.transmission_mode.is_normal() {
        //= https://www.rfc-editor.org/rfc/rfc9002#section-6.2.4
        //# When a PTO timer expires, a sender MUST send at least one ack-
        //# eliciting packet in the packet number space as a probe.

        //= https://www.rfc-editor.org/rfc/rfc9002#section-6.2.4
        //# An endpoint SHOULD include new data in packets that are sent on PTO
        //# expiration.  Previously sent data MAY be sent if no new data can be
        //# sent.

        //= https://www.rfc-editor.org/rfc/rfc9002#section-7.5
        //# Probe packets MUST NOT be blocked by the congestion controller.
        self.context.transmission_mode = transmission::Mode::LossRecoveryProbing;
        transmission::Constraint::None
    } else {
        self.context.path().transmission_constraint()
    };

Normal transmission is constrained by congestion window, while PTO probing switches to LossRecoveryProbing with no congestion constraint.

quic/s2n-quic-transport/src/recovery/manager.rs:219-276

let congestion_controlled_bytes = if outcome.is_congestion_controlled {
    outcome.bytes_sent
} else {
    0
};

let path_id = context.path_id();
let path = context.path_mut();
let cc_packet_info = path.congestion_controller.on_packet_sent(
    time_sent,
    congestion_controlled_bytes,
    app_limited,
    &path.rtt_estimator,
    &mut congestion_controller::PathPublisher::new(publisher, path_id),
);

self.sent_packets.insert(
    packet_number,
    SentPacketInfo::new(
        outcome.is_congestion_controlled,
        congestion_controlled_bytes,
        time_sent,
        outcome.ack_elicitation,
        path_id,
        ecn,
        transmission_mode,
        cc_packet_info,
    ),
);

if outcome.ack_elicitation.is_ack_eliciting() {
    self.time_of_last_ack_eliciting_packet = Some(time_sent);
    self.pto_update_pending = true;
}

Sent packet accounting excludes ACK-only packets from congestion control, but records congestion-controlled packets, including probes, in sent packet state.

Runtime Evidence

Positive controls were run for representative RTT, PTO, loss, ACK, and persistent-congestion behavior. They passed with exit code 0, showing that the basic recovery test environment was operational.

The implementation-specific focused reproducers were not run because no SpecLitmus adapter or minimal executable reproducer was present:

reason: No implementation-specific adapter was present for this candidate.

The runtime evidence did not close the behavior either way.

Inconsistency Reason

A distinguishable mismatch remains plausible for the finite-limit requirement: the endpoint must impose a finite per-connection limit on the number of pre-PTO transmissions of unacknowledged CRYPTO data. Static source review identified the relevant PTO transmission paths, but runtime evidence did not prove whether the optimization can be repeated without a per-connection bound.

Decision Reason

Static source review raised a concrete concern and positive controls passed, but no focused reproducer closed the behavior either way within the runtime budget.

Remaining Uncertainty

Runtime or deeper harness-level evidence is still needed to distinguish the exact packet-number-space, path, timer, congestion-control, key-discard, or undecryptable-packet behavior from the source-only mapping.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions