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