8000 Bound LDAP StartTLS shutdown with read timeout by atiqur-rahman-pro · Pull Request #53279 · keycloak/keycloak · GitHub
[go: up one dir, main page]

Skip to content

Bound LDAP StartTLS shutdown with read timeout - #53279

Merged
pedroigor merged 5 commits into
keycloak:mainfrom
atiqur-rahman-pro:fix/ldap-starttls-close
Oct 2, 2026
Merged

pedroigor merged 5 commits into
keycloak:mainfrom
atiqur-rahman-pro:fix/ldap-starttls-close

Conversation

@atiqur-rahman-pro
@atiqur-rahman-pro atiqur-rahman-pro commented Sep 29, 2026 •
Copy link
Copy Markdown
Contributor

Fixes the StartTLS close-notify hang that causes #51872 and #52603.

Root cause

When JNDI's background reader thread is blocked in an infinite socket read, OpenJDK's SSLSocketImpl lock hierarchy prevents StartTlsResponse.close() from acquiring the lock needed for graceful TLS shutdown. Calling setSoTimeout() on the SSLSocket does not interrupt an in-flight read. Consequently, StartTlsResponse.close() blocks indefinitely waiting for the read lock, hanging the server thread long enough to cause NoHttpResponseException and test timeouts on Windows CI.

Fix & Trade-off

Following maintainer guidance, LDAPStartTlsClose closes the captured underlying TCP transport socket first, before calling StartTlsResponse.close().

Closing the raw transport socket immediately interrupts the blocked reader with SocketException: Socket closed, releasing the read lock and guaranteeing that teardown completes without blocking.

Trade-off: This prioritizes avoiding hangs over graceful delivery of close_notify or LDAP Unbind. Since both callers (LDAPContextManager and LDAPOperationManager) dispose of the LDAP connection immediately, plaintext socket reuse is unnecessary. Context cleanups (ldapContext.close() and authCtx.close()) remain protected inside finally blocks.

Tests

  • LDAPStartTlsCloseTest#realTlsShutdownUnblocksBlockedReader: Establishes a real TLS 1.2 session layered over loopback TCP with autoClose=false. Verifies the client reader is blocked inside the underlying socket read before shutdown, leaves the peer connected without responding, runs shutdown on a daemon thread, verifies bounded unblocking/termination of both threads, and cleans up raw sockets in the test thread's finally block.
  • Unit tests verifying transport is closed prior to TLS response close and handling when transport close throws.
  • All 37 LDAP module tests pass.
  • spotless:check passes.

Closes #51872
Closes #52603

Copilot AI balanced review requested due to automatic review settings September 29, 2026 06:39
@atiqur-rahman-pro
atiqur-rahman-pro requested a review from a team as a code owner September 29, 2026 06:39
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.

Copilot review overview

🟡 Changes recommended

Successful JNDI shutdown skips restoring the underlying transport timeout because the layered TLS socket is already closed.

Review effort: Balanced
Findings: 1 Medium severity

Open (1)
What changed in this PR

Bounds LDAP StartTLS shutdown to prevent cleanup from blocking on close_notify.

Changes:

  • Adds tracked TLS socket shutdown with a 3-second timeout.
  • Integrates bounded cleanup into admin and authentication connections.
  • Adds focused shutdown tests.
File Description
LDAPStartTlsClose.java Implements tracked, bounded TLS shutdown.
LDAPContextManager.java Applies bounded shutdown to admin connections.
LDAPOperationManager.java Applies bounded shutdown to authentication cleanup.
LDAPStartTlsCloseTest.java Tests tracking, timeout, and forced closure.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Copilot AI review requested due to automatic review settings September 29, 2026 09:33
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.

Copilot review overview

🟡 Changes recommended

The timeout-restoration test currently passes without exercising the restoration branch.

Review effort: Balanced
Findings: 1 Low severity

Open (1)
Resolved since last review (1)

Copilot AI review requested due to automatic review settings September 29, 2026 09:44
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.

Copilot review overview

🟢 Approval recommended

The shutdown paths are consistently updated and covered by focused regression tests.

Review effort: Balanced
Findings: None

Resolved since last review (1)

@martin-kanis martin-kanis left a comment
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for this PR but I don't think it fixes all scenarios. Here is why:

The three-second socket timeout does not bound shutdown when JNDI’s reader thread is already blocked in a read.

