Repository navigation
Bound LDAP StartTLS shutdown with read timeout #53279
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
pedroigor
merged 5 commits into
keycloak:main
from
atiqur-rahman-pro:fix/ldap-starttls-close
Oct 2, 2026
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
f5117d1
Bound LDAP StartTLS shutdown with read timeout (#51872)
atiqur-rahman-pro f315deb
Restore timeout on transport socket when SSLSocket is closed
atiqur-rahman-pro 6fdaec1
Verify socket timeout propagation to transport in TLS close test
atiqur-rahman-pro 045ab26
Close underlying LDAP transport socket to unblock StartTLS teardown
atiqur-rahman-pro cf01473
Use TrustManagerFactory with test certificate to resolve CodeQL alert
atiqur-rahman-pro File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
136 changes: 136 additions & 0 deletions
136
...ration/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/LDAPStartTlsClose.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,136 @@ | ||
| /* | ||
| * Copyright 2026 Red Hat, Inc. and/or its affiliates | ||
| * and other contributors as indicated by the @author tags. | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
|
|
||
| package org.keycloak.storage.ldap.idm.store.ldap; | ||
|
|
||
| import java.io.IOException; | ||
| import java.net.InetAddress; | ||
| import java.net.Socket; | ||
| import java.util.concurrent.atomic.AtomicReference; | ||
| import javax.naming.ldap.StartTlsResponse; | ||
| import javax.net.ssl.SSLSocket; | ||
| import javax.net.ssl.SSLSocketFactory; | ||
|
|
||
| import org.jboss.logging.Logger; | ||
|
|
||
| /** | ||
| * Tracks the TLS socket negotiated by JNDI and ensures StartTLS teardown does not block. | ||
| * | ||
| * <p>Closing the underlying transport socket before invoking {@link StartTlsResponse#close()} | ||
| * unblocks any JNDI reader thread that may be holding the TLS read lock, which would otherwise | ||
| * prevent {@code close()} from acquiring it and returning. {@code setSoTimeout} alone is | ||
| * insufficient because it does not interrupt a read already in progress. | ||
| */ | ||
| final class LDAPStartTlsClose { | ||
|
|
||
| private static final Logger logger = Logger.getLogger(LDAPStartTlsClose.class); | ||
|
|
||
| private LDAPStartTlsClose() { | ||
| } | ||
|
|
||
| static SSLSocketFactory trackingFactory(SSLSocketFactory delegate, AtomicReference<SSLSocket> socket, AtomicReference<Socket> transport) { | ||
| return new TrackingSSLSocketFactory(delegate, socket, transport); | ||
| } | ||
|
|
||
| static void close(StartTlsResponse tlsResponse, SSLSocket socket, Socket transport) { | ||
| if (tlsResponse == null) { | ||
| return; | ||
| } | ||
|
|
||
| // Close the underlying TCP transport first. This unblocks any JNDI reader thread | ||
| // that is blocked inside a TLS read and holds the lock that StartTlsResponse.close() | ||
| // needs to acquire. Without this, close() can block indefinitely regardless of | ||
| // any socket timeout set on the SSLSocket layer. | ||
| closeSocket(socket, transport); | ||
|
|
||
| try { | ||
| tlsResponse.close(); | ||
| } catch (IOException e) { | ||
| logger.debug("Could not close LDAP TLS response after transport was closed.", e); | ||
| } | ||
| } | ||
|
|
||
| private static void closeSocket(SSLSocket socket, Socket transport) { | ||
| try { | ||
| if (transport != null) { | ||
| transport.close(); | ||
| } else if (socket != null) { | ||
| socket.close(); | ||
| } | ||
| } catch (Exception e) { | ||
| logger.debug("Could not close LDAP TLS socket.", e); | ||
| } | ||
| } | ||
|
|
||
| private static final class TrackingSSLSocketFactory extends SSLSocketFactory { | ||
|
|
||
| private final SSLSocketFactory delegate; | ||
| private final AtomicReference<SSLSocket> socket; | ||
| private final AtomicReference<Socket> transport; | ||
|
|
||
| private TrackingSSLSocketFactory(SSLSocketFactory delegate, AtomicReference<SSLSocket> socket, AtomicReference<Socket> transport) { | ||
| this.delegate = delegate; | ||
| this.socket = socket; | ||
| this.transport = transport; | ||
| } | ||
|
|
||
| private Socket track(Socket created, Socket underlyingTransport) throws IOException { | ||
| if (!(created instanceof SSLSocket)) { | ||
| created.close(); | ||
| throw new IOException("LDAP StartTLS socket factory did not create an SSLSocket"); | ||
| } | ||
| socket.set((SSLSocket) created); | ||
| transport.set(underlyingTransport == null ? created : underlyingTransport); | ||
| return created; | ||
| } | ||
|
|
||
| @Override | ||
| public String[] getDefaultCipherSuites() { | ||
| return delegate.getDefaultCipherSuites(); | ||
| } | ||
|
|
||
| @Override | ||
| public String[] getSupportedCipherSuites() { | ||
| return delegate.getSupportedCipherSuites(); | ||
| } | ||
|
|
||
| @Override | ||
| public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException { | ||
| return track(delegate.createSocket(s, host, port, autoClose), s); | ||
| } | ||
|
|
||
| @Override | ||
| public Socket createSocket(String host, int port) throws IOException { | ||
| return track(delegate.createSocket(host, port), null); | ||
| } | ||
|
|
||
| @Override | ||
| public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException { | ||
| return track(delegate.createSocket(host, port, localHost, localPort), null); | ||
| } | ||
|
|
||
| @Override | ||
| public Socket createSocket(InetAddress host, int port) throws IOException { | ||
| return track(delegate.createSocket(host, port), null); | ||
| } | ||
|
|
||
| @Override | ||
| public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException { | ||
| return track(delegate.createSocket(address, port, localAddress, localPort), null); | ||
| } | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.