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