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  * @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 }