|
1 /* |
|
2 * Copyright (c) 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 8046171 |
|
27 * @summary Class redefinition must preclude changes to nest attributes |
|
28 * @comment This is a copy of test/jdk/java/lang/instrument/RedefineNestmateAttr/ |
|
29 * @comment modified for JDI |
|
30 * @library /test/lib .. |
|
31 * @modules java.compiler |
|
32 * @run build TestScaffold VMConnection TargetListener TargetAdapter |
|
33 * @compile NamedBuffer.java |
|
34 * @compile Host/Host.java |
|
35 * @run main/othervm TestNestmateAttr Host |
|
36 * @compile HostA/Host.java |
|
37 * @run main/othervm TestNestmateAttr HostA |
|
38 * @compile HostAB/Host.java |
|
39 * @run main/othervm TestNestmateAttr HostAB |
|
40 * @compile HostABC/Host.java |
|
41 * @run main/othervm TestNestmateAttr HostABC |
|
42 */ |
|
43 |
|
44 /* Test Description |
|
45 |
|
46 The basic test class is called Host and we have variants that have zero or more |
|
47 nested classes named A, B, C etc. Each variant of Host is defined in source |
|
48 code in its own directory i.e. |
|
49 |
|
50 Host/Host.java defines zero nested classes |
|
51 HostA/Host.java defines one nested class A |
|
52 HostAB/Host.java defines two nested classes A and B (in that order) |
|
53 etc. |
|
54 |
|
55 Each Host class has the form: |
|
56 |
|
57 public class Host { |
|
58 public static String getID() { return "<directory name>/Host.java"; } |
|
59 |
|
60 < zero or more empty nested classes> |
|
61 |
|
62 public int m() { |
|
63 return 1; // original class |
|
64 } |
|
65 } |
|
66 |
|
67 Under each directory is a directory "redef" with a modified version of the Host |
|
68 class that changes the ID to e.g. Host/redef/Host.java, and the method m() |
|
69 returns 2. This allows us to check we have the redefined class loaded. |
|
70 |
|
71 Using Host' to represent the redefined version we test redefinition |
|
72 combinations as follows: |
|
73 |
|
74 Host: |
|
75 Host -> Host' - succeeds m() returns 2 |
|
76 Host -> HostA' - fails - added a nest member |
|
77 |
|
78 HostA: |
|
79 HostA -> HostA' - succeeds m() returns 2 |
|
80 HostA -> Host' - fails - removed a nest member |
|
81 HostA -> HostAB' - fails - added a nest member |
|
82 HostA -> HostB' - fails - replaced a nest member |
|
83 |
|
84 HostAB: |
|
85 HostAB -> HostAB' - succeeds m() returns 2 |
|
86 HostAB -> HostBA' - succeeds m() returns 2 |
|
87 HostAB -> HostA' - fails - removed a nest member |
|
88 HostAB -> HostABC' - fails - added a nest member |
|
89 HostAB -> HostAC' - fails - replaced a nest member |
|
90 |
|
91 HostABC: |
|
92 HostABC -> HostABC' - succeeds m() returns 2 |
|
93 HostABC -> HostACB' - succeeds m() returns 2 |
|
94 HostABC -> HostBAC' - succeeds m() returns 2 |
|
95 HostABC -> HostBCA' - succeeds m() returns 2 |
|
96 HostABC -> HostCAB' - succeeds m() returns 2 |
|
97 HostABC -> HostCBA' - succeeds m() returns 2 |
|
98 HostABC -> HostAB' - fails - removed a nest member |
|
99 HostABC -> HostABCD' - fails - added a nest member |
|
100 HostABC -> HostABD' - fails - replaced a nest member |
|
101 |
|
102 More than three nested classes doesn't add to the code coverage so |
|
103 we stop here. |
|
104 |
|
105 Note that we always try to load the redefined version even when we expect it |
|
106 to fail. |
|
107 |
|
108 We can only directly load one class Host per classloader, so to run all the |
|
109 groups we either need to use new classloaders, or we reinvoke the test |
|
110 requesting a different primary directory. We chose the latter using |
|
111 multiple @run tags. So we proceed as follows: |
|
112 |
|
113 @compile Host/Host.java |
|
114 @run TestNestmateAttr Host |
|
115 @compile HostA/Host.java - replaces previous Host.class |
|
116 @run TestNestmateAttr HostA |
|
117 @compile HostAB/Host.java - replaces previous Host.class |
|
118 @run TestNestmateAttr HostAB |
|
119 etc. |
|
120 |
|
121 Within the test we directly compile redefined versions of the classes, |
|
122 using CompilerUtil, and then read the .class file directly as a byte[]. |
|
123 |
|
124 Finally we test redefinition of the NestHost attribute - which is |
|
125 conceptually simple, but in fact very tricky to do. We do that |
|
126 when testing HostA so we can reuse the Host$A class. |
|
127 |
|
128 */ |
|
129 |
|
130 import com.sun.jdi.*; |
|
131 import com.sun.jdi.event.*; |
|
132 import com.sun.jdi.request.*; |
|
133 |
|
134 import java.io.File; |
|
135 import java.io.FileInputStream; |
|
136 import java.util.ArrayList; |
|
137 import java.util.Arrays; |
|
138 import java.util.Collections; |
|
139 import java.util.HashMap; |
|
140 import java.util.List; |
|
141 import java.util.Map; |
|
142 |
|
143 import jdk.test.lib.ByteCodeLoader; |
|
144 import jdk.test.lib.compiler.CompilerUtils; |
|
145 import jdk.test.lib.compiler.InMemoryJavaCompiler; |
|
146 import static jdk.test.lib.Asserts.assertTrue; |
|
147 |
|
148 /* For JDI the test is split across two VMs and so split into |
|
149 two main classes. This is the class we will run under the debugger. |
|
150 Package access so we can define in the same source file for ease of |
|
151 reference. |
|
152 */ |
|
153 class Target { |
|
154 // We have to load all of the variants of the classes that we will |
|
155 // attempt to redefine. This requires some in-memory compilation |
|
156 // and use of additional classloaders. |
|
157 public static void main(String[] args) throws Throwable { |
|
158 String origin = args[0]; |
|
159 System.out.println("Target: Testing original Host class from " + origin); |
|
160 |
|
161 // Make sure the Host class loaded directly is an original version |
|
162 // and from the expected location |
|
163 Host h = new Host(); |
|
164 assertTrue(h.m() == 1); |
|
165 assertTrue(Host.getID().startsWith(origin + "/")); |
|
166 |
|
167 // The rest of this setup is only needed for the case |
|
168 // when we perform the checkNestHostChanges() test. |
|
169 if (origin.equals("HostA")) { |
|
170 String name = "Host$A"; |
|
171 |
|
172 // Have to do this reflectively as there is no Host$A |
|
173 // when compiling the "Host/" case. |
|
174 Class<?> nestedA = Class.forName(name); // triggers initialization |
|
175 |
|
176 // This is compiled as a top-level class: the $ in the name is not |
|
177 // significant to the compiler. |
|
178 String hostA = "public class " + name + " {}"; |
|
179 byte[] bytes = InMemoryJavaCompiler.compile(name, hostA); |
|
180 // And we have to load this into a new classloader |
|
181 Class<?> topLevelHostA = ByteCodeLoader.load(name, bytes); |
|
182 // The loaded class has not been linked (as per ClassLoader.resolveClass) |
|
183 // and so will be filtered out by VirtualMachine.allClasses(). There are |
|
184 // a number of ways to force linking - this is the simplest. |
|
185 Object o = topLevelHostA.newInstance(); |
|
186 |
|
187 // sanity check |
|
188 assertTrue(nestedA.getClassLoader() != topLevelHostA.getClassLoader()); |
|
189 |
|
190 } |
|
191 |
|
192 allowRedefine(); // debugger stops us here to attempt redefinitions |
|
193 |
|
194 System.out.println("Target executed okay"); |
|
195 } |
|
196 |
|
197 public static void allowRedefine() { } |
|
198 } |
|
199 |
|
200 public class TestNestmateAttr extends TestScaffold { |
|
201 |
|
202 static final String SRC = System.getProperty("test.src"); |
|
203 static final String DEST = System.getProperty("test.classes"); |
|
204 static final boolean VERBOSE = Boolean.getBoolean("verbose"); |
|
205 |
|
206 static String origin; |
|
207 |
|
208 // override this to correct a bug so arguments can be passed to |
|
209 // the Target class |
|
210 protected void startUp(String targetName) { |
|
211 List<String> argList = new ArrayList<>(Arrays.asList(args)); |
|
212 argList.add(0, targetName); // pre-pend so it becomes the first "app" arg |
|
213 println("run args: " + argList); |
|
214 connect((String[]) argList.toArray(args)); |
|
215 waitForVMStart(); |
|
216 } |
|
217 |
|
218 TestNestmateAttr (String[] args) { |
|
219 super(args); |
|
220 } |
|
221 |
|
222 public static void main(String[] args) throws Throwable { |
|
223 origin = args[0]; |
|
224 new TestNestmateAttr(args).startTests(); |
|
225 } |
|
226 |
|
227 public void runTests() throws Exception { |
|
228 // Get Target into debuggable state |
|
229 BreakpointEvent bpe = startToMain("Target"); |
|
230 EventRequestManager erm = vm().eventRequestManager(); |
|
231 MethodEntryRequest mee = erm.createMethodEntryRequest(); |
|
232 mee.addClassFilter("Target"); |
|
233 mee.enable(); |
|
234 |
|
235 // Allow application to complete and shut down |
|
236 listenUntilVMDisconnect(); |
|
237 |
|
238 if (getExceptionCaught()) { |
|
239 throw new Exception("TestNestmateAttr: failed due to unexpected exception - check logs for details"); |
|
240 } |
|
241 else if (!testFailed) { |
|
242 println("TestNestmateAttr: passed"); |
|
243 } else { |
|
244 throw new Exception("TestNestmateAttr: failure reported - check log for details"); |
|
245 } |
|
246 } |
|
247 |
|
248 // All the actual work is done from here once we see we've entered Target.allowRedefine() |
|
249 public void methodEntered(MethodEntryEvent event) { |
|
250 Method meth = event.location().method(); |
|
251 |
|
252 if (!meth.name().equals("allowRedefine")) { |
|
253 return; |
|
254 } |
|
255 |
|
256 System.out.println("TestNestmateAttr: Testing original Host class from " + origin); |
|
257 |
|
258 String[] badTransforms; // directories of bad classes |
|
259 String[] goodTransforms; // directories of good classes |
|
260 |
|
261 boolean testNestHostChanges = false; |
|
262 |
|
263 switch (origin) { |
|
264 case "Host": |
|
265 badTransforms = new String[] { |
|
266 "HostA" // add member |
|
267 }; |
|
268 goodTransforms = new String[] { |
|
269 origin |
|
270 }; |
|
271 break; |
|
272 |
|
273 case "HostA": |
|
274 badTransforms = new String[] { |
|
275 "Host", // remove member |
|
276 "HostAB", // add member |
|
277 "HostB" // change member |
|
278 }; |
|
279 goodTransforms = new String[] { |
|
280 origin |
|
281 }; |
|
282 testNestHostChanges = true; |
|
283 break; |
|
284 |
|
285 case "HostAB": |
|
286 badTransforms = new String[] { |
|
287 "HostA", // remove member |
|
288 "HostABC", // add member |
|
289 "HostAC" // change member |
|
290 }; |
|
291 goodTransforms = new String[] { |
|
292 origin, |
|
293 "HostBA" // reorder members |
|
294 }; |
|
295 break; |
|
296 |
|
297 case "HostABC": |
|
298 badTransforms = new String[] { |
|
299 "HostAB", // remove member |
|
300 "HostABCD", // add member |
|
301 "HostABD" // change member |
|
302 }; |
|
303 goodTransforms = new String[] { |
|
304 origin, |
|
305 "HostACB", // reorder members |
|
306 "HostBAC", // reorder members |
|
307 "HostBCA", // reorder members |
|
308 "HostCAB", // reorder members |
|
309 "HostCBA" // reorder members |
|
310 }; |
|
311 break; |
|
312 |
|
313 default: throw new Error("Unknown test directory: " + origin); |
|
314 } |
|
315 |
|
316 // Need to locate the type we will be trying to redefine in Target |
|
317 findReferenceTypes(); |
|
318 |
|
319 try { |
|
320 // Compile and check bad transformations |
|
321 checkBadTransforms(_Host, badTransforms); |
|
322 |
|
323 // Compile and check good transformations |
|
324 checkGoodTransforms(_Host, goodTransforms); |
|
325 |
|
326 if (testNestHostChanges) |
|
327 checkNestHostChanges(); |
|
328 } |
|
329 catch (Throwable t) { |
|
330 failure(t); |
|
331 } |
|
332 } |
|
333 |
|
334 // override to give exception details |
|
335 protected void failure(Throwable t) { |
|
336 super.failure(t.getMessage()); |
|
337 t.printStackTrace(System.out); |
|
338 } |
|
339 |
|
340 // These are references to the types in Target |
|
341 // that we will be trying to redefine. |
|
342 ReferenceType _Host; |
|
343 ReferenceType _Host_A_nested; |
|
344 ReferenceType _Host_A_topLevel; |
|
345 |
|
346 void findReferenceTypes() { |
|
347 List<ReferenceType> classes = vm().allClasses(); |
|
348 ClassLoaderReference cl = null; // track the main loader |
|
349 ReferenceType a1 = null; |
|
350 ReferenceType a2 = null; |
|
351 for (ReferenceType c : classes) { |
|
352 String name = c.name(); |
|
353 if (name.equals("Host")) { |
|
354 _Host = c; |
|
355 cl = c.classLoader(); |
|
356 } |
|
357 else if (name.equals("Host$A")) { |
|
358 if (a1 == null) { |
|
359 a1 = c; |
|
360 } else if (a2 == null) { |
|
361 a2 = c; |
|
362 } |
|
363 else { |
|
364 assertTrue(false); // Too many Host$A classes found! |
|
365 } |
|
366 } |
|
367 } |
|
368 assertTrue(_Host != null); |
|
369 |
|
370 // The rest of this setup is only needed for the case |
|
371 // when we perform the checkNestHostChanges() test. |
|
372 if (origin.equals("HostA")) { |
|
373 assertTrue(a1 != null); |
|
374 assertTrue(a2 != null); |
|
375 |
|
376 if (a1.classLoader() == cl) { |
|
377 _Host_A_nested = a1; |
|
378 assertTrue(a2.classLoader() != cl); |
|
379 _Host_A_topLevel = a2; |
|
380 } |
|
381 else if (a2.classLoader() == cl) { |
|
382 _Host_A_nested = a2; |
|
383 assertTrue(a1.classLoader() != cl); |
|
384 _Host_A_topLevel = a1; |
|
385 } |
|
386 else { |
|
387 assertTrue(false); // Wrong classLoaders found |
|
388 } |
|
389 } |
|
390 } |
|
391 |
|
392 void checkNestHostChanges() throws Throwable { |
|
393 Map<ReferenceType, byte[]> map = new HashMap<>(); |
|
394 |
|
395 // case 1: remove NestHost attribute |
|
396 // - try to redefine nested Host$A with a top-level |
|
397 // class called Host$A |
|
398 System.out.println("Trying bad retransform that removes the NestHost attribute"); |
|
399 |
|
400 String name = "Host$A"; |
|
401 |
|
402 // This is compiled as a top-level class: the $ in the name is not |
|
403 // significant to the compiler. |
|
404 String hostA = "public class " + name + " {}"; |
|
405 byte[] bytes = InMemoryJavaCompiler.compile(name, hostA); |
|
406 |
|
407 map.put(_Host_A_nested, bytes); |
|
408 |
|
409 try { |
|
410 vm().redefineClasses(map); |
|
411 throw new Error("Retransformation to top-level class " + name + |
|
412 " succeeded unexpectedly"); |
|
413 } |
|
414 catch (UnsupportedOperationException uoe) { |
|
415 if (uoe.getMessage().contains("changes to class attribute not implemented")) { |
|
416 System.out.println("Got expected exception " + uoe); |
|
417 } |
|
418 else throw new Error("Wrong UnsupportedOperationException", uoe); |
|
419 } |
|
420 |
|
421 map.clear(); |
|
422 |
|
423 // case 2: add NestHost attribute |
|
424 // - This is tricky because the class with no NestHost attribute |
|
425 // has to have the name of a nested class! But we know how to |
|
426 // do that as we already created a top-level Host$A. So now |
|
427 // we try to replace with a really nested Host$A. |
|
428 |
|
429 System.out.println("Trying bad retransform that adds the NestHost attribute"); |
|
430 |
|
431 byte[] nestedBytes; |
|
432 File clsfile = new File(DEST + "/" + name + ".class"); |
|
433 if (VERBOSE) System.out.println("Reading bytes from " + clsfile); |
|
434 try (FileInputStream str = new FileInputStream(clsfile)) { |
|
435 nestedBytes = NamedBuffer.loadBufferFromStream(str); |
|
436 } |
|
437 |
|
438 map.put(_Host_A_topLevel, nestedBytes); |
|
439 |
|
440 try { |
|
441 vm().redefineClasses(map); |
|
442 throw new Error("Retransformation to nested class " + name + |
|
443 " succeeded unexpectedly"); |
|
444 } |
|
445 catch (UnsupportedOperationException uoe) { |
|
446 if (uoe.getMessage().contains("changes to class attribute not implemented")) { |
|
447 System.out.println("Got expected exception " + uoe); |
|
448 } |
|
449 else throw new Error("Wrong UnsupportedOperationException", uoe); |
|
450 } |
|
451 |
|
452 map.clear(); |
|
453 |
|
454 // case 3: replace the NestHost attribute |
|
455 // - the easiest way (perhaps only reasonable way) to do this |
|
456 // is to search for the Utf8 entry used by the Constant_ClassRef, |
|
457 // set in the NestHost attribute, and edit it to refer to a different |
|
458 // name. We reuse nestedBytes from above. |
|
459 |
|
460 System.out.println("Trying bad retransform that changes the NestHost attribute"); |
|
461 |
|
462 int utf8Entry_length = 7; |
|
463 boolean found = false; |
|
464 for (int i = 0; i < nestedBytes.length - utf8Entry_length; i++) { |
|
465 if (nestedBytes[i] == 1 && // utf8 tag |
|
466 nestedBytes[i+1] == 0 && // msb of length |
|
467 nestedBytes[i+2] == 4 && // lsb of length |
|
468 nestedBytes[i+3] == (byte) 'H' && |
|
469 nestedBytes[i+4] == (byte) 'o' && |
|
470 nestedBytes[i+5] == (byte) 's' && |
|
471 nestedBytes[i+6] == (byte) 't') { |
|
472 |
|
473 if (VERBOSE) System.out.println("Appear to have found Host utf8 entry starting at " + i); |
|
474 |
|
475 nestedBytes[i+3] = (byte) 'G'; |
|
476 found = true; |
|
477 break; |
|
478 } |
|
479 } |
|
480 |
|
481 if (!found) |
|
482 throw new Error("Could not locate 'Host' name in byte array"); |
|
483 |
|
484 map.put(_Host_A_nested, nestedBytes); |
|
485 |
|
486 try { |
|
487 vm().redefineClasses(map); |
|
488 throw new Error("Retransformation to modified nested class" + |
|
489 " succeeded unexpectedly"); |
|
490 } |
|
491 catch (UnsupportedOperationException uoe) { |
|
492 if (uoe.getMessage().contains("changes to class attribute not implemented")) { |
|
493 System.out.println("Got expected exception " + uoe); |
|
494 } |
|
495 else throw new Error("Wrong UnsupportedOperationException", uoe); |
|
496 } |
|
497 |
|
498 } |
|
499 |
|
500 void checkGoodTransforms(ReferenceType c, String[] dirs) throws Throwable { |
|
501 // To verify the redefinition actually took place we will invoke the |
|
502 // Host.getID method and check the result. To do that we need to find the |
|
503 // main thread in the target VM. We don't check that "(new Host()).m()" |
|
504 // returns 2 due to the complexity of setting that up via JDI. |
|
505 |
|
506 ThreadReference main = null; |
|
507 List<ThreadReference> threads = vm().allThreads(); |
|
508 for (ThreadReference t : threads) { |
|
509 if (t.name().equals("main")) { |
|
510 main = t; |
|
511 break; |
|
512 } |
|
513 } |
|
514 |
|
515 assertTrue(main != null); |
|
516 |
|
517 // Now find the method |
|
518 Method getID = null; |
|
519 List<Method> methods = _Host.methodsByName("getID"); |
|
520 assertTrue(methods.size() == 1); |
|
521 getID = methods.get(0); |
|
522 |
|
523 Map<ReferenceType, byte[]> map = new HashMap<>(); |
|
524 for (String dir : dirs) { |
|
525 dir += "/redef"; |
|
526 System.out.println("Trying good retransform from " + dir); |
|
527 byte[] buf = bytesForHostClass(dir); |
|
528 map.put(c, buf); |
|
529 vm().redefineClasses(map); |
|
530 map.clear(); |
|
531 // Test redefinition worked |
|
532 Value v = ((ClassType)_Host).invokeMethod(main, getID, Collections.emptyList(), 0); |
|
533 assertTrue(v instanceof StringReference); |
|
534 String id = ((StringReference)v).value(); |
|
535 if (VERBOSE) System.out.println("Redefined ID: " + id); |
|
536 assertTrue(id.startsWith(dir)); |
|
537 assertTrue(id.contains("/redef/")); |
|
538 } |
|
539 } |
|
540 |
|
541 void checkBadTransforms(ReferenceType c, String[] dirs) throws Throwable { |
|
542 Map<ReferenceType, byte[]> map = new HashMap<>(); |
|
543 for (String dir : dirs) { |
|
544 dir += "/redef"; |
|
545 System.out.println("Trying bad retransform from " + dir); |
|
546 byte[] buf = bytesForHostClass(dir); |
|
547 map.put(c, buf); |
|
548 try { |
|
549 vm().redefineClasses(map); |
|
550 throw new Error("Retransformation from directory " + dir + |
|
551 " succeeded unexpectedly"); |
|
552 } |
|
553 catch (UnsupportedOperationException uoe) { |
|
554 if (uoe.getMessage().contains("changes to class attribute not implemented")) { |
|
555 System.out.println("Got expected exception " + uoe); |
|
556 } |
|
557 else throw new Error("Wrong UnsupportedOperationException", uoe); |
|
558 } |
|
559 } |
|
560 } |
|
561 |
|
562 static byte[] bytesForHostClass(String dir) throws Throwable { |
|
563 compile("/" + dir); |
|
564 File clsfile = new File(DEST + "/" + dir + "/Host.class"); |
|
565 if (VERBOSE) System.out.println("Reading bytes from " + clsfile); |
|
566 byte[] buf = null; |
|
567 try (FileInputStream str = new FileInputStream(clsfile)) { |
|
568 return buf = NamedBuffer.loadBufferFromStream(str); |
|
569 } |
|
570 } |
|
571 |
|
572 static void compile(String dir) throws Throwable { |
|
573 File src = new File(SRC + dir); |
|
574 File dst = new File(DEST + dir); |
|
575 if (VERBOSE) System.out.println("Compiling from: " + src + "\n" + |
|
576 " to: " + dst); |
|
577 CompilerUtils.compile(src.toPath(), |
|
578 dst.toPath(), |
|
579 false /* don't recurse */, |
|
580 new String[0]); |
|
581 } |
|
582 } |