diff --git a/pom.xml b/pom.xml index bb1b493c..6d5f843a 100644 --- a/pom.xml +++ b/pom.xml @@ -125,6 +125,7 @@ 3.6.3 11 1.7.36 + 9.6 1C 2023-11-17T00:21:18Z @@ -193,6 +194,17 @@ 1.4.0 + + org.ow2.asm + asm + ${asm.version} + + + org.ow2.asm + asm-commons + ${asm.version} + + junit junit @@ -354,15 +366,6 @@ - - java17+ - - [17,) - - - -Djava.security.manager=allow - - run-its @@ -388,11 +391,6 @@ ${project.version} - - ${invoker.security.manager} diff --git a/src/it/projects/mexec-gh-389-block-exit-non-zero/verify.groovy b/src/it/projects/mexec-gh-389-block-exit-non-zero/verify.groovy index 6d7872a9..914185bf 100644 --- a/src/it/projects/mexec-gh-389-block-exit-non-zero/verify.groovy +++ b/src/it/projects/mexec-gh-389-block-exit-non-zero/verify.groovy @@ -24,4 +24,4 @@ assert buildLogLines[infoMessageLineNumber - 1] == "[one, two, three]" // Verify that subsequent lines contain the beginning of the thrown SystemExitException stack trace assert buildLogLines[infoMessageLineNumber + 1].startsWith("[WARNING]") assert buildLogLines[infoMessageLineNumber + 2].contains("SystemExitException: System::exit was called with return code 123") -assert buildLogLines[infoMessageLineNumber + 3].contains("SystemExitManager.checkExit (SystemExitManager.java") +assert buildLogLines[infoMessageLineNumber + 3].contains("SystemExitManager.exit (SystemExitManager.java") diff --git a/src/main/java/org/codehaus/mojo/exec/BlockExitTransformer.java b/src/main/java/org/codehaus/mojo/exec/BlockExitTransformer.java new file mode 100644 index 00000000..f48502ca --- /dev/null +++ b/src/main/java/org/codehaus/mojo/exec/BlockExitTransformer.java @@ -0,0 +1,91 @@ +package org.codehaus.mojo.exec; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.lang.instrument.ClassFileTransformer; +import java.security.ProtectionDomain; + +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.commons.GeneratorAdapter; + +import static org.objectweb.asm.ClassReader.EXPAND_FRAMES; +import static org.objectweb.asm.ClassWriter.COMPUTE_FRAMES; +import static org.objectweb.asm.Opcodes.ASM9; + +public class BlockExitTransformer implements ClassFileTransformer { + @Override + public byte[] transform( + final ClassLoader loader, + final String className, + final Class classBeingRedefined, + final ProtectionDomain protectionDomain, + final byte[] classfileBuffer) { + try { + final ClassReader reader = new ClassReader(classfileBuffer); + final ClassWriter writer = new ClassWriter(COMPUTE_FRAMES); + final SystemExitOverrideVisitor visitor = new SystemExitOverrideVisitor(writer); + reader.accept(visitor, EXPAND_FRAMES); + return writer.toByteArray(); + } catch (final RuntimeException re) { // too old asm for ex, ignore these classes to not block the rest + return null; + } + } + + private static class SystemExitOverrideVisitor extends ClassVisitor { + private static final String SYSTEM_REPLACEMENT = + SystemExitManager.class.getName().replace('.', '/'); + + private SystemExitOverrideVisitor(final ClassVisitor visitor) { + super(ASM9, visitor); + } + + @Override + public MethodVisitor visitMethod( + final int access, + final String name, + final String descriptor, + final String signature, + final String[] exceptions) { + return new GeneratorAdapter( + ASM9, + super.visitMethod(access, name, descriptor, signature, exceptions), + access, + name, + descriptor) { + @Override + public void visitMethodInsn( + final int opcode, + final String owner, + final String name, + final String descriptor, + final boolean isInterface) { + if (owner.equals("java/lang/System") && name.equals("exit")) { + mv.visitMethodInsn(opcode, SYSTEM_REPLACEMENT, name, descriptor, isInterface); + } else { + super.visitMethodInsn(opcode, owner, name, descriptor, isInterface); + } + } + }; + } + } +} diff --git a/src/main/java/org/codehaus/mojo/exec/ExecJavaMojo.java b/src/main/java/org/codehaus/mojo/exec/ExecJavaMojo.java index 8721b7af..9175adf1 100644 --- a/src/main/java/org/codehaus/mojo/exec/ExecJavaMojo.java +++ b/src/main/java/org/codehaus/mojo/exec/ExecJavaMojo.java @@ -202,11 +202,6 @@ public class ExecJavaMojo extends AbstractExecMojo { * exception. This way, the error is propagated without terminating the whole Maven JVM. In previous versions, users * had to use the {@code exec} instead of the {@code java} goal in such cases, which now with this option is no * longer necessary. - *

