Please find test snippet and patch attached.

[[[
Fix JavaHL crash in RequestChannel.nativeRead

The problem here is that when 'RemoteSession' is destroyed, it also
does 'apr_pool_destroy()' which frees memory behind 'apr_file_t', which
is represented by 'TunnelChannel.nativeChannel' in Java.
'TunnelChannel' runs on a different thread and is unaware that
'apr_file_t' pointer is now invalid.

Fix this by informing 'TunnelChannel' before 'apr_file_t' is freed.

One other problem is that when 'TunnelAgent.openTunnel()' throws an
exception, 'OperationContext::closeTunnel()' was not called at all.

[in subversion/bindings/javahl]
* native/OperationContext.cpp
  (close_TunnelChannel): New function to inform Java side.
  (openTunnel): Keep references to Java tunnel objects.
  (openTunnel): In case of exception, clean up properly.
  (closeTunnel): Inform Java side when tunnel is closed.
* src/org/apache/subversion/javahl/util/RequestChannel.java
  Add 'synchronized' to avoid error described in 'syncClose()'
* src/org/apache/subversion/javahl/util/ResponseChannel.java
  Add 'synchronized' to avoid error described in 'syncClose()'
* src/org/apache/subversion/javahl/util/Tunnel.java
  A new function to clean up. I decided not to change old '.close()'
  because the new function can lead to a deadlock if used incorrectly.
]]]
Index: subversion/bindings/javahl/native/OperationContext.cpp
===================================================================
--- subversion/bindings/javahl/native/OperationContext.cpp      (revision 
1880751)
+++ subversion/bindings/javahl/native/OperationContext.cpp      (working copy)
@@ -492,6 +492,8 @@
       request_out(NULL),
       response_in(NULL),
       response_out(NULL),
+      jrequest(NULL),
+      jresponse(NULL),
       jclosecb(NULL)
     {
       status = apr_file_pipe_create_ex(&request_in, &request_out,
@@ -512,6 +514,8 @@
   apr_file_t *response_in;
   apr_file_t *response_out;
   apr_status_t status;
+  jobject jrequest;
+  jobject jresponse;
   jobject jclosecb;
 };
 
@@ -523,7 +527,10 @@
   jmethodID ctor = env->GetMethodID(cls, "<init>", "(J)V");
   if (JNIUtil::isJavaExceptionThrown())
     return NULL;
-  return env->NewObject(cls, ctor, reinterpret_cast<jlong>(fd));
+  jobject channel = env->NewObject(cls, ctor, reinterpret_cast<jlong>(fd));
+  if (JNIUtil::isJavaExceptionThrown())
+    return NULL;
+  return env->NewGlobalRef(channel);
 }
 
 jobject create_RequestChannel(JNIEnv *env, apr_file_t *fd)
@@ -534,6 +541,24 @@
 {
   return create_Channel(JAVAHL_CLASS("/util/ResponseChannel"), env, fd);
 }
+void close_TunnelChannel(JNIEnv* env, jobject channel)
+{
+  // Usually after this function, the memory will be freed behind
+  // 'TunnelChannel.nativeChannel'. Ask Java side to forget it. This is the
+  // only way to avoid a JVM crash when 'TunnelAgent' tries to read/write,
+  // not knowing that 'TunnelChannel' is already closed in native side.
+  static jmethodID mid = 0;
+  if (0 == mid)
+    {
+      jclass cls;
+      SVN_JNI_CATCH_VOID(
+        cls = env->FindClass(JAVAHL_CLASS("/util/TunnelChannel")));
+      SVN_JNI_CATCH_VOID(mid = env->GetMethodID(cls, "syncClose", "()V"));
+    }
+
+  SVN_JNI_CATCH_VOID(env->CallVoidMethod(channel, mid));
+  env->DeleteGlobalRef(channel);
+}
 } // anonymous namespace
 
 svn_boolean_t
@@ -590,10 +615,10 @@
 
   JNIEnv *env = JNIUtil::getEnv();
 
-  jobject jrequest = create_RequestChannel(env, tc->request_in);
+  tc->jrequest = create_RequestChannel(env, tc->request_in);
   SVN_JNI_CATCH(, SVN_ERR_BASE);
 
-  jobject jresponse = create_ResponseChannel(env, tc->response_out);
+  tc->jresponse = create_ResponseChannel(env, tc->response_out);
   SVN_JNI_CATCH(, SVN_ERR_BASE);
 
   jstring jtunnel_name = JNIUtil::makeJString(tunnel_name);
@@ -623,12 +648,24 @@
     }
 
   jobject jtunnelcb = jobject(tunnel_baton);
