https://bz.apache.org/bugzilla/show_bug.cgi?id=62791

            Bug ID: 62791
           Summary: SecureNioChannel fails with "IllegalArgumentException:
                    You can only read using the application read buffer
                    provided by the handler."
           Product: Tomcat 9
           Version: 9.0.12
          Hardware: PC
                OS: Mac OS X 10.1
            Status: NEW
          Severity: normal
          Priority: P2
         Component: Connectors
          Assignee: dev@tomcat.apache.org
          Reporter: mfedorys...@atlassian.com
  Target Milestone: -----

I ran into issue with Tomcat 9 failing to handle data exchange via WSS: 

02-Oct-2018 17:02:01.421 SEVERE [https-jsse-nio-8443-exec-9]
org.apache.coyote.AbstractProtocol$ConnectionHandler.process Error reading
request, ignored
 java.lang.IllegalArgumentException: You can only read using the application
read buffer provided by the handler.
        at
org.apache.tomcat.util.net.SecureNioChannel.read(SecureNioChannel.java:571)
        at
org.apache.tomcat.util.net.NioEndpoint$NioSocketWrapper.fillReadBuffer(NioEndpoint.java:1204)
        at
org.apache.tomcat.util.net.NioEndpoint$NioSocketWrapper.read(NioEndpoint.java:1140)
        at
org.apache.tomcat.websocket.server.WsFrameServer.onDataAvailable(WsFrameServer.java:72)
        at
org.apache.tomcat.websocket.server.WsFrameServer.doOnDataAvailable(WsFrameServer.java:171)
        at
org.apache.tomcat.websocket.server.WsFrameServer.notifyDataAvailable(WsFrameServer.java:151)
        at
org.apache.tomcat.websocket.server.WsHttpUpgradeHandler.upgradeDispatch(WsHttpUpgradeHandler.java:148)
        at
org.apache.coyote.http11.upgrade.UpgradeProcessorInternal.dispatch(UpgradeProcessorInternal.java:54)
        at
org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:53)
        at
org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:770)
        at
org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1415)
        at
org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
        at
java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
        at
java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
        at
org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
        at java.lang.Thread.run(Thread.java:748)


On a frontend side, I have a pretty simple javascript, which sends its state
updates via web sockets (text) to the backend service (simple spring app
running in Tomcat).
After Tomcat upgrade (from 8.0.53 to 9.0.12), application started failing to
establish web socket connections with ssl-enabled connector ("plain" web
sockets work fine). 

