Connection state is not fully discarded after finalization
Summary
RFC 9000 says an endpoint should discard all connection state after closing/draining ends. s2n-quic removes the finalized connection from the endpoint container, but if an application Connection or stream-derived handle is still alive, the same Arc<ConnectionNode> keeps the full connection object alive. The original ECN evidence was unrelated.
Standard Requirement
Official reference: RFC 9000 Section 10.2, "Immediate Close": https://www.rfc-editor.org/rfc/rfc9000#section-10.2
Once its closing or draining state ends, an endpoint SHOULD discard all connection state.
Context: the next sentence allows stateless reset for later packets belonging to the old connection. That only makes sense after old per-connection state has been discarded.
Relevant Source Code
Closing timeout marks the connection finished:
// quic/s2n-quic-transport/src/connection/connection_impl.rs:1182-1187
if self.close_sender.on_timeout(timestamp).is_ready() {
self.state = ConnectionState::Finished;
}
Draining and Finished request finalization:
// quic/s2n-quic-transport/src/connection/connection_impl.rs:2193-2200
ConnectionState::Draining | ConnectionState::Finished => {
interests.transmission = false;
interests.finalization = true;
}
Finalization removes the node from the endpoint container:
// quic/s2n-quic-transport/src/connection/connection_container.rs:957-959
for connection in self.interest_lists.done_connections.take() {
self.remove_node(&connection);
}
But removal only unlinks the node from container lists/maps:
// quic/s2n-quic-transport/src/connection/connection_container.rs:1137-1145
fn remove_node(&mut self, connection: &ConnectionNode<C, L>) {
let mut cursor = self.connection_map.find_mut(&connection.internal_connection_id);
let remove_result = cursor.remove();
self.interest_lists.remove_node(connection);
}
Application handles keep another Arc to the same node:
// quic/s2n-quic-transport/src/connection/api_provider.rs:26-27
pub(crate) type ConnectionApi = Arc<dyn ConnectionApiProvider>;
// quic/s2n-quic-transport/src/connection/api.rs:26-37
pub struct Connection {
pub(super) api: ConnectionApi,
open_token: OpenToken,
}
CID cleanup is tied to dropping the contained LocalIdRegistry, not to container finalization:
// quic/s2n-quic-transport/src/connection/local_id_registry.rs:221-231
impl Drop for LocalIdRegistry {
fn drop(&mut self) {
for id_info in &self.registered_ids {
guard.local_id_map.remove(&id_info.id);
}
guard.initial_id_map.remove(&self.internal_id);
}
}
Implementation Behavior
When finalization runs, s2n-quic removes endpoint reachability for the connection. However, a live application handle can keep ConnectionNode and its ConnectionImpl allocated, including protocol-owned state such as CID registry, packet spaces, path state, timers, and API/error state.
Inconsistency Reason
The implementation does not unconditionally discard all connection state when closing/draining ends. It discards the endpoint container entry, but full state destruction is delayed until all application-owned Arc handles are dropped.
Runtime Evidence
Temporary focused test added, run, then removed:
container.insert_connection(TestConnection::default(), id);
let handle = container.get_connection_handle(&id).unwrap();
container.with_connection(id, |conn| {
conn.interests = ConnectionInterests { finalization: true, ..Default::default() };
});
assert_eq!(0, container.len());
assert!(container.get_connection_handle(&id).is_none());
assert_eq!(Ok(SocketAddress::default()), handle.remote_address());
Command run from implementions/s2n-quic:
cargo test -p s2n-quic-transport connection::connection_container::tests::finalized_connection_state_survives_while_application_handle_exists -- --exact --nocapture
Result:
running 1 test
test connection::connection_container::tests::finalized_connection_state_survives_while_application_handle_exists ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 354 filtered out; finished in 0.00s
Saved logs:
reports/runtime/0173-finalized-state-retained.stdout.log
reports/runtime/0173-finalized-state-retained.stderr.log
reports/runtime/0173-finalized-state-retained.exitcode.txt
Impact
This is a SHOULD-level conformance issue. It mainly affects cleanup semantics and memory/state lifetime after finalization; endpoint routing is removed, but full connection state can persist while application handles remain alive.
Fix Direction
On finalization, separate application-handle lifetime from protocol-state lifetime. Clear or move protocol-owned state immediately, leaving only minimal closed/error state needed for outstanding handles.
Connection state is not fully discarded after finalization
Summary
RFC 9000 says an endpoint should discard all connection state after closing/draining ends. s2n-quic removes the finalized connection from the endpoint container, but if an application
Connectionor stream-derived handle is still alive, the sameArc<ConnectionNode>keeps the full connection object alive. The original ECN evidence was unrelated.Standard Requirement
Official reference: RFC 9000 Section 10.2, "Immediate Close": https://www.rfc-editor.org/rfc/rfc9000#section-10.2
Context: the next sentence allows stateless reset for later packets belonging to the old connection. That only makes sense after old per-connection state has been discarded.
Relevant Source Code
Closing timeout marks the connection finished:
DrainingandFinishedrequest finalization:Finalization removes the node from the endpoint container:
But removal only unlinks the node from container lists/maps:
Application handles keep another
Arcto the same node:CID cleanup is tied to dropping the contained
LocalIdRegistry, not to container finalization:Implementation Behavior
When finalization runs, s2n-quic removes endpoint reachability for the connection. However, a live application handle can keep
ConnectionNodeand itsConnectionImplallocated, including protocol-owned state such as CID registry, packet spaces, path state, timers, and API/error state.Inconsistency Reason
The implementation does not unconditionally discard all connection state when closing/draining ends. It discards the endpoint container entry, but full state destruction is delayed until all application-owned
Archandles are dropped.Runtime Evidence
Temporary focused test added, run, then removed:
Command run from
implementions/s2n-quic:Result:
Saved logs:
reports/runtime/0173-finalized-state-retained.stdout.logreports/runtime/0173-finalized-state-retained.stderr.logreports/runtime/0173-finalized-state-retained.exitcode.txtImpact
This is a
SHOULD-level conformance issue. It mainly affects cleanup semantics and memory/state lifetime after finalization; endpoint routing is removed, but full connection state can persist while application handles remain alive.Fix Direction
On finalization, separate application-handle lifetime from protocol-state lifetime. Clear or move protocol-owned state immediately, leaving only minimal closed/error state needed for outstanding handles.