Compile each Groovy script in its own classloader

closes #18572
This commit is contained in:
Tanguy Leroux 2016-06-16 10:39:14 +02:00
parent 6b46bf13f0
commit 98951b1203
21 changed files with 92 additions and 217 deletions

View File

@ -39,9 +39,9 @@ public class CompiledScript {
public CompiledScript(ScriptService.ScriptType type, String name, String lang, Object compiled) { public CompiledScript(ScriptService.ScriptType type, String name, String lang, Object compiled) {
this.type = type; this.type = type;
this.name = name; this.name = name;
this.lang = lang; this.lang = lang;
this.compiled = compiled; this.compiled = compiled;
} }
/** /**
* Method to get the type of language. * Method to get the type of language.

View File

@ -90,11 +90,6 @@ public class NativeScriptEngineService extends AbstractComponent implements Scri
public void close() { public void close() {
} }
@Override
public void scriptRemoved(CompiledScript script) {
// Nothing to do here
}
@Override @Override
public boolean isInlineScriptEnabled() { public boolean isInlineScriptEnabled() {
return true; return true;

View File

@ -23,7 +23,6 @@ import org.elasticsearch.common.Nullable;
import org.elasticsearch.search.lookup.SearchLookup; import org.elasticsearch.search.lookup.SearchLookup;
import java.io.Closeable; import java.io.Closeable;
import java.util.List;
import java.util.Map; import java.util.Map;
/** /**
@ -49,13 +48,6 @@ public interface ScriptEngineService extends Closeable {
SearchScript search(CompiledScript compiledScript, SearchLookup lookup, @Nullable Map<String, Object> vars); SearchScript search(CompiledScript compiledScript, SearchLookup lookup, @Nullable Map<String, Object> vars);
/**
* Handler method called when a script is removed from the Guava cache.
*
* The passed script may be null if it has already been garbage collected.
* */
void scriptRemoved(@Nullable CompiledScript script);
/** /**
* Returns <code>true</code> if this scripting engine can safely accept inline scripts by default. The default is <code>false</code> * Returns <code>true</code> if this scripting engine can safely accept inline scripts by default. The default is <code>false</code>
*/ */

View File

@ -507,16 +507,10 @@ public class ScriptService extends AbstractComponent implements Closeable {
private class ScriptCacheRemovalListener implements RemovalListener<CacheKey, CompiledScript> { private class ScriptCacheRemovalListener implements RemovalListener<CacheKey, CompiledScript> {
@Override @Override
public void onRemoval(RemovalNotification<CacheKey, CompiledScript> notification) { public void onRemoval(RemovalNotification<CacheKey, CompiledScript> notification) {
scriptMetrics.onCacheEviction(); if (logger.isDebugEnabled()) {
for (ScriptEngineService service : scriptEngines) { logger.debug("removed {} from cache, reason: {}", notification.getValue(), notification.getRemovalReason());
try {
service.scriptRemoved(notification.getValue());
} catch (Exception e) {
logger.warn("exception calling script removal listener for script service", e);
// We don't rethrow because Guava would just catch the
// exception and log it, which we have already done
}
} }
scriptMetrics.onCacheEviction();
} }
} }

View File

@ -257,12 +257,6 @@ public class ScriptModesTests extends ESTestCase {
@Override @Override
public void close() { public void close() {
}
@Override
public void scriptRemoved(@Nullable CompiledScript script) {
} }
} }
} }

View File

@ -518,11 +518,6 @@ public class ScriptServiceTests extends ESTestCase {
} }
@Override
public void scriptRemoved(CompiledScript script) {
// Nothing to do here
}
@Override @Override
public boolean isInlineScriptEnabled() { public boolean isInlineScriptEnabled() {
return true; return true;
@ -562,12 +557,6 @@ public class ScriptServiceTests extends ESTestCase {
@Override @Override
public void close() { public void close() {
}
@Override
public void scriptRemoved(CompiledScript script) {
// Nothing to do here
} }
} }

View File

@ -95,12 +95,6 @@ public class ScriptSettingsTests extends ESTestCase {
@Override @Override
public void close() { public void close() {
}
@Override
public void scriptRemoved(@Nullable CompiledScript script) {
} }
} }

View File

@ -456,10 +456,6 @@ public class AvgIT extends AbstractNumericTestCase {
} }
}; };
} }
@Override
public void scriptRemoved(CompiledScript script) {
}
} }
/** /**
@ -564,10 +560,6 @@ public class AvgIT extends AbstractNumericTestCase {
}; };
} }
@Override
public void scriptRemoved(CompiledScript script) {
}
@Override @Override
public boolean isInlineScriptEnabled() { public boolean isInlineScriptEnabled() {
return true; return true;

View File

@ -453,10 +453,6 @@ public class SumIT extends AbstractNumericTestCase {
}; };
} }
@Override
public void scriptRemoved(CompiledScript script) {
}
@Override @Override
public boolean isInlineScriptEnabled() { public boolean isInlineScriptEnabled() {
return true; return true;
@ -573,10 +569,6 @@ public class SumIT extends AbstractNumericTestCase {
}; };
} }
@Override
public void scriptRemoved(CompiledScript script) {
}
@Override @Override
public boolean isInlineScriptEnabled() { public boolean isInlineScriptEnabled() {
return true; return true;

View File

@ -318,10 +318,6 @@ public class ValueCountIT extends ESIntegTestCase {
}; };
} }
@Override
public void scriptRemoved(CompiledScript script) {
}
@Override @Override
public boolean isInlineScriptEnabled() { public boolean isInlineScriptEnabled() {
return true; return true;

View File

@ -144,10 +144,6 @@ public class UpdateIT extends ESIntegTestCase {
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();
} }
@Override
public void scriptRemoved(CompiledScript script) {
}
@Override @Override
public boolean isInlineScriptEnabled() { public boolean isInlineScriptEnabled() {
return true; return true;
@ -214,15 +210,10 @@ public class UpdateIT extends ESIntegTestCase {
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();
} }
@Override
public void scriptRemoved(CompiledScript script) {
}
@Override @Override
public boolean isInlineScriptEnabled() { public boolean isInlineScriptEnabled() {
return true; return true;
} }
} }
public static class ScriptedUpsertScriptPlugin extends Plugin implements ScriptPlugin { public static class ScriptedUpsertScriptPlugin extends Plugin implements ScriptPlugin {
@ -285,10 +276,6 @@ public class UpdateIT extends ESIntegTestCase {
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();
} }
@Override
public void scriptRemoved(CompiledScript script) {
}
@Override @Override
public boolean isInlineScriptEnabled() { public boolean isInlineScriptEnabled() {
return true; return true;
@ -357,10 +344,6 @@ public class UpdateIT extends ESIntegTestCase {
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();
} }
@Override
public void scriptRemoved(CompiledScript script) {
}
@Override @Override
public boolean isInlineScriptEnabled() { public boolean isInlineScriptEnabled() {
return true; return true;

View File

@ -266,11 +266,6 @@ public class ExpressionScriptEngineService extends AbstractComponent implements
@Override @Override
public void close() {} public void close() {}
@Override
public void scriptRemoved(CompiledScript script) {
// Nothing to do
}
@Override @Override
public boolean isInlineScriptEnabled() { public boolean isInlineScriptEnabled() {
return true; return true;

View File

@ -33,9 +33,11 @@ import org.codehaus.groovy.classgen.GeneratorContext;
import org.codehaus.groovy.control.CompilationFailedException; import org.codehaus.groovy.control.CompilationFailedException;
import org.codehaus.groovy.control.CompilePhase; import org.codehaus.groovy.control.CompilePhase;
import org.codehaus.groovy.control.CompilerConfiguration; import org.codehaus.groovy.control.CompilerConfiguration;
import org.codehaus.groovy.control.MultipleCompilationErrorsException;
import org.codehaus.groovy.control.SourceUnit; import org.codehaus.groovy.control.SourceUnit;
import org.codehaus.groovy.control.customizers.CompilationCustomizer; import org.codehaus.groovy.control.customizers.CompilationCustomizer;
import org.codehaus.groovy.control.customizers.ImportCustomizer; import org.codehaus.groovy.control.customizers.ImportCustomizer;
import org.codehaus.groovy.control.messages.Message;
import org.elasticsearch.SpecialPermission; import org.elasticsearch.SpecialPermission;
import org.elasticsearch.bootstrap.BootstrapInfo; import org.elasticsearch.bootstrap.BootstrapInfo;
import org.elasticsearch.common.Nullable; import org.elasticsearch.common.Nullable;
@ -49,20 +51,26 @@ import org.elasticsearch.script.ExecutableScript;
import org.elasticsearch.script.LeafSearchScript; import org.elasticsearch.script.LeafSearchScript;
import org.elasticsearch.script.ScoreAccessor; import org.elasticsearch.script.ScoreAccessor;
import org.elasticsearch.script.ScriptEngineService; import org.elasticsearch.script.ScriptEngineService;
import org.elasticsearch.script.GeneralScriptException; import org.elasticsearch.script.ScriptException;
import org.elasticsearch.script.SearchScript; import org.elasticsearch.script.SearchScript;
import org.elasticsearch.search.lookup.LeafSearchLookup; import org.elasticsearch.search.lookup.LeafSearchLookup;
import org.elasticsearch.search.lookup.SearchLookup; import org.elasticsearch.search.lookup.SearchLookup;
import java.io.IOException; import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.security.AccessControlContext; import java.security.AccessControlContext;
import java.security.AccessController; import java.security.AccessController;
import java.security.PrivilegedAction; import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import static java.util.Collections.emptyList;
/** /**
* Provides the infrastructure for Groovy as a scripting language for Elasticsearch * Provides the infrastructure for Groovy as a scripting language for Elasticsearch
*/ */
@ -78,81 +86,40 @@ public class GroovyScriptEngineService extends AbstractComponent implements Scri
*/ */
public static final String GROOVY_INDY_SETTING_NAME = "indy"; public static final String GROOVY_INDY_SETTING_NAME = "indy";
private final GroovyClassLoader loader; /**
* Classloader used as a parent classloader for all Groovy scripts
*/
private final ClassLoader loader;
public GroovyScriptEngineService(Settings settings) { public GroovyScriptEngineService(Settings settings) {
super(settings); super(settings);
// Creates the classloader here in order to isolate Groovy-land code
ImportCustomizer imports = new ImportCustomizer();
imports.addStarImports("org.joda.time");
imports.addStaticStars("java.lang.Math");
CompilerConfiguration config = new CompilerConfiguration();
config.addCompilationCustomizers(imports);
// Add BigDecimal -> Double transformer
config.addCompilationCustomizers(new GroovyBigDecimalTransformer(CompilePhase.CONVERSION));
// always enable invokeDynamic, not the crazy softreference-based stuff
config.getOptimizationOptions().put(GROOVY_INDY_SETTING_NAME, true);
// Groovy class loader to isolate Groovy-land code
// classloader created here
final SecurityManager sm = System.getSecurityManager(); final SecurityManager sm = System.getSecurityManager();
if (sm != null) { if (sm != null) {
sm.checkPermission(new SpecialPermission()); sm.checkPermission(new SpecialPermission());
} }
this.loader = AccessController.doPrivileged(new PrivilegedAction<GroovyClassLoader>() { this.loader = AccessController.doPrivileged((PrivilegedAction<ClassLoader>) () -> {
@Override // snapshot our context (which has permissions for classes), since the script has none
public GroovyClassLoader run() { AccessControlContext context = AccessController.getContext();
// snapshot our context (which has permissions for classes), since the script has none return new ClassLoader(getClass().getClassLoader()) {
final AccessControlContext engineContext = AccessController.getContext(); @Override
return new GroovyClassLoader(new ClassLoader(getClass().getClassLoader()) { protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
@Override if (sm != null) {
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { try {
if (sm != null) { context.checkPermission(new ClassPermission(name));
try { } catch (SecurityException e) {
engineContext.checkPermission(new ClassPermission(name)); throw new ClassNotFoundException(name, e);
} catch (SecurityException e) {
throw new ClassNotFoundException(name, e);
}
} }
return super.loadClass(name, resolve);
} }
}, config); return super.loadClass(name, resolve);
}
});
}
@Override
public void close() {
loader.clearCache();
// close classloader here (why do we do this?)
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(new SpecialPermission());
}
AccessController.doPrivileged(new PrivilegedAction<Void>() {
@Override
public Void run() {
try {
loader.close();
} catch (IOException e) {
logger.warn("Unable to close Groovy loader", e);
} }
return null; };
}
}); });
} }
@Override @Override
public void scriptRemoved(@Nullable CompiledScript script) { public void close() throws IOException {
// script could be null, meaning the script has already been garbage collected // Nothing to do here
if (script == null || NAME.equals(script.lang())) {
// Clear the cache, this removes old script versions from the
// cache to prevent running out of PermGen space
loader.clearCache();
}
} }
@Override @Override
@ -167,30 +134,34 @@ public class GroovyScriptEngineService extends AbstractComponent implements Scri
@Override @Override
public Object compile(String scriptName, String scriptSource, Map<String, String> params) { public Object compile(String scriptName, String scriptSource, Map<String, String> params) {
try { // Create the script class name
// we reuse classloader, so do a security check just in case. String className = MessageDigests.toHexString(MessageDigests.sha1().digest(scriptSource.getBytes(StandardCharsets.UTF_8)));
SecurityManager sm = System.getSecurityManager();
if (sm != null) { final SecurityManager sm = System.getSecurityManager();
sm.checkPermission(new SpecialPermission()); if (sm != null) {
} sm.checkPermission(new SpecialPermission());
String fake = MessageDigests.toHexString(MessageDigests.sha1().digest(scriptSource.getBytes(StandardCharsets.UTF_8)));
// same logic as GroovyClassLoader.parseClass() but with a different codesource string:
return AccessController.doPrivileged(new PrivilegedAction<Object>() {
@Override
public Class<?> run() {
GroovyCodeSource gcs = new GroovyCodeSource(scriptSource, fake, BootstrapInfo.UNTRUSTED_CODEBASE);
gcs.setCachable(false);
// TODO: we could be more complicated and paranoid, and move this to separate block, to
// sandbox the compilation process itself better.
return loader.parseClass(gcs);
}
});
} catch (Throwable e) {
if (logger.isTraceEnabled()) {
logger.trace("exception compiling Groovy script:", e);
}
throw new GeneralScriptException("failed to compile groovy script", e);
} }
return AccessController.doPrivileged((PrivilegedAction<Object>) () -> {
try {
GroovyCodeSource codeSource = new GroovyCodeSource(scriptSource, className, BootstrapInfo.UNTRUSTED_CODEBASE);
codeSource.setCachable(false);
CompilerConfiguration configuration = new CompilerConfiguration()
.addCompilationCustomizers(new ImportCustomizer().addStarImports("org.joda.time").addStaticStars("java.lang.Math"))
.addCompilationCustomizers(new GroovyBigDecimalTransformer(CompilePhase.CONVERSION));
// always enable invokeDynamic, not the crazy softreference-based stuff
configuration.getOptimizationOptions().put(GROOVY_INDY_SETTING_NAME, true);
GroovyClassLoader groovyClassLoader = new GroovyClassLoader(loader, configuration);
return groovyClassLoader.parseClass(codeSource);
} catch (Throwable e) {
if (logger.isTraceEnabled()) {
logger.trace("Exception compiling Groovy script:", e);
}
throw convertToScriptException("Error compiling script " + className, scriptSource, e);
}
});
} }
/** /**
@ -215,7 +186,7 @@ public class GroovyScriptEngineService extends AbstractComponent implements Scri
} }
return new GroovyScript(compiledScript, createScript(compiledScript.compiled(), allVars), this.logger); return new GroovyScript(compiledScript, createScript(compiledScript.compiled(), allVars), this.logger);
} catch (ReflectiveOperationException e) { } catch (ReflectiveOperationException e) {
throw new GeneralScriptException("failed to build executable " + compiledScript, e); throw convertToScriptException("Failed to build executable script", compiledScript.name(), e);
} }
} }
@ -235,7 +206,7 @@ public class GroovyScriptEngineService extends AbstractComponent implements Scri
try { try {
scriptObject = createScript(compiledScript.compiled(), allVars); scriptObject = createScript(compiledScript.compiled(), allVars);
} catch (ReflectiveOperationException e) { } catch (ReflectiveOperationException e) {
throw new GeneralScriptException("failed to build search " + compiledScript, e); throw convertToScriptException("Failed to build search script", compiledScript.name(), e);
} }
return new GroovyScript(compiledScript, scriptObject, leafLookup, logger); return new GroovyScript(compiledScript, scriptObject, leafLookup, logger);
} }
@ -248,6 +219,29 @@ public class GroovyScriptEngineService extends AbstractComponent implements Scri
}; };
} }
/**
* Converts a {@link Throwable} to a {@link ScriptException}
*/
private ScriptException convertToScriptException(String message, String source, Throwable cause) {
List<String> stack = new ArrayList<>();
if (cause instanceof MultipleCompilationErrorsException) {
@SuppressWarnings({"unchecked"})
List<Message> errors = (List<Message>) ((MultipleCompilationErrorsException) cause).getErrorCollector().getErrors();
for (Message error : errors) {
try (StringWriter writer = new StringWriter()) {
error.write(new PrintWriter(writer));
stack.add(writer.toString());
} catch (IOException e1) {
logger.error("failed to write compilation error message to the stack", e1);
}
}
} else if (cause instanceof CompilationFailedException) {
CompilationFailedException error = (CompilationFailedException) cause;
stack.add(error.getMessage());
}
throw new ScriptException(message, cause, stack, source, NAME);
}
public static final class GroovyScript implements ExecutableScript, LeafSearchScript { public static final class GroovyScript implements ExecutableScript, LeafSearchScript {
private final CompiledScript compiledScript; private final CompiledScript compiledScript;
@ -298,17 +292,12 @@ public class GroovyScriptEngineService extends AbstractComponent implements Scri
try { try {
// NOTE: we truncate the stack because IndyInterface has security issue (needs getClassLoader) // NOTE: we truncate the stack because IndyInterface has security issue (needs getClassLoader)
// we don't do a security check just as a tradeoff, it cannot really escalate to anything. // we don't do a security check just as a tradeoff, it cannot really escalate to anything.
return AccessController.doPrivileged(new PrivilegedAction<Object>() { return AccessController.doPrivileged((PrivilegedAction<Object>) script::run);
@Override
public Object run() {
return script.run();
}
});
} catch (Throwable e) { } catch (Throwable e) {
if (logger.isTraceEnabled()) { if (logger.isTraceEnabled()) {
logger.trace("failed to run {}", e, compiledScript); logger.trace("failed to run {}", e, compiledScript);
} }
throw new GeneralScriptException("failed to run " + compiledScript, e); throw new ScriptException("Error evaluating " + compiledScript.name(), e, emptyList(), "", compiledScript.lang());
} }
} }

