Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

8200559: Java agents doing instrumentation need a means to define auxiliary classes #3546

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/java.base/share/classes/module-info.java
Expand Up @@ -147,6 +147,7 @@
jdk.jshell;
exports jdk.internal.access to
java.desktop,
java.instrument,
java.logging,
java.management,
java.naming,
Expand Down Expand Up @@ -174,6 +175,7 @@
exports jdk.internal.logger to
java.logging;
exports jdk.internal.org.objectweb.asm to
java.instrument,
jdk.jartool,
jdk.jfr,
jdk.jlink;
Expand All @@ -190,6 +192,7 @@
jdk.jfr;
exports jdk.internal.misc to
java.desktop,
java.instrument,
java.logging,
java.management,
java.naming,
Expand Down
Expand Up @@ -535,6 +535,50 @@ public interface Instrumentation {
void
appendToSystemClassLoaderSearch(JarFile jarfile);

/**
* Defines a new class or interface for the given class loader using the specified
* class file. This has the same effect as if calling
* {@link ClassLoader#defineClass(String, byte[], int, int, ProtectionDomain)}.
*
* <p> The {@code bytes} parameter is the class bytes of a valid class file (as defined
* by the <em>The Java Virtual Machine Specification</em>) with a class name in the
* same package as the lookup class. </p>
*
* <p> This method does not run the class initializer. The class initializer may
* run at a later time, as detailed in section 12.4 of the <em>The Java Language
* Specification</em>. </p>
*
* <p>
* <cite>The Java Virtual Machine Specification</cite>
* specifies that a subsequent attempt to resolve a symbolic
* reference that the Java virtual machine has previously unsuccessfully attempted
* to resolve always fails with the same error that was thrown as a result of the
* initial resolution attempt. Consequently, if the JAR file contains an entry
* that corresponds to a class for which the Java virtual machine has
* unsuccessfully attempted to resolve a reference, then subsequent attempts to
* resolve that reference will fail with the same error as the initial attempt.
*
* @param loader The class loader in which the class is to be defined. This might
* be {@code null} to represent the bootstrap class loader.
* @param pd The class's protection domain or {@code null} for defining a
* default protection domain with all permissions.
* @param bytes The class file of the class that is being defined.
* @throws java.lang.NullPointerException if passed <code>null</code> for the
* class file
* @throws java.lang.ClassFormatError if passed an invalid class file.\
* @throws java.lang.IllegalArgumentException if passed a non-injectable class
* file, for example a module-info
* @throws java.lang.UnsupportedClassVersionError if passed a class file in a
* version that the current VM does not support.
* @throws VerifyError if the newly created class cannot be verified
* @throws LinkageError if a class with the same name already was defined by
* the targeted class loader or if the class cannot be linked for any other reason
* @return The class that has been defined.
*
* @since 17
*/
Class<?> defineClass(ClassLoader loader, ProtectionDomain pd, byte[] bytes);

/**
* Returns whether the current JVM configuration supports
* {@linkplain #setNativeMethodPrefix(ClassFileTransformer,String)
Expand Down
Expand Up @@ -44,7 +44,12 @@
import java.util.Set;
import java.util.jar.JarFile;

import jdk.internal.access.SharedSecrets;
import jdk.internal.misc.VM;
import jdk.internal.module.Modules;
import jdk.internal.org.objectweb.asm.ClassReader;
import jdk.internal.org.objectweb.asm.Opcodes;
import jdk.internal.org.objectweb.asm.Type;
import jdk.internal.vm.annotation.IntrinsicCandidate;

/*
Expand Down Expand Up @@ -326,6 +331,67 @@ public void redefineModule(Module module,
}
}

@Override
public Class<?> defineClass(ClassLoader loader, ProtectionDomain pd, byte[] bytes) {
int magic = readInt(bytes, 0);
if (magic != 0xCAFEBABE) {
throw new ClassFormatError("Incompatible magic value: " + magic);
}
int minor = readUnsignedShort(bytes, 4);
int major = readUnsignedShort(bytes, 6);
if (!VM.isSupportedClassFileVersion(major, minor)) {
throw new UnsupportedClassVersionError("Unsupported class file version " + major + "." + minor);
}

String name;
int accessFlags;
try {
ClassReader reader = new ClassReader(bytes);
// ClassReader::getClassName does not check if `this_class` is CONSTANT_Class_info
// workaround to read `this_class` using readConst and validate the value
int thisClass = reader.readUnsignedShort(reader.header + 2);
Object constant = reader.readConst(thisClass, new char[reader.getMaxStringLength()]);
if (!(constant instanceof Type type)) {
throw new ClassFormatError("this_class item: #" + thisClass + " not a CONSTANT_Class_info");
}
if (!type.getDescriptor().startsWith("L")) {
throw new ClassFormatError("this_class item: #" + thisClass + " not a CONSTANT_Class_info");
}
name = type.getClassName();
accessFlags = reader.readUnsignedShort(reader.header);
} catch (RuntimeException e) {
// ASM exceptions are poorly specified
ClassFormatError cfe = new ClassFormatError();
cfe.initCause(e);
throw cfe;
}

// must be a class or interface
if ((accessFlags & Opcodes.ACC_MODULE) != 0) {
throw new IllegalArgumentException("Not a class or interface: ACC_MODULE flag is set");
}

return SharedSecrets.getJavaLangAccess()
.defineClass(loader, name, bytes, pd, "__instrumentation_defined__");
}

private static int readInt(byte[] bytes, int offset) {
if ((offset+4) > bytes.length) {
throw new ClassFormatError("Invalid ClassFile structure");
}
return ((bytes[offset] & 0xFF) << 24)
| ((bytes[offset + 1] & 0xFF) << 16)
| ((bytes[offset + 2] & 0xFF) << 8)
| (bytes[offset + 3] & 0xFF);
}

private static int readUnsignedShort(byte[] bytes, int offset) {
if ((offset+2) > bytes.length) {
throw new ClassFormatError("Invalid ClassFile structure");
}
return ((bytes[offset] & 0xFF) << 8) | (bytes[offset + 1] & 0xFF);
}

private Map<String, Set<Module>>
cloneAndCheckMap(Module module, Map<String, Set<Module>> map)
{
Expand Down
@@ -0,0 +1,72 @@
/*
* Copyright (c) 2021, 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 8200559
* @summary Test class definition by use of instrumentation API.
* @library /test/lib
* @modules java.instrument
* @run main RedefineClassHelper
* @run main/othervm -javaagent:redefineagent.jar DefineClassInstrumentation
*/

import java.io.InputStream;
import java.lang.instrument.Instrumentation;
import java.net.URL;
import java.net.URLClassLoader;
import java.security.AllPermission;
import java.security.ProtectionDomain;

public class DefineClassInstrumentation {

public static void main(String[] unused) throws Throwable {
doDefine(null);
doDefine(new ProtectionDomain(null, null));
}

private static void doDefine(ProtectionDomain pd) {
try {
URLClassLoader loader = new URLClassLoader(new URL[0], null);

byte[] classFile;
try (InputStream inputStream = DefineClassInstrumentation.class.getResourceAsStream("DefineClassInstrumentation.class")) {
classFile = inputStream.readAllBytes();
}
Class<?> c = RedefineClassHelper.instrumentation.defineClass(loader, pd, classFile);
if (c == DefineClassInstrumentation.class) {
throw new RuntimeException("Class defined by system loader");
}
if (pd == null) {
if (!c.getProtectionDomain().getPermissions().implies(new AllPermission())) {
throw new RuntimeException("Protection domain not set to default protection domain");
}
} else if (pd != c.getProtectionDomain()) {
throw new RuntimeException("Protection domain not set correctly");
}
} catch (Throwable t) {
t.printStackTrace();
throw new RuntimeException("Failed class definition");
}
}
}