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 8010319 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 }