1 /* 2 * Copyright (c) 2003, 2013, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. 8 * 9 * This code is distributed in the hope that it will be useful, but WITHOUT 10 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 11 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 12 * version 2 for more details (a copy is included in the LICENSE file that 13 * accompanied this code). 14 * 15 * You should have received a copy of the GNU General Public License version 16 * 2 along with this work; if not, write to the Free Software Foundation, 17 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 18 * 19 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 20 * or visit www.oracle.com if you need additional information or have any 21 * questions. 22 */ 23 24 /* 25 * @test 26 * @bug 4915825 4921009 4934965 4977469 8019584 27 * @summary Tests behavior when client or server gets object of unknown class 28 * @author Eamonn McManus 29 * @run clean MissingClassTest SingleClassLoader 30 * @run build MissingClassTest SingleClassLoader 31 * @run main MissingClassTest 32 * @key randomness 33 */ 34 35 /* 36 Tests that clients and servers react correctly when they receive 37 objects of unknown classes. This can happen easily due to version 38 skew or missing jar files on one end or the other. The default 39 behaviour of causing a connection to die because of the resultant 40 IOException is not acceptable! We try sending attributes and invoke 41 parameters to the server of classes it doesn't know, and we try 42 sending attributes, exceptions and notifications to the client of 43 classes it doesn't know. 44 45 We also test objects that are of known class but not serializable. 46 The test cases are similar. 47 */ 48 49 import java.io.ByteArrayOutputStream; 50 import java.io.IOException; 51 import java.io.NotSerializableException; 52 import java.io.ObjectOutputStream; 53 import java.net.MalformedURLException; 54 import java.util.HashMap; 55 import java.util.Map; 56 import java.util.Random; 57 import java.util.Set; 58 import javax.management.Attribute; 59 import javax.management.MBeanServer; 60 import javax.management.MBeanServerConnection; 61 import javax.management.MBeanServerFactory; 62 import javax.management.Notification; 63 import javax.management.NotificationBroadcasterSupport; 64 import javax.management.NotificationFilter; 65 import javax.management.NotificationListener; 66 import javax.management.ObjectName; 67 import javax.management.remote.JMXConnectionNotification; 68 import javax.management.remote.JMXConnector; 69 import javax.management.remote.JMXConnectorFactory; 70 import javax.management.remote.JMXConnectorServer; 71 import javax.management.remote.JMXConnectorServerFactory; 72 import javax.management.remote.JMXServiceURL; 73 import javax.management.remote.rmi.RMIConnectorServer; 74 75 public class MissingClassTest { 76 private static final int NNOTIFS = 50; 77 78 private static ClassLoader clientLoader, serverLoader; 79 private static Object serverUnknown; 80 private static Exception clientUnknown; 81 private static ObjectName on; 82 private static final Object[] NO_OBJECTS = new Object[0]; 83 private static final String[] NO_STRINGS = new String[0]; 84 85 private static final Object unserializableObject = Thread.currentThread(); 86 87 private static boolean isInstance(Object o, String cn) { 88 try { 89 Class<?> c = Class.forName(cn); 90 return c.isInstance(o); 91 } catch (ClassNotFoundException x) { 92 return false; 93 } 94 } 95 96 public static void main(String[] args) throws Exception { 97 System.out.println("Test that the client or server end of a " + 98 "connection does not fail if sent an object " + 99 "it cannot deserialize"); 100 101 on = new ObjectName("test:type=Test"); 102 103 ClassLoader testLoader = MissingClassTest.class.getClassLoader(); 104 clientLoader = 105 new SingleClassLoader("$ServerUnknown$", HashMap.class, 106 testLoader); 107 serverLoader = 108 new SingleClassLoader("$ClientUnknown$", Exception.class, 109 testLoader); 110 serverUnknown = 111 clientLoader.loadClass("$ServerUnknown$").newInstance(); 112 clientUnknown = (Exception) 113 serverLoader.loadClass("$ClientUnknown$").newInstance(); 114 115 final String[] protos = {"rmi", /*"iiop",*/ "jmxmp"}; 116 boolean ok = true; 117 for (int i = 0; i < protos.length; i++) { 118 try { 119 ok &= test(protos[i]); 120 } catch (Exception e) { 121 System.out.println("TEST FAILED WITH EXCEPTION:"); 122 e.printStackTrace(System.out); 123 ok = false; 124 } 125 } 126 127 if (ok) 128 System.out.println("Test passed"); 129 else { 130 throw new RuntimeException("TEST FAILED"); 131 } 132 } 133 134 private static boolean test(String proto) throws Exception { 135 System.out.println("Testing for proto " + proto); 136 137 boolean ok = true; 138 139 MBeanServer mbs = MBeanServerFactory.newMBeanServer(); 140 mbs.createMBean(Test.class.getName(), on); 141 142 JMXConnectorServer cs; 143 JMXServiceURL url = new JMXServiceURL(proto, null, 0); 144 Map<String,Object> serverMap = new HashMap<>(); 145 serverMap.put(JMXConnectorServerFactory.DEFAULT_CLASS_LOADER, 146 serverLoader); 147 148 // make sure no auto-close at server side 149 serverMap.put("jmx.remote.x.server.connection.timeout", "888888888"); 150 151 try { 152 cs = JMXConnectorServerFactory.newJMXConnectorServer(url, 153 serverMap, 154 mbs); 155 } catch (MalformedURLException e) { 156 System.out.println("System does not recognize URL: " + url + 157 "; ignoring"); 158 return true; 159 } 160 cs.start(); 161 JMXServiceURL addr = cs.getAddress(); 162 Map<String,Object> clientMap = new HashMap<>(); 163 clientMap.put(JMXConnectorFactory.DEFAULT_CLASS_LOADER, 164 clientLoader); 165 166 System.out.println("Connecting for client-unknown test"); 167 168 JMXConnector client = JMXConnectorFactory.connect(addr, clientMap); 169 170 // add a listener to verify no failed notif 171 CNListener cnListener = new CNListener(); 172 client.addConnectionNotificationListener(cnListener, null, null); 173 174 MBeanServerConnection mbsc = client.getMBeanServerConnection(); 175 176 System.out.println("Getting attribute with class unknown to client"); 177 try { 178 Object result = mbsc.getAttribute(on, "ClientUnknown"); 179 System.out.println("TEST FAILS: getAttribute for class " + 180 "unknown to client should fail, returned: " + 181 result); 182 ok = false; 183 } catch (IOException e) { 184 Throwable cause = e.getCause(); 185 if (isInstance(cause, "org.omg.CORBA.MARSHAL")) // see CR 4935098 186 cause = cause.getCause(); 187 if (cause instanceof ClassNotFoundException) { 188 System.out.println("Success: got an IOException wrapping " + 189 "a ClassNotFoundException"); 190 } else { 191 System.out.println("TEST FAILS: Caught IOException (" + e + 192 ") but cause should be " + 193 "ClassNotFoundException: " + cause); 194 ok = false; 195 } 196 } 197 198 System.out.println("Doing queryNames to ensure connection alive"); 199 Set<ObjectName> names = mbsc.queryNames(null, null); 200 System.out.println("queryNames returned " + names); 201 202 System.out.println("Provoke exception of unknown class"); 203 try { 204 mbsc.invoke(on, "throwClientUnknown", NO_OBJECTS, NO_STRINGS); 205 System.out.println("TEST FAILS: did not get exception"); 206 ok = false; 207 } catch (IOException e) { 208 Throwable wrapped = e.getCause(); 209 if (isInstance(wrapped, "org.omg.CORBA.MARSHAL")) // see CR 4935098 210 wrapped = wrapped.getCause(); 211 if (wrapped instanceof ClassNotFoundException) { 212 System.out.println("Success: got an IOException wrapping " + 213 "a ClassNotFoundException: " + 214 wrapped); 215 } else { 216 System.out.println("TEST FAILS: Got IOException but cause " + 217 "should be ClassNotFoundException: "); 218 if (wrapped == null) 219 System.out.println("(null)"); 220 else 221 wrapped.printStackTrace(System.out); 222 ok = false; 223 } 224 } catch (Exception e) { 225 System.out.println("TEST FAILS: Got wrong exception: " + 226 "should be IOException with cause " + 227 "ClassNotFoundException:"); 228 e.printStackTrace(System.out); 229 ok = false; 230 } 231 232 System.out.println("Doing queryNames to ensure connection alive"); 233 names = mbsc.queryNames(null, null); 234 System.out.println("queryNames returned " + names); 235 236 ok &= notifyTest(client, mbsc); 237 238 System.out.println("Doing queryNames to ensure connection alive"); 239 names = mbsc.queryNames(null, null); 240 System.out.println("queryNames returned " + names); 241 242 for (int i = 0; i < 2; i++) { 243 boolean setAttribute = (i == 0); // else invoke 244 String what = setAttribute ? "setAttribute" : "invoke"; 245 System.out.println("Trying " + what + 246 " with class unknown to server"); 247 try { 248 if (setAttribute) { 249 mbsc.setAttribute(on, new Attribute("ServerUnknown", 250 serverUnknown)); 251 } else { 252 mbsc.invoke(on, "useServerUnknown", 253 new Object[] {serverUnknown}, 254 new String[] {"java.lang.Object"}); 255 } 256 System.out.println("TEST FAILS: " + what + " with " + 257 "class unknown to server should fail " + 258 "but did not"); 259 ok = false; 260 } catch (IOException e) { 261 Throwable cause = e.getCause(); 262 if (isInstance(cause, "org.omg.CORBA.MARSHAL")) // see CR 4935098 263 cause = cause.getCause(); 264 if (cause instanceof ClassNotFoundException) { 265 System.out.println("Success: got an IOException " + 266 "wrapping a ClassNotFoundException"); 267 } else { 268 System.out.println("TEST FAILS: Caught IOException (" + e + 269 ") but cause should be " + 270 "ClassNotFoundException: " + cause); 271 e.printStackTrace(System.out); // XXX 272 ok = false; 273 } 274 } 275 } 276 277 System.out.println("Doing queryNames to ensure connection alive"); 278 names = mbsc.queryNames(null, null); 279 System.out.println("queryNames returned " + names); 280 281 System.out.println("Trying to get unserializable attribute"); 282 try { 283 mbsc.getAttribute(on, "Unserializable"); 284 System.out.println("TEST FAILS: get unserializable worked " + 285 "but should not"); 286 ok = false; 287 } catch (IOException e) { 288 System.out.println("Success: got an IOException: " + e + 289 " (cause: " + e.getCause() + ")"); 290 } 291 292 System.out.println("Doing queryNames to ensure connection alive"); 293 names = mbsc.queryNames(null, null); 294 System.out.println("queryNames returned " + names); 295 296 System.out.println("Trying to set unserializable attribute"); 297 try { 298 Attribute attr = 299 new Attribute("Unserializable", unserializableObject); 300 mbsc.setAttribute(on, attr); 301 System.out.println("TEST FAILS: set unserializable worked " + 302 "but should not"); 303 ok = false; 304 } catch (IOException e) { 305 System.out.println("Success: got an IOException: " + e + 306 " (cause: " + e.getCause() + ")"); 307 } 308 309 System.out.println("Doing queryNames to ensure connection alive"); 310 names = mbsc.queryNames(null, null); 311 System.out.println("queryNames returned " + names); 312 313 System.out.println("Trying to throw unserializable exception"); 314 try { 315 mbsc.invoke(on, "throwUnserializable", NO_OBJECTS, NO_STRINGS); 316 System.out.println("TEST FAILS: throw unserializable worked " + 317 "but should not"); 318 ok = false; 319 } catch (IOException e) { 320 System.out.println("Success: got an IOException: " + e + 321 " (cause: " + e.getCause() + ")"); 322 } 323 324 client.removeConnectionNotificationListener(cnListener); 325 ok = ok && !cnListener.failed; 326 327 client.close(); 328 cs.stop(); 329 330 if (ok) 331 System.out.println("Test passed for protocol " + proto); 332 333 System.out.println(); 334 return ok; 335 } 336 337 private static class TestListener implements NotificationListener { 338 TestListener(LostListener ll) { 339 this.lostListener = ll; 340 } 341 342 public void handleNotification(Notification n, Object h) { 343 /* Connectors can handle unserializable notifications in 344 one of two ways. Either they can arrange for the 345 client to get a NotSerializableException from its 346 fetchNotifications call (RMI connector), or they can 347 replace the unserializable notification by a 348 JMXConnectionNotification.NOTIFS_LOST (JMXMP 349 connector). The former case is handled by code within 350 the connector client which will end up sending a 351 NOTIFS_LOST to our LostListener. The logic here 352 handles the latter case by converting it into the 353 former. 354 */ 355 if (n instanceof JMXConnectionNotification 356 && n.getType().equals(JMXConnectionNotification.NOTIFS_LOST)) { 357 lostListener.handleNotification(n, h); 358 return; 359 } 360 361 synchronized (result) { 362 if (!n.getType().equals("interesting") 363 || !n.getUserData().equals("known")) { 364 System.out.println("TestListener received strange notif: " 365 + notificationString(n)); 366 result.failed = true; 367 result.notifyAll(); 368 } else { 369 result.knownCount++; 370 if (result.knownCount == NNOTIFS) 371 result.notifyAll(); 372 } 373 } 374 } 375 376 private LostListener lostListener; 377 } 378 379 private static class LostListener implements NotificationListener { 380 public void handleNotification(Notification n, Object h) { 381 synchronized (result) { 382 handle(n, h); 383 } 384 } 385 386 private void handle(Notification n, Object h) { 387 if (!(n instanceof JMXConnectionNotification)) { 388 System.out.println("LostListener received strange notif: " + 389 notificationString(n)); 390 result.failed = true; 391 result.notifyAll(); 392 return; 393 } 394 395 JMXConnectionNotification jn = (JMXConnectionNotification) n; 396 if (!jn.getType().equals(jn.NOTIFS_LOST)) { 397 System.out.println("Ignoring JMXConnectionNotification: " + 398 notificationString(jn)); 399 return; 400 } 401 final String msg = jn.getMessage(); 402 if ((!msg.startsWith("Dropped ") 403 || !msg.endsWith("classes were missing locally")) 404 && (!msg.startsWith("Not serializable: "))) { 405 System.out.println("Surprising NOTIFS_LOST getMessage: " + 406 msg); 407 } 408 if (!(jn.getUserData() instanceof Long)) { 409 System.out.println("JMXConnectionNotification userData " + 410 "not a Long: " + jn.getUserData()); 411 result.failed = true; 412 } else { 413 int lost = ((Long) jn.getUserData()).intValue(); 414 result.lostCount += lost; 415 if (result.lostCount == NNOTIFS*2) 416 result.notifyAll(); 417 } 418 } 419 } 420 421 private static class TestFilter implements NotificationFilter { 422 public boolean isNotificationEnabled(Notification n) { 423 return (n.getType().equals("interesting")); 424 } 425 } 426 427 private static class Result { 428 int knownCount, lostCount; 429 boolean failed; 430 } 431 private static Result result; 432 433 /* Send a bunch of notifications to exercise the logic to recover 434 from unknown notification classes. We have four kinds of 435 notifications: "known" ones are of a class known to the client 436 and which match its filters; "unknown" ones are of a class that 437 match the client's filters but that the client can't load; 438 "tricky" ones are unserializable; and "boring" notifications 439 are of a class that the client knows but that doesn't match its 440 filters. We emit NNOTIFS notifications of each kind. We do a 441 random shuffle on these 4*NNOTIFS notifications so it is likely 442 that we will cover the various different cases in the logic. 443 444 Specifically, what we are testing here is the logic that 445 recovers from a fetchNotifications request that gets a 446 ClassNotFoundException. Because the request can contain 447 several notifications, the client doesn't know which of them 448 generated the exception. So it redoes a request that asks for 449 just one notification. We need to be sure that this works when 450 that one notification is of an unknown class and when it is of 451 a known class, and in both cases when there are filtered 452 notifications that are skipped. 453 454 We are also testing the behaviour in the server when it tries 455 to include an unserializable notification in the response to a 456 fetchNotifications, and in the client when that happens. 457 458 If the test succeeds, the listener should receive the NNOTIFS 459 "known" notifications, and the connection listener should 460 receive an indication of NNOTIFS lost notifications 461 representing the "unknown" notifications. 462 463 We depend on some implementation-specific features here: 464 465 1. The buffer size is sufficient to contain the 4*NNOTIFS 466 notifications which are all sent at once, before the client 467 gets a chance to start receiving them. 468 469 2. When one or more notifications are dropped because they are 470 of unknown classes, the NOTIFS_LOST notification contains a 471 userData that is a Long with a count of the number dropped. 472 473 3. If a notification is not serializable on the server, the 474 client gets told about it somehow, rather than having it just 475 dropped on the floor. The somehow might be through an RMI 476 exception, or it might be by the server replacing the 477 unserializable notif by a JMXConnectionNotification.NOTIFS_LOST. 478 */ 479 private static boolean notifyTest(JMXConnector client, 480 MBeanServerConnection mbsc) 481 throws Exception { 482 System.out.println("Send notifications including unknown ones"); 483 result = new Result(); 484 LostListener ll = new LostListener(); 485 client.addConnectionNotificationListener(ll, null, null); 486 TestListener nl = new TestListener(ll); 487 mbsc.addNotificationListener(on, nl, new TestFilter(), null); 488 mbsc.invoke(on, "sendNotifs", NO_OBJECTS, NO_STRINGS); 489 490 // wait for the listeners to receive all their notifs 491 // or to fail 492 long deadline = System.currentTimeMillis() + 60000; 493 long remain; 494 while ((remain = deadline - System.currentTimeMillis()) >= 0) { 495 synchronized (result) { 496 if (result.failed 497 || (result.knownCount >= NNOTIFS 498 && result.lostCount >= NNOTIFS*2)) 499 break; 500 result.wait(remain); 501 } 502 } 503 Thread.sleep(2); // allow any spurious extra notifs to arrive 504 if (result.failed) { 505 System.out.println("TEST FAILS: Notification strangeness"); 506 return false; 507 } else if (result.knownCount == NNOTIFS 508 && result.lostCount == NNOTIFS*2) { 509 System.out.println("Success: received known notifications and " + 510 "got NOTIFS_LOST for unknown and " + 511 "unserializable ones"); 512 return true; 513 } else if (result.knownCount >= NNOTIFS 514 || result.lostCount >= NNOTIFS*2) { 515 System.out.println("TEST FAILS: Received too many notifs: " + 516 "known=" + result.knownCount + "; lost=" + result.lostCount); 517 return false; 518 } else { 519 System.out.println("TEST FAILS: Timed out without receiving " + 520 "all notifs: known=" + result.knownCount + 521 "; lost=" + result.lostCount); 522 return false; 523 } 524 } 525 526 public static interface TestMBean { 527 public Object getClientUnknown() throws Exception; 528 public void throwClientUnknown() throws Exception; 529 public void setServerUnknown(Object o) throws Exception; 530 public void useServerUnknown(Object o) throws Exception; 531 public Object getUnserializable() throws Exception; 532 public void setUnserializable(Object un) throws Exception; 533 public void throwUnserializable() throws Exception; 534 public void sendNotifs() throws Exception; 535 } 536 537 public static class Test extends NotificationBroadcasterSupport 538 implements TestMBean { 539 540 public Object getClientUnknown() { 541 return clientUnknown; 542 } 543 544 public void throwClientUnknown() throws Exception { 545 throw clientUnknown; 546 } 547 548 public void setServerUnknown(Object o) { 549 throw new IllegalArgumentException("setServerUnknown succeeded "+ 550 "but should not have"); 551 } 552 553 public void useServerUnknown(Object o) { 554 throw new IllegalArgumentException("useServerUnknown succeeded "+ 555 "but should not have"); 556 } 557 558 public Object getUnserializable() { 559 return unserializableObject; 560 } 561 562 public void setUnserializable(Object un) { 563 throw new IllegalArgumentException("setUnserializable succeeded " + 564 "but should not have"); 565 } 566 567 public void throwUnserializable() throws Exception { 568 throw new Exception() { 569 private Object unserializable = unserializableObject; 570 }; 571 } 572 573 public void sendNotifs() { 574 /* We actually send the same four notification objects 575 NNOTIFS times each. This doesn't particularly matter, 576 but note that the MBeanServer will replace "this" by 577 the sender's ObjectName the first time. Since that's 578 always the same, no problem. */ 579 Notification known = 580 new Notification("interesting", this, 1L, 1L, "known"); 581 known.setUserData("known"); 582 Notification unknown = 583 new Notification("interesting", this, 1L, 1L, "unknown"); 584 unknown.setUserData(clientUnknown); 585 Notification boring = 586 new Notification("boring", this, 1L, 1L, "boring"); 587 Notification tricky = 588 new Notification("interesting", this, 1L, 1L, "tricky"); 589 tricky.setUserData(unserializableObject); 590 591 // check that the tricky notif is indeed unserializable 592 try { 593 new ObjectOutputStream(new ByteArrayOutputStream()) 594 .writeObject(tricky); 595 throw new RuntimeException("TEST INCORRECT: tricky notif is " + 596 "serializable"); 597 } catch (NotSerializableException e) { 598 // OK: tricky notif is not serializable 599 } catch (IOException e) { 600 throw new RuntimeException("TEST INCORRECT: tricky notif " + 601 "serialization check failed"); 602 } 603 604 /* Now shuffle an imaginary deck of cards where K, U, T, and 605 B (known, unknown, tricky, boring) each appear NNOTIFS times. 606 We explicitly seed the random number generator so we 607 can reproduce rare test failures if necessary. We only 608 use a StringBuffer so we can print the shuffled deck -- 609 otherwise we could just emit the notifications as the 610 cards are placed. */ 611 long seed = System.currentTimeMillis(); 612 System.out.println("Random number seed is " + seed); 613 Random r = new Random(seed); 614 int knownCount = NNOTIFS; // remaining K cards 615 int unknownCount = NNOTIFS; // remaining U cards 616 int trickyCount = NNOTIFS; // remaining T cards 617 int boringCount = NNOTIFS; // remaining B cards 618 StringBuffer notifList = new StringBuffer(); 619 for (int i = NNOTIFS * 4; i > 0; i--) { 620 int rand = r.nextInt(i); 621 // use rand to pick a card from the remaining ones 622 if ((rand -= knownCount) < 0) { 623 notifList.append('k'); 624 knownCount--; 625 } else if ((rand -= unknownCount) < 0) { 626 notifList.append('u'); 627 unknownCount--; 628 } else if ((rand -= trickyCount) < 0) { 629 notifList.append('t'); 630 trickyCount--; 631 } else { 632 notifList.append('b'); 633 boringCount--; 634 } 635 } 636 if (knownCount != 0 || unknownCount != 0 637 || trickyCount != 0 || boringCount != 0) { 638 throw new RuntimeException("TEST INCORRECT: Shuffle failed: " + 639 "known=" + knownCount +" unknown=" + 640 unknownCount + " tricky=" + trickyCount + 641 " boring=" + boringCount + 642 " deal=" + notifList); 643 } 644 String notifs = notifList.toString(); 645 System.out.println("Shuffle: " + notifs); 646 for (int i = 0; i < NNOTIFS * 4; i++) { 647 Notification n; 648 switch (notifs.charAt(i)) { 649 case 'k': n = known; break; 650 case 'u': n = unknown; break; 651 case 't': n = tricky; break; 652 case 'b': n = boring; break; 653 default: 654 throw new RuntimeException("TEST INCORRECT: Bad shuffle char: " + 655 notifs.charAt(i)); 656 } 657 sendNotification(n); 658 } 659 } 660 } 661 662 private static String notificationString(Notification n) { 663 return n.getClass().getName() + "/" + n.getType() + " \"" + 664 n.getMessage() + "\" <" + n.getUserData() + ">"; 665 } 666 667 // 668 private static class CNListener implements NotificationListener { 669 public void handleNotification(Notification n, Object o) { 670 if (n instanceof JMXConnectionNotification) { 671 JMXConnectionNotification jn = (JMXConnectionNotification)n; 672 if (JMXConnectionNotification.FAILED.equals(jn.getType())) { 673 failed = true; 674 } 675 } 676 } 677 678 public boolean failed = false; 679 } 680 }