Uploaded image for project: 'JDK'
  1. JDK
  2. JDK-8216326

SSLSocket stream close() does not close the associated socket

    Details

    • Subcomponent:
    • Introduced In Version:
      11
    • Resolved In Build:
      b17
    • CPU:
      x86_64
    • OS:
      generic
    • Verification:
      Verified

      Backports

        Description

        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


          Attachments

            Issue Links

              Activity

                People

                • Assignee:
                  xuelei Xue-Lei Fan
                  Reporter:
                  webbuggrp Webbug Group
                • Votes:
                  0 Vote for this issue
                  Watchers:
                  4 Start watching this issue

                  Dates

                  • Created:
                    Updated:
                    Resolved: