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