- * Caveat: Since JDK 17, you need to explicitly allow security manager usage when using this option, e.g. by - * setting {@code -Djava.security.manager=allow} in {@code MAVEN_OPTS}. Otherwise, the JVM will throw an - * {@link UnsupportedOperationException} with a message like "The Security Manager is deprecated and will be removed - * in a future release". * * @since 3.2.0 */ @@ -266,8 +261,6 @@ public void execute() throws MojoExecutionException, MojoFailureException { bootClassName = mainClass; } - SecurityManager originalSecurityManager = System.getSecurityManager(); - try { Class bootClass = Thread.currentThread().getContextClassLoader().loadClass(bootClassName); @@ -277,9 +270,6 @@ public void execute() throws MojoExecutionException, MojoFailureException { MethodHandle mainHandle = lookup.findStatic(bootClass, "main", MethodType.methodType(void.class, String[].class)); - if (blockSystemExit) { - System.setSecurityManager(new SystemExitManager(originalSecurityManager)); - } mainHandle.invoke(arguments); } catch (IllegalAccessException | NoSuchMethodException | NoSuchMethodError e) { // just pass it on Thread.currentThread() @@ -302,10 +292,6 @@ public void execute() throws MojoExecutionException, MojoFailureException { } } catch (Throwable e) { // just pass it on Thread.currentThread().getThreadGroup().uncaughtException(Thread.currentThread(), e); - } finally { - if (blockSystemExit) { - System.setSecurityManager(originalSecurityManager); - } } }, mainClass + ".main()"); @@ -329,7 +315,7 @@ public void execute() throws MojoExecutionException, MojoFailureException { try { threadGroup.destroy(); - } catch (IllegalThreadStateException e) { + } catch (RuntimeException /* missing method in future java version */ e) { getLog().warn("Couldn't destroy threadgroup " + threadGroup, e); } } @@ -544,11 +530,14 @@ private URLClassLoader getClassLoader() throws MojoExecutionException { this.addAdditionalClasspathElements(classpathURLs); try { - return URLClassLoaderBuilder.builder() + final URLClassLoaderBuilder builder = URLClassLoaderBuilder.builder() .setLogger(getLog()) .setPaths(classpathURLs) - .setExclusions(classpathFilenameExclusions) - .build(); + .setExclusions(classpathFilenameExclusions); + if (blockSystemExit) { + builder.setTransformer(new BlockExitTransformer()); + } + return builder.build(); } catch (NullPointerException | IOException e) { throw new MojoExecutionException(e.getMessage(), e); } diff --git a/src/main/java/org/codehaus/mojo/exec/SystemExitManager.java b/src/main/java/org/codehaus/mojo/exec/SystemExitManager.java index fa8dd0a1..1a4ceecd 100644 --- a/src/main/java/org/codehaus/mojo/exec/SystemExitManager.java +++ b/src/main/java/org/codehaus/mojo/exec/SystemExitManager.java @@ -16,19 +16,14 @@ * limitations under the License. */ -import java.security.Permission; - /** - * A special security manager (SM) passing on permission checks to the original SM it replaces, except for - * {@link #checkExit(int)} + * Will be used by {@link BlockExitTransformer} to replace {@link System#exit(int)} by this implementation. * * @author Alexander Kriegisch */ -public class SystemExitManager extends SecurityManager { - private final SecurityManager originalSecurityManager; - - public SystemExitManager(SecurityManager originalSecurityManager) { - this.originalSecurityManager = originalSecurityManager; +public final class SystemExitManager { + private SystemExitManager() { + // no-op } /** @@ -52,15 +47,7 @@ public SystemExitManager(SecurityManager originalSecurityManager) { * * @param status the exit status */ - @Override - public void checkExit(int status) { + public static void exit(final int status) { throw new SystemExitException("System::exit was called with return code " + status, status); } - - @Override - public void checkPermission(Permission perm) { - if (originalSecurityManager != null) { - originalSecurityManager.checkPermission(perm); - } - } } diff --git a/src/main/java/org/codehaus/mojo/exec/URLClassLoaderBuilder.java b/src/main/java/org/codehaus/mojo/exec/URLClassLoaderBuilder.java index 36a6b68b..2438695e 100644 --- a/src/main/java/org/codehaus/mojo/exec/URLClassLoaderBuilder.java +++ b/src/main/java/org/codehaus/mojo/exec/URLClassLoaderBuilder.java @@ -22,6 +22,9 @@ import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; +import java.io.InputStream; +import java.lang.instrument.ClassFileTransformer; +import java.lang.instrument.IllegalClassFormatException; import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; @@ -34,6 +37,7 @@ import java.util.List; import org.apache.maven.plugin.logging.Log; +import org.codehaus.plexus.util.IOUtil; import static java.util.Arrays.asList; @@ -46,6 +50,7 @@ class URLClassLoaderBuilder { private Log logger; private Collection paths; private Collection exclusions; + private ClassFileTransformer transformer; private URLClassLoaderBuilder() {} @@ -53,6 +58,11 @@ static URLClassLoaderBuilder builder() { return new URLClassLoaderBuilder(); } + public URLClassLoaderBuilder setTransformer(final ClassFileTransformer transformer) { + this.transformer = transformer; + return this; + } + URLClassLoaderBuilder setLogger(Log logger) { this.logger = logger; return this; @@ -86,7 +96,7 @@ URLClassLoader build() throws IOException { } } - return new ExecJavaClassLoader(urls.toArray(new URL[0])); + return new ExecJavaClassLoader(urls.toArray(new URL[0]), transformer, logger); } // child first strategy @@ -100,10 +110,14 @@ private static class ExecJavaClassLoader extends URLClassLoader { } private final String jre; + private final Log logger; + private final ClassFileTransformer transformer; - public ExecJavaClassLoader(URL[] urls) { + public ExecJavaClassLoader(final URL[] urls, final ClassFileTransformer transformer, final Log logger) { super(urls); - jre = getJre(); + this.jre = getJre(); + this.logger = logger; + this.transformer = transformer; } @Override @@ -112,6 +126,10 @@ public Class loadClass(final String name, final boolean resolve) throws Class throw new ClassNotFoundException(); } + if ("org.codehaus.mojo.exec.SystemExitManager".equals(name)) { + return SystemExitManager.class; + } + synchronized (getClassLoadingLock(name)) { Class clazz; @@ -135,7 +153,7 @@ public Class loadClass(final String name, final boolean resolve) throws Class // look for it in this classloader try { - clazz = super.findClass(name); + clazz = transformer != null ? doFindClass(name) : super.findClass(name); if (clazz != null) { if (postLoad(resolve, clazz)) { return clazz; @@ -164,6 +182,23 @@ public Class loadClass(final String name, final boolean resolve) throws Class } } + private Class doFindClass(final String name) throws ClassNotFoundException { + final String resource = name.replace('.', '/') + ".class"; + final URL url = super.findResource(resource); + if (url == null) { + throw new ClassNotFoundException(name); + } + + try (final InputStream inputStream = url.openStream()) { + final byte[] raw = IOUtil.toByteArray(inputStream); + final byte[] res = transformer.transform(this, name, null, null, raw); + final byte[] bin = res == null ? raw : res; + return super.defineClass(name, bin, 0, bin.length); + } catch (final ClassFormatError | IOException | IllegalClassFormatException var4) { + throw new ClassNotFoundException(name, var4); + } + } + @Override public Enumeration getResources(String name) throws IOException { final Enumeration selfResources = findResources(name);