-
Bug
-
Resolution: Unresolved
-
P4
-
8, 11, 17, 18, 19, 20
-
generic
-
generic
ADDITIONAL SYSTEM INFORMATION :
Tested on both Linux and Windows machines with multiple versions of Java 11, up to 11.0.17.
A DESCRIPTION OF THE PROBLEM :
While writing data out from a server to a client over an SSLSocket, I discovered that when the client was reading too slowly, we were unable to close the connection on the server side while data was still buffered on the socket. Calls to socket.close() would block forever. After investigating, I discovered that this would happen when using the default SO_LINGER values, which should disable it completely. When I enabled SO_LINGER with any value, the connection would close after the specified time limit, and immediately if I used (true,0) for SO_LINGER, as expected. Looking at the source code for SSLSocketImpl.java, I saw that the closeNotify method on line 668 doesn't work as expected. If SO_LINGER is greater than or equal to 0, it will attempt to acquire a recordLock for up to SO_LINGER amount of time before starting to close the connection. However, in the else clause(SO_LINGER is less than 0 and disabled), it will try to acquire the lock with no timeout. If the client is hung and not reading any data off the socket, then can cause the socket to hang forever, still established.
STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
1. Create an SSLServerSocket.
2. Using this socket, accept a client connection.
3. On the client side, read an initial chuck of data, then stop and wait.
4. On the server side, write data to the socket until the buffer is full.(Write will block at this point)
5. In a separate thread, "detect" that there is a slow client and attempt to close the connection with socket.close().
EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
Since SO_LINGER is disabled by default, the connection is expected to close immediately without waiting.
ACTUAL -
The close will hang while waiting to acquire a write lock and not actually close.
---------- BEGIN SOURCE ----------
@Test
public void test() throws Exception {
//Some insanity to get a working keystore/truststore locally.
TrustManager tm = new X509TrustManager() {
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
}
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
}
public X509Certificate[] getAcceptedIssuers() {
return null;
}
};
KeyStore keystore = KeyStore.getInstance("pkcs12");
char[] password = "password".toCharArray();
keystore.load( new FileInputStream( new File( "C:\\path\\to\\my\\certificate.p12") ), password );
KeyManagerFactory kmf = KeyManagerFactory.getInstance( KeyManagerFactory.getDefaultAlgorithm());
kmf.init( keystore, password);
SSLContext sslContext = null;
try {
sslContext = SSLContext.getInstance("tls");
sslContext.init( kmf.getKeyManagers(), new TrustManager[] { tm }, null);
} catch (NoSuchAlgorithmException e1) {
e1.printStackTrace();
System.exit( 1 );
} catch (KeyManagementException e) {
e.printStackTrace();
}
//Start the client first.
TestClient client = new TestClient(sslContext);
Thread t = new Thread(client);
t.start();
TestServer server = new TestServer( sslContext );
Thread s = new Thread( server );
s.start();
Thread.sleep( 5000 );
System.out.println("Slow client, better kick them off...");
server.closeConnection();
System.out.println("Connection should be successfully closed now.");
}
private class TestServer implements Runnable {
boolean running = true;
SSLContext sslContext;
SSLSocket outConn;
public TestServer( SSLContext ctx ) {
this.sslContext = ctx;
}
public void closeConnection() throws IOException
{
outConn.close();
}
public void run() {
SSLServerSocket serverSock = null;
try {
serverSock = (SSLServerSocket) sslContext
.getServerSocketFactory().createServerSocket(12345);
serverSock.setNeedClientAuth(false);
serverSock.setWantClientAuth(false);
//Accept the connection
outConn = (SSLSocket) serverSock.accept();
//Uncomment this line for it to succeed.
//outConn.setSoLinger( true, 0);
System.out.println("Connection SO_LINGER: " + outConn.getSoLinger() );
//Write some random data to the socket
ExecutorService executor = Executors.newFixedThreadPool(1);
Callable<Void> task = new Callable<Void>() {
public Void call() throws Exception {
Random rd = new Random();
byte[] arr = new byte[3000000];
rd.nextBytes(arr);
outConn.getOutputStream().write(arr);
return null;
}
};
Future<Void> serverFuture = executor.submit(task);
try {
serverFuture.get( 20000, TimeUnit.MILLISECONDS );
System.out.println("Finished writing data.");
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
} catch (TimeoutException e) {
System.out.println("Timed out trying to write data.");
}
} catch (IOException e) {
e.printStackTrace();
}
finally {
if ( serverSock != null ) {
try {
serverSock.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
private class TestClient implements Runnable {
boolean running = true;
SSLContext sslContext;
public TestClient(SSLContext sslContext) {
this.sslContext = sslContext;
}
public void run() {
int i = 0;
while ( running ) {
SSLSocket sslSock = null;
try {
sslSock = (SSLSocket) sslContext.getSocketFactory().createSocket();
sslSock.setUseClientMode(true);
sslSock.connect( new InetSocketAddress( "localhost", 12345), 2000);
//Read very slowly off the socket
sslSock.getInputStream().read();
while ( sslSock.isConnected() && i < 30 )
{
try {
Thread.sleep( 1000 );
i++;
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
if ( i >= 30 ) {
System.out.println( "Client thread timing out now.");
}
} catch (IOException e) {
System.out.println("Client exception: " + e.getMessage());
}
finally {
if ( sslSock != null )
{
try {
sslSock.close();
} catch (IOException e) {
//Ignore
}
}
}
}
}
}
---------- END SOURCE ----------
CUSTOMER SUBMITTED WORKAROUND :
Setting SO_LINGER to true with a 0 second timeout will cause the socket to immediately close.
FREQUENCY : always
Tested on both Linux and Windows machines with multiple versions of Java 11, up to 11.0.17.
A DESCRIPTION OF THE PROBLEM :
While writing data out from a server to a client over an SSLSocket, I discovered that when the client was reading too slowly, we were unable to close the connection on the server side while data was still buffered on the socket. Calls to socket.close() would block forever. After investigating, I discovered that this would happen when using the default SO_LINGER values, which should disable it completely. When I enabled SO_LINGER with any value, the connection would close after the specified time limit, and immediately if I used (true,0) for SO_LINGER, as expected. Looking at the source code for SSLSocketImpl.java, I saw that the closeNotify method on line 668 doesn't work as expected. If SO_LINGER is greater than or equal to 0, it will attempt to acquire a recordLock for up to SO_LINGER amount of time before starting to close the connection. However, in the else clause(SO_LINGER is less than 0 and disabled), it will try to acquire the lock with no timeout. If the client is hung and not reading any data off the socket, then can cause the socket to hang forever, still established.
STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
1. Create an SSLServerSocket.
2. Using this socket, accept a client connection.
3. On the client side, read an initial chuck of data, then stop and wait.
4. On the server side, write data to the socket until the buffer is full.(Write will block at this point)
5. In a separate thread, "detect" that there is a slow client and attempt to close the connection with socket.close().
EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
Since SO_LINGER is disabled by default, the connection is expected to close immediately without waiting.
ACTUAL -
The close will hang while waiting to acquire a write lock and not actually close.
---------- BEGIN SOURCE ----------
@Test
public void test() throws Exception {
//Some insanity to get a working keystore/truststore locally.
TrustManager tm = new X509TrustManager() {
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
}
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
}
public X509Certificate[] getAcceptedIssuers() {
return null;
}
};
KeyStore keystore = KeyStore.getInstance("pkcs12");
char[] password = "password".toCharArray();
keystore.load( new FileInputStream( new File( "C:\\path\\to\\my\\certificate.p12") ), password );
KeyManagerFactory kmf = KeyManagerFactory.getInstance( KeyManagerFactory.getDefaultAlgorithm());
kmf.init( keystore, password);
SSLContext sslContext = null;
try {
sslContext = SSLContext.getInstance("tls");
sslContext.init( kmf.getKeyManagers(), new TrustManager[] { tm }, null);
} catch (NoSuchAlgorithmException e1) {
e1.printStackTrace();
System.exit( 1 );
} catch (KeyManagementException e) {
e.printStackTrace();
}
//Start the client first.
TestClient client = new TestClient(sslContext);
Thread t = new Thread(client);
t.start();
TestServer server = new TestServer( sslContext );
Thread s = new Thread( server );
s.start();
Thread.sleep( 5000 );
System.out.println("Slow client, better kick them off...");
server.closeConnection();
System.out.println("Connection should be successfully closed now.");
}
private class TestServer implements Runnable {
boolean running = true;
SSLContext sslContext;
SSLSocket outConn;
public TestServer( SSLContext ctx ) {
this.sslContext = ctx;
}
public void closeConnection() throws IOException
{
outConn.close();
}
public void run() {
SSLServerSocket serverSock = null;
try {
serverSock = (SSLServerSocket) sslContext
.getServerSocketFactory().createServerSocket(12345);
serverSock.setNeedClientAuth(false);
serverSock.setWantClientAuth(false);
//Accept the connection
outConn = (SSLSocket) serverSock.accept();
//Uncomment this line for it to succeed.
//outConn.setSoLinger( true, 0);
System.out.println("Connection SO_LINGER: " + outConn.getSoLinger() );
//Write some random data to the socket
ExecutorService executor = Executors.newFixedThreadPool(1);
Callable<Void> task = new Callable<Void>() {
public Void call() throws Exception {
Random rd = new Random();
byte[] arr = new byte[3000000];
rd.nextBytes(arr);
outConn.getOutputStream().write(arr);
return null;
}
};
Future<Void> serverFuture = executor.submit(task);
try {
serverFuture.get( 20000, TimeUnit.MILLISECONDS );
System.out.println("Finished writing data.");
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
} catch (TimeoutException e) {
System.out.println("Timed out trying to write data.");
}
} catch (IOException e) {
e.printStackTrace();
}
finally {
if ( serverSock != null ) {
try {
serverSock.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
private class TestClient implements Runnable {
boolean running = true;
SSLContext sslContext;
public TestClient(SSLContext sslContext) {
this.sslContext = sslContext;
}
public void run() {
int i = 0;
while ( running ) {
SSLSocket sslSock = null;
try {
sslSock = (SSLSocket) sslContext.getSocketFactory().createSocket();
sslSock.setUseClientMode(true);
sslSock.connect( new InetSocketAddress( "localhost", 12345), 2000);
//Read very slowly off the socket
sslSock.getInputStream().read();
while ( sslSock.isConnected() && i < 30 )
{
try {
Thread.sleep( 1000 );
i++;
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
if ( i >= 30 ) {
System.out.println( "Client thread timing out now.");
}
} catch (IOException e) {
System.out.println("Client exception: " + e.getMessage());
}
finally {
if ( sslSock != null )
{
try {
sslSock.close();
} catch (IOException e) {
//Ignore
}
}
}
}
}
}
---------- END SOURCE ----------
CUSTOMER SUBMITTED WORKAROUND :
Setting SO_LINGER to true with a 0 second timeout will cause the socket to immediately close.
FREQUENCY : always