Skip to content

Fix stalled TLS handshake when the server's first flight is too large - #1070

Closed
lhellebr wants to merge 2 commits into
apache:mainfrom
lhellebr:bz-flight-stall
Closed

lhellebr wants to merge 2 commits into
apache:mainfrom
lhellebr:bz-flight-stall

Conversation

@lhellebr

@lhellebr lhellebr commented Sep 18, 2026 •

Copy link
Copy Markdown

Fixes bug 70236 (see that bug report for more in depth details on why the fix looks like this and why it may not be the optimal solution).

OpenSSL writes a complete handshake flight to the network BIO in a single operation. The BIO pair used by the OpenSSL based SSLEngine implementations is created with the default 17408 byte buffer so, when the flight is larger than that - which in practice requires a large certificate chain - OpenSSL retains the remainder internally. wrap() copies whatever the BIO holds into the destination buffer and returns NEED_UNWRAP without giving OpenSSL any opportunity to write the rest.

The remainder is currently only written as a side effect of the priming SSL_read() performed by a later call to unwrap(). That hides the problem whenever the client has already sent something - a TLSv1.3 client normally sends a middlebox compatibility change cipher spec record - but a client that is waiting for the server to complete its flight sends nothing, so unwrap() is never reached and the connection stalls until it times out. With a TLSv1.2 client the failure is deterministic.

After the network BIO has been drained by wrap(), and while a handshake is still in progress, drive OpenSSL again so it can write out whatever did not previously fit. When there is nothing left to write the call is a no-op. A dedicated method is used rather than handshake() because handshake() re-snapshots the handshake counter that is used to detect completion, which must not happen in the middle of a flight. Neither implementation binds SSL_get_error() so it is not currently possible to test for SSL_ERROR_WANT_WRITE and make the call conditional.

OpenSSL writes a complete handshake flight to the network BIO in a single
operation. The BIO pair used by the OpenSSL based SSLEngine implementations
is created with the default 17408 byte buffer so, when the flight is larger
than that - which in practice requires a large certificate chain - OpenSSL
retains the remainder internally. wrap() copies whatever the BIO holds into
the destination buffer and returns NEED_UNWRAP without giving OpenSSL any
opportunity to write the rest.

The remainder is currently only written as a side effect of the priming
SSL_read() performed by a later call to unwrap(). That hides the problem
whenever the client has already sent something - a TLSv1.3 client normally
sends a middlebox compatibility change cipher spec record - but a client
that is waiting for the server to complete its flight sends nothing, so
unwrap() is never reached and the connection stalls until it times out.
With a TLSv1.2 client the failure is deterministic.

After the network BIO has been drained by wrap(), and while a handshake is
still in progress, drive OpenSSL again so it can write out whatever did not
previously fit. When there is nothing left to write the call is a no-op.
A dedicated method is used rather than handshake() because handshake()
re-snapshots the handshake counter that is used to detect completion, which
must not happen in the middle of a flight. Neither implementation binds
SSL_get_error() so it is not currently possible to test for
SSL_ERROR_WANT_WRITE and make the call conditional.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@rmaucher

Copy link
Copy Markdown
Contributor

I had read about this class of issues, but of course I assumed this was not a problem here.

@markt-asf

Copy link
Copy Markdown
Contributor

I haven't looked at the merits of the bug report or the proposed fix but the test cases add new keys and certs with no information provided on how to (re-)generate them. The current test certificate generation code that sits in the PMC private repo probably needs to be moved to trunk so this PR can update it to add the generation of these new keys and certs.

@lhellebr

Copy link
Copy Markdown
Author

The reason I provided own certs is that we need RSA 8192 (or something else big enough) CA + certs for reproducing. Rather than always generating them (which would take considerable time), I've provided them. Also, these are not typical fixtures you could use in another test as well - it's a long chain of big certs made specifically to reproduce this issue.

I can provide a script to generate these and it can certainly run in automation if it's a one-time run, I would just prefer not to run it every time the tests run.

But I am not a regular contributor to Tomcat so I may be missing something - if there is another, better way, let me know what to change.

@rmaucher

Copy link
Copy Markdown
Contributor

Personally, I agree with that rationale. I saw the PR with the certs, I was initially surprised but then since this is very special purpose to demonstrate the overflow, it seemed logical.

@markt-asf

Copy link
Copy Markdown
Contributor

The current key/cert generation is not run on every test. It runs every couple of years when the certs expire or sooner if we need to expand the set of test keys/certs.

My point was that a PR that adds text keys and certs with no way to regenerate them is setting us up for a future set of test failures. This PR should add to the existing generation script but can't (at least directly) because that script is in a private repository. My suggestion was to move that script to this repo. That is a little more complex than I'd like as the ImportKey utility is not ALv2 licensed.

I wouldn't want the complexities of moving the current test key/cert generation to delay this PR. A script that generates the keys/certs this PR requires would be sufficient. We could either use it as-is or merge it into the existing script.

@lhellebr

Copy link
Copy Markdown
Author

Do you want the script just commented in this PR, or do you want it be a part of the code? If yes, do you prefer just a comment next to the text, or a separate file?

@lhellebr

Copy link
Copy Markdown
Author

Script added as test/org/apache/tomcat/util/net/generate-longchain-certs.sh

@rmaucher

Copy link
Copy Markdown
Contributor

Merged since the script for the certs was provided. Edited due to changelog insertion (also don't want to make it too long).

@rmaucher rmaucher closed this Sep 21, 2026
@csutherl

Copy link
Copy Markdown
Member

FFR so it's easier to find later, the commit is 9e9d3a89b3 for main (12.0.x).

@lhellebr

Copy link
Copy Markdown
Author

Thanks for merging and cherrypicking!
Should I also mark the reported BZ as resolved?

@csutherl

Copy link
Copy Markdown
Member

@lhellebr @rmaucher closed it out after backporting. Thanks for the report!

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants