blob: 622572b3e05d65f06a485109a071c558d4fe7a6b [file] [log] [blame]
/*
* 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.
*/
package org.apache.lucene.util;
import java.lang.StackWalker.StackFrame;
import java.util.Locale;
import java.util.function.Predicate;
/**
* A {@link SecurityManager} that prevents tests calling {@link System#exit(int)}. Only the test
* runner itself is allowed to exit the JVM. All other security checks are handled by the default
* security policy.
*
* <p>Use this with {@code -Djava.security.manager=org.apache.lucene.util.TestSecurityManager}.
*/
public final class TestSecurityManager extends SecurityManager {
private static final String JUNIT4_TEST_RUNNER_PACKAGE = "com.carrotsearch.ant.tasks.junit4.";
private static final String ECLIPSE_TEST_RUNNER_PACKAGE =
"org.eclipse.jdt.internal.junit.runner.";
private static final String IDEA_TEST_RUNNER_PACKAGE = "com.intellij.rt.execution.junit.";
private static final String GRADLE_TEST_RUNNER_PACKAGE =
"worker.org.gradle.process.internal.worker.";
private static final String SYSTEM_CLASS_NAME = System.class.getName();
private static final String RUNTIME_CLASS_NAME = Runtime.class.getName();
/**
* Creates a new TestSecurityManager. This ctor is called on JVM startup, when {@code
* -Djava.security.manager=org.apache.lucene.util.TestSecurityManager} is passed to JVM.
*/
public TestSecurityManager() {
super();
}
/**
* {@inheritDoc}
*
* <p>This method inspects the stack trace and checks who is calling {@link System#exit(int)} and
* similar methods
*
* @throws SecurityException if the caller of this method is not the test runner itself.
*/
@Override
public void checkExit(final int status) {
if (StackWalker.getInstance()
.walk(
s ->
// skip all internal stack frames
s.dropWhile(Predicate.not(TestSecurityManager::isExitStackFrame))
// skip all exit()/halt() stack frames
.dropWhile(TestSecurityManager::isExitStackFrame)
.limit(1) // only look at one more frame (caller of exit)
.map(StackFrame::getClassName)
.noneMatch(
c ->
c.startsWith(JUNIT4_TEST_RUNNER_PACKAGE)
|| c.startsWith(ECLIPSE_TEST_RUNNER_PACKAGE)
|| c.startsWith(IDEA_TEST_RUNNER_PACKAGE)
|| c.startsWith(GRADLE_TEST_RUNNER_PACKAGE)))) {
throw new SecurityException(
String.format(
Locale.ENGLISH,
"System/Runtime.exit(%1$d) or halt(%1$d) calls are not allowed because they terminate the test runner's JVM.",
status));
}
// we passed the stack check, delegate to super, so default policy can still deny permission:
super.checkExit(status);
}
private static boolean isExitStackFrame(StackFrame f) {
final String methodName = f.getMethodName(), className = f.getClassName();
return ("exit".equals(methodName) || "halt".equals(methodName))
&& (SYSTEM_CLASS_NAME.equals(className) || RUNTIME_CLASS_NAME.equals(className));
}
}