RFC 9000 Section 10.1 idle-timeout immediate-close commitment recheck
- Real but low-severity edge deviation: s2n-quic has paths that abandon the connection before the effective idle timeout without sending
CONNECTION_CLOSE (PTO backoff overflow, supervisor ImmediateClose, and max handshake duration exceeded).
- Normal paths are compliant: explicit application close, handle drop, idle timeout expiry, and no-valid-path behavior align with RFC 9000 Section 10.1.
- The original report's static match to
path/ecn.rs was unrelated to idle timeout. This revised report keeps the concern, but bases it on the correct source paths and runtime observations.
Standard Requirement
If either endpoint specifies max_idle_timeout, the connection is silently closed and state is discarded when it remains idle longer than the minimum value advertised by both endpoints. By advertising max_idle_timeout, an endpoint commits to initiating an immediate close if it abandons the connection before the effective value.
Silent close is allowed in two relevant cases: when the idle timeout expires, and when there is no validated path on which to send. Other abandonment before the effective idle timeout must initiate immediate close under Section 10.2. Section 10.2.3 only specifies which packet protection level should carry CONNECTION_CLOSE during the handshake; it does not exempt silent close.
Code Review
-
Frame generation in quic/s2n-quic-core/src/connection/error.rs (as_frame, around lines 467-468) produces no CONNECTION_CLOSE for:
Error::MaxHandshakeDurationExceeded { .. } => None,
Error::ImmediateClose { .. } => None,
IdleTimerExpired and NoValidPath are also None, but those are standard-permitted silent-close cases.
-
The error type documents ImmediateClose as:
/// The connection should be closed immediately without notifying the peer
ImmediateClose { reason: &'static str, ... },
-
Silent abandonment path 1: PTO backoff overflow in quic/s2n-quic-transport/src/space/mod.rs:
let max_backoff = path.pto_backoff.checked_mul(2).ok_or_else(|| {
connection::Error::immediate_close("PTO backoff multiplier exceeded maximum value")
})?;
-
Silent abandonment path 2: supervisor event in quic/s2n-quic-core/src/event/generated.rs:
/// Close the connection without notifying the peer
ImmediateClose { reason: &'static str },
After on_supervisor_timeout returns this result, quic/s2n-quic-transport/src/connection/connection_impl.rs returns Err(connection::Error::immediate_close(reason)).
-
State transitions in connection_impl.rs send a local active close to Closing; IdleTimerExpired and NoValidPath go to Finished, which is permitted; ImmediateClose and other catch-all errors also go to Finished.
-
The compliant comparison path is explicit application Trait::close() or handle drop, which goes through application_close -> local Closed / Application -> as_frame emits CONNECTION_CLOSE.
Runtime Evidence
I ran the end-to-end simulation test:
cargo test -p s2n-quic-tests tests::connection_migration::pto_backoff_exceeding_max_value_closes_connection
The test passed. The observed behavior was that the server closed with ImmediateClose after PTO overflow, using reason PTO backoff multiplier exceeded maximum value. The client never received CONNECTION_CLOSE and eventually ended with IdleTimerExpired. That is direct evidence of abandonment before the effective timeout without peer notification.
I also temporarily added a unit probe that called as_frame directly. It observed:
Error::closed(Local) -> Some, producing CONNECTION_CLOSE;
Error::immediate_close(...) -> None, producing no frame;
Error::idle_timer_expired() -> None, which is a permitted silent close.
The probe passed, cargo check -p s2n-quic-core passed afterward, and the source was restored to the upstream v1.85.0 state.
Final Assessment
The fact pattern is real: there are paths that silently abandon the connection before idle timeout. They are low-severity edge cases. PTO overflow requires repeated backoff toward the upper bound while the peer remains unresponsive; supervisor ImmediateClose is a documented application choice not to notify the peer; max handshake duration exceeded occurs only during the handshake.
The original direction was plausible, but the evidence chain was wrong. This revised report keeps the concern and corrects the rationale to the silent-close paths above rather than the unrelated ECN match.
RFC 9000 Section 10.1 idle-timeout immediate-close commitment recheck
CONNECTION_CLOSE(PTO backoff overflow, supervisorImmediateClose, and max handshake duration exceeded).path/ecn.rswas unrelated to idle timeout. This revised report keeps the concern, but bases it on the correct source paths and runtime observations.Standard Requirement
If either endpoint specifies
max_idle_timeout, the connection is silently closed and state is discarded when it remains idle longer than the minimum value advertised by both endpoints. By advertisingmax_idle_timeout, an endpoint commits to initiating an immediate close if it abandons the connection before the effective value.Silent close is allowed in two relevant cases: when the idle timeout expires, and when there is no validated path on which to send. Other abandonment before the effective idle timeout must initiate immediate close under Section 10.2. Section 10.2.3 only specifies which packet protection level should carry
CONNECTION_CLOSEduring the handshake; it does not exempt silent close.Code Review
Frame generation in
quic/s2n-quic-core/src/connection/error.rs(as_frame, around lines 467-468) produces noCONNECTION_CLOSEfor:IdleTimerExpiredandNoValidPathare alsoNone, but those are standard-permitted silent-close cases.The error type documents
ImmediateCloseas:Silent abandonment path 1: PTO backoff overflow in
quic/s2n-quic-transport/src/space/mod.rs:Silent abandonment path 2: supervisor event in
quic/s2n-quic-core/src/event/generated.rs:After
on_supervisor_timeoutreturns this result,quic/s2n-quic-transport/src/connection/connection_impl.rsreturnsErr(connection::Error::immediate_close(reason)).State transitions in
connection_impl.rssend a local active close toClosing;IdleTimerExpiredandNoValidPathgo toFinished, which is permitted;ImmediateCloseand other catch-all errors also go toFinished.The compliant comparison path is explicit application
Trait::close()or handle drop, which goes throughapplication_close-> localClosed/Application->as_frameemitsCONNECTION_CLOSE.Runtime Evidence
I ran the end-to-end simulation test:
The test passed. The observed behavior was that the server closed with
ImmediateCloseafter PTO overflow, using reasonPTO backoff multiplier exceeded maximum value. The client never receivedCONNECTION_CLOSEand eventually ended withIdleTimerExpired. That is direct evidence of abandonment before the effective timeout without peer notification.I also temporarily added a unit probe that called
as_framedirectly. It observed:Error::closed(Local)->Some, producingCONNECTION_CLOSE;Error::immediate_close(...)->None, producing no frame;Error::idle_timer_expired()->None, which is a permitted silent close.The probe passed,
cargo check -p s2n-quic-corepassed afterward, and the source was restored to the upstream v1.85.0 state.Final Assessment
The fact pattern is real: there are paths that silently abandon the connection before idle timeout. They are low-severity edge cases. PTO overflow requires repeated backoff toward the upper bound while the peer remains unresponsive; supervisor
ImmediateCloseis a documented application choice not to notify the peer; max handshake duration exceeded occurs only during the handshake.The original direction was plausible, but the evidence chain was wrong. This revised report keeps the concern and corrects the rationale to the silent-close paths above rather than the unrelated ECN match.