+ * 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 + */ + @Parameter( property = "exec.blockSystemExit", defaultValue = "false" ) + private boolean blockSystemExit; + /** * Execute goal. * @@ -255,6 +274,12 @@ public void execute() IsolatedThreadGroup threadGroup = new IsolatedThreadGroup( mainClass /* name */ ); Thread bootstrapThread = new Thread( threadGroup, new Runnable() { + // TODO: + // Adjust implementation for future JDKs after removal of SecurityManager. + // See https://openjdk.org/jeps/411 for basic information. + // See https://bugs.openjdk.org/browse/JDK-8199704 for details about how users might be able to block + // System::exit in post-removal JDKs (still undecided at the time of writing this comment). + @SuppressWarnings( "removal" ) public void run() { int sepIndex = mainClass.indexOf( '/' ); @@ -268,6 +293,8 @@ public void run() { bootClassName = mainClass; } + + SecurityManager originalSecurityManager = System.getSecurityManager(); try { @@ -279,6 +306,10 @@ public void run() 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 ) @@ -292,10 +323,25 @@ public void run() Throwable exceptionToReport = e.getCause() != null ? e.getCause() : e; Thread.currentThread().getThreadGroup().uncaughtException( Thread.currentThread(), exceptionToReport ); } + catch ( SystemExitException systemExitException ) + { + getLog().info( systemExitException.getMessage() ); + if ( systemExitException.getExitCode() != 0 ) + { + throw systemExitException; + } + } catch ( Throwable e ) { // just pass it on Thread.currentThread().getThreadGroup().uncaughtException( Thread.currentThread(), e ); } + finally + { + if ( blockSystemExit ) + { + System.setSecurityManager( originalSecurityManager ); + } + } } }, mainClass + ".main()" ); URLClassLoader classLoader = getClassLoader(); diff --git a/src/main/java/org/codehaus/mojo/exec/SystemExitException.java b/src/main/java/org/codehaus/mojo/exec/SystemExitException.java new file mode 100644 index 00000000..90127be5 --- /dev/null +++ b/src/main/java/org/codehaus/mojo/exec/SystemExitException.java @@ -0,0 +1,38 @@ +package org.codehaus.mojo.exec; + +/* + * Copyright MojoHaus and Contributors + * + * 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. + */ + +/** + * Exception to be thrown by {@link SystemExitManager} when {@link System#exit(int)} is called + * + * @author Alexander Kriegisch + */ +public class SystemExitException extends SecurityException +{ + private final int exitCode; + + public SystemExitException( String s, int exitCode ) + { + super( s ); + this.exitCode = exitCode; + } + + public int getExitCode() + { + return exitCode; + } +} diff --git a/src/main/java/org/codehaus/mojo/exec/SystemExitManager.java b/src/main/java/org/codehaus/mojo/exec/SystemExitManager.java new file mode 100644 index 00000000..a312b851 --- /dev/null +++ b/src/main/java/org/codehaus/mojo/exec/SystemExitManager.java @@ -0,0 +1,72 @@ +package org.codehaus.mojo.exec; + +/* + * Copyright MojoHaus and Contributors + * + * 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.security.Permission; + +/** + * A special security manager (SM) passing on permission checks to the original SM it replaces, except for + * {@link #checkExit(int)} + * + * @author Alexander Kriegisch + */ +public class SystemExitManager extends SecurityManager +{ + private final SecurityManager originalSecurityManager; + + public SystemExitManager( SecurityManager originalSecurityManager ) + { + this.originalSecurityManager = originalSecurityManager; + } + + /** + * Always throws a {@link SystemExitException} when {@link System#exit(int)} is called, instead of terminating the + * JVM. + *
+ * The exception is meant to be handled in the {@code exec:java} goal. On the one hand, this avoids that Java + * code called in process can terminate the JVM and the whole Maven build process with it. On the other hand, the + * exception handler can also differentiate between exit status 0 (OK) and non-0 (error) by inspecting + * {@link SystemExitException#getExitCode()}: + *