Skip to content

Fix client hang when server closes during handshake - #182

Open
josephnoir wants to merge 3 commits into
apple:mainfrom
josephnoir:rh/error-during-handshake
Open

josephnoir wants to merge 3 commits into
apple:mainfrom
josephnoir:rh/error-during-handshake

Conversation

@josephnoir

@josephnoir josephnoir commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

The test testConnectionError in the StreamTableTests has started failing frequently with 0.4.0. Bisec points to #157 as the change in flakiness. However, the preexisting issue is that closing during the handshake sends the wrong error code. RFC 9000 10.2.3.:

A CONNECTION_CLOSE of type 0x1d MUST be replaced by a CONNECTION_CLOSE of type 0x1c when sending the frame in Initial or Handshake packets. Otherwise, information about the application state might be revealed.

This PR adds regression tests and two fixes:

  • Send 0x000C when the packet space applicationData has not been reached.
  • close the connection when deciding not continue processing.

@josephnoir josephnoir added the 🔨 semver/patch No public API change. label Sep 29, 2026
@agnosticdev

Copy link
Copy Markdown
Collaborator

Thanks for finding this and fixing @josephnoir!

A server that closes with an application error before it has processed
the client's Finished still has key state .handshake. It then sends
APPLICATION_CLOSE (0x1d) in a Handshake packet, which RFC 9000 §10.2.3
forbids. Since apple#157 the client rejects that frame but drops the
resulting close, so it hangs until the idle timeout.

- Add testQUICServerApplicationCloseBeforeHandshakeComplete: drop all
  client packets after the ClientHello so the server closes at key
  state .handshake, and check that the client disconnects without
  having to reject an APPLICATION_CLOSE
- Add testQUICClientClosesOnFrameNotAllowedDuringHandshake: the server
  sends HANDSHAKE_DONE in a Handshake packet and the client must close
  right away. This covers the receiver side on its own, since fixing
  the sender keeps the first test from reaching that path
- Add waitForServerConnected to QUICTestHarness so a test can continue
  once only the client is connected

Both tests fail until the fix in the next commit.
@josephnoir
josephnoir force-pushed the rh/error-during-handshake branch from 4ea57e5 to f6fce3d Compare October 6, 2026 13:15
A server that closes with an application error before it has processed
the client's Finished still has key state .handshake, so it sent
APPLICATION_CLOSE (0x1d) in a Handshake packet. Before apple#157 the client
processed that frame anyway. Since then it rejects it as a
PROTOCOL_VIOLATION, but the close is deferred during inbound processing
and was dropped when handleInboundPacket returned false, so the client
never reported the disconnect and waited for the idle timeout.

- Send CONNECTION_CLOSE (0x1c) with APPLICATION_ERROR and no reason
  instead of APPLICATION_CLOSE in Initial and Handshake packets
  (RFC 9000 §10.2.3)
- Run the deferred close when handleInboundPacket stops processing a
  datagram
@josephnoir
josephnoir force-pushed the rh/error-during-handshake branch from f6fce3d to 01a2a5d Compare October 6, 2026 13:15
@josephnoir

Copy link
Copy Markdown
Contributor Author

You are welcome! The additional close(in:) bothered me a bit in my first version. I took another pass and simply switching the error check with the continuation check works just the same and looks cleaner.

There is also an earlier close that is redundant since this is the only call site. I removed it in a separate commit, so I can easily revert that part if you would prefer to keep it.

@josephnoir
josephnoir marked this pull request as ready for review October 6, 2026 13:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🔨 semver/patch No public API change.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants