diff --git a/pom.xml b/pom.xml index a563fa87..6657bd04 100644 --- a/pom.xml +++ b/pom.xml @@ -94,6 +94,15 @@ Europe/Berlin + + Alexander Kriegisch + protected + Scrum-Master.de - Agiles Projektmanagement + https://scrum-master.de + + Feature Contributor + + @@ -335,6 +344,15 @@ 1 + + java17+ + + [17,) + + + -Djava.security.manager=allow + + run-its @@ -360,6 +378,11 @@ ${project.version} + + ${invoker.security.manager} diff --git a/src/it/projects/mexec-gh-389-block-exit-non-zero/invoker.properties b/src/it/projects/mexec-gh-389-block-exit-non-zero/invoker.properties new file mode 100644 index 00000000..07bfe50a --- /dev/null +++ b/src/it/projects/mexec-gh-389-block-exit-non-zero/invoker.properties @@ -0,0 +1,3 @@ +invoker.goals = clean process-classes +invoker.buildResult = failure +invoker.debug = false diff --git a/src/it/projects/mexec-gh-389-block-exit-non-zero/pom.xml b/src/it/projects/mexec-gh-389-block-exit-non-zero/pom.xml new file mode 100644 index 00000000..39de6618 --- /dev/null +++ b/src/it/projects/mexec-gh-389-block-exit-non-zero/pom.xml @@ -0,0 +1,47 @@ + + + 4.0.0 + + + org.codehaus.mojo.exec.it + parent + 0.1 + + + mexec-gh-389 + 0.0.1-SNAPSHOT + + + + + org.codehaus.mojo + exec-maven-plugin + @project.version@ + + + process-classes + + java + + + Main + true + + + exitBehaviour + system-exit-error + + + + one + two + three + + + + + + + + diff --git a/src/it/projects/mexec-gh-389-block-exit-non-zero/src/main/java/Main.java b/src/it/projects/mexec-gh-389-block-exit-non-zero/src/main/java/Main.java new file mode 100644 index 00000000..ba642258 --- /dev/null +++ b/src/it/projects/mexec-gh-389-block-exit-non-zero/src/main/java/Main.java @@ -0,0 +1,19 @@ +import java.util.Arrays; + +public class Main +{ + public static void main( String[] args ) + { + System.out.println( Arrays.toString( args ) ); + switch ( System.getProperty( "exitBehaviour", "ok" ) ) + { + case "throw-exception": + throw new RuntimeException( "uh-oh" ); + case "system-exit-ok": + System.exit( 0 ); + case "system-exit-error": + System.exit( 123 ); + } + System.out.println( "OK" ); + } +} 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 new file mode 100644 index 00000000..6d7872a9 --- /dev/null +++ b/src/it/projects/mexec-gh-389-block-exit-non-zero/verify.groovy @@ -0,0 +1,27 @@ +/* + * 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. + */ + +def buildLogLines = new File( basedir, "build.log" ).readLines() + +// Find "System::exit was called" line index +def infoMessageLineNumber = buildLogLines.indexOf("[INFO] System::exit was called with return code 123") +assert infoMessageLineNumber > 0 +// Verify that preceding line is program output +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") diff --git a/src/it/projects/mexec-gh-389-block-exit-zero/invoker.properties b/src/it/projects/mexec-gh-389-block-exit-zero/invoker.properties new file mode 100644 index 00000000..22647cc2 --- /dev/null +++ b/src/it/projects/mexec-gh-389-block-exit-zero/invoker.properties @@ -0,0 +1,3 @@ +invoker.goals = clean process-classes +invoker.buildResult = success +invoker.debug = false diff --git a/src/it/projects/mexec-gh-389-block-exit-zero/pom.xml b/src/it/projects/mexec-gh-389-block-exit-zero/pom.xml new file mode 100644 index 00000000..67e32700 --- /dev/null +++ b/src/it/projects/mexec-gh-389-block-exit-zero/pom.xml @@ -0,0 +1,47 @@ + + + 4.0.0 + + + org.codehaus.mojo.exec.it + parent + 0.1 + + + mexec-gh-389 + 0.0.1-SNAPSHOT + + + + + org.codehaus.mojo + exec-maven-plugin + @project.version@ + + + process-classes + + java + + + Main + true + + + exitBehaviour + system-exit-ok + + + + one + two + three + + + + + + + + diff --git a/src/it/projects/mexec-gh-389-block-exit-zero/src/main/java/Main.java b/src/it/projects/mexec-gh-389-block-exit-zero/src/main/java/Main.java new file mode 100644 index 00000000..ba642258 --- /dev/null +++ b/src/it/projects/mexec-gh-389-block-exit-zero/src/main/java/Main.java @@ -0,0 +1,19 @@ +import java.util.Arrays; + +public class Main +{ + public static void main( String[] args ) + { + System.out.println( Arrays.toString( args ) ); + switch ( System.getProperty( "exitBehaviour", "ok" ) ) + { + case "throw-exception": + throw new RuntimeException( "uh-oh" ); + case "system-exit-ok": + System.exit( 0 ); + case "system-exit-error": + System.exit( 123 ); + } + System.out.println( "OK" ); + } +} diff --git a/src/it/projects/mexec-gh-389-block-exit-zero/verify.groovy b/src/it/projects/mexec-gh-389-block-exit-zero/verify.groovy new file mode 100644 index 00000000..51cec3e2 --- /dev/null +++ b/src/it/projects/mexec-gh-389-block-exit-zero/verify.groovy @@ -0,0 +1,23 @@ +/* + * 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. + */ + +def buildLogLines = new File( basedir, "build.log" ).readLines() + +// Find "System::exit was called" line index +def infoMessageLineNumber = buildLogLines.indexOf("[INFO] System::exit was called with return code 0") +assert infoMessageLineNumber > 0 +// Verify that preceding line is program output +assert buildLogLines[infoMessageLineNumber - 1] == "[one, two, three]" diff --git a/src/it/projects/mexec-gh-389-default-permit-exit/invoker.properties b/src/it/projects/mexec-gh-389-default-permit-exit/invoker.properties new file mode 100644 index 00000000..4c2f7483 --- /dev/null +++ b/src/it/projects/mexec-gh-389-default-permit-exit/invoker.properties @@ -0,0 +1,4 @@ +invoker.goals = clean process-classes +# Cannot not check result, because build terminates unexpectedly +# invoker.buildResult = failure +invoker.debug = false diff --git a/src/it/projects/mexec-gh-389-default-permit-exit/pom.xml b/src/it/projects/mexec-gh-389-default-permit-exit/pom.xml new file mode 100644 index 00000000..b4f6ba90 --- /dev/null +++ b/src/it/projects/mexec-gh-389-default-permit-exit/pom.xml @@ -0,0 +1,49 @@ + + + 4.0.0 + + + org.codehaus.mojo.exec.it + parent + 0.1 + + + mexec-gh-389 + 0.0.1-SNAPSHOT + + + + + org.codehaus.mojo + exec-maven-plugin + @project.version@ + + + process-classes + + java + + + Main + + + + + exitBehaviour + + system-exit-ok + + + + one + two + three + + + + + + + + diff --git a/src/it/projects/mexec-gh-389-default-permit-exit/src/main/java/Main.java b/src/it/projects/mexec-gh-389-default-permit-exit/src/main/java/Main.java new file mode 100644 index 00000000..ba642258 --- /dev/null +++ b/src/it/projects/mexec-gh-389-default-permit-exit/src/main/java/Main.java @@ -0,0 +1,19 @@ +import java.util.Arrays; + +public class Main +{ + public static void main( String[] args ) + { + System.out.println( Arrays.toString( args ) ); + switch ( System.getProperty( "exitBehaviour", "ok" ) ) + { + case "throw-exception": + throw new RuntimeException( "uh-oh" ); + case "system-exit-ok": + System.exit( 0 ); + case "system-exit-error": + System.exit( 123 ); + } + System.out.println( "OK" ); + } +} diff --git a/src/it/projects/mexec-gh-389-default-permit-exit/verify.groovy b/src/it/projects/mexec-gh-389-default-permit-exit/verify.groovy new file mode 100644 index 00000000..80a6d2d0 --- /dev/null +++ b/src/it/projects/mexec-gh-389-default-permit-exit/verify.groovy @@ -0,0 +1,21 @@ +/* + * 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. + */ + +def buildLogLines = new File( basedir, "build.log" ).readLines() + +// Second-last line is the last line the called program prints before exiting the JVM with System.exit. +// Last line is "Running post-build script: ...", i.e. we need to disregard it. +assert buildLogLines[-2] == "[one, two, three]" diff --git a/src/main/java/org/codehaus/mojo/exec/ExecJavaMojo.java b/src/main/java/org/codehaus/mojo/exec/ExecJavaMojo.java index f9444883..9f4245c1 100644 --- a/src/main/java/org/codehaus/mojo/exec/ExecJavaMojo.java +++ b/src/main/java/org/codehaus/mojo/exec/ExecJavaMojo.java @@ -206,6 +206,25 @@ public class ExecJavaMojo @Parameter private List classpathFilenameExclusions; + /** + * Whether to try and prohibit the called Java program from terminating the JVM (and with it the whole Maven build) + * by calling {@link System#exit(int)}. When active, a special security manager will intercept those calls. In case + * of an exit code 0 (OK), it will simply log the fact that {@link System#exit(int)} was called. Otherwise, it will + * throw a {@link SystemExitException}, failing the Maven goal as if the called Java code itself had exited with an + * 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 + */ + @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()}: + *

    + *
  • + * Exit status 0 (OK): Just log the fact that {@link System#exit(int)} was called. + *
  • + *
  • + * Exit status non-0 (error): In addition to logging, the exception is also passed on, failing the mojo + * execution as if the called Java code had terminated with an exception instead of trying to terminate the + * JVM with an error code. + *
  • + *
+ * + * @param status the exit status + */ + @Override + public void checkExit( 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 ); + } + } + +}