--- /dev/null 2018-04-28 00:26:07.190086997 -0400 +++ new/test/jdk/com/sun/jdi/RedefineNestmateAttr/TestNestmateAttr.java 2018-05-18 03:37:56.179508988 -0400 @@ -0,0 +1,582 @@ +/* + * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @bug 8010319 + * @summary Class redefinition must preclude changes to nest attributes + * @comment This is a copy of test/jdk/java/lang/instrument/RedefineNestmateAttr/ + * @comment modified for JDI + * @library /test/lib .. + * @modules java.compiler + * @run build TestScaffold VMConnection TargetListener TargetAdapter + * @compile NamedBuffer.java + * @compile Host/Host.java + * @run main/othervm TestNestmateAttr Host + * @compile HostA/Host.java + * @run main/othervm TestNestmateAttr HostA + * @compile HostAB/Host.java + * @run main/othervm TestNestmateAttr HostAB + * @compile HostABC/Host.java + * @run main/othervm TestNestmateAttr HostABC + */ + +/* Test Description + +The basic test class is called Host and we have variants that have zero or more +nested classes named A, B, C etc. Each variant of Host is defined in source +code in its own directory i.e. + +Host/Host.java defines zero nested classes +HostA/Host.java defines one nested class A +HostAB/Host.java defines two nested classes A and B (in that order) +etc. + +Each Host class has the form: + + public class Host { + public static String getID() { return "/Host.java"; } + + < zero or more empty nested classes> + + public int m() { + return 1; // original class + } + } + +Under each directory is a directory "redef" with a modified version of the Host +class that changes the ID to e.g. Host/redef/Host.java, and the method m() +returns 2. This allows us to check we have the redefined class loaded. + +Using Host' to represent the redefined version we test redefinition +combinations as follows: + +Host: + Host -> Host' - succeeds m() returns 2 + Host -> HostA' - fails - added a nest member + +HostA: + HostA -> HostA' - succeeds m() returns 2 + HostA -> Host' - fails - removed a nest member + HostA -> HostAB' - fails - added a nest member + HostA -> HostB' - fails - replaced a nest member + +HostAB: + HostAB -> HostAB' - succeeds m() returns 2 + HostAB -> HostBA' - succeeds m() returns 2 + HostAB -> HostA' - fails - removed a nest member + HostAB -> HostABC' - fails - added a nest member + HostAB -> HostAC' - fails - replaced a nest member + +HostABC: + HostABC -> HostABC' - succeeds m() returns 2 + HostABC -> HostACB' - succeeds m() returns 2 + HostABC -> HostBAC' - succeeds m() returns 2 + HostABC -> HostBCA' - succeeds m() returns 2 + HostABC -> HostCAB' - succeeds m() returns 2 + HostABC -> HostCBA' - succeeds m() returns 2 + HostABC -> HostAB' - fails - removed a nest member + HostABC -> HostABCD' - fails - added a nest member + HostABC -> HostABD' - fails - replaced a nest member + +More than three nested classes doesn't add to the code coverage so +we stop here. + +Note that we always try to load the redefined version even when we expect it +to fail. + +We can only directly load one class Host per classloader, so to run all the +groups we either need to use new classloaders, or we reinvoke the test +requesting a different primary directory. We chose the latter using +multiple @run tags. So we proceed as follows: + + @compile Host/Host.java + @run TestNestmateAttr Host + @compile HostA/Host.java - replaces previous Host.class + @run TestNestmateAttr HostA + @compile HostAB/Host.java - replaces previous Host.class + @run TestNestmateAttr HostAB +etc. + +Within the test we directly compile redefined versions of the classes, +using CompilerUtil, and then read the .class file directly as a byte[]. + +Finally we test redefinition of the NestHost attribute - which is +conceptually simple, but in fact very tricky to do. We do that +when testing HostA so we can reuse the Host$A class. + +*/ + +import com.sun.jdi.*; +import com.sun.jdi.event.*; +import com.sun.jdi.request.*; + +import java.io.File; +import java.io.FileInputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import jdk.test.lib.ByteCodeLoader; +import jdk.test.lib.compiler.CompilerUtils; +import jdk.test.lib.compiler.InMemoryJavaCompiler; +import static jdk.test.lib.Asserts.assertTrue; + +/* For JDI the test is split across two VMs and so split into + two main classes. This is the class we will run under the debugger. + Package access so we can define in the same source file for ease of + reference. +*/ +class Target { + // We have to load all of the variants of the classes that we will + // attempt to redefine. This requires some in-memory compilation + // and use of additional classloaders. + public static void main(String[] args) throws Throwable { + String origin = args[0]; + System.out.println("Target: Testing original Host class from " + origin); + + // Make sure the Host class loaded directly is an original version + // and from the expected location + Host h = new Host(); + assertTrue(h.m() == 1); + assertTrue(Host.getID().startsWith(origin + "/")); + + // The rest of this setup is only needed for the case + // when we perform the checkNestHostChanges() test. + if (origin.equals("HostA")) { + String name = "Host$A"; + + // Have to do this reflectively as there is no Host$A + // when compiling the "Host/" case. + Class nestedA = Class.forName(name); // triggers initialization + + // This is compiled as a top-level class: the $ in the name is not + // significant to the compiler. + String hostA = "public class " + name + " {}"; + byte[] bytes = InMemoryJavaCompiler.compile(name, hostA); + // And we have to load this into a new classloader + Class topLevelHostA = ByteCodeLoader.load(name, bytes); + // The loaded class has not been linked (as per ClassLoader.resolveClass) + // and so will be filtered out by VirtualMachine.allClasses(). There are + // a number of ways to force linking - this is the simplest. + Object o = topLevelHostA.newInstance(); + + // sanity check + assertTrue(nestedA.getClassLoader() != topLevelHostA.getClassLoader()); + + } + + allowRedefine(); // debugger stops us here to attempt redefinitions + + System.out.println("Target executed okay"); + } + + public static void allowRedefine() { } +} + +public class TestNestmateAttr extends TestScaffold { + + static final String SRC = System.getProperty("test.src"); + static final String DEST = System.getProperty("test.classes"); + static final boolean VERBOSE = Boolean.getBoolean("verbose"); + + static String origin; + + // override this to correct a bug so arguments can be passed to + // the Target class + protected void startUp(String targetName) { + List argList = new ArrayList<>(Arrays.asList(args)); + argList.add(0, targetName); // pre-pend so it becomes the first "app" arg + println("run args: " + argList); + connect((String[]) argList.toArray(args)); + waitForVMStart(); + } + + TestNestmateAttr (String[] args) { + super(args); + } + + public static void main(String[] args) throws Throwable { + origin = args[0]; + new TestNestmateAttr(args).startTests(); + } + + public void runTests() throws Exception { + // Get Target into debuggable state + BreakpointEvent bpe = startToMain("Target"); + EventRequestManager erm = vm().eventRequestManager(); + MethodEntryRequest mee = erm.createMethodEntryRequest(); + mee.addClassFilter("Target"); + mee.enable(); + + // Allow application to complete and shut down + listenUntilVMDisconnect(); + + if (getExceptionCaught()) { + throw new Exception("TestNestmateAttr: failed due to unexpected exception - check logs for details"); + } + else if (!testFailed) { + println("TestNestmateAttr: passed"); + } else { + throw new Exception("TestNestmateAttr: failure reported - check log for details"); + } + } + + // All the actual work is done from here once we see we've entered Target.allowRedefine() + public void methodEntered(MethodEntryEvent event) { + Method meth = event.location().method(); + + if (!meth.name().equals("allowRedefine")) { + return; + } + + System.out.println("TestNestmateAttr: Testing original Host class from " + origin); + + String[] badTransforms; // directories of bad classes + String[] goodTransforms; // directories of good classes + + boolean testNestHostChanges = false; + + switch (origin) { + case "Host": + badTransforms = new String[] { + "HostA" // add member + }; + goodTransforms = new String[] { + origin + }; + break; + + case "HostA": + badTransforms = new String[] { + "Host", // remove member + "HostAB", // add member + "HostB" // change member + }; + goodTransforms = new String[] { + origin + }; + testNestHostChanges = true; + break; + + case "HostAB": + badTransforms = new String[] { + "HostA", // remove member + "HostABC", // add member + "HostAC" // change member + }; + goodTransforms = new String[] { + origin, + "HostBA" // reorder members + }; + break; + + case "HostABC": + badTransforms = new String[] { + "HostAB", // remove member + "HostABCD", // add member + "HostABD" // change member + }; + goodTransforms = new String[] { + origin, + "HostACB", // reorder members + "HostBAC", // reorder members + "HostBCA", // reorder members + "HostCAB", // reorder members + "HostCBA" // reorder members + }; + break; + + default: throw new Error("Unknown test directory: " + origin); + } + + // Need to locate the type we will be trying to redefine in Target + findReferenceTypes(); + + try { + // Compile and check bad transformations + checkBadTransforms(_Host, badTransforms); + + // Compile and check good transformations + checkGoodTransforms(_Host, goodTransforms); + + if (testNestHostChanges) + checkNestHostChanges(); + } + catch (Throwable t) { + failure(t); + } + } + + // override to give exception details + protected void failure(Throwable t) { + super.failure(t.getMessage()); + t.printStackTrace(System.out); + } + + // These are references to the types in Target + // that we will be trying to redefine. + ReferenceType _Host; + ReferenceType _Host_A_nested; + ReferenceType _Host_A_topLevel; + + void findReferenceTypes() { + List classes = vm().allClasses(); + ClassLoaderReference cl = null; // track the main loader + ReferenceType a1 = null; + ReferenceType a2 = null; + for (ReferenceType c : classes) { + String name = c.name(); + if (name.equals("Host")) { + _Host = c; + cl = c.classLoader(); + } + else if (name.equals("Host$A")) { + if (a1 == null) { + a1 = c; + } else if (a2 == null) { + a2 = c; + } + else { + assertTrue(false); // Too many Host$A classes found! + } + } + } + assertTrue(_Host != null); + + // The rest of this setup is only needed for the case + // when we perform the checkNestHostChanges() test. + if (origin.equals("HostA")) { + assertTrue(a1 != null); + assertTrue(a2 != null); + + if (a1.classLoader() == cl) { + _Host_A_nested = a1; + assertTrue(a2.classLoader() != cl); + _Host_A_topLevel = a2; + } + else if (a2.classLoader() == cl) { + _Host_A_nested = a2; + assertTrue(a1.classLoader() != cl); + _Host_A_topLevel = a1; + } + else { + assertTrue(false); // Wrong classLoaders found + } + } + } + + void checkNestHostChanges() throws Throwable { + Map map = new HashMap<>(); + + // case 1: remove NestHost attribute + // - try to redefine nested Host$A with a top-level + // class called Host$A + System.out.println("Trying bad retransform that removes the NestHost attribute"); + + String name = "Host$A"; + + // This is compiled as a top-level class: the $ in the name is not + // significant to the compiler. + String hostA = "public class " + name + " {}"; + byte[] bytes = InMemoryJavaCompiler.compile(name, hostA); + + map.put(_Host_A_nested, bytes); + + try { + vm().redefineClasses(map); + throw new Error("Retransformation to top-level class " + name + + " succeeded unexpectedly"); + } + catch (UnsupportedOperationException uoe) { + if (uoe.getMessage().contains("changes to class attribute not implemented")) { + System.out.println("Got expected exception " + uoe); + } + else throw new Error("Wrong UnsupportedOperationException", uoe); + } + + map.clear(); + + // case 2: add NestHost attribute + // - This is tricky because the class with no NestHost attribute + // has to have the name of a nested class! But we know how to + // do that as we already created a top-level Host$A. So now + // we try to replace with a really nested Host$A. + + System.out.println("Trying bad retransform that adds the NestHost attribute"); + + byte[] nestedBytes; + File clsfile = new File(DEST + "/" + name + ".class"); + if (VERBOSE) System.out.println("Reading bytes from " + clsfile); + try (FileInputStream str = new FileInputStream(clsfile)) { + nestedBytes = NamedBuffer.loadBufferFromStream(str); + } + + map.put(_Host_A_topLevel, nestedBytes); + + try { + vm().redefineClasses(map); + throw new Error("Retransformation to nested class " + name + + " succeeded unexpectedly"); + } + catch (UnsupportedOperationException uoe) { + if (uoe.getMessage().contains("changes to class attribute not implemented")) { + System.out.println("Got expected exception " + uoe); + } + else throw new Error("Wrong UnsupportedOperationException", uoe); + } + + map.clear(); + + // case 3: replace the NestHost attribute + // - the easiest way (perhaps only reasonable way) to do this + // is to search for the Utf8 entry used by the Constant_ClassRef, + // set in the NestHost attribute, and edit it to refer to a different + // name. We reuse nestedBytes from above. + + System.out.println("Trying bad retransform that changes the NestHost attribute"); + + int utf8Entry_length = 7; + boolean found = false; + for (int i = 0; i < nestedBytes.length - utf8Entry_length; i++) { + if (nestedBytes[i] == 1 && // utf8 tag + nestedBytes[i+1] == 0 && // msb of length + nestedBytes[i+2] == 4 && // lsb of length + nestedBytes[i+3] == (byte) 'H' && + nestedBytes[i+4] == (byte) 'o' && + nestedBytes[i+5] == (byte) 's' && + nestedBytes[i+6] == (byte) 't') { + + if (VERBOSE) System.out.println("Appear to have found Host utf8 entry starting at " + i); + + nestedBytes[i+3] = (byte) 'G'; + found = true; + break; + } + } + + if (!found) + throw new Error("Could not locate 'Host' name in byte array"); + + map.put(_Host_A_nested, nestedBytes); + + try { + vm().redefineClasses(map); + throw new Error("Retransformation to modified nested class" + + " succeeded unexpectedly"); + } + catch (UnsupportedOperationException uoe) { + if (uoe.getMessage().contains("changes to class attribute not implemented")) { + System.out.println("Got expected exception " + uoe); + } + else throw new Error("Wrong UnsupportedOperationException", uoe); + } + + } + + void checkGoodTransforms(ReferenceType c, String[] dirs) throws Throwable { + // To verify the redefinition actually took place we will invoke the + // Host.getID method and check the result. To do that we need to find the + // main thread in the target VM. We don't check that "(new Host()).m()" + // returns 2 due to the complexity of setting that up via JDI. + + ThreadReference main = null; + List threads = vm().allThreads(); + for (ThreadReference t : threads) { + if (t.name().equals("main")) { + main = t; + break; + } + } + + assertTrue(main != null); + + // Now find the method + Method getID = null; + List methods = _Host.methodsByName("getID"); + assertTrue(methods.size() == 1); + getID = methods.get(0); + + Map map = new HashMap<>(); + for (String dir : dirs) { + dir += "/redef"; + System.out.println("Trying good retransform from " + dir); + byte[] buf = bytesForHostClass(dir); + map.put(c, buf); + vm().redefineClasses(map); + map.clear(); + // Test redefinition worked + Value v = ((ClassType)_Host).invokeMethod(main, getID, Collections.emptyList(), 0); + assertTrue(v instanceof StringReference); + String id = ((StringReference)v).value(); + if (VERBOSE) System.out.println("Redefined ID: " + id); + assertTrue(id.startsWith(dir)); + assertTrue(id.contains("/redef/")); + } + } + + void checkBadTransforms(ReferenceType c, String[] dirs) throws Throwable { + Map map = new HashMap<>(); + for (String dir : dirs) { + dir += "/redef"; + System.out.println("Trying bad retransform from " + dir); + byte[] buf = bytesForHostClass(dir); + map.put(c, buf); + try { + vm().redefineClasses(map); + throw new Error("Retransformation from directory " + dir + + " succeeded unexpectedly"); + } + catch (UnsupportedOperationException uoe) { + if (uoe.getMessage().contains("changes to class attribute not implemented")) { + System.out.println("Got expected exception " + uoe); + } + else throw new Error("Wrong UnsupportedOperationException", uoe); + } + } + } + + static byte[] bytesForHostClass(String dir) throws Throwable { + compile("/" + dir); + File clsfile = new File(DEST + "/" + dir + "/Host.class"); + if (VERBOSE) System.out.println("Reading bytes from " + clsfile); + byte[] buf = null; + try (FileInputStream str = new FileInputStream(clsfile)) { + return buf = NamedBuffer.loadBufferFromStream(str); + } + } + + static void compile(String dir) throws Throwable { + File src = new File(SRC + dir); + File dst = new File(DEST + dir); + if (VERBOSE) System.out.println("Compiling from: " + src + "\n" + + " to: " + dst); + CompilerUtils.compile(src.toPath(), + dst.toPath(), + false /* don't recurse */, + new String[0]); + } +}