diff --git a/generator/build.gradle b/generator/build.gradle index fd4a580e..fcfd72d5 100644 --- a/generator/build.gradle +++ b/generator/build.gradle @@ -39,6 +39,9 @@ dependencies { implementation 'net.bytebuddy:byte-buddy' implementation 'org.ow2.asm:asm-commons' + implementation 'net.java.dev.jna:jna' + implementation 'net.java.dev.jna:jna-platform' + implementation 'javax.servlet:javax.servlet-api' implementation 'javax.websocket:javax.websocket-api' diff --git a/generator/src/main/java/com/reajason/javaweb/memshell/Packers.java b/generator/src/main/java/com/reajason/javaweb/memshell/Packers.java index 2616c34f..b2861300 100644 --- a/generator/src/main/java/com/reajason/javaweb/memshell/Packers.java +++ b/generator/src/main/java/com/reajason/javaweb/memshell/Packers.java @@ -19,6 +19,8 @@ import com.reajason.javaweb.memshell.packer.groovy.GroovyClassDefinerPacker; import com.reajason.javaweb.memshell.packer.groovy.GroovyPacker; import com.reajason.javaweb.memshell.packer.groovy.GroovyScriptEnginePacker; import com.reajason.javaweb.memshell.packer.jar.AgentJarPacker; +import com.reajason.javaweb.memshell.packer.jar.AgentJarWithJDKAttacherPacker; +import com.reajason.javaweb.memshell.packer.jar.AgentJarWithJREAttacherPacker; import com.reajason.javaweb.memshell.packer.jar.DefaultJarPacker; import com.reajason.javaweb.memshell.packer.jexl.JEXLPacker; import com.reajason.javaweb.memshell.packer.jinjava.JinJavaPacker; @@ -121,6 +123,8 @@ public enum Packers { HessianXSLTScriptEngine(new HessianXSLTScriptEnginePacker(), HessianPacker.class), AgentJar(new AgentJarPacker()), + AgentJarWithJDKAttacher(new AgentJarWithJDKAttacherPacker()), + AgentJarWithJREAttacher(new AgentJarWithJREAttacherPacker()), XxlJob(new XxlJobPacker()), ; diff --git a/generator/src/main/java/com/reajason/javaweb/memshell/packer/jar/AgentJarWithJDKAttacherPacker.java b/generator/src/main/java/com/reajason/javaweb/memshell/packer/jar/AgentJarWithJDKAttacherPacker.java new file mode 100644 index 00000000..a36110ad --- /dev/null +++ b/generator/src/main/java/com/reajason/javaweb/memshell/packer/jar/AgentJarWithJDKAttacherPacker.java @@ -0,0 +1,189 @@ +package com.reajason.javaweb.memshell.packer.jar; + +import com.reajason.javaweb.asm.ClassRenameUtils; +import com.reajason.javaweb.memshell.config.GenerateResult; +import com.reajason.javaweb.memshell.packer.jar.attach.Attacher; +import com.reajason.javaweb.memshell.packer.jar.attach.VirtualMachine; +import com.reajason.javaweb.memshell.utils.CommonUtil; +import lombok.SneakyThrows; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; +import org.objectweb.asm.Opcodes; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; +import java.util.jar.*; + +/** + * @author ReaJason + * @since 2025/1/1 + */ +public class AgentJarWithJDKAttacherPacker implements JarPacker { + private static Path tempBootPath; + + @Override + @SneakyThrows + public byte[] packBytes(GenerateResult generateResult) { + String packageName = CommonUtil.getPackageName(generateResult.getInjectorClassName()); + String mainClassName = packageName + "." + Attacher.class.getSimpleName(); + Manifest manifest = createManifest(generateResult.getInjectorClassName(), mainClassName); + String relocatePrefix = "shade/"; + + Map classes = new HashMap<>(); + Map attacherClasses = com.reajason.javaweb.buddy.ClassRenameUtils.renamePackage(Attacher.class, packageName); + Map virtualMachineClasses = com.reajason.javaweb.buddy.ClassRenameUtils.renamePackage(VirtualMachine.class, packageName); + classes.putAll(attacherClasses); + classes.putAll(virtualMachineClasses); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + try (JarOutputStream targetJar = new JarOutputStream(outputStream, manifest)) { + addDependencies(targetJar, relocatePrefix); + addClassesToJar(targetJar, generateResult, relocatePrefix); + for (Map.Entry entry : classes.entrySet()) { + String className = entry.getKey(); + byte[] bytes = entry.getValue(); + targetJar.putNextEntry(new JarEntry(className.replace('.', '/') + ".class")); + targetJar.write(bytes); + targetJar.closeEntry(); + } + } + return outputStream.toByteArray(); + } + + private Manifest createManifest(String agentClass, String mainClass) { + Manifest manifest = new Manifest(); + Attributes attributes = manifest.getMainAttributes(); + attributes.putValue("Manifest-Version", "1.0"); + attributes.putValue("Agent-Class", agentClass); + attributes.putValue("Premain-Class", agentClass); + attributes.putValue("Main-Class", mainClass); + attributes.putValue("Can-Redefine-Classes", "true"); + attributes.putValue("Can-Retransform-Classes", "true"); + return manifest; + } + + @SneakyThrows + private void addDependencies(JarOutputStream targetJar, String relocatePrefix) { + String baseName = Opcodes.class.getPackage().getName().replace('.', '/'); + addDependency(targetJar, Opcodes.class, baseName, relocatePrefix); + } + + @SneakyThrows + private void addClassesToJar(JarOutputStream targetJar, GenerateResult generateResult, String relocatePrefix) { + String dependencyPackage = Opcodes.class.getPackage().getName(); + // Add injector class + addClassEntry(targetJar, + generateResult.getInjectorClassName(), + generateResult.getInjectorBytes(), + dependencyPackage, + relocatePrefix); + + // Add shell class + addClassEntry(targetJar, + generateResult.getShellClassName(), + generateResult.getShellBytes(), + dependencyPackage, + relocatePrefix); + + // Add inner classes + for (Map.Entry entry : generateResult.getInjectorInnerClassBytes().entrySet()) { + addClassEntry(targetJar, + entry.getKey(), + entry.getValue(), + dependencyPackage, + relocatePrefix); + } + } + + @SneakyThrows + private void addClassEntry(JarOutputStream targetJar, String className, byte[] classBytes, + String dependencyPackage, String relocatePrefix) { + targetJar.putNextEntry(new JarEntry(className.replace('.', '/') + ".class")); + byte[] processedBytes = ClassRenameUtils.relocateClass(classBytes, dependencyPackage, relocatePrefix + dependencyPackage); + targetJar.write(processedBytes); + targetJar.closeEntry(); + } + + @SneakyThrows + public static void addDependency(JarOutputStream targetJar, Class baseClass, String baseName, String relocatePrefix) { + URL sourceUrl = baseClass.getProtectionDomain().getCodeSource().getLocation(); + String sourceUrlString = sourceUrl.toString(); + if (sourceUrlString.contains("!BOOT-INF")) { + String path = sourceUrlString.substring("jar:nested:".length()); + path = path.substring(0, path.indexOf("!/")); + String[] split = path.split("/!"); + String bootJarPath = split[0]; + String internalJarPath = split[1]; + if (tempBootPath == null) { + tempBootPath = Files.createTempDirectory("mem-shell-boot"); + unzip(bootJarPath, tempBootPath.toFile().getAbsolutePath()); + } + sourceUrl = tempBootPath.resolve(internalJarPath).toUri().toURL(); + } + try (JarFile sourceJar = new JarFile(new File(sourceUrl.toURI()))) { + Enumeration entries = sourceJar.entries(); + while (entries.hasMoreElements()) { + JarEntry entry = entries.nextElement(); + String entryName = entry.getName(); + if (entryName.equals("META-INF/MANIFEST.MF") + || entryName.contains("module-info.class")) { + continue; + } + if (!entry.isDirectory()) { + try (InputStream entryStream = sourceJar.getInputStream(entry)) { + byte[] bytes = IOUtils.toByteArray(entryStream); + if (StringUtils.isNoneEmpty(relocatePrefix)) { + targetJar.putNextEntry(new JarEntry(relocatePrefix + entryName)); + if (entryName.endsWith(".class")) { + if (bytes.length > 0) { + bytes = ClassRenameUtils.relocateClass(bytes, baseName, relocatePrefix + baseName); + } + } else { + targetJar.putNextEntry(entry); + } + } else { + targetJar.putNextEntry(entry); + } + targetJar.write(bytes); + } + } + targetJar.closeEntry(); + } + } + } + + /** + * Extracts a JAR file to a temporary directory + * + * @param jarPath Path to the source JAR file + * @param tempPath Path to the temporary directory + */ + @SneakyThrows + public static void unzip(String jarPath, String tempPath) { + try (JarFile jarFile = new JarFile(jarPath)) { + Enumeration entries = jarFile.entries(); + while (entries.hasMoreElements()) { + JarEntry jarEntry = entries.nextElement(); + File targetFile = new File(tempPath, jarEntry.getName()); + + if (jarEntry.isDirectory()) { + targetFile.mkdirs(); + continue; + } + + targetFile.getParentFile().mkdirs(); + try (InputStream inputStream = jarFile.getInputStream(jarEntry); + FileOutputStream outputStream = new FileOutputStream(targetFile)) { + IOUtils.copy(inputStream, outputStream); + } + } + } + } +} \ No newline at end of file diff --git a/generator/src/main/java/com/reajason/javaweb/memshell/packer/jar/AgentJarWithJREAttacherPacker.java b/generator/src/main/java/com/reajason/javaweb/memshell/packer/jar/AgentJarWithJREAttacherPacker.java new file mode 100644 index 00000000..0c7cc65b --- /dev/null +++ b/generator/src/main/java/com/reajason/javaweb/memshell/packer/jar/AgentJarWithJREAttacherPacker.java @@ -0,0 +1,209 @@ +package com.reajason.javaweb.memshell.packer.jar; + +import com.reajason.javaweb.asm.ClassRenameUtils; +import com.reajason.javaweb.memshell.config.GenerateResult; +import com.reajason.javaweb.memshell.packer.jar.attach.Attacher; +import com.reajason.javaweb.memshell.packer.jar.attach.VirtualMachine; +import com.reajason.javaweb.memshell.utils.CommonUtil; +import com.sun.jna.Platform; +import com.sun.jna.platform.DesktopWindow; +import lombok.SneakyThrows; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; +import org.objectweb.asm.Opcodes; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; +import java.util.jar.*; + +/** + * @author ReaJason + * @since 2025/1/1 + */ +public class AgentJarWithJREAttacherPacker implements JarPacker { + private static Path tempBootPath; + + @Override + @SneakyThrows + public byte[] packBytes(GenerateResult generateResult) { + String packageName = CommonUtil.getPackageName(generateResult.getInjectorClassName()); + String mainClassName = packageName + "." + Attacher.class.getSimpleName(); + Manifest manifest = createManifest(generateResult.getInjectorClassName(), mainClassName); + String relocatePrefix = "shade/"; + + Map classes = new HashMap<>(); + Map attacherClasses = com.reajason.javaweb.buddy.ClassRenameUtils.renamePackage(Attacher.class, packageName); + Map virtualMachineClasses = com.reajason.javaweb.buddy.ClassRenameUtils.renamePackage(VirtualMachine.class, packageName); + classes.putAll(attacherClasses); + classes.putAll(virtualMachineClasses); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + try (JarOutputStream targetJar = new JarOutputStream(outputStream, manifest)) { + addDependencies(targetJar, relocatePrefix); + addClassesToJar(targetJar, generateResult, relocatePrefix); + for (Map.Entry entry : classes.entrySet()) { + String className = entry.getKey(); + byte[] bytes = entry.getValue(); + targetJar.putNextEntry(new JarEntry(className.replace('.', '/') + ".class")); + targetJar.write(bytes); + targetJar.closeEntry(); + } + + String[] windowsDll = new String[]{ + "win32-x86/attach_hotspot_windows.dll", + "win32-x86-64/attach_hotspot_windows.dll" + }; + for (String dll : windowsDll) { + InputStream stream = this.getClass().getClassLoader().getResourceAsStream(dll); + if (stream != null) { + byte[] bytes = IOUtils.toByteArray(stream); + targetJar.putNextEntry(new JarEntry(dll)); + targetJar.write(bytes); + targetJar.closeEntry(); + } + } + } + return outputStream.toByteArray(); + } + + private Manifest createManifest(String agentClass, String mainClass) { + Manifest manifest = new Manifest(); + Attributes attributes = manifest.getMainAttributes(); + attributes.putValue("Manifest-Version", "1.0"); + attributes.putValue("Agent-Class", agentClass); + attributes.putValue("Premain-Class", agentClass); + attributes.putValue("Main-Class", mainClass); + attributes.putValue("Can-Redefine-Classes", "true"); + attributes.putValue("Can-Retransform-Classes", "true"); + return manifest; + } + + @SneakyThrows + private void addDependencies(JarOutputStream targetJar, String relocatePrefix) { + String baseName = Opcodes.class.getPackage().getName().replace('.', '/'); + addDependency(targetJar, Opcodes.class, baseName, relocatePrefix); + + String jnaBaseName = Platform.class.getPackage().getName().replace('.', '/'); + addDependency(targetJar, Platform.class, jnaBaseName, null); + addDependency(targetJar, DesktopWindow.class, jnaBaseName, null); + } + + @SneakyThrows + private void addClassesToJar(JarOutputStream targetJar, GenerateResult generateResult, String relocatePrefix) { + String dependencyPackage = Opcodes.class.getPackage().getName(); + // Add injector class + addClassEntry(targetJar, + generateResult.getInjectorClassName(), + generateResult.getInjectorBytes(), + dependencyPackage, + relocatePrefix); + + // Add shell class + addClassEntry(targetJar, + generateResult.getShellClassName(), + generateResult.getShellBytes(), + dependencyPackage, + relocatePrefix); + + // Add inner classes + for (Map.Entry entry : generateResult.getInjectorInnerClassBytes().entrySet()) { + addClassEntry(targetJar, + entry.getKey(), + entry.getValue(), + dependencyPackage, + relocatePrefix); + } + } + + @SneakyThrows + private void addClassEntry(JarOutputStream targetJar, String className, byte[] classBytes, + String dependencyPackage, String relocatePrefix) { + targetJar.putNextEntry(new JarEntry(className.replace('.', '/') + ".class")); + byte[] processedBytes = ClassRenameUtils.relocateClass(classBytes, dependencyPackage, relocatePrefix + dependencyPackage); + targetJar.write(processedBytes); + targetJar.closeEntry(); + } + + @SneakyThrows + public static void addDependency(JarOutputStream targetJar, Class baseClass, String baseName, String relocatePrefix) { + URL sourceUrl = baseClass.getProtectionDomain().getCodeSource().getLocation(); + String sourceUrlString = sourceUrl.toString(); + if (sourceUrlString.contains("!BOOT-INF")) { + String path = sourceUrlString.substring("jar:nested:".length()); + path = path.substring(0, path.indexOf("!/")); + String[] split = path.split("/!"); + String bootJarPath = split[0]; + String internalJarPath = split[1]; + if (tempBootPath == null) { + tempBootPath = Files.createTempDirectory("mem-shell-boot"); + unzip(bootJarPath, tempBootPath.toFile().getAbsolutePath()); + } + sourceUrl = tempBootPath.resolve(internalJarPath).toUri().toURL(); + } + try (JarFile sourceJar = new JarFile(new File(sourceUrl.toURI()))) { + Enumeration entries = sourceJar.entries(); + while (entries.hasMoreElements()) { + JarEntry entry = entries.nextElement(); + String entryName = entry.getName(); + if (entryName.startsWith("META-INF") + || entryName.contains("module-info.class")) { + continue; + } + if (!entry.isDirectory()) { + try (InputStream entryStream = sourceJar.getInputStream(entry)) { + byte[] bytes = IOUtils.toByteArray(entryStream); + if (StringUtils.isNoneEmpty(relocatePrefix)) { + targetJar.putNextEntry(new JarEntry(relocatePrefix + entryName)); + if (entryName.endsWith(".class")) { + if (bytes.length > 0) { + bytes = ClassRenameUtils.relocateClass(bytes, baseName, relocatePrefix + baseName); + } + } else { + targetJar.putNextEntry(entry); + } + } else { + targetJar.putNextEntry(entry); + } + targetJar.write(bytes); + } + } + targetJar.closeEntry(); + } + } + } + + /** + * Extracts a JAR file to a temporary directory + * + * @param jarPath Path to the source JAR file + * @param tempPath Path to the temporary directory + */ + @SneakyThrows + public static void unzip(String jarPath, String tempPath) { + try (JarFile jarFile = new JarFile(jarPath)) { + Enumeration entries = jarFile.entries(); + while (entries.hasMoreElements()) { + JarEntry jarEntry = entries.nextElement(); + File targetFile = new File(tempPath, jarEntry.getName()); + + if (jarEntry.isDirectory()) { + targetFile.mkdirs(); + continue; + } + + targetFile.getParentFile().mkdirs(); + try (InputStream inputStream = jarFile.getInputStream(jarEntry); + FileOutputStream outputStream = new FileOutputStream(targetFile)) { + IOUtils.copy(inputStream, outputStream); + } + } + } + } +} \ No newline at end of file diff --git a/generator/src/main/java/com/reajason/javaweb/memshell/packer/jar/attach/Attacher.java b/generator/src/main/java/com/reajason/javaweb/memshell/packer/jar/attach/Attacher.java new file mode 100644 index 00000000..43b4447f --- /dev/null +++ b/generator/src/main/java/com/reajason/javaweb/memshell/packer/jar/attach/Attacher.java @@ -0,0 +1,942 @@ +package com.reajason.javaweb.memshell.packer.jar.attach;/* + * Copyright 2014 - Present Rafael Winterhalter + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import java.io.File; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.MalformedURLException; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLClassLoader; +import java.security.CodeSource; +import java.security.PrivilegedAction; +import java.security.ProtectionDomain; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * Copy from Byte Buddy + */ +public class Attacher { + + /** + * Representation of the bootstrap {@link ClassLoader}. + */ + private static final ClassLoader BOOTSTRAP_CLASS_LOADER = null; + + /** + * The character that is used to mark the beginning of the argument to the agent. + */ + private static final String AGENT_ARGUMENT_SEPARATOR = "="; + + /** + * The agent provides only {@code static} utility methods and should not be instantiated. + */ + private Attacher() { + throw new UnsupportedOperationException("This class is a utility class and not supposed to be instantiated"); + } + + public static void main(String[] args) throws Exception { + try { + Attacher.attach(args[0]); + } catch (Exception e) { + if (!e.getMessage().equals("0")) { + throw e; + } + } + System.out.println("ok"); + } + + /** + *

+ * Attaches the given agent Jar on the target process which must be a virtual machine process. The default attachment provider + * is used for applying the attachment. This operation blocks until the attachment is complete. If the current VM does not supply + * any known form of attachment to a remote VM, an {@link IllegalStateException} is thrown. The agent is not provided an argument. + *

+ *

+ * Important: It is only possible to attach to processes that are executed by the same operating system user. + *

+ * + * @param agentJar The agent jar file. + * @param processId The target process id. + */ + public static void attach(File agentJar, String processId) { + attach(agentJar, processId, null); + } + + public static void attach(String processId) { + attach(trySelfResolve(), processId, null); + } + + /** + *

+ * Attaches the given agent Jar on the target process which must be a virtual machine process. The default attachment provider + * is used for applying the attachment. This operation blocks until the attachment is complete. If the current VM does not supply + * any known form of attachment to a remote VM, an {@link IllegalStateException} is thrown. + *

+ *

+ * Important: It is only possible to attach to processes that are executed by the same operating system user. + *

+ * + * @param agentJar The agent jar file. + * @param processId The target process id. + * @param argument The argument to provide to the agent. + */ + public static void attach(File agentJar, String processId, String argument) { + install(processId, argument, new AgentProvider.ForExistingAgent(agentJar)); + } + + /** + * Installs a Java agent on a target VM. + * + * @param processId The process id of the target JVM process. + * @param argument The argument to provide to the agent. + * @param agentProvider The agent provider for the agent jar or library. + */ + private static void install(String processId, String argument, AgentProvider agentProvider) { + AttachmentProvider.Accessor attachmentAccessor = AttachmentProvider.DEFAULT.attempt(); + if (!attachmentAccessor.isAvailable()) { + throw new IllegalStateException("No compatible attachment provider is available"); + } + try { + Class virtualMachineType = attachmentAccessor.getVirtualMachineType(); + String agent = agentProvider.resolve().getAbsolutePath(); + Object virtualMachineInstance = virtualMachineType + .getMethod("attach", String.class) + .invoke(null, processId); + try { + virtualMachineType + .getMethod("loadAgent", String.class, String.class) + .invoke(virtualMachineInstance, agent, argument); + } finally { + virtualMachineType + .getMethod("detach") + .invoke(virtualMachineInstance); + } + } catch (RuntimeException exception) { + throw exception; + } catch (Exception exception) { + throw new IllegalStateException("Error during attachment using: " + AttachmentProvider.DEFAULT, exception); + } + } + + /** + * Attempts to resolve the location of the {@link Attacher} class for a self-attachment. Doing so avoids the creation of a temporary jar file. + * + * @return The self-resolved jar file or {@code null} if the jar file cannot be located. + */ + private static File trySelfResolve() { + try { + ProtectionDomain protectionDomain = Attacher.class.getProtectionDomain(); + if (protectionDomain == null) { + return null; + } + CodeSource codeSource = protectionDomain.getCodeSource(); + if (codeSource == null) { + return null; + } + URL location = codeSource.getLocation(); + if (!location.getProtocol().equals("file")) { + return null; + } + try { + File file = new File(location.toURI()); + if (file.getPath().contains(AGENT_ARGUMENT_SEPARATOR)) { + return null; + } + return file; + } catch (URISyntaxException ignored) { + return new File(location.getPath()); + } + } catch (Exception ignored) { + return null; + } + } + + /** + * An attachment provider is responsible for making the Java attachment API available. + */ + public interface AttachmentProvider { + + /** + * The default attachment provider to be used. + */ + AttachmentProvider DEFAULT = new Compound(ForModularizedVm.INSTANCE, + ForJ9Vm.INSTANCE, + ForStandardToolsJarVm.JVM_ROOT, + ForStandardToolsJarVm.JDK_ROOT, + ForStandardToolsJarVm.MACINTOSH, + ForUserDefinedToolsJar.INSTANCE, + ForEmulatedAttachment.INSTANCE); + + /** + * Attempts the creation of an accessor for a specific JVM's attachment API. + * + * @return The accessor this attachment provider can supply for the currently running JVM. + */ + Accessor attempt(); + + /** + * An accessor for a JVM's attachment API. + */ + interface Accessor { + + /** + * The name of the {@code VirtualMachine} class on any OpenJDK or Oracle JDK implementation. + */ + String VIRTUAL_MACHINE_TYPE_NAME = "com.sun.tools.attach.VirtualMachine"; + + /** + * The name of the {@code VirtualMachine} class on IBM J9 VMs. + */ + String VIRTUAL_MACHINE_TYPE_NAME_J9 = "com.ibm.tools.attach.VirtualMachine"; + + /** + * Determines if this accessor is applicable for the currently running JVM. + * + * @return {@code true} if this accessor is available. + */ + boolean isAvailable(); + + /** + * Returns {@code true} if this accessor prohibits attachment to the same virtual machine in Java 9 and later. + * + * @return {@code true} if this accessor prohibits attachment to the same virtual machine in Java 9 and later. + */ + boolean isExternalAttachmentRequired(); + + /** + * Returns a {@code VirtualMachine} class. This method must only be called for available accessors. + * + * @return The virtual machine type. + */ + Class getVirtualMachineType(); + + /** + * Returns a description of a virtual machine class for an external attachment. + * + * @return A description of the external attachment. + */ + ExternalAttachment getExternalAttachment(); + + /** + * A canonical implementation of an unavailable accessor. + */ + enum Unavailable implements Accessor { + + /** + * The singleton instance. + */ + INSTANCE; + + /** + * {@inheritDoc} + */ + public boolean isAvailable() { + return false; + } + + /** + * {@inheritDoc} + */ + public boolean isExternalAttachmentRequired() { + throw new IllegalStateException("Cannot read the virtual machine type for an unavailable accessor"); + } + + /** + * {@inheritDoc} + */ + public Class getVirtualMachineType() { + throw new IllegalStateException("Cannot read the virtual machine type for an unavailable accessor"); + } + + /** + * {@inheritDoc} + */ + public ExternalAttachment getExternalAttachment() { + throw new IllegalStateException("Cannot read the virtual machine type for an unavailable accessor"); + } + } + + /** + * Describes an external attachment to a Java virtual machine. + */ + class ExternalAttachment { + + /** + * The fully-qualified binary name of the virtual machine type. + */ + private final String virtualMachineType; + + /** + * The class path elements required for loading the supplied virtual machine type. + */ + private final List classPath; + + /** + * Creates an external attachment. + * + * @param virtualMachineType The fully-qualified binary name of the virtual machine type. + * @param classPath The class path elements required for loading the supplied virtual machine type. + */ + public ExternalAttachment(String virtualMachineType, List classPath) { + this.virtualMachineType = virtualMachineType; + this.classPath = classPath; + } + + /** + * Returns the fully-qualified binary name of the virtual machine type. + * + * @return The fully-qualified binary name of the virtual machine type. + */ + public String getVirtualMachineType() { + return virtualMachineType; + } + + /** + * Returns the class path elements required for loading the supplied virtual machine type. + * + * @return The class path elements required for loading the supplied virtual machine type. + */ + public List getClassPath() { + return classPath; + } + } + + /** + * A simple implementation of an accessible accessor. + */ + abstract class Simple implements Accessor { + + /** + * A {@code VirtualMachine} class. + */ + protected final Class virtualMachineType; + + /** + * Creates a new simple accessor. + * + * @param virtualMachineType A {@code VirtualMachine} class. + */ + protected Simple(Class virtualMachineType) { + this.virtualMachineType = virtualMachineType; + } + + /** + *

+ * Creates an accessor by reading the process id from the JMX runtime bean and by attempting + * to load the {@code com.sun.tools.attach.VirtualMachine} class from the provided class loader. + *

+ *

+ * This accessor is supposed to work on any implementation of the OpenJDK or Oracle JDK. + *

+ * + * @param classLoader A class loader that is capable of loading the virtual machine type. + * @param classPath The class path required to load the virtual machine class. + * @return An appropriate accessor. + */ + public static Accessor of(ClassLoader classLoader, File... classPath) { + try { + return new WithExternalAttachment(Class.forName(VIRTUAL_MACHINE_TYPE_NAME, + false, + classLoader), Arrays.asList(classPath)); + } catch (ClassNotFoundException ignored) { + return Unavailable.INSTANCE; + } + } + + /** + *

+ * Creates an accessor by reading the process id from the JMX runtime bean and by attempting + * to load the {@code com.ibm.tools.attach.VirtualMachine} class from the provided class loader. + *

+ *

+ * This accessor is supposed to work on any implementation of IBM's J9. + *

+ * + * @return An appropriate accessor. + */ + public static Accessor ofJ9() { + try { + return new WithExternalAttachment(ClassLoader.getSystemClassLoader().loadClass(VIRTUAL_MACHINE_TYPE_NAME_J9), + Collections.emptyList()); + } catch (ClassNotFoundException ignored) { + return Unavailable.INSTANCE; + } + } + + /** + * {@inheritDoc} + */ + public boolean isAvailable() { + return true; + } + + /** + * {@inheritDoc} + */ + public Class getVirtualMachineType() { + return virtualMachineType; + } + + /** + * A simple implementation of an accessible accessor that allows for external attachment. + */ + protected static class WithExternalAttachment extends Simple { + + /** + * The class path required for loading the virtual machine type. + */ + private final List classPath; + + /** + * Creates a new simple accessor that allows for external attachment. + * + * @param virtualMachineType The {@code com.sun.tools.attach.VirtualMachine} class. + * @param classPath The class path required for loading the virtual machine type. + */ + public WithExternalAttachment(Class virtualMachineType, List classPath) { + super(virtualMachineType); + this.classPath = classPath; + } + + /** + * {@inheritDoc} + */ + public boolean isExternalAttachmentRequired() { + return true; + } + + /** + * {@inheritDoc} + */ + public ExternalAttachment getExternalAttachment() { + return new ExternalAttachment(virtualMachineType.getName(), classPath); + } + } + + /** + * A simple implementation of an accessible accessor that attaches using a virtual machine emulation that does not require external attachment. + */ + protected static class WithDirectAttachment extends Simple { + + /** + * Creates a new simple accessor that implements direct attachment. + * + * @param virtualMachineType A {@code VirtualMachine} class. + */ + public WithDirectAttachment(Class virtualMachineType) { + super(virtualMachineType); + } + + /** + * {@inheritDoc} + */ + public boolean isExternalAttachmentRequired() { + return false; + } + + /** + * {@inheritDoc} + */ + public ExternalAttachment getExternalAttachment() { + throw new IllegalStateException("Cannot apply external attachment"); + } + } + } + } + + /** + * An attachment provider that locates the attach API directly from the system class loader, as possible since + * introducing the Java module system via the {@code jdk.attach} module. + */ + enum ForModularizedVm implements AttachmentProvider { + + /** + * The singleton instance. + */ + INSTANCE; + + /** + * {@inheritDoc} + */ + public Accessor attempt() { + return Accessor.Simple.of(ClassLoader.getSystemClassLoader()); + } + } + + /** + * An attachment provider that locates the attach API directly from the system class loader expecting + * an IBM J9 VM. + */ + enum ForJ9Vm implements AttachmentProvider { + + /** + * The singleton instance. + */ + INSTANCE; + + /** + * {@inheritDoc} + */ + public Accessor attempt() { + return Accessor.Simple.ofJ9(); + } + } + + /** + * An attachment provider that is dependant on the existence of a tools.jar file on the local + * file system. + */ + enum ForStandardToolsJarVm implements AttachmentProvider { + + /** + * An attachment provider that locates the tools.jar from a Java home directory. + */ + JVM_ROOT("../lib/tools.jar"), + + /** + * An attachment provider that locates the tools.jar from a Java installation directory. + * In practice, several virtual machines do not return the JRE's location for the + * java.home property against the property's specification. + */ + JDK_ROOT("lib/tools.jar"), + + /** + * An attachment provider that locates the tools.jar as it is set for several JVM + * installations on Apple Macintosh computers. + */ + MACINTOSH("../Classes/classes.jar"); + + /** + * The Java home system property. + */ + private static final String JAVA_HOME_PROPERTY = "java.home"; + + /** + * The path to the tools.jar file, starting from the Java home directory. + */ + private final String toolsJarPath; + + /** + * Creates a new attachment provider that loads the virtual machine class from the tools.jar. + * + * @param toolsJarPath The path to the tools.jar file, starting from the Java home directory. + */ + ForStandardToolsJarVm(String toolsJarPath) { + this.toolsJarPath = toolsJarPath; + } + + /** + * {@inheritDoc} + */ + public Accessor attempt() { + File toolsJar = new File(System.getProperty(JAVA_HOME_PROPERTY), toolsJarPath); + try { + return toolsJar.isFile() && toolsJar.canRead() + ? Accessor.Simple.of(new URLClassLoader(new URL[]{toolsJar.toURI().toURL()}, BOOTSTRAP_CLASS_LOADER), toolsJar) + : Accessor.Unavailable.INSTANCE; + } catch (MalformedURLException exception) { + throw new IllegalStateException("Could not represent " + toolsJar + " as URL"); + } + } + } + + /** + * An attachment provider that attempts to locate a {@code tools.jar} from a custom location set via a system property. + */ + enum ForUserDefinedToolsJar implements AttachmentProvider { + + /** + * The singelton instance. + */ + INSTANCE; + + /** + * The property being read for locating {@code tools.jar}. + */ + public static final String PROPERTY = "net.bytebuddy.agent.toolsjar"; + + /** + * {@inheritDoc} + */ + public Accessor attempt() { + String location = System.getProperty(PROPERTY); + if (location == null) { + return Accessor.Unavailable.INSTANCE; + } else { + File toolsJar = new File(location); + try { + return Accessor.Simple.of(new URLClassLoader(new URL[]{toolsJar.toURI().toURL()}, BOOTSTRAP_CLASS_LOADER), toolsJar); + } catch (MalformedURLException exception) { + throw new IllegalStateException("Could not represent " + toolsJar + " as URL"); + } + } + } + } + + /** + * An attachment provider that uses Byte Buddy's attachment API emulation. To use this feature, JNA is required. + */ + enum ForEmulatedAttachment implements AttachmentProvider { + + /** + * The singleton instance. + */ + INSTANCE; + + /** + * {@inheritDoc} + */ + public Accessor attempt() { + try { + return new Accessor.Simple.WithDirectAttachment(VirtualMachine.Resolver.INSTANCE.get()); + } catch (Throwable ignored) { + return Accessor.Unavailable.INSTANCE; + } + } + } + + /** + * A compound attachment provider that attempts the attachment by delegation to other providers. If + * none of the providers of this compound provider is capable of providing a valid accessor, an + * non-available accessor is returned. + */ + class Compound implements AttachmentProvider { + + /** + * A list of attachment providers in the order of their application. + */ + private final List attachmentProviders; + + /** + * Creates a new compound attachment provider. + * + * @param attachmentProvider A list of attachment providers in the order of their application. + */ + public Compound(AttachmentProvider... attachmentProvider) { + this(Arrays.asList(attachmentProvider)); + } + + /** + * Creates a new compound attachment provider. + * + * @param attachmentProviders A list of attachment providers in the order of their application. + */ + public Compound(List attachmentProviders) { + this.attachmentProviders = new ArrayList(); + for (AttachmentProvider attachmentProvider : attachmentProviders) { + if (attachmentProvider instanceof Compound) { + this.attachmentProviders.addAll(((Compound) attachmentProvider).attachmentProviders); + } else { + this.attachmentProviders.add(attachmentProvider); + } + } + } + + /** + * {@inheritDoc} + */ + public Accessor attempt() { + for (AttachmentProvider attachmentProvider : attachmentProviders) { + Accessor accessor = attachmentProvider.attempt(); + if (accessor.isAvailable()) { + return accessor; + } + } + return Accessor.Unavailable.INSTANCE; + } + } + } + + /** + * A process provider is responsible for providing the process id of the current VM. + */ + public interface ProcessProvider { + + /** + * Resolves a process id for the current JVM. + * + * @return The resolved process id. + */ + String resolve(); + + /** + * Supplies the current VM's process id. + */ + enum ForCurrentVm implements ProcessProvider { + + /** + * The singleton instance. + */ + INSTANCE; + + /** + * The best process provider for the current VM. + */ + private final ProcessProvider dispatcher; + + /** + * Creates a process provider that supplies the current VM's process id. + */ + ForCurrentVm() { + dispatcher = ForJava9CapableVm.make(); + } + + /** + * {@inheritDoc} + */ + public String resolve() { + return dispatcher.resolve(); + } + + /** + * A process provider for a legacy VM that reads the process id from its JMX properties. This strategy + * is only used prior to Java 9 such that the java.management module never is resolved, even if + * the module system is used, as the module system was not available in any relevant JVM version. + */ + protected enum ForLegacyVm implements ProcessProvider { + + /** + * The singleton instance. + */ + INSTANCE; + + /** + * {@inheritDoc} + */ + public String resolve() { + String runtimeName; + try { + Method method = Class.forName("java.lang.management.ManagementFactory").getMethod("getRuntimeMXBean"); + runtimeName = (String) method.getReturnType().getMethod("getName").invoke(method.invoke(null)); + } catch (Exception exception) { + throw new IllegalStateException("Failed to access VM name via management factory", exception); + } + int processIdIndex = runtimeName.indexOf('@'); + if (processIdIndex == -1) { + throw new IllegalStateException("Cannot extract process id from runtime management bean"); + } else { + return runtimeName.substring(0, processIdIndex); + } + } + } + + /** + * A process provider for a Java 9 capable VM with access to the introduced process API. + */ + protected static class ForJava9CapableVm implements ProcessProvider { + + /** + * The {@code java.lang.ProcessHandle#current()} method. + */ + private final Method current; + + /** + * The {@code java.lang.ProcessHandle#pid()} method. + */ + private final Method pid; + + /** + * Creates a new Java 9 capable dispatcher for reading the current process's id. + * + * @param current The {@code java.lang.ProcessHandle#current()} method. + * @param pid The {@code java.lang.ProcessHandle#pid()} method. + */ + protected ForJava9CapableVm(Method current, Method pid) { + this.current = current; + this.pid = pid; + } + + /** + * Attempts to create a dispatcher for a Java 9 VM and falls back to a legacy dispatcher + * if this is not possible. + * + * @return A dispatcher for the current VM. + */ + public static ProcessProvider make() { + try { + return new ForJava9CapableVm(Class.forName("java.lang.ProcessHandle").getMethod("current"), + Class.forName("java.lang.ProcessHandle").getMethod("pid")); + } catch (Exception ignored) { + return ForLegacyVm.INSTANCE; + } + } + + /** + * {@inheritDoc} + */ + public String resolve() { + try { + return pid.invoke(current.invoke(null)).toString(); + } catch (IllegalAccessException exception) { + throw new IllegalStateException("Cannot access Java 9 process API", exception); + } catch (InvocationTargetException exception) { + throw new IllegalStateException("Error when accessing Java 9 process API", exception.getTargetException()); + } + } + } + } + } + + /** + * An agent provider is responsible for handling and providing the jar file of an agent that is being attached. + */ + protected interface AgentProvider { + + /** + * Provides an agent jar file for attachment. + * + * @return The provided agent. + * @throws IOException If the agent cannot be written to disk. + */ + File resolve() throws IOException; + + /** + * An agent provider that supplies an existing agent that is not deleted after attachment. + */ + class ForExistingAgent implements AgentProvider { + + /** + * The supplied agent. + */ + private final File agent; + + /** + * Creates an agent provider for an existing agent. + * + * @param agent The supplied agent. + */ + protected ForExistingAgent(File agent) { + this.agent = agent; + } + + /** + * {@inheritDoc} + */ + public File resolve() { + return agent; + } + } + } + + /** + * An attachment evaluator is responsible for deciding if an agent can be attached from the current process. + */ + protected interface AttachmentTypeEvaluator { + + /** + * Checks if the current VM requires external attachment for the supplied process id. + * + * @param processId The process id of the process to which to attach. + * @return {@code true} if the current VM requires external attachment for the supplied process. + */ + boolean requiresExternalAttachment(String processId); + + /** + * An installation action for creating an attachment type evaluator. + */ + enum InstallationAction implements PrivilegedAction { + + /** + * The singleton instance. + */ + INSTANCE; + + /** + * The OpenJDK's property for specifying the legality of self-attachment. + */ + private static final String JDK_ALLOW_SELF_ATTACH = "jdk.attach.allowAttachSelf"; + + /** + * {@inheritDoc} + */ + public AttachmentTypeEvaluator run() { + try { + if (Boolean.getBoolean(JDK_ALLOW_SELF_ATTACH)) { + return Disabled.INSTANCE; + } else { + return new ForJava9CapableVm(Class.forName("java.lang.ProcessHandle").getMethod("current"), + Class.forName("java.lang.ProcessHandle").getMethod("pid")); + } + } catch (Exception ignored) { + return Disabled.INSTANCE; + } + } + } + + /** + * An attachment type evaluator that never requires external attachment. + */ + enum Disabled implements AttachmentTypeEvaluator { + + /** + * The singleton instance. + */ + INSTANCE; + + /** + * {@inheritDoc} + */ + public boolean requiresExternalAttachment(String processId) { + return false; + } + } + + /** + * An attachment type evaluator that checks a process id against the current process id. + */ + class ForJava9CapableVm implements AttachmentTypeEvaluator { + + /** + * The {@code java.lang.ProcessHandle#current()} method. + */ + private final Method current; + + /** + * The {@code java.lang.ProcessHandle#pid()} method. + */ + private final Method pid; + + /** + * Creates a new attachment type evaluator. + * + * @param current The {@code java.lang.ProcessHandle#current()} method. + * @param pid The {@code java.lang.ProcessHandle#pid()} method. + */ + protected ForJava9CapableVm(Method current, Method pid) { + this.current = current; + this.pid = pid; + } + + /** + * {@inheritDoc} + */ + public boolean requiresExternalAttachment(String processId) { + try { + return pid.invoke(current.invoke(null)).toString().equals(processId); + } catch (IllegalAccessException exception) { + throw new IllegalStateException("Cannot access Java 9 process API", exception); + } catch (InvocationTargetException exception) { + throw new IllegalStateException("Error when accessing Java 9 process API", exception.getTargetException()); + } + } + } + } +} diff --git a/generator/src/main/java/com/reajason/javaweb/memshell/packer/jar/attach/VirtualMachine.java b/generator/src/main/java/com/reajason/javaweb/memshell/packer/jar/attach/VirtualMachine.java new file mode 100644 index 00000000..c7a80da3 --- /dev/null +++ b/generator/src/main/java/com/reajason/javaweb/memshell/packer/jar/attach/VirtualMachine.java @@ -0,0 +1,2888 @@ +package com.reajason.javaweb.memshell.packer.jar.attach;/* + * Copyright 2014 - Present Rafael Winterhalter + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import com.sun.jna.*; +import com.sun.jna.platform.win32.*; +import com.sun.jna.ptr.IntByReference; +import com.sun.jna.win32.StdCallLibrary; +import com.sun.jna.win32.W32APIOptions; + +import java.io.*; +import java.net.ServerSocket; +import java.net.Socket; +import java.nio.ByteBuffer; +import java.nio.channels.FileLock; +import java.security.PrivilegedAction; +import java.security.SecureRandom; +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + *

+ * An implementation for attachment on a virtual machine. This interface mimics the tooling API's virtual + * machine interface to allow for similar usage by where all calls are made via + * reflection such that this structural typing suffices for interoperability. + *

+ *

+ * Note: Implementations are required to declare a static method {@code attach(String)} returning an + * instance of a class that declares the methods defined by {@link VirtualMachine}. + *

+ */ +public interface VirtualMachine { + + /** + * Loads the target VMs system properties. + * + * @return The target VM properties. + * @throws IOException If an I/O exception occurs. + */ + Properties getSystemProperties() throws IOException; + + /** + * Loads the target VMs agent properties. + * + * @return The target VM properties. + * @throws IOException If an I/O exception occurs. + */ + Properties getAgentProperties() throws IOException; + + /** + * Loads an agent into the represented virtual machine. + * + * @param jarFile The jar file to attach. + * @throws IOException If an I/O exception occurs. + */ + void loadAgent(String jarFile) throws IOException; + + /** + * Loads an agent into the represented virtual machine. + * + * @param jarFile The jar file to attach. + * @param argument The argument to provide or {@code null} if no argument should be provided. + * @throws IOException If an I/O exception occurs. + */ + void loadAgent(String jarFile, String argument) throws IOException; + + /** + * Loads a native agent into the represented virtual machine. + * + * @param path The agent path. + * @throws IOException If an I/O exception occurs. + */ + void loadAgentPath(String path) throws IOException; + + /** + * Loads a native agent into the represented virtual machine. + * + * @param path The agent path. + * @param argument The argument to provide or {@code null} if no argument should be provided. + * @throws IOException If an I/O exception occurs. + */ + void loadAgentPath(String path, String argument) throws IOException; + + /** + * Loads a native agent library into the represented virtual machine. + * + * @param library The agent library. + * @throws IOException If an I/O exception occurs. + */ + void loadAgentLibrary(String library) throws IOException; + + /** + * Loads a native agent library into the represented virtual machine. + * + * @param library The agent library. + * @param argument The argument to provide or {@code null} if no argument should be provided. + * @throws IOException If an I/O exception occurs. + */ + void loadAgentLibrary(String library, String argument) throws IOException; + + /** + * Starts a JMX management agent. + * + * @param properties The properties to transfer to the JMX agent. + * @throws IOException If an I/O error occurs. + */ + void startManagementAgent(Properties properties) throws IOException; + + /** + * Starts a local management agent. + * + * @return The local connector address. + * @throws IOException If an I/O error occurs. + */ + String startLocalManagementAgent() throws IOException; + + /** + * Detaches this virtual machine representation. + * + * @throws IOException If an I/O exception occurs. + */ + void detach() throws IOException; + + /** + * A resolver for the current VM's virtual machine attachment emulation. + */ + enum Resolver { + + /** + * The singleton instance. + */ + INSTANCE; + + /** + * {@inheritDoc} + */ + public Class get() { + try { + Class.forName("com.sun.jna.Platform"); + } catch (ClassNotFoundException exception) { + throw new IllegalStateException("Optional JNA dependency is not available", exception); + } + return System.getProperty("java.vm.name", "").toUpperCase(Locale.US).contains("J9") + ? ForOpenJ9.class + : ForHotSpot.class; + + } + } + + /** + * An abstract base implementation for a virtual machine. + */ + abstract class AbstractBase implements VirtualMachine { + + /** + * {@inheritDoc} + */ + public void loadAgent(String jarFile) throws IOException { + loadAgent(jarFile, null); + } + + /** + * {@inheritDoc} + */ + public void loadAgentPath(String path) throws IOException { + loadAgentPath(path, null); + } + + /** + * {@inheritDoc} + */ + public void loadAgentLibrary(String library) throws IOException { + loadAgentLibrary(library, null); + } + } + + /** + * A virtual machine attachment implementation for a HotSpot VM or any compatible JVM. + */ + class ForHotSpot extends AbstractBase { + + /** + * The protocol version to use for communication. + */ + private static final String PROTOCOL_VERSION = "1"; + + /** + * The {@code load} command. + */ + private static final String LOAD_COMMAND = "load"; + + /** + * The {@code instrument} command. + */ + private static final String INSTRUMENT_COMMAND = "instrument"; + + /** + * A delimiter to be used for attachment. + */ + private static final String ARGUMENT_DELIMITER = "="; + + /** + * The virtual machine connection. + */ + private final Connection connection; + + /** + * Creates a new virtual machine connection for HotSpot. + * + * @param connection The virtual machine connection. + */ + protected ForHotSpot(Connection connection) { + this.connection = connection; + } + + /** + * Attaches to the supplied process id using the default JNA implementation. + * + * @param processId The process id. + * @return A suitable virtual machine implementation. + * @throws IOException If an IO exception occurs during establishing the connection. + */ + public static VirtualMachine attach(String processId) throws IOException { + if (Platform.isWindows()) { + return attach(processId, new Connection.ForJnaWindowsNamedPipe.Factory()); + } else if (Platform.isSolaris()) { + return attach(processId, new Connection.ForJnaSolarisDoor.Factory(15, 100, TimeUnit.MILLISECONDS)); + } else { + return attach(processId, Connection.ForJnaPosixSocket.Factory.withDefaultTemporaryFolder(15, 100, TimeUnit.MILLISECONDS)); + } + } + + /** + * Attaches to the supplied process id using the supplied connection factory. + * + * @param processId The process id. + * @param connectionFactory The connection factory to use. + * @return A suitable virtual machine implementation. + * @throws IOException If an IO exception occurs during establishing the connection. + */ + public static VirtualMachine attach(String processId, Connection.Factory connectionFactory) throws IOException { + return new ForHotSpot(connectionFactory.connect(processId)); + } + + /** + * Checks the header of a response. + * + * @param response The response to check the header for. + * @throws IOException If an I/O exception occurs. + */ + private static void checkHeader(Connection.Response response) throws IOException { + byte[] buffer = new byte[1]; + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + int length; + while ((length = response.read(buffer)) != -1) { + if (length > 0) { + if (buffer[0] == '\n') { + break; + } + outputStream.write(buffer[0]); + } + } + switch (Integer.parseInt(outputStream.toString("UTF-8"))) { + case 0: + return; + case 101: + throw new IOException("Protocol mismatch with target VM"); + default: + buffer = new byte[1024]; + outputStream = new ByteArrayOutputStream(); + while ((length = response.read(buffer)) != -1) { + outputStream.write(buffer, 0, length); + } + throw new IllegalStateException(outputStream.toString("UTF-8")); + } + } + + /** + * {@inheritDoc} + */ + public Properties getSystemProperties() throws IOException { + return getProperties("properties"); + } + + /** + * {@inheritDoc} + */ + public Properties getAgentProperties() throws IOException { + return getProperties("agentProperties"); + } + + /** + * Loads properties of the target VM. + * + * @param command The command for fetching properties. + * @return The read properties. + * @throws IOException If an I/O exception occurs. + */ + private Properties getProperties(String command) throws IOException { + Connection.Response response = connection.execute(PROTOCOL_VERSION, command, null, null, null); + try { + checkHeader(response); + byte[] buffer = new byte[1024]; + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + int length; + while ((length = response.read(buffer)) != -1) { + outputStream.write(buffer, 0, length); + } + Properties properties = new Properties(); + properties.load(new ByteArrayInputStream(outputStream.toByteArray())); + return properties; + } finally { + response.close(); + } + } + + /** + * {@inheritDoc} + */ + public void loadAgent(String jarFile, String argument) throws IOException { + load(jarFile, false, argument); + } + + /** + * {@inheritDoc} + */ + public void loadAgentPath(String path, String argument) throws IOException { + load(path, true, argument); + } + + /** + * {@inheritDoc} + */ + public void loadAgentLibrary(String library, String argument) throws IOException { + load(library, false, argument); + } + + /** + * Loads an agent by the given command. + * + * @param file The Java agent or library to be loaded. + * @param absolute {@code true} if the agent location is absolute. + * @param argument The argument to the agent or {@code null} if no argument is given. + * @throws IOException If an I/O exception occurs. + */ + protected void load(String file, boolean absolute, String argument) throws IOException { + Connection.Response response = connection.execute(PROTOCOL_VERSION, LOAD_COMMAND, INSTRUMENT_COMMAND, Boolean.toString(absolute), (argument == null + ? file + : file + ARGUMENT_DELIMITER + argument)); + try { + checkHeader(response); + } finally { + response.close(); + } + } + + /** + * {@inheritDoc} + */ + public void startManagementAgent(Properties properties) throws IOException { + StringBuilder stringBuilder = new StringBuilder("ManagementAgent.start "); + boolean first = true; + for (Map.Entry entry : properties.entrySet()) { + if (!(entry.getKey() instanceof String) || !((String) entry.getKey()).startsWith("com.sun.management.")) { + throw new IllegalArgumentException("Illegal property name: " + entry.getKey()); + } else if (first) { + first = false; + } else { + stringBuilder.append(' '); + } + stringBuilder.append(((String) entry.getKey()).substring("com.sun.management.".length())).append('='); + String value = entry.getValue().toString(); + if (value.contains(" ")) { + stringBuilder.append('\'').append(value).append('\''); + } else { + stringBuilder.append(value); + } + } + Connection.Response response = connection.execute(PROTOCOL_VERSION, "jcmd", stringBuilder.toString(), null, null); + try { + checkHeader(response); + } finally { + response.close(); + } + } + + /** + * {@inheritDoc} + */ + public String startLocalManagementAgent() throws IOException { + Connection.Response response = connection.execute(PROTOCOL_VERSION, "jcmd", "ManagementAgent.start_local", null, null); + try { + checkHeader(response); + return getAgentProperties().getProperty("com.sun.management.jmxremote.localConnectorAddress"); + } finally { + response.close(); + } + } + + /** + * {@inheritDoc} + */ + public void detach() throws IOException { + connection.close(); + } + + /** + * Represents a connection to a virtual machine. + */ + public interface Connection extends Closeable { + + /** + * Executes a command on the current connection. + * + * @param protocol The target VMs protocol version for the attach API. + * @param argument The arguments to send to the target VM. + * @return The response of the target JVM. + * @throws IOException If an I/O error occurred. + */ + Response execute(String protocol, String... argument) throws IOException; + + /** + * A response to an execution command to a VM. + */ + interface Response extends Closeable { + + /** + * Reads a buffer from the target VM. + * + * @param buffer The buffer to read to. + * @return The bytes read or {@code -1} if no more bytes could be read. + * @throws IOException If an I/O exception occurred. + */ + int read(byte[] buffer) throws IOException; + } + + /** + * A factory for creating connections to virtual machines. + */ + interface Factory { + + /** + * Connects to the supplied process. + * + * @param processId The process id. + * @return The connection to the virtual machine with the supplied process id. + * @throws IOException If an I/O exception occurs during connecting to the targeted VM. + */ + Connection connect(String processId) throws IOException; + + /** + * A factory for attaching via a socket file. + */ + abstract class ForSocketFile implements Factory { + + /** + * The name prefix for a socket. + */ + private static final String SOCKET_FILE_PREFIX = ".java_pid"; + + /** + * The name prefix for an attachment file indicator. + */ + private static final String ATTACH_FILE_PREFIX = ".attach_pid"; + + /** + * The temporary directory to use. + */ + private final String temporaryDirectory; + + /** + * The maximum amount of attempts for checking the establishment of a socket connection. + */ + private final int attempts; + + /** + * The pause between two checks for an established socket connection. + */ + private final long pause; + + /** + * The time unit of the pause time. + */ + private final TimeUnit timeUnit; + + /** + * Creates a connection factory for creating a socket connection via a file. + * + * @param temporaryDirectory The temporary directory to use. + * @param attempts The maximum amount of attempts for checking the establishment of a socket connection. + * @param pause The pause between two checks for an established socket connection. + * @param timeUnit The time unit of the pause time. + */ + protected ForSocketFile(String temporaryDirectory, int attempts, long pause, TimeUnit timeUnit) { + this.temporaryDirectory = temporaryDirectory; + this.attempts = attempts; + this.pause = pause; + this.timeUnit = timeUnit; + } + + /** + * {@inheritDoc} + */ + public Connection connect(String processId) throws IOException { + File socket = new File(temporaryDirectory, SOCKET_FILE_PREFIX + processId); + if (!socket.exists()) { + String target = ATTACH_FILE_PREFIX + processId, path = "/proc/" + processId + "/cwd/" + target; + File attachFile = new File(path); + try { + if (!attachFile.createNewFile() && !attachFile.isFile()) { + throw new IllegalStateException("Could not create attach file: " + attachFile); + } + } catch (IOException ignored) { + attachFile = new File(temporaryDirectory, target); + if (!attachFile.createNewFile() && !attachFile.isFile()) { + throw new IllegalStateException("Could not create attach file: " + attachFile); + } + } + try { + kill(processId, 3); + int attempts = this.attempts; + while (!socket.exists() && attempts-- > 0) { + timeUnit.sleep(pause); + } + if (!socket.exists()) { + throw new IllegalStateException("Target VM did not respond: " + processId); + } + } catch (InterruptedException exception) { + Thread.currentThread().interrupt(); + throw new IllegalStateException(exception); + } finally { + if (!attachFile.delete()) { + attachFile.deleteOnExit(); + } + } + } + return doConnect(socket); + } + + /** + * Sends a kill signal to the target process. + * + * @param processId The process id. + * @param signal The signal to send. + */ + protected abstract void kill(String processId, int signal); + + /** + * Connects to the supplied POSIX socket. + * + * @param socket The socket to connect to. + * @return An active connection to the supplied socket. + * @throws IOException If an error occurs during connection. + */ + protected abstract Connection doConnect(File socket) throws IOException; + } + } + + /** + * A connection that is represented by a byte channel that is persistent during communication. + * + * @param The connection representation. + */ + abstract class OnPersistentByteChannel implements Connection { + + /** + * A blank line argument. + */ + private static final byte[] BLANK = new byte[]{0}; + + /** + * {@inheritDoc} + */ + public Connection.Response execute(String protocol, String... argument) throws IOException { + T connection = connect(); + try { + write(connection, protocol.getBytes("UTF-8")); + write(connection, BLANK); + for (String anArgument : argument) { + if (anArgument != null) { + write(connection, anArgument.getBytes("UTF-8")); + } + write(connection, BLANK); + } + return new Response(connection); + } catch (Throwable throwable) { + close(connection); + if (throwable instanceof RuntimeException) { + throw (RuntimeException) throwable; + } else if (throwable instanceof IOException) { + throw (IOException) throwable; + } else { + throw new IllegalStateException(throwable); + } + } + } + + /** + * A response of a persistent byte channel. + */ + private class Response implements Connection.Response { + + /** + * The connection representing this response. + */ + private final T connection; + + /** + * Creates a new response for a persistent byte channel. + * + * @param connection The connection representing this response. + */ + private Response(T connection) { + this.connection = connection; + } + + /** + * {@inheritDoc} + */ + public int read(byte[] buffer) throws IOException { + return OnPersistentByteChannel.this.read(connection, buffer); + } + + /** + * {@inheritDoc} + */ + public void close() throws IOException { + OnPersistentByteChannel.this.close(connection); + } + } + + /** + * Creates a new connection to the target VM. + * + * @return Returns a new connection to the target VM. + * @throws IOException If an I/O exception occurs. + */ + protected abstract T connect() throws IOException; + + /** + * Closes the connection to the target VM. + * + * @param connection The connection to close. + * @throws IOException If an I/O exception occurs. + */ + protected abstract void close(T connection) throws IOException; + + /** + * Writes to the target VM. + * + * @param connection The connection to write to. + * @param buffer The buffer to write to. + * @throws IOException If an I/O exception occurs during writing. + */ + protected abstract void write(T connection, byte[] buffer) throws IOException; + + /** + * Reads from the target VM. + * + * @param connection The connection to read from. + * @param buffer The buffer to store the result in. + * @return The number of byte that were read. + * @throws IOException If an I/O exception occurs. + */ + protected abstract int read(T connection, byte[] buffer) throws IOException; + } + + /** + * Implements a connection for a POSIX socket in JNA. + */ + class ForJnaPosixSocket extends OnPersistentByteChannel { + + /** + * The JNA library to use. + */ + private final PosixLibrary library; + + /** + * The POSIX socket. + */ + private final File socket; + + /** + * Creates a connection for a virtual POSIX socket implemented in JNA. + * + * @param library The JNA library to use. + * @param socket The POSIX socket. + */ + protected ForJnaPosixSocket(PosixLibrary library, File socket) { + this.library = library; + this.socket = socket; + } + + @Override + protected Integer connect() { + int handle = library.socket(1, 1, 0); + try { + PosixLibrary.SocketAddress address = new PosixLibrary.SocketAddress(); + try { + address.setPath(socket.getAbsolutePath()); + library.connect(handle, address, address.size()); + return handle; + } finally { + address = null; + } + } catch (RuntimeException exception) { + library.close(handle); + throw exception; + } + } + + @Override + protected int read(Integer handle, byte[] buffer) { + int read = library.read(handle, ByteBuffer.wrap(buffer), buffer.length); + return read == 0 ? -1 : read; + } + + @Override + protected void write(Integer handle, byte[] buffer) { + library.write(handle, ByteBuffer.wrap(buffer), buffer.length); + } + + @Override + protected void close(Integer handle) { + library.close(handle); + } + + /** + * {@inheritDoc} + */ + public void close() { + /* do nothing */ + } + + /** + * A JNA library binding for POSIX sockets. + */ + protected interface PosixLibrary extends Library { + + /** + * Sends a kill command. + * + * @param processId The process id to kill. + * @param signal The signal to send. + * @return The return code. + * @throws LastErrorException If an error occurs. + */ + int kill(int processId, int signal) throws LastErrorException; + + /** + * Creates a POSIX socket connection. + * + * @param domain The socket's domain. + * @param type The socket's type. + * @param protocol The protocol version. + * @return A handle to the socket that was created or {@code 0} if no socket could be created. + * @throws LastErrorException If an error occurs. + */ + int socket(int domain, int type, int protocol) throws LastErrorException; + + /** + * Connects a socket connection. + * + * @param handle The socket's handle. + * @param address The address of the POSIX socket. + * @param length The length of the socket value. + * @return The return code. + * @throws LastErrorException If an error occurs. + */ + int connect(int handle, SocketAddress address, int length) throws LastErrorException; + + /** + * Reads from a POSIX socket. + * + * @param handle The socket's handle. + * @param buffer The buffer to read from. + * @param count The bytes being read. + * @return The amount of bytes that could be read. + * @throws LastErrorException If an error occurs. + */ + int read(int handle, ByteBuffer buffer, int count) throws LastErrorException; + + /** + * Writes to a POSIX socket. + * + * @param handle The socket's handle. + * @param buffer The buffer to write to. + * @param count The bytes being written. + * @return The return code. + * @throws LastErrorException If an error occurs. + */ + int write(int handle, ByteBuffer buffer, int count) throws LastErrorException; + + /** + * Closes the socket connection. + * + * @param handle The handle of the connection. + * @return The return code. + * @throws LastErrorException If an error occurs. + */ + int close(int handle) throws LastErrorException; + + /** + * Represents an address for a POSIX socket. + */ + class SocketAddress extends Structure { + + /** + * The socket family. + */ + @SuppressWarnings("unused") + public short family = 1; + + /** + * The socket path. + */ + public byte[] path = new byte[100]; + + /** + * Sets the socket path. + * + * @param path The socket path. + */ + protected void setPath(String path) { + try { + System.arraycopy(path.getBytes("UTF-8"), 0, this.path, 0, path.length()); + System.arraycopy(new byte[]{0}, 0, this.path, path.length(), 1); + } catch (UnsupportedEncodingException exception) { + throw new IllegalStateException(exception); + } + } + + @Override + protected List getFieldOrder() { + return Arrays.asList("family", "path"); + } + } + } + + /** + * A factory for a POSIX socket connection to a JVM using JNA. + */ + public static class Factory extends Connection.Factory.ForSocketFile { + + /** + * The socket library API. + */ + private final PosixLibrary library; + + /** + * Creates a connection factory for a POSIX socket using JNA. + * + * @param temporaryDirectory The temporary directory to use. + * @param attempts The maximum amount of attempts for checking the establishment of a socket connection. + * @param pause The pause between two checks for an established socket connection. + * @param timeUnit The time unit of the pause time. + */ + @SuppressWarnings("deprecation") + public Factory(String temporaryDirectory, int attempts, long pause, TimeUnit timeUnit) { + super(temporaryDirectory, attempts, pause, timeUnit); + library = Native.loadLibrary("c", PosixLibrary.class); + } + + /** + * Creates a connection factory for a POSIX socket using JNA while locating the default temporary directory used on the + * current platform. + * + * @param attempts The maximum amount of attempts for checking the establishment of a socket connection. + * @param pause The pause between two checks for an established socket connection. + * @param timeUnit The time unit of the pause time. + * @return An appropriate connection factory. + */ + @SuppressWarnings("deprecation") + public static Connection.Factory withDefaultTemporaryFolder(int attempts, long pause, TimeUnit timeUnit) { + String temporaryDirectory; + if (Platform.isMac()) { + MacLibrary library = Native.loadLibrary("c", MacLibrary.class); + Memory memory = new Memory(4096); + try { + long length = library.confstr(MacLibrary.CS_DARWIN_USER_TEMP_DIR, memory, memory.size()); + if (length == 0 || length > 4096) { + temporaryDirectory = "/tmp"; + } else { + temporaryDirectory = memory.getString(0); + } + } finally { + memory = null; + } + } else { + temporaryDirectory = "/tmp"; + } + return new Factory(temporaryDirectory, attempts, pause, timeUnit); + } + + @Override + protected void kill(String processId, int signal) { + library.kill(Integer.parseInt(processId), signal); + } + + @Override + public Connection doConnect(File socket) { + return new Connection.ForJnaPosixSocket(library, socket); + } + + /** + * A library for reading a Mac user's temporary directory. + */ + public interface MacLibrary extends Library { + + /** + * The temporary directory. + */ + int CS_DARWIN_USER_TEMP_DIR = 65537; + + /** + * Reads a configuration dependant variable into a memory segment. + * + * @param name The name of the variable. + * @param buffer The buffer to read the variable into. + * @param length The length of the buffer. + * @return The amount of bytes written to the buffer. + */ + long confstr(int name, Pointer buffer, long length); + } + } + } + + /** + * Implements a connection for a Windows named pipe in JNA. + */ + class ForJnaWindowsNamedPipe implements Connection { + + /** + * Indicates a memory release. + */ + private static final int MEM_RELEASE = 0x8000; + + /** + * The library to use for communicating with Windows native functions. + */ + private final WindowsLibrary library; + + /** + * The library to use for communicating with Windows attachment extension that is included as a DLL. + */ + private final WindowsAttachLibrary attachLibrary; + + /** + * The handle of the target VM's process. + */ + private final WinNT.HANDLE process; + + /** + * A pointer to the code that was injected into the target process. + */ + private final WinDef.LPVOID code; + + /** + * A source of random values being used for generating pipe names. + */ + private final SecureRandom random; + + /** + * Creates a new connection via a named pipe. + * + * @param library The library to use for communicating with Windows native functions. + * @param attachLibrary The library to use for communicating with Windows attachment extension that is included as a DLL. + * @param process The handle of the target VM's process. + * @param code A pointer to the code that was injected into the target process. + */ + protected ForJnaWindowsNamedPipe(WindowsLibrary library, + WindowsAttachLibrary attachLibrary, + WinNT.HANDLE process, + WinDef.LPVOID code) { + this.library = library; + this.attachLibrary = attachLibrary; + this.process = process; + this.code = code; + random = new SecureRandom(); + } + + /** + * {@inheritDoc} + */ + public Response execute(String protocol, String... argument) { + if (!"1".equals(protocol)) { + throw new IllegalArgumentException("Unknown protocol version: " + protocol); + } else if (argument.length > 4) { + throw new IllegalArgumentException("Cannot supply more then four arguments to Windows attach mechanism: " + Arrays.asList(argument)); + } + String name = "\\\\.\\pipe\\javatool" + Math.abs(random.nextInt() + 1); + + WinBase.SECURITY_ATTRIBUTES sa = createSecurityAttributesToAllowMediumIntegrity(); + + WinNT.HANDLE pipe = Kernel32.INSTANCE.CreateNamedPipe(name, + WinBase.PIPE_ACCESS_INBOUND, + WinBase.PIPE_TYPE_BYTE | WinBase.PIPE_READMODE_BYTE | WinBase.PIPE_WAIT, + 1, + 4096, + 8192, + WinBase.NMPWAIT_USE_DEFAULT_WAIT, + sa); + if (pipe == null) { + throw new Win32Exception(Native.getLastError()); + } + try { + WinDef.LPVOID data = attachLibrary.allocate_remote_argument(process, + name, + argument.length < 1 ? null : argument[0], + argument.length < 2 ? null : argument[1], + argument.length < 3 ? null : argument[2], + argument.length < 4 ? null : argument[3]); + if (data == null) { + throw new Win32Exception(Native.getLastError()); + } + try { + WinNT.HANDLE thread = library.CreateRemoteThread(process, null, 0, code.getPointer(), data.getPointer(), null, null); + if (thread == null) { + throw new Win32Exception(Native.getLastError()); + } + try { + int result = Kernel32.INSTANCE.WaitForSingleObject(thread, WinBase.INFINITE); + if (result != 0) { + throw new Win32Exception(result); + } + IntByReference exitCode = new IntByReference(); + if (!library.GetExitCodeThread(thread, exitCode)) { + throw new Win32Exception(Native.getLastError()); + } else if (exitCode.getValue() != 0) { + throw new IllegalStateException("Target VM could not dispatch command successfully: " + exitCode.getValue()); + } + if (!Kernel32.INSTANCE.ConnectNamedPipe(pipe, null)) { + int code = Native.getLastError(); + if (code != WinError.ERROR_PIPE_CONNECTED) { + throw new Win32Exception(code); + } + } + return new NamedPipeResponse(pipe); + } finally { + if (!Kernel32.INSTANCE.CloseHandle(thread)) { + throw new Win32Exception(Native.getLastError()); + } + } + } finally { + if (!library.VirtualFreeEx(process, data.getPointer(), 0, MEM_RELEASE)) { + throw new Win32Exception(Native.getLastError()); + } + } + } catch (Throwable throwable) { + if (!Kernel32.INSTANCE.CloseHandle(pipe)) { + throw new Win32Exception(Native.getLastError()); + } else if (throwable instanceof RuntimeException) { + throw (RuntimeException) throwable; + } else { + throw new IllegalStateException(throwable); + } + } + } + + /** + * Custom {@link WinBase.SECURITY_ATTRIBUTES} is required here to "get" Medium Integrity Level. + * In order to allow Medium Integrity Level clients to open + * and use a NamedPipe created by an High Integrity Level process. + * + * @return A security attributes object that gives everyone read and write access. + */ + private WinBase.SECURITY_ATTRIBUTES createSecurityAttributesToAllowMediumIntegrity() { + // Allow read/write to Everybody + WinNT.PSID pSidEverybody = new WinNT.PSID(WinNT.SECURITY_MAX_SID_SIZE); + if (!Advapi32.INSTANCE.CreateWellKnownSid(WinNT.WELL_KNOWN_SID_TYPE.WinWorldSid, null, pSidEverybody, new IntByReference(WinNT.SECURITY_MAX_SID_SIZE))) { + throw new Win32Exception(Kernel32.INSTANCE.GetLastError()); + } + // Allow full control to System + WinNT.PSID pSidSystem = new WinNT.PSID(WinNT.SECURITY_MAX_SID_SIZE); + if (!Advapi32.INSTANCE.CreateWellKnownSid(WinNT.WELL_KNOWN_SID_TYPE.WinBuiltinSystemOperatorsSid, null, pSidSystem, new IntByReference(WinNT.SECURITY_MAX_SID_SIZE))) { + throw new Win32Exception(Kernel32.INSTANCE.GetLastError()); + } + // Allow full control to Administrators + WinNT.PSID pSidAdmin = new WinNT.PSID(WinNT.SECURITY_MAX_SID_SIZE); + if (!Advapi32.INSTANCE.CreateWellKnownSid(WinNT.WELL_KNOWN_SID_TYPE.WinBuiltinAdministratorsSid, null, pSidAdmin, new IntByReference(WinNT.SECURITY_MAX_SID_SIZE))) { + throw new Win32Exception(Kernel32.INSTANCE.GetLastError()); + } + + // Prepare ACL + int cbAcl = Native.getNativeSize(WinNT.ACL.class, null); + cbAcl += Native.getNativeSize(WinNT.ACCESS_ALLOWED_ACE.class, null) * 3; + cbAcl += (Advapi32.INSTANCE.GetLengthSid(pSidEverybody) - WinDef.DWORD.SIZE); + cbAcl += (Advapi32.INSTANCE.GetLengthSid(pSidSystem) - WinDef.DWORD.SIZE); + cbAcl += (Advapi32.INSTANCE.GetLengthSid(pSidAdmin) - WinDef.DWORD.SIZE); + cbAcl = Advapi32Util.alignOnDWORD(cbAcl); + WinNT.ACL pAcl = new WinNT.ACL(cbAcl); + if (!Advapi32.INSTANCE.InitializeAcl(pAcl, cbAcl, WinNT.ACL_REVISION)) { + throw new Win32Exception(Kernel32.INSTANCE.GetLastError()); + } + if (!Advapi32.INSTANCE.AddAccessAllowedAce(pAcl, WinNT.ACL_REVISION, WinNT.GENERIC_READ | WinNT.GENERIC_WRITE, pSidEverybody)) { + throw new Win32Exception(Kernel32.INSTANCE.GetLastError()); + } + if (!Advapi32.INSTANCE.AddAccessAllowedAce(pAcl, WinNT.ACL_REVISION, WinNT.GENERIC_ALL, pSidSystem)) { + throw new Win32Exception(Kernel32.INSTANCE.GetLastError()); + } + if (!Advapi32.INSTANCE.AddAccessAllowedAce(pAcl, WinNT.ACL_REVISION, WinNT.GENERIC_ALL, pSidAdmin)) { + throw new Win32Exception(Kernel32.INSTANCE.GetLastError()); + } + WinNT.SECURITY_DESCRIPTOR sd = new WinNT.SECURITY_DESCRIPTOR(64 * 1024); + if (!Advapi32.INSTANCE.InitializeSecurityDescriptor(sd, WinNT.SECURITY_DESCRIPTOR_REVISION)) { + throw new Win32Exception(Kernel32.INSTANCE.GetLastError()); + } + if (!Advapi32.INSTANCE.SetSecurityDescriptorDacl(sd, true, pAcl, false)) { + throw new Win32Exception(Kernel32.INSTANCE.GetLastError()); + } + WinBase.SECURITY_ATTRIBUTES sa = new WinBase.SECURITY_ATTRIBUTES(); + sa.dwLength = new WinDef.DWORD(sa.size()); + sa.lpSecurityDescriptor = sd.getPointer(); + return sa; + } + + /** + * {@inheritDoc} + */ + public void close() { + try { + if (!library.VirtualFreeEx(process, code.getPointer(), 0, MEM_RELEASE)) { + throw new Win32Exception(Native.getLastError()); + } + } finally { + if (!Kernel32.INSTANCE.CloseHandle(process)) { + throw new Win32Exception(Native.getLastError()); + } + } + } + + /** + * A library for interacting with Windows. + */ + protected interface WindowsLibrary extends StdCallLibrary { + + /** + * Changes the state of memory in a given process. + * + * @param process The process in which to change the memory. + * @param address The address of the memory to allocate. + * @param size The size of the allocated region. + * @param allocationType The allocation type. + * @param protect The memory protection. + * @return A pointer to the allocated memory. + */ + @SuppressWarnings({"unused", "checkstyle:methodname"}) + Pointer VirtualAllocEx(WinNT.HANDLE process, Pointer address, int size, int allocationType, int protect); + + /** + * Frees memory in the given process. + * + * @param process The process in which to change the memory. + * @param address The address of the memory to free. + * @param size The size of the freed region. + * @param freeType The freeing type. + * @return {@code true} if the operation succeeded. + */ + @SuppressWarnings("checkstyle:methodname") + boolean VirtualFreeEx(WinNT.HANDLE process, Pointer address, int size, int freeType); + + /** + * An alternative implementation of + * {@link Kernel32#CreateRemoteThread(WinNT.HANDLE, WinBase.SECURITY_ATTRIBUTES, int, WinBase.FOREIGN_THREAD_START_ROUTINE, Pointer, WinDef.DWORD, Pointer)} + * that uses a pointer as the {@code code} argument rather then a structure to avoid accessing foreign memory. + * + * @param process A handle of the target process. + * @param securityAttributes The security attributes to use or {@code null} if no attributes are provided. + * @param stackSize The stack size or {@code 0} for using the system default. + * @param code A pointer to the code to execute. + * @param argument A pointer to the argument to provide to the code being executed. + * @param creationFlags The creation flags or {@code null} if no flags are set. + * @param threadId A pointer to the thread id or {@code null} if no thread reference is set. + * @return A handle to the created remote thread or {@code null} if the creation failed. + */ + @SuppressWarnings("checkstyle:methodname") + WinNT.HANDLE CreateRemoteThread(WinNT.HANDLE process, + WinBase.SECURITY_ATTRIBUTES securityAttributes, + int stackSize, + Pointer code, + Pointer argument, + WinDef.DWORD creationFlags, + Pointer threadId); + + /** + * Receives the exit code of a given thread. + * + * @param thread A handle to the targeted thread. + * @param exitCode A reference to the exit code value. + * @return {@code true} if the exit code retrieval succeeded. + */ + @SuppressWarnings("checkstyle:methodname") + boolean GetExitCodeThread(WinNT.HANDLE thread, IntByReference exitCode); + } + + /** + * A library for interacting with Windows. + */ + protected interface WindowsAttachLibrary extends StdCallLibrary { + + /** + * Allocates the code to invoke on the remote VM. + * + * @param process A handle to the target process. + * @return A pointer to the allocated code or {@code null} if the code could not be allocated. + */ + + @SuppressWarnings("checkstyle:methodname") + WinDef.LPVOID allocate_remote_code(WinNT.HANDLE process); + + /** + * Allocates the remote argument to supply to the remote code upon execution. + * + * @param process A handle to the target process. + * @param pipe The name of the pipe used for supplying an answer. + * @param argument0 The first argument or {@code null} if no such argument is provided. + * @param argument1 The second argument or {@code null} if no such argument is provided. + * @param argument2 The third argument or {@code null} if no such argument is provided. + * @param argument3 The forth argument or {@code null} if no such argument is provided. + * @return A pointer to the allocated argument or {@code null} if the argument could not be allocated. + */ + + @SuppressWarnings("checkstyle:methodname") + WinDef.LPVOID allocate_remote_argument(WinNT.HANDLE process, + String pipe, + String argument0, + String argument1, + String argument2, + String argument3); + } + + /** + * A response that is sent via a named pipe. + */ + protected static class NamedPipeResponse implements Response { + + /** + * A handle of the named pipe. + */ + private final WinNT.HANDLE pipe; + + /** + * Creates a new response via a named pipe. + * + * @param pipe The handle of the named pipe. + */ + protected NamedPipeResponse(WinNT.HANDLE pipe) { + this.pipe = pipe; + } + + /** + * {@inheritDoc} + */ + public int read(byte[] buffer) { + IntByReference read = new IntByReference(); + if (!Kernel32.INSTANCE.ReadFile(pipe, buffer, buffer.length, read, null)) { + int code = Native.getLastError(); + if (code == WinError.ERROR_BROKEN_PIPE) { + return -1; + } else { + throw new Win32Exception(code); + } + } + return read.getValue(); + } + + /** + * {@inheritDoc} + */ + public void close() { + try { + if (!Kernel32.INSTANCE.DisconnectNamedPipe(pipe)) { + throw new Win32Exception(Native.getLastError()); + } + } finally { + if (!Kernel32.INSTANCE.CloseHandle(pipe)) { + throw new Win32Exception(Native.getLastError()); + } + } + } + } + + /** + * A factory for establishing a connection to a JVM using a named pipe in JNA. + */ + public static class Factory implements Connection.Factory { + + /** + * The name of the native code library that is included in this artifact to support Windows attachment. + * This property can be set by other libraries that shade Byte Buddy agent and relocates the library. + */ + public static final String LIBRARY_NAME = "net.bytebuddy.library.name"; + + /** + * The library to use for communicating with Windows native functions. + */ + private final WindowsLibrary library; + + /** + * The library to use for communicating with Windows attachment extension that is included as a DLL. + */ + private final WindowsAttachLibrary attachLibrary; + + /** + * Creates a new connection factory for Windows using JNA. + */ + @SuppressWarnings("deprecation") + public Factory() { + library = Native.loadLibrary("kernel32", WindowsLibrary.class, W32APIOptions.DEFAULT_OPTIONS); + attachLibrary = Native.loadLibrary(System.getProperty(LIBRARY_NAME, "attach_hotspot_windows"), WindowsAttachLibrary.class); + } + + /** + * {@inheritDoc} + */ + public Connection connect(String processId) { + WinNT.HANDLE process = Kernel32.INSTANCE.OpenProcess(WinNT.PROCESS_ALL_ACCESS, false, Integer.parseInt(processId)); + if (process == null) { + throw new Win32Exception(Native.getLastError()); + } + try { + WinDef.LPVOID code = attachLibrary.allocate_remote_code(process); + if (code == null) { + throw new Win32Exception(Native.getLastError()); + } + return new ForJnaWindowsNamedPipe(library, attachLibrary, process, code); + } catch (Throwable throwable) { + if (!Kernel32.INSTANCE.CloseHandle(process)) { + throw new Win32Exception(Native.getLastError()); + } else if (throwable instanceof RuntimeException) { + throw (RuntimeException) throwable; + } else { + throw new IllegalStateException(throwable); + } + } + } + } + } + + /** + * A connection to a VM using a Solaris door. + */ + class ForJnaSolarisDoor implements Connection { + + /** + * The library to use for interacting with Solaris. + */ + private final SolarisLibrary library; + + /** + * The socket used for communication. + */ + private final File socket; + + /** + * Creates a new connection using a Solaris door. + * + * @param library The library to use for interacting with Solaris. + * @param socket The socket used for communication. + */ + protected ForJnaSolarisDoor(SolarisLibrary library, File socket) { + this.library = library; + this.socket = socket; + } + + /** + * {@inheritDoc} + */ + public Connection.Response execute(String protocol, String... argument) throws IOException { + int handle = library.open(socket.getAbsolutePath(), 2); + try { + SolarisLibrary.DoorArgument door = new SolarisLibrary.DoorArgument(); + try { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + outputStream.write(protocol.getBytes("UTF-8")); + outputStream.write(0); + for (String anArgument : argument) { + if (anArgument != null) { + outputStream.write(anArgument.getBytes("UTF-8")); + } + outputStream.write(0); + } + door.dataSize = outputStream.size(); + Memory dataPointer = new Memory(outputStream.size()); + try { + dataPointer.write(0, outputStream.toByteArray(), 0, outputStream.size()); + door.dataPointer = dataPointer; + Memory result = new Memory(128); + try { + door.resultPointer = result; + door.resultSize = (int) result.size(); + if (library.door_call(handle, door) != 0) { + throw new IllegalStateException("Door call to target VM failed"); + } else if (door.resultSize < 4 || door.resultPointer.getInt(0) != 0) { + throw new IllegalStateException("Target VM could not execute door call"); + } else if (door.descriptorCount != 1 || door.descriptorPointer == null) { + throw new IllegalStateException("Did not receive communication descriptor from target VM"); + } else { + return new Response(library, door.descriptorPointer.getInt(4)); + } + } finally { + result = null; + } + } finally { + dataPointer = null; + } + } finally { + door = null; + } + } finally { + library.close(handle); + } + } + + /** + * {@inheritDoc} + */ + public void close() { + /* do nothing */ + } + + /** + * A library for interacting with Solaris. + */ + protected interface SolarisLibrary extends Library { + + /** + * Sends a kill signal to the target VM. + * + * @param processId The target process's id. + * @param signal The signal to send. + * @return The return code. + * @throws LastErrorException If an error occurred while sending the signal. + */ + int kill(int processId, int signal) throws LastErrorException; + + /** + * Opens a file. + * + * @param file The file name. + * @param flags the flags for opening. + * @return The file descriptor. + * @throws LastErrorException If the file could not be opened. + */ + int open(String file, int flags) throws LastErrorException; + + /** + * Reads from a handle. + * + * @param handle The handle representing the source being read. + * @param buffer The buffer to read to. + * @param length The buffer length. + * @return The amount of bytes being read. + * @throws LastErrorException If a read operation failed. + */ + int read(int handle, ByteBuffer buffer, int length) throws LastErrorException; + + /** + * Releases a descriptor. + * + * @param descriptor The descriptor to release. + * @return The return code. + * @throws LastErrorException If the descriptor could not be closed. + */ + int close(int descriptor) throws LastErrorException; + + /** + * Executes a door call. + * + * @param descriptor The door's descriptor. + * @param argument A pointer to the argument. + * @return The door's handle. + * @throws LastErrorException If the door call failed. + */ + @SuppressWarnings("checkstyle:methodname") + int door_call(int descriptor, DoorArgument argument) throws LastErrorException; + + /** + * A structure representing the argument to a Solaris door operation. + */ + class DoorArgument extends Structure { + + /** + * A pointer to the operation argument. + */ + + public Pointer dataPointer; + + /** + * The size of the argument being pointed to. + */ + public int dataSize; + + /** + * A pointer to the operation descriptor. + */ + + public Pointer descriptorPointer; + + /** + * The size of the operation argument. + */ + public int descriptorCount; + + /** + * A pointer to the operation result. + */ + public Pointer resultPointer; + + /** + * The size of the operation argument. + */ + public int resultSize; + + @Override + protected List getFieldOrder() { + return Arrays.asList("dataPointer", "dataSize", "descriptorPointer", "descriptorCount", "resultPointer", "resultSize"); + } + } + } + + /** + * A response from a VM using a Solaris door. + */ + protected static class Response implements Connection.Response { + + /** + * The Solaris library to use. + */ + private final SolarisLibrary library; + + /** + * The door handle. + */ + private final int handle; + + /** + * Creates a response from a VM using a Solaris door. + * + * @param library The Solaris library to use. + * @param handle The door handle. + */ + protected Response(SolarisLibrary library, int handle) { + this.library = library; + this.handle = handle; + } + + /** + * {@inheritDoc} + */ + public int read(byte[] buffer) { + int read = library.read(handle, ByteBuffer.wrap(buffer), buffer.length); + return read == 0 ? -1 : read; + } + + /** + * {@inheritDoc} + */ + public void close() { + library.close(handle); + } + } + + /** + * A factory for establishing a connection to a JVM using a Solaris door in JNA. + */ + public static class Factory extends Connection.Factory.ForSocketFile { + + /** + * The library to use for interacting with Solaris. + */ + private final SolarisLibrary library; + + /** + * Creates a new connection factory for a Solaris VM. + * + * @param attempts The maximum amount of attempts for checking the establishment of a socket connection. + * @param pause The pause between two checks for an established socket connection. + * @param timeUnit The time unit of the pause time. + */ + @SuppressWarnings("deprecation") + public Factory(int attempts, long pause, TimeUnit timeUnit) { + super("/tmp", attempts, pause, timeUnit); + library = Native.loadLibrary("c", SolarisLibrary.class); + } + + /** + * {@inheritDoc} + */ + protected void kill(String processId, int signal) { + library.kill(Integer.parseInt(processId), signal); + } + + /** + * {@inheritDoc} + */ + protected Connection doConnect(File socket) { + return new ForJnaSolarisDoor(library, socket); + } + } + } + } + } + + /** + * A virtual machine attachment implementation for OpenJ9 or any compatible JVM. + */ + class ForOpenJ9 extends AbstractBase { + + /** + * The temporary folder for attachment files for OpenJ9 VMs. + */ + private static final String IBM_TEMPORARY_FOLDER = "com.ibm.tools.attach.directory"; + + /** + * A secure random for generating randomized ids. + */ + private static final SecureRandom SECURE_RANDOM = new SecureRandom(); + + /** + * The socket on which this VM and the target VM communicate. + */ + private final Socket socket; + + /** + * Creates a new virtual machine connection for OpenJ9. + * + * @param socket The socket on which this VM and the target VM communicate. + */ + protected ForOpenJ9(Socket socket) { + this.socket = socket; + } + + /** + * Attaches to the supplied process id using the default JNA implementation. This method will not consider + * attaching to VMs owned by different users than the current user. + * + * @param processId The process id. + * @return A suitable virtual machine implementation. + * @throws IOException If an IO exception occurs during establishing the connection. + */ + public static VirtualMachine attach(String processId) throws IOException { + return attach(processId, 5000, Platform.isWindows() + ? new Dispatcher.ForJnaWindowsEnvironment() + : new Dispatcher.ForJnaPosixEnvironment(15, 100, TimeUnit.MILLISECONDS)); + } + + /** + * Attaches to the supplied process id. + * + * @param processId The process id. + * @param timeout The timeout for establishing the socket connection. + * @param dispatcher The connector to use to communicate with the target VM. + * @return A suitable virtual machine implementation. + * @throws IOException If an IO exception occurs during establishing the connection. + */ + public static VirtualMachine attach(String processId, int timeout, Dispatcher dispatcher) throws IOException { + String temporary = System.getProperty(IBM_TEMPORARY_FOLDER); + File directory = temporary == null + ? new File(dispatcher.getTemporaryFolder(processId), ".com_ibm_tools_attach") + : new File(temporary); + long userId = dispatcher.userId(); + RandomAccessFile attachLock = new RandomAccessFile(new File(directory, "_attachlock"), "rw"); + try { + FileLock attachLockLock = attachLock.getChannel().lock(); + try { + List virtualMachines; + RandomAccessFile master = new RandomAccessFile(new File(directory, "_master"), "rw"); + try { + FileLock masterLock = master.getChannel().lock(); + try { + File[] vmFolder = directory.listFiles(); + if (vmFolder == null) { + throw new IllegalStateException("No descriptor files found in " + directory); + } + virtualMachines = new ArrayList(); + for (File aVmFolder : vmFolder) { + if (aVmFolder.isDirectory() && (userId == 0L || dispatcher.getOwnerIdOf(aVmFolder) == userId)) { + File attachInfo = new File(aVmFolder, "attachInfo"); + if (attachInfo.isFile()) { + Properties virtualMachine = new Properties(); + FileInputStream inputStream = new FileInputStream(attachInfo); + try { + virtualMachine.load(inputStream); + } finally { + inputStream.close(); + } + int targetProcessId = Integer.parseInt(virtualMachine.getProperty("processId")); + long targetUserId; + try { + targetUserId = Long.parseLong(virtualMachine.getProperty("userUid")); + } catch (NumberFormatException ignored) { + targetUserId = 0L; + } + if (userId != 0L && targetUserId == 0L) { + targetUserId = dispatcher.getOwnerIdOf(attachInfo); + } + if (targetProcessId == 0L || dispatcher.isExistingProcess(targetProcessId)) { + virtualMachines.add(virtualMachine); + } else if (userId == 0L || targetUserId == userId) { + File[] vmFile = aVmFolder.listFiles(); + if (vmFile != null) { + for (File aVmFile : vmFile) { + if (!aVmFile.delete()) { + aVmFile.deleteOnExit(); + } + } + } + if (!aVmFolder.delete()) { + aVmFolder.deleteOnExit(); + } + } + } + } + } + } finally { + masterLock.release(); + } + } finally { + master.close(); + } + Properties target = null; + for (Properties virtualMachine : virtualMachines) { + if (virtualMachine.getProperty("processId").equalsIgnoreCase(processId)) { + target = virtualMachine; + break; + } + } + if (target == null) { + throw new IllegalStateException("Could not locate target process info in " + directory); + } + ServerSocket serverSocket = new ServerSocket(0); + try { + serverSocket.setSoTimeout(timeout); + File receiver = new File(directory, target.getProperty("vmId")); + String key; + synchronized (SECURE_RANDOM) { + key = Long.toHexString(SECURE_RANDOM.nextLong()); + } + File reply = new File(receiver, "replyInfo"); + long targetUserId; + try { + targetUserId = Long.parseLong(target.getProperty("userUid")); + } catch (NumberFormatException ignored) { + targetUserId = 0L; + } + try { + if (reply.createNewFile()) { + dispatcher.setPermissions(reply, 0600); + } + if (userId == 0L && targetUserId != 0L) { + dispatcher.chownFileToUser(reply, targetUserId); + } + FileOutputStream outputStream = new FileOutputStream(reply); + try { + outputStream.write(key.getBytes("UTF-8")); + outputStream.write("\n".getBytes("UTF-8")); + outputStream.write(Long.toString(serverSocket.getLocalPort()).getBytes("UTF-8")); + outputStream.write("\n".getBytes("UTF-8")); + } finally { + outputStream.close(); + } + Map locks = new HashMap(); + try { + String pid = Long.toString(dispatcher.pid()); + for (Properties virtualMachine : virtualMachines) { + if (!virtualMachine.getProperty("processId").equalsIgnoreCase(pid)) { + String attachNotificationSync = virtualMachine.getProperty("attachNotificationSync"); + RandomAccessFile syncFile = new RandomAccessFile(attachNotificationSync == null + ? new File(directory, "attachNotificationSync") + : new File(attachNotificationSync), "rw"); + try { + locks.put(syncFile, syncFile.getChannel().lock()); + } catch (IOException ignored) { + syncFile.close(); + } + } + } + int notifications = 0; + File[] item = directory.listFiles(); + if (item != null) { + for (File anItem : item) { + String name = anItem.getName(); + if (!name.startsWith(".trash_") + && !name.equalsIgnoreCase("_attachlock") + && !name.equalsIgnoreCase("_master") + && !name.equalsIgnoreCase("_notifier")) { + notifications += 1; + } + } + } + boolean global = Boolean.parseBoolean(target.getProperty("globalSemaphore")); + dispatcher.incrementSemaphore(directory, "_notifier", global, notifications); + try { + Socket socket = serverSocket.accept(); + String answer = new String(read(socket), "UTF-8"); + if (answer.contains(' ' + key + ' ')) { + return new ForOpenJ9(socket); + } else { + socket.close(); + throw new IllegalStateException("Unexpected answered to attachment: " + answer); + } + } finally { + dispatcher.decrementSemaphore(directory, "_notifier", global, notifications); + } + } finally { + for (Map.Entry entry : locks.entrySet()) { + try { + try { + entry.getValue().release(); + } finally { + entry.getKey().close(); + } + } catch (Throwable ignored) { + /* do nothing */ + } + } + } + } finally { + if (!reply.delete()) { + reply.deleteOnExit(); + } + } + } finally { + serverSocket.close(); + } + } finally { + attachLockLock.release(); + } + } finally { + attachLock.close(); + } + } + + /** + * {@inheritDoc} + */ + public Properties getSystemProperties() throws IOException { + write(socket, "ATTACH_GETSYSTEMPROPERTIES".getBytes("UTF-8")); + Properties properties = new Properties(); + properties.load(new ByteArrayInputStream(read(socket))); + return properties; + } + + /** + * {@inheritDoc} + */ + public Properties getAgentProperties() throws IOException { + write(socket, "ATTACH_GETAGENTPROPERTIES".getBytes("UTF-8")); + Properties properties = new Properties(); + properties.load(new ByteArrayInputStream(read(socket))); + return properties; + } + + /** + * {@inheritDoc} + */ + public void loadAgent(String jarFile, String argument) throws IOException { + write(socket, ("ATTACH_LOADAGENT(instrument," + jarFile + '=' + (argument == null ? "" : argument) + ')').getBytes("UTF-8")); + String answer = new String(read(socket), "UTF-8"); + if (answer.startsWith("ATTACH_ERR")) { + throw new IllegalStateException("Target VM failed loading agent: " + answer); + } else if (!answer.startsWith("ATTACH_ACK") && !answer.startsWith("ATTACH_RESULT=")) { + throw new IllegalStateException("Unexpected response: " + answer); + } + } + + /** + * {@inheritDoc} + */ + public void loadAgentPath(String path, String argument) throws IOException { + write(socket, ("ATTACH_LOADAGENTPATH(" + path + (argument == null ? "" : (',' + argument)) + ')').getBytes("UTF-8")); + String answer = new String(read(socket), "UTF-8"); + if (answer.startsWith("ATTACH_ERR")) { + throw new IllegalStateException("Target VM failed loading native agent: " + answer); + } else if (!answer.startsWith("ATTACH_ACK") && !answer.startsWith("ATTACH_RESULT=")) { + throw new IllegalStateException("Unexpected response: " + answer); + } + } + + /** + * {@inheritDoc} + */ + public void loadAgentLibrary(String library, String argument) throws IOException { + write(socket, ("ATTACH_LOADAGENTLIBRARY(" + library + (argument == null ? "" : (',' + argument)) + ')').getBytes("UTF-8")); + String answer = new String(read(socket), "UTF-8"); + if (answer.startsWith("ATTACH_ERR")) { + throw new IllegalStateException("Target VM failed loading native library: " + answer); + } else if (!answer.startsWith("ATTACH_ACK") && !answer.startsWith("ATTACH_RESULT=")) { + throw new IllegalStateException("Unexpected response: " + answer); + } + } + + /** + * {@inheritDoc} + */ + public void startManagementAgent(Properties properties) throws IOException { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + properties.store(outputStream, null); + write(socket, "ATTACH_START_MANAGEMENT_AGENT".getBytes("UTF-8")); + write(socket, outputStream.toByteArray()); + String answer = new String(read(socket), "UTF-8"); + if (answer.startsWith("ATTACH_ERR")) { + throw new IllegalStateException("Target VM could not start management agent: " + answer); + } else if (!answer.startsWith("ATTACH_ACK") && !answer.startsWith("ATTACH_RESULT=")) { + throw new IllegalStateException("Unexpected response: " + answer); + } + } + + /** + * {@inheritDoc} + */ + public String startLocalManagementAgent() throws IOException { + write(socket, "ATTACH_START_LOCAL_MANAGEMENT_AGENT".getBytes("UTF-8")); + String answer = new String(read(socket), "UTF-8"); + if (answer.startsWith("ATTACH_ERR")) { + throw new IllegalStateException("Target VM could not start management agent: " + answer); + } else if (answer.startsWith("ATTACH_ACK")) { + return answer.substring("ATTACH_ACK".length()); + } else if (answer.startsWith("ATTACH_RESULT=")) { + return answer.substring("ATTACH_RESULT=".length()); + } else { + throw new IllegalStateException("Unexpected response: " + answer); + } + } + + /** + * {@inheritDoc} + */ + public void detach() throws IOException { + try { + write(socket, "ATTACH_DETACH".getBytes("UTF-8")); + read(socket); // The answer is intentionally ignored. + } finally { + socket.close(); + } + } + + /** + * Writes the supplied value to the target socket. + * + * @param socket The socket to write to. + * @param value The value being written. + * @throws IOException If an I/O exception occurs. + */ + private static void write(Socket socket, byte[] value) throws IOException { + socket.getOutputStream().write(value); + socket.getOutputStream().write(0); + socket.getOutputStream().flush(); + } + + /** + * Reads a {@code '\0'}-terminated value from the target socket. + * + * @param socket The socket to read from. + * @return The value that was read. + * @throws IOException If an I/O exception occurs. + */ + private static byte[] read(Socket socket) throws IOException { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + byte[] buffer = new byte[1024]; + int length; + while ((length = socket.getInputStream().read(buffer)) != -1) { + if (length > 0 && buffer[length - 1] == 0) { + outputStream.write(buffer, 0, length - 1); + break; + } else { + outputStream.write(buffer, 0, length); + } + } + return outputStream.toByteArray(); + } + + /** + * A dispatcher for native operations being used for communication with an OpenJ9 virtual machine. + */ + public interface Dispatcher { + + /** + * Returns this machine's temporary folder. + * + * @param processId The target process's id. + * @return The temporary folder. + */ + String getTemporaryFolder(String processId); + + /** + * Returns the process id of this process. + * + * @return The process id of this process. + */ + int pid(); + + /** + * Returns the user id of this process. + * + * @return The user id of this process + */ + int userId(); + + /** + * Returns {@code true} if the supplied process id is a running process. + * + * @param processId The process id to evaluate. + * @return {@code true} if the supplied process id is currently running. + */ + boolean isExistingProcess(int processId); + + /** + * Returns the user id of the owner of the supplied file. + * + * @param file The file for which to locate the owner. + * @return The owner id of the supplied file. + */ + int getOwnerIdOf(File file); + + /** + * Sets permissions for the supplied file. + * + * @param file The file for which to set the permissions. + * @param permissions The permission bits to set. + */ + void setPermissions(File file, int permissions); + + /** + * Increments a semaphore. + * + * @param directory The sempahore's control directory. + * @param name The semaphore's name. + * @param global {@code true} if the semaphore is in the global namespace (only applicable on Windows). + * @param count The amount of increments. + */ + void incrementSemaphore(File directory, String name, boolean global, int count); + + /** + * Decrements a semaphore. + * + * @param directory The sempahore's control directory. + * @param name The semaphore's name. + * @param global {@code true} if the semaphore is in the global namespace (only applicable on Windows). + * @param count The amount of decrements. + */ + void decrementSemaphore(File directory, String name, boolean global, int count); + + /** + * Changes the ownership of a file. Can be called only if this process is owned by root. + * + * @param file The path of the file to change ownership of. + * @param userId The user that should own the file. + */ + void chownFileToUser(File file, long userId); + + /** + * A connector implementation for a POSIX environment using JNA. + */ + class ForJnaPosixEnvironment implements Dispatcher { + + /** + * The JNA library to use. + */ + private final PosixLibrary library; + + /** + * The POSIX owner provider to use. + */ + private final PosixOwnerProvider provider; + + /** + * Creates a new connector for a POSIX environment using JNA. + * + * @param attempts The maximum amount of attempts for checking the result of a foreign process. + * @param pause The pause between two checks for another process to return. + * @param timeUnit The time unit of the pause time. + */ + @SuppressWarnings("deprecation") + public ForJnaPosixEnvironment(int attempts, long pause, TimeUnit timeUnit) { + provider = Platform.isAIX() + ? new PosixOwnerProvider.UsingIStat(attempts, pause, timeUnit) + : new PosixOwnerProvider.UsingStat(attempts, pause, timeUnit); + library = Native.loadLibrary("c", PosixLibrary.class); + } + + /** + * {@inheritDoc} + */ + public String getTemporaryFolder(String processId) { + if (Platform.isLinux()) { + File file = new File("/proc/" + processId + "/root/tmp"); + if (file.isDirectory() && file.canRead()) { + return file.getAbsolutePath(); + } + } + String temporaryFolder = System.getenv("TMPDIR"); + return temporaryFolder == null ? "/tmp" : temporaryFolder; + } + + /** + * {@inheritDoc} + */ + public int pid() { + return library.getpid(); + } + + /** + * {@inheritDoc} + */ + public int userId() { + return library.getuid(); + } + + /** + * {@inheritDoc} + */ + public boolean isExistingProcess(int processId) { + return library.kill(processId, PosixLibrary.NULL_SIGNAL) != PosixLibrary.ESRCH; + } + + /** + * {@inheritDoc} + */ + public int getOwnerIdOf(File file) { + return provider.getOwnerIdOf(file); + } + + /** + * {@inheritDoc} + */ + public void setPermissions(File file, int permissions) { + library.chmod(file.getAbsolutePath(), permissions); + } + + /** + * {@inheritDoc} + */ + public void incrementSemaphore(File directory, String name, boolean global, int count) { + notifySemaphore(directory, name, count, (short) 1, (short) 0, false); + } + + /** + * {@inheritDoc} + */ + public void decrementSemaphore(File directory, String name, boolean global, int count) { + notifySemaphore(directory, name, count, (short) -1, (short) (PosixLibrary.SEM_UNDO | PosixLibrary.IPC_NOWAIT), true); + } + + /** + * {@inheritDoc} + */ + public void chownFileToUser(File file, long userId) { + library.chown(file.getAbsolutePath(), userId); + } + + /** + * Notifies a POSIX semaphore. + * + * @param directory The semaphore's directory. + * @param name The semaphore's name. + * @param count The amount of notifications to send. + * @param operation The operation to apply. + * @param flags The flags to set. + * @param acceptUnavailable {@code true} if a {@code EAGAIN} code should be accepted. + */ + private void notifySemaphore(File directory, String name, int count, short operation, short flags, boolean acceptUnavailable) { + int semaphore = library.semget(library.ftok(new File(directory, name).getAbsolutePath(), 0xA1), 2, 0666); + PosixLibrary.SemaphoreOperation target = new PosixLibrary.SemaphoreOperation(); + target.operation = operation; + target.flags = flags; + while (count-- > 0) { + try { + library.semop(semaphore, target, 1); + } catch (LastErrorException exception) { + if (acceptUnavailable && (Native.getLastError() == PosixLibrary.EAGAIN + || Native.getLastError() == PosixLibrary.EDEADLK)) { + break; + } else { + throw exception; + } + } + } + } + + /** + * An API for interaction with POSIX systems. + */ + protected interface PosixLibrary extends Library { + + /** + * A null signal. + */ + int NULL_SIGNAL = 0; + + /** + * Indicates that a process does not exist. + */ + int ESRCH = 3; + + /** + * Indicates that a request timed out. + */ + int EAGAIN = 11; + + /** + * Indicates a dead lock on a resource. + */ + int EDEADLK = 35; + + /** + * Indicates that a semaphore's operations should be undone at process shutdown. + */ + short SEM_UNDO = 0x1000; + + /** + * Indicates that one should not wait for the release of a semaphore if it is not currently available. + */ + short IPC_NOWAIT = 04000; + + /** + * Runs the {@code getpid} command. + * + * @return The command's return value. + * @throws LastErrorException If an error occurred. + */ + int getpid() throws LastErrorException; + + /** + * Runs the {@code getuid} command. + * + * @return The command's return value. + * @throws LastErrorException If an error occurred. + */ + int getuid() throws LastErrorException; + + /** + * Runs the {@code kill} command. + * + * @param processId The target process id. + * @param signal The signal to send. + * @return The command's return value. + * @throws LastErrorException If an error occurred. + */ + int kill(int processId, int signal) throws LastErrorException; + + /** + * Runs the {@code chmod} command. + * + * @param path The file path. + * @param mode The mode to set. + * @return The return code. + * @throws LastErrorException If an error occurred. + */ + int chmod(String path, int mode) throws LastErrorException; + + /** + * Runs the {@code chown} command. + * + * @param path The file path. + * @param userId The user id to set. + * @return The return code. + * @throws LastErrorException If an error occurred. + */ + int chown(String path, long userId) throws LastErrorException; + + /** + * Runs the {@code ftok} command. + * + * @param path The file path. + * @param id The id being used for creating the generated key. + * @return The generated key. + * @throws LastErrorException If an error occurred. + */ + int ftok(String path, int id) throws LastErrorException; + + /** + * Runs the {@code semget} command. + * + * @param key The key of the semaphore. + * @param count The initial count of the semaphore. + * @param flags The flags to set. + * @return The id of the semaphore. + * @throws LastErrorException If an error occurred. + */ + int semget(int key, int count, int flags) throws LastErrorException; + + /** + * Runs the {@code semop} command. + * + * @param id The id of the semaphore. + * @param operation The initial count of the semaphore. + * @param flags The flags to set. + * @return The return code. + * @throws LastErrorException If the operation was not successful. + */ + int semop(int id, SemaphoreOperation operation, int flags) throws LastErrorException; + + /** + * A structure to represent a semaphore operation for {@code semop}. + */ + class SemaphoreOperation extends Structure { + + /** + * The semaphore number. + */ + @SuppressWarnings("unused") + public short number; + + /** + * The operation to execute. + */ + public short operation; + + /** + * The flags being set for the operation. + */ + public short flags; + + @Override + protected List getFieldOrder() { + return Arrays.asList("number", "operation", "flags"); + } + } + } + + /** + * Represents a system that supports POSIX ownership. + */ + protected interface PosixOwnerProvider { + + /** + * Returns the user id of the owner of the supplied file. + * + * @param file The file for which to locate the owner. + * @return The owner id of the supplied file. + */ + int getOwnerIdOf(File file); + + /** + * An implementation of reading POSIX ownership using {@code stat}. + */ + class UsingStat implements PosixOwnerProvider { + + /** + * The maximum amount of attempts for checking the result of a foreign process. + */ + private final int attempts; + + /** + * The pause between two checks for another process to return. + */ + private final long pause; + + /** + * The time unit of the pause time. + */ + private final TimeUnit timeUnit; + + /** + * Creates a new provider where an owner is derived using the {@code stat} command. + * + * @param attempts The maximum amount of attempts for checking the result of a foreign process. + * @param pause The pause between two checks for another process to return. + * @param timeUnit The time unit of the pause time. + */ + public UsingStat(int attempts, long pause, TimeUnit timeUnit) { + this.attempts = attempts; + this.pause = pause; + this.timeUnit = timeUnit; + } + + /** + * {@inheritDoc} + */ + public int getOwnerIdOf(File file) { + try { + // The binding for 'stat' is very platform dependant. To avoid the complexity of binding the correct method, + // stat is called as a separate command. This is less efficient but more portable. + Process process = Runtime.getRuntime().exec(new String[]{"stat", + Platform.isMac() ? "-f" : "-c", + "%u", + file.getAbsolutePath()}); + int attempts = this.attempts; + String line = null; + do { + try { + if (process.exitValue() != 0) { + throw new IllegalStateException("Error while executing stat"); + } + BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(), "UTF-8")); + try { + line = reader.readLine(); + } finally { + reader.close(); + } + break; + } catch (IllegalThreadStateException ignored) { + try { + Thread.sleep(timeUnit.toMillis(pause)); + } catch (InterruptedException exception) { + Thread.currentThread().interrupt(); + throw new IllegalStateException(exception); + } + } + } while (--attempts > 0); + if (line == null) { + process.destroy(); + throw new IllegalStateException("Command for stat did not exit in time"); + } + return Integer.parseInt(line); + } catch (IOException exception) { + throw new IllegalStateException("Unable to execute stat command", exception); + } + } + } + + /** + * An implementation for reading a POSIX owner using {@code istat}. + */ + class UsingIStat implements PosixOwnerProvider { + + /** + * A pattern to represent the owner on the console output. + */ + private static final Pattern AIX_OWNER_PATTERN = Pattern.compile("Owner: (\\d+)\\("); + + /** + * The maximum amount of attempts for checking the result of a foreign process. + */ + private final int attempts; + + /** + * The pause between two checks for another process to return. + */ + private final long pause; + + /** + * The time unit of the pause time. + */ + private final TimeUnit timeUnit; + + /** + * Creates a new provider for reading a POSIX owner using {@code istat}. + * + * @param attempts The maximum amount of attempts for checking the result of a foreign process. + * @param pause The pause between two checks for another process to return. + * @param timeUnit The time unit of the pause time. + */ + public UsingIStat(int attempts, long pause, TimeUnit timeUnit) { + this.attempts = attempts; + this.pause = pause; + this.timeUnit = timeUnit; + } + + /** + * {@inheritDoc} + */ + public int getOwnerIdOf(File file) { + try { + Process process = Runtime.getRuntime().exec(new String[]{"istat", file.getAbsolutePath()}); + int attempts = this.attempts; + String lines = null; + do { + try { + if (process.exitValue() != 0) { + throw new IllegalStateException("Error while executing istat"); + } + StringBuilder output = new StringBuilder(); + BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(), "UTF-8")); + try { + String line; + while ((line = reader.readLine()) != null) { + output.append(line).append("\n"); + } + } finally { + reader.close(); + } + lines = output.toString(); + break; + } catch (IllegalThreadStateException ignored) { + try { + Thread.sleep(timeUnit.toMillis(pause)); + } catch (InterruptedException exception) { + Thread.currentThread().interrupt(); + throw new IllegalStateException(exception); + } + } + } while (--attempts > 0); + if (lines == null) { + process.destroy(); + throw new IllegalStateException("Command for istat did not exit in time"); + } + Matcher matcher = AIX_OWNER_PATTERN.matcher(lines); + if (matcher.find()) { + return Integer.parseInt(matcher.group(1)); + } else { + throw new IllegalStateException("Unable to parse response from istat command: " + lines); + } + } catch (IOException exception) { + throw new IllegalStateException("Unable to execute istat command", exception); + } + } + } + } + } + + /** + * A connector implementation for a Windows environment using JNA. + */ + class ForJnaWindowsEnvironment implements Dispatcher { + + /** + * Indicates a missing user id what is not supported on Windows. + */ + private static final int NO_USER_ID = 0; + + /** + * The name of the creation mutex. + */ + private static final String CREATION_MUTEX_NAME = "j9shsemcreationMutex"; + + /** + * A library to use for interacting with Windows. + */ + private final WindowsLibrary library; + + /** + * Creates a new connector for a Windows environment using JNA. + */ + @SuppressWarnings("deprecation") + public ForJnaWindowsEnvironment() { + library = Native.loadLibrary("kernel32", WindowsLibrary.class, W32APIOptions.DEFAULT_OPTIONS); + } + + /** + * {@inheritDoc} + */ + public String getTemporaryFolder(String processId) { + WinDef.DWORD length = new WinDef.DWORD(WinDef.MAX_PATH); + char[] path = new char[length.intValue()]; + if (Kernel32.INSTANCE.GetTempPath(length, path).intValue() == 0) { + throw new Win32Exception(Kernel32.INSTANCE.GetLastError()); + } + return Native.toString(path); + } + + /** + * {@inheritDoc} + */ + public int pid() { + return Kernel32.INSTANCE.GetCurrentProcessId(); + } + + /** + * {@inheritDoc} + */ + public int userId() { + return NO_USER_ID; + } + + /** + * {@inheritDoc} + */ + public boolean isExistingProcess(int processId) { + WinNT.HANDLE handle = Kernel32.INSTANCE.OpenProcess(WinNT.PROCESS_QUERY_INFORMATION, false, processId); + if (handle == null) { + throw new Win32Exception(Kernel32.INSTANCE.GetLastError()); + } + IntByReference exists = new IntByReference(); + if (!Kernel32.INSTANCE.GetExitCodeProcess(handle, exists)) { + throw new Win32Exception(Kernel32.INSTANCE.GetLastError()); + } + return exists.getValue() == WinBase.STILL_ACTIVE; + } + + /** + * {@inheritDoc} + */ + public int getOwnerIdOf(File file) { + return NO_USER_ID; + } + + /** + * {@inheritDoc} + */ + public void setPermissions(File file, int permissions) { + /* do nothing */ + } + + /** + * {@inheritDoc} + */ + public void incrementSemaphore(File directory, String name, boolean global, int count) { + AttachmentHandle handle = openSemaphore(directory, name, global); + try { + while (count-- > 0) { + if (!library.ReleaseSemaphore(handle.getHandle(), 1, null)) { + throw new Win32Exception(Kernel32.INSTANCE.GetLastError()); + } + } + } finally { + handle.close(); + } + } + + /** + * {@inheritDoc} + */ + public void decrementSemaphore(File directory, String name, boolean global, int count) { + AttachmentHandle handle = openSemaphore(directory, name, global); + try { + while (count-- > 0) { + int result = Kernel32.INSTANCE.WaitForSingleObject(handle.getHandle(), 0); + switch (result) { + case WinBase.WAIT_ABANDONED: + case WinBase.WAIT_OBJECT_0: + break; + case WinError.WAIT_TIMEOUT: + return; + default: + throw new Win32Exception(result); + } + } + } finally { + handle.close(); + } + } + + /** + * {@inheritDoc} + */ + public void chownFileToUser(File file, long userId) { + /* do nothing */ + } + + /** + * Opens a semaphore for signaling another process that an attachment is performed. + * + * @param directory The control directory. + * @param name The semaphore's name. + * @param global {@code true} if the semaphore is in the global namespace. + * @return A handle for signaling an attachment to the target process. + */ + private AttachmentHandle openSemaphore(File directory, String name, boolean global) { + WinNT.SECURITY_DESCRIPTOR securityDescriptor = new WinNT.SECURITY_DESCRIPTOR(64 * 1024); + try { + if (!Advapi32.INSTANCE.InitializeSecurityDescriptor(securityDescriptor, WinNT.SECURITY_DESCRIPTOR_REVISION)) { + throw new Win32Exception(Kernel32.INSTANCE.GetLastError()); + } + if (!Advapi32.INSTANCE.SetSecurityDescriptorDacl(securityDescriptor, true, null, true)) { + throw new Win32Exception(Kernel32.INSTANCE.GetLastError()); + } + WindowsLibrary.SecurityAttributes securityAttributes = new WindowsLibrary.SecurityAttributes(); + try { + securityAttributes.length = new WinDef.DWORD(securityAttributes.size()); + securityAttributes.securityDescriptor = securityDescriptor.getPointer(); + WinNT.HANDLE mutex = library.CreateMutex(securityAttributes, false, CREATION_MUTEX_NAME); + if (mutex == null) { + int lastError = Kernel32.INSTANCE.GetLastError(); + if (lastError == WinError.ERROR_ALREADY_EXISTS) { + mutex = library.OpenMutex(WinNT.STANDARD_RIGHTS_REQUIRED | WinNT.SYNCHRONIZE | 0x0001, false, CREATION_MUTEX_NAME); + if (mutex == null) { + throw new Win32Exception(Kernel32.INSTANCE.GetLastError()); + } + } else { + throw new Win32Exception(lastError); + } + } + int result = Kernel32.INSTANCE.WaitForSingleObject(mutex, 2000); + switch (result) { + case WinBase.WAIT_FAILED: + case WinError.WAIT_TIMEOUT: + throw new Win32Exception(result); + default: + try { + String target = (global ? "Global\\" : "") + + (directory.getAbsolutePath() + '_' + name).replaceAll("[^a-zA-Z0-9_]", "") + + "_semaphore"; + WinNT.HANDLE parent = library.OpenSemaphoreW(WindowsLibrary.SEMAPHORE_ALL_ACCESS, false, target); + if (parent == null) { + parent = library.CreateSemaphoreW(null, 0, Integer.MAX_VALUE, target); + if (parent == null) { + throw new Win32Exception(Kernel32.INSTANCE.GetLastError()); + } + WinNT.HANDLE child = library.CreateSemaphoreW(null, 0, Integer.MAX_VALUE, target + "_set0"); + if (child == null) { + throw new Win32Exception(Kernel32.INSTANCE.GetLastError()); + } + return new AttachmentHandle(parent, child); + } else { + WinNT.HANDLE child = library.OpenSemaphoreW(WindowsLibrary.SEMAPHORE_ALL_ACCESS, false, target + "_set0"); + if (child == null) { + throw new Win32Exception(Kernel32.INSTANCE.GetLastError()); + } + return new AttachmentHandle(parent, child); + } + } finally { + if (!library.ReleaseMutex(mutex)) { + throw new Win32Exception(Native.getLastError()); + } + } + } + } finally { + securityAttributes = null; + } + } finally { + securityDescriptor = null; + } + } + + /** + * A library for interacting with Windows. + */ + protected interface WindowsLibrary extends StdCallLibrary { + + /** + * Indicates that a semaphore requires all access rights. + */ + int SEMAPHORE_ALL_ACCESS = 0x1F0003; + + /** + * Opens an existing semaphore. + * + * @param access The access rights. + * @param inheritHandle {@code true} if the handle is inherited. + * @param name The semaphore's name. + * @return The handle or {@code null} if the handle could not be created. + */ + + @SuppressWarnings("checkstyle:methodname") + WinNT.HANDLE OpenSemaphoreW(int access, boolean inheritHandle, String name); + + /** + * Creates a new semaphore. + * + * @param securityAttributes The security attributes for the created semaphore. + * @param count The initial count for the semaphore. + * @param maximumCount The maximum count for the semaphore. + * @param name The semaphore's name. + * @return The handle or {@code null} if the handle could not be created. + */ + + @SuppressWarnings("checkstyle:methodname") + WinNT.HANDLE CreateSemaphoreW(WinBase.SECURITY_ATTRIBUTES securityAttributes, + long count, + long maximumCount, + String name); + + /** + * Releases the semaphore. + * + * @param handle The semaphore's handle. + * @param count The amount with which to increase the semaphore. + * @param previousCount The previous count of the semaphore or {@code null}. + * @return {@code true} if the semaphore was successfully released. + */ + @SuppressWarnings("checkstyle:methodname") + boolean ReleaseSemaphore(WinNT.HANDLE handle, long count, Long previousCount); + + /** + * Create or opens a mutex. + * + * @param attributes The mutex's security attributes. + * @param owner {@code true} if the caller is supposed to be the initial owner. + * @param name The mutex name. + * @return The handle to the mutex or {@code null} if the mutex could not be created. + */ + + @SuppressWarnings("checkstyle:methodname") + WinNT.HANDLE CreateMutex(SecurityAttributes attributes, boolean owner, String name); + + /** + * Opens an existing object. + * + * @param access The required access privileges. + * @param inherit {@code true} if the mutex should be inherited. + * @param name The mutex's name. + * @return The handle or {@code null} if the mutex could not be opened. + */ + @SuppressWarnings("checkstyle:methodname") + WinNT.HANDLE OpenMutex(int access, boolean inherit, String name); + + /** + * Releases the supplied mutex. + * + * @param handle The handle to the mutex. + * @return {@code true} if the handle was successfully released. + */ + @SuppressWarnings("checkstyle:methodname") + boolean ReleaseMutex(WinNT.HANDLE handle); + + /** + * A structure representing a mutex's security attributes. + */ + class SecurityAttributes extends Structure { + + /** + * The descriptor's length. + */ + + public WinDef.DWORD length; + + /** + * A pointer to the descriptor. + */ + + public Pointer securityDescriptor; + + /** + * {@code true} if the attributes are inherited. + */ + @SuppressWarnings("unused") + public boolean inherit; + + @Override + protected List getFieldOrder() { + return Arrays.asList("length", "securityDescriptor", "inherit"); + } + } + } + + /** + * A handle for an attachment which is represented by a pair of handles. + */ + protected static class AttachmentHandle implements Closeable { + + /** + * The parent handle. + */ + private final WinNT.HANDLE parent; + + /** + * The child handle. + */ + private final WinNT.HANDLE child; + + /** + * Creates a new attachment handle. + * + * @param parent The parent handle. + * @param child The child handle. + */ + protected AttachmentHandle(WinNT.HANDLE parent, WinNT.HANDLE child) { + this.parent = parent; + this.child = child; + } + + /** + * Returns the handle on which signals are to be sent. + * + * @return The handle on which signals are to be sent. + */ + protected WinNT.HANDLE getHandle() { + return child; + } + + /** + * {@inheritDoc} + */ + public void close() { + boolean closed; + try { + if (!Kernel32.INSTANCE.CloseHandle(child)) { + throw new Win32Exception(Kernel32.INSTANCE.GetLastError()); + } + } finally { + closed = Kernel32.INSTANCE.CloseHandle(parent); + } + if (!closed) { + throw new Win32Exception(Kernel32.INSTANCE.GetLastError()); + } + } + } + } + } + } +} \ No newline at end of file diff --git a/generator/src/main/java/com/reajason/javaweb/memshell/utils/CommonUtil.java b/generator/src/main/java/com/reajason/javaweb/memshell/utils/CommonUtil.java index 8406b02e..d70ff7df 100644 --- a/generator/src/main/java/com/reajason/javaweb/memshell/utils/CommonUtil.java +++ b/generator/src/main/java/com/reajason/javaweb/memshell/utils/CommonUtil.java @@ -91,6 +91,10 @@ public class CommonUtil { return PACKAGE_NAMES[new Random().nextInt(PACKAGE_NAMES.length)] + "." + getRandomString(5); } + public static String getPackageName(String className) { + return className.substring(0, className.lastIndexOf(".")); + } + public static String generateShellClassName() { return getRandomPackageName() + ".ErrorHandler"; } diff --git a/generator/src/main/resources/win32-x86-64/attach_hotspot_windows.dll b/generator/src/main/resources/win32-x86-64/attach_hotspot_windows.dll new file mode 100755 index 00000000..6748a72a Binary files /dev/null and b/generator/src/main/resources/win32-x86-64/attach_hotspot_windows.dll differ diff --git a/generator/src/main/resources/win32-x86/attach_hotspot_windows.dll b/generator/src/main/resources/win32-x86/attach_hotspot_windows.dll new file mode 100755 index 00000000..38046bfb Binary files /dev/null and b/generator/src/main/resources/win32-x86/attach_hotspot_windows.dll differ diff --git a/integration-test/src/test/java/com/reajason/javaweb/integration/ShellAssertionTool.java b/integration-test/src/test/java/com/reajason/javaweb/integration/ShellAssertionTool.java index ed16d308..b30b7d28 100644 --- a/integration-test/src/test/java/com/reajason/javaweb/integration/ShellAssertionTool.java +++ b/integration-test/src/test/java/com/reajason/javaweb/integration/ShellAssertionTool.java @@ -5,6 +5,8 @@ import com.reajason.javaweb.behinder.BehinderManager; import com.reajason.javaweb.godzilla.GodzillaManager; import com.reajason.javaweb.memshell.*; import com.reajason.javaweb.memshell.config.*; +import com.reajason.javaweb.memshell.packer.jar.AgentJarPacker; +import com.reajason.javaweb.memshell.packer.jar.AgentJarWithJREAttacherPacker; import com.reajason.javaweb.memshell.packer.jar.JarPacker; import com.reajason.javaweb.suo5.Suo5Manager; import lombok.SneakyThrows; @@ -16,6 +18,7 @@ import okhttp3.Request; import okhttp3.Response; import org.java_websocket.client.WebSocketClient; import org.java_websocket.handshake.ServerHandshake; +import org.testcontainers.containers.Container; import org.testcontainers.containers.GenericContainer; import org.testcontainers.shaded.org.apache.commons.io.FileUtils; import org.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils; @@ -92,13 +95,12 @@ public class ShellAssertionTool { @SneakyThrows public static void packerResultAndInject(GenerateResult generateResult, String url, ShellTool shellTool, String shellType, Packers packer, GenericContainer appContainer) { String content = null; - if (packer.getInstance() instanceof JarPacker) { + if (packer.getInstance() instanceof AgentJarPacker) { byte[] bytes = ((JarPacker) packer.getInstance()).packBytes(generateResult); Path tempJar = Files.createTempFile("temp", "jar"); Files.write(tempJar, bytes); String jarPath = "/" + shellTool + shellType + packer.name() + ".jar"; appContainer.copyFileToContainer(MountableFile.forHostPath(tempJar, 0100666), jarPath); -// Files.copy(tempJar, Paths.get("target.jar")); FileUtils.deleteQuietly(tempJar.toFile()); String pidInContainer = appContainer.execInContainer("bash", "/fetch_pid.sh").getStdout(); assertDoesNotThrow(() -> Long.parseLong(pidInContainer)); @@ -108,6 +110,27 @@ public class ShellAssertionTool { containsString("ATTACH_ACK"), containsString("JVM response code = 0") )); + } else if (packer.getInstance() instanceof AgentJarWithJREAttacherPacker) { + byte[] bytes = ((JarPacker) packer.getInstance()).packBytes(generateResult); + Path tempJar = Files.createTempFile("temp", "jar"); + Files.write(tempJar, bytes); + String jarPath = "/" + shellTool + shellType + packer.name() + ".jar"; + appContainer.copyFileToContainer(MountableFile.forHostPath(tempJar, 0100666), jarPath); + FileUtils.deleteQuietly(tempJar.toFile()); + String pidInContainer = appContainer.execInContainer("bash", "/fetch_pid.sh").getStdout(); + assertDoesNotThrow(() -> Long.parseLong(pidInContainer)); + Container.ExecResult execResult = appContainer.execInContainer("java", "-jar", jarPath, pidInContainer); + String stdout = execResult.getStdout(); + if (stdout.contains("executable file not found")) { + execResult = appContainer.execInContainer("/opt/IBM/WebSphere/AppServer/java/bin/java", "-jar", jarPath, pidInContainer); + } + stdout = execResult.getStdout(); + System.out.println(stdout); + System.out.println(execResult.getStderr()); + log.info("attach result: {}", stdout); + assertThat(stdout, anyOf( + containsString("ok") + )); } else { content = packer.getInstance().pack(generateResult); assertInjectIsOk(url, shellType, shellTool, content, packer, appContainer); diff --git a/integration-test/src/test/java/com/reajason/javaweb/integration/TestCasesProvider.java b/integration-test/src/test/java/com/reajason/javaweb/integration/TestCasesProvider.java index 01620f97..2f94a6a9 100644 --- a/integration-test/src/test/java/com/reajason/javaweb/integration/TestCasesProvider.java +++ b/integration-test/src/test/java/com/reajason/javaweb/integration/TestCasesProvider.java @@ -19,16 +19,30 @@ import static org.junit.jupiter.params.provider.Arguments.arguments; */ public class TestCasesProvider { - public static Stream getTestCases(String imageName, Server server, List testShellTypes, List testPackers, List> unSupportedCases) { + public static Stream getTestCases(String imageName, + Server server, + List testShellTypes, + List testPackers, + List> unSupportedCases) { return getTestCases(imageName, server, testShellTypes, testPackers, unSupportedCases, null); } - public static Stream getTestCases(String imageName, Server server, List testShellTypes, List testPackers, List> unSupportedCases, List unSupportedShellTools) { + public static Stream getTestCases(String imageName, + Server server, + List testShellTypes, + List testPackers, + List> unSupportedCases, + List unSupportedShellTools) { Set supportedShellTools = new TreeSet<>(server.getShell().getSupportedShellTools()); if (unSupportedShellTools != null) { unSupportedShellTools.forEach(supportedShellTools::remove); } - Set unSupported = unSupportedCases == null ? Collections.emptySet() : unSupportedCases.stream().map(i -> i.getLeft() + i.getMiddle() + i.getRight()).collect(Collectors.toSet()); + Set unSupported = unSupportedCases == null ? + Collections.emptySet() : + unSupportedCases + .stream() + .map(i -> i.getLeft() + i.getMiddle() + i.getRight()) + .collect(Collectors.toSet()); return supportedShellTools.stream() .flatMap(supportedShellTool -> { List toolSupportedShellTypes = new ArrayList<>(); @@ -41,7 +55,9 @@ public class TestCasesProvider { return toolSupportedShellTypes.stream().flatMap(supportedShellType -> { if (supportedShellType.startsWith(ShellType.AGENT)) { if (!unSupported.contains(supportedShellType + supportedShellTool + Packers.AgentJar)) { - return Stream.of(arguments(imageName, supportedShellType, supportedShellTool, Packers.AgentJar)); + return Stream.of( + arguments(imageName, supportedShellType, supportedShellTool, Packers.AgentJar) + ); } return Stream.empty(); } else { @@ -58,7 +74,10 @@ public class TestCasesProvider { }); } - public static Stream getTestCases(String imageName, Server server, List testShellTypes, List testPackers) { + public static Stream getTestCases(String imageName, + Server server, + List testShellTypes, + List testPackers) { return getTestCases(imageName, server, testShellTypes, testPackers, null); } } diff --git a/memshell-agent/memshell-agent-attacher/build.gradle b/memshell-agent/memshell-agent-attacher/build.gradle new file mode 100644 index 00000000..b85d1c11 --- /dev/null +++ b/memshell-agent/memshell-agent-attacher/build.gradle @@ -0,0 +1,33 @@ +plugins { + id 'java' + id "com.gradleup.shadow" version "8.3.6" +} + +group = 'com.reajason.javaweb' +version = '1.0.0' + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(8) + } + sourceCompatibility = JavaVersion.VERSION_1_6 + targetCompatibility = JavaVersion.VERSION_1_6 +} + +dependencies { + implementation 'net.java.dev.jna:jna:5.17.0' + implementation 'net.java.dev.jna:jna-platform:5.17.0' +} + +jar { + manifest { + attributes 'Premain-Class': 'Agent' + attributes 'Agent-Class': 'Agent' + attributes 'Main-Class': "Main" + attributes 'Can-Redefine-Classes': true + attributes 'Can-Retransform-Classes': true + attributes 'Can-Set-Native-Method-Prefix': true + } +} + +jar.finalizedBy shadowJar diff --git a/memshell-agent/memshell-agent-attacher/src/main/c/attach_hotspot_windows.c b/memshell-agent/memshell-agent-attacher/src/main/c/attach_hotspot_windows.c new file mode 100644 index 00000000..4072ca3c --- /dev/null +++ b/memshell-agent/memshell-agent-attacher/src/main/c/attach_hotspot_windows.c @@ -0,0 +1,130 @@ +/** + * Copyright 2014 - Present Rafael Winterhalter + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include + +#define OPEN_JVM_ERROR 200 +#define GET_ENQUEUE_FUNCTION_ERROR 201 +#define CODE_SIZE (SIZE_T) 1024 +#define MAX_ARGUMENT 1024 + +typedef HMODULE (WINAPI *GetModuleHandle_t)(LPCTSTR); +typedef FARPROC (WINAPI *GetProcAddress_t)(HMODULE, LPCSTR); +typedef int (__stdcall *JVM_EnqueueOperation_t)(char *, char *, char *, char *, char *); + +typedef struct { + GetModuleHandle_t GetModuleHandleA; + GetProcAddress_t GetProcAddress; + char library[32]; + char command[32]; + char commandFallback[32]; + char pipe[MAX_PATH]; + char argument[4][MAX_ARGUMENT]; +} EnqueueOperation; + +#pragma check_stack(off) + +/** + * Executes the attachment on the remote thread. This method is executed on the target JVM and must not reference any addresses unknown to that address. + * + * @param argument The argument provided by the JVM executing the attachment. + * @return The result of the attachment. + */ +DWORD WINAPI execute_remote_attach + (LPVOID argument) +{ + EnqueueOperation *operation = (EnqueueOperation *) argument; + HMODULE library = operation->GetModuleHandleA(operation->library); + if (library == NULL) { + return OPEN_JVM_ERROR; + } + JVM_EnqueueOperation_t JVM_EnqueueOperation = (JVM_EnqueueOperation_t) operation->GetProcAddress(library, operation->command); + if (JVM_EnqueueOperation == NULL) { + JVM_EnqueueOperation = (JVM_EnqueueOperation_t) operation->GetProcAddress(library, operation->commandFallback); + } + if (JVM_EnqueueOperation == NULL) { + return GET_ENQUEUE_FUNCTION_ERROR; + } + return (DWORD) JVM_EnqueueOperation(operation->argument[0], + operation->argument[1], + operation->argument[2], + operation->argument[3], + operation->pipe); +} + +#pragma check_stack + +/** + * Allocates the code to execute on the remote machine. + * + * @param process The process handle of the remote process to which to attach. + * @return A pointer to the allocated code or {@code NULL} if the allocation failed. + */ +LPVOID allocate_remote_code + (HANDLE process) +{ + LPVOID code = VirtualAllocEx(process, NULL, CODE_SIZE, MEM_COMMIT, PAGE_EXECUTE_READWRITE); + if (code == NULL) { + return NULL; + } else if (!WriteProcessMemory(process, code, execute_remote_attach, CODE_SIZE, NULL)) { + VirtualFreeEx(process, code, 0, MEM_RELEASE); + return NULL; + } else { + return code; + } +} + +/** + * Allocates the argument to the remote execution. + * + * @param process A handle to the remote process to which to attach. + * @param pipe The name of the pipe to which the attachment result is written + * @param argument0 The first argument to provide to the {@code JVM_EnqueueOperation}. + * @param argument1 The second argument to provide to the {@code JVM_EnqueueOperation}. + * @param argument2 The third argument to provide to the {@code JVM_EnqueueOperation}. + * @param argument3 The forth argument to provide to the {@code JVM_EnqueueOperation}. + * @return A pointer to the allocated argument or {@code NULL} if the allocation was not possible. + */ +LPVOID allocate_remote_argument + (HANDLE process, LPCSTR pipe, LPCSTR argument0, LPCSTR argument1, LPCSTR argument2, LPCSTR argument3) +{ + if (strlen(pipe) >= MAX_PATH + || argument0 != NULL && strlen(argument0) >= MAX_ARGUMENT + || argument1 != NULL && strlen(argument1) >= MAX_ARGUMENT + || argument2 != NULL && strlen(argument2) >= MAX_ARGUMENT + || argument3 != NULL && strlen(argument3) >= MAX_ARGUMENT) { + return NULL; + } + EnqueueOperation operation; + operation.GetModuleHandleA = GetModuleHandleA; + operation.GetProcAddress = GetProcAddress; + strcpy(operation.library, "jvm"); + strcpy(operation.command, "JVM_EnqueueOperation"); + strcpy(operation.commandFallback, "_JVM_EnqueueOperation@20"); + strcpy(operation.pipe, pipe); + strcpy(operation.argument[0], argument0 == NULL ? "" : argument0); + strcpy(operation.argument[1], argument1 == NULL ? "" : argument1); + strcpy(operation.argument[2], argument2 == NULL ? "" : argument2); + strcpy(operation.argument[3], argument3 == NULL ? "" : argument3); + LPVOID allocation = VirtualAllocEx(process, NULL, sizeof(EnqueueOperation), MEM_COMMIT, PAGE_READWRITE); + if (allocation == NULL) { + return NULL; + } else if (!WriteProcessMemory(process, allocation, &operation, sizeof(operation), NULL)) { + VirtualFreeEx(process, allocation, 0, MEM_RELEASE); + return NULL; + } else { + return allocation; + } +} diff --git a/memshell-agent/memshell-agent-attacher/src/main/java/Agent.java b/memshell-agent/memshell-agent-attacher/src/main/java/Agent.java new file mode 100644 index 00000000..54372ce3 --- /dev/null +++ b/memshell-agent/memshell-agent-attacher/src/main/java/Agent.java @@ -0,0 +1,15 @@ +import java.lang.instrument.Instrumentation; + +/** + * @author ReaJason + * @since 2025/5/22 + */ +public class Agent { + public static void premain(String args, Instrumentation inst) { + System.out.println("hello premain"); + } + + public static void agentmain(String args, Instrumentation inst) { + System.out.println("hello agentmain"); + } +} diff --git a/memshell-agent/memshell-agent-attacher/src/main/java/Attacher.java b/memshell-agent/memshell-agent-attacher/src/main/java/Attacher.java new file mode 100644 index 00000000..e9af79c5 --- /dev/null +++ b/memshell-agent/memshell-agent-attacher/src/main/java/Attacher.java @@ -0,0 +1,931 @@ +/* + * Copyright 2014 - Present Rafael Winterhalter + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import java.io.File; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.MalformedURLException; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLClassLoader; +import java.security.CodeSource; +import java.security.PrivilegedAction; +import java.security.ProtectionDomain; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * Copy from Byte Buddy + */ +public class Attacher { + + /** + * Representation of the bootstrap {@link java.lang.ClassLoader}. + */ + private static final ClassLoader BOOTSTRAP_CLASS_LOADER = null; + + /** + * The character that is used to mark the beginning of the argument to the agent. + */ + private static final String AGENT_ARGUMENT_SEPARATOR = "="; + + /** + * The agent provides only {@code static} utility methods and should not be instantiated. + */ + private Attacher() { + throw new UnsupportedOperationException("This class is a utility class and not supposed to be instantiated"); + } + + /** + *

+ * Attaches the given agent Jar on the target process which must be a virtual machine process. The default attachment provider + * is used for applying the attachment. This operation blocks until the attachment is complete. If the current VM does not supply + * any known form of attachment to a remote VM, an {@link IllegalStateException} is thrown. The agent is not provided an argument. + *

+ *

+ * Important: It is only possible to attach to processes that are executed by the same operating system user. + *

+ * + * @param agentJar The agent jar file. + * @param processId The target process id. + */ + public static void attach(File agentJar, String processId) { + attach(agentJar, processId, null); + } + + public static void attach(String processId) { + attach(trySelfResolve(), processId, null); + } + + /** + *

+ * Attaches the given agent Jar on the target process which must be a virtual machine process. The default attachment provider + * is used for applying the attachment. This operation blocks until the attachment is complete. If the current VM does not supply + * any known form of attachment to a remote VM, an {@link IllegalStateException} is thrown. + *

+ *

+ * Important: It is only possible to attach to processes that are executed by the same operating system user. + *

+ * + * @param agentJar The agent jar file. + * @param processId The target process id. + * @param argument The argument to provide to the agent. + */ + public static void attach(File agentJar, String processId, String argument) { + install(processId, argument, new AgentProvider.ForExistingAgent(agentJar)); + } + + /** + * Installs a Java agent on a target VM. + * + * @param processId The process id of the target JVM process. + * @param argument The argument to provide to the agent. + * @param agentProvider The agent provider for the agent jar or library. + */ + private static void install(String processId, String argument, AgentProvider agentProvider) { + AttachmentProvider.Accessor attachmentAccessor = AttachmentProvider.DEFAULT.attempt(); + if (!attachmentAccessor.isAvailable()) { + throw new IllegalStateException("No compatible attachment provider is available"); + } + try { + Class virtualMachineType = attachmentAccessor.getVirtualMachineType(); + String agent = agentProvider.resolve().getAbsolutePath(); + Object virtualMachineInstance = virtualMachineType + .getMethod("attach", String.class) + .invoke(null, processId); + try { + virtualMachineType + .getMethod("loadAgent", String.class, String.class) + .invoke(virtualMachineInstance, agent, argument); + } finally { + virtualMachineType + .getMethod("detach") + .invoke(virtualMachineInstance); + } + } catch (RuntimeException exception) { + throw exception; + } catch (Exception exception) { + throw new IllegalStateException("Error during attachment using: " + AttachmentProvider.DEFAULT, exception); + } + } + + /** + * Attempts to resolve the location of the {@link Attacher} class for a self-attachment. Doing so avoids the creation of a temporary jar file. + * + * @return The self-resolved jar file or {@code null} if the jar file cannot be located. + */ + private static File trySelfResolve() { + try { + ProtectionDomain protectionDomain = Attacher.class.getProtectionDomain(); + if (protectionDomain == null) { + return null; + } + CodeSource codeSource = protectionDomain.getCodeSource(); + if (codeSource == null) { + return null; + } + URL location = codeSource.getLocation(); + if (!location.getProtocol().equals("file")) { + return null; + } + try { + File file = new File(location.toURI()); + if (file.getPath().contains(AGENT_ARGUMENT_SEPARATOR)) { + return null; + } + return file; + } catch (URISyntaxException ignored) { + return new File(location.getPath()); + } + } catch (Exception ignored) { + return null; + } + } + + /** + * An attachment provider is responsible for making the Java attachment API available. + */ + public interface AttachmentProvider { + + /** + * The default attachment provider to be used. + */ + AttachmentProvider DEFAULT = new Compound(ForModularizedVm.INSTANCE, + ForJ9Vm.INSTANCE, + ForStandardToolsJarVm.JVM_ROOT, + ForStandardToolsJarVm.JDK_ROOT, + ForStandardToolsJarVm.MACINTOSH, + ForUserDefinedToolsJar.INSTANCE, + ForEmulatedAttachment.INSTANCE); + + /** + * Attempts the creation of an accessor for a specific JVM's attachment API. + * + * @return The accessor this attachment provider can supply for the currently running JVM. + */ + Accessor attempt(); + + /** + * An accessor for a JVM's attachment API. + */ + interface Accessor { + + /** + * The name of the {@code VirtualMachine} class on any OpenJDK or Oracle JDK implementation. + */ + String VIRTUAL_MACHINE_TYPE_NAME = "com.sun.tools.attach.VirtualMachine"; + + /** + * The name of the {@code VirtualMachine} class on IBM J9 VMs. + */ + String VIRTUAL_MACHINE_TYPE_NAME_J9 = "com.ibm.tools.attach.VirtualMachine"; + + /** + * Determines if this accessor is applicable for the currently running JVM. + * + * @return {@code true} if this accessor is available. + */ + boolean isAvailable(); + + /** + * Returns {@code true} if this accessor prohibits attachment to the same virtual machine in Java 9 and later. + * + * @return {@code true} if this accessor prohibits attachment to the same virtual machine in Java 9 and later. + */ + boolean isExternalAttachmentRequired(); + + /** + * Returns a {@code VirtualMachine} class. This method must only be called for available accessors. + * + * @return The virtual machine type. + */ + Class getVirtualMachineType(); + + /** + * Returns a description of a virtual machine class for an external attachment. + * + * @return A description of the external attachment. + */ + ExternalAttachment getExternalAttachment(); + + /** + * A canonical implementation of an unavailable accessor. + */ + enum Unavailable implements Accessor { + + /** + * The singleton instance. + */ + INSTANCE; + + /** + * {@inheritDoc} + */ + public boolean isAvailable() { + return false; + } + + /** + * {@inheritDoc} + */ + public boolean isExternalAttachmentRequired() { + throw new IllegalStateException("Cannot read the virtual machine type for an unavailable accessor"); + } + + /** + * {@inheritDoc} + */ + public Class getVirtualMachineType() { + throw new IllegalStateException("Cannot read the virtual machine type for an unavailable accessor"); + } + + /** + * {@inheritDoc} + */ + public ExternalAttachment getExternalAttachment() { + throw new IllegalStateException("Cannot read the virtual machine type for an unavailable accessor"); + } + } + + /** + * Describes an external attachment to a Java virtual machine. + */ + class ExternalAttachment { + + /** + * The fully-qualified binary name of the virtual machine type. + */ + private final String virtualMachineType; + + /** + * The class path elements required for loading the supplied virtual machine type. + */ + private final List classPath; + + /** + * Creates an external attachment. + * + * @param virtualMachineType The fully-qualified binary name of the virtual machine type. + * @param classPath The class path elements required for loading the supplied virtual machine type. + */ + public ExternalAttachment(String virtualMachineType, List classPath) { + this.virtualMachineType = virtualMachineType; + this.classPath = classPath; + } + + /** + * Returns the fully-qualified binary name of the virtual machine type. + * + * @return The fully-qualified binary name of the virtual machine type. + */ + public String getVirtualMachineType() { + return virtualMachineType; + } + + /** + * Returns the class path elements required for loading the supplied virtual machine type. + * + * @return The class path elements required for loading the supplied virtual machine type. + */ + public List getClassPath() { + return classPath; + } + } + + /** + * A simple implementation of an accessible accessor. + */ + abstract class Simple implements Accessor { + + /** + * A {@code VirtualMachine} class. + */ + protected final Class virtualMachineType; + + /** + * Creates a new simple accessor. + * + * @param virtualMachineType A {@code VirtualMachine} class. + */ + protected Simple(Class virtualMachineType) { + this.virtualMachineType = virtualMachineType; + } + + /** + *

+ * Creates an accessor by reading the process id from the JMX runtime bean and by attempting + * to load the {@code com.sun.tools.attach.VirtualMachine} class from the provided class loader. + *

+ *

+ * This accessor is supposed to work on any implementation of the OpenJDK or Oracle JDK. + *

+ * + * @param classLoader A class loader that is capable of loading the virtual machine type. + * @param classPath The class path required to load the virtual machine class. + * @return An appropriate accessor. + */ + public static Accessor of(ClassLoader classLoader, File... classPath) { + try { + return new Simple.WithExternalAttachment(Class.forName(VIRTUAL_MACHINE_TYPE_NAME, + false, + classLoader), Arrays.asList(classPath)); + } catch (ClassNotFoundException ignored) { + return Unavailable.INSTANCE; + } + } + + /** + *

+ * Creates an accessor by reading the process id from the JMX runtime bean and by attempting + * to load the {@code com.ibm.tools.attach.VirtualMachine} class from the provided class loader. + *

+ *

+ * This accessor is supposed to work on any implementation of IBM's J9. + *

+ * + * @return An appropriate accessor. + */ + public static Accessor ofJ9() { + try { + return new Simple.WithExternalAttachment(ClassLoader.getSystemClassLoader().loadClass(VIRTUAL_MACHINE_TYPE_NAME_J9), + Collections.emptyList()); + } catch (ClassNotFoundException ignored) { + return Unavailable.INSTANCE; + } + } + + /** + * {@inheritDoc} + */ + public boolean isAvailable() { + return true; + } + + /** + * {@inheritDoc} + */ + public Class getVirtualMachineType() { + return virtualMachineType; + } + + /** + * A simple implementation of an accessible accessor that allows for external attachment. + */ + protected static class WithExternalAttachment extends Simple { + + /** + * The class path required for loading the virtual machine type. + */ + private final List classPath; + + /** + * Creates a new simple accessor that allows for external attachment. + * + * @param virtualMachineType The {@code com.sun.tools.attach.VirtualMachine} class. + * @param classPath The class path required for loading the virtual machine type. + */ + public WithExternalAttachment(Class virtualMachineType, List classPath) { + super(virtualMachineType); + this.classPath = classPath; + } + + /** + * {@inheritDoc} + */ + public boolean isExternalAttachmentRequired() { + return true; + } + + /** + * {@inheritDoc} + */ + public ExternalAttachment getExternalAttachment() { + return new ExternalAttachment(virtualMachineType.getName(), classPath); + } + } + + /** + * A simple implementation of an accessible accessor that attaches using a virtual machine emulation that does not require external attachment. + */ + protected static class WithDirectAttachment extends Simple { + + /** + * Creates a new simple accessor that implements direct attachment. + * + * @param virtualMachineType A {@code VirtualMachine} class. + */ + public WithDirectAttachment(Class virtualMachineType) { + super(virtualMachineType); + } + + /** + * {@inheritDoc} + */ + public boolean isExternalAttachmentRequired() { + return false; + } + + /** + * {@inheritDoc} + */ + public ExternalAttachment getExternalAttachment() { + throw new IllegalStateException("Cannot apply external attachment"); + } + } + } + } + + /** + * An attachment provider that locates the attach API directly from the system class loader, as possible since + * introducing the Java module system via the {@code jdk.attach} module. + */ + enum ForModularizedVm implements AttachmentProvider { + + /** + * The singleton instance. + */ + INSTANCE; + + /** + * {@inheritDoc} + */ + public Accessor attempt() { + return Accessor.Simple.of(ClassLoader.getSystemClassLoader()); + } + } + + /** + * An attachment provider that locates the attach API directly from the system class loader expecting + * an IBM J9 VM. + */ + enum ForJ9Vm implements AttachmentProvider { + + /** + * The singleton instance. + */ + INSTANCE; + + /** + * {@inheritDoc} + */ + public Accessor attempt() { + return Accessor.Simple.ofJ9(); + } + } + + /** + * An attachment provider that is dependant on the existence of a tools.jar file on the local + * file system. + */ + enum ForStandardToolsJarVm implements AttachmentProvider { + + /** + * An attachment provider that locates the tools.jar from a Java home directory. + */ + JVM_ROOT("../lib/tools.jar"), + + /** + * An attachment provider that locates the tools.jar from a Java installation directory. + * In practice, several virtual machines do not return the JRE's location for the + * java.home property against the property's specification. + */ + JDK_ROOT("lib/tools.jar"), + + /** + * An attachment provider that locates the tools.jar as it is set for several JVM + * installations on Apple Macintosh computers. + */ + MACINTOSH("../Classes/classes.jar"); + + /** + * The Java home system property. + */ + private static final String JAVA_HOME_PROPERTY = "java.home"; + + /** + * The path to the tools.jar file, starting from the Java home directory. + */ + private final String toolsJarPath; + + /** + * Creates a new attachment provider that loads the virtual machine class from the tools.jar. + * + * @param toolsJarPath The path to the tools.jar file, starting from the Java home directory. + */ + ForStandardToolsJarVm(String toolsJarPath) { + this.toolsJarPath = toolsJarPath; + } + + /** + * {@inheritDoc} + */ + public Accessor attempt() { + File toolsJar = new File(System.getProperty(JAVA_HOME_PROPERTY), toolsJarPath); + try { + return toolsJar.isFile() && toolsJar.canRead() + ? Accessor.Simple.of(new URLClassLoader(new URL[]{toolsJar.toURI().toURL()}, BOOTSTRAP_CLASS_LOADER), toolsJar) + : Accessor.Unavailable.INSTANCE; + } catch (MalformedURLException exception) { + throw new IllegalStateException("Could not represent " + toolsJar + " as URL"); + } + } + } + + /** + * An attachment provider that attempts to locate a {@code tools.jar} from a custom location set via a system property. + */ + enum ForUserDefinedToolsJar implements AttachmentProvider { + + /** + * The singelton instance. + */ + INSTANCE; + + /** + * The property being read for locating {@code tools.jar}. + */ + public static final String PROPERTY = "net.bytebuddy.agent.toolsjar"; + + /** + * {@inheritDoc} + */ + public Accessor attempt() { + String location = System.getProperty(PROPERTY); + if (location == null) { + return Accessor.Unavailable.INSTANCE; + } else { + File toolsJar = new File(location); + try { + return Accessor.Simple.of(new URLClassLoader(new URL[]{toolsJar.toURI().toURL()}, BOOTSTRAP_CLASS_LOADER), toolsJar); + } catch (MalformedURLException exception) { + throw new IllegalStateException("Could not represent " + toolsJar + " as URL"); + } + } + } + } + + /** + * An attachment provider that uses Byte Buddy's attachment API emulation. To use this feature, JNA is required. + */ + enum ForEmulatedAttachment implements AttachmentProvider { + + /** + * The singleton instance. + */ + INSTANCE; + + /** + * {@inheritDoc} + */ + public Accessor attempt() { + try { + return new Accessor.Simple.WithDirectAttachment(VirtualMachine.Resolver.INSTANCE.get()); + } catch (Throwable ignored) { + return Accessor.Unavailable.INSTANCE; + } + } + } + + /** + * A compound attachment provider that attempts the attachment by delegation to other providers. If + * none of the providers of this compound provider is capable of providing a valid accessor, an + * non-available accessor is returned. + */ + class Compound implements AttachmentProvider { + + /** + * A list of attachment providers in the order of their application. + */ + private final List attachmentProviders; + + /** + * Creates a new compound attachment provider. + * + * @param attachmentProvider A list of attachment providers in the order of their application. + */ + public Compound(AttachmentProvider... attachmentProvider) { + this(Arrays.asList(attachmentProvider)); + } + + /** + * Creates a new compound attachment provider. + * + * @param attachmentProviders A list of attachment providers in the order of their application. + */ + public Compound(List attachmentProviders) { + this.attachmentProviders = new ArrayList(); + for (AttachmentProvider attachmentProvider : attachmentProviders) { + if (attachmentProvider instanceof Compound) { + this.attachmentProviders.addAll(((Compound) attachmentProvider).attachmentProviders); + } else { + this.attachmentProviders.add(attachmentProvider); + } + } + } + + /** + * {@inheritDoc} + */ + public Accessor attempt() { + for (AttachmentProvider attachmentProvider : attachmentProviders) { + Accessor accessor = attachmentProvider.attempt(); + if (accessor.isAvailable()) { + return accessor; + } + } + return Accessor.Unavailable.INSTANCE; + } + } + } + + /** + * A process provider is responsible for providing the process id of the current VM. + */ + public interface ProcessProvider { + + /** + * Resolves a process id for the current JVM. + * + * @return The resolved process id. + */ + String resolve(); + + /** + * Supplies the current VM's process id. + */ + enum ForCurrentVm implements ProcessProvider { + + /** + * The singleton instance. + */ + INSTANCE; + + /** + * The best process provider for the current VM. + */ + private final ProcessProvider dispatcher; + + /** + * Creates a process provider that supplies the current VM's process id. + */ + ForCurrentVm() { + dispatcher = ForJava9CapableVm.make(); + } + + /** + * {@inheritDoc} + */ + public String resolve() { + return dispatcher.resolve(); + } + + /** + * A process provider for a legacy VM that reads the process id from its JMX properties. This strategy + * is only used prior to Java 9 such that the java.management module never is resolved, even if + * the module system is used, as the module system was not available in any relevant JVM version. + */ + protected enum ForLegacyVm implements ProcessProvider { + + /** + * The singleton instance. + */ + INSTANCE; + + /** + * {@inheritDoc} + */ + public String resolve() { + String runtimeName; + try { + Method method = Class.forName("java.lang.management.ManagementFactory").getMethod("getRuntimeMXBean"); + runtimeName = (String) method.getReturnType().getMethod("getName").invoke(method.invoke(null)); + } catch (Exception exception) { + throw new IllegalStateException("Failed to access VM name via management factory", exception); + } + int processIdIndex = runtimeName.indexOf('@'); + if (processIdIndex == -1) { + throw new IllegalStateException("Cannot extract process id from runtime management bean"); + } else { + return runtimeName.substring(0, processIdIndex); + } + } + } + + /** + * A process provider for a Java 9 capable VM with access to the introduced process API. + */ + protected static class ForJava9CapableVm implements ProcessProvider { + + /** + * The {@code java.lang.ProcessHandle#current()} method. + */ + private final Method current; + + /** + * The {@code java.lang.ProcessHandle#pid()} method. + */ + private final Method pid; + + /** + * Creates a new Java 9 capable dispatcher for reading the current process's id. + * + * @param current The {@code java.lang.ProcessHandle#current()} method. + * @param pid The {@code java.lang.ProcessHandle#pid()} method. + */ + protected ForJava9CapableVm(Method current, Method pid) { + this.current = current; + this.pid = pid; + } + + /** + * Attempts to create a dispatcher for a Java 9 VM and falls back to a legacy dispatcher + * if this is not possible. + * + * @return A dispatcher for the current VM. + */ + public static ProcessProvider make() { + try { + return new ForJava9CapableVm(Class.forName("java.lang.ProcessHandle").getMethod("current"), + Class.forName("java.lang.ProcessHandle").getMethod("pid")); + } catch (Exception ignored) { + return ForLegacyVm.INSTANCE; + } + } + + /** + * {@inheritDoc} + */ + public String resolve() { + try { + return pid.invoke(current.invoke(null)).toString(); + } catch (IllegalAccessException exception) { + throw new IllegalStateException("Cannot access Java 9 process API", exception); + } catch (InvocationTargetException exception) { + throw new IllegalStateException("Error when accessing Java 9 process API", exception.getTargetException()); + } + } + } + } + } + + /** + * An agent provider is responsible for handling and providing the jar file of an agent that is being attached. + */ + protected interface AgentProvider { + + /** + * Provides an agent jar file for attachment. + * + * @return The provided agent. + * @throws IOException If the agent cannot be written to disk. + */ + File resolve() throws IOException; + + /** + * An agent provider that supplies an existing agent that is not deleted after attachment. + */ + class ForExistingAgent implements AgentProvider { + + /** + * The supplied agent. + */ + private final File agent; + + /** + * Creates an agent provider for an existing agent. + * + * @param agent The supplied agent. + */ + protected ForExistingAgent(File agent) { + this.agent = agent; + } + + /** + * {@inheritDoc} + */ + public File resolve() { + return agent; + } + } + } + + /** + * An attachment evaluator is responsible for deciding if an agent can be attached from the current process. + */ + protected interface AttachmentTypeEvaluator { + + /** + * Checks if the current VM requires external attachment for the supplied process id. + * + * @param processId The process id of the process to which to attach. + * @return {@code true} if the current VM requires external attachment for the supplied process. + */ + boolean requiresExternalAttachment(String processId); + + /** + * An installation action for creating an attachment type evaluator. + */ + enum InstallationAction implements PrivilegedAction { + + /** + * The singleton instance. + */ + INSTANCE; + + /** + * The OpenJDK's property for specifying the legality of self-attachment. + */ + private static final String JDK_ALLOW_SELF_ATTACH = "jdk.attach.allowAttachSelf"; + + /** + * {@inheritDoc} + */ + public AttachmentTypeEvaluator run() { + try { + if (Boolean.getBoolean(JDK_ALLOW_SELF_ATTACH)) { + return Disabled.INSTANCE; + } else { + return new ForJava9CapableVm(Class.forName("java.lang.ProcessHandle").getMethod("current"), + Class.forName("java.lang.ProcessHandle").getMethod("pid")); + } + } catch (Exception ignored) { + return Disabled.INSTANCE; + } + } + } + + /** + * An attachment type evaluator that never requires external attachment. + */ + enum Disabled implements AttachmentTypeEvaluator { + + /** + * The singleton instance. + */ + INSTANCE; + + /** + * {@inheritDoc} + */ + public boolean requiresExternalAttachment(String processId) { + return false; + } + } + + /** + * An attachment type evaluator that checks a process id against the current process id. + */ + class ForJava9CapableVm implements AttachmentTypeEvaluator { + + /** + * The {@code java.lang.ProcessHandle#current()} method. + */ + private final Method current; + + /** + * The {@code java.lang.ProcessHandle#pid()} method. + */ + private final Method pid; + + /** + * Creates a new attachment type evaluator. + * + * @param current The {@code java.lang.ProcessHandle#current()} method. + * @param pid The {@code java.lang.ProcessHandle#pid()} method. + */ + protected ForJava9CapableVm(Method current, Method pid) { + this.current = current; + this.pid = pid; + } + + /** + * {@inheritDoc} + */ + public boolean requiresExternalAttachment(String processId) { + try { + return pid.invoke(current.invoke(null)).toString().equals(processId); + } catch (IllegalAccessException exception) { + throw new IllegalStateException("Cannot access Java 9 process API", exception); + } catch (InvocationTargetException exception) { + throw new IllegalStateException("Error when accessing Java 9 process API", exception.getTargetException()); + } + } + } + } +} diff --git a/memshell-agent/memshell-agent-attacher/src/main/java/Main.java b/memshell-agent/memshell-agent-attacher/src/main/java/Main.java new file mode 100644 index 00000000..e25bf1b5 --- /dev/null +++ b/memshell-agent/memshell-agent-attacher/src/main/java/Main.java @@ -0,0 +1,9 @@ +/** + * @author ReaJason + * @since 2025/5/16 + */ +public class Main { + public static void main(String[] args) throws Exception { + Attacher.attach(args[0]); + } +} diff --git a/memshell-agent/memshell-agent-attacher/src/main/java/VirtualMachine.java b/memshell-agent/memshell-agent-attacher/src/main/java/VirtualMachine.java new file mode 100644 index 00000000..91935b1c --- /dev/null +++ b/memshell-agent/memshell-agent-attacher/src/main/java/VirtualMachine.java @@ -0,0 +1,2888 @@ +/* + * Copyright 2014 - Present Rafael Winterhalter + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import com.sun.jna.*; +import com.sun.jna.platform.win32.*; +import com.sun.jna.ptr.IntByReference; +import com.sun.jna.win32.StdCallLibrary; +import com.sun.jna.win32.W32APIOptions; + +import java.io.*; +import java.net.ServerSocket; +import java.net.Socket; +import java.nio.ByteBuffer; +import java.nio.channels.FileLock; +import java.security.PrivilegedAction; +import java.security.SecureRandom; +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + *

+ * An implementation for attachment on a virtual machine. This interface mimics the tooling API's virtual + * machine interface to allow for similar usage by where all calls are made via + * reflection such that this structural typing suffices for interoperability. + *

+ *

+ * Note: Implementations are required to declare a static method {@code attach(String)} returning an + * instance of a class that declares the methods defined by {@link VirtualMachine}. + *

+ */ +public interface VirtualMachine { + + /** + * Loads the target VMs system properties. + * + * @return The target VM properties. + * @throws IOException If an I/O exception occurs. + */ + Properties getSystemProperties() throws IOException; + + /** + * Loads the target VMs agent properties. + * + * @return The target VM properties. + * @throws IOException If an I/O exception occurs. + */ + Properties getAgentProperties() throws IOException; + + /** + * Loads an agent into the represented virtual machine. + * + * @param jarFile The jar file to attach. + * @throws IOException If an I/O exception occurs. + */ + void loadAgent(String jarFile) throws IOException; + + /** + * Loads an agent into the represented virtual machine. + * + * @param jarFile The jar file to attach. + * @param argument The argument to provide or {@code null} if no argument should be provided. + * @throws IOException If an I/O exception occurs. + */ + void loadAgent(String jarFile, String argument) throws IOException; + + /** + * Loads a native agent into the represented virtual machine. + * + * @param path The agent path. + * @throws IOException If an I/O exception occurs. + */ + void loadAgentPath(String path) throws IOException; + + /** + * Loads a native agent into the represented virtual machine. + * + * @param path The agent path. + * @param argument The argument to provide or {@code null} if no argument should be provided. + * @throws IOException If an I/O exception occurs. + */ + void loadAgentPath(String path, String argument) throws IOException; + + /** + * Loads a native agent library into the represented virtual machine. + * + * @param library The agent library. + * @throws IOException If an I/O exception occurs. + */ + void loadAgentLibrary(String library) throws IOException; + + /** + * Loads a native agent library into the represented virtual machine. + * + * @param library The agent library. + * @param argument The argument to provide or {@code null} if no argument should be provided. + * @throws IOException If an I/O exception occurs. + */ + void loadAgentLibrary(String library, String argument) throws IOException; + + /** + * Starts a JMX management agent. + * + * @param properties The properties to transfer to the JMX agent. + * @throws IOException If an I/O error occurs. + */ + void startManagementAgent(Properties properties) throws IOException; + + /** + * Starts a local management agent. + * + * @return The local connector address. + * @throws IOException If an I/O error occurs. + */ + String startLocalManagementAgent() throws IOException; + + /** + * Detaches this virtual machine representation. + * + * @throws IOException If an I/O exception occurs. + */ + void detach() throws IOException; + + /** + * A resolver for the current VM's virtual machine attachment emulation. + */ + enum Resolver { + + /** + * The singleton instance. + */ + INSTANCE; + + /** + * {@inheritDoc} + */ + public Class get() { + try { + Class.forName("com.sun.jna.Platform"); + } catch (ClassNotFoundException exception) { + throw new IllegalStateException("Optional JNA dependency is not available", exception); + } + return System.getProperty("java.vm.name", "").toUpperCase(Locale.US).contains("J9") + ? ForOpenJ9.class + : ForHotSpot.class; + + } + } + + /** + * An abstract base implementation for a virtual machine. + */ + abstract class AbstractBase implements VirtualMachine { + + /** + * {@inheritDoc} + */ + public void loadAgent(String jarFile) throws IOException { + loadAgent(jarFile, null); + } + + /** + * {@inheritDoc} + */ + public void loadAgentPath(String path) throws IOException { + loadAgentPath(path, null); + } + + /** + * {@inheritDoc} + */ + public void loadAgentLibrary(String library) throws IOException { + loadAgentLibrary(library, null); + } + } + + /** + * A virtual machine attachment implementation for a HotSpot VM or any compatible JVM. + */ + class ForHotSpot extends AbstractBase { + + /** + * The protocol version to use for communication. + */ + private static final String PROTOCOL_VERSION = "1"; + + /** + * The {@code load} command. + */ + private static final String LOAD_COMMAND = "load"; + + /** + * The {@code instrument} command. + */ + private static final String INSTRUMENT_COMMAND = "instrument"; + + /** + * A delimiter to be used for attachment. + */ + private static final String ARGUMENT_DELIMITER = "="; + + /** + * The virtual machine connection. + */ + private final Connection connection; + + /** + * Creates a new virtual machine connection for HotSpot. + * + * @param connection The virtual machine connection. + */ + protected ForHotSpot(Connection connection) { + this.connection = connection; + } + + /** + * Attaches to the supplied process id using the default JNA implementation. + * + * @param processId The process id. + * @return A suitable virtual machine implementation. + * @throws IOException If an IO exception occurs during establishing the connection. + */ + public static VirtualMachine attach(String processId) throws IOException { + if (Platform.isWindows()) { + return attach(processId, new Connection.ForJnaWindowsNamedPipe.Factory()); + } else if (Platform.isSolaris()) { + return attach(processId, new Connection.ForJnaSolarisDoor.Factory(15, 100, TimeUnit.MILLISECONDS)); + } else { + return attach(processId, Connection.ForJnaPosixSocket.Factory.withDefaultTemporaryFolder(15, 100, TimeUnit.MILLISECONDS)); + } + } + + /** + * Attaches to the supplied process id using the supplied connection factory. + * + * @param processId The process id. + * @param connectionFactory The connection factory to use. + * @return A suitable virtual machine implementation. + * @throws IOException If an IO exception occurs during establishing the connection. + */ + public static VirtualMachine attach(String processId, Connection.Factory connectionFactory) throws IOException { + return new ForHotSpot(connectionFactory.connect(processId)); + } + + /** + * Checks the header of a response. + * + * @param response The response to check the header for. + * @throws IOException If an I/O exception occurs. + */ + private static void checkHeader(Connection.Response response) throws IOException { + byte[] buffer = new byte[1]; + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + int length; + while ((length = response.read(buffer)) != -1) { + if (length > 0) { + if (buffer[0] == '\n') { + break; + } + outputStream.write(buffer[0]); + } + } + switch (Integer.parseInt(outputStream.toString("UTF-8"))) { + case 0: + return; + case 101: + throw new IOException("Protocol mismatch with target VM"); + default: + buffer = new byte[1024]; + outputStream = new ByteArrayOutputStream(); + while ((length = response.read(buffer)) != -1) { + outputStream.write(buffer, 0, length); + } + throw new IllegalStateException(outputStream.toString("UTF-8")); + } + } + + /** + * {@inheritDoc} + */ + public Properties getSystemProperties() throws IOException { + return getProperties("properties"); + } + + /** + * {@inheritDoc} + */ + public Properties getAgentProperties() throws IOException { + return getProperties("agentProperties"); + } + + /** + * Loads properties of the target VM. + * + * @param command The command for fetching properties. + * @return The read properties. + * @throws IOException If an I/O exception occurs. + */ + private Properties getProperties(String command) throws IOException { + Connection.Response response = connection.execute(PROTOCOL_VERSION, command, null, null, null); + try { + checkHeader(response); + byte[] buffer = new byte[1024]; + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + int length; + while ((length = response.read(buffer)) != -1) { + outputStream.write(buffer, 0, length); + } + Properties properties = new Properties(); + properties.load(new ByteArrayInputStream(outputStream.toByteArray())); + return properties; + } finally { + response.close(); + } + } + + /** + * {@inheritDoc} + */ + public void loadAgent(String jarFile, String argument) throws IOException { + load(jarFile, false, argument); + } + + /** + * {@inheritDoc} + */ + public void loadAgentPath(String path, String argument) throws IOException { + load(path, true, argument); + } + + /** + * {@inheritDoc} + */ + public void loadAgentLibrary(String library, String argument) throws IOException { + load(library, false, argument); + } + + /** + * Loads an agent by the given command. + * + * @param file The Java agent or library to be loaded. + * @param absolute {@code true} if the agent location is absolute. + * @param argument The argument to the agent or {@code null} if no argument is given. + * @throws IOException If an I/O exception occurs. + */ + protected void load(String file, boolean absolute, String argument) throws IOException { + Connection.Response response = connection.execute(PROTOCOL_VERSION, LOAD_COMMAND, INSTRUMENT_COMMAND, Boolean.toString(absolute), (argument == null + ? file + : file + ARGUMENT_DELIMITER + argument)); + try { + checkHeader(response); + } finally { + response.close(); + } + } + + /** + * {@inheritDoc} + */ + public void startManagementAgent(Properties properties) throws IOException { + StringBuilder stringBuilder = new StringBuilder("ManagementAgent.start "); + boolean first = true; + for (Map.Entry entry : properties.entrySet()) { + if (!(entry.getKey() instanceof String) || !((String) entry.getKey()).startsWith("com.sun.management.")) { + throw new IllegalArgumentException("Illegal property name: " + entry.getKey()); + } else if (first) { + first = false; + } else { + stringBuilder.append(' '); + } + stringBuilder.append(((String) entry.getKey()).substring("com.sun.management.".length())).append('='); + String value = entry.getValue().toString(); + if (value.contains(" ")) { + stringBuilder.append('\'').append(value).append('\''); + } else { + stringBuilder.append(value); + } + } + Connection.Response response = connection.execute(PROTOCOL_VERSION, "jcmd", stringBuilder.toString(), null, null); + try { + checkHeader(response); + } finally { + response.close(); + } + } + + /** + * {@inheritDoc} + */ + public String startLocalManagementAgent() throws IOException { + Connection.Response response = connection.execute(PROTOCOL_VERSION, "jcmd", "ManagementAgent.start_local", null, null); + try { + checkHeader(response); + return getAgentProperties().getProperty("com.sun.management.jmxremote.localConnectorAddress"); + } finally { + response.close(); + } + } + + /** + * {@inheritDoc} + */ + public void detach() throws IOException { + connection.close(); + } + + /** + * Represents a connection to a virtual machine. + */ + public interface Connection extends Closeable { + + /** + * Executes a command on the current connection. + * + * @param protocol The target VMs protocol version for the attach API. + * @param argument The arguments to send to the target VM. + * @return The response of the target JVM. + * @throws IOException If an I/O error occurred. + */ + Response execute(String protocol, String... argument) throws IOException; + + /** + * A response to an execution command to a VM. + */ + interface Response extends Closeable { + + /** + * Reads a buffer from the target VM. + * + * @param buffer The buffer to read to. + * @return The bytes read or {@code -1} if no more bytes could be read. + * @throws IOException If an I/O exception occurred. + */ + int read(byte[] buffer) throws IOException; + } + + /** + * A factory for creating connections to virtual machines. + */ + interface Factory { + + /** + * Connects to the supplied process. + * + * @param processId The process id. + * @return The connection to the virtual machine with the supplied process id. + * @throws IOException If an I/O exception occurs during connecting to the targeted VM. + */ + Connection connect(String processId) throws IOException; + + /** + * A factory for attaching via a socket file. + */ + abstract class ForSocketFile implements Factory { + + /** + * The name prefix for a socket. + */ + private static final String SOCKET_FILE_PREFIX = ".java_pid"; + + /** + * The name prefix for an attachment file indicator. + */ + private static final String ATTACH_FILE_PREFIX = ".attach_pid"; + + /** + * The temporary directory to use. + */ + private final String temporaryDirectory; + + /** + * The maximum amount of attempts for checking the establishment of a socket connection. + */ + private final int attempts; + + /** + * The pause between two checks for an established socket connection. + */ + private final long pause; + + /** + * The time unit of the pause time. + */ + private final TimeUnit timeUnit; + + /** + * Creates a connection factory for creating a socket connection via a file. + * + * @param temporaryDirectory The temporary directory to use. + * @param attempts The maximum amount of attempts for checking the establishment of a socket connection. + * @param pause The pause between two checks for an established socket connection. + * @param timeUnit The time unit of the pause time. + */ + protected ForSocketFile(String temporaryDirectory, int attempts, long pause, TimeUnit timeUnit) { + this.temporaryDirectory = temporaryDirectory; + this.attempts = attempts; + this.pause = pause; + this.timeUnit = timeUnit; + } + + /** + * {@inheritDoc} + */ + public Connection connect(String processId) throws IOException { + File socket = new File(temporaryDirectory, SOCKET_FILE_PREFIX + processId); + if (!socket.exists()) { + String target = ATTACH_FILE_PREFIX + processId, path = "/proc/" + processId + "/cwd/" + target; + File attachFile = new File(path); + try { + if (!attachFile.createNewFile() && !attachFile.isFile()) { + throw new IllegalStateException("Could not create attach file: " + attachFile); + } + } catch (IOException ignored) { + attachFile = new File(temporaryDirectory, target); + if (!attachFile.createNewFile() && !attachFile.isFile()) { + throw new IllegalStateException("Could not create attach file: " + attachFile); + } + } + try { + kill(processId, 3); + int attempts = this.attempts; + while (!socket.exists() && attempts-- > 0) { + timeUnit.sleep(pause); + } + if (!socket.exists()) { + throw new IllegalStateException("Target VM did not respond: " + processId); + } + } catch (InterruptedException exception) { + Thread.currentThread().interrupt(); + throw new IllegalStateException(exception); + } finally { + if (!attachFile.delete()) { + attachFile.deleteOnExit(); + } + } + } + return doConnect(socket); + } + + /** + * Sends a kill signal to the target process. + * + * @param processId The process id. + * @param signal The signal to send. + */ + protected abstract void kill(String processId, int signal); + + /** + * Connects to the supplied POSIX socket. + * + * @param socket The socket to connect to. + * @return An active connection to the supplied socket. + * @throws IOException If an error occurs during connection. + */ + protected abstract Connection doConnect(File socket) throws IOException; + } + } + + /** + * A connection that is represented by a byte channel that is persistent during communication. + * + * @param The connection representation. + */ + abstract class OnPersistentByteChannel implements Connection { + + /** + * A blank line argument. + */ + private static final byte[] BLANK = new byte[]{0}; + + /** + * {@inheritDoc} + */ + public Connection.Response execute(String protocol, String... argument) throws IOException { + T connection = connect(); + try { + write(connection, protocol.getBytes("UTF-8")); + write(connection, BLANK); + for (String anArgument : argument) { + if (anArgument != null) { + write(connection, anArgument.getBytes("UTF-8")); + } + write(connection, BLANK); + } + return new Response(connection); + } catch (Throwable throwable) { + close(connection); + if (throwable instanceof RuntimeException) { + throw (RuntimeException) throwable; + } else if (throwable instanceof IOException) { + throw (IOException) throwable; + } else { + throw new IllegalStateException(throwable); + } + } + } + + /** + * A response of a persistent byte channel. + */ + private class Response implements Connection.Response { + + /** + * The connection representing this response. + */ + private final T connection; + + /** + * Creates a new response for a persistent byte channel. + * + * @param connection The connection representing this response. + */ + private Response(T connection) { + this.connection = connection; + } + + /** + * {@inheritDoc} + */ + public int read(byte[] buffer) throws IOException { + return OnPersistentByteChannel.this.read(connection, buffer); + } + + /** + * {@inheritDoc} + */ + public void close() throws IOException { + OnPersistentByteChannel.this.close(connection); + } + } + + /** + * Creates a new connection to the target VM. + * + * @return Returns a new connection to the target VM. + * @throws IOException If an I/O exception occurs. + */ + protected abstract T connect() throws IOException; + + /** + * Closes the connection to the target VM. + * + * @param connection The connection to close. + * @throws IOException If an I/O exception occurs. + */ + protected abstract void close(T connection) throws IOException; + + /** + * Writes to the target VM. + * + * @param connection The connection to write to. + * @param buffer The buffer to write to. + * @throws IOException If an I/O exception occurs during writing. + */ + protected abstract void write(T connection, byte[] buffer) throws IOException; + + /** + * Reads from the target VM. + * + * @param connection The connection to read from. + * @param buffer The buffer to store the result in. + * @return The number of byte that were read. + * @throws IOException If an I/O exception occurs. + */ + protected abstract int read(T connection, byte[] buffer) throws IOException; + } + + /** + * Implements a connection for a POSIX socket in JNA. + */ + class ForJnaPosixSocket extends OnPersistentByteChannel { + + /** + * The JNA library to use. + */ + private final PosixLibrary library; + + /** + * The POSIX socket. + */ + private final File socket; + + /** + * Creates a connection for a virtual POSIX socket implemented in JNA. + * + * @param library The JNA library to use. + * @param socket The POSIX socket. + */ + protected ForJnaPosixSocket(PosixLibrary library, File socket) { + this.library = library; + this.socket = socket; + } + + @Override + protected Integer connect() { + int handle = library.socket(1, 1, 0); + try { + PosixLibrary.SocketAddress address = new PosixLibrary.SocketAddress(); + try { + address.setPath(socket.getAbsolutePath()); + library.connect(handle, address, address.size()); + return handle; + } finally { + address = null; + } + } catch (RuntimeException exception) { + library.close(handle); + throw exception; + } + } + + @Override + protected int read(Integer handle, byte[] buffer) { + int read = library.read(handle, ByteBuffer.wrap(buffer), buffer.length); + return read == 0 ? -1 : read; + } + + @Override + protected void write(Integer handle, byte[] buffer) { + library.write(handle, ByteBuffer.wrap(buffer), buffer.length); + } + + @Override + protected void close(Integer handle) { + library.close(handle); + } + + /** + * {@inheritDoc} + */ + public void close() { + /* do nothing */ + } + + /** + * A JNA library binding for POSIX sockets. + */ + protected interface PosixLibrary extends Library { + + /** + * Sends a kill command. + * + * @param processId The process id to kill. + * @param signal The signal to send. + * @return The return code. + * @throws LastErrorException If an error occurs. + */ + int kill(int processId, int signal) throws LastErrorException; + + /** + * Creates a POSIX socket connection. + * + * @param domain The socket's domain. + * @param type The socket's type. + * @param protocol The protocol version. + * @return A handle to the socket that was created or {@code 0} if no socket could be created. + * @throws LastErrorException If an error occurs. + */ + int socket(int domain, int type, int protocol) throws LastErrorException; + + /** + * Connects a socket connection. + * + * @param handle The socket's handle. + * @param address The address of the POSIX socket. + * @param length The length of the socket value. + * @return The return code. + * @throws LastErrorException If an error occurs. + */ + int connect(int handle, SocketAddress address, int length) throws LastErrorException; + + /** + * Reads from a POSIX socket. + * + * @param handle The socket's handle. + * @param buffer The buffer to read from. + * @param count The bytes being read. + * @return The amount of bytes that could be read. + * @throws LastErrorException If an error occurs. + */ + int read(int handle, ByteBuffer buffer, int count) throws LastErrorException; + + /** + * Writes to a POSIX socket. + * + * @param handle The socket's handle. + * @param buffer The buffer to write to. + * @param count The bytes being written. + * @return The return code. + * @throws LastErrorException If an error occurs. + */ + int write(int handle, ByteBuffer buffer, int count) throws LastErrorException; + + /** + * Closes the socket connection. + * + * @param handle The handle of the connection. + * @return The return code. + * @throws LastErrorException If an error occurs. + */ + int close(int handle) throws LastErrorException; + + /** + * Represents an address for a POSIX socket. + */ + class SocketAddress extends Structure { + + /** + * The socket family. + */ + @SuppressWarnings("unused") + public short family = 1; + + /** + * The socket path. + */ + public byte[] path = new byte[100]; + + /** + * Sets the socket path. + * + * @param path The socket path. + */ + protected void setPath(String path) { + try { + System.arraycopy(path.getBytes("UTF-8"), 0, this.path, 0, path.length()); + System.arraycopy(new byte[]{0}, 0, this.path, path.length(), 1); + } catch (UnsupportedEncodingException exception) { + throw new IllegalStateException(exception); + } + } + + @Override + protected List getFieldOrder() { + return Arrays.asList("family", "path"); + } + } + } + + /** + * A factory for a POSIX socket connection to a JVM using JNA. + */ + public static class Factory extends Connection.Factory.ForSocketFile { + + /** + * The socket library API. + */ + private final PosixLibrary library; + + /** + * Creates a connection factory for a POSIX socket using JNA. + * + * @param temporaryDirectory The temporary directory to use. + * @param attempts The maximum amount of attempts for checking the establishment of a socket connection. + * @param pause The pause between two checks for an established socket connection. + * @param timeUnit The time unit of the pause time. + */ + @SuppressWarnings("deprecation") + public Factory(String temporaryDirectory, int attempts, long pause, TimeUnit timeUnit) { + super(temporaryDirectory, attempts, pause, timeUnit); + library = Native.loadLibrary("c", PosixLibrary.class); + } + + /** + * Creates a connection factory for a POSIX socket using JNA while locating the default temporary directory used on the + * current platform. + * + * @param attempts The maximum amount of attempts for checking the establishment of a socket connection. + * @param pause The pause between two checks for an established socket connection. + * @param timeUnit The time unit of the pause time. + * @return An appropriate connection factory. + */ + @SuppressWarnings("deprecation") + public static Connection.Factory withDefaultTemporaryFolder(int attempts, long pause, TimeUnit timeUnit) { + String temporaryDirectory; + if (Platform.isMac()) { + MacLibrary library = Native.loadLibrary("c", MacLibrary.class); + Memory memory = new Memory(4096); + try { + long length = library.confstr(MacLibrary.CS_DARWIN_USER_TEMP_DIR, memory, memory.size()); + if (length == 0 || length > 4096) { + temporaryDirectory = "/tmp"; + } else { + temporaryDirectory = memory.getString(0); + } + } finally { + memory = null; + } + } else { + temporaryDirectory = "/tmp"; + } + return new Factory(temporaryDirectory, attempts, pause, timeUnit); + } + + @Override + protected void kill(String processId, int signal) { + library.kill(Integer.parseInt(processId), signal); + } + + @Override + public Connection doConnect(File socket) { + return new Connection.ForJnaPosixSocket(library, socket); + } + + /** + * A library for reading a Mac user's temporary directory. + */ + public interface MacLibrary extends Library { + + /** + * The temporary directory. + */ + int CS_DARWIN_USER_TEMP_DIR = 65537; + + /** + * Reads a configuration dependant variable into a memory segment. + * + * @param name The name of the variable. + * @param buffer The buffer to read the variable into. + * @param length The length of the buffer. + * @return The amount of bytes written to the buffer. + */ + long confstr(int name, Pointer buffer, long length); + } + } + } + + /** + * Implements a connection for a Windows named pipe in JNA. + */ + class ForJnaWindowsNamedPipe implements Connection { + + /** + * Indicates a memory release. + */ + private static final int MEM_RELEASE = 0x8000; + + /** + * The library to use for communicating with Windows native functions. + */ + private final WindowsLibrary library; + + /** + * The library to use for communicating with Windows attachment extension that is included as a DLL. + */ + private final WindowsAttachLibrary attachLibrary; + + /** + * The handle of the target VM's process. + */ + private final WinNT.HANDLE process; + + /** + * A pointer to the code that was injected into the target process. + */ + private final WinDef.LPVOID code; + + /** + * A source of random values being used for generating pipe names. + */ + private final SecureRandom random; + + /** + * Creates a new connection via a named pipe. + * + * @param library The library to use for communicating with Windows native functions. + * @param attachLibrary The library to use for communicating with Windows attachment extension that is included as a DLL. + * @param process The handle of the target VM's process. + * @param code A pointer to the code that was injected into the target process. + */ + protected ForJnaWindowsNamedPipe(WindowsLibrary library, + WindowsAttachLibrary attachLibrary, + WinNT.HANDLE process, + WinDef.LPVOID code) { + this.library = library; + this.attachLibrary = attachLibrary; + this.process = process; + this.code = code; + random = new SecureRandom(); + } + + /** + * {@inheritDoc} + */ + public Response execute(String protocol, String... argument) { + if (!"1".equals(protocol)) { + throw new IllegalArgumentException("Unknown protocol version: " + protocol); + } else if (argument.length > 4) { + throw new IllegalArgumentException("Cannot supply more then four arguments to Windows attach mechanism: " + Arrays.asList(argument)); + } + String name = "\\\\.\\pipe\\javatool" + Math.abs(random.nextInt() + 1); + + WinBase.SECURITY_ATTRIBUTES sa = createSecurityAttributesToAllowMediumIntegrity(); + + WinNT.HANDLE pipe = Kernel32.INSTANCE.CreateNamedPipe(name, + WinBase.PIPE_ACCESS_INBOUND, + WinBase.PIPE_TYPE_BYTE | WinBase.PIPE_READMODE_BYTE | WinBase.PIPE_WAIT, + 1, + 4096, + 8192, + WinBase.NMPWAIT_USE_DEFAULT_WAIT, + sa); + if (pipe == null) { + throw new Win32Exception(Native.getLastError()); + } + try { + WinDef.LPVOID data = attachLibrary.allocate_remote_argument(process, + name, + argument.length < 1 ? null : argument[0], + argument.length < 2 ? null : argument[1], + argument.length < 3 ? null : argument[2], + argument.length < 4 ? null : argument[3]); + if (data == null) { + throw new Win32Exception(Native.getLastError()); + } + try { + WinNT.HANDLE thread = library.CreateRemoteThread(process, null, 0, code.getPointer(), data.getPointer(), null, null); + if (thread == null) { + throw new Win32Exception(Native.getLastError()); + } + try { + int result = Kernel32.INSTANCE.WaitForSingleObject(thread, WinBase.INFINITE); + if (result != 0) { + throw new Win32Exception(result); + } + IntByReference exitCode = new IntByReference(); + if (!library.GetExitCodeThread(thread, exitCode)) { + throw new Win32Exception(Native.getLastError()); + } else if (exitCode.getValue() != 0) { + throw new IllegalStateException("Target VM could not dispatch command successfully: " + exitCode.getValue()); + } + if (!Kernel32.INSTANCE.ConnectNamedPipe(pipe, null)) { + int code = Native.getLastError(); + if (code != WinError.ERROR_PIPE_CONNECTED) { + throw new Win32Exception(code); + } + } + return new NamedPipeResponse(pipe); + } finally { + if (!Kernel32.INSTANCE.CloseHandle(thread)) { + throw new Win32Exception(Native.getLastError()); + } + } + } finally { + if (!library.VirtualFreeEx(process, data.getPointer(), 0, MEM_RELEASE)) { + throw new Win32Exception(Native.getLastError()); + } + } + } catch (Throwable throwable) { + if (!Kernel32.INSTANCE.CloseHandle(pipe)) { + throw new Win32Exception(Native.getLastError()); + } else if (throwable instanceof RuntimeException) { + throw (RuntimeException) throwable; + } else { + throw new IllegalStateException(throwable); + } + } + } + + /** + * Custom {@link WinBase.SECURITY_ATTRIBUTES} is required here to "get" Medium Integrity Level. + * In order to allow Medium Integrity Level clients to open + * and use a NamedPipe created by an High Integrity Level process. + * + * @return A security attributes object that gives everyone read and write access. + */ + private WinBase.SECURITY_ATTRIBUTES createSecurityAttributesToAllowMediumIntegrity() { + // Allow read/write to Everybody + WinNT.PSID pSidEverybody = new WinNT.PSID(WinNT.SECURITY_MAX_SID_SIZE); + if (!Advapi32.INSTANCE.CreateWellKnownSid(WinNT.WELL_KNOWN_SID_TYPE.WinWorldSid, null, pSidEverybody, new IntByReference(WinNT.SECURITY_MAX_SID_SIZE))) { + throw new Win32Exception(Kernel32.INSTANCE.GetLastError()); + } + // Allow full control to System + WinNT.PSID pSidSystem = new WinNT.PSID(WinNT.SECURITY_MAX_SID_SIZE); + if (!Advapi32.INSTANCE.CreateWellKnownSid(WinNT.WELL_KNOWN_SID_TYPE.WinBuiltinSystemOperatorsSid, null, pSidSystem, new IntByReference(WinNT.SECURITY_MAX_SID_SIZE))) { + throw new Win32Exception(Kernel32.INSTANCE.GetLastError()); + } + // Allow full control to Administrators + WinNT.PSID pSidAdmin = new WinNT.PSID(WinNT.SECURITY_MAX_SID_SIZE); + if (!Advapi32.INSTANCE.CreateWellKnownSid(WinNT.WELL_KNOWN_SID_TYPE.WinBuiltinAdministratorsSid, null, pSidAdmin, new IntByReference(WinNT.SECURITY_MAX_SID_SIZE))) { + throw new Win32Exception(Kernel32.INSTANCE.GetLastError()); + } + + // Prepare ACL + int cbAcl = Native.getNativeSize(WinNT.ACL.class, null); + cbAcl += Native.getNativeSize(WinNT.ACCESS_ALLOWED_ACE.class, null) * 3; + cbAcl += (Advapi32.INSTANCE.GetLengthSid(pSidEverybody) - WinDef.DWORD.SIZE); + cbAcl += (Advapi32.INSTANCE.GetLengthSid(pSidSystem) - WinDef.DWORD.SIZE); + cbAcl += (Advapi32.INSTANCE.GetLengthSid(pSidAdmin) - WinDef.DWORD.SIZE); + cbAcl = Advapi32Util.alignOnDWORD(cbAcl); + WinNT.ACL pAcl = new WinNT.ACL(cbAcl); + if (!Advapi32.INSTANCE.InitializeAcl(pAcl, cbAcl, WinNT.ACL_REVISION)) { + throw new Win32Exception(Kernel32.INSTANCE.GetLastError()); + } + if (!Advapi32.INSTANCE.AddAccessAllowedAce(pAcl, WinNT.ACL_REVISION, WinNT.GENERIC_READ | WinNT.GENERIC_WRITE, pSidEverybody)) { + throw new Win32Exception(Kernel32.INSTANCE.GetLastError()); + } + if (!Advapi32.INSTANCE.AddAccessAllowedAce(pAcl, WinNT.ACL_REVISION, WinNT.GENERIC_ALL, pSidSystem)) { + throw new Win32Exception(Kernel32.INSTANCE.GetLastError()); + } + if (!Advapi32.INSTANCE.AddAccessAllowedAce(pAcl, WinNT.ACL_REVISION, WinNT.GENERIC_ALL, pSidAdmin)) { + throw new Win32Exception(Kernel32.INSTANCE.GetLastError()); + } + WinNT.SECURITY_DESCRIPTOR sd = new WinNT.SECURITY_DESCRIPTOR(64 * 1024); + if (!Advapi32.INSTANCE.InitializeSecurityDescriptor(sd, WinNT.SECURITY_DESCRIPTOR_REVISION)) { + throw new Win32Exception(Kernel32.INSTANCE.GetLastError()); + } + if (!Advapi32.INSTANCE.SetSecurityDescriptorDacl(sd, true, pAcl, false)) { + throw new Win32Exception(Kernel32.INSTANCE.GetLastError()); + } + WinBase.SECURITY_ATTRIBUTES sa = new WinBase.SECURITY_ATTRIBUTES(); + sa.dwLength = new WinDef.DWORD(sa.size()); + sa.lpSecurityDescriptor = sd.getPointer(); + return sa; + } + + /** + * {@inheritDoc} + */ + public void close() { + try { + if (!library.VirtualFreeEx(process, code.getPointer(), 0, MEM_RELEASE)) { + throw new Win32Exception(Native.getLastError()); + } + } finally { + if (!Kernel32.INSTANCE.CloseHandle(process)) { + throw new Win32Exception(Native.getLastError()); + } + } + } + + /** + * A library for interacting with Windows. + */ + protected interface WindowsLibrary extends StdCallLibrary { + + /** + * Changes the state of memory in a given process. + * + * @param process The process in which to change the memory. + * @param address The address of the memory to allocate. + * @param size The size of the allocated region. + * @param allocationType The allocation type. + * @param protect The memory protection. + * @return A pointer to the allocated memory. + */ + @SuppressWarnings({"unused", "checkstyle:methodname"}) + Pointer VirtualAllocEx(WinNT.HANDLE process, Pointer address, int size, int allocationType, int protect); + + /** + * Frees memory in the given process. + * + * @param process The process in which to change the memory. + * @param address The address of the memory to free. + * @param size The size of the freed region. + * @param freeType The freeing type. + * @return {@code true} if the operation succeeded. + */ + @SuppressWarnings("checkstyle:methodname") + boolean VirtualFreeEx(WinNT.HANDLE process, Pointer address, int size, int freeType); + + /** + * An alternative implementation of + * {@link Kernel32#CreateRemoteThread(WinNT.HANDLE, WinBase.SECURITY_ATTRIBUTES, int, WinBase.FOREIGN_THREAD_START_ROUTINE, Pointer, WinDef.DWORD, Pointer)} + * that uses a pointer as the {@code code} argument rather then a structure to avoid accessing foreign memory. + * + * @param process A handle of the target process. + * @param securityAttributes The security attributes to use or {@code null} if no attributes are provided. + * @param stackSize The stack size or {@code 0} for using the system default. + * @param code A pointer to the code to execute. + * @param argument A pointer to the argument to provide to the code being executed. + * @param creationFlags The creation flags or {@code null} if no flags are set. + * @param threadId A pointer to the thread id or {@code null} if no thread reference is set. + * @return A handle to the created remote thread or {@code null} if the creation failed. + */ + @SuppressWarnings("checkstyle:methodname") + WinNT.HANDLE CreateRemoteThread(WinNT.HANDLE process, + WinBase.SECURITY_ATTRIBUTES securityAttributes, + int stackSize, + Pointer code, + Pointer argument, + WinDef.DWORD creationFlags, + Pointer threadId); + + /** + * Receives the exit code of a given thread. + * + * @param thread A handle to the targeted thread. + * @param exitCode A reference to the exit code value. + * @return {@code true} if the exit code retrieval succeeded. + */ + @SuppressWarnings("checkstyle:methodname") + boolean GetExitCodeThread(WinNT.HANDLE thread, IntByReference exitCode); + } + + /** + * A library for interacting with Windows. + */ + protected interface WindowsAttachLibrary extends StdCallLibrary { + + /** + * Allocates the code to invoke on the remote VM. + * + * @param process A handle to the target process. + * @return A pointer to the allocated code or {@code null} if the code could not be allocated. + */ + + @SuppressWarnings("checkstyle:methodname") + WinDef.LPVOID allocate_remote_code(WinNT.HANDLE process); + + /** + * Allocates the remote argument to supply to the remote code upon execution. + * + * @param process A handle to the target process. + * @param pipe The name of the pipe used for supplying an answer. + * @param argument0 The first argument or {@code null} if no such argument is provided. + * @param argument1 The second argument or {@code null} if no such argument is provided. + * @param argument2 The third argument or {@code null} if no such argument is provided. + * @param argument3 The forth argument or {@code null} if no such argument is provided. + * @return A pointer to the allocated argument or {@code null} if the argument could not be allocated. + */ + + @SuppressWarnings("checkstyle:methodname") + WinDef.LPVOID allocate_remote_argument(WinNT.HANDLE process, + String pipe, + String argument0, + String argument1, + String argument2, + String argument3); + } + + /** + * A response that is sent via a named pipe. + */ + protected static class NamedPipeResponse implements Response { + + /** + * A handle of the named pipe. + */ + private final WinNT.HANDLE pipe; + + /** + * Creates a new response via a named pipe. + * + * @param pipe The handle of the named pipe. + */ + protected NamedPipeResponse(WinNT.HANDLE pipe) { + this.pipe = pipe; + } + + /** + * {@inheritDoc} + */ + public int read(byte[] buffer) { + IntByReference read = new IntByReference(); + if (!Kernel32.INSTANCE.ReadFile(pipe, buffer, buffer.length, read, null)) { + int code = Native.getLastError(); + if (code == WinError.ERROR_BROKEN_PIPE) { + return -1; + } else { + throw new Win32Exception(code); + } + } + return read.getValue(); + } + + /** + * {@inheritDoc} + */ + public void close() { + try { + if (!Kernel32.INSTANCE.DisconnectNamedPipe(pipe)) { + throw new Win32Exception(Native.getLastError()); + } + } finally { + if (!Kernel32.INSTANCE.CloseHandle(pipe)) { + throw new Win32Exception(Native.getLastError()); + } + } + } + } + + /** + * A factory for establishing a connection to a JVM using a named pipe in JNA. + */ + public static class Factory implements Connection.Factory { + + /** + * The name of the native code library that is included in this artifact to support Windows attachment. + * This property can be set by other libraries that shade Byte Buddy agent and relocates the library. + */ + public static final String LIBRARY_NAME = "net.bytebuddy.library.name"; + + /** + * The library to use for communicating with Windows native functions. + */ + private final WindowsLibrary library; + + /** + * The library to use for communicating with Windows attachment extension that is included as a DLL. + */ + private final WindowsAttachLibrary attachLibrary; + + /** + * Creates a new connection factory for Windows using JNA. + */ + @SuppressWarnings("deprecation") + public Factory() { + library = Native.loadLibrary("kernel32", WindowsLibrary.class, W32APIOptions.DEFAULT_OPTIONS); + attachLibrary = Native.loadLibrary(System.getProperty(LIBRARY_NAME, "attach_hotspot_windows"), WindowsAttachLibrary.class); + } + + /** + * {@inheritDoc} + */ + public Connection connect(String processId) { + WinNT.HANDLE process = Kernel32.INSTANCE.OpenProcess(WinNT.PROCESS_ALL_ACCESS, false, Integer.parseInt(processId)); + if (process == null) { + throw new Win32Exception(Native.getLastError()); + } + try { + WinDef.LPVOID code = attachLibrary.allocate_remote_code(process); + if (code == null) { + throw new Win32Exception(Native.getLastError()); + } + return new ForJnaWindowsNamedPipe(library, attachLibrary, process, code); + } catch (Throwable throwable) { + if (!Kernel32.INSTANCE.CloseHandle(process)) { + throw new Win32Exception(Native.getLastError()); + } else if (throwable instanceof RuntimeException) { + throw (RuntimeException) throwable; + } else { + throw new IllegalStateException(throwable); + } + } + } + } + } + + /** + * A connection to a VM using a Solaris door. + */ + class ForJnaSolarisDoor implements Connection { + + /** + * The library to use for interacting with Solaris. + */ + private final SolarisLibrary library; + + /** + * The socket used for communication. + */ + private final File socket; + + /** + * Creates a new connection using a Solaris door. + * + * @param library The library to use for interacting with Solaris. + * @param socket The socket used for communication. + */ + protected ForJnaSolarisDoor(SolarisLibrary library, File socket) { + this.library = library; + this.socket = socket; + } + + /** + * {@inheritDoc} + */ + public Connection.Response execute(String protocol, String... argument) throws IOException { + int handle = library.open(socket.getAbsolutePath(), 2); + try { + SolarisLibrary.DoorArgument door = new SolarisLibrary.DoorArgument(); + try { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + outputStream.write(protocol.getBytes("UTF-8")); + outputStream.write(0); + for (String anArgument : argument) { + if (anArgument != null) { + outputStream.write(anArgument.getBytes("UTF-8")); + } + outputStream.write(0); + } + door.dataSize = outputStream.size(); + Memory dataPointer = new Memory(outputStream.size()); + try { + dataPointer.write(0, outputStream.toByteArray(), 0, outputStream.size()); + door.dataPointer = dataPointer; + Memory result = new Memory(128); + try { + door.resultPointer = result; + door.resultSize = (int) result.size(); + if (library.door_call(handle, door) != 0) { + throw new IllegalStateException("Door call to target VM failed"); + } else if (door.resultSize < 4 || door.resultPointer.getInt(0) != 0) { + throw new IllegalStateException("Target VM could not execute door call"); + } else if (door.descriptorCount != 1 || door.descriptorPointer == null) { + throw new IllegalStateException("Did not receive communication descriptor from target VM"); + } else { + return new Response(library, door.descriptorPointer.getInt(4)); + } + } finally { + result = null; + } + } finally { + dataPointer = null; + } + } finally { + door = null; + } + } finally { + library.close(handle); + } + } + + /** + * {@inheritDoc} + */ + public void close() { + /* do nothing */ + } + + /** + * A library for interacting with Solaris. + */ + protected interface SolarisLibrary extends Library { + + /** + * Sends a kill signal to the target VM. + * + * @param processId The target process's id. + * @param signal The signal to send. + * @return The return code. + * @throws LastErrorException If an error occurred while sending the signal. + */ + int kill(int processId, int signal) throws LastErrorException; + + /** + * Opens a file. + * + * @param file The file name. + * @param flags the flags for opening. + * @return The file descriptor. + * @throws LastErrorException If the file could not be opened. + */ + int open(String file, int flags) throws LastErrorException; + + /** + * Reads from a handle. + * + * @param handle The handle representing the source being read. + * @param buffer The buffer to read to. + * @param length The buffer length. + * @return The amount of bytes being read. + * @throws LastErrorException If a read operation failed. + */ + int read(int handle, ByteBuffer buffer, int length) throws LastErrorException; + + /** + * Releases a descriptor. + * + * @param descriptor The descriptor to release. + * @return The return code. + * @throws LastErrorException If the descriptor could not be closed. + */ + int close(int descriptor) throws LastErrorException; + + /** + * Executes a door call. + * + * @param descriptor The door's descriptor. + * @param argument A pointer to the argument. + * @return The door's handle. + * @throws LastErrorException If the door call failed. + */ + @SuppressWarnings("checkstyle:methodname") + int door_call(int descriptor, DoorArgument argument) throws LastErrorException; + + /** + * A structure representing the argument to a Solaris door operation. + */ + class DoorArgument extends Structure { + + /** + * A pointer to the operation argument. + */ + + public Pointer dataPointer; + + /** + * The size of the argument being pointed to. + */ + public int dataSize; + + /** + * A pointer to the operation descriptor. + */ + + public Pointer descriptorPointer; + + /** + * The size of the operation argument. + */ + public int descriptorCount; + + /** + * A pointer to the operation result. + */ + public Pointer resultPointer; + + /** + * The size of the operation argument. + */ + public int resultSize; + + @Override + protected List getFieldOrder() { + return Arrays.asList("dataPointer", "dataSize", "descriptorPointer", "descriptorCount", "resultPointer", "resultSize"); + } + } + } + + /** + * A response from a VM using a Solaris door. + */ + protected static class Response implements Connection.Response { + + /** + * The Solaris library to use. + */ + private final SolarisLibrary library; + + /** + * The door handle. + */ + private final int handle; + + /** + * Creates a response from a VM using a Solaris door. + * + * @param library The Solaris library to use. + * @param handle The door handle. + */ + protected Response(SolarisLibrary library, int handle) { + this.library = library; + this.handle = handle; + } + + /** + * {@inheritDoc} + */ + public int read(byte[] buffer) { + int read = library.read(handle, ByteBuffer.wrap(buffer), buffer.length); + return read == 0 ? -1 : read; + } + + /** + * {@inheritDoc} + */ + public void close() { + library.close(handle); + } + } + + /** + * A factory for establishing a connection to a JVM using a Solaris door in JNA. + */ + public static class Factory extends Connection.Factory.ForSocketFile { + + /** + * The library to use for interacting with Solaris. + */ + private final SolarisLibrary library; + + /** + * Creates a new connection factory for a Solaris VM. + * + * @param attempts The maximum amount of attempts for checking the establishment of a socket connection. + * @param pause The pause between two checks for an established socket connection. + * @param timeUnit The time unit of the pause time. + */ + @SuppressWarnings("deprecation") + public Factory(int attempts, long pause, TimeUnit timeUnit) { + super("/tmp", attempts, pause, timeUnit); + library = Native.loadLibrary("c", SolarisLibrary.class); + } + + /** + * {@inheritDoc} + */ + protected void kill(String processId, int signal) { + library.kill(Integer.parseInt(processId), signal); + } + + /** + * {@inheritDoc} + */ + protected Connection doConnect(File socket) { + return new ForJnaSolarisDoor(library, socket); + } + } + } + } + } + + /** + * A virtual machine attachment implementation for OpenJ9 or any compatible JVM. + */ + class ForOpenJ9 extends AbstractBase { + + /** + * The temporary folder for attachment files for OpenJ9 VMs. + */ + private static final String IBM_TEMPORARY_FOLDER = "com.ibm.tools.attach.directory"; + + /** + * A secure random for generating randomized ids. + */ + private static final SecureRandom SECURE_RANDOM = new SecureRandom(); + + /** + * The socket on which this VM and the target VM communicate. + */ + private final Socket socket; + + /** + * Creates a new virtual machine connection for OpenJ9. + * + * @param socket The socket on which this VM and the target VM communicate. + */ + protected ForOpenJ9(Socket socket) { + this.socket = socket; + } + + /** + * Attaches to the supplied process id using the default JNA implementation. This method will not consider + * attaching to VMs owned by different users than the current user. + * + * @param processId The process id. + * @return A suitable virtual machine implementation. + * @throws IOException If an IO exception occurs during establishing the connection. + */ + public static VirtualMachine attach(String processId) throws IOException { + return attach(processId, 5000, Platform.isWindows() + ? new Dispatcher.ForJnaWindowsEnvironment() + : new Dispatcher.ForJnaPosixEnvironment(15, 100, TimeUnit.MILLISECONDS)); + } + + /** + * Attaches to the supplied process id. + * + * @param processId The process id. + * @param timeout The timeout for establishing the socket connection. + * @param dispatcher The connector to use to communicate with the target VM. + * @return A suitable virtual machine implementation. + * @throws IOException If an IO exception occurs during establishing the connection. + */ + public static VirtualMachine attach(String processId, int timeout, Dispatcher dispatcher) throws IOException { + String temporary = System.getProperty(IBM_TEMPORARY_FOLDER); + File directory = temporary == null + ? new File(dispatcher.getTemporaryFolder(processId), ".com_ibm_tools_attach") + : new File(temporary); + long userId = dispatcher.userId(); + RandomAccessFile attachLock = new RandomAccessFile(new File(directory, "_attachlock"), "rw"); + try { + FileLock attachLockLock = attachLock.getChannel().lock(); + try { + List virtualMachines; + RandomAccessFile master = new RandomAccessFile(new File(directory, "_master"), "rw"); + try { + FileLock masterLock = master.getChannel().lock(); + try { + File[] vmFolder = directory.listFiles(); + if (vmFolder == null) { + throw new IllegalStateException("No descriptor files found in " + directory); + } + virtualMachines = new ArrayList(); + for (File aVmFolder : vmFolder) { + if (aVmFolder.isDirectory() && (userId == 0L || dispatcher.getOwnerIdOf(aVmFolder) == userId)) { + File attachInfo = new File(aVmFolder, "attachInfo"); + if (attachInfo.isFile()) { + Properties virtualMachine = new Properties(); + FileInputStream inputStream = new FileInputStream(attachInfo); + try { + virtualMachine.load(inputStream); + } finally { + inputStream.close(); + } + int targetProcessId = Integer.parseInt(virtualMachine.getProperty("processId")); + long targetUserId; + try { + targetUserId = Long.parseLong(virtualMachine.getProperty("userUid")); + } catch (NumberFormatException ignored) { + targetUserId = 0L; + } + if (userId != 0L && targetUserId == 0L) { + targetUserId = dispatcher.getOwnerIdOf(attachInfo); + } + if (targetProcessId == 0L || dispatcher.isExistingProcess(targetProcessId)) { + virtualMachines.add(virtualMachine); + } else if (userId == 0L || targetUserId == userId) { + File[] vmFile = aVmFolder.listFiles(); + if (vmFile != null) { + for (File aVmFile : vmFile) { + if (!aVmFile.delete()) { + aVmFile.deleteOnExit(); + } + } + } + if (!aVmFolder.delete()) { + aVmFolder.deleteOnExit(); + } + } + } + } + } + } finally { + masterLock.release(); + } + } finally { + master.close(); + } + Properties target = null; + for (Properties virtualMachine : virtualMachines) { + if (virtualMachine.getProperty("processId").equalsIgnoreCase(processId)) { + target = virtualMachine; + break; + } + } + if (target == null) { + throw new IllegalStateException("Could not locate target process info in " + directory); + } + ServerSocket serverSocket = new ServerSocket(0); + try { + serverSocket.setSoTimeout(timeout); + File receiver = new File(directory, target.getProperty("vmId")); + String key; + synchronized (SECURE_RANDOM) { + key = Long.toHexString(SECURE_RANDOM.nextLong()); + } + File reply = new File(receiver, "replyInfo"); + long targetUserId; + try { + targetUserId = Long.parseLong(target.getProperty("userUid")); + } catch (NumberFormatException ignored) { + targetUserId = 0L; + } + try { + if (reply.createNewFile()) { + dispatcher.setPermissions(reply, 0600); + } + if (userId == 0L && targetUserId != 0L) { + dispatcher.chownFileToUser(reply, targetUserId); + } + FileOutputStream outputStream = new FileOutputStream(reply); + try { + outputStream.write(key.getBytes("UTF-8")); + outputStream.write("\n".getBytes("UTF-8")); + outputStream.write(Long.toString(serverSocket.getLocalPort()).getBytes("UTF-8")); + outputStream.write("\n".getBytes("UTF-8")); + } finally { + outputStream.close(); + } + Map locks = new HashMap(); + try { + String pid = Long.toString(dispatcher.pid()); + for (Properties virtualMachine : virtualMachines) { + if (!virtualMachine.getProperty("processId").equalsIgnoreCase(pid)) { + String attachNotificationSync = virtualMachine.getProperty("attachNotificationSync"); + RandomAccessFile syncFile = new RandomAccessFile(attachNotificationSync == null + ? new File(directory, "attachNotificationSync") + : new File(attachNotificationSync), "rw"); + try { + locks.put(syncFile, syncFile.getChannel().lock()); + } catch (IOException ignored) { + syncFile.close(); + } + } + } + int notifications = 0; + File[] item = directory.listFiles(); + if (item != null) { + for (File anItem : item) { + String name = anItem.getName(); + if (!name.startsWith(".trash_") + && !name.equalsIgnoreCase("_attachlock") + && !name.equalsIgnoreCase("_master") + && !name.equalsIgnoreCase("_notifier")) { + notifications += 1; + } + } + } + boolean global = Boolean.parseBoolean(target.getProperty("globalSemaphore")); + dispatcher.incrementSemaphore(directory, "_notifier", global, notifications); + try { + Socket socket = serverSocket.accept(); + String answer = new String(read(socket), "UTF-8"); + if (answer.contains(' ' + key + ' ')) { + return new ForOpenJ9(socket); + } else { + socket.close(); + throw new IllegalStateException("Unexpected answered to attachment: " + answer); + } + } finally { + dispatcher.decrementSemaphore(directory, "_notifier", global, notifications); + } + } finally { + for (Map.Entry entry : locks.entrySet()) { + try { + try { + entry.getValue().release(); + } finally { + entry.getKey().close(); + } + } catch (Throwable ignored) { + /* do nothing */ + } + } + } + } finally { + if (!reply.delete()) { + reply.deleteOnExit(); + } + } + } finally { + serverSocket.close(); + } + } finally { + attachLockLock.release(); + } + } finally { + attachLock.close(); + } + } + + /** + * {@inheritDoc} + */ + public Properties getSystemProperties() throws IOException { + write(socket, "ATTACH_GETSYSTEMPROPERTIES".getBytes("UTF-8")); + Properties properties = new Properties(); + properties.load(new ByteArrayInputStream(read(socket))); + return properties; + } + + /** + * {@inheritDoc} + */ + public Properties getAgentProperties() throws IOException { + write(socket, "ATTACH_GETAGENTPROPERTIES".getBytes("UTF-8")); + Properties properties = new Properties(); + properties.load(new ByteArrayInputStream(read(socket))); + return properties; + } + + /** + * {@inheritDoc} + */ + public void loadAgent(String jarFile, String argument) throws IOException { + write(socket, ("ATTACH_LOADAGENT(instrument," + jarFile + '=' + (argument == null ? "" : argument) + ')').getBytes("UTF-8")); + String answer = new String(read(socket), "UTF-8"); + if (answer.startsWith("ATTACH_ERR")) { + throw new IllegalStateException("Target VM failed loading agent: " + answer); + } else if (!answer.startsWith("ATTACH_ACK") && !answer.startsWith("ATTACH_RESULT=")) { + throw new IllegalStateException("Unexpected response: " + answer); + } + } + + /** + * {@inheritDoc} + */ + public void loadAgentPath(String path, String argument) throws IOException { + write(socket, ("ATTACH_LOADAGENTPATH(" + path + (argument == null ? "" : (',' + argument)) + ')').getBytes("UTF-8")); + String answer = new String(read(socket), "UTF-8"); + if (answer.startsWith("ATTACH_ERR")) { + throw new IllegalStateException("Target VM failed loading native agent: " + answer); + } else if (!answer.startsWith("ATTACH_ACK") && !answer.startsWith("ATTACH_RESULT=")) { + throw new IllegalStateException("Unexpected response: " + answer); + } + } + + /** + * {@inheritDoc} + */ + public void loadAgentLibrary(String library, String argument) throws IOException { + write(socket, ("ATTACH_LOADAGENTLIBRARY(" + library + (argument == null ? "" : (',' + argument)) + ')').getBytes("UTF-8")); + String answer = new String(read(socket), "UTF-8"); + if (answer.startsWith("ATTACH_ERR")) { + throw new IllegalStateException("Target VM failed loading native library: " + answer); + } else if (!answer.startsWith("ATTACH_ACK") && !answer.startsWith("ATTACH_RESULT=")) { + throw new IllegalStateException("Unexpected response: " + answer); + } + } + + /** + * {@inheritDoc} + */ + public void startManagementAgent(Properties properties) throws IOException { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + properties.store(outputStream, null); + write(socket, "ATTACH_START_MANAGEMENT_AGENT".getBytes("UTF-8")); + write(socket, outputStream.toByteArray()); + String answer = new String(read(socket), "UTF-8"); + if (answer.startsWith("ATTACH_ERR")) { + throw new IllegalStateException("Target VM could not start management agent: " + answer); + } else if (!answer.startsWith("ATTACH_ACK") && !answer.startsWith("ATTACH_RESULT=")) { + throw new IllegalStateException("Unexpected response: " + answer); + } + } + + /** + * {@inheritDoc} + */ + public String startLocalManagementAgent() throws IOException { + write(socket, "ATTACH_START_LOCAL_MANAGEMENT_AGENT".getBytes("UTF-8")); + String answer = new String(read(socket), "UTF-8"); + if (answer.startsWith("ATTACH_ERR")) { + throw new IllegalStateException("Target VM could not start management agent: " + answer); + } else if (answer.startsWith("ATTACH_ACK")) { + return answer.substring("ATTACH_ACK".length()); + } else if (answer.startsWith("ATTACH_RESULT=")) { + return answer.substring("ATTACH_RESULT=".length()); + } else { + throw new IllegalStateException("Unexpected response: " + answer); + } + } + + /** + * {@inheritDoc} + */ + public void detach() throws IOException { + try { + write(socket, "ATTACH_DETACH".getBytes("UTF-8")); + read(socket); // The answer is intentionally ignored. + } finally { + socket.close(); + } + } + + /** + * Writes the supplied value to the target socket. + * + * @param socket The socket to write to. + * @param value The value being written. + * @throws IOException If an I/O exception occurs. + */ + private static void write(Socket socket, byte[] value) throws IOException { + socket.getOutputStream().write(value); + socket.getOutputStream().write(0); + socket.getOutputStream().flush(); + } + + /** + * Reads a {@code '\0'}-terminated value from the target socket. + * + * @param socket The socket to read from. + * @return The value that was read. + * @throws IOException If an I/O exception occurs. + */ + private static byte[] read(Socket socket) throws IOException { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + byte[] buffer = new byte[1024]; + int length; + while ((length = socket.getInputStream().read(buffer)) != -1) { + if (length > 0 && buffer[length - 1] == 0) { + outputStream.write(buffer, 0, length - 1); + break; + } else { + outputStream.write(buffer, 0, length); + } + } + return outputStream.toByteArray(); + } + + /** + * A dispatcher for native operations being used for communication with an OpenJ9 virtual machine. + */ + public interface Dispatcher { + + /** + * Returns this machine's temporary folder. + * + * @param processId The target process's id. + * @return The temporary folder. + */ + String getTemporaryFolder(String processId); + + /** + * Returns the process id of this process. + * + * @return The process id of this process. + */ + int pid(); + + /** + * Returns the user id of this process. + * + * @return The user id of this process + */ + int userId(); + + /** + * Returns {@code true} if the supplied process id is a running process. + * + * @param processId The process id to evaluate. + * @return {@code true} if the supplied process id is currently running. + */ + boolean isExistingProcess(int processId); + + /** + * Returns the user id of the owner of the supplied file. + * + * @param file The file for which to locate the owner. + * @return The owner id of the supplied file. + */ + int getOwnerIdOf(File file); + + /** + * Sets permissions for the supplied file. + * + * @param file The file for which to set the permissions. + * @param permissions The permission bits to set. + */ + void setPermissions(File file, int permissions); + + /** + * Increments a semaphore. + * + * @param directory The sempahore's control directory. + * @param name The semaphore's name. + * @param global {@code true} if the semaphore is in the global namespace (only applicable on Windows). + * @param count The amount of increments. + */ + void incrementSemaphore(File directory, String name, boolean global, int count); + + /** + * Decrements a semaphore. + * + * @param directory The sempahore's control directory. + * @param name The semaphore's name. + * @param global {@code true} if the semaphore is in the global namespace (only applicable on Windows). + * @param count The amount of decrements. + */ + void decrementSemaphore(File directory, String name, boolean global, int count); + + /** + * Changes the ownership of a file. Can be called only if this process is owned by root. + * + * @param file The path of the file to change ownership of. + * @param userId The user that should own the file. + */ + void chownFileToUser(File file, long userId); + + /** + * A connector implementation for a POSIX environment using JNA. + */ + class ForJnaPosixEnvironment implements Dispatcher { + + /** + * The JNA library to use. + */ + private final PosixLibrary library; + + /** + * The POSIX owner provider to use. + */ + private final PosixOwnerProvider provider; + + /** + * Creates a new connector for a POSIX environment using JNA. + * + * @param attempts The maximum amount of attempts for checking the result of a foreign process. + * @param pause The pause between two checks for another process to return. + * @param timeUnit The time unit of the pause time. + */ + @SuppressWarnings("deprecation") + public ForJnaPosixEnvironment(int attempts, long pause, TimeUnit timeUnit) { + provider = Platform.isAIX() + ? new PosixOwnerProvider.UsingIStat(attempts, pause, timeUnit) + : new PosixOwnerProvider.UsingStat(attempts, pause, timeUnit); + library = Native.loadLibrary("c", PosixLibrary.class); + } + + /** + * {@inheritDoc} + */ + public String getTemporaryFolder(String processId) { + if (Platform.isLinux()) { + File file = new File("/proc/" + processId + "/root/tmp"); + if (file.isDirectory() && file.canRead()) { + return file.getAbsolutePath(); + } + } + String temporaryFolder = System.getenv("TMPDIR"); + return temporaryFolder == null ? "/tmp" : temporaryFolder; + } + + /** + * {@inheritDoc} + */ + public int pid() { + return library.getpid(); + } + + /** + * {@inheritDoc} + */ + public int userId() { + return library.getuid(); + } + + /** + * {@inheritDoc} + */ + public boolean isExistingProcess(int processId) { + return library.kill(processId, PosixLibrary.NULL_SIGNAL) != PosixLibrary.ESRCH; + } + + /** + * {@inheritDoc} + */ + public int getOwnerIdOf(File file) { + return provider.getOwnerIdOf(file); + } + + /** + * {@inheritDoc} + */ + public void setPermissions(File file, int permissions) { + library.chmod(file.getAbsolutePath(), permissions); + } + + /** + * {@inheritDoc} + */ + public void incrementSemaphore(File directory, String name, boolean global, int count) { + notifySemaphore(directory, name, count, (short) 1, (short) 0, false); + } + + /** + * {@inheritDoc} + */ + public void decrementSemaphore(File directory, String name, boolean global, int count) { + notifySemaphore(directory, name, count, (short) -1, (short) (PosixLibrary.SEM_UNDO | PosixLibrary.IPC_NOWAIT), true); + } + + /** + * {@inheritDoc} + */ + public void chownFileToUser(File file, long userId) { + library.chown(file.getAbsolutePath(), userId); + } + + /** + * Notifies a POSIX semaphore. + * + * @param directory The semaphore's directory. + * @param name The semaphore's name. + * @param count The amount of notifications to send. + * @param operation The operation to apply. + * @param flags The flags to set. + * @param acceptUnavailable {@code true} if a {@code EAGAIN} code should be accepted. + */ + private void notifySemaphore(File directory, String name, int count, short operation, short flags, boolean acceptUnavailable) { + int semaphore = library.semget(library.ftok(new File(directory, name).getAbsolutePath(), 0xA1), 2, 0666); + PosixLibrary.SemaphoreOperation target = new PosixLibrary.SemaphoreOperation(); + target.operation = operation; + target.flags = flags; + while (count-- > 0) { + try { + library.semop(semaphore, target, 1); + } catch (LastErrorException exception) { + if (acceptUnavailable && (Native.getLastError() == PosixLibrary.EAGAIN + || Native.getLastError() == PosixLibrary.EDEADLK)) { + break; + } else { + throw exception; + } + } + } + } + + /** + * An API for interaction with POSIX systems. + */ + protected interface PosixLibrary extends Library { + + /** + * A null signal. + */ + int NULL_SIGNAL = 0; + + /** + * Indicates that a process does not exist. + */ + int ESRCH = 3; + + /** + * Indicates that a request timed out. + */ + int EAGAIN = 11; + + /** + * Indicates a dead lock on a resource. + */ + int EDEADLK = 35; + + /** + * Indicates that a semaphore's operations should be undone at process shutdown. + */ + short SEM_UNDO = 0x1000; + + /** + * Indicates that one should not wait for the release of a semaphore if it is not currently available. + */ + short IPC_NOWAIT = 04000; + + /** + * Runs the {@code getpid} command. + * + * @return The command's return value. + * @throws LastErrorException If an error occurred. + */ + int getpid() throws LastErrorException; + + /** + * Runs the {@code getuid} command. + * + * @return The command's return value. + * @throws LastErrorException If an error occurred. + */ + int getuid() throws LastErrorException; + + /** + * Runs the {@code kill} command. + * + * @param processId The target process id. + * @param signal The signal to send. + * @return The command's return value. + * @throws LastErrorException If an error occurred. + */ + int kill(int processId, int signal) throws LastErrorException; + + /** + * Runs the {@code chmod} command. + * + * @param path The file path. + * @param mode The mode to set. + * @return The return code. + * @throws LastErrorException If an error occurred. + */ + int chmod(String path, int mode) throws LastErrorException; + + /** + * Runs the {@code chown} command. + * + * @param path The file path. + * @param userId The user id to set. + * @return The return code. + * @throws LastErrorException If an error occurred. + */ + int chown(String path, long userId) throws LastErrorException; + + /** + * Runs the {@code ftok} command. + * + * @param path The file path. + * @param id The id being used for creating the generated key. + * @return The generated key. + * @throws LastErrorException If an error occurred. + */ + int ftok(String path, int id) throws LastErrorException; + + /** + * Runs the {@code semget} command. + * + * @param key The key of the semaphore. + * @param count The initial count of the semaphore. + * @param flags The flags to set. + * @return The id of the semaphore. + * @throws LastErrorException If an error occurred. + */ + int semget(int key, int count, int flags) throws LastErrorException; + + /** + * Runs the {@code semop} command. + * + * @param id The id of the semaphore. + * @param operation The initial count of the semaphore. + * @param flags The flags to set. + * @return The return code. + * @throws LastErrorException If the operation was not successful. + */ + int semop(int id, SemaphoreOperation operation, int flags) throws LastErrorException; + + /** + * A structure to represent a semaphore operation for {@code semop}. + */ + class SemaphoreOperation extends Structure { + + /** + * The semaphore number. + */ + @SuppressWarnings("unused") + public short number; + + /** + * The operation to execute. + */ + public short operation; + + /** + * The flags being set for the operation. + */ + public short flags; + + @Override + protected List getFieldOrder() { + return Arrays.asList("number", "operation", "flags"); + } + } + } + + /** + * Represents a system that supports POSIX ownership. + */ + protected interface PosixOwnerProvider { + + /** + * Returns the user id of the owner of the supplied file. + * + * @param file The file for which to locate the owner. + * @return The owner id of the supplied file. + */ + int getOwnerIdOf(File file); + + /** + * An implementation of reading POSIX ownership using {@code stat}. + */ + class UsingStat implements PosixOwnerProvider { + + /** + * The maximum amount of attempts for checking the result of a foreign process. + */ + private final int attempts; + + /** + * The pause between two checks for another process to return. + */ + private final long pause; + + /** + * The time unit of the pause time. + */ + private final TimeUnit timeUnit; + + /** + * Creates a new provider where an owner is derived using the {@code stat} command. + * + * @param attempts The maximum amount of attempts for checking the result of a foreign process. + * @param pause The pause between two checks for another process to return. + * @param timeUnit The time unit of the pause time. + */ + public UsingStat(int attempts, long pause, TimeUnit timeUnit) { + this.attempts = attempts; + this.pause = pause; + this.timeUnit = timeUnit; + } + + /** + * {@inheritDoc} + */ + public int getOwnerIdOf(File file) { + try { + // The binding for 'stat' is very platform dependant. To avoid the complexity of binding the correct method, + // stat is called as a separate command. This is less efficient but more portable. + Process process = Runtime.getRuntime().exec(new String[]{"stat", + Platform.isMac() ? "-f" : "-c", + "%u", + file.getAbsolutePath()}); + int attempts = this.attempts; + String line = null; + do { + try { + if (process.exitValue() != 0) { + throw new IllegalStateException("Error while executing stat"); + } + BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(), "UTF-8")); + try { + line = reader.readLine(); + } finally { + reader.close(); + } + break; + } catch (IllegalThreadStateException ignored) { + try { + Thread.sleep(timeUnit.toMillis(pause)); + } catch (InterruptedException exception) { + Thread.currentThread().interrupt(); + throw new IllegalStateException(exception); + } + } + } while (--attempts > 0); + if (line == null) { + process.destroy(); + throw new IllegalStateException("Command for stat did not exit in time"); + } + return Integer.parseInt(line); + } catch (IOException exception) { + throw new IllegalStateException("Unable to execute stat command", exception); + } + } + } + + /** + * An implementation for reading a POSIX owner using {@code istat}. + */ + class UsingIStat implements PosixOwnerProvider { + + /** + * A pattern to represent the owner on the console output. + */ + private static final Pattern AIX_OWNER_PATTERN = Pattern.compile("Owner: (\\d+)\\("); + + /** + * The maximum amount of attempts for checking the result of a foreign process. + */ + private final int attempts; + + /** + * The pause between two checks for another process to return. + */ + private final long pause; + + /** + * The time unit of the pause time. + */ + private final TimeUnit timeUnit; + + /** + * Creates a new provider for reading a POSIX owner using {@code istat}. + * + * @param attempts The maximum amount of attempts for checking the result of a foreign process. + * @param pause The pause between two checks for another process to return. + * @param timeUnit The time unit of the pause time. + */ + public UsingIStat(int attempts, long pause, TimeUnit timeUnit) { + this.attempts = attempts; + this.pause = pause; + this.timeUnit = timeUnit; + } + + /** + * {@inheritDoc} + */ + public int getOwnerIdOf(File file) { + try { + Process process = Runtime.getRuntime().exec(new String[]{"istat", file.getAbsolutePath()}); + int attempts = this.attempts; + String lines = null; + do { + try { + if (process.exitValue() != 0) { + throw new IllegalStateException("Error while executing istat"); + } + StringBuilder output = new StringBuilder(); + BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(), "UTF-8")); + try { + String line; + while ((line = reader.readLine()) != null) { + output.append(line).append("\n"); + } + } finally { + reader.close(); + } + lines = output.toString(); + break; + } catch (IllegalThreadStateException ignored) { + try { + Thread.sleep(timeUnit.toMillis(pause)); + } catch (InterruptedException exception) { + Thread.currentThread().interrupt(); + throw new IllegalStateException(exception); + } + } + } while (--attempts > 0); + if (lines == null) { + process.destroy(); + throw new IllegalStateException("Command for istat did not exit in time"); + } + Matcher matcher = AIX_OWNER_PATTERN.matcher(lines); + if (matcher.find()) { + return Integer.parseInt(matcher.group(1)); + } else { + throw new IllegalStateException("Unable to parse response from istat command: " + lines); + } + } catch (IOException exception) { + throw new IllegalStateException("Unable to execute istat command", exception); + } + } + } + } + } + + /** + * A connector implementation for a Windows environment using JNA. + */ + class ForJnaWindowsEnvironment implements Dispatcher { + + /** + * Indicates a missing user id what is not supported on Windows. + */ + private static final int NO_USER_ID = 0; + + /** + * The name of the creation mutex. + */ + private static final String CREATION_MUTEX_NAME = "j9shsemcreationMutex"; + + /** + * A library to use for interacting with Windows. + */ + private final WindowsLibrary library; + + /** + * Creates a new connector for a Windows environment using JNA. + */ + @SuppressWarnings("deprecation") + public ForJnaWindowsEnvironment() { + library = Native.loadLibrary("kernel32", WindowsLibrary.class, W32APIOptions.DEFAULT_OPTIONS); + } + + /** + * {@inheritDoc} + */ + public String getTemporaryFolder(String processId) { + WinDef.DWORD length = new WinDef.DWORD(WinDef.MAX_PATH); + char[] path = new char[length.intValue()]; + if (Kernel32.INSTANCE.GetTempPath(length, path).intValue() == 0) { + throw new Win32Exception(Kernel32.INSTANCE.GetLastError()); + } + return Native.toString(path); + } + + /** + * {@inheritDoc} + */ + public int pid() { + return Kernel32.INSTANCE.GetCurrentProcessId(); + } + + /** + * {@inheritDoc} + */ + public int userId() { + return NO_USER_ID; + } + + /** + * {@inheritDoc} + */ + public boolean isExistingProcess(int processId) { + WinNT.HANDLE handle = Kernel32.INSTANCE.OpenProcess(WinNT.PROCESS_QUERY_INFORMATION, false, processId); + if (handle == null) { + throw new Win32Exception(Kernel32.INSTANCE.GetLastError()); + } + IntByReference exists = new IntByReference(); + if (!Kernel32.INSTANCE.GetExitCodeProcess(handle, exists)) { + throw new Win32Exception(Kernel32.INSTANCE.GetLastError()); + } + return exists.getValue() == WinBase.STILL_ACTIVE; + } + + /** + * {@inheritDoc} + */ + public int getOwnerIdOf(File file) { + return NO_USER_ID; + } + + /** + * {@inheritDoc} + */ + public void setPermissions(File file, int permissions) { + /* do nothing */ + } + + /** + * {@inheritDoc} + */ + public void incrementSemaphore(File directory, String name, boolean global, int count) { + AttachmentHandle handle = openSemaphore(directory, name, global); + try { + while (count-- > 0) { + if (!library.ReleaseSemaphore(handle.getHandle(), 1, null)) { + throw new Win32Exception(Kernel32.INSTANCE.GetLastError()); + } + } + } finally { + handle.close(); + } + } + + /** + * {@inheritDoc} + */ + public void decrementSemaphore(File directory, String name, boolean global, int count) { + AttachmentHandle handle = openSemaphore(directory, name, global); + try { + while (count-- > 0) { + int result = Kernel32.INSTANCE.WaitForSingleObject(handle.getHandle(), 0); + switch (result) { + case WinBase.WAIT_ABANDONED: + case WinBase.WAIT_OBJECT_0: + break; + case WinError.WAIT_TIMEOUT: + return; + default: + throw new Win32Exception(result); + } + } + } finally { + handle.close(); + } + } + + /** + * {@inheritDoc} + */ + public void chownFileToUser(File file, long userId) { + /* do nothing */ + } + + /** + * Opens a semaphore for signaling another process that an attachment is performed. + * + * @param directory The control directory. + * @param name The semaphore's name. + * @param global {@code true} if the semaphore is in the global namespace. + * @return A handle for signaling an attachment to the target process. + */ + private AttachmentHandle openSemaphore(File directory, String name, boolean global) { + WinNT.SECURITY_DESCRIPTOR securityDescriptor = new WinNT.SECURITY_DESCRIPTOR(64 * 1024); + try { + if (!Advapi32.INSTANCE.InitializeSecurityDescriptor(securityDescriptor, WinNT.SECURITY_DESCRIPTOR_REVISION)) { + throw new Win32Exception(Kernel32.INSTANCE.GetLastError()); + } + if (!Advapi32.INSTANCE.SetSecurityDescriptorDacl(securityDescriptor, true, null, true)) { + throw new Win32Exception(Kernel32.INSTANCE.GetLastError()); + } + WindowsLibrary.SecurityAttributes securityAttributes = new WindowsLibrary.SecurityAttributes(); + try { + securityAttributes.length = new WinDef.DWORD(securityAttributes.size()); + securityAttributes.securityDescriptor = securityDescriptor.getPointer(); + WinNT.HANDLE mutex = library.CreateMutex(securityAttributes, false, CREATION_MUTEX_NAME); + if (mutex == null) { + int lastError = Kernel32.INSTANCE.GetLastError(); + if (lastError == WinError.ERROR_ALREADY_EXISTS) { + mutex = library.OpenMutex(WinNT.STANDARD_RIGHTS_REQUIRED | WinNT.SYNCHRONIZE | 0x0001, false, CREATION_MUTEX_NAME); + if (mutex == null) { + throw new Win32Exception(Kernel32.INSTANCE.GetLastError()); + } + } else { + throw new Win32Exception(lastError); + } + } + int result = Kernel32.INSTANCE.WaitForSingleObject(mutex, 2000); + switch (result) { + case WinBase.WAIT_FAILED: + case WinError.WAIT_TIMEOUT: + throw new Win32Exception(result); + default: + try { + String target = (global ? "Global\\" : "") + + (directory.getAbsolutePath() + '_' + name).replaceAll("[^a-zA-Z0-9_]", "") + + "_semaphore"; + WinNT.HANDLE parent = library.OpenSemaphoreW(WindowsLibrary.SEMAPHORE_ALL_ACCESS, false, target); + if (parent == null) { + parent = library.CreateSemaphoreW(null, 0, Integer.MAX_VALUE, target); + if (parent == null) { + throw new Win32Exception(Kernel32.INSTANCE.GetLastError()); + } + WinNT.HANDLE child = library.CreateSemaphoreW(null, 0, Integer.MAX_VALUE, target + "_set0"); + if (child == null) { + throw new Win32Exception(Kernel32.INSTANCE.GetLastError()); + } + return new AttachmentHandle(parent, child); + } else { + WinNT.HANDLE child = library.OpenSemaphoreW(WindowsLibrary.SEMAPHORE_ALL_ACCESS, false, target + "_set0"); + if (child == null) { + throw new Win32Exception(Kernel32.INSTANCE.GetLastError()); + } + return new AttachmentHandle(parent, child); + } + } finally { + if (!library.ReleaseMutex(mutex)) { + throw new Win32Exception(Native.getLastError()); + } + } + } + } finally { + securityAttributes = null; + } + } finally { + securityDescriptor = null; + } + } + + /** + * A library for interacting with Windows. + */ + protected interface WindowsLibrary extends StdCallLibrary { + + /** + * Indicates that a semaphore requires all access rights. + */ + int SEMAPHORE_ALL_ACCESS = 0x1F0003; + + /** + * Opens an existing semaphore. + * + * @param access The access rights. + * @param inheritHandle {@code true} if the handle is inherited. + * @param name The semaphore's name. + * @return The handle or {@code null} if the handle could not be created. + */ + + @SuppressWarnings("checkstyle:methodname") + WinNT.HANDLE OpenSemaphoreW(int access, boolean inheritHandle, String name); + + /** + * Creates a new semaphore. + * + * @param securityAttributes The security attributes for the created semaphore. + * @param count The initial count for the semaphore. + * @param maximumCount The maximum count for the semaphore. + * @param name The semaphore's name. + * @return The handle or {@code null} if the handle could not be created. + */ + + @SuppressWarnings("checkstyle:methodname") + WinNT.HANDLE CreateSemaphoreW(WinBase.SECURITY_ATTRIBUTES securityAttributes, + long count, + long maximumCount, + String name); + + /** + * Releases the semaphore. + * + * @param handle The semaphore's handle. + * @param count The amount with which to increase the semaphore. + * @param previousCount The previous count of the semaphore or {@code null}. + * @return {@code true} if the semaphore was successfully released. + */ + @SuppressWarnings("checkstyle:methodname") + boolean ReleaseSemaphore(WinNT.HANDLE handle, long count, Long previousCount); + + /** + * Create or opens a mutex. + * + * @param attributes The mutex's security attributes. + * @param owner {@code true} if the caller is supposed to be the initial owner. + * @param name The mutex name. + * @return The handle to the mutex or {@code null} if the mutex could not be created. + */ + + @SuppressWarnings("checkstyle:methodname") + WinNT.HANDLE CreateMutex(SecurityAttributes attributes, boolean owner, String name); + + /** + * Opens an existing object. + * + * @param access The required access privileges. + * @param inherit {@code true} if the mutex should be inherited. + * @param name The mutex's name. + * @return The handle or {@code null} if the mutex could not be opened. + */ + @SuppressWarnings("checkstyle:methodname") + WinNT.HANDLE OpenMutex(int access, boolean inherit, String name); + + /** + * Releases the supplied mutex. + * + * @param handle The handle to the mutex. + * @return {@code true} if the handle was successfully released. + */ + @SuppressWarnings("checkstyle:methodname") + boolean ReleaseMutex(WinNT.HANDLE handle); + + /** + * A structure representing a mutex's security attributes. + */ + class SecurityAttributes extends Structure { + + /** + * The descriptor's length. + */ + + public WinDef.DWORD length; + + /** + * A pointer to the descriptor. + */ + + public Pointer securityDescriptor; + + /** + * {@code true} if the attributes are inherited. + */ + @SuppressWarnings("unused") + public boolean inherit; + + @Override + protected List getFieldOrder() { + return Arrays.asList("length", "securityDescriptor", "inherit"); + } + } + } + + /** + * A handle for an attachment which is represented by a pair of handles. + */ + protected static class AttachmentHandle implements Closeable { + + /** + * The parent handle. + */ + private final WinNT.HANDLE parent; + + /** + * The child handle. + */ + private final WinNT.HANDLE child; + + /** + * Creates a new attachment handle. + * + * @param parent The parent handle. + * @param child The child handle. + */ + protected AttachmentHandle(WinNT.HANDLE parent, WinNT.HANDLE child) { + this.parent = parent; + this.child = child; + } + + /** + * Returns the handle on which signals are to be sent. + * + * @return The handle on which signals are to be sent. + */ + protected WinNT.HANDLE getHandle() { + return child; + } + + /** + * {@inheritDoc} + */ + public void close() { + boolean closed; + try { + if (!Kernel32.INSTANCE.CloseHandle(child)) { + throw new Win32Exception(Kernel32.INSTANCE.GetLastError()); + } + } finally { + closed = Kernel32.INSTANCE.CloseHandle(parent); + } + if (!closed) { + throw new Win32Exception(Kernel32.INSTANCE.GetLastError()); + } + } + } + } + } + } +} \ No newline at end of file diff --git a/memshell-agent/memshell-agent-attacher/src/main/resources/win32-x86-64/attach_hotspot_windows.dll b/memshell-agent/memshell-agent-attacher/src/main/resources/win32-x86-64/attach_hotspot_windows.dll new file mode 100755 index 00000000..6748a72a Binary files /dev/null and b/memshell-agent/memshell-agent-attacher/src/main/resources/win32-x86-64/attach_hotspot_windows.dll differ diff --git a/memshell-agent/memshell-agent-attacher/src/main/resources/win32-x86/attach_hotspot_windows.dll b/memshell-agent/memshell-agent-attacher/src/main/resources/win32-x86/attach_hotspot_windows.dll new file mode 100755 index 00000000..38046bfb Binary files /dev/null and b/memshell-agent/memshell-agent-attacher/src/main/resources/win32-x86/attach_hotspot_windows.dll differ diff --git a/memshell-party-bom/build.gradle b/memshell-party-bom/build.gradle index 52608189..676dfdb3 100644 --- a/memshell-party-bom/build.gradle +++ b/memshell-party-bom/build.gradle @@ -11,6 +11,9 @@ dependencies { api 'net.bytebuddy:byte-buddy:1.17.5' api 'org.ow2.asm:asm-commons:9.7.1' + api 'net.java.dev.jna:jna:5.17.0' + api 'net.java.dev.jna:jna-platform:5.17.0' + api 'javax.servlet:javax.servlet-api:3.0.1' api 'jakarta.servlet:jakarta.servlet-api:6.0.0' api 'javax.websocket:javax.websocket-api:1.1' diff --git a/memshell-party-common/src/main/java/com/reajason/javaweb/buddy/ClassRenameUtils.java b/memshell-party-common/src/main/java/com/reajason/javaweb/buddy/ClassRenameUtils.java new file mode 100644 index 00000000..fbb717a3 --- /dev/null +++ b/memshell-party-common/src/main/java/com/reajason/javaweb/buddy/ClassRenameUtils.java @@ -0,0 +1,56 @@ +package com.reajason.javaweb.buddy; + +import com.reajason.javaweb.asm.InnerClassDiscovery; +import lombok.SneakyThrows; +import net.bytebuddy.ByteBuddy; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.dynamic.ClassFileLocator; +import net.bytebuddy.dynamic.DynamicType; +import net.bytebuddy.dynamic.scaffold.TypeValidation; +import net.bytebuddy.jar.asm.Opcodes; +import net.bytebuddy.pool.TypePool; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +/** + * @author ReaJason + * @since 2025/5/22 + */ +public class ClassRenameUtils { + + @SneakyThrows + public static Map renamePackage(Class clazz, String packageName) { + String originalClassName = clazz.getName(); + String originalPackageName = clazz.getPackage().getName(); + String newClassName = packageName + "." + clazz.getSimpleName(); + Map map = new HashMap<>(); + try (DynamicType.Unloaded make = new ByteBuddy() + .redefine(clazz) + .visit(new ClassRenameVisitorWrapper(originalClassName, newClassName)) + .visit(new TargetJreVersionVisitorWrapper(Opcodes.V1_6)) + .make()) { + map.put(newClassName, make.getBytes()); + } + Set innerClassNames = InnerClassDiscovery.findAllInnerClasses(clazz); + ClassFileLocator classFileLocator = ClassFileLocator.ForClassLoader.of(clazz.getClassLoader()); + TypePool typePool = TypePool.Default.of(classFileLocator); + for (String innerClassName : innerClassNames) { + TypeDescription innerTypeDesc = typePool.describe(innerClassName).resolve(); + String newInnerClassName = innerClassName.replace(originalClassName, newClassName); + DynamicType.Builder innerBuilder = new ByteBuddy() + .with(TypeValidation.DISABLED) + .redefine(innerTypeDesc, classFileLocator) + .visit(new ClassRenameVisitorWrapper(originalPackageName, packageName)) + .visit(new TargetJreVersionVisitorWrapper(Opcodes.V1_6)); + + try (DynamicType.Unloaded unloaded = innerBuilder.make()) { + for (Map.Entry entry : unloaded.getAllTypes().entrySet()) { + map.put(newInnerClassName, entry.getValue()); + } + } + } + return map; + } +} diff --git a/settings.gradle b/settings.gradle index 9f0dab69..a454deae 100644 --- a/settings.gradle +++ b/settings.gradle @@ -35,7 +35,7 @@ include 'vul:vul-springboot2-webflux' include 'vul:vul-springboot3-webflux' include 'memshell-agent' +include 'memshell-agent:memshell-agent-attacher' include 'memshell-agent:memshell-agent-asm' include 'memshell-agent:memshell-agent-javassist' -include 'memshell-agent:memshell-agent-bytebuddy' - +include 'memshell-agent:memshell-agent-bytebuddy' \ No newline at end of file diff --git a/web/src/i18n/en.json b/web/src/i18n/en.json index 0ce30a20..33ac2021 100644 --- a/web/src/i18n/en.json +++ b/web/src/i18n/en.json @@ -65,7 +65,9 @@ "ScriptEngine": "ScriptEngine", "SpEL": "SpEL", "Velocity": "Velocity", - "XxlJob": "XXL-JOB Executor" + "XxlJob": "XXL-JOB Executor", + "AgentJarWithJDKAttacher": "AgentJarWithJDKAttacher", + "AgentJarWithJREAttacher": "AgentJarWithJREAttacher" }, "title": "Package Method" }, diff --git a/web/src/i18n/zh-CN.json b/web/src/i18n/zh-CN.json index 20b40022..d4223291 100644 --- a/web/src/i18n/zh-CN.json +++ b/web/src/i18n/zh-CN.json @@ -65,7 +65,9 @@ "ScriptEngine": "内置脚本引擎", "SpEL": "SpEL 表达式", "Velocity": "Velocity", - "XxlJob": "XXL-JOB Executor" + "XxlJob": "XXL-JOB Executor", + "AgentJarWithJDKAttacher": "AgentJarWithJDKAttacher", + "AgentJarWithJREAttacher": "AgentJarWithJREAttacher" }, "title": "打包方式" },