Skip to content

Fix | Login TLS handshake hangs on Android (Conscrypt): read up to maxBytes during SSL handshake - #2980

Merged
David Engel (David-Engel) merged 3 commits into
mainfrom
david/android-conscrypt-tls-read
Jul 22, 2026
Merged

Fix | Login TLS handshake hangs on Android (Conscrypt): read up to maxBytes during SSL handshake#2980
David Engel (David-Engel) merged 3 commits into
mainfrom
david/android-conscrypt-tls-read

Conversation

@David-Engel

@David-Engel David Engel (David-Engel) commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

Problem description

On Android (which uses Conscrypt as its default TLS provider), establishing a JDBC connection hangs during the login-phase TLS handshake and eventually fails. SQL Server performs a TLS handshake during login even when encrypt=false, so both encrypt=false and encrypt=true are affected. In a shipped (assertions-off) build the connect blocks until loginTimeout and throws SQLServerException: ... could not establish a secure connection ... Read timed out. In a debuggable build (where the Android Gradle Plugin / d8 force-enables Java assertions) it fails earlier with AssertionError: numMsgsRcvd:1 should be less than numMsgsSent:1 from TDSReader.readPacket — the same root cause, surfaced earlier by an assertion.

Root cause: TDSChannel.SSLHandshakeInputStream.readInternal(byte[], int, int) read exactly maxBytes (blocking across TDS packets) and always returned maxBytes, instead of reading up to maxBytes and returning the actual number of bytes read as the InputStream.read contract requires. SunJSSE reads the TLS handshake in small, exact record-sized chunks and never requests more than is available, so the defect is masked. Conscrypt issues a single large read (its buffer size, e.g. 16709 bytes); the driver then blocks trying to fill 16709 bytes while the server has already sent its complete handshake flight (~1.5 KB in one TDS packet) and is waiting for the client's next flight — a deadlock.

Fixes existing GitHub issue

Fixes #2979

Fix Description

In SSLHandshakeInputStream.readInternal, after ensureSSLPayload() guarantees at least one buffered TDS packet, read Math.min(maxBytes, tdsReader.available()) bytes and return the actual count instead of blocking for the full maxBytes. TDSReader.available() reports only non-blocking buffered bytes, so the handshake input stream now honors the read-up-to contract and hands each buffered chunk to the SSL engine, which can then advance the handshake. A FINEST log records when a short read occurs. There is no behavior change for SunJSSE, which already read within a single packet.

New Public APIs

None.

Testing

@codecov

codecov Bot commented Jul 7, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 77.77778% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 59.15%. Comparing base (76ac158) to head (8bf1850).
⚠️ Report is 4 commits behind head on main.

Files with missing lines Patch % Lines
...in/java/com/microsoft/sqlserver/jdbc/IOBuffer.java 77.77% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##               main    #2980      +/-   ##
============================================
- Coverage     59.25%   59.15%   -0.10%     
+ Complexity     5058     5023      -35     
============================================
  Files           153      153              
  Lines         36339    36343       +4     
  Branches       6645     6647       +2     
============================================
- Hits          21531    21499      -32     
+ Misses        11091    11088       -3     
- Partials       3717     3756      +39     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@David-Engel
David Engel (David-Engel) marked this pull request as draft July 8, 2026 00:35
The read-up-to change lets the handshake input stream re-enter ensureSSLPayload
mid-response when a server handshake message (e.g. a large certificate chain)
spans multiple TDS packets. endMessage() then ran with no buffered output and
tripped 'assert messageStarted'. Make endMessage() a no-op when nothing is
buffered. Verified against a multi-TDS-packet server certificate with assertions
enabled on desktop (SunJSSE) and Android (Conscrypt), release and debug.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a TLS login-handshake deadlock affecting Android/Conscrypt by making the driver’s SSL handshake InputStream honor the InputStream.read(..) “read up to N bytes” contract, preventing the driver from blocking while trying to fill an oversized TLS-provider buffer.

Changes:

  • Update SSLHandshakeInputStream.readInternal(..) to read up to maxBytes based on currently buffered TDS payload, returning the actual count instead of always blocking for/returning maxBytes.
  • Relax SSLHandshakeOutputStream.endMessage() to no-op when no handshake output message is currently started (avoids assertion failures during multi-packet handshake responses).

Comment thread src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java
Address PR review: readInternal returned min(maxBytes, tdsReader.available()),
which could be 0 if ensureSSLPayload() read a zero-length-payload TDS packet.
Returning 0 for maxBytes > 0 violates the InputStream.read contract and can make
SSL engines busy-spin (our own single-byte read() loops while 0).

Loop ensureSSLPayload() until at least one payload byte is buffered when
maxBytes > 0. This is bounded: readPacket() blocks for server data and
terminates on premature EOF, so it cannot spin. The normal path (payload already
present) never iterates.
// non-zero request, which would violate the InputStream.read contract and can make
// SSL engines busy-spin. Reading the next packet is bounded: readPacket() blocks
// for server data and terminates on premature EOF, so this cannot spin.
while (maxBytes > 0 && 0 == tdsReader.available())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hopefully there are no side effects on the existing mechanism/behavior for SunJSSE path.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@David-Engel
David Engel (David-Engel) enabled auto-merge (squash) July 20, 2026 23:28
@David-Engel
David Engel (David-Engel) merged commit 40ad6d2 into main Jul 22, 2026
22 of 23 checks passed
@github-project-automation github-project-automation Bot moved this from In progress to Closed/Merged PRs in MSSQL JDBC Jul 22, 2026
@machavan Mahendra Chavan (machavan) added this to the 13.5.1 milestone Jul 22, 2026
@David-Engel
David Engel (David-Engel) deleted the david/android-conscrypt-tls-read branch August 3, 2026 16:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Closed/Merged PRs

Development

Successfully merging this pull request may close these issues.

Connection hangs during login TLS handshake on Android (Conscrypt)

4 participants