/* * Copyright (c) 2003, 2015, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ /* * @test * @bug 4915825 4921009 4934965 4977469 8019584 * @key randomness * @summary Tests behavior when client or server gets object of unknown class * @author Eamonn McManus * * @run clean MissingClassTest SingleClassLoader * @run build MissingClassTest SingleClassLoader * @run main MissingClassTest */ /* Tests that clients and servers react correctly when they receive objects of unknown classes. This can happen easily due to version skew or missing jar files on one end or the other. The default behaviour of causing a connection to die because of the resultant IOException is not acceptable! We try sending attributes and invoke parameters to the server of classes it doesn't know, and we try sending attributes, exceptions and notifications to the client of classes it doesn't know. We also test objects that are of known class but not serializable. The test cases are similar. */ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.NotSerializableException; import java.io.ObjectOutputStream; import java.net.MalformedURLException; import java.util.HashMap; import java.util.Map; import java.util.Random; import java.util.Set; import javax.management.Attribute; import javax.management.MBeanServer; import javax.management.MBeanServerConnection; import javax.management.MBeanServerFactory; import javax.management.Notification; import javax.management.NotificationBroadcasterSupport; import javax.management.NotificationFilter; import javax.management.NotificationListener; import javax.management.ObjectName; import javax.management.remote.JMXConnectionNotification; import javax.management.remote.JMXConnector; import javax.management.remote.JMXConnectorFactory; import javax.management.remote.JMXConnectorServer; import javax.management.remote.JMXConnectorServerFactory; import javax.management.remote.JMXServiceURL; import javax.management.remote.rmi.RMIConnectorServer; public class MissingClassTest { private static final int NNOTIFS = 50; private static ClassLoader clientLoader, serverLoader; private static Object serverUnknown; private static Exception clientUnknown; private static ObjectName on; private static final Object[] NO_OBJECTS = new Object[0]; private static final String[] NO_STRINGS = new String[0]; private static final Object unserializableObject = Thread.currentThread(); private static boolean isInstance(Object o, String cn) { try { Class c = Class.forName(cn); return c.isInstance(o); } catch (ClassNotFoundException x) { return false; } } public static void main(String[] args) throws Exception { System.out.println("Test that the client or server end of a " + "connection does not fail if sent an object " + "it cannot deserialize"); on = new ObjectName("test:type=Test"); ClassLoader testLoader = MissingClassTest.class.getClassLoader(); clientLoader = new SingleClassLoader("$ServerUnknown$", HashMap.class, testLoader); serverLoader = new SingleClassLoader("$ClientUnknown$", Exception.class, testLoader); serverUnknown = clientLoader.loadClass("$ServerUnknown$").newInstance(); clientUnknown = (Exception) serverLoader.loadClass("$ClientUnknown$").newInstance(); final String[] protos = {"rmi", /*"iiop",*/ "jmxmp"}; boolean ok = true; for (int i = 0; i < protos.length; i++) { try { ok &= test(protos[i]); } catch (Exception e) { System.out.println("TEST FAILED WITH EXCEPTION:"); e.printStackTrace(System.out); ok = false; } } if (ok) System.out.println("Test passed"); else { throw new RuntimeException("TEST FAILED"); } } private static boolean test(String proto) throws Exception { System.out.println("Testing for proto " + proto); boolean ok = true; MBeanServer mbs = MBeanServerFactory.newMBeanServer(); mbs.createMBean(Test.class.getName(), on); JMXConnectorServer cs; JMXServiceURL url = new JMXServiceURL(proto, null, 0); Map serverMap = new HashMap<>(); serverMap.put(JMXConnectorServerFactory.DEFAULT_CLASS_LOADER, serverLoader); // make sure no auto-close at server side serverMap.put("jmx.remote.x.server.connection.timeout", "888888888"); try { cs = JMXConnectorServerFactory.newJMXConnectorServer(url, serverMap, mbs); } catch (MalformedURLException e) { System.out.println("System does not recognize URL: " + url + "; ignoring"); return true; } cs.start(); JMXServiceURL addr = cs.getAddress(); Map clientMap = new HashMap<>(); clientMap.put(JMXConnectorFactory.DEFAULT_CLASS_LOADER, clientLoader); System.out.println("Connecting for client-unknown test"); JMXConnector client = JMXConnectorFactory.connect(addr, clientMap); // add a listener to verify no failed notif CNListener cnListener = new CNListener(); client.addConnectionNotificationListener(cnListener, null, null); MBeanServerConnection mbsc = client.getMBeanServerConnection(); System.out.println("Getting attribute with class unknown to client"); try { Object result = mbsc.getAttribute(on, "ClientUnknown"); System.out.println("TEST FAILS: getAttribute for class " + "unknown to client should fail, returned: " + result); ok = false; } catch (IOException e) { Throwable cause = e.getCause(); if (isInstance(cause, "org.omg.CORBA.MARSHAL")) // see CR 4935098 cause = cause.getCause(); if (cause instanceof ClassNotFoundException) { System.out.println("Success: got an IOException wrapping " + "a ClassNotFoundException"); } else { System.out.println("TEST FAILS: Caught IOException (" + e + ") but cause should be " + "ClassNotFoundException: " + cause); ok = false; } } System.out.println("Doing queryNames to ensure connection alive"); Set names = mbsc.queryNames(null, null); System.out.println("queryNames returned " + names); System.out.println("Provoke exception of unknown class"); try { mbsc.invoke(on, "throwClientUnknown", NO_OBJECTS, NO_STRINGS); System.out.println("TEST FAILS: did not get exception"); ok = false; } catch (IOException e) { Throwable wrapped = e.getCause(); if (isInstance(wrapped, "org.omg.CORBA.MARSHAL")) // see CR 4935098 wrapped = wrapped.getCause(); if (wrapped instanceof ClassNotFoundException) { System.out.println("Success: got an IOException wrapping " + "a ClassNotFoundException: " + wrapped); } else { System.out.println("TEST FAILS: Got IOException but cause " + "should be ClassNotFoundException: "); if (wrapped == null) System.out.println("(null)"); else wrapped.printStackTrace(System.out); ok = false; } } catch (Exception e) { System.out.println("TEST FAILS: Got wrong exception: " + "should be IOException with cause " + "ClassNotFoundException:"); e.printStackTrace(System.out); ok = false; } System.out.println("Doing queryNames to ensure connection alive"); names = mbsc.queryNames(null, null); System.out.println("queryNames returned " + names); ok &= notifyTest(client, mbsc); System.out.println("Doing queryNames to ensure connection alive"); names = mbsc.queryNames(null, null); System.out.println("queryNames returned " + names); for (int i = 0; i < 2; i++) { boolean setAttribute = (i == 0); // else invoke String what = setAttribute ? "setAttribute" : "invoke"; System.out.println("Trying " + what + " with class unknown to server"); try { if (setAttribute) { mbsc.setAttribute(on, new Attribute("ServerUnknown", serverUnknown)); } else { mbsc.invoke(on, "useServerUnknown", new Object[] {serverUnknown}, new String[] {"java.lang.Object"}); } System.out.println("TEST FAILS: " + what + " with " + "class unknown to server should fail " + "but did not"); ok = false; } catch (IOException e) { Throwable cause = e.getCause(); if (isInstance(cause, "org.omg.CORBA.MARSHAL")) // see CR 4935098 cause = cause.getCause(); if (cause instanceof ClassNotFoundException) { System.out.println("Success: got an IOException " + "wrapping a ClassNotFoundException"); } else { System.out.println("TEST FAILS: Caught IOException (" + e + ") but cause should be " + "ClassNotFoundException: " + cause); e.printStackTrace(System.out); // XXX ok = false; } } } System.out.println("Doing queryNames to ensure connection alive"); names = mbsc.queryNames(null, null); System.out.println("queryNames returned " + names); System.out.println("Trying to get unserializable attribute"); try { mbsc.getAttribute(on, "Unserializable"); System.out.println("TEST FAILS: get unserializable worked " + "but should not"); ok = false; } catch (IOException e) { System.out.println("Success: got an IOException: " + e + " (cause: " + e.getCause() + ")"); } System.out.println("Doing queryNames to ensure connection alive"); names = mbsc.queryNames(null, null); System.out.println("queryNames returned " + names); System.out.println("Trying to set unserializable attribute"); try { Attribute attr = new Attribute("Unserializable", unserializableObject); mbsc.setAttribute(on, attr); System.out.println("TEST FAILS: set unserializable worked " + "but should not"); ok = false; } catch (IOException e) { System.out.println("Success: got an IOException: " + e + " (cause: " + e.getCause() + ")"); } System.out.println("Doing queryNames to ensure connection alive"); names = mbsc.queryNames(null, null); System.out.println("queryNames returned " + names); System.out.println("Trying to throw unserializable exception"); try { mbsc.invoke(on, "throwUnserializable", NO_OBJECTS, NO_STRINGS); System.out.println("TEST FAILS: throw unserializable worked " + "but should not"); ok = false; } catch (IOException e) { System.out.println("Success: got an IOException: " + e + " (cause: " + e.getCause() + ")"); } client.removeConnectionNotificationListener(cnListener); ok = ok && !cnListener.failed; client.close(); cs.stop(); if (ok) System.out.println("Test passed for protocol " + proto); System.out.println(); return ok; } private static class TestListener implements NotificationListener { TestListener(LostListener ll) { this.lostListener = ll; } public void handleNotification(Notification n, Object h) { /* Connectors can handle unserializable notifications in one of two ways. Either they can arrange for the client to get a NotSerializableException from its fetchNotifications call (RMI connector), or they can replace the unserializable notification by a JMXConnectionNotification.NOTIFS_LOST (JMXMP connector). The former case is handled by code within the connector client which will end up sending a NOTIFS_LOST to our LostListener. The logic here handles the latter case by converting it into the former. */ if (n instanceof JMXConnectionNotification && n.getType().equals(JMXConnectionNotification.NOTIFS_LOST)) { lostListener.handleNotification(n, h); return; } synchronized (result) { if (!n.getType().equals("interesting") || !n.getUserData().equals("known")) { System.out.println("TestListener received strange notif: " + notificationString(n)); result.failed = true; result.notifyAll(); } else { result.knownCount++; if (result.knownCount == NNOTIFS) result.notifyAll(); } } } private LostListener lostListener; } private static class LostListener implements NotificationListener { public void handleNotification(Notification n, Object h) { synchronized (result) { handle(n, h); } } private void handle(Notification n, Object h) { if (!(n instanceof JMXConnectionNotification)) { System.out.println("LostListener received strange notif: " + notificationString(n)); result.failed = true; result.notifyAll(); return; } JMXConnectionNotification jn = (JMXConnectionNotification) n; if (!jn.getType().equals(jn.NOTIFS_LOST)) { System.out.println("Ignoring JMXConnectionNotification: " + notificationString(jn)); return; } final String msg = jn.getMessage(); if ((!msg.startsWith("Dropped ") || !msg.endsWith("classes were missing locally")) && (!msg.startsWith("Not serializable: "))) { System.out.println("Surprising NOTIFS_LOST getMessage: " + msg); } if (!(jn.getUserData() instanceof Long)) { System.out.println("JMXConnectionNotification userData " + "not a Long: " + jn.getUserData()); result.failed = true; } else { int lost = ((Long) jn.getUserData()).intValue(); result.lostCount += lost; if (result.lostCount == NNOTIFS*2) result.notifyAll(); } } } private static class TestFilter implements NotificationFilter { public boolean isNotificationEnabled(Notification n) { return (n.getType().equals("interesting")); } } private static class Result { int knownCount, lostCount; boolean failed; } private static Result result; /* Send a bunch of notifications to exercise the logic to recover from unknown notification classes. We have four kinds of notifications: "known" ones are of a class known to the client and which match its filters; "unknown" ones are of a class that match the client's filters but that the client can't load; "tricky" ones are unserializable; and "boring" notifications are of a class that the client knows but that doesn't match its filters. We emit NNOTIFS notifications of each kind. We do a random shuffle on these 4*NNOTIFS notifications so it is likely that we will cover the various different cases in the logic. Specifically, what we are testing here is the logic that recovers from a fetchNotifications request that gets a ClassNotFoundException. Because the request can contain several notifications, the client doesn't know which of them generated the exception. So it redoes a request that asks for just one notification. We need to be sure that this works when that one notification is of an unknown class and when it is of a known class, and in both cases when there are filtered notifications that are skipped. We are also testing the behaviour in the server when it tries to include an unserializable notification in the response to a fetchNotifications, and in the client when that happens. If the test succeeds, the listener should receive the NNOTIFS "known" notifications, and the connection listener should receive an indication of NNOTIFS lost notifications representing the "unknown" notifications. We depend on some implementation-specific features here: 1. The buffer size is sufficient to contain the 4*NNOTIFS notifications which are all sent at once, before the client gets a chance to start receiving them. 2. When one or more notifications are dropped because they are of unknown classes, the NOTIFS_LOST notification contains a userData that is a Long with a count of the number dropped. 3. If a notification is not serializable on the server, the client gets told about it somehow, rather than having it just dropped on the floor. The somehow might be through an RMI exception, or it might be by the server replacing the unserializable notif by a JMXConnectionNotification.NOTIFS_LOST. */ private static boolean notifyTest(JMXConnector client, MBeanServerConnection mbsc) throws Exception { System.out.println("Send notifications including unknown ones"); result = new Result(); LostListener ll = new LostListener(); client.addConnectionNotificationListener(ll, null, null); TestListener nl = new TestListener(ll); mbsc.addNotificationListener(on, nl, new TestFilter(), null); mbsc.invoke(on, "sendNotifs", NO_OBJECTS, NO_STRINGS); // wait for the listeners to receive all their notifs // or to fail long deadline = System.currentTimeMillis() + 60000; long remain; while ((remain = deadline - System.currentTimeMillis()) >= 0) { synchronized (result) { if (result.failed || (result.knownCount >= NNOTIFS && result.lostCount >= NNOTIFS*2)) break; result.wait(remain); } } Thread.sleep(2); // allow any spurious extra notifs to arrive if (result.failed) { System.out.println("TEST FAILS: Notification strangeness"); return false; } else if (result.knownCount == NNOTIFS && result.lostCount == NNOTIFS*2) { System.out.println("Success: received known notifications and " + "got NOTIFS_LOST for unknown and " + "unserializable ones"); return true; } else if (result.knownCount >= NNOTIFS || result.lostCount >= NNOTIFS*2) { System.out.println("TEST FAILS: Received too many notifs: " + "known=" + result.knownCount + "; lost=" + result.lostCount); return false; } else { System.out.println("TEST FAILS: Timed out without receiving " + "all notifs: known=" + result.knownCount + "; lost=" + result.lostCount); return false; } } public static interface TestMBean { public Object getClientUnknown() throws Exception; public void throwClientUnknown() throws Exception; public void setServerUnknown(Object o) throws Exception; public void useServerUnknown(Object o) throws Exception; public Object getUnserializable() throws Exception; public void setUnserializable(Object un) throws Exception; public void throwUnserializable() throws Exception; public void sendNotifs() throws Exception; } public static class Test extends NotificationBroadcasterSupport implements TestMBean { public Object getClientUnknown() { return clientUnknown; } public void throwClientUnknown() throws Exception { throw clientUnknown; } public void setServerUnknown(Object o) { throw new IllegalArgumentException("setServerUnknown succeeded "+ "but should not have"); } public void useServerUnknown(Object o) { throw new IllegalArgumentException("useServerUnknown succeeded "+ "but should not have"); } public Object getUnserializable() { return unserializableObject; } public void setUnserializable(Object un) { throw new IllegalArgumentException("setUnserializable succeeded " + "but should not have"); } public void throwUnserializable() throws Exception { throw new Exception() { private Object unserializable = unserializableObject; }; } public void sendNotifs() { /* We actually send the same four notification objects NNOTIFS times each. This doesn't particularly matter, but note that the MBeanServer will replace "this" by the sender's ObjectName the first time. Since that's always the same, no problem. */ Notification known = new Notification("interesting", this, 1L, 1L, "known"); known.setUserData("known"); Notification unknown = new Notification("interesting", this, 1L, 1L, "unknown"); unknown.setUserData(clientUnknown); Notification boring = new Notification("boring", this, 1L, 1L, "boring"); Notification tricky = new Notification("interesting", this, 1L, 1L, "tricky"); tricky.setUserData(unserializableObject); // check that the tricky notif is indeed unserializable try { new ObjectOutputStream(new ByteArrayOutputStream()) .writeObject(tricky); throw new RuntimeException("TEST INCORRECT: tricky notif is " + "serializable"); } catch (NotSerializableException e) { // OK: tricky notif is not serializable } catch (IOException e) { throw new RuntimeException("TEST INCORRECT: tricky notif " + "serialization check failed"); } /* Now shuffle an imaginary deck of cards where K, U, T, and B (known, unknown, tricky, boring) each appear NNOTIFS times. We explicitly seed the random number generator so we can reproduce rare test failures if necessary. We only use a StringBuffer so we can print the shuffled deck -- otherwise we could just emit the notifications as the cards are placed. */ long seed = System.currentTimeMillis(); System.out.println("Random number seed is " + seed); Random r = new Random(seed); int knownCount = NNOTIFS; // remaining K cards int unknownCount = NNOTIFS; // remaining U cards int trickyCount = NNOTIFS; // remaining T cards int boringCount = NNOTIFS; // remaining B cards StringBuffer notifList = new StringBuffer(); for (int i = NNOTIFS * 4; i > 0; i--) { int rand = r.nextInt(i); // use rand to pick a card from the remaining ones if ((rand -= knownCount) < 0) { notifList.append('k'); knownCount--; } else if ((rand -= unknownCount) < 0) { notifList.append('u'); unknownCount--; } else if ((rand -= trickyCount) < 0) { notifList.append('t'); trickyCount--; } else { notifList.append('b'); boringCount--; } } if (knownCount != 0 || unknownCount != 0 || trickyCount != 0 || boringCount != 0) { throw new RuntimeException("TEST INCORRECT: Shuffle failed: " + "known=" + knownCount +" unknown=" + unknownCount + " tricky=" + trickyCount + " boring=" + boringCount + " deal=" + notifList); } String notifs = notifList.toString(); System.out.println("Shuffle: " + notifs); for (int i = 0; i < NNOTIFS * 4; i++) { Notification n; switch (notifs.charAt(i)) { case 'k': n = known; break; case 'u': n = unknown; break; case 't': n = tricky; break; case 'b': n = boring; break; default: throw new RuntimeException("TEST INCORRECT: Bad shuffle char: " + notifs.charAt(i)); } sendNotification(n); } } } private static String notificationString(Notification n) { return n.getClass().getName() + "/" + n.getType() + " \"" + n.getMessage() + "\" <" + n.getUserData() + ">"; } // private static class CNListener implements NotificationListener { public void handleNotification(Notification n, Object o) { if (n instanceof JMXConnectionNotification) { JMXConnectionNotification jn = (JMXConnectionNotification)n; if (JMXConnectionNotification.FAILED.equals(jn.getType())) { failed = true; } } } public boolean failed = false; } }