JDK-8216326 : SSLSocket stream close() does not close the associated socket
  • Type: Bug
  • Component: security-libs
  • Sub-Component: javax.net.ssl
  • Affected Version: 11,12
  • Priority: P2
  • Status: Closed
  • Resolution: Fixed
  • OS: generic
  • CPU: x86_64
  • Submitted: 2019-01-05
  • Updated: 2020-11-23
  • Resolved: 2019-04-17
The Version table provides details related to the release that this issue/RFE will be addressed.

Unresolved : Release in which this issue/RFE will be addressed.
Resolved: Release in which this issue/RFE has been resolved.
Fixed : Release in which this issue/RFE has been fixed. The release containing this fix may be available for download as an Early Access Release or a General Availability Release.

To download the current JDK release, click here.
JDK 11 JDK 13 JDK 8 Other
11.0.5-oracleFixed 13 b17Fixed 8u261Fixed openjdk8u272Fixed
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



Comments
Fix request (11u): Request backport of this fix for parity with Oracle 11.0.5. Patch applies cleanly and will be run through SAP's test system.
23-06-2019

Once again a half-close caused issue.
14-01-2019

To reproduce the issue, run the attached test case. JDK 10.0.2 - Pass JDK 11 GA - Fail JDK 11.0.1 - Fail JDK 12-ea+26 - Fail Output in failed versions: Server (:) Starting on localhost/127.0.0.1:0 Server (:) Accepting connections on [SSL: ServerSocket[addr=localhost/127.0.0.1,localport=55951]] Client ? Connecting to localhost/127.0.0.1:55951 Server (:) Accepted a connection from /127.0.0.1:55952 Server (:) Waiting for data from the client /127.0.0.1:55952 Client ? Connected via the socket Socket[addr=localhost/127.0.0.1,port=55951,localport=55952] Client ? Waiting for data from the server Client ? Closing the socket Client ? Failed to close the socket Server (:) Received EOF Server (:) Shut down Client ? Still waiting for data from the server... Client ? Still waiting for data from the server... Client ? Still waiting for data from the server... Client ? Still waiting for data from the server... Client ? Still waiting for data from the server... Client ? Still waiting for data from the server... Client ? Still waiting for data from the server... Client ? Still waiting for data from the server... Client ? Still waiting for data from the server... Client ? Still waiting for data from the server... Client ? Still waiting for data from the server... Client ? Still waiting for data from the server... Output in passed version: Server (:) Starting on localhost/127.0.0.1:0 Server (:) Accepting connections on [SSL: ServerSocket[addr=localhost/127.0.0.1,localport=55956]] Client ? Connecting to localhost/127.0.0.1:55956 Server (:) Accepted a connection from /127.0.0.1:55957 Server (:) Waiting for data from the client /127.0.0.1:55957 Client ? Connected via the socket 9f70c54[TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384: Socket[addr=localhost/127.0.0.1,port=55956,localport=55957]] Client ? Waiting for data from the server Client ? Closing the socket Server (:) Received EOF Server (:) Shut down Client ? Received EOF Client ? Stopped waiting for data from the server Client ? Closed the socket Client ? Disconnected
08-01-2019