|
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 * @library /test/lib |
|
29 * @modules java.base/jdk.internal.misc |
|
30 * @modules java.compiler |
|
31 * java.instrument |
|
32 * jdk.jartool/sun.tools.jar |
|
33 * @compile ../NamedBuffer.java |
|
34 * @run main RedefineClassHelper |
|
35 * @compile Host/Host.java |
|
36 * @run main/othervm -javaagent:redefineagent.jar -Xlog:redefine+class+nestmates=trace TestNestmateAttr Host |
|
37 * @compile HostA/Host.java |
|
38 * @run main/othervm -javaagent:redefineagent.jar -Xlog:redefine+class+nestmates=trace TestNestmateAttr HostA |
|
39 * @compile HostAB/Host.java |
|
40 * @run main/othervm -javaagent:redefineagent.jar -Xlog:redefine+class+nestmates=trace TestNestmateAttr HostAB |
|
41 * @compile HostABC/Host.java |
|
42 * @run main/othervm -javaagent:redefineagent.jar -Xlog:redefine+class+nestmates=trace TestNestmateAttr HostABC |
|
43 */ |
|
44 |
|
45 /* Test Description |
|
46 |
|
47 The basic test class is call Host and we have variants that have zero or more |
|
48 nested classes named A, B, C etc. Each variant of Host is defined in source |
|
49 code in its own directory i.e. |
|
50 |
|
51 Host/Host.java defines zero nested classes |
|
52 HostA/Host.java defines one nested class A |
|
53 HostAB/Host.java defines two nested classes A and B (in that order) |
|
54 etc. |
|
55 |
|
56 Each Host class has the form: |
|
57 |
|
58 public class Host { |
|
59 public static String getID() { return "<directory name>/Host.java"; } |
|
60 |
|
61 < zero or more empty nested classes> |
|
62 |
|
63 public int m() { |
|
64 return 1; // original class |
|
65 } |
|
66 } |
|
67 |
|
68 Under each directory is a directory "redef" with a modified version of the Host |
|
69 class that changes the ID to e.g. Host/redef/Host.java, and the method m() |
|
70 returns 2. This allows us to check we have the redefined class loaded. |
|
71 |
|
72 Using Host' to represent the redefined version we test redefinition |
|
73 combinations as follows: |
|
74 |
|
75 Host: |
|
76 Host -> Host' - succeeds m() returns 2 |
|
77 Host -> HostA' - fails - added a nest member |
|
78 |
|
79 HostA: |
|
80 HostA -> HostA' - succeeds m() returns 2 |
|
81 HostA -> Host' - fails - removed a nest member |
|
82 HostA -> HostAB' - fails - added a nest member |
|
83 HostA -> HostB' - fails - replaced a nest member |
|
84 |
|
85 HostAB: |
|
86 HostAB -> HostAB' - succeeds m() returns 2 |
|
87 HostAB -> HostBA' - succeeds m() returns 2 |
|
88 HostAB -> HostA' - fails - removed a nest member |
|
89 HostAB -> HostABC' - fails - added a nest member |
|
90 HostAB -> HostAC' - fails - replaced a nest member |
|
91 |
|
92 HostABC: |
|
93 HostABC -> HostABC' - succeeds m() returns 2 |
|
94 HostABC -> HostACB' - succeeds m() returns 2 |
|
95 HostABC -> HostBAC' - succeeds m() returns 2 |
|
96 HostABC -> HostBCA' - succeeds m() returns 2 |
|
97 HostABC -> HostCAB' - succeeds m() returns 2 |
|
98 HostABC -> HostCBA' - succeeds m() returns 2 |
|
99 HostABC -> HostAB' - fails - removed a nest member |
|
100 HostABC -> HostABCD' - fails - added a nest member |
|
101 HostABC -> HostABD' - fails - replaced a nest member |
|
102 |
|
103 More than three nested classes doesn't add to the code coverage so |
|
104 we stop here. |
|
105 |
|
106 Note that we always try to load the redefined version even when we expect it |
|
107 to fail. |
|
108 |
|
109 We can only directly load one class Host per classloader, so to run all the |
|
110 groups we either need to use new classloaders, or we reinvoke the test |
|
111 requesting a different primary directory. We chose the latter using |
|
112 multiple @run tags. So we preceed as follows: |
|
113 |
|
114 @compile Host/Host.java |
|
115 @run TestNestmateAttr Host |
|
116 @compile HostA/Host.java - replaces previous Host.class |
|
117 @run TestNestmateAttr HostA |
|
118 @compile HostAB/Host.java - replaces previous Host.class |
|
119 @run TestNestmateAttr HostAB |
|
120 etc. |
|
121 |
|
122 Within the test we directly compile redefined versions of the classes, |
|
123 using CompilerUtil, and then read the .class file directly as a byte[] |
|
124 to use with the RedefineClassHelper. |
|
125 |
|
126 Finally we test redefinition of the NestHost attribute - which is |
|
127 conceptually simple, but in fact very tricky to do. We do that |
|
128 when testing HostA so we can reuse the Host$A class. |
|
129 |
|
130 */ |
|
131 |
|
132 import java.io.File; |
|
133 import java.io.FileInputStream; |
|
134 import jdk.test.lib.ByteCodeLoader; |
|
135 import jdk.test.lib.compiler.CompilerUtils; |
|
136 import jdk.test.lib.compiler.InMemoryJavaCompiler; |
|
137 import static jdk.test.lib.Asserts.assertTrue; |
|
138 |
|
139 public class TestNestmateAttr { |
|
140 |
|
141 static final String SRC = System.getProperty("test.src"); |
|
142 static final String DEST = System.getProperty("test.classes"); |
|
143 static final boolean VERBOSE = Boolean.getBoolean("verbose"); |
|
144 |
|
145 public static void main(String[] args) throws Throwable { |
|
146 String origin = args[0]; |
|
147 System.out.println("Testing original Host class from " + origin); |
|
148 |
|
149 // Make sure the Host class loaded directly is an original version |
|
150 // and from the expected location |
|
151 Host h = new Host(); |
|
152 assertTrue(h.m() == 1); |
|
153 assertTrue(Host.getID().startsWith(origin + "/")); |
|
154 |
|
155 String[] badTransforms; // directories of bad classes |
|
156 String[] goodTransforms; // directories of good classes |
|
157 |
|
158 boolean testNestHostChanges = false; |
|
159 |
|
160 switch (origin) { |
|
161 case "Host": |
|
162 badTransforms = new String[] { |
|
163 "HostA" // add member |
|
164 }; |
|
165 goodTransforms = new String[] { |
|
166 origin |
|
167 }; |
|
168 break; |
|
169 |
|
170 case "HostA": |
|
171 badTransforms = new String[] { |
|
172 "Host", // remove member |
|
173 "HostAB", // add member |
|
174 "HostB" // change member |
|
175 }; |
|
176 goodTransforms = new String[] { |
|
177 origin |
|
178 }; |
|
179 testNestHostChanges = true; |
|
180 break; |
|
181 |
|
182 case "HostAB": |
|
183 badTransforms = new String[] { |
|
184 "HostA", // remove member |
|
185 "HostABC", // add member |
|
186 "HostAC" // change member |
|
187 }; |
|
188 goodTransforms = new String[] { |
|
189 origin, |
|
190 "HostBA" // reorder members |
|
191 }; |
|
192 break; |
|
193 |
|
194 case "HostABC": |
|
195 badTransforms = new String[] { |
|
196 "HostAB", // remove member |
|
197 "HostABCD", // add member |
|
198 "HostABD" // change member |
|
199 }; |
|
200 goodTransforms = new String[] { |
|
201 origin, |
|
202 "HostACB", // reorder members |
|
203 "HostBAC", // reorder members |
|
204 "HostBCA", // reorder members |
|
205 "HostCAB", // reorder members |
|
206 "HostCBA" // reorder members |
|
207 }; |
|
208 break; |
|
209 |
|
210 default: throw new Error("Unknown test directory: " + origin); |
|
211 } |
|
212 |
|
213 // Compile and check bad transformations |
|
214 checkBadTransforms(Host.class, badTransforms); |
|
215 |
|
216 // Compile and check good transformations |
|
217 checkGoodTransforms(Host.class, goodTransforms); |
|
218 |
|
219 if (testNestHostChanges) |
|
220 checkNestHostChanges(); |
|
221 } |
|
222 |
|
223 static void checkNestHostChanges() throws Throwable { |
|
224 // case 1: remove NestHost attribute |
|
225 // - try to redefine Host$A with a top-level |
|
226 // class called Host$A |
|
227 System.out.println("Trying bad retransform that removes the NestHost attribute"); |
|
228 |
|
229 String name = "Host$A"; |
|
230 // This is compiled as a top-level class: the $ in the name is not |
|
231 // significant to the compiler. |
|
232 String hostA = "public class " + name + " {}"; |
|
233 |
|
234 // Have to do this reflectively as there is no Host$A |
|
235 // when compiling the "Host/" case. |
|
236 Class<?> nestedA = Class.forName(name); |
|
237 |
|
238 try { |
|
239 RedefineClassHelper.redefineClass(nestedA, hostA); |
|
240 throw new Error("Retransformation to top-level class " + name + |
|
241 " succeeded unexpectedly"); |
|
242 } |
|
243 catch (UnsupportedOperationException uoe) { |
|
244 if (uoe.getMessage().contains("attempted to change the class Nest")) { |
|
245 System.out.println("Got expected exception " + uoe); |
|
246 } |
|
247 else throw new Error("Wrong UnsupportedOperationException", uoe); |
|
248 } |
|
249 |
|
250 // case 2: add NestHost attribute |
|
251 // - This is tricky because the class with no NestHost attribute |
|
252 // has to have the name of a nested class! Plus we need the |
|
253 // redefining class in bytecode form. |
|
254 // - Use the InMemoryJavaCompiler plus ByteCodeLoader to load |
|
255 // the top-level Host$A class |
|
256 // - Try to redefine that class with a real nested Host$A |
|
257 |
|
258 System.out.println("Trying bad retransform that adds the NestHost attribute"); |
|
259 byte[] bytes = InMemoryJavaCompiler.compile(name, hostA); |
|
260 Class<?> topLevelHostA = ByteCodeLoader.load(name, bytes); |
|
261 |
|
262 byte[] nestedBytes; |
|
263 File clsfile = new File(DEST + "/" + name + ".class"); |
|
264 if (VERBOSE) System.out.println("Reading bytes from " + clsfile); |
|
265 try (FileInputStream str = new FileInputStream(clsfile)) { |
|
266 nestedBytes = NamedBuffer.loadBufferFromStream(str); |
|
267 } |
|
268 try { |
|
269 RedefineClassHelper.redefineClass(topLevelHostA, nestedBytes); |
|
270 throw new Error("Retransformation to nested class " + name + |
|
271 " succeeded unexpectedly"); |
|
272 } |
|
273 catch (UnsupportedOperationException uoe) { |
|
274 if (uoe.getMessage().contains("attempted to change the class Nest")) { |
|
275 System.out.println("Got expected exception " + uoe); |
|
276 } |
|
277 else throw new Error("Wrong UnsupportedOperationException", uoe); |
|
278 } |
|
279 |
|
280 // case 3: replace the NestHost attribute |
|
281 // - the easiest way (perhaps only reasonable way) to do this |
|
282 // is to search for the Utf8 entry used by the Constant_ClassRef, |
|
283 // set in the NestHost attribute, and edit it to refer to a different |
|
284 // name. |
|
285 System.out.println("Trying bad retransform that changes the NestHost attribute"); |
|
286 int utf8Entry_length = 7; |
|
287 boolean found = false; |
|
288 for (int i = 0; i < nestedBytes.length - utf8Entry_length; i++) { |
|
289 if (nestedBytes[i] == 1 && // utf8 tag |
|
290 nestedBytes[i+1] == 0 && // msb of length |
|
291 nestedBytes[i+2] == 4 && // lsb of length |
|
292 nestedBytes[i+3] == (byte) 'H' && |
|
293 nestedBytes[i+4] == (byte) 'o' && |
|
294 nestedBytes[i+5] == (byte) 's' && |
|
295 nestedBytes[i+6] == (byte) 't') { |
|
296 |
|
297 if (VERBOSE) System.out.println("Appear to have found Host utf8 entry starting at " + i); |
|
298 |
|
299 nestedBytes[i+3] = (byte) 'G'; |
|
300 found = true; |
|
301 break; |
|
302 } |
|
303 } |
|
304 |
|
305 if (!found) |
|
306 throw new Error("Could not locate 'Host' name in byte array"); |
|
307 |
|
308 try { |
|
309 RedefineClassHelper.redefineClass(nestedA, nestedBytes); |
|
310 throw new Error("Retransformation to modified nested class" + |
|
311 " succeeded unexpectedly"); |
|
312 } |
|
313 catch (UnsupportedOperationException uoe) { |
|
314 if (uoe.getMessage().contains("attempted to change the class Nest")) { |
|
315 System.out.println("Got expected exception " + uoe); |
|
316 } |
|
317 else throw new Error("Wrong UnsupportedOperationException", uoe); |
|
318 } |
|
319 |
|
320 } |
|
321 |
|
322 static void checkGoodTransforms(Class<?> c, String[] dirs) throws Throwable { |
|
323 for (String dir : dirs) { |
|
324 dir += "/redef"; |
|
325 System.out.println("Trying good retransform from " + dir); |
|
326 byte[] buf = bytesForHostClass(dir); |
|
327 RedefineClassHelper.redefineClass(c, buf); |
|
328 |
|
329 // Test redefintion worked |
|
330 Host h = new Host(); |
|
331 assertTrue(h.m() == 2); |
|
332 if (VERBOSE) System.out.println("Redefined ID: " + Host.getID()); |
|
333 assertTrue(Host.getID().startsWith(dir)); |
|
334 } |
|
335 } |
|
336 |
|
337 static void checkBadTransforms(Class<?> c, String[] dirs) throws Throwable { |
|
338 for (String dir : dirs) { |
|
339 dir += "/redef"; |
|
340 System.out.println("Trying bad retransform from " + dir); |
|
341 byte[] buf = bytesForHostClass(dir); |
|
342 try { |
|
343 RedefineClassHelper.redefineClass(c, buf); |
|
344 throw new Error("Retransformation from directory " + dir + |
|
345 " succeeded unexpectedly"); |
|
346 } |
|
347 catch (UnsupportedOperationException uoe) { |
|
348 if (uoe.getMessage().contains("attempted to change the class Nest")) { |
|
349 System.out.println("Got expected exception " + uoe); |
|
350 } |
|
351 else throw new Error("Wrong UnsupportedOperationException", uoe); |
|
352 } |
|
353 } |
|
354 } |
|
355 |
|
356 static byte[] bytesForHostClass(String dir) throws Throwable { |
|
357 compile("/" + dir); |
|
358 File clsfile = new File(DEST + "/" + dir + "/Host.class"); |
|
359 if (VERBOSE) System.out.println("Reading bytes from " + clsfile); |
|
360 byte[] buf = null; |
|
361 try (FileInputStream str = new FileInputStream(clsfile)) { |
|
362 return buf = NamedBuffer.loadBufferFromStream(str); |
|
363 } |
|
364 } |
|
365 |
|
366 static void compile(String dir) throws Throwable { |
|
367 File src = new File(SRC + dir); |
|
368 File dst = new File(DEST + dir); |
|
369 if (VERBOSE) System.out.println("Compiling from: " + src + "\n" + |
|
370 " to: " + dst); |
|
371 CompilerUtils.compile(src.toPath(), |
|
372 dst.toPath(), |
|
373 false /* don't recurse */, |
|
374 new String[0]); |
|
375 } |
|
376 } |