-  SVN_JNI_CATCH(
-      tc->jclosecb = env->CallObjectMethod(
-          jtunnelcb, mid, jrequest, jresponse,
-          jtunnel_name, juser, jhostname, jint(port)),
-      SVN_ERR_BASE);
+  tc->jclosecb = env->CallObjectMethod(
+    jtunnelcb, mid, tc->jrequest, tc->jresponse,
+    jtunnel_name, juser, jhostname, jint(port));
+  svn_error_t* openTunnelError = JNIUtil::checkJavaException(SVN_ERR_BASE);
+  if (SVN_NO_ERROR != openTunnelError)
+    {
+      // Clear exception to let OperationContext::closeTunnel() work.
+      env->ExceptionClear();
 
+      // OperationContext::closeTunnel() will never be called, clean up here.
+      // This also prevents a JVM native crash, see comment in
+      // close_TunnelChannel().
+      *close_baton = 0;
+      tc->jclosecb = 0;
+      OperationContext::closeTunnel(tc, 0);
+      SVN_ERR(openTunnelError);
+    }
+
   return SVN_NO_ERROR;
 }
 
@@ -636,24 +673,35 @@
 OperationContext::closeTunnel(void *tunnel_context, void *)
 {
   TunnelContext* tc = static_cast<TunnelContext*>(tunnel_context);
+  jobject jrequest = tc->jrequest;
+  jobject jresponse = tc->jresponse;
   jobject jclosecb = tc->jclosecb;
+
+  // Note that this closes other end of the pipe, which cancels and
+  // prevents further read/writes in 'TunnelAgent'
   delete tc;
 
-  if (!jclosecb)
-    return;
-
   JNIEnv *env = JNIUtil::getEnv();
   if (JNIUtil::isJavaExceptionThrown())
     return;
 
-  static jmethodID mid = 0;
-  if (0 == mid)
+  if (jclosecb)
     {
-      jclass cls;
-      SVN_JNI_CATCH_VOID(
-          cls= env->FindClass(
-              JAVAHL_CLASS("/callback/TunnelAgent$CloseTunnelCallback")));
-      SVN_JNI_CATCH_VOID(mid = env->GetMethodID(cls, "closeTunnel", "()V"));
+      static jmethodID mid = 0;
+      if (0 == mid)
+        {
+          jclass cls;
+          SVN_JNI_CATCH_VOID(
+              cls= env->FindClass(
+                  JAVAHL_CLASS("/callback/TunnelAgent$CloseTunnelCallback")));
+          SVN_JNI_CATCH_VOID(mid = env->GetMethodID(cls, "closeTunnel", 
"()V"));
+        }
+      SVN_JNI_CATCH_VOID(env->CallVoidMethod(jclosecb, mid));
     }
-  SVN_JNI_CATCH_VOID(env->CallVoidMethod(jclosecb, mid));
+
+  if (jrequest)
+    close_TunnelChannel(env, jrequest);
+
+  if (jresponse)
+    close_TunnelChannel(env, jresponse);
 }
Index: 
subversion/bindings/javahl/src/org/apache/subversion/javahl/util/RequestChannel.java
===================================================================
--- 
subversion/bindings/javahl/src/org/apache/subversion/javahl/util/RequestChannel.java
        (revision 1880751)
+++ 
subversion/bindings/javahl/src/org/apache/subversion/javahl/util/RequestChannel.java
        (working copy)
@@ -42,15 +42,18 @@
 
     public int read(ByteBuffer dst) throws IOException
     {
-        long channel = nativeChannel.get();
-        if (channel != 0)
-            try {
-                return nativeRead(channel, dst);
-            } catch (IOException ex) {
-                nativeChannel.set(0); // Close the channel
-                throw ex;
-            }
-        throw new ClosedChannelException();
+        synchronized (nativeChannel)
+        {
+            long channel = nativeChannel.get();
+            if (channel != 0)
+                try {
+                    return nativeRead(channel, dst);
+                } catch (IOException ex) {
+                    nativeChannel.set(0); // Close the channel
+                    throw ex;
+                }
+            throw new ClosedChannelException();
+        }
     }
 
     private static native int nativeRead(long nativeChannel, ByteBuffer dst)
Index: 
subversion/bindings/javahl/src/org/apache/subversion/javahl/util/TunnelChannel.java
===================================================================
--- 
subversion/bindings/javahl/src/org/apache/subversion/javahl/util/TunnelChannel.java
 (revision 1880751)
+++ 
subversion/bindings/javahl/src/org/apache/subversion/javahl/util/TunnelChannel.java
 (working copy)
@@ -50,6 +50,39 @@
             nativeClose(channel);
     }
 
