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(); } }