Steps to reproduce:
1. Generate certificate (I used steps from here:
https://confluence.atlassian.com/doc/running-confluence-over-ssl-or-https-161203.html#RunningConfluenceOverSSLorHTTPS-Option1:Createaself-signedcertificate)
2. Configure "secured" connector in server.xml:
<Connector port="8443" maxHttpHeaderSize="8192"
                   maxThreads="150" minSpareThreads="25"
                   protocol="org.apache.coyote.http11.Http11NioProtocol"
                   enableLookups="false" disableUploadTimeout="true"
                   acceptCount="100" scheme="https" secure="true"
                   clientAuth="false" sslProtocols="TLSv1,TLSv1.1,TLSv1.2"      
                   sslEnabledProtocols="TLSv1,TLSv1.1,TLSv1.2"
                   SSLEnabled="true"
                   URIEncoding="UTF-8" keystorePass="changeit"
                   keystoreFile="/Users/user/.keystore"/>

With such connector settings, all attempts to establish web socket connections
over ssl fail with exception above. At the same time:
1. Plain web sockets work
<Connector port="8090" connectionTimeout="20000" 
                   maxThreads="48" minSpareThreads="10"
                   enableLookups="false" acceptCount="10" debug="0"
URIEncoding="UTF-8"
                   protocol="org.apache.coyote.http11.Http11NioProtocol"/>
2. All non-websockets requests work (both http and https)
3. Everything works when I downgrade Tomcat to version 8.0.53 (I didn't try
other versions)

Looking a bit deeper into the problem, it seems that handshake succeeds, but
Tomcat fails on the first attempt to read the data from a web socket.

It looks like WsFrameServer does not respect SocketBufferHandler settings from
NioEndpoint$NioSocketWrapper at the time of its (WsFrameServer) instantiation
in WsHttpUpgradeHandler, and always creates its own read buffer
(WsFrameBase.java:93). Later, NioEndpoint$NioSocketWrapper tries its best to
fill in this buffer as much as it could, and attempts to read directly to the
buffer from the websocket (NioEndpoint.java:1204), which causes buffer check
failure in SecureNioChannel.

There are several ways of fixing it.
1. The most obvious solution is to disable direct reads from the socket. Easy,
but probably comes with performance degradation in some cases:
Index: java/org/apache/tomcat/util/net/NioEndpoint.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- java/org/apache/tomcat/util/net/NioEndpoint.java    (revision
b648bba7acc3529c35d7bc5c54639a3dcb08e812)
+++ java/org/apache/tomcat/util/net/NioEndpoint.java    (date 1538469054000)
@@ -1135,26 +1135,16 @@

             // The socket read buffer capacity is socket.appReadBufSize
             int limit = socketBufferHandler.getReadBuffer().capacity();
-            if (to.remaining() >= limit) {
-                to.limit(to.position() + limit);
-                nRead = fillReadBuffer(block, to);
-                if (log.isDebugEnabled()) {
-                    log.debug("Socket: [" + this + "], Read direct from
socket: [" + nRead + "]");
-                }
-                updateLastRead();
-            } else {
-                // Fill the read buffer as best we can.
-                nRead = fillReadBuffer(block);
-                if (log.isDebugEnabled()) {
-                    log.debug("Socket: [" + this + "], Read into buffer: [" +
nRead + "]");
-                }
-                updateLastRead();
+            nRead = fillReadBuffer(block);
+            if (log.isDebugEnabled()) {
+                log.debug("Socket: [" + this + "], Read into buffer: [" +
nRead + "]");
+            }
+            updateLastRead();

-                // Fill as much of the remaining byte array as possible with
the
-                // data that was just read
-                if (nRead > 0) {
-                    nRead = populateReadBuffer(to);
-                }
+            // Fill as much of the remaining byte array as possible with the
+            // data that was just read
+            if (nRead > 0) {
+                nRead = populateReadBuffer(to);
             }
             return nRead;
         }

2. Another possible solution would be to use temporary buffer in WsFrameServer:
Index: java/org/apache/tomcat/websocket/server/WsFrameServer.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- java/org/apache/tomcat/websocket/server/WsFrameServer.java  (revision
b648bba7acc3529c35d7bc5c54639a3dcb08e812)
+++ java/org/apache/tomcat/websocket/server/WsFrameServer.java  (date
1538469610000)
@@ -69,7 +69,10 @@
             // Fill up the input buffer with as much data as we can
             inputBuffer.mark();
            
inputBuffer.position(inputBuffer.limit()).limit(inputBuffer.capacity());
-            int read = socketWrapper.read(false, inputBuffer);
+            int capacity = inputBuffer.capacity();
+            byte[] byteArray = new byte[capacity];
+            int read = socketWrapper.read(false, byteArray, 0, capacity);
+            inputBuffer.put(byteArray);
             inputBuffer.limit(inputBuffer.position()).reset();
             if (read < 0) {
                 throw new EOFException();
Disadvantages for this approach would be creation of unnecessary byte buffer
(i.e. allocating unnecessary memory) and (possibly) race conditions (if
inputBuffer is written while we're reading to byteArray)

3. We may also get rid of buffer check in SecureNioChannel (security concern)
or add some parameter to ignore that check (a bit "dirty" solution as for me):
Index: java/org/apache/tomcat/util/net/NioEndpoint.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- java/org/apache/tomcat/util/net/NioEndpoint.java    (revision
b648bba7acc3529c35d7bc5c54639a3dcb08e812)
+++ java/org/apache/tomcat/util/net/NioEndpoint.java    (date 1538470221000)
@@ -1201,7 +1201,7 @@
                     }
                 }
             } else {
-                nRead = channel.read(to);
+                nRead = channel instanceof SecureNioChannel ?
((SecureNioChannel)channel).read(to, true) : channel.read(to);
                 if (nRead == -1) {
                     throw new EOFException();
                 }
Index: java/org/apache/tomcat/util/net/SecureNioChannel.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- java/org/apache/tomcat/util/net/SecureNioChannel.java       (revision
b648bba7acc3529c35d7bc5c54639a3dcb08e812)
+++ java/org/apache/tomcat/util/net/SecureNioChannel.java       (date
1538470221000)
@@ -565,8 +565,12 @@
      */
     @Override
     public int read(ByteBuffer dst) throws IOException {
+        return read(dst, false);
+    }
+
+    public int read(ByteBuffer dst, boolean ignoreBufferCheck) throws
IOException {
         // Make sure we only use the ApplicationBufferHandler's buffers
-        if (dst != getBufHandler().getReadBuffer() && (getAppReadBufHandler()
== null
+        if (!ignoreBufferCheck && dst != getBufHandler().getReadBuffer() &&
(getAppReadBufHandler() == null
                 || dst != getAppReadBufHandler().getByteBuffer())) {
             throw new
IllegalArgumentException(sm.getString("channel.nio.ssl.invalidBuffer"));
         }



There is a workaround: to use Http11Nio2Protocol instead of Http11NioProtocol

Java version: 1.8.0_171-b11

OS: OSX and Amazon Linux

Related changes:
Introduce fillReadBuffer(boolean, ByteBuffer)  git-svn-id:
https://svn.apache.org/repos/asf/tomcat/trunk@1758000
13f79535-47bb-0310-9956-ffa450edef68
Introduce a new method SocketWrapperBase.read(boolean, ByteBuffer)  git-svn-id:
https://svn.apache.org/repos/asf/tomcat/trunk@1758058
13f79535-47bb-0310-9956-ffa450edef68

-- 
You are receiving this mail because:
You are the assignee for the bug.
---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscr...@tomcat.apache.org
For additional commands, e-mail: dev-h...@tomcat.apache.org

Reply via email to