+    /**
+     * Wait for current read/write to complete, then close() channel.
+     * Compared to close(), it has the following differences:
+     * <ul>
+     *   <li>
+     *     Prevents race condition where read/write could use incorrect file :
+     *     <ol>
+     *       <li>Writer thread extracts OS file descriptor from 
nativeChannel.</li>
+     *       <li>Writer thread calls OS API to write to file, passing file 
descriptor.</li>
+     *       <li>Writer thread is interrupted.</li>
+     *       <li>Closer thread closes OS file descriptor. The file descriptor 
number is now free.</li>
+     *       <li>Unrelated thread opens a new file. OS reuses the old file 
descriptor (currently free).</li>
+     *       <li>Writer thread resumes inside OS API to write to file.</li>
+     *       <li>Writer thread writes to unrelated file, corrupting it with 
unexpected data.</li>
+     *     </ol>
+     *   </li>
+     *   <li>
+     *     It can no longer cancel a read/write operation already in progress.
+     *     The native implementation closes the other end of the pipe, 
breaking the pipe,
+     *     which prevents the risk of never-completing read/write.
+     *   </li>
+     * <ul/>
+     *
+     * @throws IOException
+     */
+    public void syncClose() throws IOException
+    {
+        synchronized (nativeChannel)
+        {
+            close();
+        }
+    }
+
     private native static void nativeClose(long nativeChannel)
         throws IOException;
 
Index: 
subversion/bindings/javahl/src/org/apache/subversion/javahl/util/ResponseChannel.java
===================================================================
--- 
subversion/bindings/javahl/src/org/apache/subversion/javahl/util/ResponseChannel.java
       (revision 1880751)
+++ 
subversion/bindings/javahl/src/org/apache/subversion/javahl/util/ResponseChannel.java
       (working copy)
@@ -42,15 +42,18 @@
 
     public int write(ByteBuffer src) throws IOException
     {
-        long channel = this.nativeChannel.get();
-        if (channel != 0)
-            try {
-                return nativeWrite(channel, src);
-            } catch (IOException ex) {
-                nativeChannel.set(0); // Close the channel
-                throw ex;
-            }
-        throw new ClosedChannelException();
+        synchronized (nativeChannel)
+        {
+            long channel = this.nativeChannel.get();
+            if (channel != 0)
+                try {
+                    return nativeWrite(channel, src);
+                } catch (IOException ex) {
+                    nativeChannel.set(0); // Close the channel
+                    throw ex;
+                }
+            throw new ClosedChannelException();
+        }
     }
 
     private static native int nativeWrite(long nativeChannel, ByteBuffer src)
import java.io.*;
import java.lang.reflect.Field;
import java.nio.*;
import java.nio.channels.*;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicLong;

import org.apache.subversion.javahl.*;
import org.apache.subversion.javahl.callback.*;

public class JavaHL_Crash_RequestChannel_nativeRead {
        public static void main(String[] args) {
                final SVNClient svnClient = new SVNClient();
                final CountDownLatch exceptionReceived = new CountDownLatch(1);

                svnClient.setTunnelAgent(new TunnelAgent() {
                        @Override
                        public boolean checkTunnel(String name) {
                                return true;
                        }

                        @Override
                        public CloseTunnelCallback 
openTunnel(ReadableByteChannel request, WritableByteChannel response, String 
name, String user, String hostname, int port) throws Throwable {
                                new Thread(() -> {
                                        try {
                                                
dump_RequestChannel_nativeChannel(request);
                                                exceptionReceived.await();
                                                
request.read(ByteBuffer.allocate(1024));

                                                System.out.println();
                                                
System.out.println("request.read() succeed, kind of...");
                                        } catch (IOException e) {
                                                System.out.println();
                                                System.out.println("IOException 
was caught.");
                                        } catch (Exception e) {
                                                e.printStackTrace(System.out);
                                        } finally {
                                                System.out.println("This 
happens when already-freed apr_file_t was lucky to have reasonable fields to 
avoid the crash.");
                                                System.out.println("In order to 
reproduce the crash, do one of:");
                                                System.out.println("a) Try 
again. Usually it reproduces once in 10 times or so.");
                                                System.out.println("b) Use 
Application Verifier with Basics/Heaps for java.exe");
                                        }
                                }).start();

                                // Exception causes 'svn_ra_open5()' to call 
'svn_pool_destroy(sesspool)'
                                // which also destroys 
'TunnelChannel.nativeClose'.
                                throw ClientException.fromException(new 
RuntimeException("Test exception"));
                        }
                });

                try {
                        svnClient.openRemoteSession("svn+ssh://localhost/test");
                } catch (SubversionException e) {
                        exceptionReceived.countDown();
                }
        }

        // For debugging purposes, not needed to reproduce the crash.
        static void dump_RequestChannel_nativeChannel(ReadableByteChannel 
channel) throws Exception {
                Class class_TunnelChannel = 
Class.forName("org.apache.subversion.javahl.util.TunnelChannel");
                Field field_nativeChannel = 
class_TunnelChannel.getDeclaredField("nativeChannel");
                field_nativeChannel.setAccessible(true);

                AtomicLong nativeChannel = 
(AtomicLong)field_nativeChannel.get(channel);
                System.out.format("%X = TunnelChannel.nativeChannel%n", 
nativeChannel.get());
                System.out.flush();
        }
}

Reply via email to