With TLS 1.2 and JNDI’s layered socket (autoClose=false), TLS shutdown can wait for the read lock held by that reader. The pending read retains its original infinite timeout; calling setSoTimeout(3000) does
not update it. Consequently, tlsResponse.close() can remain blocked indefinitely, and the catch (IOException) fallback that closes the underlying transport is never reached. See OpenJDK’s socket read
implementation and TLS shutdown lock acquisition.

The current tests cannot catch this. They use fake sockets and explicitly throw SocketTimeoutException, which verifies cleanup after an exception but assumes the behavior that needs proving: that real TLS
shutdown actually times out.

For these teardown paths, a simpler fix would be to capture the original TCP socket passed to SSLSocketFactory.createSocket(Socket, …) and close it before invoking TLS cleanup. Closing the underlying transport
unblocks the reader. Keep LDAP context cleanup in finally. This sacrifices graceful close_notify and potentially LDAP Unbind delivery, but both callers are disposing of the connection immediately, so plaintext reuse is unnecessary.

A regression test should:

  • Establish a real TLS 1.2 session layered over a TCP socket with autoClose=false.
  • Start a client reader and verify it is blocked inside the TLS read before beginning shutdown; a latch fired before read() plus a sleep is insufficient.
  • Keep the peer connected without reading or responding to close_notify.
  • Assert that both shutdown and the reader terminate within a bounded time.
  • Run shutdown on a separate daemon thread and always close the raw sockets from the test thread in finally, so a regression fails the test without hanging cleanup.

Copilot AI review requested due to automatic review settings September 29, 2026 14:01
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.

Copilot review overview

🟡 Changes recommended

The implementation immediately aborts transport instead of providing the timeout-based graceful shutdown described by the PR.

Review effort: Balanced
Findings: 1 Medium severity

Open (1)

@atiqur-rahman-pro
Copy link
Copy Markdown
Contributor Author

Hi @martin-kanis,

Thank you martin for your valuable guidance and I have updated the PR accordingly.

Teardown: Close the underlying TCP transport before calling tlsResponse.close(), so a pending socket read is interrupted and TLS shutdown cannot wait indefinitely for the read lock.

Context cleanup: Keep ldapContext.close() and authCtx.close() protected by finally blocks.

Regression test: Add a real TLS 1.2 session layered over loopback TCP with autoClose=false. The test verifies the reader is blocked in the underlying socket read before shutdown, leaves the peer connected without reading, runs shutdown on a daemon thread, checks that both threads terminate within a bound, and closes the raw sockets in the test thread’s finally block.

Validation: the 5 focused tests pass on Java 17 and 21; all 37 LDAP module tests pass on Java 17; spotless:check passes.

Could you please take another look? thank you so much.

Copilot AI balanced review requested due to automatic review settings September 30, 2026 17:54
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.

Copilot review overview

🟢 Approval recommended

The implementation consistently applies the intentional teardown strategy and includes focused regression coverage.

Review effort: Balanced
Findings: None

Resolved since last review (1)

Signed-off-by: Atiqur Rahman <rahman.atiqur.pro@gmail.com>
Signed-off-by: Atiqur Rahman <rahman.atiqur.pro@gmail.com>
Signed-off-by: Atiqur Rahman <rahman.atiqur.pro@gmail.com>
Signed-off-by: Atiqur Rahman <rahman.atiqur.pro@gmail.com>
Signed-off-by: Atiqur Rahman <rahman.atiqur.pro@gmail.com>
Copilot AI balanced review requested due to automatic review settings October 2, 2026 10:36
@pedroigor
pedroigor force-pushed the fix/ldap-starttls-close branch from 73f1dd8 to cf01473 Compare October 2, 2026 10:36
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.

Copilot review overview

🟢 Approval recommended

The transport-first cleanup is consistently integrated and covered by focused regression tests.

Review effort: Balanced
Findings: None

@martin-kanis martin-kanis left a comment
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

30 stability reruns of Distribution IT https://github.com/martin-kanis/keycloak/actions/runs/36990030052 didn't reveal any issue.
@atiqur-rahman-pro Thank you!

@pedroigor
pedroigor enabled auto-merge (rebase) October 2, 2026 11:24
@pedroigor pedroigor 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.

@pedroigor
pedroigor merged commit 3a2ecae into keycloak:main Oct 2, 2026
157 of 159 checks passed
@atiqur-rahman-pro
atiqur-rahman-pro deleted the fix/ldap-starttls-close branch October 2, 2026 11:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

5 participants

0