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 }