Author: amiloslavskiy
Date: Thu Oct 15 10:12:52 2020
New Revision: 1882518
URL: http://svn.apache.org/viewvc?rev=1882518&view=rev
Log:
JavaHL: Introduce tests showing JVM crashes with TunnelAgent
The crashes will be addressed in subsequent commits.
[in subversion/bindings/javahl]
* tests/org/apache/subversion/javahl/BasicTests.java
Add helper class 'TestTunnelAgent' to emulate server replies in test
environment.
Add new tests which currently causes JVM to crash:
* testCrash_RemoteSession_nativeDispose
* testCrash_RequestChannel_nativeRead_AfterException
* testCrash_RequestChannel_nativeRead_AfterSvnError
Modified:
subversion/branches/javahl-1.14-fixes/subversion/bindings/javahl/tests/org/apache/subversion/javahl/BasicTests.java
Modified:
subversion/branches/javahl-1.14-fixes/subversion/bindings/javahl/tests/org/apache/subversion/javahl/BasicTests.java
URL:
http://svn.apache.org/viewvc/subversion/branches/javahl-1.14-fixes/subversion/bindings/javahl/tests/org/apache/subversion/javahl/BasicTests.java?rev=1882518&r1=1882517&r2=1882518&view=diff
==============================================================================
---
subversion/branches/javahl-1.14-fixes/subversion/bindings/javahl/tests/org/apache/subversion/javahl/BasicTests.java
(original)
+++
subversion/branches/javahl-1.14-fixes/subversion/bindings/javahl/tests/org/apache/subversion/javahl/BasicTests.java
Thu Oct 15 10:12:52 2020
@@ -25,6 +25,7 @@ package org.apache.subversion.javahl;
import static org.junit.Assert.*;
import org.apache.subversion.javahl.callback.*;
+import org.apache.subversion.javahl.remote.*;
import org.apache.subversion.javahl.types.*;
import java.io.File;
@@ -36,6 +37,7 @@ import java.io.PrintWriter;
import java.io.ByteArrayOutputStream;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
+import java.nio.channels.ClosedChannelException;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;
import java.text.ParseException;
@@ -4417,6 +4419,298 @@ public class BasicTests extends SVNTests
assertEquals("fake", new String(revprop));
}
+ public static int FLAG_ECHO = 0x00000001;
+ public static int FLAG_THROW_IN_OPEN = 0x00000002;
+
+ public enum Actions
+ {
+ READ_CLIENT, // Read a request from SVN client
+ EMUL_SERVER, // Emulate server response
+ WAIT_TUNNEL, // Wait for tunnel to be closed
+ };
+
+ public static class ScriptItem
+ {
+ Actions action;
+ String value;
+
+ ScriptItem(Actions action, String value)
+ {
+ this.action = action;
+ this.value = value;
+ }
+ }
+
+ private static class TestTunnelAgent extends Thread
+ implements TunnelAgent
+ {
+ ScriptItem[] script;
+ int flags;
+ String error = null;
+ ReadableByteChannel request;
+ WritableByteChannel response;
+
+ final CloseTunnelCallback closeTunnelCallback = () ->
+ {
+ if ((flags & FLAG_ECHO) != 0)
+ System.out.println("TunnelAgent.CloseTunnelCallback");
+ };
+
+ TestTunnelAgent(int flags, ScriptItem[] script)
+ {
+ this.flags = flags;
+ this.script = script;
+ }
+
+ public void joinAndTest()
+ {
+ try
+ {
+ join();
+ }
+ catch (InterruptedException e)
+ {
+ fail("InterruptedException was caught");
+ }
+
+ if (error != null)
+ fail(error);
+ }
+
+ @Override
+ public boolean checkTunnel(String name)
+ {
+ return true;
+ }
+
+ private String readClient(ByteBuffer readBuffer)
+ throws IOException
+ {
+ readBuffer.reset();
+ request.read(readBuffer);
+
+ final int offset = readBuffer.arrayOffset();
+ return new String(readBuffer.array(),
+ offset,
+ readBuffer.position() - offset);
+ }
+
+ private void emulateServer(String serverMessage)
+ throws IOException
+ {
+ final byte[] responseBytes = serverMessage.getBytes();
+ response.write(ByteBuffer.wrap(responseBytes));
+ }
+
+ private void doScriptItem(ScriptItem scriptItem, ByteBuffer readBuffer)
+ throws Exception
+ {
+ switch (scriptItem.action)
+ {
+ case READ_CLIENT:
+ final String actualLine = readClient(readBuffer);
+
+ if ((flags & FLAG_ECHO) != 0)
+ {
+ System.out.println("SERVER: " + scriptItem.value);
+ System.out.flush();
+ }
+
+ if (!actualLine.contains(scriptItem.value))
+ {
+ System.err.println("Expected: " + scriptItem.value);
+ System.err.println("Actual: " + actualLine);
+ System.err.flush();
+
+ // Unblock the SVN thread by emulating a server error
+ final String serverError = "( success (
( ) 0: ) ) ( failure ( ( 160000 39:Test script received unexpected request 0: 0
) ) ) ";
+ emulateServer(serverError);
+
+ fail("Unexpected client request");
+ }
+ break;
+ case EMUL_SERVER:
+ if ((flags & FLAG_ECHO) != 0)
+ {
+ System.out.println("CLIENT: " + scriptItem.value);
+ System.out.flush();
+ }
+
+ emulateServer(scriptItem.value);
+ break;
+ case WAIT_TUNNEL:
+ // The loop will end with an exception when tunnel is closed
+ for (;;)
+ {
+ readClient(readBuffer);
+ }
+ }
+ }
+
+ public void run()
+ {
+ final ByteBuffer readBuffer = ByteBuffer.allocate(1024 * 1024);
+ readBuffer.mark();
+
+ for (ScriptItem scriptItem : script)
+ {
+ try {
+ doScriptItem(scriptItem, readBuffer);
+ } catch (ClosedChannelException ex) {
+ // Expected when closed properly
+ } catch (IOException e) {
+ // IOException occurs when already-freed apr_file_t was
lucky
+ // to have reasonable fields to avoid the crash. It still
+ // indicates a problem.
+ error = "IOException was caught in run()";
+ return;
+ } catch (Throwable t) {
+ // No other exceptions are expected here.
+ error = "Exception was caught in run()";
+ t.printStackTrace();
+ return;
+ }
+ }
+ }
+
+ @Override
+ public CloseTunnelCallback openTunnel(ReadableByteChannel request,
+ WritableByteChannel response,
+ String name,
+ String user,
+ String hostname,
+ int port)
+ throws Throwable
+ {
+ this.request = request;
+ this.response = response;
+
+ start();
+
+ if ((flags & FLAG_THROW_IN_OPEN) != 0)
+ throw ClientException.fromException(new RuntimeException("Test
exception"));
+
+ return closeTunnelCallback;
+ }
+ };
+
+ /**
+ * Test scenario which previously caused a JVM crash.
+ * In this scenario, GC is invoked before closing tunnel.
+ */
+ public void testCrash_RemoteSession_nativeDispose() {
+ final ScriptItem[] script = new ScriptItem[]{
+ new ScriptItem(Actions.EMUL_SERVER, "( success ( 2 2 ( ) (
edit-pipeline svndiff1 absent-entries commit-revprops depth log-revprops
atomic-revprops partial-replay inherited-props ephemeral-txnprops
file-revs-reverse ) ) ) "),
+ new ScriptItem(Actions.READ_CLIENT, "edit-pipeline"),
+ new ScriptItem(Actions.EMUL_SERVER, "( success ( ( ANONYMOUS )
36:0113e071-0208-4a7b-9f20-3038f9caf0f0 ) ) "),
+ new ScriptItem(Actions.READ_CLIENT, "ANONYMOUS"),
+ new ScriptItem(Actions.EMUL_SERVER, "( success ( ) ) ( success (
36:00000000-0000-0000-0000-000000000000 25:svn+test://localhost/test (
mergeinfo ) ) ) "),
+ };
+
+ final TestTunnelAgent tunnelAgent = new TestTunnelAgent(0, script);
+ final RemoteFactory remoteFactory = new RemoteFactory();
+ remoteFactory.setTunnelAgent(tunnelAgent);
+
+ ISVNRemote remote = null;
+ try {
+ remote =
remoteFactory.openRemoteSession("svn+test://localhost/test", 1);
+ } catch (SubversionException e) {
+ fail("SubversionException was caught");
+ }
+
+ // 'OperationContext::openTunnel()' doesn't 'NewGlobalRef()' callback
returned by 'TunnelAgent.openTunnel()'.
+ // When GC runs, it disposes the callback. When JavaHL tries to call
it in 'remote.dispose()', JVM crashes.
+ System.gc();
+ remote.dispose();
+
+ tunnelAgent.joinAndTest();
+ }
+
+ /**
+ * Test scenario which previously caused a JVM crash.
+ * In this scenario, tunnel is not properly closed after exception in
+ * 'TunnelAgent.openTunnel()'.
+ */
+ public void testCrash_RequestChannel_nativeRead_AfterException()
+ {
+ // Exception causes TunnelChannel's native side to be destroyed with
+ // the following abbreviated stack:
+ // TunnelChannel.nativeClose()
+ // svn_pool_destroy(sesspool)
+ // svn_ra_open5()
+ // If TunnelAgent is unaware and calls 'RequestChannel.nativeRead()'
+ // or 'ResponseChannel.nativeWrite()', it will either crash or try to
+ // use a random file.
+ final int flags = FLAG_THROW_IN_OPEN;
+
+ final ScriptItem[] script = new ScriptItem[]{
+ new ScriptItem(Actions.EMUL_SERVER, "( success ( 2 2 ( ) (
edit-pipeline svndiff1 absent-entries commit-revprops depth log-revprops
atomic-revprops partial-replay inherited-props ephemeral-txnprops
file-revs-reverse ) ) ) "),
+ new ScriptItem(Actions.WAIT_TUNNEL, ""),
+ };
+
+ final TestTunnelAgent tunnelAgent = new TestTunnelAgent(flags, script);
+ final SVNClient svnClient = new SVNClient();
+ svnClient.setTunnelAgent(tunnelAgent);
+
+ try {
+ svnClient.openRemoteSession("svn+test://localhost/test");
+ } catch (SubversionException e) {
+ // RuntimeException("Test exception") is expected here
+ }
+
+ tunnelAgent.joinAndTest();
+ }
+
+ /**
+ * Test scenario which previously caused a JVM crash.
+ * In this scenario, tunnel is not properly closed after an SVN error.
+ */
+ public void testCrash_RequestChannel_nativeRead_AfterSvnError()
+ {
+ final String wcRoot = new File("tempSvnRepo").getAbsolutePath();
+
+ final ScriptItem[] script = new ScriptItem[]{
+ // openRemoteSession
+ new ScriptItem(Actions.EMUL_SERVER, "( success ( 2 2 ( ) (
edit-pipeline svndiff1 absent-entries commit-revprops depth log-revprops
atomic-revprops partial-replay inherited-props ephemeral-txnprops
file-revs-reverse ) ) ) "),
+ new ScriptItem(Actions.READ_CLIENT, "edit-pipeline"),
+ new ScriptItem(Actions.EMUL_SERVER, "( success ( ( ANONYMOUS )
36:0113e071-0208-4a7b-9f20-3038f9caf0f0 ) ) "),
+ new ScriptItem(Actions.READ_CLIENT, "ANONYMOUS"),
+ new ScriptItem(Actions.EMUL_SERVER, "( success ( ) ) ( success (
36:00000000-0000-0000-0000-000000000000 25:svn+test://localhost/test (
mergeinfo ) ) ) "),
+ // checkout
+ new ScriptItem(Actions.READ_CLIENT, "( get-latest-rev ( ) ) "),
+ // Error causes a SubversionException to be created, which then
+ // skips closing the Tunnel properly due to 'ExceptionOccurred()'
+ // in 'OperationContext::closeTunnel()'.
+ // If TunnelAgent is unaware and calls
'RequestChannel.nativeRead()',
+ // it will either crash or try to use a random file.
+ new ScriptItem(Actions.EMUL_SERVER, "( success ( ( ) 0: ) ) (
failure ( ( 160006 20:This is a test error 0: 0 ) ) ) "),
+ // TunnelAgent is not aware about the error and just keeps reading.
+ new ScriptItem(Actions.WAIT_TUNNEL, ""),
+ };
+
+ final TestTunnelAgent tunnelAgent = new TestTunnelAgent(0, script);
+ final SVNClient svnClient = new SVNClient();
+ svnClient.setTunnelAgent(tunnelAgent);
+
+ try {
+ svnClient.checkout("svn+test://localhost/test",
+ wcRoot,
+ Revision.getInstance(1),
+ null,
+ Depth.infinity,
+ true,
+ false);
+
+ svnClient.dispose();
+ } catch (ClientException ex) {
+ final int SVN_ERR_FS_NO_SUCH_REVISION = 160006;
+ if (SVN_ERR_FS_NO_SUCH_REVISION !=
ex.getAllMessages().get(0).getCode())
+ ex.printStackTrace();
+ }
+
+ tunnelAgent.joinAndTest();
+ }
+
/**
* @return <code>file</code> converted into a -- possibly
* <code>canonical</code>-ized -- Subversion-internal path