View File

@ -26,8 +26,6 @@ grant {
permission java.lang.RuntimePermission "accessDeclaredMembers"; permission java.lang.RuntimePermission "accessDeclaredMembers";
permission java.lang.RuntimePermission "accessClassInPackage.sun.reflect"; permission java.lang.RuntimePermission "accessClassInPackage.sun.reflect";
permission java.lang.RuntimePermission "accessClassInPackage.jdk.internal.reflect"; permission java.lang.RuntimePermission "accessClassInPackage.jdk.internal.reflect";
// needed by GroovyScriptEngineService to close its classloader (why?)
permission java.lang.RuntimePermission "closeClassLoader";
// Allow executing groovy scripts with codesource of /untrusted // Allow executing groovy scripts with codesource of /untrusted
permission groovy.security.GroovyCodeSourcePermission "/untrusted"; permission groovy.security.GroovyCodeSourcePermission "/untrusted";

View File

@ -24,7 +24,7 @@ import org.apache.lucene.util.Constants;
import org.codehaus.groovy.control.MultipleCompilationErrorsException; import org.codehaus.groovy.control.MultipleCompilationErrorsException;
import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.script.CompiledScript; import org.elasticsearch.script.CompiledScript;
import org.elasticsearch.script.GeneralScriptException; import org.elasticsearch.script.ScriptException;
import org.elasticsearch.script.ScriptService; import org.elasticsearch.script.ScriptService;
import org.elasticsearch.test.ESTestCase; import org.elasticsearch.test.ESTestCase;
@ -151,7 +151,7 @@ public class GroovySecurityTests extends ESTestCase {
try { try {
doTest(script); doTest(script);
fail("did not get expected exception"); fail("did not get expected exception");
} catch (GeneralScriptException expected) { } catch (ScriptException expected) {
Throwable cause = expected.getCause(); Throwable cause = expected.getCause();
assertNotNull(cause); assertNotNull(cause);
if (exceptionClass.isAssignableFrom(cause.getClass()) == false) { if (exceptionClass.isAssignableFrom(cause.getClass()) == false) {

View File

@ -51,7 +51,7 @@
- do: - do:
catch: /Unable.to.parse.*/ catch: /script_exception,.+Error.compiling.script.*/
put_script: put_script:
id: "1" id: "1"
lang: "groovy" lang: "groovy"

View File

@ -133,11 +133,6 @@ public final class MustacheScriptEngineService extends AbstractComponent impleme
// Nothing to do here // Nothing to do here
} }
@Override
public void scriptRemoved(CompiledScript script) {
// Nothing to do here
}
// permission checked before doing crazy reflection // permission checked before doing crazy reflection
static final SpecialPermission SPECIAL_PERMISSION = new SpecialPermission(); static final SpecialPermission SPECIAL_PERMISSION = new SpecialPermission();

View File

@ -202,15 +202,6 @@ public final class PainlessScriptEngineService extends AbstractComponent impleme
}; };
} }
/**
* Action taken when a script is removed from the cache.
* @param script The removed script.
*/
@Override
public void scriptRemoved(final CompiledScript script) {
// Nothing to do.
}
/** /**
* Action taken when the engine is closed. * Action taken when the engine is closed.
*/ */

View File

@ -148,11 +148,6 @@ public class JavaScriptScriptEngineService extends AbstractComponent implements
@Override @Override
public void close() { public void close() {
}
@Override
public void scriptRemoved(@Nullable CompiledScript compiledScript) {
// Nothing to do here // Nothing to do here
} }

View File

@ -142,11 +142,6 @@ public class PythonScriptEngineService extends AbstractComponent implements Scri
interp.cleanup(); interp.cleanup();
} }
@Override
public void scriptRemoved(@Nullable CompiledScript compiledScript) {
// Nothing to do
}
public class PythonExecutableScript implements ExecutableScript { public class PythonExecutableScript implements ExecutableScript {
private final PyCode code; private final PyCode code;

View File

@ -110,10 +110,6 @@ public class MockScriptEngine implements ScriptEngineService {
}; };
} }
@Override
public void scriptRemoved(@Nullable CompiledScript script) {
}
@Override @Override
public void close() throws IOException { public void close() throws IOException {
} }