ADDITIONAL SYSTEM INFORMATION : OS: macOS 10.13.6 (17G4015). JDKs with this bug (11): - openjdk 11.0.1 2018-10-16, OpenJDK Runtime Environment 18.9 (build 11.0.1+13), OpenJDK 64-Bit Server VM 18.9 (build 11.0.1+13, mixed mode); - openjdk 11 2018-09-25, OpenJDK Runtime Environment 18.9 (build 11+28), OpenJDK 64-Bit Server VM 18.9 (build 11+28, mixed mode). A DESCRIPTION OF THE PROBLEM : The method SSLSocket.getOutputStream() has the same specification as Socket.getOutputStream(), and the specification says: "Closing the returned OutputStream will close the associated socket" (see https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/net/Socket.html#getOutputStream()). However, starting with JDK 11 this behaviour is broken for SSLSocket, and still works for Socket. REGRESSION : Last worked in version 10.0.2 STEPS TO FOLLOW TO REPRODUCE THE PROBLEM : Step 0. It may be more convenient to get the description and the source code of the minimal working example here: https://github.com/stIncMale/JDK11-SSLSocket.getOutputStream.close-bug Otherwise, proceed with the following steps. Step 1. Create file "./keystore.jks" with the following command: keytool -keystore ./keystore.jks -storepass "password" -storetype JKS -genkeypair -dname "CN=Unknown" -alias "myKey" -keypass "password" -keyalg RSA -validity 999999 Create file "./SslSocketOutputStreamCloseBug.java" by using the source code from "Source code for an executable test case". Step 2. Run the test with the following command: java -Djavax.net.ssl.trustStore=./keystore.jks -Djavax.net.ssl.trustStorePassword=password -Djavax.net.ssl.keyStore=./keystore.jks -Djavax.net.ssl.keyStorePassword=password ./SslSocketOutputStreamCloseBug.java If you want to run it with any JDK before JDK 11 (e.g. JDK 10), use the following command: javac ./SslSocketOutputStreamCloseBug.java && java -Djavax.net.ssl.trustStore=./keystore.jks -Djavax.net.ssl.trustStorePassword=password -Djavax.net.ssl.keyStore=./keystore.jks -Djavax.net.ssl.keyStorePassword=password SslSocketOutputStreamCloseBug ; rm ./SslSocketOutputStreamCloseBug*.class EXPECTED VERSUS ACTUAL BEHAVIOR : EXPECTED - The program outputs among other messages "Client �������� Closed the socket" and exits. ACTUAL - The program outputs among other messages "Client �������� Failed to close the socket" and hangs infinitely printing "Client �������� Still waiting for data from the server...". I observed the expected result with JDK 10, 9, 8: - openjdk version "10.0.2" 2018-07-17, OpenJDK Runtime Environment 18.3 (build 10.0.2+13), OpenJDK 64-Bit Server VM 18.3 (build 10.0.2+13, mixed mode); - openjdk version "10.0.1" 2018-04-17, OpenJDK Runtime Environment (build 10.0.1+10), OpenJDK 64-Bit Server VM (build 10.0.1+10, mixed mode); - openjdk version "10" 2018-03-20, OpenJDK Runtime Environment 18.3 (build 10+46), OpenJDK 64-Bit Server VM 18.3 (build 10+46, mixed mode); - openjdk 9.0.4, OpenJDK Runtime Environment (build 9.0.4+11), OpenJDK 64-Bit Server VM (build 9.0.4+11, mixed mode); - java version "1.8.0_162", Java(TM) SE Runtime Environment (build 1.8.0_162-b12), Java HotSpot(TM) 64-Bit Server VM (build 25.162-b12, mixed mode). ---------- BEGIN SOURCE ---------- //file "./SslSocketOutputStreamCloseBug.java" import java.io.IOException; import java.io.InputStream; import java.net.InetSocketAddress; import java.net.ServerSocket; import java.net.Socket; import java.net.SocketAddress; import java.util.Locale; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import javax.net.ssl.SSLServerSocketFactory; import javax.net.ssl.SSLSocket; import javax.net.ssl.SSLSocketFactory; class SslSocketOutputStreamCloseBug { public static void main(String... args) throws IOException, InterruptedException, ExecutionException { boolean useTls = args.length == 0 || !args[0].equalsIgnoreCase("noTls"); SocketAddress serverSocketAddress = startServer(useTls); try (Socket clientSideSocket = useTls ? SSLSocketFactory.getDefault().createSocket() : new Socket()) { clientSideSocket.setSoTimeout(0); logClient("Connecting to " + serverSocketAddress); clientSideSocket.connect(serverSocketAddress, 0); if (clientSideSocket instanceof SSLSocket) { ((SSLSocket)clientSideSocket).startHandshake(); } logClient("Connected via the socket " + clientSideSocket); InputStream clientSideInputStream = clientSideSocket.getInputStream(); Thread clientSideReadingThread = new Thread(() -> { try { logClient("Waiting for data from the server"); logClient("Received " + toHexOrEof(clientSideInputStream.read()));//wait for data from the server that never sends any } catch (IOException e) { //clientSideSocket was closed } catch (RuntimeException e) { e.printStackTrace(System.out); } finally { logClient("Stopped waiting for data from the server"); } }); clientSideReadingThread.start(); clientSideReadingThread.join(500);//wait until clientSideReadingThread starts reading logClient("Closing the socket"); clientSideSocket.getOutputStream().close();//must close clientSideSocket if (clientSideSocket.isClosed()) { logClient("Closed the socket"); } else { logClient("Failed to close the socket"); } do {//wait until clientSideReadingThread dies clientSideReadingThread.join(1000); if (clientSideReadingThread.isAlive()) { logClient("Still waiting for data from the server..."); } else { break; } } while (true); } finally { logClient("Disconnected"); } } private static SocketAddress startServer(boolean useTls) throws ExecutionException, InterruptedException { CompletableFuture<SocketAddress> resultFuture = new CompletableFuture<>(); Thread serverSideThread = new Thread(() -> { try (ServerSocket serverSocket = useTls ? SSLServerSocketFactory.getDefault().createServerSocket() : new ServerSocket()) { SocketAddress serverSocketAddress = new InetSocketAddress("localhost", 0); logServer("Starting on " + serverSocketAddress); serverSocket.setSoTimeout(0); serverSocket.bind(serverSocketAddress); resultFuture.complete(new InetSocketAddress(serverSocket.getInetAddress(), serverSocket.getLocalPort())); logServer("Accepting connections on " + serverSocket); Socket serverSideSocket = serverSocket.accept(); logServer("Accepted a connection from " + serverSideSocket.getRemoteSocketAddress()); logServer("Waiting for data from the client " + serverSideSocket.getRemoteSocketAddress()); logServer("Received " + toHexOrEof(serverSideSocket.getInputStream().read()));//wait for data from the client that never sends any } catch (IOException | RuntimeException e) { resultFuture.completeExceptionally(e); } finally { resultFuture.completeExceptionally(new RuntimeException()); logServer("Shut down"); } }); serverSideThread.start(); return resultFuture.get(); } private static void logServer(Object msg) { System.out.println("Server (:) " + msg); } private static void logClient(Object msg) { System.out.println("Client \uD83D\uDD0C " + msg); } private static String toHexOrEof(int baseTenByte) { return baseTenByte == -1 ? "EOF" : String.format(Locale.ROOT, "0x%02X", baseTenByte); } } ---------- END SOURCE ---------- CUSTOMER SUBMITTED WORKAROUND : As a workaround in projects we can: - Close an SSLSocket directly via SSLSocket.close(). - Use Socket :) At least Socket.getOutputStream().close() does not have this bug. FREQUENCY : always
|